Merge "Fix migration 112 to use live_data_migration API"

This commit is contained in:
Zuul 2017-10-24 17:53:42 +00:00 committed by Gerrit Code Review
commit c1bee6c204
10 changed files with 100 additions and 36 deletions

View File

@ -205,7 +205,10 @@ class HostCommands(object):
class DbCommands(object): class DbCommands(object):
"""Class for managing the database.""" """Class for managing the database."""
online_migrations = () online_migrations = (
# Added in Queens
db.service_uuids_online_data_migration,
)
def __init__(self): def __init__(self):
pass pass
@ -250,13 +253,13 @@ class DbCommands(object):
"logs for more details.")) "logs for more details."))
sys.exit(1) sys.exit(1)
def _run_migration(self, ctxt, max_count, ignore_state): def _run_migration(self, ctxt, max_count):
ran = 0 ran = 0
migrations = {} migrations = {}
for migration_meth in self.online_migrations: for migration_meth in self.online_migrations:
count = max_count - ran count = max_count - ran
try: try:
found, done = migration_meth(ctxt, count, ignore_state) found, done = migration_meth(ctxt, count)
except Exception: except Exception:
print(_("Error attempting to run %(method)s") % print(_("Error attempting to run %(method)s") %
{'method': migration_meth.__name__}) {'method': migration_meth.__name__})
@ -283,11 +286,7 @@ class DbCommands(object):
@args('--max_count', metavar='<number>', dest='max_count', type=int, @args('--max_count', metavar='<number>', dest='max_count', type=int,
help='Maximum number of objects to consider.') help='Maximum number of objects to consider.')
@args('--ignore_state', action='store_true', dest='ignore_state', def online_data_migrations(self, max_count=None):
help='Force records to migrate even if another operation is '
'performed on them. This may be dangerous, please refer to '
'release notes for more information.')
def online_data_migrations(self, max_count=None, ignore_state=False):
"""Perform online data migrations for the release in batches.""" """Perform online data migrations for the release in batches."""
ctxt = context.get_admin_context() ctxt = context.get_admin_context()
if max_count is not None: if max_count is not None:
@ -303,19 +302,18 @@ class DbCommands(object):
ran = None ran = None
migration_info = {} migration_info = {}
while ran is None or ran != 0: while ran is None or ran != 0:
migrations = self._run_migration(ctxt, max_count, ignore_state) migrations = self._run_migration(ctxt, max_count)
migration_info.update(migrations) migration_info.update(migrations)
ran = sum([done for found, done, remaining in migrations.values()]) ran = sum([done for found, done, remaining in migrations.values()])
if not unlimited: if not unlimited:
break break
headers = ["{}".format(_('Migration')), headers = ["{}".format(_('Migration')),
"{}".format(_('Found')), "{}".format(_('Total Needed')),
"{}".format(_('Done')), "{}".format(_('Completed')), ]
"{}".format(_('Remaining'))]
t = prettytable.PrettyTable(headers) t = prettytable.PrettyTable(headers)
for name in sorted(migration_info.keys()): for name in sorted(migration_info.keys()):
info = migration_info[name] info = migration_info[name]
t.add_row([name, info[0], info[1], info[2]]) t.add_row([name, info[0], info[1]])
print(t) print(t)
sys.exit(1 if ran else 0) sys.exit(1 if ran else 0)

View File

@ -93,6 +93,10 @@ def dispose_engine():
################### ###################
def service_uuids_online_data_migration(context, max_count):
return IMPL.service_uuids_online_data_migration(context, max_count)
def service_destroy(context, service_id): def service_destroy(context, service_id):
"""Destroy the service or raise if it does not exist.""" """Destroy the service or raise if it does not exist."""
return IMPL.service_destroy(context, service_id) return IMPL.service_destroy(context, service_id)
@ -142,6 +146,14 @@ def service_update(context, service_id, values):
return IMPL.service_update(context, service_id, values) return IMPL.service_update(context, service_id, values)
def service_get_by_uuid(context, service_uuid):
"""Get a service by it's uuid.
Return Service ref or raise if it does not exist.
"""
return IMPL.service_get_by_uuid(context, service_uuid)
############### ###############

View File

