20
20
from tempfile import NamedTemporaryFile
21
21
from textwrap import dedent
23
from bzrlib.branch import Branch
24
from bzrlib.errors import NotBranchError
23
26
from canonical.launchpad.scripts import db_options, logger_options, logger
24
from canonical.database.sqlbase import connect, ISOLATION_LEVEL_AUTOCOMMIT
27
from canonical.database.sqlbase import (
29
ISOLATION_LEVEL_AUTOCOMMIT,
25
32
from canonical.database.postgresql import fqn
26
33
import replication.helpers
90
97
AND LaunchpadDatabaseRevision.start_time <> prev_end_time;
92
99
UPDATE LaunchpadDatabaseRevision
93
SET start_time=_start_time.start_time
101
start_time=_start_time.start_time,
96
107
LaunchpadDatabaseRevision.start_time
97
108
= transaction_timestamp() AT TIME ZONE 'UTC';
110
START_UPDATE_LOG_SQL = dedent("""\
111
INSERT INTO LaunchpadDatabaseUpdateLog (
112
start_time, end_time, branch_nick, revno, revid)
113
VALUES (transaction_timestamp() AT TIME ZONE 'UTC', NULL, %s, %s, %s);
115
FINISH_UPDATE_LOG_SQL = dedent("""\
116
UPDATE LaunchpadDatabaseUpdateLog
117
SET end_time = statement_timestamp() AT TIME ZONE 'UTC'
118
WHERE start_time = transaction_timestamp() AT TIME ZONE 'UTC';
101
122
def to_seconds(td):
149
170
def apply_patches_normal(con):
150
171
"""Update a non replicated database."""
172
# On dev environments, until we create a fresh database baseline the
173
# LaunchpadDatabaseUpdateLog tables does not exist at this point (it
174
# will be created later via database patch). Don't try to update
175
# LaunchpadDatabaseUpdateLog if it does not exist.
179
SELECT TRUE FROM information_schema.tables
181
table_schema='public'
182
AND table_name='launchpaddatabaseupdatelog')
184
updatelog_exists = cur.fetchone()[0]
186
# Add a record to LaunchpadDatabaseUpdateLog that we are starting
189
cur.execute(START_UPDATE_LOG_SQL % sqlvalues(*get_bzr_details()))
151
191
# trusted.sql contains all our stored procedures, which may
152
192
# be required for patches to apply correctly so must be run first.
153
193
apply_other(con, 'trusted.sql')
155
195
# Prepare to repair patch timestamps if necessary.
156
con.cursor().execute(FIX_PATCH_TIMES_PRE_SQL)
196
cur.execute(FIX_PATCH_TIMES_PRE_SQL)
158
198
# Apply the patches
159
199
patches = get_patchlist(con)
161
201
apply_patch(con, major, minor, patch, patch_file)
163
203
# Repair patch timestamps if necessary.
164
con.cursor().execute(FIX_PATCH_TIMES_POST_SQL)
205
FIX_PATCH_TIMES_POST_SQL % sqlvalues(*get_bzr_details()))
166
207
# Update comments.
167
208
apply_comments(con)
210
# Update the LaunchpadDatabaseUpdateLog record, stating the
213
cur.execute(FINISH_UPDATE_LOG_SQL)
170
216
def apply_patches_replicated():
171
217
"""Update a Slony-I cluster."""
181
227
log.info("Waiting for cluster to sync, pre-update.")
182
228
replication.helpers.sync(timeout=600)
230
# Slonik script we are generating.
184
231
outf = StringIO()
186
233
# Start a transaction block.
187
234
print >> outf, "try {"
192
if os.path.isabs(script):
195
full_path = os.path.abspath(os.path.join(SCHEMA_DIR, script))
196
assert os.path.exists(full_path), "%s doesn't exist." % full_path
197
sql_to_run.append(full_path)
199
# We are going to generate some temporary files using
200
# NamedTempoararyFile. Store them here so we can control when
201
# they get closed and cleaned up.
236
# All the SQL we need to run, combined into one file. This minimizes
237
# Slony-I syncs and downtime.
238
combined_sql = NamedTemporaryFile(prefix='dbupdate', suffix='.sql')
243
assert sql.endswith(';'), "SQL not terminated with ';': %s" % sql
244
print >> combined_sql, sql
245
# Flush or we might lose statements from buffering.
248
# Add a LaunchpadDatabaseUpdateLog record that we are starting patch
250
add_sql(START_UPDATE_LOG_SQL % sqlvalues(*get_bzr_details()))
204
252
# Apply trusted.sql
205
run_sql('trusted.sql')
207
# We are going to generate some temporary files using
208
# NamedTempoararyFile. Store them here so we can control when
209
# they get closed and cleaned up.
212
# Apply DB patches as one big hunk.
213
combined_script = NamedTemporaryFile(prefix='patch', suffix='.sql')
214
temporary_files.append(combined_script)
253
add_sql(open(os.path.join(SCHEMA_DIR, 'trusted.sql'), 'r').read())
216
255
# Prepare to repair the start timestamps in
217
256
# LaunchpadDatabaseRevision.
218
print >> combined_script, FIX_PATCH_TIMES_PRE_SQL
257
add_sql(FIX_PATCH_TIMES_PRE_SQL)
220
259
patches = get_patchlist(con)
221
260
for (major, minor, patch), patch_file in patches:
222
print >> combined_script, open(patch_file, 'r').read()
261
add_sql(open(patch_file, 'r').read())
224
263
# Trigger a failure if the patch neglected to update
225
264
# LaunchpadDatabaseRevision.
226
print >> combined_script, (
227
266
"SELECT assert_patch_applied(%d, %d, %d);"
228
267
% (major, minor, patch))
230
269
# Fix the start timestamps in LaunchpadDatabaseRevision.
231
print >> combined_script, FIX_PATCH_TIMES_POST_SQL
233
combined_script.flush()
234
run_sql(combined_script.name)
236
# Now combine all the written SQL (probably trusted.sql and
237
# patch*.sql) into one big file, which we execute with a single
238
# slonik execute_script statement to avoid multiple syncs.
239
single = NamedTemporaryFile(prefix='single', suffix='.sql')
240
for path in sql_to_run:
241
print >> single, open(path, 'r').read()
270
add_sql(FIX_PATCH_TIMES_POST_SQL % sqlvalues(*get_bzr_details()))
245
272
print >> outf, dedent("""\
247
274
set id = @lpmain_set, event node = @master_node,
277
""" % combined_sql.name)
252
279
# Close transaction block and abort on error.
253
280
print >> outf, dedent("""\
598
620
apply_other(con, 'comments.sql')
623
_bzr_details_cache = None
626
def get_bzr_details():
627
"""Return (branch_nick, revno, revision_id) of this Bazaar branch.
629
Returns (None, None, None) if the tree this code is running from
630
is not a Bazaar branch.
632
global _bzr_details_cache
633
if _bzr_details_cache is None:
635
branch = Branch.open_containing(SCHEMA_DIR)[0]
636
revno, revision_id = branch.last_revision_info()
637
branch_nick = branch.get_config().get_nickname()
638
except NotBranchError:
639
log.warning("Not a Bazaar branch - branch details unavailable")
640
revision_id, revno, branch_nick = None, None, None
641
_bzr_details_cache = (branch_nick, revno, revision_id)
642
return _bzr_details_cache
601
645
if __name__ == '__main__':
602
646
parser = OptionParser()
603
647
db_options(parser)