blob: 0d57c8bf847d771cbed273dd4f700e6b45fd8f36 [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
4import MySQLdb, MySQLdb.constants.ER
mblighb090f142008-02-27 21:33:46 +00005from optparse import OptionParser
mbligh9b907d62008-05-13 17:56:24 +00006import common
7from 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):
jadmanski0afbb632008-06-06 21:10:57 +000020 def __init__(self, filename):
21 self.version = int(filename[:3])
22 self.name = filename[:-3]
23 self.module = __import__(self.name, globals(), locals(), [])
24 assert hasattr(self.module, 'migrate_up')
25 assert hasattr(self.module, 'migrate_down')
showarddecbe502008-03-28 16:31:10 +000026
27
jadmanski0afbb632008-06-06 21:10:57 +000028 def migrate_up(self, manager):
29 self.module.migrate_up(manager)
showarddecbe502008-03-28 16:31:10 +000030
31
jadmanski0afbb632008-06-06 21:10:57 +000032 def migrate_down(self, manager):
33 self.module.migrate_down(manager)
mblighe8819cd2008-02-15 16:48:40 +000034
35
36class MigrationManager(object):
jadmanski0afbb632008-06-06 21:10:57 +000037 connection = None
38 cursor = None
39 migrations_dir = None
mblighe8819cd2008-02-15 16:48:40 +000040
showard0e73c852008-10-03 10:15:50 +000041 def __init__(self, database_connection, migrations_dir=None, force=False):
42 self._database = database_connection
showard50c0e712008-09-22 16:20:37 +000043 self.force = force
showard0e73c852008-10-03 10:15:50 +000044 self._set_migrations_dir(migrations_dir)
45
46
47 def _set_migrations_dir(self, migrations_dir=None):
48 config_section = self._database.global_config_section
jadmanski0afbb632008-06-06 21:10:57 +000049 if migrations_dir is None:
showard50c0e712008-09-22 16:20:37 +000050 migrations_dir = os.path.abspath(
showard0e73c852008-10-03 10:15:50 +000051 _MIGRATIONS_DIRS.get(config_section, _DEFAULT_MIGRATIONS_DIR))
jadmanski0afbb632008-06-06 21:10:57 +000052 self.migrations_dir = migrations_dir
53 sys.path.append(migrations_dir)
showard0e73c852008-10-03 10:15:50 +000054 assert os.path.exists(migrations_dir), migrations_dir + " doesn't exist"
mblighe8819cd2008-02-15 16:48:40 +000055
56
showard0e73c852008-10-03 10:15:50 +000057 def _get_db_name(self):
58 return self._database.get_database_info()['db_name']
mblighe8819cd2008-02-15 16:48:40 +000059
60
jadmanski0afbb632008-06-06 21:10:57 +000061 def execute(self, query, *parameters):
showard0e73c852008-10-03 10:15:50 +000062 return self._database.execute(query, parameters)
mblighe8819cd2008-02-15 16:48:40 +000063
64
jadmanski0afbb632008-06-06 21:10:57 +000065 def execute_script(self, script):
showardb1e51872008-10-07 11:08:18 +000066 sql_statements = [statement.strip()
67 for statement in script.split(';')
68 if statement.strip()]
jadmanski0afbb632008-06-06 21:10:57 +000069 for statement in sql_statements:
showardb1e51872008-10-07 11:08:18 +000070 self.execute(statement)
mblighe8819cd2008-02-15 16:48:40 +000071
72
jadmanski0afbb632008-06-06 21:10:57 +000073 def check_migrate_table_exists(self):
74 try:
75 self.execute("SELECT * FROM %s" % MIGRATE_TABLE)
76 return True
showard0e73c852008-10-03 10:15:50 +000077 except self._database.DatabaseError, exc:
78 # we can't check for more specifics due to differences between DB
79 # backends (we can't even check for a subclass of DatabaseError)
80 return False
mblighe8819cd2008-02-15 16:48:40 +000081
82
jadmanski0afbb632008-06-06 21:10:57 +000083 def create_migrate_table(self):
84 if not self.check_migrate_table_exists():
85 self.execute("CREATE TABLE %s (`version` integer)" %
86 MIGRATE_TABLE)
87 else:
88 self.execute("DELETE FROM %s" % MIGRATE_TABLE)
89 self.execute("INSERT INTO %s VALUES (0)" % MIGRATE_TABLE)
showard0e73c852008-10-03 10:15:50 +000090 assert self._database.rowcount == 1
mblighe8819cd2008-02-15 16:48:40 +000091
92
jadmanski0afbb632008-06-06 21:10:57 +000093 def set_db_version(self, version):
94 assert isinstance(version, int)
95 self.execute("UPDATE %s SET version=%%s" % MIGRATE_TABLE,
96 version)
showard0e73c852008-10-03 10:15:50 +000097 assert self._database.rowcount == 1
mblighe8819cd2008-02-15 16:48:40 +000098
99
jadmanski0afbb632008-06-06 21:10:57 +0000100 def get_db_version(self):
101 if not self.check_migrate_table_exists():
102 return 0
showard0e73c852008-10-03 10:15:50 +0000103 rows = self.execute("SELECT * FROM %s" % MIGRATE_TABLE)
jadmanski0afbb632008-06-06 21:10:57 +0000104 if len(rows) == 0:
105 return 0
106 assert len(rows) == 1 and len(rows[0]) == 1
107 return rows[0][0]
mblighe8819cd2008-02-15 16:48:40 +0000108
109
jadmanski0afbb632008-06-06 21:10:57 +0000110 def get_migrations(self, minimum_version=None, maximum_version=None):
111 migrate_files = [filename for filename
112 in os.listdir(self.migrations_dir)
113 if re.match(r'^\d\d\d_.*\.py$', filename)]
114 migrate_files.sort()
115 migrations = [Migration(filename) for filename in migrate_files]
116 if minimum_version is not None:
117 migrations = [migration for migration in migrations
118 if migration.version >= minimum_version]
119 if maximum_version is not None:
120 migrations = [migration for migration in migrations
121 if migration.version <= maximum_version]
122 return migrations
mblighe8819cd2008-02-15 16:48:40 +0000123
124
jadmanski0afbb632008-06-06 21:10:57 +0000125 def do_migration(self, migration, migrate_up=True):
126 print 'Applying migration %s' % migration.name, # no newline
127 if migrate_up:
128 print 'up'
129 assert self.get_db_version() == migration.version - 1
130 migration.migrate_up(self)
131 new_version = migration.version
132 else:
133 print 'down'
134 assert self.get_db_version() == migration.version
135 migration.migrate_down(self)
136 new_version = migration.version - 1
137 self.set_db_version(new_version)
mblighe8819cd2008-02-15 16:48:40 +0000138
139
jadmanski0afbb632008-06-06 21:10:57 +0000140 def migrate_to_version(self, version):
141 current_version = self.get_db_version()
142 if current_version < version:
143 lower, upper = current_version, version
144 migrate_up = True
145 else:
146 lower, upper = version, current_version
147 migrate_up = False
mblighe8819cd2008-02-15 16:48:40 +0000148
jadmanski0afbb632008-06-06 21:10:57 +0000149 migrations = self.get_migrations(lower + 1, upper)
150 if not migrate_up:
151 migrations.reverse()
152 for migration in migrations:
153 self.do_migration(migration, migrate_up)
mblighe8819cd2008-02-15 16:48:40 +0000154
jadmanski0afbb632008-06-06 21:10:57 +0000155 assert self.get_db_version() == version
156 print 'At version', version
mblighe8819cd2008-02-15 16:48:40 +0000157
158
jadmanski0afbb632008-06-06 21:10:57 +0000159 def get_latest_version(self):
160 migrations = self.get_migrations()
161 return migrations[-1].version
showardd2d4e2c2008-03-12 21:32:46 +0000162
163
jadmanski0afbb632008-06-06 21:10:57 +0000164 def migrate_to_latest(self):
165 latest_version = self.get_latest_version()
166 self.migrate_to_version(latest_version)
mblighe8819cd2008-02-15 16:48:40 +0000167
168
jadmanski0afbb632008-06-06 21:10:57 +0000169 def initialize_test_db(self):
showard0e73c852008-10-03 10:15:50 +0000170 db_name = self._get_db_name()
171 test_db_name = 'test_' + db_name
jadmanski0afbb632008-06-06 21:10:57 +0000172 # first, connect to no DB so we can create a test DB
showard0e73c852008-10-03 10:15:50 +0000173 self._database.connect(db_name='')
jadmanski0afbb632008-06-06 21:10:57 +0000174 print 'Creating test DB', test_db_name
175 self.execute('CREATE DATABASE ' + test_db_name)
showard0e73c852008-10-03 10:15:50 +0000176 self._database.disconnect()
jadmanski0afbb632008-06-06 21:10:57 +0000177 # now connect to the test DB
showard0e73c852008-10-03 10:15:50 +0000178 self._database.connect(db_name=test_db_name)
mblighe8819cd2008-02-15 16:48:40 +0000179
180
jadmanski0afbb632008-06-06 21:10:57 +0000181 def remove_test_db(self):
182 print 'Removing test DB'
showard0e73c852008-10-03 10:15:50 +0000183 self.execute('DROP DATABASE ' + self._get_db_name())
184 # reset connection back to real DB
185 self._database.disconnect()
186 self._database.connect()
mblighe8819cd2008-02-15 16:48:40 +0000187
188
jadmanski0afbb632008-06-06 21:10:57 +0000189 def get_mysql_args(self):
showard0e73c852008-10-03 10:15:50 +0000190 return ('-u %(username)s -p%(password)s -h %(host)s %(db_name)s' %
191 self._database.get_database_info())
mblighe8819cd2008-02-15 16:48:40 +0000192
193
jadmanski0afbb632008-06-06 21:10:57 +0000194 def migrate_to_version_or_latest(self, version):
195 if version is None:
196 self.migrate_to_latest()
197 else:
198 self.migrate_to_version(version)
mblighaa383b72008-03-12 20:11:56 +0000199
200
jadmanski0afbb632008-06-06 21:10:57 +0000201 def do_sync_db(self, version=None):
showard0e73c852008-10-03 10:15:50 +0000202 print 'Migration starting for database', self._get_db_name()
jadmanski0afbb632008-06-06 21:10:57 +0000203 self.migrate_to_version_or_latest(version)
204 print 'Migration complete'
mblighe8819cd2008-02-15 16:48:40 +0000205
206
jadmanski0afbb632008-06-06 21:10:57 +0000207 def test_sync_db(self, version=None):
208 """\
209 Create a fresh DB and run all migrations on it.
210 """
211 self.initialize_test_db()
212 try:
showard0e73c852008-10-03 10:15:50 +0000213 print 'Starting migration test on DB', self._get_db_name()
jadmanski0afbb632008-06-06 21:10:57 +0000214 self.migrate_to_version_or_latest(version)
215 # show schema to the user
216 os.system('mysqldump %s --no-data=true '
217 '--add-drop-table=false' %
218 self.get_mysql_args())
219 finally:
220 self.remove_test_db()
221 print 'Test finished successfully'
mblighe8819cd2008-02-15 16:48:40 +0000222
223
jadmanski0afbb632008-06-06 21:10:57 +0000224 def simulate_sync_db(self, version=None):
225 """\
226 Create a fresh DB, copy the existing DB to it, and then
227 try to synchronize it.
228 """
jadmanski0afbb632008-06-06 21:10:57 +0000229 db_version = self.get_db_version()
jadmanski0afbb632008-06-06 21:10:57 +0000230 # don't do anything if we're already at the latest version
231 if db_version == self.get_latest_version():
232 print 'Skipping simulation, already at latest version'
233 return
234 # get existing data
jadmanski0afbb632008-06-06 21:10:57 +0000235 print 'Dumping existing data'
236 dump_fd, dump_file = tempfile.mkstemp('.migrate_dump')
jadmanski0afbb632008-06-06 21:10:57 +0000237 os.system('mysqldump %s >%s' %
238 (self.get_mysql_args(), dump_file))
239 # fill in test DB
240 self.initialize_test_db()
241 print 'Filling in test DB'
242 os.system('mysql %s <%s' % (self.get_mysql_args(), dump_file))
showard0e73c852008-10-03 10:15:50 +0000243 os.close(dump_fd)
jadmanski0afbb632008-06-06 21:10:57 +0000244 os.remove(dump_file)
245 try:
showard0e73c852008-10-03 10:15:50 +0000246 print 'Starting migration test on DB', self._get_db_name()
jadmanski0afbb632008-06-06 21:10:57 +0000247 self.migrate_to_version_or_latest(version)
248 finally:
249 self.remove_test_db()
250 print 'Test finished successfully'
mblighe8819cd2008-02-15 16:48:40 +0000251
252
mblighc2f24452008-03-31 16:46:13 +0000253USAGE = """\
254%s [options] sync|test|simulate|safesync [version]
255Options:
256 -d --database Which database to act on
257 -a --action Which action to perform"""\
258 % sys.argv[0]
mblighe8819cd2008-02-15 16:48:40 +0000259
260
261def main():
jadmanski0afbb632008-06-06 21:10:57 +0000262 parser = OptionParser()
263 parser.add_option("-d", "--database",
264 help="which database to act on",
265 dest="database")
266 parser.add_option("-a", "--action", help="what action to perform",
267 dest="action")
showard50c0e712008-09-22 16:20:37 +0000268 parser.add_option("-f", "--force", help="don't ask for confirmation",
269 action="store_true")
jadmanski0afbb632008-06-06 21:10:57 +0000270 (options, args) = parser.parse_args()
showard0e73c852008-10-03 10:15:50 +0000271 database = database_connection.DatabaseConnection(options.database)
272 database.reconnect_enabled = False
273 database.connect()
274 manager = MigrationManager(database, force=options.force)
jadmanski0afbb632008-06-06 21:10:57 +0000275
276 if len(args) > 0:
277 if len(args) > 1:
278 version = int(args[1])
279 else:
280 version = None
281 if args[0] == 'sync':
282 manager.do_sync_db(version)
283 elif args[0] == 'test':
284 manager.test_sync_db(version)
285 elif args[0] == 'simulate':
286 manager.simulate_sync_db(version)
287 elif args[0] == 'safesync':
288 print 'Simluating migration'
289 manager.simulate_sync_db(version)
290 print 'Performing real migration'
291 manager.do_sync_db(version)
292 else:
293 print USAGE
294 return
295
296 print USAGE
mblighe8819cd2008-02-15 16:48:40 +0000297
298
299if __name__ == '__main__':
jadmanski0afbb632008-06-06 21:10:57 +0000300 main()