blob: 07a286a46cac843bf69f9dfdf36cf973e708bf6c [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
mbligh9b907d62008-05-13 17:56:24 +00007from autotest_lib.client.common_lib import global_config
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
showard0e73c852008-10-03 10:15:50 +000073 self._set_migrations_dir(migrations_dir)
74
75
76 def _set_migrations_dir(self, migrations_dir=None):
77 config_section = self._database.global_config_section
jadmanski0afbb632008-06-06 21:10:57 +000078 if migrations_dir is None:
showard50c0e712008-09-22 16:20:37 +000079 migrations_dir = os.path.abspath(
showard0e73c852008-10-03 10:15:50 +000080 _MIGRATIONS_DIRS.get(config_section, _DEFAULT_MIGRATIONS_DIR))
jadmanski0afbb632008-06-06 21:10:57 +000081 self.migrations_dir = migrations_dir
82 sys.path.append(migrations_dir)
showard0e73c852008-10-03 10:15:50 +000083 assert os.path.exists(migrations_dir), migrations_dir + " doesn't exist"
mblighe8819cd2008-02-15 16:48:40 +000084
85
showardeab66ce2009-12-23 00:03:56 +000086 def get_db_name(self):
showard0e73c852008-10-03 10:15:50 +000087 return self._database.get_database_info()['db_name']
mblighe8819cd2008-02-15 16:48:40 +000088
89
jadmanski0afbb632008-06-06 21:10:57 +000090 def execute(self, query, *parameters):
showard0e73c852008-10-03 10:15:50 +000091 return self._database.execute(query, parameters)
mblighe8819cd2008-02-15 16:48:40 +000092
93
jadmanski0afbb632008-06-06 21:10:57 +000094 def execute_script(self, script):
showardb1e51872008-10-07 11:08:18 +000095 sql_statements = [statement.strip()
96 for statement in script.split(';')
97 if statement.strip()]
jadmanski0afbb632008-06-06 21:10:57 +000098 for statement in sql_statements:
showardb1e51872008-10-07 11:08:18 +000099 self.execute(statement)
mblighe8819cd2008-02-15 16:48:40 +0000100
101
jadmanski0afbb632008-06-06 21:10:57 +0000102 def check_migrate_table_exists(self):
103 try:
104 self.execute("SELECT * FROM %s" % MIGRATE_TABLE)
105 return True
showard0e73c852008-10-03 10:15:50 +0000106 except self._database.DatabaseError, exc:
107 # we can't check for more specifics due to differences between DB
108 # backends (we can't even check for a subclass of DatabaseError)
109 return False
mblighe8819cd2008-02-15 16:48:40 +0000110
111
jadmanski0afbb632008-06-06 21:10:57 +0000112 def create_migrate_table(self):
113 if not self.check_migrate_table_exists():
114 self.execute("CREATE TABLE %s (`version` integer)" %
115 MIGRATE_TABLE)
116 else:
117 self.execute("DELETE FROM %s" % MIGRATE_TABLE)
118 self.execute("INSERT INTO %s VALUES (0)" % MIGRATE_TABLE)
showard0e73c852008-10-03 10:15:50 +0000119 assert self._database.rowcount == 1
mblighe8819cd2008-02-15 16:48:40 +0000120
121
jadmanski0afbb632008-06-06 21:10:57 +0000122 def set_db_version(self, version):
123 assert isinstance(version, int)
124 self.execute("UPDATE %s SET version=%%s" % MIGRATE_TABLE,
125 version)
showard0e73c852008-10-03 10:15:50 +0000126 assert self._database.rowcount == 1
mblighe8819cd2008-02-15 16:48:40 +0000127
128
jadmanski0afbb632008-06-06 21:10:57 +0000129 def get_db_version(self):
130 if not self.check_migrate_table_exists():
131 return 0
showard0e73c852008-10-03 10:15:50 +0000132 rows = self.execute("SELECT * FROM %s" % MIGRATE_TABLE)
jadmanski0afbb632008-06-06 21:10:57 +0000133 if len(rows) == 0:
134 return 0
135 assert len(rows) == 1 and len(rows[0]) == 1
136 return rows[0][0]
mblighe8819cd2008-02-15 16:48:40 +0000137
138
jadmanski0afbb632008-06-06 21:10:57 +0000139 def get_migrations(self, minimum_version=None, maximum_version=None):
140 migrate_files = [filename for filename
141 in os.listdir(self.migrations_dir)
142 if re.match(r'^\d\d\d_.*\.py$', filename)]
143 migrate_files.sort()
showardeca1f212009-05-13 20:28:12 +0000144 migrations = [Migration.from_file(filename)
145 for filename in migrate_files]
jadmanski0afbb632008-06-06 21:10:57 +0000146 if minimum_version is not None:
147 migrations = [migration for migration in migrations
148 if migration.version >= minimum_version]
149 if maximum_version is not None:
150 migrations = [migration for migration in migrations
151 if migration.version <= maximum_version]
152 return migrations
mblighe8819cd2008-02-15 16:48:40 +0000153
154
jadmanski0afbb632008-06-06 21:10:57 +0000155 def do_migration(self, migration, migrate_up=True):
156 print 'Applying migration %s' % migration.name, # no newline
157 if migrate_up:
158 print 'up'
159 assert self.get_db_version() == migration.version - 1
160 migration.migrate_up(self)
161 new_version = migration.version
162 else:
163 print 'down'
164 assert self.get_db_version() == migration.version
165 migration.migrate_down(self)
166 new_version = migration.version - 1
167 self.set_db_version(new_version)
mblighe8819cd2008-02-15 16:48:40 +0000168
169
jadmanski0afbb632008-06-06 21:10:57 +0000170 def migrate_to_version(self, version):
171 current_version = self.get_db_version()
172 if current_version < version:
173 lower, upper = current_version, version
174 migrate_up = True
175 else:
176 lower, upper = version, current_version
177 migrate_up = False
mblighe8819cd2008-02-15 16:48:40 +0000178
jadmanski0afbb632008-06-06 21:10:57 +0000179 migrations = self.get_migrations(lower + 1, upper)
180 if not migrate_up:
181 migrations.reverse()
182 for migration in migrations:
183 self.do_migration(migration, migrate_up)
mblighe8819cd2008-02-15 16:48:40 +0000184
jadmanski0afbb632008-06-06 21:10:57 +0000185 assert self.get_db_version() == version
186 print 'At version', version
mblighe8819cd2008-02-15 16:48:40 +0000187
188
jadmanski0afbb632008-06-06 21:10:57 +0000189 def get_latest_version(self):
190 migrations = self.get_migrations()
191 return migrations[-1].version
showardd2d4e2c2008-03-12 21:32:46 +0000192
193
jadmanski0afbb632008-06-06 21:10:57 +0000194 def migrate_to_latest(self):
195 latest_version = self.get_latest_version()
196 self.migrate_to_version(latest_version)
mblighe8819cd2008-02-15 16:48:40 +0000197
198
jadmanski0afbb632008-06-06 21:10:57 +0000199 def initialize_test_db(self):
showardeab66ce2009-12-23 00:03:56 +0000200 db_name = self.get_db_name()
showard0e73c852008-10-03 10:15:50 +0000201 test_db_name = 'test_' + db_name
jadmanski0afbb632008-06-06 21:10:57 +0000202 # first, connect to no DB so we can create a test DB
showard0e73c852008-10-03 10:15:50 +0000203 self._database.connect(db_name='')
jadmanski0afbb632008-06-06 21:10:57 +0000204 print 'Creating test DB', test_db_name
205 self.execute('CREATE DATABASE ' + test_db_name)
showard0e73c852008-10-03 10:15:50 +0000206 self._database.disconnect()
jadmanski0afbb632008-06-06 21:10:57 +0000207 # now connect to the test DB
showard0e73c852008-10-03 10:15:50 +0000208 self._database.connect(db_name=test_db_name)
mblighe8819cd2008-02-15 16:48:40 +0000209
210
jadmanski0afbb632008-06-06 21:10:57 +0000211 def remove_test_db(self):
212 print 'Removing test DB'
showardeab66ce2009-12-23 00:03:56 +0000213 self.execute('DROP DATABASE ' + self.get_db_name())
showard0e73c852008-10-03 10:15:50 +0000214 # reset connection back to real DB
215 self._database.disconnect()
216 self._database.connect()
mblighe8819cd2008-02-15 16:48:40 +0000217
218
jadmanski0afbb632008-06-06 21:10:57 +0000219 def get_mysql_args(self):
showard0e73c852008-10-03 10:15:50 +0000220 return ('-u %(username)s -p%(password)s -h %(host)s %(db_name)s' %
221 self._database.get_database_info())
mblighe8819cd2008-02-15 16:48:40 +0000222
223
jadmanski0afbb632008-06-06 21:10:57 +0000224 def migrate_to_version_or_latest(self, version):
225 if version is None:
226 self.migrate_to_latest()
227 else:
228 self.migrate_to_version(version)
mblighaa383b72008-03-12 20:11:56 +0000229
230
jadmanski0afbb632008-06-06 21:10:57 +0000231 def do_sync_db(self, version=None):
showardeab66ce2009-12-23 00:03:56 +0000232 print 'Migration starting for database', self.get_db_name()
jadmanski0afbb632008-06-06 21:10:57 +0000233 self.migrate_to_version_or_latest(version)
234 print 'Migration complete'
mblighe8819cd2008-02-15 16:48:40 +0000235
236
jadmanski0afbb632008-06-06 21:10:57 +0000237 def test_sync_db(self, version=None):
238 """\
239 Create a fresh DB and run all migrations on it.
240 """
241 self.initialize_test_db()
242 try:
showardeab66ce2009-12-23 00:03:56 +0000243 print 'Starting migration test on DB', self.get_db_name()
jadmanski0afbb632008-06-06 21:10:57 +0000244 self.migrate_to_version_or_latest(version)
245 # show schema to the user
246 os.system('mysqldump %s --no-data=true '
247 '--add-drop-table=false' %
248 self.get_mysql_args())
249 finally:
250 self.remove_test_db()
251 print 'Test finished successfully'
mblighe8819cd2008-02-15 16:48:40 +0000252
253
jadmanski0afbb632008-06-06 21:10:57 +0000254 def simulate_sync_db(self, version=None):
255 """\
256 Create a fresh DB, copy the existing DB to it, and then
257 try to synchronize it.
258 """
jadmanski0afbb632008-06-06 21:10:57 +0000259 db_version = self.get_db_version()
jadmanski0afbb632008-06-06 21:10:57 +0000260 # don't do anything if we're already at the latest version
261 if db_version == self.get_latest_version():
262 print 'Skipping simulation, already at latest version'
263 return
264 # get existing data
jadmanski0afbb632008-06-06 21:10:57 +0000265 print 'Dumping existing data'
266 dump_fd, dump_file = tempfile.mkstemp('.migrate_dump')
jadmanski0afbb632008-06-06 21:10:57 +0000267 os.system('mysqldump %s >%s' %
268 (self.get_mysql_args(), dump_file))
269 # fill in test DB
270 self.initialize_test_db()
271 print 'Filling in test DB'
272 os.system('mysql %s <%s' % (self.get_mysql_args(), dump_file))
showard0e73c852008-10-03 10:15:50 +0000273 os.close(dump_fd)
jadmanski0afbb632008-06-06 21:10:57 +0000274 os.remove(dump_file)
275 try:
showardeab66ce2009-12-23 00:03:56 +0000276 print 'Starting migration test on DB', self.get_db_name()
jadmanski0afbb632008-06-06 21:10:57 +0000277 self.migrate_to_version_or_latest(version)
278 finally:
279 self.remove_test_db()
280 print 'Test finished successfully'
mblighe8819cd2008-02-15 16:48:40 +0000281
282
mblighc2f24452008-03-31 16:46:13 +0000283USAGE = """\
284%s [options] sync|test|simulate|safesync [version]
285Options:
286 -d --database Which database to act on
287 -a --action Which action to perform"""\
288 % sys.argv[0]
mblighe8819cd2008-02-15 16:48:40 +0000289
290
291def main():
jadmanski0afbb632008-06-06 21:10:57 +0000292 parser = OptionParser()
293 parser.add_option("-d", "--database",
294 help="which database to act on",
295 dest="database")
296 parser.add_option("-a", "--action", help="what action to perform",
297 dest="action")
showard50c0e712008-09-22 16:20:37 +0000298 parser.add_option("-f", "--force", help="don't ask for confirmation",
299 action="store_true")
showard049b0fd2009-05-29 18:39:07 +0000300 parser.add_option('--debug', help='print all DB queries',
301 action='store_true')
jadmanski0afbb632008-06-06 21:10:57 +0000302 (options, args) = parser.parse_args()
showard250d84d2010-01-12 21:59:48 +0000303 manager = get_migration_manager(db_name=options.database,
304 debug=options.debug, force=options.force)
jadmanski0afbb632008-06-06 21:10:57 +0000305
306 if len(args) > 0:
307 if len(args) > 1:
308 version = int(args[1])
309 else:
310 version = None
311 if args[0] == 'sync':
312 manager.do_sync_db(version)
313 elif args[0] == 'test':
314 manager.test_sync_db(version)
315 elif args[0] == 'simulate':
316 manager.simulate_sync_db(version)
317 elif args[0] == 'safesync':
318 print 'Simluating migration'
319 manager.simulate_sync_db(version)
320 print 'Performing real migration'
321 manager.do_sync_db(version)
322 else:
323 print USAGE
324 return
325
326 print USAGE
mblighe8819cd2008-02-15 16:48:40 +0000327
328
showard250d84d2010-01-12 21:59:48 +0000329def get_migration_manager(db_name, debug, force):
330 database = database_connection.DatabaseConnection(db_name)
331 database.debug = debug
332 database.reconnect_enabled = False
333 database.connect()
334 return MigrationManager(database, force=force)
335
336
mblighe8819cd2008-02-15 16:48:40 +0000337if __name__ == '__main__':
jadmanski0afbb632008-06-06 21:10:57 +0000338 main()