diff --git a/doc/source/db.rst b/doc/source/db.rst index 770565e7..1e83351b 100644 --- a/doc/source/db.rst +++ b/doc/source/db.rst @@ -29,11 +29,31 @@ The commands should be executed as a subcommand of 'db': Sync the Database ----------------- - glance-manage db sync + glance-manage db sync [VERSION] Place an existing database under migration control and upgrade it to the -specified VERSION. +specified VERSION or to the latest migration level if VERSION is not specified. +.. note:: Prior to Ocata release the database version was a numeric value. + For example: for the Newton release, the latest migration level was ``44``. + Starting with Ocata, database version will be a revision name + corresponding to the latest migration included in the release. For the + Ocata release, there is only one database migration and it is identified + by revision ``ocata01``. So, the database version for Ocata release would + be ``ocata01``. + + However, with the introduction of zero-downtime upgrades, database version + will be a composite version including both expand and contract revisions. + To achieve zero-downtime upgrades, we split the ``ocata01`` migration into + ``ocata_expand01`` and ``ocata_contract01``. During the upgrade process, + the database would initially be marked with ``ocata_expand01`` and + eventually after completing the full upgrade process, the database will be + marked with ``ocata_contract01``. So, instead of one database version, an + operator will see a composite database version that will have both expand + and contract versions. A database will be considered at Ocata version only + when both expand and contract revisions are at the latest revisions + possible. For a successful Ocata rolling upgrade, the database should be + marked with both ``ocata_expand01``, ``ocata_contract01``. Determining the Database Version -------------------------------- @@ -46,11 +66,40 @@ This will print the current migration level of a Glance database. Upgrading an Existing Database ------------------------------ - glance-manage db upgrade + glance-manage db upgrade [VERSION] This will take an existing database and upgrade it to the specified VERSION. +Expanding the Database +---------------------- + + glance-manage db expand + +This will run the expansion phase of a rolling upgrade process. +Database expansion should be run as the first step in the rolling upgrade +process before any new services are started. + + +Migrating the Data +------------------ + + glance-manage db migrate + +This will run the data migrate phase of a rolling upgrade process. +Database migration should be run after database expansion and before +database contraction has been performed. + + +Contracting the Database +------------------------ + + glance-manage db contract + +This will run the contraction phase of a rolling upgrade process. +Database contraction should be run as the last step of the rolling upgrade +process after all old services are upgraded to new ones. + Downgrading an Existing Database -------------------------------- diff --git a/doc/source/man/glancemanage.rst b/doc/source/man/glancemanage.rst index 0ecd2895..9fd9ade3 100644 --- a/doc/source/man/glancemanage.rst +++ b/doc/source/man/glancemanage.rst @@ -57,6 +57,18 @@ COMMANDS Place an existing database under migration control and upgrade it to the specified VERSION. + **db_expand** + Run this command to expand the database as the first step of a rolling + upgrade process. + + **db_migrate** + Run this command to migrate the database as the second step of a + rolling upgrade process. + + **db_contract** + Run this command to contract the database as the last step of a rolling + upgrade process. + **db_export_metadefs [PATH | PREFIX]** Export the metadata definitions into json format. By default the definitions are exported to /etc/glance/metadefs directory. diff --git a/glance/cmd/manage.py b/glance/cmd/manage.py index aa2ec04c..5a854bf8 100644 --- a/glance/cmd/manage.py +++ b/glance/cmd/manage.py @@ -51,6 +51,7 @@ from glance.common import exception from glance import context from glance.db import migration as db_migration from glance.db.sqlalchemy import alembic_migrations +from glance.db.sqlalchemy.alembic_migrations import data_migrations from glance.db.sqlalchemy import api as db_api from glance.db.sqlalchemy import metadata from glance.i18n import _ @@ -88,7 +89,7 @@ class DbCommands(object): 'alembic migration control.')) @args('--version', metavar='', help='Database version') - def upgrade(self, version='heads'): + def upgrade(self, version=db_migration.LATEST_REVISION): """Upgrade the database's migration level""" self.sync(version) @@ -105,12 +106,12 @@ class DbCommands(object): "revision:"), version) @args('--version', metavar='', help='Database version') - def sync(self, version='heads'): + def sync(self, version=db_migration.LATEST_REVISION): """ Place an existing database under migration control and upgrade it. """ if version is None: - version = 'heads' + version = db_migration.LATEST_REVISION alembic_migrations.place_database_under_alembic_control() @@ -118,14 +119,76 @@ class DbCommands(object): alembic_command.upgrade(a_config, version) heads = alembic_migrations.get_current_alembic_heads() if heads is None: - raise Exception("Database sync failed") + raise exception.GlanceException("Database sync failed") revs = ", ".join(heads) - if version is 'heads': + if version == 'heads': print(_("Upgraded database, current revision(s):"), revs) else: print(_('Upgraded database to: %(v)s, current revision(s): %(r)s') % {'v': version, 'r': revs}) + def expand(self): + """Run the expansion phase of a rolling upgrade procedure.""" + expand_head = alembic_migrations.get_alembic_branch_head( + db_migration.EXPAND_BRANCH) + if not expand_head: + sys.exit(_('Database expansion failed. Couldn\'t find head ' + 'revision of expand branch.')) + + self.sync(version=expand_head) + + curr_heads = alembic_migrations.get_current_alembic_heads() + if expand_head not in curr_heads: + sys.exit(_('Database expansion failed. Database expansion should ' + 'have brought the database version up to "%(e_rev)s" ' + 'revision. But, current revisions are: %(curr_revs)s ') + % {'e_rev': expand_head, 'curr_revs': curr_heads}) + + def contract(self): + """Run the contraction phase of a rolling upgrade procedure.""" + contract_head = alembic_migrations.get_alembic_branch_head( + db_migration.CONTRACT_BRANCH) + if not contract_head: + sys.exit(_('Database contraction failed. Couldn\'t find head ' + 'revision of contract branch.')) + + curr_heads = alembic_migrations.get_current_alembic_heads() + expand_head = alembic_migrations.get_alembic_branch_head( + db_migration.EXPAND_BRANCH) + if expand_head not in curr_heads: + sys.exit(_('Database contraction did not run. Database ' + 'contraction cannot be run before database expansion. ' + 'Run database expansion first using ' + '"glance-manage db expand"')) + + if data_migrations.has_pending_migrations(db_api.get_engine()): + sys.exit(_('Database contraction did not run. Database ' + 'contraction cannot be run before data migration is ' + 'complete. Run data migration using "glance-manage db ' + 'migrate".')) + + self.sync(version=contract_head) + + curr_heads = alembic_migrations.get_current_alembic_heads() + if contract_head not in curr_heads: + sys.exit(_('Database contraction failed. Database contraction ' + 'should have brought the database version up to ' + '"%(e_rev)s" revision. But, current revisions are: ' + '%(curr_revs)s ') % {'e_rev': expand_head, + 'curr_revs': curr_heads}) + + def migrate(self): + curr_heads = alembic_migrations.get_current_alembic_heads() + expand_head = alembic_migrations.get_alembic_branch_head( + db_migration.EXPAND_BRANCH) + if expand_head not in curr_heads: + sys.exit(_('Data migration did not run. Data migration cannot be ' + 'run before database expansion. Run database ' + 'expansion first using "glance-manage db expand"')) + + rows_migrated = data_migrations.migrate(db_api.get_engine()) + print(_('Migrated %s rows') % rows_migrated) + @args('--path', metavar='', help='Path to the directory or file ' 'where json metadata is stored') @args('--merge', action='store_true', @@ -198,15 +261,24 @@ class DbLegacyCommands(object): def version(self): self.command_object.version() - def upgrade(self, version='heads'): + def upgrade(self, version=db_migration.LATEST_REVISION): self.command_object.upgrade(CONF.command.version) def version_control(self, version=db_migration.ALEMBIC_INIT_VERSION): self.command_object.version_control(CONF.command.version) - def sync(self, version='heads'): + def sync(self, version=db_migration.LATEST_REVISION): self.command_object.sync(CONF.command.version) + def expand(self): + self.command_object.expand() + + def contract(self): + self.command_object.contract() + + def migrate(self): + self.command_object.migrate() + def load_metadefs(self, path=None, merge=False, prefer_new=False, overwrite=False): self.command_object.load_metadefs(CONF.command.path, @@ -244,6 +316,18 @@ def add_legacy_command_parsers(command_object, subparsers): parser.add_argument('version', nargs='?') parser.set_defaults(action='db_sync') + parser = subparsers.add_parser('db_expand') + parser.set_defaults(action_fn=legacy_command_object.expand) + parser.set_defaults(action='db_expand') + + parser = subparsers.add_parser('db_contract') + parser.set_defaults(action_fn=legacy_command_object.contract) + parser.set_defaults(action='db_contract') + + parser = subparsers.add_parser('db_migrate') + parser.set_defaults(action_fn=legacy_command_object.migrate) + parser.set_defaults(action='db_migrate') + parser = subparsers.add_parser('db_load_metadefs') parser.set_defaults(action_fn=legacy_command_object.load_metadefs) parser.add_argument('path', nargs='?') diff --git a/glance/common/exception.py b/glance/common/exception.py index 4a0fda80..bea56af3 100644 --- a/glance/common/exception.py +++ b/glance/common/exception.py @@ -557,3 +557,9 @@ class InvalidJsonPatchPath(JsonPatchException): def __init__(self, message=None, *args, **kwargs): self.explanation = kwargs.get("explanation") super(InvalidJsonPatchPath, self).__init__(message, *args, **kwargs) + + +class InvalidDataMigrationScript(GlanceException): + message = _("Invalid data migration script '%(script)s'. A valid data " + "migration script must implement functions 'has_migrations' " + "and 'migrate'.") diff --git a/glance/db/migration.py b/glance/db/migration.py index e6f51796..5e1d690a 100644 --- a/glance/db/migration.py +++ b/glance/db/migration.py @@ -43,7 +43,13 @@ def get_backend(): cfg.CONF.database.backend).driver return _IMPL + +# Migration-related constants +EXPAND_BRANCH = 'expand' +CONTRACT_BRANCH = 'contract' +CURRENT_RELEASE = 'ocata' ALEMBIC_INIT_VERSION = 'liberty' +LATEST_REVISION = 'ocata01' INIT_VERSION = 0 MIGRATE_REPO_PATH = os.path.join( diff --git a/glance/db/sqlalchemy/alembic_migrations/__init__.py b/glance/db/sqlalchemy/alembic_migrations/__init__.py index 32476db9..40392629 100644 --- a/glance/db/sqlalchemy/alembic_migrations/__init__.py +++ b/glance/db/sqlalchemy/alembic_migrations/__init__.py @@ -19,6 +19,7 @@ import sys from alembic import command as alembic_command from alembic import config as alembic_config from alembic import migration as alembic_migration +from alembic import script as alembic_script from oslo_db import exception as db_exception from oslo_db.sqlalchemy import migration as sqla_migration @@ -98,3 +99,10 @@ def place_database_under_alembic_control(): print(_("Placing database under Alembic's migration control at " "revision:"), alembic_version) alembic_command.stamp(a_config, alembic_version) + + +def get_alembic_branch_head(branch): + """Return head revision name for particular branch""" + a_config = get_alembic_config() + script = alembic_script.ScriptDirectory.from_config(a_config) + return script.revision_map.get_current_head(branch) diff --git a/glance/db/sqlalchemy/alembic_migrations/data_migrations/__init__.py b/glance/db/sqlalchemy/alembic_migrations/data_migrations/__init__.py new file mode 100644 index 00000000..4d65ff1c --- /dev/null +++ b/glance/db/sqlalchemy/alembic_migrations/data_migrations/__init__.py @@ -0,0 +1,70 @@ +# Copyright 2016 Rackspace +# Copyright 2016 Intel Corporation +# +# 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 importlib +import os.path +import pkgutil + +from glance.common import exception +from glance.db import migration as db_migrations +from glance.db.sqlalchemy import api as db_api + + +def _find_migration_modules(release): + migrations = list() + for _, module_name, _ in pkgutil.iter_modules([os.path.dirname(__file__)]): + if module_name.startswith(release): + migrations.append(module_name) + + migration_modules = list() + for migration in sorted(migrations): + module = importlib.import_module('.'.join([__package__, migration])) + has_migrations_function = getattr(module, 'has_migrations', None) + migrate_function = getattr(module, 'migrate', None) + + if has_migrations_function is None or migrate_function is None: + raise exception.InvalidDataMigrationScript(script=module.__name__) + + migration_modules.append(module) + + return migration_modules + + +def _run_migrations(engine, migrations): + rows_migrated = 0 + for migration in migrations: + if migration.has_migrations(engine): + rows_migrated += migration.migrate(engine) + + return rows_migrated + + +def has_pending_migrations(engine=None): + if not engine: + engine = db_api.get_engine() + + migrations = _find_migration_modules(db_migrations.CURRENT_RELEASE) + if not migrations: + return False + return any([x.has_migrations(engine) for x in migrations]) + + +def migrate(engine=None): + if not engine: + engine = db_api.get_engine() + + migrations = _find_migration_modules(db_migrations.CURRENT_RELEASE) + rows_migrated = _run_migrations(engine, migrations) + return rows_migrated diff --git a/glance/tests/functional/db/test_migrations.py b/glance/tests/functional/db/test_migrations.py index 2621ed34..0d596ade 100644 --- a/glance/tests/functional/db/test_migrations.py +++ b/glance/tests/functional/db/test_migrations.py @@ -23,6 +23,7 @@ from oslo_db.sqlalchemy import test_base from oslo_db.sqlalchemy import test_migrations import sqlalchemy.types as types +from glance.db import migration as dm from glance.db.sqlalchemy import alembic_migrations from glance.db.sqlalchemy.alembic_migrations import versions from glance.db.sqlalchemy import models @@ -35,7 +36,8 @@ class AlembicMigrationsMixin(object): def _get_revisions(self, config): scripts_dir = alembic_script.ScriptDirectory.from_config(config) - revisions = list(scripts_dir.walk_revisions(base='base', head='heads')) + revisions = list(scripts_dir.walk_revisions(base='base', + head=dm.LATEST_REVISION)) revisions = list(reversed(revisions)) revisions = [rev.revision for rev in revisions] return revisions diff --git a/glance/tests/functional/test_glance_manage.py b/glance/tests/functional/test_glance_manage.py index c2b5a962..0ce9310d 100644 --- a/glance/tests/functional/test_glance_manage.py +++ b/glance/tests/functional/test_glance_manage.py @@ -47,28 +47,20 @@ class TestGlanceManage(functional.FunctionalTest): (sys.executable, self.conf_filepath)) execute(cmd, raise_error=True) - def _assert_tables(self): - cmd = "sqlite3 %s '.schema'" % self.db_filepath + def _assert_table_exists(self, db_table): + cmd = ("sqlite3 {0} \"SELECT name FROM sqlite_master WHERE " + "type='table' AND name='{1}'\"").format(self.db_filepath, + db_table) exitcode, out, err = execute(cmd, raise_error=True) - - self.assertIn('CREATE TABLE images', out) - self.assertIn('CREATE TABLE image_tags', out) - self.assertIn('CREATE TABLE image_locations', out) - - # NOTE(bcwaldon): For some reason we need double-quotes around - # these two table names - # NOTE(vsergeyev): There are some cases when we have no double-quotes - self.assertTrue( - 'CREATE TABLE "image_members"' in out or - 'CREATE TABLE image_members' in out) - self.assertTrue( - 'CREATE TABLE "image_properties"' in out or - 'CREATE TABLE image_properties' in out) + msg = "Expected table {0} was not found in the schema".format(db_table) + self.assertEqual(out.rstrip(), db_table, msg) @depends_on_exe('sqlite3') @skip_if_disabled def test_db_creation(self): - """Test DB creation by db_sync on a fresh DB""" + """Test schema creation by db_sync on a fresh DB""" self._sync_db() - self._assert_tables() + for table in ['images', 'image_tags', 'image_locations', + 'image_members', 'image_properties']: + self._assert_table_exists(table) diff --git a/glance/tests/unit/test_data_migration_framework.py b/glance/tests/unit/test_data_migration_framework.py new file mode 100644 index 00000000..c5b3d470 --- /dev/null +++ b/glance/tests/unit/test_data_migration_framework.py @@ -0,0 +1,204 @@ +# 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 mock + +from glance.db.sqlalchemy.alembic_migrations import data_migrations +from glance.tests import utils as test_utils + + +class TestDataMigrationFramework(test_utils.BaseTestCase): + + @mock.patch('glance.db.sqlalchemy.alembic_migrations.data_migrations' + '._find_migration_modules') + def test_has_pending_migrations_no_migrations(self, mock_find): + mock_find.return_value = None + self.assertFalse(data_migrations.has_pending_migrations(mock.Mock())) + + @mock.patch('glance.db.sqlalchemy.alembic_migrations.data_migrations' + '._find_migration_modules') + def test_has_pending_migrations_one_migration_no_pending(self, mock_find): + mock_migration1 = mock.Mock() + mock_migration1.has_migrations.return_value = False + mock_find.return_value = [mock_migration1] + + self.assertFalse(data_migrations.has_pending_migrations(mock.Mock())) + + @mock.patch('glance.db.sqlalchemy.alembic_migrations.data_migrations' + '._find_migration_modules') + def test_has_pending_migrations_one_migration_with_pending(self, + mock_find): + mock_migration1 = mock.Mock() + mock_migration1.has_migrations.return_value = True + mock_find.return_value = [mock_migration1] + + self.assertTrue(data_migrations.has_pending_migrations(mock.Mock())) + + @mock.patch('glance.db.sqlalchemy.alembic_migrations.data_migrations' + '._find_migration_modules') + def test_has_pending_migrations_mult_migration_no_pending(self, mock_find): + mock_migration1 = mock.Mock() + mock_migration1.has_migrations.return_value = False + mock_migration2 = mock.Mock() + mock_migration2.has_migrations.return_value = False + mock_migration3 = mock.Mock() + mock_migration3.has_migrations.return_value = False + + mock_find.return_value = [mock_migration1, mock_migration2, + mock_migration3] + + self.assertFalse(data_migrations.has_pending_migrations(mock.Mock())) + + @mock.patch('glance.db.sqlalchemy.alembic_migrations.data_migrations' + '._find_migration_modules') + def test_has_pending_migrations_mult_migration_one_pending(self, + mock_find): + mock_migration1 = mock.Mock() + mock_migration1.has_migrations.return_value = False + mock_migration2 = mock.Mock() + mock_migration2.has_migrations.return_value = True + mock_migration3 = mock.Mock() + mock_migration3.has_migrations.return_value = False + + mock_find.return_value = [mock_migration1, mock_migration2, + mock_migration3] + + self.assertTrue(data_migrations.has_pending_migrations(mock.Mock())) + + @mock.patch('glance.db.sqlalchemy.alembic_migrations.data_migrations' + '._find_migration_modules') + def test_has_pending_migrations_mult_migration_some_pending(self, + mock_find): + mock_migration1 = mock.Mock() + mock_migration1.has_migrations.return_value = False + mock_migration2 = mock.Mock() + mock_migration2.has_migrations.return_value = True + mock_migration3 = mock.Mock() + mock_migration3.has_migrations.return_value = False + mock_migration4 = mock.Mock() + mock_migration4.has_migrations.return_value = True + + mock_find.return_value = [mock_migration1, mock_migration2, + mock_migration3, mock_migration4] + + self.assertTrue(data_migrations.has_pending_migrations(mock.Mock())) + + @mock.patch('importlib.import_module') + @mock.patch('pkgutil.iter_modules') + def test_find_migrations(self, mock_iter, mock_import): + def fake_iter_modules(blah): + yield 'blah', 'ocata01', 'blah' + yield 'blah', 'ocata02', 'blah' + yield 'blah', 'pike01', 'blah' + yield 'blah', 'newton', 'blah' + yield 'blah', 'mitaka456', 'blah' + + mock_iter.side_effect = fake_iter_modules + + ocata1 = mock.Mock() + ocata1.has_migrations.return_value = mock.Mock() + ocata1.migrate.return_value = mock.Mock() + ocata2 = mock.Mock() + ocata2.has_migrations.return_value = mock.Mock() + ocata2.migrate.return_value = mock.Mock() + + fake_imported_modules = [ocata1, ocata2] + mock_import.side_effect = fake_imported_modules + + actual = data_migrations._find_migration_modules('ocata') + self.assertEqual(2, len(actual)) + self.assertEqual(fake_imported_modules, actual) + + @mock.patch('pkgutil.iter_modules') + def test_find_migrations_no_migrations(self, mock_iter): + def fake_iter_modules(blah): + yield 'blah', 'liberty01', 'blah' + yield 'blah', 'kilo01', 'blah' + yield 'blah', 'mitaka01', 'blah' + yield 'blah', 'newton01', 'blah' + yield 'blah', 'pike01', 'blah' + + mock_iter.side_effect = fake_iter_modules + + actual = data_migrations._find_migration_modules('ocata') + self.assertEqual(0, len(actual)) + self.assertEqual([], actual) + + def test_run_migrations(self): + ocata1 = mock.Mock() + ocata1.has_migrations.return_value = True + ocata1.migrate.return_value = 100 + ocata2 = mock.Mock() + ocata2.has_migrations.return_value = True + ocata2.migrate.return_value = 50 + migrations = [ocata1, ocata2] + + engine = mock.Mock() + actual = data_migrations._run_migrations(engine, migrations) + self.assertEqual(150, actual) + ocata1.has_migrations.assert_called_once_with(engine) + ocata1.migrate.assert_called_once_with(engine) + ocata2.has_migrations.assert_called_once_with(engine) + ocata2.migrate.assert_called_once_with(engine) + + def test_run_migrations_with_one_pending_migration(self): + ocata1 = mock.Mock() + ocata1.has_migrations.return_value = False + ocata1.migrate.return_value = 0 + ocata2 = mock.Mock() + ocata2.has_migrations.return_value = True + ocata2.migrate.return_value = 50 + migrations = [ocata1, ocata2] + + engine = mock.Mock() + actual = data_migrations._run_migrations(engine, migrations) + self.assertEqual(50, actual) + ocata1.has_migrations.assert_called_once_with(engine) + ocata1.migrate.assert_not_called() + ocata2.has_migrations.assert_called_once_with(engine) + ocata2.migrate.assert_called_once_with(engine) + + def test_run_migrations_with_no_migrations(self): + migrations = [] + + actual = data_migrations._run_migrations(mock.Mock(), migrations) + self.assertEqual(0, actual) + + @mock.patch('importlib.import_module') + @mock.patch('pkgutil.iter_modules') + def test_migrate(self, mock_iter, mock_import): + def fake_iter_modules(blah): + yield 'blah', 'ocata01', 'blah' + yield 'blah', 'ocata02', 'blah' + yield 'blah', 'pike01', 'blah' + yield 'blah', 'newton', 'blah' + yield 'blah', 'mitaka456', 'blah' + + mock_iter.side_effect = fake_iter_modules + + ocata1 = mock.Mock() + ocata1.has_migrations.return_value = True + ocata1.migrate.return_value = 100 + ocata2 = mock.Mock() + ocata2.has_migrations.return_value = True + ocata2.migrate.return_value = 50 + + fake_imported_modules = [ocata1, ocata2] + mock_import.side_effect = fake_imported_modules + + engine = mock.Mock() + actual = data_migrations.migrate(engine) + self.assertEqual(150, actual) + ocata1.has_migrations.assert_called_once_with(engine) + ocata1.migrate.assert_called_once_with(engine) + ocata2.has_migrations.assert_called_once_with(engine) + ocata2.migrate.assert_called_once_with(engine) diff --git a/glance/tests/unit/test_manage.py b/glance/tests/unit/test_manage.py index 0211e10e..80b0c670 100644 --- a/glance/tests/unit/test_manage.py +++ b/glance/tests/unit/test_manage.py @@ -78,6 +78,21 @@ class TestLegacyManage(TestManageBase): self._main_test_helper(['glance.cmd.manage', 'db_upgrade', 'liberty'], manage.DbCommands.upgrade, 'liberty') + @mock.patch.object(manage.DbCommands, 'expand') + def test_legacy_db_expand(self, db_expand): + self._main_test_helper(['glance.cmd.manage', 'db_expand'], + manage.DbCommands.expand) + + @mock.patch.object(manage.DbCommands, 'migrate') + def test_legacy_db_migrate(self, db_migrate): + self._main_test_helper(['glance.cmd.manage', 'db_migrate'], + manage.DbCommands.migrate) + + @mock.patch.object(manage.DbCommands, 'contract') + def test_legacy_db_contract(self, db_contract): + self._main_test_helper(['glance.cmd.manage', 'db_contract'], + manage.DbCommands.contract) + def test_db_metadefs_unload(self): db_metadata.db_unload_metadefs = mock.Mock() self._main_test_helper(['glance.cmd.manage', 'db_unload_metadefs'], @@ -172,6 +187,21 @@ class TestManage(TestManageBase): 'upgrade', 'liberty'], manage.DbCommands.upgrade, 'liberty') + @mock.patch.object(manage.DbCommands, 'expand') + def test_db_expand(self, expand): + self._main_test_helper(['glance.cmd.manage', 'db', 'expand'], + manage.DbCommands.expand) + + @mock.patch.object(manage.DbCommands, 'migrate') + def test_db_migrate(self, migrate): + self._main_test_helper(['glance.cmd.manage', 'db', 'migrate'], + manage.DbCommands.migrate) + + @mock.patch.object(manage.DbCommands, 'contract') + def test_db_contract(self, contract): + self._main_test_helper(['glance.cmd.manage', 'db', 'contract'], + manage.DbCommands.contract) + def test_db_metadefs_unload(self): db_metadata.db_unload_metadefs = mock.Mock() self._main_test_helper(['glance.cmd.manage', 'db', 'unload_metadefs'], diff --git a/glance/tests/utils.py b/glance/tests/utils.py index 48897ade..61472721 100644 --- a/glance/tests/utils.py +++ b/glance/tests/utils.py @@ -43,6 +43,7 @@ from glance.common import timeutils from glance.common import utils from glance.common import wsgi from glance import context +from glance.db import migration as db_migration from glance.db.sqlalchemy import alembic_migrations from glance.db.sqlalchemy import api as db_api from glance.db.sqlalchemy import models as db_models @@ -677,7 +678,7 @@ class HttplibWsgiAdapter(object): def db_sync(version=None, engine=None): """Migrate the database to `version` or the most recent version.""" if version is None: - version = 'heads' + version = db_migration.LATEST_REVISION if engine is None: engine = db_api.get_engine()