Add name scheme update script for alembic version table

The goal of this patch is to buil in the ability to upgrade
the alembic version table of both new and existing RefStack
databases in order to allow for the usage of subunit2sql tooling
within the existing db, thus facilitating the upload and storage
of subunit data in RefStack server.

[TEST CASES HANDLED]
* default name  -> nondefault name
* nondefault name -> default name
* nondefault -> another nondefault

[OTHER ACTION ITEMS]
* tighten up workflow for cleaner code

Change-Id: I7aa0965b22a46439d66c81108fc0b8947316579d
This commit is contained in:
Megan Guiney 2017-08-27 19:51:47 -07:00 committed by megan guiney
parent 5f94bbe0d8
commit 3f3e80d7e6
6 changed files with 150 additions and 24 deletions

View File

@ -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():

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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]