Wedge DB migrations if flavor migrations are not complete

In Kilo, we moved per-instance flavor information from sysmeta to a new
location. We have a lot of compatibity code as a result, which we want
to drop ASAP. In order to do that, we need to be sure that nobody can run
the post-drop code with unmigrated instances. This adds a new DB migration
that just does a sanity check and blocks moving forward if any unmigrated
instances are found.

NOTE: This does not operate on deleted instances, which matches the
behavior of the nova-manage command to push migrations.

Depends-On: Ib38f4bdb2b8e478973e0e5aaaeb4a25771af720e
Change-Id: Ide5492b8b52dec665088d7bca78c386418eba0fe
This commit is contained in:
Dan Smith 2015-04-16 10:39:36 -07:00
parent de8d19dec1
commit bf8b3946e6
3 changed files with 128 additions and 0 deletions

View File

@ -0,0 +1,35 @@
# 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.
from sqlalchemy import MetaData, Table, func, select
from sqlalchemy.sql import and_
from nova import exception
from nova.i18n import _
def upgrade(migrate_engine):
meta = MetaData(migrate_engine)
instances = Table('instances', meta, autoload=True)
sysmeta = Table('instance_system_metadata', meta, autoload=True)
count = select([func.count()]).select_from(sysmeta).where(
and_(instances.c.uuid == sysmeta.c.instance_uuid,
sysmeta.c.key == 'instance_type_id',
sysmeta.c.deleted != sysmeta.c.id,
instances.c.deleted != instances.c.id)).execute().scalar()
if count > 0:
msg = _('There are still %(count)i unmigrated flavor records. '
'Migration cannot continue until all instance flavor '
'records have been migrated to the new format. Please run '
'`nova-manage db migrate_flavor_data\' first.') % {
'count': count}
raise exception.ValidationError(detail=msg)

View File

@ -669,6 +669,11 @@ class NovaMigrationsCheckers(test_migrations.ModelsMigrationsSync,
key_pairs = oslodbutils.get_table(engine, 'key_pairs')
self.assertFalse(key_pairs.c.name.nullable)
def _check_291(self, engine, data):
# NOTE(danms): This is a dummy migration that just does a consistency
# check
pass
class TestNovaMigrationsSQLite(NovaMigrationsCheckers,
test_base.DbTestCase,

View File

@ -12,7 +12,9 @@
# License for the specific language governing permissions and limitations
# under the License.
import importlib
import mock
import uuid
from migrate import exceptions as versioning_exceptions
from migrate import UniqueConstraint
@ -20,8 +22,12 @@ from migrate.versioning import api as versioning_api
from oslo_db.sqlalchemy import utils as db_utils
import sqlalchemy
from nova.compute import flavors
from nova import context
from nova.db.sqlalchemy import api as db_api
from nova.db.sqlalchemy import migration
from nova import exception
from nova import objects
from nova import test
@ -178,3 +184,85 @@ class TestGetEngine(test.NoDBTestCase):
engine = migration.get_engine('api')
self.assertEqual('api_engine', engine)
mock_get_engine.assert_called_once_with()
class TestFlavorCheck(test.TestCase):
def setUp(self):
super(TestFlavorCheck, self).setUp()
self.context = context.get_admin_context()
self.migration = importlib.import_module(
'nova.db.sqlalchemy.migrate_repo.versions.'
'291_enforce_flavors_migrated')
self.engine = db_api.get_engine()
def test_upgrade_clean(self):
inst = objects.Instance(context=self.context,
uuid=uuid.uuid4(),
user_id=self.context.user_id,
project_id=self.context.project_id,
system_metadata={'foo': 'bar'})
inst.create()
self.migration.upgrade(self.engine)
def test_upgrade_dirty(self):
inst = objects.Instance(context=self.context,
uuid=uuid.uuid4(),
user_id=self.context.user_id,
project_id=self.context.project_id,
system_metadata={'foo': 'bar',
'instance_type_id': 'foo'})
inst.create()
self.assertRaises(exception.ValidationError,
self.migration.upgrade, self.engine)
def test_upgrade_flavor_deleted_instances(self):
inst = objects.Instance(context=self.context,
uuid=uuid.uuid4(),
user_id=self.context.user_id,
project_id=self.context.project_id,
system_metadata={'foo': 'bar',
'instance_type_id': 'foo'})
inst.create()
inst.destroy()
self.migration.upgrade(self.engine)
def test_upgrade_flavor_deleted_sysmeta(self):
flavor = flavors.get_default_flavor()
sysmeta = flavors.save_flavor_info({}, flavor)
sysmeta['foo'] = 'bar'
inst = objects.Instance(context=self.context,
uuid=uuid.uuid4(),
user_id=self.context.user_id,
project_id=self.context.project_id,
system_metadata=sysmeta)
inst.create()
sysmeta = db_api.instance_system_metadata_get(self.context,
inst.uuid)
sysmeta = {k: v for k, v in sysmeta.items()
if not k.startswith('instance_type_')}
db_api.instance_system_metadata_update(self.context, inst.uuid,
sysmeta, True)
inst.refresh()
self.assertEqual({'foo': 'bar'}, inst.system_metadata)
self.migration.upgrade(self.engine)
def test_upgrade_flavor_already_migrated(self):
flavor = flavors.get_default_flavor()
sysmeta = flavors.save_flavor_info({}, flavor)
sysmeta['foo'] = 'bar'
inst = objects.Instance(context=self.context,
uuid=uuid.uuid4(),
user_id=self.context.user_id,
project_id=self.context.project_id,
system_metadata=sysmeta)
inst.create()
# Trigger the migration by lazy-loading flavor
inst.flavor
inst.save()
self.assertNotIn('instance_type_id', inst.system_metadata)
sysmeta = db_api.instance_system_metadata_get(self.context,
inst.uuid)
self.assertEqual({'foo': 'bar'}, sysmeta)
self.migration.upgrade(self.engine)