233 lines
8.8 KiB
Python
233 lines
8.8 KiB
Python
# Licensed 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 collections
|
|
import functools
|
|
import prettytable
|
|
import six
|
|
import sys
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
import pbr.version
|
|
|
|
from placement import conf
|
|
from placement import context
|
|
from placement.db.sqlalchemy import migration
|
|
from placement import db_api
|
|
from placement.objects import consumer as consumer_obj
|
|
from placement.objects import resource_provider as rp_obj
|
|
|
|
version_info = pbr.version.VersionInfo('openstack-placement')
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
online_migrations = (
|
|
# These functions are called with a DB context and a count, which is the
|
|
# maximum batch size requested by the user. They must be idempotent.
|
|
# At most $count records should be migrated. The function must return a
|
|
# tuple of (found, done). The found value indicates how many
|
|
# unmigrated/candidate records existed in the database prior to the
|
|
# migration (either total, or up to the $count limit provided), and a
|
|
# nonzero found value may tell the user that there is still work to do.
|
|
# The done value indicates whether or not any records were actually
|
|
# migrated by the function. Thus if both (found, done) are nonzero, work
|
|
# was done and some work remains. If found is nonzero and done is zero,
|
|
# some records are not migratable, but all migrations that can complete
|
|
# have finished.
|
|
|
|
# Added in Stein
|
|
rp_obj.set_root_provider_ids,
|
|
# Added in Stein (copied from migration added to Nova in Rocky)
|
|
consumer_obj.create_incomplete_consumers,
|
|
)
|
|
|
|
|
|
class DbCommands(object):
|
|
def __init__(self, config):
|
|
self.config = config
|
|
|
|
def db_sync(self):
|
|
# Let exceptions raise for now, they will go to stderr.
|
|
migration.upgrade('head')
|
|
return 0
|
|
|
|
def db_version(self):
|
|
print(migration.version())
|
|
return 0
|
|
|
|
def db_stamp(self):
|
|
migration.stamp(self.config.command.version)
|
|
return 0
|
|
|
|
def db_online_data_migrations(self):
|
|
"""Processes online data migration.
|
|
|
|
:returns: 0 if no (further) updates are possible, 1 if the
|
|
``--max-count`` option was used and some updates were
|
|
completed successfully (even if others generated errors),
|
|
2 if some updates generated errors and no other migrations
|
|
were able to take effect in the last batch attempted, or
|
|
127 if invalid input is provided.
|
|
"""
|
|
max_count = self.config.command.max_count
|
|
if max_count is not None:
|
|
try:
|
|
max_count = int(max_count)
|
|
except ValueError:
|
|
max_count = -1
|
|
if max_count < 1:
|
|
print('Must supply a positive value for max_count')
|
|
return 127
|
|
limited = True
|
|
else:
|
|
max_count = 50
|
|
limited = False
|
|
print('Running batches of %i until complete' % max_count)
|
|
|
|
ran = None
|
|
migration_info = collections.OrderedDict()
|
|
exceptions = False
|
|
while ran is None or ran != 0:
|
|
migrations, exceptions = self._run_online_migration(max_count)
|
|
ran = 0
|
|
# For each batch of migration method results, build the cumulative
|
|
# set of results.
|
|
for name in migrations:
|
|
migration_info.setdefault(name, (0, 0))
|
|
migration_info[name] = (
|
|
migration_info[name][0] + migrations[name][0],
|
|
migration_info[name][1] + migrations[name][1],
|
|
)
|
|
ran += migrations[name][1]
|
|
if limited:
|
|
break
|
|
|
|
t = prettytable.PrettyTable(
|
|
['Migration', 'Total Found', 'Completed'])
|
|
for name, info in migration_info.items():
|
|
t.add_row([name, info[0], info[1]])
|
|
print(t)
|
|
|
|
# NOTE(tetsuro): In "limited" case, if some update has been "ran",
|
|
# exceptions are not considered fatal because work may still remain
|
|
# to be done, and that work may resolve dependencies for the failing
|
|
# migrations.
|
|
if exceptions and not (limited and ran):
|
|
print("Some migrations failed unexpectedly. Check log for "
|
|
"details.")
|
|
return 2
|
|
|
|
# TODO(mriedem): Potentially add another return code for
|
|
# "there are more migrations, but not completable right now"
|
|
return ran and 1 or 0
|
|
|
|
def _run_online_migration(self, max_count):
|
|
ctxt = context.RequestContext(config=self.config)
|
|
ran = 0
|
|
exceptions = False
|
|
migrations = collections.OrderedDict()
|
|
for migration_meth in online_migrations:
|
|
count = max_count - ran
|
|
try:
|
|
found, done = migration_meth(ctxt, count)
|
|
except Exception:
|
|
msg = ("Error attempting to run %(method)s" % dict(
|
|
method=migration_meth))
|
|
print(msg)
|
|
LOG.exception(msg)
|
|
exceptions = True
|
|
found = done = 0
|
|
|
|
name = migration_meth.__name__
|
|
if found:
|
|
print('%(total)i rows matched query %(meth)s, %(done)i '
|
|
'migrated' % {'total': found,
|
|
'meth': name,
|
|
'done': done})
|
|
# This is the per-migration method result for this batch, and
|
|
# _run_online_migration will either continue on to the next
|
|
# migration, or stop if up to this point we've processed max_count
|
|
# of records across all migration methods.
|
|
migrations[name] = found, done
|
|
ran += done
|
|
if ran >= max_count:
|
|
break
|
|
return migrations, exceptions
|
|
|
|
|
|
def add_db_command_parsers(subparsers, config):
|
|
command_object = DbCommands(config)
|
|
|
|
# If we set False here, we avoid having an exit during the parse
|
|
# args part of CONF processing and we can thus print out meaningful
|
|
# help text.
|
|
subparsers.required = False
|
|
parser = subparsers.add_parser('db')
|
|
# Avoid https://bugs.python.org/issue9351 with cpython < 2.7.9
|
|
if not six.PY2:
|
|
parser.set_defaults(func=parser.print_help)
|
|
db_parser = parser.add_subparsers(description='database commands')
|
|
|
|
help = 'Sync the datatabse to the current version.'
|
|
sync_parser = db_parser.add_parser('sync', help=help, description=help)
|
|
sync_parser.set_defaults(func=command_object.db_sync)
|
|
|
|
help = 'Report the current database version.'
|
|
version_parser = db_parser.add_parser(
|
|
'version', help=help, description=help)
|
|
version_parser.set_defaults(func=command_object.db_version)
|
|
|
|
help = 'Stamp the revision table with the given version.'
|
|
stamp_parser = db_parser.add_parser('stamp', help=help, description=help)
|
|
stamp_parser.add_argument('version', help='the version to stamp')
|
|
stamp_parser.set_defaults(func=command_object.db_stamp)
|
|
|
|
help = 'Run the online data migrations.'
|
|
online_dm_parser = db_parser.add_parser(
|
|
'online_data_migrations', help=help, description=help)
|
|
online_dm_parser.add_argument(
|
|
'--max-count', metavar='<number>',
|
|
help='Maximum number of objects to consider')
|
|
online_dm_parser.set_defaults(
|
|
func=command_object.db_online_data_migrations)
|
|
|
|
|
|
def setup_commands(config):
|
|
# This is a separate method because it facilitates unit testing.
|
|
# Use an additional SubCommandOpt and parser for each new sub command.
|
|
add_db_cmd_parsers = functools.partial(
|
|
add_db_command_parsers, config=config)
|
|
command_opt = cfg.SubCommandOpt(
|
|
'db', dest='command', title='Command', help='Available DB commands',
|
|
handler=add_db_cmd_parsers)
|
|
return [command_opt]
|
|
|
|
|
|
def main():
|
|
config = cfg.ConfigOpts()
|
|
conf.register_opts(config)
|
|
command_opts = setup_commands(config)
|
|
config.register_cli_opts(command_opts)
|
|
config(sys.argv[1:], project='placement',
|
|
version=version_info.version_string(),
|
|
default_config_files=None)
|
|
db_api.configure(config)
|
|
|
|
try:
|
|
func = config.command.func
|
|
return_code = func()
|
|
# If return_code ends up None we assume 0.
|
|
sys.exit(return_code or 0)
|
|
except cfg.NoSuchOptError:
|
|
config.print_help()
|
|
sys.exit(1)
|