@ -32,6 +32,7 @@ import uuid
from oslo_config import cfg from oslo_config import cfg
from oslo_db import exception as db_exc from oslo_db import exception as db_exc
from oslo_db import options from oslo_db import options
from oslo_db.sqlalchemy import enginefacade
from oslo_db.sqlalchemy import session as db_session from oslo_db.sqlalchemy import session as db_session
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import importutils from oslo_utils import importutils
@ -555,6 +556,16 @@ def service_get_all(context, backend_match_level=None, **filters):
return [] if not query else query.all() return [] if not query else query.all()
@require_admin_context
def service_get_by_uuid(context, service_uuid):
query = model_query(context, models.Service).fitler_by(uuid=service_uuid)
result = query.first()
if not result:
raise exception.ServiceNotFound(service_id=service_uuid)
return result
@require_admin_context @require_admin_context
def service_create(context, values): def service_create(context, values):
if not values.get('uuid'): if not values.get('uuid'):
@ -585,6 +596,27 @@ def service_update(context, service_id, values):
raise exception.ServiceNotFound(service_id=service_id) raise exception.ServiceNotFound(service_id=service_id)
@enginefacade.writer
def service_uuids_online_data_migration(context, max_count):
from cinder.objects import service
total = 0
updated = 0
db_services = model_query(context, models.Service).filter_by(
uuid=None).limit(max_count)
for db_service in db_services:
total += 1
# The conversion in the Service object code
# will generate a UUID and save it for us.
service_obj = service.Service._from_db_object(
context, service.Service(), db_service)
if 'uuid' in service_obj:
updated += 1
return total, updated
################### ###################
@ -605,6 +637,7 @@ def is_backend_frozen(context, host, cluster_name):
################### ###################
def _cluster_query(context, is_up=None, get_services=False, def _cluster_query(context, is_up=None, get_services=False,
services_summary=False, read_deleted='no', services_summary=False, read_deleted='no',
name_match_level=None, name=None, session=None, **filters): name_match_level=None, name=None, session=None, **filters):

View File

@ -10,9 +10,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import six
import uuid
from sqlalchemy import Column from sqlalchemy import Column
from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.reflection import Inspector
from sqlalchemy import Index from sqlalchemy import Index
@ -33,9 +30,3 @@ def upgrade(migrate_engine):
if uuid_index_name not in (i['name'] for i in indexes): if uuid_index_name not in (i['name'] for i in indexes):
services = Table('services', meta, autoload=True) services = Table('services', meta, autoload=True)
Index(uuid_index_name, services.c.uuid, unique=True).create() Index(uuid_index_name, services.c.uuid, unique=True).create()
service_list = list(services.select().execute())
for s in service_list:
if not s.uuid:
services.update().where(services.c.id == s.id).\
values(uuid=six.text_type(uuid.uuid4())).execute()

View File

@ -135,6 +135,7 @@ OBJ_VERSIONS.add('1.24', {'LogLevel': '1.0', 'LogLevelList': '1.0'})
OBJ_VERSIONS.add('1.25', {'Group': '1.2'}) OBJ_VERSIONS.add('1.25', {'Group': '1.2'})
OBJ_VERSIONS.add('1.26', {'Snapshot': '1.5'}) OBJ_VERSIONS.add('1.26', {'Snapshot': '1.5'})
OBJ_VERSIONS.add('1.27', {'Backup': '1.5', 'BackupImport': '1.5'}) OBJ_VERSIONS.add('1.27', {'Backup': '1.5', 'BackupImport': '1.5'})
OBJ_VERSIONS.add('1.28', {'Service': '1.5'})
class CinderObjectRegistry(base.VersionedObjectRegistry): class CinderObjectRegistry(base.VersionedObjectRegistry):

View File

