Modify sql banned operations for each of the new repos

This patch covers all the regular table and column operations
across the four repos. It does not, however, check for triggers
and indexes - which will be done in a separate patch.

Limitations: Due to the fact that migrating versions causes an
implicit table update (to increase the version number) we don't
yet include checking agains inappropriate table updates of our
own tables in the expand and data migration phases.

Partial-Bug: #1615024
Change-Id: Ia012c614b9a5b9af6b8bb447b39a9901caaf1fb5
This commit is contained in:
Henry Nash 2016-08-20 11:57:30 +01:00
parent 5caf5212d8
commit 743e1102c8
1 changed files with 202 additions and 100 deletions

View File

@ -16,12 +16,15 @@
import os
import fixtures
from migrate.versioning import api as versioning_api
from migrate.versioning import repository
from oslo_db.sqlalchemy import test_base
from oslo_db.sqlalchemy import test_migrations
import sqlalchemy
import testtools
from keystone.common.sql import contract_repo
from keystone.common.sql import data_migration_repo
from keystone.common.sql import expand_repo
from keystone.common.sql import migrate_repo
from keystone.common.sql import migration_helpers
@ -34,57 +37,114 @@ class DBOperationNotAllowed(Exception):
class BannedDBSchemaOperations(fixtures.Fixture):
"""Ban some operations for migrations."""
def __init__(self, banned_resources=None):
def __init__(self, banned_ops=None,
migration_repo=migrate_repo.__file__):
super(BannedDBSchemaOperations, self).__init__()
self._banned_resources = banned_resources or []
self._banned_ops = banned_ops or {}
self._migration_repo = migration_repo
@staticmethod
def _explode(resource, op):
def _explode(resource_op, repo):
# Extract the repo name prior to the trailing '/__init__.py'
repo_name = repo.split('/')[-2]
raise DBOperationNotAllowed(
'Operation %s.%s() is not allowed in a database migration' % (
resource, op))
'Operation %s() is not allowed in %s database migrations' % (
resource_op, repo_name))
def setUp(self):
super(BannedDBSchemaOperations, self).setUp()
for resource in self._banned_resources:
self.useFixture(fixtures.MonkeyPatch(
'sqlalchemy.%s.drop' % resource,
lambda *a, **k: self._explode(resource, 'drop')))
self.useFixture(fixtures.MonkeyPatch(
'sqlalchemy.%s.alter' % resource,
lambda *a, **k: self._explode(resource, 'alter')))
explode_lambda = {
'Table.create': lambda *a, **k: self._explode(
'Table.create', self._migration_repo),
'Table.alter': lambda *a, **k: self._explode(
'Table.alter', self._migration_repo),
'Table.drop': lambda *a, **k: self._explode(
'Table.drop', self._migration_repo),
'Table.insert': lambda *a, **k: self._explode(
'Table.insert', self._migration_repo),
'Table.update': lambda *a, **k: self._explode(
'Table.update', self._migration_repo),
'Table.delete': lambda *a, **k: self._explode(
'Table.delete', self._migration_repo),
'Column.create': lambda *a, **k: self._explode(
'Column.create', self._migration_repo),
'Column.alter': lambda *a, **k: self._explode(
'Column.alter', self._migration_repo),
'Column.drop': lambda *a, **k: self._explode(
'Column.drop', self._migration_repo)
}
for resource in self._banned_ops:
for op in self._banned_ops[resource]:
resource_op = '%(resource)s.%(op)s' % {
'resource': resource, 'op': op}
self.useFixture(fixtures.MonkeyPatch(
'sqlalchemy.%s' % resource_op,
explode_lambda[resource_op]))
class TestBannedDBSchemaOperations(testtools.TestCase):
"""Test the BannedDBSchemaOperations fixture."""
def test_column(self):
"""Test column drops and alters raise DBOperationNotAllowed."""
"""Test column operations raise DBOperationNotAllowed."""
column = sqlalchemy.Column()
with BannedDBSchemaOperations(banned_resources=['Column']):
with BannedDBSchemaOperations(
banned_ops={'Column': ['create', 'alter', 'drop']}):
self.assertRaises(DBOperationNotAllowed, column.drop)
self.assertRaises(DBOperationNotAllowed, column.alter)
self.assertRaises(DBOperationNotAllowed, column.create)
def test_table(self):
"""Test table drops and alters raise DBOperationNotAllowed."""
"""Test table operations raise DBOperationNotAllowed."""
table = sqlalchemy.Table()
with BannedDBSchemaOperations(banned_resources=['Table']):
with BannedDBSchemaOperations(
banned_ops={'Table': ['create', 'alter', 'drop',
'insert', 'update', 'delete']}):
self.assertRaises(DBOperationNotAllowed, table.drop)
self.assertRaises(DBOperationNotAllowed, table.alter)
self.assertRaises(DBOperationNotAllowed, table.create)
self.assertRaises(DBOperationNotAllowed, table.insert)
self.assertRaises(DBOperationNotAllowed, table.update)
self.assertRaises(DBOperationNotAllowed, table.delete)
class KeystoneMigrationsCheckers(test_migrations.WalkVersionsMixin):
"""Walk over and test all sqlalchemy-migrate migrations."""
# NOTE(xek): We start requiring things be additive in Newton, so
# ignore all migrations before the first version in Newton.
migrate_file = migrate_repo.__file__
first_version = 101
# NOTE(henry-nash): We don't ban data modification in the legacy repo,
# since there are already migrations that do this for Newton (and these
# do not cause us issues, or are already worked around).
banned_ops = {'Table': ['alter', 'drop'],
'Column': ['alter', 'drop']}
exceptions = [
# NOTE(xek): Reviewers: DO NOT ALLOW THINGS TO BE ADDED HERE UNLESS
# JUSTIFICATION CAN BE PROVIDED AS TO WHY THIS WILL NOT CAUSE
# PROBLEMS FOR ROLLING UPGRADES.
# Migration 102 drops the domain table in the Newton release. All
# code that referenced the domain table was removed in the Mitaka
# release, hence this migration will not cause problems when
# running a mixture of Mitaka and Newton versions of keystone.
102,
# Migration 106 simply allows the password column to be nullable.
# This change would not impact a rolling upgrade.
106
]
@property
def INIT_VERSION(self):
return migration_helpers.get_init_version()
return migration_helpers.get_init_version(
abs_path=os.path.abspath(os.path.dirname(self.migrate_file)))
@property
def REPOSITORY(self):
migrate_file = migrate_repo.__file__
return repository.Repository(
os.path.abspath(os.path.dirname(migrate_file))
os.path.abspath(os.path.dirname(self.migrate_file))
)
@property
@ -97,60 +157,37 @@ class KeystoneMigrationsCheckers(test_migrations.WalkVersionsMixin):
def migrate_engine(self):
return self.engine
def migrate_fully(self, repo_name):
abs_path = os.path.abspath(os.path.dirname(repo_name))
init_version = migration_helpers.get_init_version(abs_path=abs_path)
schema = versioning_api.ControlledSchema.create(
self.migrate_engine, abs_path, init_version)
max_version = schema.repository.version().version
upgrade = True
err = ''
version = versioning_api._migrate_version(
schema, max_version, upgrade, err)
schema.upgrade(version)
def migrate_up(self, version, with_data=False):
"""Check that migrations don't cause downtime.
Schema migrations can be done online, allowing for rolling upgrades.
"""
# NOTE(xek):
# This is a list of migrations where we allow dropping and altering
# things. The rules for adding exceptions are very specific:
#
# 1) Migrations which don't cause incompatibilities are allowed,
# for example dropping an index or constraint.
#
# 2) Migrations removing structures not used in the previous version
# are allowed (we keep compatibility between releases), ex.:
#
# a) feature is deprecated according to the deprecation policies
# (release 1),
#
# b) code supporting the feature is removed the following release
# (release 2),
#
# c) table can be dropped a release after the code has been removed
# (i.e. in release 3).
#
# 3) Any other changes which don't pass this test are disallowed.
# self.exceptions contains a list of migrations where we allow the
# banned operations. Only Migrations which don't cause
# incompatibilities are allowed, for example dropping an index or
# constraint.
#
# Please follow the guidelines outlined at:
# http://docs.openstack.org/developer/keystone/developing.html#online-migration
exceptions = [
# NOTE(xek): Reviewers: DO NOT ALLOW THINGS TO BE ADDED HERE UNLESS
# JUSTIFICATION CAN BE PROVIDED AS TO WHY THIS WILL NOT CAUSE
# PROBLEMS FOR ROLLING UPGRADES.
# Migration 102 drops the domain table in the Newton release. All
# code that referenced the domain table was removed in the Mitaka
# release, hence this migration will not cause problems when
# running a mixture of Mitaka and Newton versions of keystone.
102,
# Migration 106 simply allows the password column to be nullable.
# This change would not impact a rolling upgrade.
106
]
# NOTE(xek): We start requiring things be additive in Newton, so
# ignore all migrations before that point.
NEWTON_START = 101
if version >= NEWTON_START and version not in exceptions:
banned = ['Table', 'Column']
if version >= self.first_version and version not in self.exceptions:
banned_ops = self.banned_ops
else:
banned = None
with BannedDBSchemaOperations(banned):
banned_ops = None
with BannedDBSchemaOperations(banned_ops, self.migrate_file):
super(KeystoneMigrationsCheckers,
self).migrate_up(version, with_data)
@ -176,49 +213,114 @@ class TestKeystoneMigrationsSQLite(
pass
class TestKeystoneExpandSchemaMigrations(
KeystoneMigrationsCheckers):
migrate_file = expand_repo.__file__
first_version = 1
# TODO(henry-nash): we should include Table update here as well, but this
# causes the update of the migration version to appear as a banned
# operation!
banned_ops = {'Table': ['alter', 'drop', 'insert', 'delete'],
'Column': ['alter', 'drop']}
exceptions = [
# NOTE(xek, henry-nash): Reviewers: DO NOT ALLOW THINGS TO BE ADDED
# HERE UNLESS JUSTIFICATION CAN BE PROVIDED AS TO WHY THIS WILL NOT
# CAUSE PROBLEMS FOR ROLLING UPGRADES.
]
def setUp(self):
super(TestKeystoneExpandSchemaMigrations, self).setUp()
self.migrate_fully(migrate_repo.__file__)
class TestKeystoneExpandSchemaMigrationsMySQL(
KeystoneMigrationsCheckers, test_base.MySQLOpportunisticTestCase):
@property
def INIT_VERSION(self):
return migration_helpers.get_init_version(
abs_path=os.path.abspath(os.path.dirname(expand_repo.__file__)))
@property
def REPOSITORY(self):
migrate_file = expand_repo.__file__
return repository.Repository(
os.path.abspath(os.path.dirname(migrate_file))
)
TestKeystoneExpandSchemaMigrations,
test_base.MySQLOpportunisticTestCase):
pass
class TestKeystoneExpandSchemaMigrationsPostgreSQL(
KeystoneMigrationsCheckers, test_base.PostgreSQLOpportunisticTestCase):
@property
def INIT_VERSION(self):
return migration_helpers.get_init_version(
abs_path=os.path.abspath(os.path.dirname(expand_repo.__file__)))
@property
def REPOSITORY(self):
migrate_file = expand_repo.__file__
return repository.Repository(
os.path.abspath(os.path.dirname(migrate_file))
)
TestKeystoneExpandSchemaMigrations,
test_base.PostgreSQLOpportunisticTestCase):
pass
class TestKeystoneExpandSchemaMigrationsSQLite(
KeystoneMigrationsCheckers, test_base.DbTestCase):
class TestKeystoneDataMigrations(
KeystoneMigrationsCheckers):
@property
def INIT_VERSION(self):
return migration_helpers.get_init_version(
abs_path=os.path.abspath(os.path.dirname(expand_repo.__file__)))
migrate_file = data_migration_repo.__file__
first_version = 1
banned_ops = {'Table': ['create', 'alter', 'drop'],
'Column': ['create', 'alter', 'drop']}
exceptions = [
# NOTE(xek, henry-nash): Reviewers: DO NOT ALLOW THINGS TO BE ADDED
# HERE UNLESS JUSTIFICATION CAN BE PROVIDED AS TO WHY THIS WILL NOT
# CAUSE PROBLEMS FOR ROLLING UPGRADES.
]
@property
def REPOSITORY(self):
migrate_file = expand_repo.__file__
return repository.Repository(
os.path.abspath(os.path.dirname(migrate_file))
)
def setUp(self):
super(TestKeystoneDataMigrations, self).setUp()
self.migrate_fully(migrate_repo.__file__)
self.migrate_fully(expand_repo.__file__)
class TestKeystoneDataMigrationsMySQL(
TestKeystoneDataMigrations,
test_base.MySQLOpportunisticTestCase):
pass
class TestKeystoneDataMigrationsPostgreSQL(
TestKeystoneDataMigrations,
test_base.PostgreSQLOpportunisticTestCase):
pass
class TestKeystoneDataMigrationsSQLite(
TestKeystoneDataMigrations, test_base.DbTestCase):
pass
class TestKeystoneContractSchemaMigrations(
KeystoneMigrationsCheckers):
migrate_file = contract_repo.__file__
first_version = 1
# TODO(henry-nash): we should include Table update here as well, but this
# causes the update of the migration version to appear as a banned
# operation!
banned_ops = {'Table': ['create', 'insert', 'delete'],
'Column': ['create']}
exceptions = [
# NOTE(xek, henry-nash): Reviewers: DO NOT ALLOW THINGS TO BE ADDED
# HERE UNLESS JUSTIFICATION CAN BE PROVIDED AS TO WHY THIS WILL NOT
# CAUSE PROBLEMS FOR ROLLING UPGRADES.
]
def setUp(self):
super(TestKeystoneContractSchemaMigrations, self).setUp()
self.migrate_fully(migrate_repo.__file__)
self.migrate_fully(expand_repo.__file__)
self.migrate_fully(data_migration_repo.__file__)
class TestKeystoneContractSchemaMigrationsMySQL(
TestKeystoneContractSchemaMigrations,
test_base.MySQLOpportunisticTestCase):
pass
class TestKeystoneContractSchemaMigrationsPostgreSQL(
TestKeystoneContractSchemaMigrations,
test_base.PostgreSQLOpportunisticTestCase):
pass
class TestKeystoneContractSchemaMigrationsSQLite(
TestKeystoneContractSchemaMigrations, test_base.DbTestCase):
# In Sqlite an alter will appear as a create, so if we check for creates
# we will get false positives.
def setUp(self):
super(TestKeystoneContractSchemaMigrationsSQLite, self).setUp()
self.banned_ops['Table'].remove('create')