forked from apache/cloudstack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcloud-migrate-databases.in
More file actions
287 lines (244 loc) · 10.6 KB
/
cloud-migrate-databases.in
File metadata and controls
287 lines (244 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
#!/usr/bin/env python
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import os,logging,sys
from optparse import OptionParser
import MySQLdb
import subprocess
import glob
# ---- This snippet of code adds the sources path and the waf configured PYTHONDIR to the Python path ----
# ---- We do this so cloud_utils can be looked up in the following order:
# ---- 1) Sources directory
# ---- 2) waf configured PYTHONDIR
# ---- 3) System Python path
for pythonpath in (
"@PYTHONDIR@",
os.path.join(os.path.dirname(__file__),os.path.pardir,os.path.pardir,"python","lib"),
):
if os.path.isdir(pythonpath): sys.path.insert(0,pythonpath)
# ---- End snippet of code ----
from cloud_utils import check_selinux, CheckFailed, resolves_to_ipv6
import cloud_utils
# RUN ME LIKE THIS
# python setup/bindir/cloud-migrate-databases.in --config=client/tomcatconf/override/db.properties --resourcedir=setup/db --dry-run
# --dry-run makes it so the changes to the database in the context of the migrator are rolled back
# This program / library breaks down as follows:
# high-level breakdown:
# the module calls main()
# main processes command-line options
# main() instantiates a migrator with a a list of possible migration steps
# migrator discovers and topologically sorts migration steps from the given list
# main() run()s the migrator
# for each one of the migration steps:
# the migrator instantiates the migration step with the context as first parameter
# the instantiated migration step saves the context onto itself as self.context
# the migrator run()s the instantiated migration step. within run(), self.context is the context
# the migrator commits the migration context to the database (or rollsback if --dry-run is specified)
# that is it
# The specific library code is in cloud_utils.py
# What needs to be implemented is MigrationSteps
# Specifically in the FromInitialTo21 evolver.
# What Db20to21MigrationUtil.java does, needs to be done within run() of that class
# refer to the class docstring to find out how
# implement them below
class CloudContext(cloud_utils.MigrationContext):
def __init__(self,host,port,username,password,database,configdir,resourcedir):
self.host = host
self.port = port
self.username = username
self.password = password
self.database = database
self.configdir = configdir
self.resourcedir = resourcedir
self.conn = MySQLdb.connect(host=self.host,
user=self.username,
passwd=self.password,
db=self.database,
port=self.port)
self.conn.autocommit(False)
self.db = self.conn.cursor()
def wrapex(func):
sqlogger = logging.getLogger("SQL")
def f(stmt,parms=None):
if parms: sqlogger.debug("%s | with parms %s",stmt,parms)
else: sqlogger.debug("%s",stmt)
return func(stmt,parms)
return f
self.db.execute = wrapex(self.db.execute)
def __str__(self):
return "CloudStack %s database at %s"%(self.database,self.host)
def get_schema_level(self):
return self.get_config_value('schema.level') or cloud_utils.INITIAL_LEVEL
def set_schema_level(self,l):
self.db.execute(
"INSERT INTO configuration (category,instance,component,name,value,description) VALUES ('Hidden', 'DEFAULT', 'database', 'schema.level', %s, 'The schema level of this database') ON DUPLICATE KEY UPDATE value = %s", (l,l)
)
self.commit()
def commit(self):
self.conn.commit()
#self.conn.close()
def close(self):
self.conn.close()
def get_config_value(self,name):
self.db.execute("select value from configuration where name = %s",(name,))
try: return self.db.fetchall()[0][0]
except IndexError: return
def run_sql_resource(self,resource):
sqlfiletext = file(os.path.join(self.resourcedir,resource)).read(-1)
sqlstatements = sqlfiletext.split(";")
for stmt in sqlstatements:
if not stmt.strip(): continue # skip empty statements
self.db.execute(stmt)
class FromInitialTo21NewSchema(cloud_utils.MigrationStep):
def __str__(self): return "Altering the database schema"
from_level = cloud_utils.INITIAL_LEVEL
to_level = "2.1-01"
def run(self): self.context.run_sql_resource("schema-20to21.sql")
class From21NewSchemaTo21NewSchemaPlusIndex(cloud_utils.MigrationStep):
def __str__(self): return "Altering indexes"
from_level = "2.1-01"
to_level = "2.1-02"
def run(self): self.context.run_sql_resource("index-20to21.sql")
class From21NewSchemaPlusIndexTo21DataMigratedPart1(cloud_utils.MigrationStep):
def __str__(self): return "Performing data migration, stage 1"
from_level = "2.1-02"
to_level = "2.1-03"
def run(self): self.context.run_sql_resource("data-20to21.sql")
class From21step1toTo21datamigrated(cloud_utils.MigrationStep):
def __str__(self): return "Performing data migration, stage 2"
from_level = "2.1-03"
to_level = "2.1-04"
def run(self):
systemjars = "@SYSTEMJARS@".split()
pipe = subprocess.Popen(["build-classpath"]+systemjars,stdout=subprocess.PIPE)
systemcp,throwaway = pipe.communicate()
systemcp = systemcp.strip()
if pipe.wait(): # this means that build-classpath failed miserably
systemcp = "@SYSTEMCLASSPATH@"
pcp = os.path.pathsep.join( glob.glob( os.path.join ( "@PREMIUMJAVADIR@" , "*" ) ) )
mscp = "@MSCLASSPATH@"
depscp = "@DEPSCLASSPATH@"
migrationxml = "@SERVERSYSCONFDIR@"
conf = self.context.configdir
cp = os.path.pathsep.join([pcp,systemcp,depscp,mscp,migrationxml,conf])
cmd = ["java"]
cmd += ["-cp",cp]
cmd += ["com.cloud.migration.Db20to21MigrationUtil"]
logging.debug("Running command: %s"," ".join(cmd))
subprocess.check_call(cmd)
class From21datamigratedTo21postprocessed(cloud_utils.MigrationStep):
def __str__(self): return "Postprocessing migrated data"
from_level = "2.1-04"
to_level = "2.1"
def run(self): self.context.run_sql_resource("postprocess-20to21.sql")
class From21To213(cloud_utils.MigrationStep):
def __str__(self): return "Dropping obsolete indexes"
from_level = "2.1"
to_level = "2.1.3"
def run(self): self.context.run_sql_resource("index-212to213.sql")
class From213To22data(cloud_utils.MigrationStep):
def __str__(self): return "Migrating data"
from_level = "2.1.3"
to_level = "2.2-01"
def run(self): self.context.run_sql_resource("data-21to22.sql")
class From22dataTo22(cloud_utils.MigrationStep):
def __str__(self): return "Migrating indexes"
from_level = "2.2-01"
to_level = "2.2"
def run(self): self.context.run_sql_resource("index-21to22.sql")
# command line harness functions
def setup_logging(level):
l = logging.getLogger()
l.setLevel(level)
h = logging.StreamHandler(sys.stderr)
l.addHandler(h)
def setup_optparse():
usage = \
"""%prog [ options ... ]
This command migrates the CloudStack database."""
parser = OptionParser(usage=usage)
parser.add_option("-c", "--config", action="store", type="string",dest='configdir',
default=os.path.join("@MSCONF@"),
help="Configuration directory with a db.properties file, pointing to the CloudStack database")
parser.add_option("-r", "--resourcedir", action="store", type="string",dest='resourcedir',
default="@SETUPDATADIR@",
help="Resource directory with database SQL files used by the migration process")
parser.add_option("-d", "--debug", action="store_true", dest='debug',
default=False,
help="Increase log level from INFO to DEBUG")
parser.add_option("-e", "--dump-evolvers", action="store_true", dest='dumpevolvers',
default=False,
help="Dump evolvers in the order they would be executed, but do not run them")
#parser.add_option("-n", "--dry-run", action="store_true", dest='dryrun',
#default=False,
#help="Run the process as it would normally run, but do not commit the final transaction, so database changes are never saved")
parser.add_option("-f", "--start-at-level", action="store", type="string",dest='fromlevel',
default=None,
help="Rather than discovering the database schema level to start from, start migration from this level. The special value '-' (a dash without quotes) represents the earliest schema level")
parser.add_option("-t", "--end-at-level", action="store", type="string",dest='tolevel',
default=None,
help="Rather than evolving the database to the most up-to-date level, end migration at this level")
return parser
def main(*args):
"""The entry point of this program"""
parser = setup_optparse()
opts, args = parser.parse_args(*args)
if args: parser.error("This command accepts no parameters")
if opts.debug: loglevel = logging.DEBUG
else: loglevel = logging.INFO
setup_logging(loglevel)
# FIXME implement
opts.dryrun = False
configdir = opts.configdir
resourcedir = opts.resourcedir
try:
props = cloud_utils.read_properties(os.path.join(configdir,'db.properties'))
except (IOError,OSError),e:
logging.error("Cannot read from config file: %s",e)
logging.error("You may want to point to a specific config directory with the --config= option")
return 2
if not os.path.isdir(resourcedir):
logging.error("Cannot find directory with SQL files %s",resourcedir)
logging.error("You may want to point to a specific resource directory with the --resourcedir= option")
return 2
host = props["db.cloud.host"]
port = int(props["db.cloud.port"])
username = props["db.cloud.username"]
password = props["db.cloud.password"]
database = props["db.cloud.name"]
# tell the migrator to load its steps from the globals list
migrator = cloud_utils.Migrator(globals().values())
if opts.dumpevolvers:
print "Evolution steps:"
print " %s %s %s"%("From","To","Evolver in charge")
for f,t,e in migrator.get_evolver_chain():
print " %s %s %s"%(f,t,e)
return
#initialize a context with the read configuration
context = CloudContext(host=host,port=port,username=username,password=password,database=database,configdir=configdir,resourcedir=resourcedir)
try:
try:
migrator.run(context,dryrun=opts.dryrun,starting_level=opts.fromlevel,ending_level=opts.tolevel)
finally:
context.close()
except (cloud_utils.NoMigrationPath,cloud_utils.NoMigrator),e:
logging.error("%s",e)
return 4
if __name__ == "__main__":
retval = main()
if retval: sys.exit(retval)
else: sys.exit()