Make nova-manage handle completely missing flavor information

If you have a really (really) old instance without any flavor information
stashed in sysmeta, then nova-manage will not be able to convert it to
a proper flavor object in extra. This patch makes it handle that case
by looking up the flavor by id instead. Not only will this facilitate
the transition, but will also ensure that any such legacy instances are
properly brought up to date so that going forward we can just assume that
they have all been converted (and remove some other places where we
handle the has-no-flavor-info case).

This involves changing the flavor cache to contain flavor objects
instead of DB objects so that the two methods that use the cache can
coexist. It doesn't much affect the _augment_flavors_to_migrate() path,
other than some mechanics and test changes.

Conflicts:
	nova/tests/unit/db/test_db_api.py

Change-Id: I54a056d339d98bc4092af8cf9f4f5d24b882506b
Closes-Bug: #1460673
(cherry picked from commit 240fb9c539)
This commit is contained in:
Dan Smith 2015-06-01 07:31:26 -07:00
parent d203dd7f7f
commit 22adc62466
2 changed files with 189 additions and 16 deletions

View File

@ -6014,13 +6014,14 @@ def archive_deleted_rows(context, max_rows=None):
return rows_archived
def _augment_flavor_to_migrate(flavor_to_migrate, db_flavor):
def _augment_flavor_to_migrate(flavor_to_migrate, full_flavor):
"""Make sure that extra_specs on the flavor to migrate is updated."""
if not flavor_to_migrate.obj_attr_is_set('extra_specs'):
flavor_to_migrate.extra_specs = {}
for key in db_flavor['extra_specs']:
for key in full_flavor['extra_specs']:
if key not in flavor_to_migrate.extra_specs:
flavor_to_migrate.extra_specs[key] = db_flavor['extra_specs'][key]
flavor_to_migrate.extra_specs[key] = \
full_flavor.extra_specs[key]
def _augment_flavors_to_migrate(instance, flavor_cache):
@ -6030,17 +6031,40 @@ def _augment_flavors_to_migrate(instance, flavor_cache):
:param flavor_cache: Dict to persist flavors we look up from the DB
"""
# NOTE(danms): Avoid circular import
from nova import objects
deleted_ctx = instance._context.elevated(read_deleted='yes')
for flavorprop in ['flavor', 'old_flavor', 'new_flavor']:
flavor = getattr(instance, flavorprop)
if flavor is None:
continue
flavorid = flavor.flavorid
if flavorid not in flavor_cache:
flavor_cache[flavorid] = flavor_get_by_flavor_id(
instance._context, flavorid, 'yes')
flavor_cache[flavorid] = objects.Flavor.get_by_flavor_id(
deleted_ctx, flavorid)
_augment_flavor_to_migrate(flavor, flavor_cache[flavorid])
def _load_missing_flavor(instance, flavor_cache):
# NOTE(danms): Avoid circular import
from nova import objects
deleted_ctx = instance._context.elevated(read_deleted='yes')
flavor_cache_by_id = {flavor.id: flavor
for flavor in flavor_cache.values()}
if instance.instance_type_id in flavor_cache_by_id:
instance.flavor = flavor_cache_by_id[instance.instance_type_id]
else:
instance.flavor = objects.Flavor.get_by_id(deleted_ctx,
instance.instance_type_id)
flavor_cache[instance.flavor.flavorid] = instance.flavor
instance.old_flavor = None
instance.new_flavor = None
@require_admin_context
def migrate_flavor_data(context, max_count, flavor_cache):
# NOTE(danms): This is only ever run in nova-manage, and we need to avoid
@ -6074,7 +6098,22 @@ def migrate_flavor_data(context, max_count, flavor_cache):
if instance.vm_state in [vm_states.RESCUED, vm_states.RESIZED]:
continue
_augment_flavors_to_migrate(instance, flavor_cache)
# NOTE(danms): If we have a really old instance with no flavor
# information at all, flavor will not have been set during load.
# If that's the case, look up the flavor by id (which implies that
# old_ and new_flavor are None). No need to augment with extra_specs
# since we're doing the lookup from scratch.
if not instance.obj_attr_is_set('flavor'):
try:
_load_missing_flavor(instance, flavor_cache)
except exception.FlavorNotFound:
LOG.error(_LE('Unable to lookup flavor for legacy instance; '
'migration is not possible without manual '
'intervention'),
instance=instance)
continue
else:
_augment_flavors_to_migrate(instance, flavor_cache)
if instance.obj_what_changed():
if db_instance.get('extra') is None:
_instance_extra_create(context,

View File

@ -7499,25 +7499,23 @@ class Ec2TestCase(test.TestCase):
class FlavorMigrationTestCase(test.TestCase):
def test_augment_flavor_to_migrate_no_extra_specs(self):
flavor = objects.Flavor()
db_flavor = {
'extra_specs': {'foo': 'bar'}}
sqlalchemy_api._augment_flavor_to_migrate(flavor, db_flavor)
full_flavor = objects.Flavor(extra_specs={'foo': 'bar'})
sqlalchemy_api._augment_flavor_to_migrate(flavor, full_flavor)
self.assertTrue(flavor.obj_attr_is_set('extra_specs'))
self.assertEqual(db_flavor['extra_specs'], flavor.extra_specs)
self.assertEqual(full_flavor.extra_specs, flavor.extra_specs)
def test_augment_flavor_to_migrate_extra_specs_merge(self):
flavor = objects.Flavor()
flavor.extra_specs = {'foo': '1', 'bar': '2'}
db_flavor = {
'extra_specs': {'bar': '3', 'baz': '4'}
}
sqlalchemy_api._augment_flavor_to_migrate(flavor, db_flavor)
full_flavor = objects.Flavor(extra_specs={'bar': '3', 'baz': '4'})
sqlalchemy_api._augment_flavor_to_migrate(flavor, full_flavor)
self.assertEqual({'foo': '1', 'bar': '2', 'baz': '4'},
flavor.extra_specs)
@mock.patch('nova.db.sqlalchemy.api._augment_flavor_to_migrate')
def test_augment_flavors_to_migrate(self, mock_augment):
instance = objects.Instance()
instance._context = context.get_admin_context()
instance.flavor = objects.Flavor(flavorid='foo')
instance.old_flavor = None
instance.new_flavor = None
@ -7526,7 +7524,7 @@ class FlavorMigrationTestCase(test.TestCase):
mock_augment.assert_called_once_with(instance.flavor, 'bar')
@mock.patch('nova.db.sqlalchemy.api._augment_flavor_to_migrate')
@mock.patch('nova.db.sqlalchemy.api.flavor_get_by_flavor_id')
@mock.patch('nova.objects.Flavor.get_by_flavor_id')
def test_augment_flavors_to_migrate_uses_cache(self, mock_get,
mock_augment):
instance = objects.Instance(context=context.get_admin_context())
@ -7540,7 +7538,7 @@ class FlavorMigrationTestCase(test.TestCase):
sqlalchemy_api._augment_flavors_to_migrate(instance, flavor_cache)
self.assertIn('foo', flavor_cache)
self.assertEqual('foo_flavor', flavor_cache['foo'])
mock_get.assert_called_once_with(instance._context, 'foo', 'yes')
mock_get.assert_called_once_with(mock.ANY, 'foo')
def test_migrate_flavor(self):
ctxt = context.get_admin_context()
@ -7669,6 +7667,142 @@ class FlavorMigrationTestCase(test.TestCase):
self.assertIsNotNone(extra)
self.assertIsNotNone(extra.flavor)
def test_migrate_flavor_with_deleted_things(self):
ctxt = context.get_admin_context()
flavor = flavors.get_default_flavor()
sysmeta = flavors.save_flavor_info({}, flavor)
# Instance with some deleted metadata bits. We create
# with a flavor, then change one of the keys so that
# we end up with a soft-deleted row for that value.
values = {'uuid': str(stdlib_uuid.uuid4()),
'system_metadata': sysmeta,
}
inst1 = db.instance_create(ctxt, values)
_sysmeta = dict(sysmeta, instance_type_id=123)
db.instance_system_metadata_update(ctxt, inst1.uuid,
_sysmeta, True)
# Deleted instance. Without a full flavor in sysmeta,
# if this is hit in the migration, we'll explode trying
# to construct the full flavor.
values = {'uuid': str(stdlib_uuid.uuid4()),
'system_metadata': sysmeta,
}
inst2 = db.instance_create(ctxt, values)
inst2.soft_delete(session=None)
# Instance that has only deleted flavor metadata. This
# looks like an instance that previously had flavor stuff
# in sysmeta, but has since been converted. Since extra.flavor
# is not a legitimate structure, we'll explode if the migration
# code tries to hit this.
values = {'uuid': str(stdlib_uuid.uuid4()),
'system_metadata': {'instance_type_id': '123'},
'extra': {'flavor': 'foobar'},
}
inst3 = db.instance_create(ctxt, values)
db.instance_system_metadata_update(ctxt, inst3.uuid,
{'foo': 'bar'},
True)
match, done = db.migrate_flavor_data(ctxt, None, {})
self.assertEqual(1, match)
self.assertEqual(1, done)
@mock.patch('nova.objects.Flavor.get_by_id')
def _test_migrate_flavor_with_no_sysmeta(self, flavor, inst,
expected_count,
mock_get_flavor):
ctxt = context.get_admin_context()
mock_get_flavor.return_value = flavor
flavor_cache = {}
total, hit = db.migrate_flavor_data(ctxt, None, flavor_cache)
self.assertEqual(expected_count, total)
self.assertEqual(expected_count, hit)
mock_get_flavor.assert_called_once_with(mock.ANY, 123)
self.assertIn(flavor.flavorid, flavor_cache)
# NOTE(danms): Fetch the pieces to make sure the sans-sysmeta
# instance is actually migrated in the database
extra = db.instance_extra_get_by_instance_uuid(ctxt, inst['uuid'])
self.assertIn('flavor', extra)
extra_flavor = objects.Flavor.obj_from_primitive(
jsonutils.loads(extra['flavor'])['cur'])
self.assertEqual(flavor.id, extra_flavor.id)
sysmeta = db.instance_system_metadata_get(ctxt, inst['uuid'])
self.assertNotIn('instance_type_id', sysmeta)
def test_migrate_flavor_with_no_sysmeta_first(self):
flavor = flavors.get_default_flavor()
sysmeta = flavors.save_flavor_info({}, flavor)
ctxt = context.get_admin_context()
values = {'uuid': str(stdlib_uuid.uuid4()),
'instance_type_id': 123,
'system_metadata': sysmeta,
}
db.instance_create(ctxt, values)
values = {'uuid': str(stdlib_uuid.uuid4()),
'instance_type_id': 123,
'system_metadata': {},
}
bad_inst = db.instance_create(ctxt, values)
self._test_migrate_flavor_with_no_sysmeta(flavor, bad_inst, 2)
def test_migrate_flavor_with_no_sysmeta_last(self):
flavor = flavors.get_default_flavor()
sysmeta = flavors.save_flavor_info({}, flavor)
ctxt = context.get_admin_context()
values = {'uuid': str(stdlib_uuid.uuid4()),
'instance_type_id': 123,
'system_metadata': {},
}
bad_inst = db.instance_create(ctxt, values)
values = {'uuid': str(stdlib_uuid.uuid4()),
'instance_type_id': 123,
'system_metadata': sysmeta,
}
db.instance_create(ctxt, values)
self._test_migrate_flavor_with_no_sysmeta(flavor, bad_inst, 2)
def test_migrate_flavor_with_no_sysmeta_alone(self):
flavor = flavors.get_default_flavor()
ctxt = context.get_admin_context()
values = {'uuid': str(stdlib_uuid.uuid4()),
'instance_type_id': 123,
'system_metadata': {},
}
bad_inst = db.instance_create(ctxt, values)
self._test_migrate_flavor_with_no_sysmeta(flavor, bad_inst, 1)
@mock.patch('nova.objects.Instance.save')
@mock.patch('nova.objects.Flavor.get_by_id')
def test_migrate_flavor_with_no_sysmeta_no_flavor(self, mock_get_flavor,
mock_save):
ctxt = context.get_admin_context()
mock_get_flavor.side_effect = exception.FlavorNotFound(flavor_id=123)
values = {'uuid': str(stdlib_uuid.uuid4()),
'instance_type_id': 123,
'system_metadata': {},
}
db.instance_create(ctxt, values)
flavor_cache = {}
db.migrate_flavor_data(ctxt, None, flavor_cache)
mock_get_flavor.assert_called_once_with(mock.ANY, 123)
self.assertEqual({}, flavor_cache)
self.assertFalse(mock_save.called)
class ArchiveTestCase(test.TestCase):