@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from oslo_log import log as logging
from oslo_utils import uuidutils
from oslo_utils import versionutils from oslo_utils import versionutils
from oslo_versionedobjects import fields from oslo_versionedobjects import fields
@ -24,6 +26,9 @@ from cinder.objects import fields as c_fields
from cinder import utils from cinder import utils
LOG = logging.getLogger(__name__)
@base.CinderObjectRegistry.register @base.CinderObjectRegistry.register
class Service(base.CinderPersistentObject, base.CinderObject, class Service(base.CinderPersistentObject, base.CinderObject,
base.CinderObjectDictCompat, base.CinderComparableObject, base.CinderObjectDictCompat, base.CinderComparableObject,
@ -33,7 +38,8 @@ class Service(base.CinderPersistentObject, base.CinderObject,
# Version 1.2: Add get_minimum_rpc_version() and get_minimum_obj_version() # Version 1.2: Add get_minimum_rpc_version() and get_minimum_obj_version()
# Version 1.3: Add replication fields # Version 1.3: Add replication fields
# Version 1.4: Add cluster fields # Version 1.4: Add cluster fields
VERSION = '1.4' # Version 1.5: Add UUID field
VERSION = '1.5'
OPTIONAL_FIELDS = ('cluster',) OPTIONAL_FIELDS = ('cluster',)
@ -59,6 +65,8 @@ class Service(base.CinderPersistentObject, base.CinderObject,
'replication_status': c_fields.ReplicationStatusField(nullable=True), 'replication_status': c_fields.ReplicationStatusField(nullable=True),
'frozen': fields.BooleanField(default=False), 'frozen': fields.BooleanField(default=False),
'active_backend_id': fields.StringField(nullable=True), 'active_backend_id': fields.StringField(nullable=True),
'uuid': fields.StringField(nullable=True),
} }
def obj_make_compatible(self, primitive, target_version): def obj_make_compatible(self, primitive, target_version):
@ -71,6 +79,8 @@ class Service(base.CinderPersistentObject, base.CinderObject,
if target_version < (1, 4): if target_version < (1, 4):
for obj_field in ('cluster', 'cluster_name'): for obj_field in ('cluster', 'cluster_name'):
primitive.pop(obj_field, None) primitive.pop(obj_field, None)
if target_version < (1, 5) and 'uuid' in primitive:
del primitive['uuid']
@staticmethod @staticmethod
def _from_db_object(context, service, db_service, expected_attrs=None): def _from_db_object(context, service, db_service, expected_attrs=None):
@ -98,6 +108,14 @@ class Service(base.CinderPersistentObject, base.CinderObject,
service.cluster = None service.cluster = None
service.obj_reset_changes() service.obj_reset_changes()
# TODO(jdg): Remove in S when we're sure all Services have UUID in db
if 'uuid' not in service:
service.uuid = uuidutils.generate_uuid()
LOG.debug('Generated UUID %(uuid)s for service %(id)i',
dict(uuid=service.uuid, id=service.id))
service.save()
return service return service
def obj_load_attr(self, attrname): def obj_load_attr(self, attrname):
@ -131,6 +149,11 @@ class Service(base.CinderPersistentObject, base.CinderObject,
db_service = db.service_get(context, host=host, binary=binary_key) db_service = db.service_get(context, host=host, binary=binary_key)
return cls._from_db_object(context, cls(context), db_service) return cls._from_db_object(context, cls(context), db_service)
@classmethod
def get_by_uuid(cls, context, service_uuid):
db_service = db.service_get_by_uuid(context, service_uuid)
return cls._from_db_object(context, cls(), db_service)
def create(self): def create(self):
if self.obj_attr_is_set('id'): if self.obj_attr_is_set('id'):
raise exception.ObjectActionError(action='create', raise exception.ObjectActionError(action='create',
@ -139,6 +162,10 @@ class Service(base.CinderPersistentObject, base.CinderObject,
if 'cluster' in updates: if 'cluster' in updates:
raise exception.ObjectActionError( raise exception.ObjectActionError(
action='create', reason=_('cluster assigned')) action='create', reason=_('cluster assigned'))
if 'uuid' not in updates:
updates['uuid'] = uuidutils.generate_uuid()
self.uuid = updates['uuid']
db_service = db.service_create(self._context, updates) db_service = db.service_create(self._context, updates)
self._from_db_object(self._context, self, db_service) self._from_db_object(self._context, self, db_service)

View File

@ -34,6 +34,7 @@ def fake_db_service(**updates):
'deleted_at': None, 'deleted_at': None,
'deleted': False, 'deleted': False,
'id': 123, 'id': 123,
'uuid': 'ce59413f-4061-425c-9ad0-3479bd102ab2',
'host': 'fake-host', 'host': 'fake-host',
'binary': 'fake-service', 'binary': 'fake-service',
'topic': 'fake-service-topic', 'topic': 'fake-service-topic',

View File

@ -43,7 +43,7 @@ object_data = {
'QualityOfServiceSpecs': '1.0-0b212e0a86ee99092229874e03207fe8', 'QualityOfServiceSpecs': '1.0-0b212e0a86ee99092229874e03207fe8',
'QualityOfServiceSpecsList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e', 'QualityOfServiceSpecsList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'RequestSpec': '1.1-b0bd1a28d191d75648901fa853e8a733', 'RequestSpec': '1.1-b0bd1a28d191d75648901fa853e8a733',
'Service': '1.4-a6727ccda6d4043f5e38e75c7c518c7f', 'Service': '1.5-dfa465098dd9017bad825cab55eacc93',
'ServiceList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e', 'ServiceList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'Snapshot': '1.5-ac1cdbd5b89588f6a8f44afdf6b8b201', 'Snapshot': '1.5-ac1cdbd5b89588f6a8f44afdf6b8b201',
'SnapshotList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e', 'SnapshotList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',

View File

@ -62,7 +62,8 @@ class TestService(test_objects.BaseObjectsTestCase):
service = objects.Service(context=self.context) service = objects.Service(context=self.context)
service.create() service.create()
self.assertEqual(db_service['id'], service.id) self.assertEqual(db_service['id'], service.id)
service_create.assert_called_once_with(self.context, {}) service_create.assert_called_once_with(self.context,
{'uuid': mock.ANY})
@mock.patch('cinder.db.service_update') @mock.patch('cinder.db.service_update')
def test_save(self, service_update): def test_save(self, service_update):

View File

@ -227,7 +227,7 @@ class TestCinderManageCmd(test.TestCase):
exit = self.assertRaises(SystemExit, db_cmds.online_data_migrations) exit = self.assertRaises(SystemExit, db_cmds.online_data_migrations)
self.assertEqual(0, exit.code) self.assertEqual(0, exit.code)
cinder_manage.DbCommands.online_migrations[0].assert_has_calls( cinder_manage.DbCommands.online_migrations[0].assert_has_calls(
(mock.call(mock.ANY, 50, False),) * 2) (mock.call(mock.ANY, 50),) * 2)
def _fake_db_command(self, migrations=None): def _fake_db_command(self, migrations=None):
if migrations is None: if migrations is None:
@ -254,17 +254,17 @@ class TestCinderManageCmd(test.TestCase):
expected = """\ expected = """\
5 rows matched query mock_mig_1, 4 migrated, 1 remaining 5 rows matched query mock_mig_1, 4 migrated, 1 remaining
6 rows matched query mock_mig_2, 6 migrated, 0 remaining 6 rows matched query mock_mig_2, 6 migrated, 0 remaining
+------------+-------+------+-----------+ +------------+--------------+-----------+
| Migration | Found | Done | Remaining | | Migration | Total Needed | Completed |
+------------+-------+------+-----------+ +------------+--------------+-----------+
| mock_mig_1 | 5 | 4 | 1 | | mock_mig_1 | 5 | 4 |
| mock_mig_2 | 6 | 6 | 0 | | mock_mig_2 | 6 | 6 |
+------------+-------+------+-----------+ +------------+--------------+-----------+
""" """
command.online_migrations[0].assert_has_calls([mock.call(ctxt, command.online_migrations[0].assert_has_calls([mock.call(ctxt,
10, False)]) 10)])
command.online_migrations[1].assert_has_calls([mock.call(ctxt, command.online_migrations[1].assert_has_calls([mock.call(ctxt,
6, False)]) 6)])
self.assertEqual(expected, sys.stdout.getvalue()) self.assertEqual(expected, sys.stdout.getvalue())
@ -273,10 +273,10 @@ class TestCinderManageCmd(test.TestCase):
def test_db_commands_online_data_migrations_ignore_state_and_max(self): def test_db_commands_online_data_migrations_ignore_state_and_max(self):
db_cmds = cinder_manage.DbCommands() db_cmds = cinder_manage.DbCommands()
exit = self.assertRaises(SystemExit, db_cmds.online_data_migrations, exit = self.assertRaises(SystemExit, db_cmds.online_data_migrations,
2, True) 2)
self.assertEqual(1, exit.code) self.assertEqual(1, exit.code)
cinder_manage.DbCommands.online_migrations[0].assert_called_once_with( cinder_manage.DbCommands.online_migrations[0].assert_called_once_with(
mock.ANY, 2, True) mock.ANY, 2)
@mock.patch('cinder.cmd.manage.DbCommands.online_migrations', @mock.patch('cinder.cmd.manage.DbCommands.online_migrations',
(mock.Mock(side_effect=((2, 2), (0, 0)), __name__='foo'),)) (mock.Mock(side_effect=((2, 2), (0, 0)), __name__='foo'),))