diff --git a/nova/db/sqlalchemy/api_migrations/migrate_repo/versions/030_require_cell_setup.py b/nova/db/sqlalchemy/api_migrations/migrate_repo/versions/030_require_cell_setup.py new file mode 100644 index 000000000000..ffd975c3877a --- /dev/null +++ b/nova/db/sqlalchemy/api_migrations/migrate_repo/versions/030_require_cell_setup.py @@ -0,0 +1,57 @@ +# 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 nova import exception +from nova.i18n import _ +from nova import objects + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + flavors = Table('flavors', meta, autoload=True) + count = select([func.count()]).select_from(flavors).scalar() + if count == 0: + # NOTE(danms): We need to be careful here if this is a new + # installation, which can't possibly have any mappings. Check + # to see if any flavors are defined to determine if we are + # upgrading an existing system. If not, then don't obsess over + # the lack of mappings + return + + cell_mappings = Table('cell_mappings', meta, autoload=True) + count = select([func.count()]).select_from(cell_mappings).scalar() + # Two mappings are required at a minimum, cell0 and your first cell + if count < 2: + msg = _('Cell mappings are not created, but required for Ocata. ' + 'Please run nova-manage db simple_cell_setup before ' + 'continuing.') + raise exception.ValidationError(detail=msg) + + count = select([func.count()]).select_from(cell_mappings).where( + cell_mappings.c.uuid == objects.CellMapping.CELL0_UUID).scalar() + if count != 1: + msg = _('A mapping for Cell0 was not found, but is required for ' + 'Ocata. Please run nova-manage db simple_cell_setup before ' + 'continuing.') + raise exception.ValidationError(detail=msg) + + host_mappings = Table('host_mappings', meta, autoload=True) + count = select([func.count()]).select_from(host_mappings).scalar() + if count == 0: + msg = _('No host mappings were found, but are required for Ocata. ' + 'Please run nova-manage db simple_cell_setup before ' + 'continuing.') + raise exception.ValidationError(detail=msg) diff --git a/nova/tests/functional/db/api/test_migrations.py b/nova/tests/functional/db/api/test_migrations.py index 13f2717be527..3a9171528583 100644 --- a/nova/tests/functional/db/api/test_migrations.py +++ b/nova/tests/functional/db/api/test_migrations.py @@ -164,7 +164,10 @@ class NovaAPIMigrationsWalk(test_migrations.WalkVersionsMixin): def _skippable_migrations(self): mitaka_placeholders = range(8, 13) newton_placeholders = range(21, 26) - return mitaka_placeholders + newton_placeholders + special_cases = [ + 30, # Enforcement migration, no changes to test + ] + return mitaka_placeholders + newton_placeholders + special_cases def migrate_up(self, version, with_data=False): if with_data: diff --git a/nova/tests/unit/db/test_sqlalchemy_migration.py b/nova/tests/unit/db/test_sqlalchemy_migration.py index 3b8627eda525..c4a8b8650447 100644 --- a/nova/tests/unit/db/test_sqlalchemy_migration.py +++ b/nova/tests/unit/db/test_sqlalchemy_migration.py @@ -365,3 +365,103 @@ class TestOcataCheck(test.TestCase): group = db_api.instance_group_create(self.context, self.ig_values) db_api.instance_group_delete(self.context, group['uuid']) self.migration.upgrade(self.engine) + + +class TestNewtonCellsCheck(test.NoDBTestCase): + USES_DB_SELF = True + + def setUp(self): + super(TestNewtonCellsCheck, self).setUp() + self.useFixture(nova_fixtures.DatabaseAtVersion(28, 'api')) + self.context = context.get_admin_context() + self.migration = importlib.import_module( + 'nova.db.sqlalchemy.api_migrations.migrate_repo.versions.' + '030_require_cell_setup') + self.engine = db_api.get_api_engine() + + @mock.patch('nova.objects.Flavor._ensure_migrated') + def _flavor_me(self, _): + flavor = objects.Flavor(context=self.context, + name='foo', memory_mb=123, + vcpus=1, root_gb=1, + flavorid='m1.foo') + flavor.create() + + def test_upgrade_with_no_cell_mappings(self): + self._flavor_me() + self.assertRaisesRegex(exception.ValidationError, + 'Cell mappings', + self.migration.upgrade, self.engine) + + def test_upgrade_with_only_cell0(self): + self._flavor_me() + cell0 = objects.CellMapping(context=self.context, + uuid=objects.CellMapping.CELL0_UUID, + name='cell0', + transport_url='fake', + database_connection='fake') + cell0.create() + self.assertRaisesRegex(exception.ValidationError, + 'Cell mappings', + self.migration.upgrade, self.engine) + + def test_upgrade_without_cell0(self): + self._flavor_me() + cell1 = objects.CellMapping(context=self.context, + uuid=uuidsentinel.cell1, + name='cell1', + transport_url='fake', + database_connection='fake') + cell1.create() + cell2 = objects.CellMapping(context=self.context, + uuid=uuidsentinel.cell2, + name='cell2', + transport_url='fake', + database_connection='fake') + cell2.create() + self.assertRaisesRegex(exception.ValidationError, + 'Cell0', + self.migration.upgrade, self.engine) + + def test_upgrade_with_no_host_mappings(self): + self._flavor_me() + cell0 = objects.CellMapping(context=self.context, + uuid=objects.CellMapping.CELL0_UUID, + name='cell0', + transport_url='fake', + database_connection='fake') + cell0.create() + cell1 = objects.CellMapping(context=self.context, + uuid=uuidsentinel.cell1, + name='cell1', + transport_url='fake', + database_connection='fake') + cell1.create() + + self.assertRaisesRegex(exception.ValidationError, + 'host mappings', + self.migration.upgrade, self.engine) + + def test_upgrade_with_required_mappings(self): + self._flavor_me() + cell0 = objects.CellMapping(context=self.context, + uuid=objects.CellMapping.CELL0_UUID, + name='cell0', + transport_url='fake', + database_connection='fake') + cell0.create() + cell1 = objects.CellMapping(context=self.context, + uuid=uuidsentinel.cell1, + name='cell1', + transport_url='fake', + database_connection='fake') + cell1.create() + hostmapping = objects.HostMapping(context=self.context, + cell_mapping=cell1, + host='foo') + hostmapping.create() + + self.migration.upgrade(self.engine) + + def test_upgrade_new_deploy(self): + self.migration.upgrade(self.engine) diff --git a/releasenotes/notes/ocata-requires-cellv2-96bd243be874d77f.yaml b/releasenotes/notes/ocata-requires-cellv2-96bd243be874d77f.yaml new file mode 100644 index 000000000000..7e1470034fc6 --- /dev/null +++ b/releasenotes/notes/ocata-requires-cellv2-96bd243be874d77f.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - Ocata requires that your deployment have created the + cell and host mappings in Newton. If you have not done + this, Ocata's `db sync` command will fail. Small deployments + will want to run `nova-manage db simple_cell_setup` + on Newton before upgrading.