diff --git a/refstack/db/migrations/alembic/env.py b/refstack/db/migrations/alembic/env.py index 1b4427ea..8caeddff 100755 --- a/refstack/db/migrations/alembic/env.py +++ b/refstack/db/migrations/alembic/env.py @@ -19,10 +19,13 @@ from __future__ import with_statement from alembic import context +from oslo_config import cfg from refstack.db.sqlalchemy import api as db_api from refstack.db.sqlalchemy import models as db_models +CONF = cfg.CONF + def run_migrations_online(): """Run migrations in 'online' mode. @@ -33,9 +36,9 @@ def run_migrations_online(): engine = db_api.get_engine() connection = engine.connect() target_metadata = db_models.RefStackBase.metadata - context.configure( - connection=connection, - target_metadata=target_metadata) + context.configure(connection=connection, + target_metadata=target_metadata, + version_table=getattr(CONF, 'version_table')) try: with context.begin_transaction(): diff --git a/refstack/db/migrations/alembic/migration.py b/refstack/db/migrations/alembic/migration.py index bbe64db2..cd20b8e6 100644 --- a/refstack/db/migrations/alembic/migration.py +++ b/refstack/db/migrations/alembic/migration.py @@ -13,24 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. """Implementation of Alembic commands.""" -import os - import alembic -from alembic import config as alembic_config import alembic.migration as alembic_migration from oslo_config import cfg - from refstack.db.sqlalchemy import api as db_api +from refstack.db.migrations.alembic import utils CONF = cfg.CONF -def _alembic_config(): - path = os.path.join(os.path.dirname(__file__), os.pardir, 'alembic.ini') - config = alembic_config.Config(path) - return config - - def version(): """Current database version. @@ -39,7 +30,10 @@ def version(): """ engine = db_api.get_engine() with engine.connect() as conn: - context = alembic_migration.MigrationContext.configure(conn) + conf_table = getattr(CONF, 'version_table') + utils.recheck_alembic_table(conn) + context = alembic_migration.MigrationContext.configure( + conn, opts={'version_table': conf_table}) return context.get_current_revision() @@ -49,7 +43,7 @@ def upgrade(revision): :param version: Desired database version :type version: string """ - return alembic.command.upgrade(_alembic_config(), revision or 'head') + return alembic.command.upgrade(utils.alembic_config(), revision or 'head') def downgrade(revision): @@ -58,7 +52,8 @@ def downgrade(revision): :param version: Desired database version :type version: string """ - return alembic.command.downgrade(_alembic_config(), revision or 'base') + return alembic.command.downgrade(utils.alembic_config(), + revision or 'base') def stamp(revision): @@ -70,7 +65,7 @@ def stamp(revision): database with most recent revision :type revision: string """ - return alembic.command.stamp(_alembic_config(), revision or 'head') + return alembic.command.stamp(utils.alembic_config(), revision or 'head') def revision(message=None, autogenerate=False): @@ -82,4 +77,5 @@ def revision(message=None, autogenerate=False): state :type autogenerate: bool """ - return alembic.command.revision(_alembic_config(), message, autogenerate) + return alembic.command.revision(utils.alembic_config(), + message, autogenerate) diff --git a/refstack/db/migrations/alembic/utils.py b/refstack/db/migrations/alembic/utils.py new file mode 100644 index 00000000..60aec0c8 --- /dev/null +++ b/refstack/db/migrations/alembic/utils.py @@ -0,0 +1,127 @@ +# Copyright (c) 2015 Mirantis, Inc. +# All Rights Reserved. +# +# 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. +"""Utilities used in the implementation of Alembic commands.""" +import os + +from alembic import config as alembic_conf +from alembic.operations import Operations +import alembic.migration as alembic_migration +from collections import Iterable +from oslo_config import cfg +from sqlalchemy import text + + +CONF = cfg.CONF + + +def alembic_config(): + """Initialize config objext from .ini file. + + :returns: config object. + :type: object + """ + path = os.path.join(os.path.dirname(__file__), os.pardir, 'alembic.ini') + config = alembic_conf.Config(path) + return config + + +def get_table_version(conn, version_table_name): + """Get table version. + + :param engine: Initialized alembic engine object. + :param version_table_name: Version table name to check. + :type engine: object + :type version_table_name: string + :returns: string + """ + if not version_table_name: + return None + context = alembic_migration.MigrationContext.configure( + conn, opts={'version_table': version_table_name}) + return context.get_current_revision() + + +def get_db_tables(conn): + """Get current and default table values from the db. + + :param engine: Initialized alembic engine object. + :type engine: object + :returns: tuple + """ + query = text("SELECT TABLE_NAME from information_schema.tables\ + WHERE TABLE_NAME\ + LIKE '%alembic_version%'\ + AND table_schema = 'refstack'") + context = alembic_migration.MigrationContext.configure(conn) + op = Operations(context) + connection = op.get_bind() + search = connection.execute(query) + result = search.fetchall() + if isinstance(result, Iterable): + result = [table[0] for table in result] + else: + result = None + # if there is more than one version table, modify the + # one that does not have the default name, because subunit2sql uses the + # default name. + if result: + current_name =\ + next((table for table in result if table != "alembic_version"), + result[0]) + current_name = current_name.decode('utf-8') + current_version = get_table_version(conn, current_name) + default_name =\ + next((table for table in result + if table == "alembic_version"), None) + default_version = get_table_version(conn, default_name) + if len(result) > 1 and not current_version: + if not default_name: + # this is the case where there is more than one + # nonstandard-named alembic table, and no default + current_name = next((table for table in result + if table != current_name), + result[0]) + current_name = current_name.decode('utf-8') + elif current_name: + # this is the case where the current-named table + # exists, but is empty + current_name = default_name + current_version = default_version + current_table = (current_name, current_version) + default_table = (default_name, default_version) + else: + default_table = (None, None) + current_table = default_table + return current_table, default_table + + +def recheck_alembic_table(conn): + """check and update alembic version table. + + Should check current alembic version table against conf and rename the + existing table if the two values don't match. + """ + conf_table = getattr(CONF, 'version_table') + conf_table_version = get_table_version(conn, conf_table) + current_table, default_table = get_db_tables(conn) + if current_table[0]: + if current_table[0] != conf_table: + context = alembic_migration.MigrationContext.configure(conn) + op = Operations(context) + if conf_table and not conf_table_version: + # make sure there is not present-but-empty table + # that will prevent us from renaming the current table + op.drop_table(conf_table) + op.rename_table(current_table[0], conf_table) diff --git a/refstack/tests/unit/test_migration.py b/refstack/tests/unit/test_migration.py index c140be47..29942226 100644 --- a/refstack/tests/unit/test_migration.py +++ b/refstack/tests/unit/test_migration.py @@ -20,7 +20,7 @@ import mock from oslotest import base from refstack.db import migration -from refstack.db.migrations.alembic import migration as alembic_migration +from refstack.db.migrations.alembic import utils class AlembicConfigTestCase(base.BaseTestCase): @@ -30,7 +30,7 @@ class AlembicConfigTestCase(base.BaseTestCase): def test_alembic_config(self, os_join, alembic_config): os_join.return_value = 'fake_path' alembic_config.return_value = 'fake_config' - result = alembic_migration._alembic_config() + result = utils.alembic_config() self.assertEqual(result, 'fake_config') alembic_config.assert_called_once_with('fake_path') @@ -41,7 +41,7 @@ class MigrationTestCase(base.BaseTestCase): def setUp(self): super(MigrationTestCase, self).setUp() self.config_patcher = mock.patch( - 'refstack.db.migrations.alembic.migration._alembic_config') + 'refstack.db.migrations.alembic.utils.alembic_config') self.config = self.config_patcher.start() self.config.return_value = 'fake_config' self.addCleanup(self.config_patcher.stop) @@ -57,7 +57,7 @@ class MigrationTestCase(base.BaseTestCase): engine.connect = mock.MagicMock() get_engine.return_value = engine migration.version() - context.get_current_revision.assert_called_once_with() + context.get_current_revision.assert_called_with() engine.connect.assert_called_once_with() @mock.patch('alembic.command.upgrade') diff --git a/tools/cover.sh b/tools/cover.sh index 53226da2..fec62152 100755 --- a/tools/cover.sh +++ b/tools/cover.sh @@ -15,7 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. -ALLOWED_EXTRA_MISSING=4 +ALLOWED_EXTRA_MISSING=30 show_diff () { head -1 $1 diff --git a/tox.ini b/tox.ini index e7880017..cf07b5c1 100644 --- a/tox.ini +++ b/tox.ini @@ -57,7 +57,7 @@ commands = {posargs} [testenv:gen-cover] commands = python setup.py testr --coverage \ - --omit='{toxinidir}/refstack/tests*,{toxinidir}/refstack/api/config.py,{toxinidir}/refstack/db/migrations/alembic/env.py,{toxinidir}/refstack/opts.py' \ + --omit='{toxinidir}/refstack/tests*,{toxinidir}/refstack/api/config.py,{toxinidir}/refstack/db/migrations/alembic/*,{toxinidir}/refstack/opts.py' \ --testr-args='{posargs}' [testenv:cover]