Merge "Add db migrations test framework with data"
This commit is contained in:
commit
bb7917f476
14
neutron/tests/functional/db/test_migrations.conf
Normal file
14
neutron/tests/functional/db/test_migrations.conf
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[migration_dbs]
|
||||||
|
# Migration DB details are listed separately as they can't be connected to
|
||||||
|
# concurrently. These databases can't be the same as above
|
||||||
|
|
||||||
|
# Note, sqlite:// is in-memory and unique each time it is spawned.
|
||||||
|
# However file sqlite's are not unique.
|
||||||
|
|
||||||
|
#sqlite=sqlite://
|
||||||
|
#sqlitefile=sqlite:///test_migrations.db
|
||||||
|
#mysql=mysql+mysqldb://user:pass@localhost/test_migrations
|
||||||
|
#postgresql=postgresql+psycopg2://user:pass@localhost/test_migrations
|
||||||
|
|
||||||
|
[walk_style]
|
||||||
|
snake_walk=yes
|
@ -12,21 +12,29 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import abc
|
||||||
from alembic import script as alembic_script
|
from alembic import script as alembic_script
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
import os
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_config import fixture as config_fixture
|
from oslo_config import fixture as config_fixture
|
||||||
|
from oslo_db.sqlalchemy import session
|
||||||
from oslo_db.sqlalchemy import test_base
|
from oslo_db.sqlalchemy import test_base
|
||||||
from oslo_db.sqlalchemy import test_migrations
|
from oslo_db.sqlalchemy import test_migrations
|
||||||
|
from oslo_db.sqlalchemy import utils as oslo_utils
|
||||||
import six
|
import six
|
||||||
|
from six.moves import configparser
|
||||||
|
from six.moves.urllib import parse
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event
|
||||||
import sqlalchemy.types as types
|
import sqlalchemy.types as types
|
||||||
|
import subprocess
|
||||||
|
|
||||||
import neutron.db.migration as migration_help
|
import neutron.db.migration as migration_help
|
||||||
from neutron.db.migration.alembic_migrations import external
|
from neutron.db.migration.alembic_migrations import external
|
||||||
from neutron.db.migration import cli as migration
|
from neutron.db.migration import cli as migration
|
||||||
from neutron.db.migration.models import head as head_models
|
from neutron.db.migration.models import head as head_models
|
||||||
|
from neutron.tests import base as base_tests
|
||||||
from neutron.tests.common import base
|
from neutron.tests.common import base
|
||||||
|
|
||||||
cfg.CONF.import_opt('core_plugin', 'neutron.common.config')
|
cfg.CONF.import_opt('core_plugin', 'neutron.common.config')
|
||||||
@ -326,10 +334,10 @@ class TestSanityCheck(test_base.DbTestCase):
|
|||||||
script.check_sanity, conn)
|
script.check_sanity, conn)
|
||||||
|
|
||||||
|
|
||||||
class TestWalkMigrations(test_base.DbTestCase):
|
class TestWalkDowngrade(test_base.DbTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestWalkMigrations, self).setUp()
|
super(TestWalkDowngrade, self).setUp()
|
||||||
self.alembic_config = migration.get_neutron_config()
|
self.alembic_config = migration.get_neutron_config()
|
||||||
self.alembic_config.neutron_config = cfg.CONF
|
self.alembic_config.neutron_config = cfg.CONF
|
||||||
|
|
||||||
@ -345,3 +353,262 @@ class TestWalkMigrations(test_base.DbTestCase):
|
|||||||
|
|
||||||
if failed_revisions:
|
if failed_revisions:
|
||||||
self.fail('Migrations %s have downgrade' % failed_revisions)
|
self.fail('Migrations %s have downgrade' % failed_revisions)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_backend_avail(backend,
|
||||||
|
user="openstack_citest",
|
||||||
|
passwd="openstack_citest",
|
||||||
|
database="openstack_citest"):
|
||||||
|
# is_backend_avail will be soon deprecated from oslo_db
|
||||||
|
# thats why its added here
|
||||||
|
try:
|
||||||
|
connect_uri = oslo_utils.get_connect_string(backend, user=user,
|
||||||
|
passwd=passwd,
|
||||||
|
database=database)
|
||||||
|
engine = session.create_engine(connect_uri)
|
||||||
|
connection = engine.connect()
|
||||||
|
except Exception:
|
||||||
|
# intentionally catch all to handle exceptions even if we don't
|
||||||
|
# have any backend code loaded.
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
connection.close()
|
||||||
|
engine.dispose()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class _TestWalkMigrations(base_tests.BaseTestCase):
|
||||||
|
'''This will add framework for testing schema migarations
|
||||||
|
for different backends.
|
||||||
|
|
||||||
|
Right now it supports pymysql and postgresql backends. Pymysql
|
||||||
|
and postgresql commands are executed to walk between to do updates.
|
||||||
|
For upgrade and downgrade migrate_up and migrate down functions
|
||||||
|
have been added.
|
||||||
|
'''
|
||||||
|
|
||||||
|
DEFAULT_CONFIG_FILE = os.path.join(os.path.dirname(__file__),
|
||||||
|
'test_migrations.conf')
|
||||||
|
CONFIG_FILE_PATH = os.environ.get('NEUTRON_TEST_MIGRATIONS_CONF',
|
||||||
|
DEFAULT_CONFIG_FILE)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
if not _is_backend_avail(self.BACKEND):
|
||||||
|
self.skipTest("%s not available" % self.BACKEND)
|
||||||
|
|
||||||
|
super(_TestWalkMigrations, self).setUp()
|
||||||
|
|
||||||
|
self.snake_walk = False
|
||||||
|
self.test_databases = {}
|
||||||
|
|
||||||
|
if os.path.exists(self.CONFIG_FILE_PATH):
|
||||||
|
cp = configparser.RawConfigParser()
|
||||||
|
try:
|
||||||
|
cp.read(self.CONFIG_FILE_PATH)
|
||||||
|
options = cp.options('migration_dbs')
|
||||||
|
for key in options:
|
||||||
|
self.test_databases[key] = cp.get('migration_dbs', key)
|
||||||
|
self.snake_walk = cp.getboolean('walk_style', 'snake_walk')
|
||||||
|
except configparser.ParsingError as e:
|
||||||
|
self.fail("Failed to read test_migrations.conf config "
|
||||||
|
"file. Got error: %s" % e)
|
||||||
|
else:
|
||||||
|
self.fail("Failed to find test_migrations.conf config "
|
||||||
|
"file.")
|
||||||
|
|
||||||
|
self.engines = {}
|
||||||
|
for key, value in self.test_databases.items():
|
||||||
|
self.engines[key] = sqlalchemy.create_engine(value)
|
||||||
|
|
||||||
|
# We start each test case with a completely blank slate.
|
||||||
|
self._reset_databases()
|
||||||
|
|
||||||
|
def assertColumnInTable(self, engine, table_name, column):
|
||||||
|
table = oslo_utils.get_table(engine, table_name)
|
||||||
|
self.assertIn(column, table.columns)
|
||||||
|
|
||||||
|
def assertColumnNotInTables(self, engine, table_name, column):
|
||||||
|
table = oslo_utils.get_table(engine, table_name)
|
||||||
|
self.assertNotIn(column, table.columns)
|
||||||
|
|
||||||
|
def execute_cmd(self, cmd=None):
|
||||||
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT, shell=True)
|
||||||
|
output = proc.communicate()[0]
|
||||||
|
self.assertEqual(0, proc.returncode, 'Command failed with '
|
||||||
|
'output:\n%s' % output)
|
||||||
|
|
||||||
|
@abc.abstractproperty
|
||||||
|
def BACKEND(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _database_recreate(self, user, password, database, host):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _reset_databases(self):
|
||||||
|
for key, engine in self.engines.items():
|
||||||
|
conn_string = self.test_databases[key]
|
||||||
|
conn_pieces = parse.urlparse(conn_string)
|
||||||
|
engine.dispose()
|
||||||
|
user, password, database, host = oslo_utils.get_db_connection_info(
|
||||||
|
conn_pieces)
|
||||||
|
self._database_recreate(user, password, database, host)
|
||||||
|
|
||||||
|
def _get_alembic_config(self, uri):
|
||||||
|
db_config = migration.get_neutron_config()
|
||||||
|
self.script_dir = alembic_script.ScriptDirectory.from_config(db_config)
|
||||||
|
db_config.neutron_config = cfg.CONF
|
||||||
|
db_config.neutron_config.set_override('connection',
|
||||||
|
six.text_type(uri),
|
||||||
|
group='database')
|
||||||
|
return db_config
|
||||||
|
|
||||||
|
def _revisions(self, downgrade=False):
|
||||||
|
"""Provides revisions and its parent revisions.
|
||||||
|
|
||||||
|
:param downgrade: whether to include downgrade behavior or not.
|
||||||
|
:type downgrade: Bool
|
||||||
|
:return: List of tuples. Every tuple contains revision and its parent
|
||||||
|
revision.
|
||||||
|
"""
|
||||||
|
revisions = list(self.script_dir.walk_revisions("base", "heads"))
|
||||||
|
if not downgrade:
|
||||||
|
revisions = list(reversed(revisions))
|
||||||
|
|
||||||
|
for rev in revisions:
|
||||||
|
if downgrade:
|
||||||
|
# Destination, current
|
||||||
|
yield rev.down_revision, rev.revision
|
||||||
|
else:
|
||||||
|
# Destination, current
|
||||||
|
yield rev.revision, rev.down_revision
|
||||||
|
|
||||||
|
def _walk_versions(self, config, engine, downgrade=True, snake_walk=False):
|
||||||
|
"""Test migrations ability to upgrade and downgrade.
|
||||||
|
|
||||||
|
:param downgrade: whether to include downgrade behavior or not.
|
||||||
|
:type downgrade: Bool
|
||||||
|
:snake_walk: enable mode when at every upgrade revision will be
|
||||||
|
downgraded and upgraded in previous state at upgrade and backward at
|
||||||
|
downgrade.
|
||||||
|
:type snake_walk: Bool
|
||||||
|
"""
|
||||||
|
|
||||||
|
revisions = self._revisions()
|
||||||
|
for dest, curr in revisions:
|
||||||
|
self._migrate_up(config, engine, dest, curr, with_data=True)
|
||||||
|
|
||||||
|
if snake_walk and dest != 'None':
|
||||||
|
# NOTE(I159): Pass reversed arguments into `_migrate_down`
|
||||||
|
# method because we have been upgraded to a destination
|
||||||
|
# revision and now we going to downgrade back.
|
||||||
|
self._migrate_down(config, engine, curr, dest, with_data=True)
|
||||||
|
self._migrate_up(config, engine, dest, curr, with_data=True)
|
||||||
|
|
||||||
|
if downgrade:
|
||||||
|
revisions = self._revisions(downgrade)
|
||||||
|
for dest, curr in revisions:
|
||||||
|
self._migrate_down(config, engine, dest, curr, with_data=True)
|
||||||
|
if snake_walk:
|
||||||
|
self._migrate_up(config, engine, curr,
|
||||||
|
dest, with_data=True)
|
||||||
|
self._migrate_down(config, engine, dest,
|
||||||
|
curr, with_data=True)
|
||||||
|
|
||||||
|
def _migrate_down(self, config, engine, dest, curr, with_data=False):
|
||||||
|
# First upgrade it to current to do downgrade
|
||||||
|
if dest:
|
||||||
|
migration.do_alembic_command(config, 'downgrade', dest)
|
||||||
|
else:
|
||||||
|
meta = sqlalchemy.MetaData(bind=engine)
|
||||||
|
meta.drop_all()
|
||||||
|
|
||||||
|
if with_data:
|
||||||
|
post_downgrade = getattr(
|
||||||
|
self, "_post_downgrade_%s" % curr, None)
|
||||||
|
if post_downgrade:
|
||||||
|
post_downgrade(engine)
|
||||||
|
|
||||||
|
def _migrate_up(self, config, engine, dest, curr, with_data=False):
|
||||||
|
if with_data:
|
||||||
|
data = None
|
||||||
|
pre_upgrade = getattr(
|
||||||
|
self, "_pre_upgrade_%s" % dest, None)
|
||||||
|
if pre_upgrade:
|
||||||
|
data = pre_upgrade(engine)
|
||||||
|
migration.do_alembic_command(config, 'upgrade', dest)
|
||||||
|
if with_data:
|
||||||
|
check = getattr(self, "_check_%s" % dest, None)
|
||||||
|
if check and data:
|
||||||
|
check(engine, data)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWalkMigrationsMysql(_TestWalkMigrations):
|
||||||
|
|
||||||
|
BACKEND = 'mysql+pymysql'
|
||||||
|
|
||||||
|
def _database_recreate(self, user, password, database, host):
|
||||||
|
# We can execute the MySQL client to destroy and re-create
|
||||||
|
# the MYSQL database, which is easier and less error-prone
|
||||||
|
# than using SQLAlchemy to do this via MetaData...trust me.
|
||||||
|
sql = ("drop database if exists %(database)s; create "
|
||||||
|
"database %(database)s;") % {'database': database}
|
||||||
|
cmd = ("mysql -u \"%(user)s\" -p%(password)s -h %(host)s "
|
||||||
|
"-e \"%(sql)s\"") % {'user': user, 'password': password,
|
||||||
|
'host': host, 'sql': sql}
|
||||||
|
self.execute_cmd(cmd)
|
||||||
|
|
||||||
|
def test_mysql_opportunistically(self):
|
||||||
|
connect_string = oslo_utils.get_connect_string(self.BACKEND,
|
||||||
|
"openstack_citest", user="openstack_citest",
|
||||||
|
passwd="openstack_citest")
|
||||||
|
engine = session.create_engine(connect_string)
|
||||||
|
config = self._get_alembic_config(connect_string)
|
||||||
|
self.engines["mysqlcitest"] = engine
|
||||||
|
self.test_databases["mysqlcitest"] = connect_string
|
||||||
|
|
||||||
|
# build a fully populated mysql database with all the tables
|
||||||
|
self._reset_databases()
|
||||||
|
self._walk_versions(config, engine, False, False)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWalkMigrationsPsql(_TestWalkMigrations):
|
||||||
|
|
||||||
|
BACKEND = 'postgresql'
|
||||||
|
|
||||||
|
def _database_recreate(self, user, password, database, host):
|
||||||
|
os.environ['PGPASSWORD'] = password
|
||||||
|
os.environ['PGUSER'] = user
|
||||||
|
# note(boris-42): We must create and drop database, we can't
|
||||||
|
# drop database which we have connected to, so for such
|
||||||
|
# operations there is a special database template1.
|
||||||
|
sqlcmd = ("psql -w -U %(user)s -h %(host)s -c"
|
||||||
|
" '%(sql)s' -d template1")
|
||||||
|
sql = "drop database if exists %(database)s;"
|
||||||
|
sql = sql % {'database': database}
|
||||||
|
droptable = sqlcmd % {'user': user, 'host': host,
|
||||||
|
'sql': sql}
|
||||||
|
self.execute_cmd(droptable)
|
||||||
|
sql = "create database %(database)s;"
|
||||||
|
sql = sql % {'database': database}
|
||||||
|
createtable = sqlcmd % {'user': user, 'host': host,
|
||||||
|
'sql': sql}
|
||||||
|
self.execute_cmd(createtable)
|
||||||
|
|
||||||
|
def test_postgresql_opportunistically(self):
|
||||||
|
# add this to the global lists to make reset work with it, it's removed
|
||||||
|
# automatically in tearDown so no need to clean it up here.
|
||||||
|
connect_string = oslo_utils.get_connect_string(self.BACKEND,
|
||||||
|
"openstack_citest",
|
||||||
|
"openstack_citest",
|
||||||
|
"openstack_citest")
|
||||||
|
engine = session.create_engine(connect_string)
|
||||||
|
config = self._get_alembic_config(connect_string)
|
||||||
|
self.engines["postgresqlcitest"] = engine
|
||||||
|
self.test_databases["postgresqlcitest"] = connect_string
|
||||||
|
|
||||||
|
# build a fully populated postgresql database with all the tables
|
||||||
|
self._reset_databases()
|
||||||
|
self._walk_versions(config, engine, False, False)
|
||||||
|
Loading…
Reference in New Issue
Block a user