From 5bc46d861e9c8f355d2bdd68912be3e64c2dc9e9 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 15 Jan 2013 21:26:57 -0600 Subject: Create a default domain (bp default-domain) This changes rewrites some of our migration history since the folsom release so that we can create a default domain prior to creating non-nullable foreignkey's in the user and project tables in migration 9 (numbered according to this change). DocImpact Change-Id: I807f7b1dca1d6a895f7417c316bcbce24ada61c0 --- etc/keystone.conf.sample | 8 ++ .../versions/008_create_default_domain.py | 63 ++++++++++++ .../versions/008_normalize_identity.py | 93 ----------------- .../versions/009_normalize_identity.py | 93 +++++++++++++++++ .../versions/009_normalize_identity_migration.py | 114 --------------------- .../sql/migrate_repo/versions/010_endpoints_v3.py | 54 ---------- .../versions/010_normalize_identity_migration.py | 114 +++++++++++++++++++++ .../sql/migrate_repo/versions/011_endpoints_v3.py | 54 ++++++++++ .../versions/011_populate_endpoint_type.py | 97 ------------------ .../versions/012_drop_legacy_endpoints.py | 51 --------- .../versions/012_populate_endpoint_type.py | 97 ++++++++++++++++++ .../migrate_repo/versions/013_add_group_tables.py | 93 ----------------- .../versions/013_drop_legacy_endpoints.py | 51 +++++++++ .../migrate_repo/versions/014_add_group_tables.py | 93 +++++++++++++++++ .../migrate_repo/versions/014_tenant_to_project.py | 20 ---- .../migrate_repo/versions/015_tenant_to_project.py | 20 ++++ keystone/config.py | 3 + keystone/identity/controllers.py | 9 ++ tests/test_sql_upgrade.py | 44 ++++---- 19 files changed, 627 insertions(+), 544 deletions(-) create mode 100644 keystone/common/sql/migrate_repo/versions/008_create_default_domain.py delete mode 100644 keystone/common/sql/migrate_repo/versions/008_normalize_identity.py create mode 100644 keystone/common/sql/migrate_repo/versions/009_normalize_identity.py delete mode 100644 keystone/common/sql/migrate_repo/versions/009_normalize_identity_migration.py delete mode 100644 keystone/common/sql/migrate_repo/versions/010_endpoints_v3.py create mode 100644 keystone/common/sql/migrate_repo/versions/010_normalize_identity_migration.py create mode 100644 keystone/common/sql/migrate_repo/versions/011_endpoints_v3.py delete mode 100644 keystone/common/sql/migrate_repo/versions/011_populate_endpoint_type.py delete mode 100644 keystone/common/sql/migrate_repo/versions/012_drop_legacy_endpoints.py create mode 100644 keystone/common/sql/migrate_repo/versions/012_populate_endpoint_type.py delete mode 100644 keystone/common/sql/migrate_repo/versions/013_add_group_tables.py create mode 100644 keystone/common/sql/migrate_repo/versions/013_drop_legacy_endpoints.py create mode 100644 keystone/common/sql/migrate_repo/versions/014_add_group_tables.py delete mode 100644 keystone/common/sql/migrate_repo/versions/014_tenant_to_project.py create mode 100644 keystone/common/sql/migrate_repo/versions/015_tenant_to_project.py diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample index 3b67b300..489f83d7 100644 --- a/etc/keystone.conf.sample +++ b/etc/keystone.conf.sample @@ -76,6 +76,14 @@ [identity] # driver = keystone.identity.backends.sql.Identity +# This references the domain to use for all Identity API v2 requests (which are +# not aware of domains). A domain with this ID will be created for you by +# keystone-manage db_sync in migration 008. The domain referenced by this ID +# cannot be deleted on the v3 API, to prevent accidentally breaking the v2 API. +# There is nothing special about this domain, other than the fact that it must +# exist to order to maintain support for your v2 clients. +# default_domain_id = default + [catalog] # dynamic, sql-based backend (supports API/CLI-based management commands) # driver = keystone.catalog.backends.sql.Catalog diff --git a/keystone/common/sql/migrate_repo/versions/008_create_default_domain.py b/keystone/common/sql/migrate_repo/versions/008_create_default_domain.py new file mode 100644 index 00000000..27d4b28b --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/008_create_default_domain.py @@ -0,0 +1,63 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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 json + +import sqlalchemy as sql +from sqlalchemy import orm +from keystone import config + + +CONF = config.CONF +DEFAULT_DOMAIN_ID = CONF['identity']['default_domain_id'] + + +def upgrade(migrate_engine): + """Creates the default domain.""" + meta = sql.MetaData() + meta.bind = migrate_engine + + domain_table = sql.Table('domain', meta, autoload=True) + + domain = { + 'id': DEFAULT_DOMAIN_ID, + 'name': 'Default', + 'enabled': True, + 'extra': json.dumps({ + 'description': 'Owns users and tenants (i.e. projects) available ' + 'on Identity API v2.'})} + + session = orm.sessionmaker(bind=migrate_engine)() + + session.execute( + 'INSERT INTO `%s` (%s) VALUES (%s)' % ( + domain_table.name, + ', '.join(['`%s`' % k for k in domain.keys()]), + ', '.join([':%s' % k for k in domain.keys()])), + domain) + session.commit() + + +def downgrade(migrate_engine): + """Delete the default domain.""" + meta = sql.MetaData() + meta.bind = migrate_engine + + sql.Table('domain', meta, autoload=True) + session = orm.sessionmaker(bind=migrate_engine)() + session.execute( + 'DELETE FROM `domain` WHERE `id`=:id', {'id': DEFAULT_DOMAIN_ID}) + session.commit() diff --git a/keystone/common/sql/migrate_repo/versions/008_normalize_identity.py b/keystone/common/sql/migrate_repo/versions/008_normalize_identity.py deleted file mode 100644 index 3f5ae3b8..00000000 --- a/keystone/common/sql/migrate_repo/versions/008_normalize_identity.py +++ /dev/null @@ -1,93 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC -# -# 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. - - -from sqlalchemy import Column, MetaData, String, Table, Text, types -from sqlalchemy.orm import sessionmaker - - -#sqlite doesn't support dropping columns. Copy to a new table instead -def downgrade_user_table(meta, migrate_engine): - maker = sessionmaker(bind=migrate_engine) - session = maker() - session.execute("ALTER TABLE user RENAME TO orig_user;") - - user_table = Table( - 'user', - meta, - Column('id', String(64), primary_key=True), - Column('name', String(64), unique=True, nullable=False), - Column('extra', Text())) - user_table.create(migrate_engine, checkfirst=True) - - orig_user_table = Table('orig_user', meta, autoload=True) - for user in session.query(orig_user_table): - session.execute("insert into user (id, name, extra) " - "values ( :id, :name, :extra);", - {'id': user.id, - 'name': user.name, - 'extra': user.extra}) - session.execute("drop table orig_user;") - - -def downgrade_tenant_table(meta, migrate_engine): - maker = sessionmaker(bind=migrate_engine) - session = maker() - session.execute("ALTER TABLE tenant RENAME TO orig_tenant;") - - tenant_table = Table( - 'tenant', - meta, - Column('id', String(64), primary_key=True), - Column('name', String(64), unique=True, nullable=False), - Column('extra', Text())) - tenant_table.create(migrate_engine, checkfirst=True) - - orig_tenant_table = Table('orig_tenant', meta, autoload=True) - for tenant in session.query(orig_tenant_table): - session.execute("insert into tenant (id, name, extra) " - "values ( :id, :name, :extra);", - {'id': tenant.id, - 'name': tenant.name, - 'extra': tenant.extra}) - session.execute("drop table orig_tenant;") - - -def upgrade_user_table(meta, migrate_engine): - user_table = Table('user', meta, autoload=True) - user_table.create_column(Column("password", String(128))) - user_table.create_column(Column("enabled", types.Boolean, - default=True)) - - -def upgrade_tenant_table(meta, migrate_engine): - tenant_table = Table('tenant', meta, autoload=True) - tenant_table.create_column(Column("description", Text())) - tenant_table.create_column(Column("enabled", types.Boolean)) - - -def upgrade(migrate_engine): - meta = MetaData() - meta.bind = migrate_engine - upgrade_user_table(meta, migrate_engine) - upgrade_tenant_table(meta, migrate_engine) - - -def downgrade(migrate_engine): - meta = MetaData() - meta.bind = migrate_engine - downgrade_user_table(meta, migrate_engine) - downgrade_tenant_table(meta, migrate_engine) diff --git a/keystone/common/sql/migrate_repo/versions/009_normalize_identity.py b/keystone/common/sql/migrate_repo/versions/009_normalize_identity.py new file mode 100644 index 00000000..3f5ae3b8 --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/009_normalize_identity.py @@ -0,0 +1,93 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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. + + +from sqlalchemy import Column, MetaData, String, Table, Text, types +from sqlalchemy.orm import sessionmaker + + +#sqlite doesn't support dropping columns. Copy to a new table instead +def downgrade_user_table(meta, migrate_engine): + maker = sessionmaker(bind=migrate_engine) + session = maker() + session.execute("ALTER TABLE user RENAME TO orig_user;") + + user_table = Table( + 'user', + meta, + Column('id', String(64), primary_key=True), + Column('name', String(64), unique=True, nullable=False), + Column('extra', Text())) + user_table.create(migrate_engine, checkfirst=True) + + orig_user_table = Table('orig_user', meta, autoload=True) + for user in session.query(orig_user_table): + session.execute("insert into user (id, name, extra) " + "values ( :id, :name, :extra);", + {'id': user.id, + 'name': user.name, + 'extra': user.extra}) + session.execute("drop table orig_user;") + + +def downgrade_tenant_table(meta, migrate_engine): + maker = sessionmaker(bind=migrate_engine) + session = maker() + session.execute("ALTER TABLE tenant RENAME TO orig_tenant;") + + tenant_table = Table( + 'tenant', + meta, + Column('id', String(64), primary_key=True), + Column('name', String(64), unique=True, nullable=False), + Column('extra', Text())) + tenant_table.create(migrate_engine, checkfirst=True) + + orig_tenant_table = Table('orig_tenant', meta, autoload=True) + for tenant in session.query(orig_tenant_table): + session.execute("insert into tenant (id, name, extra) " + "values ( :id, :name, :extra);", + {'id': tenant.id, + 'name': tenant.name, + 'extra': tenant.extra}) + session.execute("drop table orig_tenant;") + + +def upgrade_user_table(meta, migrate_engine): + user_table = Table('user', meta, autoload=True) + user_table.create_column(Column("password", String(128))) + user_table.create_column(Column("enabled", types.Boolean, + default=True)) + + +def upgrade_tenant_table(meta, migrate_engine): + tenant_table = Table('tenant', meta, autoload=True) + tenant_table.create_column(Column("description", Text())) + tenant_table.create_column(Column("enabled", types.Boolean)) + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + upgrade_user_table(meta, migrate_engine) + upgrade_tenant_table(meta, migrate_engine) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + downgrade_user_table(meta, migrate_engine) + downgrade_tenant_table(meta, migrate_engine) diff --git a/keystone/common/sql/migrate_repo/versions/009_normalize_identity_migration.py b/keystone/common/sql/migrate_repo/versions/009_normalize_identity_migration.py deleted file mode 100644 index 4ad4db74..00000000 --- a/keystone/common/sql/migrate_repo/versions/009_normalize_identity_migration.py +++ /dev/null @@ -1,114 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC -# -# 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 json - -from sqlalchemy import MetaData, Table -from sqlalchemy.orm import sessionmaker - - -DISABLED_VALUES = ['false', 'disabled', 'no', '0'] - - -def is_enabled(enabled): - # no explicit value means enabled - if enabled is True or enabled is None: - return True - if isinstance(enabled, basestring) and enabled.lower() in DISABLED_VALUES: - return False - return bool(enabled) - - -def downgrade_user_table(meta, migrate_engine): - user_table = Table('user', meta, autoload=True) - maker = sessionmaker(bind=migrate_engine) - session = maker() - for user in session.query(user_table).all(): - extra = json.loads(user.extra) - extra['password'] = user.password - extra['enabled'] = '%r' % user.enabled - session.execute( - 'UPDATE `user` SET `extra`=:extra WHERE `id`=:id', - {'id': user.id, 'extra': json.dumps(extra)}) - session.commit() - - -def downgrade_tenant_table(meta, migrate_engine): - tenant_table = Table('tenant', meta, autoload=True) - maker = sessionmaker(bind=migrate_engine) - session = maker() - for tenant in session.query(tenant_table).all(): - extra = json.loads(tenant.extra) - extra['description'] = tenant.description - extra['enabled'] = '%r' % tenant.enabled - session.execute( - 'UPDATE `tenant` SET `extra`=:extra WHERE `id`=:id', - {'id': tenant.id, 'extra': json.dumps(extra)}) - session.commit() - - -def upgrade_user_table(meta, migrate_engine): - user_table = Table('user', meta, autoload=True) - maker = sessionmaker(bind=migrate_engine) - session = maker() - - for user in session.query(user_table).all(): - extra = json.loads(user.extra) - password = extra.pop('password', None) - enabled = extra.pop('enabled', True) - session.execute( - 'UPDATE `user` SET `password`=:password, `enabled`=:enabled, ' - '`extra`=:extra WHERE `id`=:id', - { - 'id': user.id, - 'password': password, - 'enabled': is_enabled(enabled), - 'extra': json.dumps(extra)}) - session.commit() - - -def upgrade_tenant_table(meta, migrate_engine): - tenant_table = Table('tenant', meta, autoload=True) - - maker = sessionmaker(bind=migrate_engine) - session = maker() - for tenant in session.query(tenant_table): - extra = json.loads(tenant.extra) - description = extra.pop('description', None) - enabled = extra.pop('enabled', True) - session.execute( - 'UPDATE `tenant` SET `enabled`=:enabled, `extra`=:extra, ' - '`description`=:description WHERE `id`=:id', - { - 'id': tenant.id, - 'description': description, - 'enabled': is_enabled(enabled), - 'extra': json.dumps(extra)}) - session.commit() - - -def upgrade(migrate_engine): - meta = MetaData() - meta.bind = migrate_engine - upgrade_user_table(meta, migrate_engine) - upgrade_tenant_table(meta, migrate_engine) - - -def downgrade(migrate_engine): - meta = MetaData() - meta.bind = migrate_engine - downgrade_user_table(meta, migrate_engine) - downgrade_tenant_table(meta, migrate_engine) diff --git a/keystone/common/sql/migrate_repo/versions/010_endpoints_v3.py b/keystone/common/sql/migrate_repo/versions/010_endpoints_v3.py deleted file mode 100644 index 31117486..00000000 --- a/keystone/common/sql/migrate_repo/versions/010_endpoints_v3.py +++ /dev/null @@ -1,54 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC -# -# 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): - """Create API-version specific endpoint tables.""" - meta = sql.MetaData() - meta.bind = migrate_engine - - legacy_table = sql.Table('endpoint', meta, autoload=True) - legacy_table.rename('endpoint_v2') - - sql.Table('service', meta, autoload=True) - new_table = sql.Table( - 'endpoint_v3', - meta, - sql.Column('id', sql.String(64), primary_key=True), - sql.Column('legacy_endpoint_id', sql.String(64)), - sql.Column('interface', sql.String(8), nullable=False), - sql.Column('region', sql.String(255)), - sql.Column('service_id', - sql.String(64), - sql.ForeignKey('service.id'), - nullable=False), - sql.Column('url', sql.Text(), nullable=False), - sql.Column('extra', sql.Text())) - new_table.create(migrate_engine, checkfirst=True) - - -def downgrade(migrate_engine): - """Replace API-version specific endpoint tables with one based on v2.""" - meta = sql.MetaData() - meta.bind = migrate_engine - - new_table = sql.Table('endpoint_v3', meta, autoload=True) - new_table.drop() - - legacy_table = sql.Table('endpoint_v2', meta, autoload=True) - legacy_table.rename('endpoint') diff --git a/keystone/common/sql/migrate_repo/versions/010_normalize_identity_migration.py b/keystone/common/sql/migrate_repo/versions/010_normalize_identity_migration.py new file mode 100644 index 00000000..4ad4db74 --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/010_normalize_identity_migration.py @@ -0,0 +1,114 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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 json + +from sqlalchemy import MetaData, Table +from sqlalchemy.orm import sessionmaker + + +DISABLED_VALUES = ['false', 'disabled', 'no', '0'] + + +def is_enabled(enabled): + # no explicit value means enabled + if enabled is True or enabled is None: + return True + if isinstance(enabled, basestring) and enabled.lower() in DISABLED_VALUES: + return False + return bool(enabled) + + +def downgrade_user_table(meta, migrate_engine): + user_table = Table('user', meta, autoload=True) + maker = sessionmaker(bind=migrate_engine) + session = maker() + for user in session.query(user_table).all(): + extra = json.loads(user.extra) + extra['password'] = user.password + extra['enabled'] = '%r' % user.enabled + session.execute( + 'UPDATE `user` SET `extra`=:extra WHERE `id`=:id', + {'id': user.id, 'extra': json.dumps(extra)}) + session.commit() + + +def downgrade_tenant_table(meta, migrate_engine): + tenant_table = Table('tenant', meta, autoload=True) + maker = sessionmaker(bind=migrate_engine) + session = maker() + for tenant in session.query(tenant_table).all(): + extra = json.loads(tenant.extra) + extra['description'] = tenant.description + extra['enabled'] = '%r' % tenant.enabled + session.execute( + 'UPDATE `tenant` SET `extra`=:extra WHERE `id`=:id', + {'id': tenant.id, 'extra': json.dumps(extra)}) + session.commit() + + +def upgrade_user_table(meta, migrate_engine): + user_table = Table('user', meta, autoload=True) + maker = sessionmaker(bind=migrate_engine) + session = maker() + + for user in session.query(user_table).all(): + extra = json.loads(user.extra) + password = extra.pop('password', None) + enabled = extra.pop('enabled', True) + session.execute( + 'UPDATE `user` SET `password`=:password, `enabled`=:enabled, ' + '`extra`=:extra WHERE `id`=:id', + { + 'id': user.id, + 'password': password, + 'enabled': is_enabled(enabled), + 'extra': json.dumps(extra)}) + session.commit() + + +def upgrade_tenant_table(meta, migrate_engine): + tenant_table = Table('tenant', meta, autoload=True) + + maker = sessionmaker(bind=migrate_engine) + session = maker() + for tenant in session.query(tenant_table): + extra = json.loads(tenant.extra) + description = extra.pop('description', None) + enabled = extra.pop('enabled', True) + session.execute( + 'UPDATE `tenant` SET `enabled`=:enabled, `extra`=:extra, ' + '`description`=:description WHERE `id`=:id', + { + 'id': tenant.id, + 'description': description, + 'enabled': is_enabled(enabled), + 'extra': json.dumps(extra)}) + session.commit() + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + upgrade_user_table(meta, migrate_engine) + upgrade_tenant_table(meta, migrate_engine) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + downgrade_user_table(meta, migrate_engine) + downgrade_tenant_table(meta, migrate_engine) diff --git a/keystone/common/sql/migrate_repo/versions/011_endpoints_v3.py b/keystone/common/sql/migrate_repo/versions/011_endpoints_v3.py new file mode 100644 index 00000000..31117486 --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/011_endpoints_v3.py @@ -0,0 +1,54 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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): + """Create API-version specific endpoint tables.""" + meta = sql.MetaData() + meta.bind = migrate_engine + + legacy_table = sql.Table('endpoint', meta, autoload=True) + legacy_table.rename('endpoint_v2') + + sql.Table('service', meta, autoload=True) + new_table = sql.Table( + 'endpoint_v3', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('legacy_endpoint_id', sql.String(64)), + sql.Column('interface', sql.String(8), nullable=False), + sql.Column('region', sql.String(255)), + sql.Column('service_id', + sql.String(64), + sql.ForeignKey('service.id'), + nullable=False), + sql.Column('url', sql.Text(), nullable=False), + sql.Column('extra', sql.Text())) + new_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + """Replace API-version specific endpoint tables with one based on v2.""" + meta = sql.MetaData() + meta.bind = migrate_engine + + new_table = sql.Table('endpoint_v3', meta, autoload=True) + new_table.drop() + + legacy_table = sql.Table('endpoint_v2', meta, autoload=True) + legacy_table.rename('endpoint') diff --git a/keystone/common/sql/migrate_repo/versions/011_populate_endpoint_type.py b/keystone/common/sql/migrate_repo/versions/011_populate_endpoint_type.py deleted file mode 100644 index abfe728a..00000000 --- a/keystone/common/sql/migrate_repo/versions/011_populate_endpoint_type.py +++ /dev/null @@ -1,97 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC -# -# 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 json -import uuid - -import sqlalchemy as sql -from sqlalchemy import orm - - -ENDPOINT_TYPES = ['public', 'internal', 'admin'] - - -def upgrade(migrate_engine): - """Split each legacy endpoint into seperate records for each interface.""" - meta = sql.MetaData() - meta.bind = migrate_engine - - legacy_table = sql.Table('endpoint_v2', meta, autoload=True) - new_table = sql.Table('endpoint_v3', meta, autoload=True) - - session = orm.sessionmaker(bind=migrate_engine)() - for ref in session.query(legacy_table).all(): - # pull urls out of extra - extra = json.loads(ref.extra) - urls = dict((i, extra.pop('%surl' % i)) for i in ENDPOINT_TYPES) - - for interface in ENDPOINT_TYPES: - endpoint = { - 'id': uuid.uuid4().hex, - 'legacy_endpoint_id': ref.id, - 'interface': interface, - 'region': ref.region, - 'service_id': ref.service_id, - 'url': urls[interface], - 'extra': json.dumps(extra), - } - session.execute( - 'INSERT INTO `%s` (%s) VALUES (%s)' % ( - new_table.name, - ', '.join('%s' % k for k in endpoint.keys()), - ', '.join([':%s' % k for k in endpoint.keys()])), - endpoint) - session.commit() - - -def downgrade(migrate_engine): - """Re-create the v2 endpoints table based on v3 endpoints.""" - meta = sql.MetaData() - meta.bind = migrate_engine - - legacy_table = sql.Table('endpoint_v2', meta, autoload=True) - new_table = sql.Table('endpoint_v3', meta, autoload=True) - - session = orm.sessionmaker(bind=migrate_engine)() - for ref in session.query(new_table).all(): - extra = json.loads(ref.extra) - extra['%surl' % ref.interface] = ref.url - endpoint = { - 'id': ref.legacy_endpoint_id, - 'region': ref.region, - 'service_id': ref.service_id, - 'extra': json.dumps(extra), - } - - try: - session.execute( - 'INSERT INTO `%s` (%s) VALUES (%s)' % ( - legacy_table.name, - ', '.join('%s' % k for k in endpoint.keys()), - ', '.join([':%s' % k for k in endpoint.keys()])), - endpoint) - except sql.exc.IntegrityError: - q = session.query(legacy_table) - q = q.filter_by(id=ref.legacy_endpoint_id) - legacy_ref = q.one() - extra = json.loads(legacy_ref.extra) - extra['%surl' % ref.interface] = ref.url - - session.execute( - 'UPDATE `%s` SET extra=:extra WHERE id=:id' % ( - legacy_table.name), - {'extra': json.dumps(extra), 'id': legacy_ref.id}) - session.commit() diff --git a/keystone/common/sql/migrate_repo/versions/012_drop_legacy_endpoints.py b/keystone/common/sql/migrate_repo/versions/012_drop_legacy_endpoints.py deleted file mode 100644 index f75ce3c0..00000000 --- a/keystone/common/sql/migrate_repo/versions/012_drop_legacy_endpoints.py +++ /dev/null @@ -1,51 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC -# -# 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): - """Replace API-version specific endpoint tables with one based on v3.""" - meta = sql.MetaData() - meta.bind = migrate_engine - - legacy_table = sql.Table('endpoint_v2', meta, autoload=True) - legacy_table.drop() - - new_table = sql.Table('endpoint_v3', meta, autoload=True) - new_table.rename('endpoint') - - -def downgrade(migrate_engine): - """Create API-version specific endpoint tables.""" - meta = sql.MetaData() - meta.bind = migrate_engine - - new_table = sql.Table('endpoint', meta, autoload=True) - new_table.rename('endpoint_v3') - - sql.Table('service', meta, autoload=True) - legacy_table = sql.Table( - 'endpoint_v2', - meta, - sql.Column('id', sql.String(64), primary_key=True), - sql.Column('region', sql.String(255)), - sql.Column('service_id', - sql.String(64), - sql.ForeignKey('service.id'), - nullable=False), - sql.Column('extra', sql.Text())) - legacy_table.create(migrate_engine, checkfirst=True) diff --git a/keystone/common/sql/migrate_repo/versions/012_populate_endpoint_type.py b/keystone/common/sql/migrate_repo/versions/012_populate_endpoint_type.py new file mode 100644 index 00000000..abfe728a --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/012_populate_endpoint_type.py @@ -0,0 +1,97 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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 json +import uuid + +import sqlalchemy as sql +from sqlalchemy import orm + + +ENDPOINT_TYPES = ['public', 'internal', 'admin'] + + +def upgrade(migrate_engine): + """Split each legacy endpoint into seperate records for each interface.""" + meta = sql.MetaData() + meta.bind = migrate_engine + + legacy_table = sql.Table('endpoint_v2', meta, autoload=True) + new_table = sql.Table('endpoint_v3', meta, autoload=True) + + session = orm.sessionmaker(bind=migrate_engine)() + for ref in session.query(legacy_table).all(): + # pull urls out of extra + extra = json.loads(ref.extra) + urls = dict((i, extra.pop('%surl' % i)) for i in ENDPOINT_TYPES) + + for interface in ENDPOINT_TYPES: + endpoint = { + 'id': uuid.uuid4().hex, + 'legacy_endpoint_id': ref.id, + 'interface': interface, + 'region': ref.region, + 'service_id': ref.service_id, + 'url': urls[interface], + 'extra': json.dumps(extra), + } + session.execute( + 'INSERT INTO `%s` (%s) VALUES (%s)' % ( + new_table.name, + ', '.join('%s' % k for k in endpoint.keys()), + ', '.join([':%s' % k for k in endpoint.keys()])), + endpoint) + session.commit() + + +def downgrade(migrate_engine): + """Re-create the v2 endpoints table based on v3 endpoints.""" + meta = sql.MetaData() + meta.bind = migrate_engine + + legacy_table = sql.Table('endpoint_v2', meta, autoload=True) + new_table = sql.Table('endpoint_v3', meta, autoload=True) + + session = orm.sessionmaker(bind=migrate_engine)() + for ref in session.query(new_table).all(): + extra = json.loads(ref.extra) + extra['%surl' % ref.interface] = ref.url + endpoint = { + 'id': ref.legacy_endpoint_id, + 'region': ref.region, + 'service_id': ref.service_id, + 'extra': json.dumps(extra), + } + + try: + session.execute( + 'INSERT INTO `%s` (%s) VALUES (%s)' % ( + legacy_table.name, + ', '.join('%s' % k for k in endpoint.keys()), + ', '.join([':%s' % k for k in endpoint.keys()])), + endpoint) + except sql.exc.IntegrityError: + q = session.query(legacy_table) + q = q.filter_by(id=ref.legacy_endpoint_id) + legacy_ref = q.one() + extra = json.loads(legacy_ref.extra) + extra['%surl' % ref.interface] = ref.url + + session.execute( + 'UPDATE `%s` SET extra=:extra WHERE id=:id' % ( + legacy_table.name), + {'extra': json.dumps(extra), 'id': legacy_ref.id}) + session.commit() diff --git a/keystone/common/sql/migrate_repo/versions/013_add_group_tables.py b/keystone/common/sql/migrate_repo/versions/013_add_group_tables.py deleted file mode 100644 index a42b5772..00000000 --- a/keystone/common/sql/migrate_repo/versions/013_add_group_tables.py +++ /dev/null @@ -1,93 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC -# -# 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 - - sql.Table('domain', meta, autoload=True) - group_table = sql.Table( - 'group', - meta, - sql.Column('id', sql.String(64), primary_key=True), - sql.Column('domain_id', sql.String(64), sql.ForeignKey('domain.id')), - sql.Column('name', sql.String(64), unique=True, nullable=False), - sql.Column('description', sql.Text()), - sql.Column('extra', sql.Text())) - group_table.create(migrate_engine, checkfirst=True) - - sql.Table('user', meta, autoload=True) - user_group_membership_table = sql.Table( - 'user_group_membership', - meta, - sql.Column( - 'user_id', - sql.String(64), - sql.ForeignKey('user.id'), - primary_key=True), - sql.Column( - 'group_id', - sql.String(64), - sql.ForeignKey('group.id'), - primary_key=True)) - user_group_membership_table.create(migrate_engine, checkfirst=True) - - sql.Table('tenant', meta, autoload=True) - group_project_metadata_table = sql.Table( - 'group_project_metadata', - meta, - sql.Column( - 'group_id', - sql.String(64), - sql.ForeignKey('group.id'), - primary_key=True), - sql.Column( - 'project_id', - sql.String(64), - sql.ForeignKey('tenant.id'), - primary_key=True), - sql.Column('data', sql.Text())) - group_project_metadata_table.create(migrate_engine, checkfirst=True) - - group_domain_metadata_table = sql.Table( - 'group_domain_metadata', - meta, - sql.Column( - 'group_id', - sql.String(64), - sql.ForeignKey('group.id'), - primary_key=True), - sql.Column( - 'domain_id', - sql.String(64), - sql.ForeignKey('domain.id'), - primary_key=True), - sql.Column('data', sql.Text())) - group_domain_metadata_table.create(migrate_engine, checkfirst=True) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - - tables = ['user_group_membership', 'group_project_metadata', - 'group_domain_metadata', 'group'] - for t in tables: - table = sql.Table(t, meta, autoload=True) - table.drop(migrate_engine, checkfirst=True) diff --git a/keystone/common/sql/migrate_repo/versions/013_drop_legacy_endpoints.py b/keystone/common/sql/migrate_repo/versions/013_drop_legacy_endpoints.py new file mode 100644 index 00000000..f75ce3c0 --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/013_drop_legacy_endpoints.py @@ -0,0 +1,51 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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): + """Replace API-version specific endpoint tables with one based on v3.""" + meta = sql.MetaData() + meta.bind = migrate_engine + + legacy_table = sql.Table('endpoint_v2', meta, autoload=True) + legacy_table.drop() + + new_table = sql.Table('endpoint_v3', meta, autoload=True) + new_table.rename('endpoint') + + +def downgrade(migrate_engine): + """Create API-version specific endpoint tables.""" + meta = sql.MetaData() + meta.bind = migrate_engine + + new_table = sql.Table('endpoint', meta, autoload=True) + new_table.rename('endpoint_v3') + + sql.Table('service', meta, autoload=True) + legacy_table = sql.Table( + 'endpoint_v2', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('region', sql.String(255)), + sql.Column('service_id', + sql.String(64), + sql.ForeignKey('service.id'), + nullable=False), + sql.Column('extra', sql.Text())) + legacy_table.create(migrate_engine, checkfirst=True) diff --git a/keystone/common/sql/migrate_repo/versions/014_add_group_tables.py b/keystone/common/sql/migrate_repo/versions/014_add_group_tables.py new file mode 100644 index 00000000..a42b5772 --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/014_add_group_tables.py @@ -0,0 +1,93 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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 + + sql.Table('domain', meta, autoload=True) + group_table = sql.Table( + 'group', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('domain_id', sql.String(64), sql.ForeignKey('domain.id')), + sql.Column('name', sql.String(64), unique=True, nullable=False), + sql.Column('description', sql.Text()), + sql.Column('extra', sql.Text())) + group_table.create(migrate_engine, checkfirst=True) + + sql.Table('user', meta, autoload=True) + user_group_membership_table = sql.Table( + 'user_group_membership', + meta, + sql.Column( + 'user_id', + sql.String(64), + sql.ForeignKey('user.id'), + primary_key=True), + sql.Column( + 'group_id', + sql.String(64), + sql.ForeignKey('group.id'), + primary_key=True)) + user_group_membership_table.create(migrate_engine, checkfirst=True) + + sql.Table('tenant', meta, autoload=True) + group_project_metadata_table = sql.Table( + 'group_project_metadata', + meta, + sql.Column( + 'group_id', + sql.String(64), + sql.ForeignKey('group.id'), + primary_key=True), + sql.Column( + 'project_id', + sql.String(64), + sql.ForeignKey('tenant.id'), + primary_key=True), + sql.Column('data', sql.Text())) + group_project_metadata_table.create(migrate_engine, checkfirst=True) + + group_domain_metadata_table = sql.Table( + 'group_domain_metadata', + meta, + sql.Column( + 'group_id', + sql.String(64), + sql.ForeignKey('group.id'), + primary_key=True), + sql.Column( + 'domain_id', + sql.String(64), + sql.ForeignKey('domain.id'), + primary_key=True), + sql.Column('data', sql.Text())) + group_domain_metadata_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + tables = ['user_group_membership', 'group_project_metadata', + 'group_domain_metadata', 'group'] + for t in tables: + table = sql.Table(t, meta, autoload=True) + table.drop(migrate_engine, checkfirst=True) diff --git a/keystone/common/sql/migrate_repo/versions/014_tenant_to_project.py b/keystone/common/sql/migrate_repo/versions/014_tenant_to_project.py deleted file mode 100644 index 9b413338..00000000 --- a/keystone/common/sql/migrate_repo/versions/014_tenant_to_project.py +++ /dev/null @@ -1,20 +0,0 @@ -import sqlalchemy as sql - - -def upgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - legacy_table = sql.Table('tenant', meta, autoload=True) - legacy_table.rename('project') - legacy_table = sql.Table('user_tenant_membership', meta, autoload=True) - legacy_table.rename('user_project_membership') - - -def downgrade(migrate_engine): - """Replace API-version specific endpoint tables with one based on v2.""" - meta = sql.MetaData() - meta.bind = migrate_engine - upgrade_table = sql.Table('project', meta, autoload=True) - upgrade_table.rename('tenant') - upgrade_table = sql.Table('user_project_membership', meta, autoload=True) - upgrade_table.rename('user_tenant_membership') diff --git a/keystone/common/sql/migrate_repo/versions/015_tenant_to_project.py b/keystone/common/sql/migrate_repo/versions/015_tenant_to_project.py new file mode 100644 index 00000000..9b413338 --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/015_tenant_to_project.py @@ -0,0 +1,20 @@ +import sqlalchemy as sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + legacy_table = sql.Table('tenant', meta, autoload=True) + legacy_table.rename('project') + legacy_table = sql.Table('user_tenant_membership', meta, autoload=True) + legacy_table.rename('user_project_membership') + + +def downgrade(migrate_engine): + """Replace API-version specific endpoint tables with one based on v2.""" + meta = sql.MetaData() + meta.bind = migrate_engine + upgrade_table = sql.Table('project', meta, autoload=True) + upgrade_table.rename('tenant') + upgrade_table = sql.Table('user_project_membership', meta, autoload=True) + upgrade_table.rename('user_tenant_membership') diff --git a/keystone/config.py b/keystone/config.py index ce79ddd5..e7f31394 100644 --- a/keystone/config.py +++ b/keystone/config.py @@ -142,6 +142,9 @@ register_str('policy_default_rule', default=None) #default max request size is 112k register_int('max_request_body_size', default=114688) +# identity +register_str('default_domain_id', group='identity', default='default') + #ssl options register_bool('enable', group='ssl', default=False) register_str('certfile', group='ssl', default=None) diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index 4751a691..9e8d9f3f 100644 --- a/keystone/identity/controllers.py +++ b/keystone/identity/controllers.py @@ -22,9 +22,12 @@ import uuid from keystone.common import controller from keystone.common import logging +from keystone import config from keystone import exception +CONF = config.CONF +DEFAULT_DOMAIN_ID = CONF['identity']['default_domain_id'] LOG = logging.getLogger(__name__) @@ -442,6 +445,12 @@ class DomainV3(controller.V3Controller): @controller.protected def delete_domain(self, context, domain_id): + # explicitly forbid deleting the default domain (this should be a + # carefully orchestrated manual process involving configuration + # changes, etc) + if domain_id == DEFAULT_DOMAIN_ID: + raise exception.ForbiddenAction(action='delete the default domain') + return self.identity_api.delete_domain(context, domain_id) diff --git a/tests/test_sql_upgrade.py b/tests/test_sql_upgrade.py index 4acda07b..e04fc3e5 100644 --- a/tests/test_sql_upgrade.py +++ b/tests/test_sql_upgrade.py @@ -119,11 +119,11 @@ class SqlUpgradeTests(test.TestCase): self.assertTableExists('policy') self.assertTableColumns('policy', ['id', 'type', 'blob', 'extra']) - def test_upgrade_7_to_9(self): - self.upgrade(7) + def test_upgrade_8_to_10(self): + self.upgrade(8) self.populate_user_table() self.populate_tenant_table() - self.upgrade(9) + self.upgrade(10) self.assertTableColumns("user", ["id", "name", "extra", "password", "enabled"]) @@ -149,15 +149,15 @@ class SqlUpgradeTests(test.TestCase): self.assertEqual(a_tenant.description, 'description') session.commit() - def test_downgrade_9_to_7(self): - self.upgrade(7) + def test_downgrade_10_to_8(self): + self.upgrade(8) self.populate_user_table() self.populate_tenant_table() - self.upgrade(9) - self.downgrade(7) + self.upgrade(10) + self.downgrade(8) - def test_upgrade_9_to_12(self): - self.upgrade(9) + def test_upgrade_10_to_13(self): + self.upgrade(10) service_extra = { 'name': uuid.uuid4().hex, @@ -184,7 +184,7 @@ class SqlUpgradeTests(test.TestCase): self.insert_dict(session, 'endpoint', endpoint) session.commit() - self.upgrade(12) + self.upgrade(13) self.assertTableColumns( 'service', @@ -225,35 +225,35 @@ class SqlUpgradeTests(test.TestCase): self.assertTableDoesNotExist('user_tenant_membership') def test_upgrade_tenant_to_project(self): - self.upgrade(13) - self.assertTenantTables() self.upgrade(14) + self.assertTenantTables() + self.upgrade(15) self.assertProjectTables() def test_downgrade_project_to_tenant(self): - self.upgrade(14) + self.upgrade(15) self.assertProjectTables() - self.downgrade(13) + self.downgrade(14) self.assertTenantTables() - def test_upgrade_12_to_13(self): - self.upgrade(12) + def test_upgrade_13_to_14(self): self.upgrade(13) + self.upgrade(14) self.assertTableExists('group') self.assertTableExists('group_project_metadata') self.assertTableExists('group_domain_metadata') self.assertTableExists('user_group_membership') - def test_downgrade_13_to_12(self): - self.upgrade(13) - self.downgrade(12) + def test_downgrade_14_to_13(self): + self.upgrade(14) + self.downgrade(13) self.assertTableDoesNotExist('group') self.assertTableDoesNotExist('group_project_metadata') self.assertTableDoesNotExist('group_domain_metadata') self.assertTableDoesNotExist('user_group_membership') - def test_downgrade_12_to_9(self): - self.upgrade(12) + def test_downgrade_13_to_10(self): + self.upgrade(13) service_extra = { 'name': uuid.uuid4().hex, @@ -295,7 +295,7 @@ class SqlUpgradeTests(test.TestCase): self.insert_dict(session, 'endpoint', endpoint) session.commit() - self.downgrade(8) + self.downgrade(9) self.assertTableColumns( 'service', -- cgit