diff --git a/keystone/common/sql/contract_repo/versions/030_contract_add_project_tags_table.py b/keystone/common/sql/contract_repo/versions/030_contract_add_project_tags_table.py new file mode 100644 index 0000000000..8aa15c1ef2 --- /dev/null +++ b/keystone/common/sql/contract_repo/versions/030_contract_add_project_tags_table.py @@ -0,0 +1,15 @@ +# 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. + + +def upgrade(migrate_engine): + pass diff --git a/keystone/common/sql/core.py b/keystone/common/sql/core.py index 4b4ca14b7e..e34afd04ca 100644 --- a/keystone/common/sql/core.py +++ b/keystone/common/sql/core.py @@ -71,6 +71,7 @@ PrimaryKeyConstraint = sql.PrimaryKeyConstraint joinedload = sql.orm.joinedload # Suppress flake8's unused import warning for flag_modified: flag_modified = flag_modified +Unicode = sql.Unicode def initialize(): diff --git a/keystone/common/sql/data_migration_repo/versions/030_migrate_add_project_tags_table.py b/keystone/common/sql/data_migration_repo/versions/030_migrate_add_project_tags_table.py new file mode 100644 index 0000000000..8aa15c1ef2 --- /dev/null +++ b/keystone/common/sql/data_migration_repo/versions/030_migrate_add_project_tags_table.py @@ -0,0 +1,15 @@ +# 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. + + +def upgrade(migrate_engine): + pass diff --git a/keystone/common/sql/expand_repo/versions/030_expand_add_project_tags_table.py b/keystone/common/sql/expand_repo/versions/030_expand_add_project_tags_table.py new file mode 100644 index 0000000000..71ff49d43a --- /dev/null +++ b/keystone/common/sql/expand_repo/versions/030_expand_add_project_tags_table.py @@ -0,0 +1,44 @@ +# 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + + meta = sql.MetaData() + meta.bind = migrate_engine + + project_table = sql.Table('project', meta, autoload=True) + + # NOTE(lamt) To allow tag name to be case sensitive for MySQL, the 'name' + # column needs to use collation, which is incompatible with Postgresql. + # Using unicode to mirror nova's server tag: + # https://github.com/openstack/nova/blob/master/nova/db/sqlalchemy/models.py + project_tags_table = sql.Table( + 'project_tag', + meta, + sql.Column('project_id', + sql.String(64), + sql.ForeignKey(project_table.c.id, ondelete='CASCADE'), + nullable=False, + primary_key=True), + sql.Column('name', + sql.Unicode(255), + nullable=False, + primary_key=True), + sql.UniqueConstraint('project_id', 'name'), + mysql_engine='InnoDB', + mysql_charset='utf8' + ) + + project_tags_table.create(migrate_engine, checkfirst=True) diff --git a/keystone/tests/unit/test_sql_upgrade.py b/keystone/tests/unit/test_sql_upgrade.py index 9176c821e7..39d18050f4 100644 --- a/keystone/tests/unit/test_sql_upgrade.py +++ b/keystone/tests/unit/test_sql_upgrade.py @@ -2421,6 +2421,74 @@ class FullMigration(SqlMigrateBase, unit.TestCase): pw_table = sqlalchemy.Table('password', meta, autoload=True) self.assertFalse(pw_table.c.created_at_int.nullable) + def test_migration_30_expand_add_project_tags_table(self): + self.expand(29) + self.migrate(29) + self.contract(29) + + table_name = 'project_tag' + self.assertTableDoesNotExist(table_name) + + self.expand(30) + self.migrate(30) + self.contract(30) + + self.assertTableExists(table_name) + self.assertTableColumns( + table_name, + ['project_id', 'name']) + + def test_migration_030_project_tags_works_correctly_after_migration(self): + if self.engine.name == 'sqlite': + self.skipTest('sqlite backend does not support foreign keys') + + self.expand(30) + self.migrate(30) + self.contract(30) + + project_table = sqlalchemy.Table( + 'project', self.metadata, autoload=True) + tag_table = sqlalchemy.Table( + 'project_tag', self.metadata, autoload=True) + + session = self.sessionmaker() + project_id = uuid.uuid4().hex + + project = { + 'id': project_id, + 'name': uuid.uuid4().hex, + 'enabled': True, + 'domain_id': resource_base.NULL_DOMAIN_ID, + 'is_domain': False + } + + tag = { + 'project_id': project_id, + 'name': uuid.uuid4().hex + } + + self.insert_dict(session, 'project', project) + self.insert_dict(session, 'project_tag', tag) + + tags_query = session.query(tag_table).filter_by( + project_id=project_id).all() + self.assertThat(tags_query, matchers.HasLength(1)) + + # Adding duplicate tags should cause error. + self.assertRaises(db_exception.DBDuplicateEntry, + self.insert_dict, + session, 'project_tag', tag) + + session.execute( + project_table.delete().where(project_table.c.id == project_id) + ) + + tags_query = session.query(tag_table).filter_by( + project_id=project_id).all() + self.assertThat(tags_query, matchers.HasLength(0)) + + session.close() + class MySQLOpportunisticFullMigration(FullMigration): FIXTURE = test_base.MySQLOpportunisticFixture