blob: 7117242619f22e4b03cddbbaa650d069a94822a6 [file] [log] [blame]
mblighfa29a2a2008-05-16 22:48:09 +00001#!/usr/bin/python -u
mblighe8819cd2008-02-15 16:48:40 +00002
3import os, sys, re, subprocess, tempfile
mblighb090f142008-02-27 21:33:46 +00004from optparse import OptionParser
mbligh9b907d62008-05-13 17:56:24 +00005import common
mbligh8bcd23a2009-02-03 19:14:06 +00006import MySQLdb, MySQLdb.constants.ER
jamesren92afa562010-03-23 00:19:26 +00007from autotest_lib.client.common_lib import global_config, utils
showard0e73c852008-10-03 10:15:50 +00008from autotest_lib.database import database_connection
mblighe8819cd2008-02-15 16:48:40 +00009
10MIGRATE_TABLE = 'migrate_info'
showard50c0e712008-09-22 16:20:37 +000011
12_AUTODIR = os.path.join(os.path.dirname(__file__), '..')
13_MIGRATIONS_DIRS = {
14 'AUTOTEST_WEB' : os.path.join(_AUTODIR, 'frontend', 'migrations'),
15 'TKO' : os.path.join(_AUTODIR, 'tko', 'migrations'),
16}
17_DEFAULT_MIGRATIONS_DIR = 'migrations' # use CWD
mblighe8819cd2008-02-15 16:48:40 +000018
mblighe8819cd2008-02-15 16:48:40 +000019class Migration(object):
showardeca1f212009-05-13 20:28:12 +000020 _UP_ATTRIBUTES = ('migrate_up', 'UP_SQL')
21 _DOWN_ATTRIBUTES = ('migrate_down', 'DOWN_SQL')
22
23 def __init__(self, name, version, module):
24 self.name = name
25 self.version = version
26 self.module = module
27 self._check_attributes(self._UP_ATTRIBUTES)
28 self._check_attributes(self._DOWN_ATTRIBUTES)
29
30
31 @classmethod
32 def from_file(cls, filename):
33 version = int(filename[:3])
34 name = filename[:-3]
35 module = __import__(name, globals(), locals(), [])
36 return cls(name, version, module)
37
38
39 def _check_attributes(self, attributes):
40 method_name, sql_name = attributes
41 assert (hasattr(self.module, method_name) or
42 hasattr(self.module, sql_name))
43
44
45 def _execute_migration(self, attributes, manager):
46 method_name, sql_name = attributes
47 method = getattr(self.module, method_name, None)
48 if method:
49 assert callable(method)
50 method(manager)
51 else:
52 sql = getattr(self.module, sql_name)
53 assert isinstance(sql, basestring)
54 manager.execute_script(sql)
showarddecbe502008-03-28 16:31:10 +000055
56
jadmanski0afbb632008-06-06 21:10:57 +000057 def migrate_up(self, manager):
showardeca1f212009-05-13 20:28:12 +000058 self._execute_migration(self._UP_ATTRIBUTES, manager)
showarddecbe502008-03-28 16:31:10 +000059
60
jadmanski0afbb632008-06-06 21:10:57 +000061 def migrate_down(self, manager):
showardeca1f212009-05-13 20:28:12 +000062 self._execute_migration(self._DOWN_ATTRIBUTES, manager)
mblighe8819cd2008-02-15 16:48:40 +000063
64
65class MigrationManager(object):
jadmanski0afbb632008-06-06 21:10:57 +000066 connection = None
67 cursor = None
68 migrations_dir = None
mblighe8819cd2008-02-15 16:48:40 +000069
showard0e73c852008-10-03 10:15:50 +000070 def __init__(self, database_connection, migrations_dir=None, force=False):
71 self._database = database_connection
showard50c0e712008-09-22 16:20:37 +000072 self.force = force
showard12454c62010-01-15 19:15:14 +000073 # A boolean, this will only be set to True if this migration should be
74 # simulated rather than actually taken. For use with migrations that
75 # may make destructive queries
76 self.simulate = False
showard0e73c852008-10-03 10:15:50 +000077 self._set_migrations_dir(migrations_dir)
78
79
80 def _set_migrations_dir(self, migrations_dir=None):
jamesren92afa562010-03-23 00:19:26 +000081 config_section = self._config_section()
jadmanski0afbb632008-06-06 21:10:57 +000082 if migrations_dir is None:
showard50c0e712008-09-22 16:20:37 +000083 migrations_dir = os.path.abspath(
showard0e73c852008-10-03 10:15:50 +000084 _MIGRATIONS_DIRS.get(config_section, _DEFAULT_MIGRATIONS_DIR))
jadmanski0afbb632008-06-06 21:10:57 +000085 self.migrations_dir = migrations_dir
86 sys.path.append(migrations_dir)
showard0e73c852008-10-03 10:15:50 +000087 assert os.path.exists(migrations_dir), migrations_dir + " doesn't exist"
mblighe8819cd2008-02-15 16:48:40 +000088
89
jamesren92afa562010-03-23 00:19:26 +000090 def _config_section(self):
91 return self._database.global_config_section
92
93
showardeab66ce2009-12-23 00:03:56 +000094 def get_db_name(self):
showard0e73c852008-10-03 10:15:50 +000095 return self._database.get_database_info()['db_name']
mblighe8819cd2008-02-15 16:48:40 +000096
97
jadmanski0afbb632008-06-06 21:10:57 +000098 def execute(self, query, *parameters):
showard0e73c852008-10-03 10:15:50 +000099 return self._database.execute(query, parameters)
mblighe8819cd2008-02-15 16:48:40 +0000100
101
jadmanski0afbb632008-06-06 21:10:57 +0000102 def execute_script(self, script):
showardb1e51872008-10-07 11:08:18 +0000103 sql_statements = [statement.strip()
104 for statement in script.split(';')
105 if statement.strip()]
jadmanski0afbb632008-06-06 21:10:57 +0000106 for statement in sql_statements:
showardb1e51872008-10-07 11:08:18 +0000107 self.execute(statement)
mblighe8819cd2008-02-15 16:48:40 +0000108
109
jadmanski0afbb632008-06-06 21:10:57 +0000110 def check_migrate_table_exists(self):
111 try:
112 self.execute("SELECT * FROM %s" % MIGRATE_TABLE)
113 return True
showard0e73c852008-10-03 10:15:50 +0000114 except self._database.DatabaseError, exc:
115 # we can't check for more specifics due to differences between DB
116 # backends (we can't even check for a subclass of DatabaseError)
117 return False
mblighe8819cd2008-02-15 16:48:40 +0000118
119
jadmanski0afbb632008-06-06 21:10:57 +0000120 def create_migrate_table(self):
121 if not self.check_migrate_table_exists():
122 self.execute("CREATE TABLE %s (`version` integer)" %
123 MIGRATE_TABLE)
124 else:
125 self.execute("DELETE FROM %s" % MIGRATE_TABLE)
126 self.execute("INSERT INTO %s VALUES (0)" % MIGRATE_TABLE)
showard0e73c852008-10-03 10:15:50 +0000127 assert self._database.rowcount == 1
mblighe8819cd2008-02-15 16:48:40 +0000128
129
jadmanski0afbb632008-06-06 21:10:57 +0000130 def set_db_version(self, version):
131 assert isinstance(version, int)
132 self.execute("UPDATE %s SET version=%%s" % MIGRATE_TABLE,
133 version)
showard0e73c852008-10-03 10:15:50 +0000134 assert self._database.rowcount == 1
mblighe8819cd2008-02-15 16:48:40 +0000135
136
jadmanski0afbb632008-06-06 21:10:57 +0000137 def get_db_version(self):
138 if not self.check_migrate_table_exists():
139 return 0
showard0e73c852008-10-03 10:15:50 +0000140 rows = self.execute("SELECT * FROM %s" % MIGRATE_TABLE)
jadmanski0afbb632008-06-06 21:10:57 +0000141 if len(rows) == 0:
142 return 0
143 assert len(rows) == 1 and len(rows[0]) == 1
144 return rows[0][0]
mblighe8819cd2008-02-15 16:48:40 +0000145
146
jadmanski0afbb632008-06-06 21:10:57 +0000147 def get_migrations(self, minimum_version=None, maximum_version=None):
148 migrate_files = [filename for filename
149 in os.listdir(self.migrations_dir)
150 if re.match(r'^\d\d\d_.*\.py$', filename)]
151 migrate_files.sort()
showardeca1f212009-05-13 20:28:12 +0000152 migrations = [Migration.from_file(filename)
153 for filename in migrate_files]
jadmanski0afbb632008-06-06 21:10:57 +0000154 if minimum_version is not None:
155 migrations = [migration for migration in migrations
156 if migration.version >= minimum_version]
157 if maximum_version is not None:
158 migrations = [migration for migration in migrations
159 if migration.version <= maximum_version]
160 return migrations
mblighe8819cd2008-02-15 16:48:40 +0000161
162
jadmanski0afbb632008-06-06 21:10:57 +0000163 def do_migration(self, migration, migrate_up=True):
164 print 'Applying migration %s' % migration.name, # no newline
165 if migrate_up:
166 print 'up'
167 assert self.get_db_version() == migration.version - 1
168 migration.migrate_up(self)
169 new_version = migration.version
170 else:
171 print 'down'
172 assert self.get_db_version() == migration.version
173 migration.migrate_down(self)
174 new_version = migration.version - 1
175 self.set_db_version(new_version)
mblighe8819cd2008-02-15 16:48:40 +0000176
177
jadmanski0afbb632008-06-06 21:10:57 +0000178 def migrate_to_version(self, version):
179 current_version = self.get_db_version()
jamesren92afa562010-03-23 00:19:26 +0000180 if current_version == 0 and self._config_section() == 'AUTOTEST_WEB':
181 self._migrate_from_base()
182 current_version = self.get_db_version()
183
jadmanski0afbb632008-06-06 21:10:57 +0000184 if current_version < version:
185 lower, upper = current_version, version
186 migrate_up = True
187 else:
188 lower, upper = version, current_version
189 migrate_up = False
mblighe8819cd2008-02-15 16:48:40 +0000190
jadmanski0afbb632008-06-06 21:10:57 +0000191 migrations = self.get_migrations(lower + 1, upper)
192 if not migrate_up:
193 migrations.reverse()
194 for migration in migrations:
195 self.do_migration(migration, migrate_up)
mblighe8819cd2008-02-15 16:48:40 +0000196
jadmanski0afbb632008-06-06 21:10:57 +0000197 assert self.get_db_version() == version
198 print 'At version', version
mblighe8819cd2008-02-15 16:48:40 +0000199
200
jamesren92afa562010-03-23 00:19:26 +0000201 def _migrate_from_base(self):
202 self.confirm_initialization()
203
204 migration_script = utils.read_file(
205 os.path.join(os.path.dirname(__file__), 'schema_051.sql'))
jamesrenef692982010-04-05 18:14:07 +0000206 migration_script = migration_script % (
207 dict(username=self._database.get_database_info()['username']))
jamesren92afa562010-03-23 00:19:26 +0000208 self.execute_script(migration_script)
209
210 self.create_migrate_table()
211 self.set_db_version(51)
212
213
214 def confirm_initialization(self):
215 if not self.force:
216 response = raw_input(
217 'Your %s database does not appear to be initialized. Do you '
218 'want to recreate it (this will result in loss of any existing '
219 'data) (yes/No)? ' % self.get_db_name())
220 if response != 'yes':
221 raise Exception('User has chosen to abort migration')
222
223
jadmanski0afbb632008-06-06 21:10:57 +0000224 def get_latest_version(self):
225 migrations = self.get_migrations()
226 return migrations[-1].version
showardd2d4e2c2008-03-12 21:32:46 +0000227
228
jadmanski0afbb632008-06-06 21:10:57 +0000229 def migrate_to_latest(self):
230 latest_version = self.get_latest_version()
231 self.migrate_to_version(latest_version)
mblighe8819cd2008-02-15 16:48:40 +0000232
233
jadmanski0afbb632008-06-06 21:10:57 +0000234 def initialize_test_db(self):
showardeab66ce2009-12-23 00:03:56 +0000235 db_name = self.get_db_name()
showard0e73c852008-10-03 10:15:50 +0000236 test_db_name = 'test_' + db_name
jadmanski0afbb632008-06-06 21:10:57 +0000237 # first, connect to no DB so we can create a test DB
showard0e73c852008-10-03 10:15:50 +0000238 self._database.connect(db_name='')
jadmanski0afbb632008-06-06 21:10:57 +0000239 print 'Creating test DB', test_db_name
240 self.execute('CREATE DATABASE ' + test_db_name)
showard0e73c852008-10-03 10:15:50 +0000241 self._database.disconnect()
jadmanski0afbb632008-06-06 21:10:57 +0000242 # now connect to the test DB
showard0e73c852008-10-03 10:15:50 +0000243 self._database.connect(db_name=test_db_name)
mblighe8819cd2008-02-15 16:48:40 +0000244
245
jadmanski0afbb632008-06-06 21:10:57 +0000246 def remove_test_db(self):
247 print 'Removing test DB'
showardeab66ce2009-12-23 00:03:56 +0000248 self.execute('DROP DATABASE ' + self.get_db_name())
showard0e73c852008-10-03 10:15:50 +0000249 # reset connection back to real DB
250 self._database.disconnect()
251 self._database.connect()
mblighe8819cd2008-02-15 16:48:40 +0000252
253
jadmanski0afbb632008-06-06 21:10:57 +0000254 def get_mysql_args(self):
showard0e73c852008-10-03 10:15:50 +0000255 return ('-u %(username)s -p%(password)s -h %(host)s %(db_name)s' %
256 self._database.get_database_info())
mblighe8819cd2008-02-15 16:48:40 +0000257
258
jadmanski0afbb632008-06-06 21:10:57 +0000259 def migrate_to_version_or_latest(self, version):
260 if version is None:
261 self.migrate_to_latest()
262 else:
263 self.migrate_to_version(version)
mblighaa383b72008-03-12 20:11:56 +0000264
265
jadmanski0afbb632008-06-06 21:10:57 +0000266 def do_sync_db(self, version=None):
showardeab66ce2009-12-23 00:03:56 +0000267 print 'Migration starting for database', self.get_db_name()
jadmanski0afbb632008-06-06 21:10:57 +0000268 self.migrate_to_version_or_latest(version)
269 print 'Migration complete'
mblighe8819cd2008-02-15 16:48:40 +0000270
271
jadmanski0afbb632008-06-06 21:10:57 +0000272 def test_sync_db(self, version=None):
273 """\
274 Create a fresh DB and run all migrations on it.
275 """
276 self.initialize_test_db()
277 try:
showardeab66ce2009-12-23 00:03:56 +0000278 print 'Starting migration test on DB', self.get_db_name()
jadmanski0afbb632008-06-06 21:10:57 +0000279 self.migrate_to_version_or_latest(version)
280 # show schema to the user
281 os.system('mysqldump %s --no-data=true '
282 '--add-drop-table=false' %
283 self.get_mysql_args())
284 finally:
285 self.remove_test_db()
286 print 'Test finished successfully'
mblighe8819cd2008-02-15 16:48:40 +0000287
288
jadmanski0afbb632008-06-06 21:10:57 +0000289 def simulate_sync_db(self, version=None):
290 """\
291 Create a fresh DB, copy the existing DB to it, and then
292 try to synchronize it.
293 """
jadmanski0afbb632008-06-06 21:10:57 +0000294 db_version = self.get_db_version()
jadmanski0afbb632008-06-06 21:10:57 +0000295 # don't do anything if we're already at the latest version
296 if db_version == self.get_latest_version():
297 print 'Skipping simulation, already at latest version'
298 return
299 # get existing data
showard12454c62010-01-15 19:15:14 +0000300 self.initialize_and_fill_test_db()
301 try:
302 print 'Starting migration test on DB', self.get_db_name()
303 self.migrate_to_version_or_latest(version)
304 finally:
305 self.remove_test_db()
306 print 'Test finished successfully'
307
308
309 def initialize_and_fill_test_db(self):
jadmanski0afbb632008-06-06 21:10:57 +0000310 print 'Dumping existing data'
311 dump_fd, dump_file = tempfile.mkstemp('.migrate_dump')
jadmanski0afbb632008-06-06 21:10:57 +0000312 os.system('mysqldump %s >%s' %
313 (self.get_mysql_args(), dump_file))
314 # fill in test DB
315 self.initialize_test_db()
316 print 'Filling in test DB'
317 os.system('mysql %s <%s' % (self.get_mysql_args(), dump_file))
showard0e73c852008-10-03 10:15:50 +0000318 os.close(dump_fd)
jadmanski0afbb632008-06-06 21:10:57 +0000319 os.remove(dump_file)
mblighe8819cd2008-02-15 16:48:40 +0000320
321
mblighc2f24452008-03-31 16:46:13 +0000322USAGE = """\
323%s [options] sync|test|simulate|safesync [version]
324Options:
325 -d --database Which database to act on
326 -a --action Which action to perform"""\
327 % sys.argv[0]
mblighe8819cd2008-02-15 16:48:40 +0000328
329
330def main():
jadmanski0afbb632008-06-06 21:10:57 +0000331 parser = OptionParser()
332 parser.add_option("-d", "--database",
333 help="which database to act on",
jamesren92afa562010-03-23 00:19:26 +0000334 dest="database",
335 default="AUTOTEST_WEB")
jadmanski0afbb632008-06-06 21:10:57 +0000336 parser.add_option("-a", "--action", help="what action to perform",
337 dest="action")
showard50c0e712008-09-22 16:20:37 +0000338 parser.add_option("-f", "--force", help="don't ask for confirmation",
339 action="store_true")
showard049b0fd2009-05-29 18:39:07 +0000340 parser.add_option('--debug', help='print all DB queries',
341 action='store_true')
jadmanski0afbb632008-06-06 21:10:57 +0000342 (options, args) = parser.parse_args()
showard250d84d2010-01-12 21:59:48 +0000343 manager = get_migration_manager(db_name=options.database,
344 debug=options.debug, force=options.force)
jadmanski0afbb632008-06-06 21:10:57 +0000345
346 if len(args) > 0:
347 if len(args) > 1:
348 version = int(args[1])
349 else:
350 version = None
351 if args[0] == 'sync':
352 manager.do_sync_db(version)
353 elif args[0] == 'test':
showard12454c62010-01-15 19:15:14 +0000354 manager.simulate=True
jadmanski0afbb632008-06-06 21:10:57 +0000355 manager.test_sync_db(version)
356 elif args[0] == 'simulate':
showard12454c62010-01-15 19:15:14 +0000357 manager.simulate=True
jadmanski0afbb632008-06-06 21:10:57 +0000358 manager.simulate_sync_db(version)
359 elif args[0] == 'safesync':
360 print 'Simluating migration'
showard12454c62010-01-15 19:15:14 +0000361 manager.simulate=True
jadmanski0afbb632008-06-06 21:10:57 +0000362 manager.simulate_sync_db(version)
363 print 'Performing real migration'
showard12454c62010-01-15 19:15:14 +0000364 manager.simulate=False
jadmanski0afbb632008-06-06 21:10:57 +0000365 manager.do_sync_db(version)
366 else:
367 print USAGE
368 return
369
370 print USAGE
mblighe8819cd2008-02-15 16:48:40 +0000371
372
showard250d84d2010-01-12 21:59:48 +0000373def get_migration_manager(db_name, debug, force):
374 database = database_connection.DatabaseConnection(db_name)
375 database.debug = debug
376 database.reconnect_enabled = False
377 database.connect()
378 return MigrationManager(database, force=force)
379
380
mblighe8819cd2008-02-15 16:48:40 +0000381if __name__ == '__main__':
jadmanski0afbb632008-06-06 21:10:57 +0000382 main()