333 lines
11 KiB
Python
333 lines
11 KiB
Python
# Copyright 2012 OpenStack Foundation
|
|
#
|
|
# 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.
|
|
|
|
"""
|
|
Test for SQL migration extensions.
|
|
|
|
To run these tests against a live database:
|
|
|
|
1. Set up a blank, live database.
|
|
2. Export database information to environment variable
|
|
``OS_TEST_DBAPI_ADMIN_CONNECTION``. For example::
|
|
|
|
export OS_TEST_DBAPI_ADMIN_CONNECTION=postgresql://localhost/postgres?host=
|
|
/var/folders/7k/pwdhb_mj2cv4zyr0kyrlzjx40000gq/T/tmpMGqN8C&port=9824
|
|
|
|
3. Run the tests using::
|
|
|
|
tox -e py39 -- keystone.tests.unit.test_sql_upgrade
|
|
|
|
For further information, see `oslo.db documentation
|
|
<https://docs.openstack.org/oslo.db/latest/contributor/index.html#how-to-run-unit-tests>`_.
|
|
|
|
.. warning::
|
|
|
|
Your database will be wiped.
|
|
|
|
Do not do this against a database with valuable data as
|
|
all data will be lost.
|
|
"""
|
|
|
|
import fixtures
|
|
from oslo_db import options as db_options
|
|
from oslo_db.sqlalchemy import enginefacade
|
|
from oslo_db.sqlalchemy import test_fixtures as db_fixtures
|
|
from oslo_log import fixture as log_fixture
|
|
from oslo_log import log
|
|
import sqlalchemy.exc
|
|
|
|
from keystone.cmd import cli
|
|
from keystone.common import sql
|
|
from keystone.common.sql import upgrades
|
|
import keystone.conf
|
|
from keystone.tests import unit
|
|
from keystone.tests.unit import ksfixtures
|
|
|
|
CONF = keystone.conf.CONF
|
|
|
|
# NOTE(morganfainberg): This should be updated when each DB migration collapse
|
|
# is done to mirror the expected structure of the DB in the format of
|
|
# { <DB_TABLE_NAME>: [<COLUMN>, <COLUMN>, ...], ... }
|
|
INITIAL_TABLE_STRUCTURE = {
|
|
'config_register': [
|
|
'type', 'domain_id',
|
|
],
|
|
'credential': [
|
|
'id', 'user_id', 'project_id', 'type', 'extra', 'key_hash',
|
|
'encrypted_blob',
|
|
],
|
|
'endpoint': [
|
|
'id', 'legacy_endpoint_id', 'interface', 'region_id', 'service_id',
|
|
'url', 'enabled', 'extra',
|
|
],
|
|
'group': [
|
|
'id', 'domain_id', 'name', 'description', 'extra',
|
|
],
|
|
'policy': [
|
|
'id', 'type', 'blob', 'extra',
|
|
],
|
|
'project': [
|
|
'id', 'name', 'extra', 'description', 'enabled', 'domain_id',
|
|
'parent_id', 'is_domain',
|
|
],
|
|
'project_option': [
|
|
'project_id', 'option_id', 'option_value',
|
|
],
|
|
'project_tag': [
|
|
'project_id', 'name',
|
|
],
|
|
'role': [
|
|
'id', 'name', 'extra', 'domain_id', 'description',
|
|
],
|
|
'role_option': [
|
|
'role_id', 'option_id', 'option_value',
|
|
],
|
|
'service': [
|
|
'id', 'type', 'extra', 'enabled',
|
|
],
|
|
'token': [
|
|
'id', 'expires', 'extra', 'valid', 'trust_id', 'user_id',
|
|
],
|
|
'trust': [
|
|
'id', 'trustor_user_id', 'trustee_user_id', 'project_id',
|
|
'impersonation', 'deleted_at', 'expires_at', 'remaining_uses', 'extra',
|
|
'expires_at_int', 'redelegated_trust_id', 'redelegation_count',
|
|
],
|
|
'trust_role': [
|
|
'trust_id', 'role_id',
|
|
],
|
|
'user': [
|
|
'id', 'extra', 'enabled', 'default_project_id', 'created_at',
|
|
'last_active_at', 'domain_id',
|
|
],
|
|
'user_option': [
|
|
'user_id', 'option_id', 'option_value',
|
|
],
|
|
'user_group_membership': [
|
|
'user_id', 'group_id',
|
|
],
|
|
'region': [
|
|
'id', 'description', 'parent_region_id', 'extra',
|
|
],
|
|
'assignment': [
|
|
'type', 'actor_id', 'target_id', 'role_id', 'inherited',
|
|
],
|
|
'id_mapping': [
|
|
'public_id', 'domain_id', 'local_id', 'entity_type',
|
|
],
|
|
'whitelisted_config': [
|
|
'domain_id', 'group', 'option', 'value',
|
|
],
|
|
'sensitive_config': [
|
|
'domain_id', 'group', 'option', 'value',
|
|
],
|
|
'policy_association': [
|
|
'id', 'policy_id', 'endpoint_id', 'service_id', 'region_id',
|
|
],
|
|
'identity_provider': [
|
|
'id', 'enabled', 'description', 'domain_id', 'authorization_ttl',
|
|
],
|
|
'federation_protocol': [
|
|
'id', 'idp_id', 'mapping_id', 'remote_id_attribute',
|
|
],
|
|
'mapping': [
|
|
'id', 'rules', 'schema_version',
|
|
],
|
|
'service_provider': [
|
|
'auth_url', 'id', 'enabled', 'description', 'sp_url',
|
|
'relay_state_prefix',
|
|
],
|
|
'idp_remote_ids': [
|
|
'idp_id', 'remote_id',
|
|
],
|
|
'consumer': [
|
|
'id', 'description', 'secret', 'extra',
|
|
],
|
|
'request_token': [
|
|
'id', 'request_secret', 'verifier', 'authorizing_user_id',
|
|
'requested_project_id', 'role_ids', 'consumer_id', 'expires_at',
|
|
],
|
|
'access_token': [
|
|
'id', 'access_secret', 'authorizing_user_id', 'project_id', 'role_ids',
|
|
'consumer_id', 'expires_at',
|
|
],
|
|
'revocation_event': [
|
|
'id', 'domain_id', 'project_id', 'user_id', 'role_id', 'trust_id',
|
|
'consumer_id', 'access_token_id', 'issued_before', 'expires_at',
|
|
'revoked_at', 'audit_id', 'audit_chain_id',
|
|
],
|
|
'project_endpoint': [
|
|
'endpoint_id', 'project_id'
|
|
],
|
|
'endpoint_group': [
|
|
'id', 'name', 'description', 'filters',
|
|
],
|
|
'project_endpoint_group': [
|
|
'endpoint_group_id', 'project_id',
|
|
],
|
|
'implied_role': [
|
|
'prior_role_id', 'implied_role_id',
|
|
],
|
|
'local_user': [
|
|
'id', 'user_id', 'domain_id', 'name', 'failed_auth_count',
|
|
'failed_auth_at',
|
|
],
|
|
'password': [
|
|
'id', 'local_user_id', 'created_at', 'expires_at',
|
|
'self_service', 'password_hash', 'created_at_int', 'expires_at_int',
|
|
],
|
|
'federated_user': [
|
|
'id', 'user_id', 'idp_id', 'protocol_id', 'unique_id', 'display_name',
|
|
],
|
|
'nonlocal_user': [
|
|
'domain_id', 'name', 'user_id',
|
|
],
|
|
'system_assignment': [
|
|
'type', 'actor_id', 'target_id', 'role_id', 'inherited',
|
|
],
|
|
'registered_limit': [
|
|
'internal_id', 'id', 'service_id', 'region_id', 'resource_name',
|
|
'default_limit', 'description',
|
|
],
|
|
'limit': [
|
|
'internal_id', 'id', 'project_id', 'resource_limit', 'description',
|
|
'registered_limit_id', 'domain_id',
|
|
],
|
|
'application_credential': [
|
|
'internal_id', 'id', 'name', 'secret_hash', 'description', 'user_id',
|
|
'project_id', 'expires_at', 'system', 'unrestricted',
|
|
],
|
|
'application_credential_role': [
|
|
'application_credential_id', 'role_id',
|
|
],
|
|
'access_rule': [
|
|
'id', 'service', 'path', 'method', 'external_id', 'user_id',
|
|
],
|
|
'application_credential_access_rule': [
|
|
'application_credential_id', 'access_rule_id',
|
|
],
|
|
'expiring_user_group_membership': [
|
|
'user_id', 'group_id', 'idp_id', 'last_verified',
|
|
],
|
|
}
|
|
|
|
|
|
class MigrateBase(
|
|
db_fixtures.OpportunisticDBTestMixin,
|
|
):
|
|
"""Test complete orchestration between all database phases."""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.useFixture(log_fixture.get_logging_handle_error_fixture())
|
|
self.stdlog = self.useFixture(ksfixtures.StandardLogging())
|
|
self.useFixture(ksfixtures.WarningsFixture())
|
|
|
|
self.engine = enginefacade.writer.get_engine()
|
|
self.sessionmaker = enginefacade.writer.get_sessionmaker()
|
|
|
|
db_options.set_defaults(CONF, connection=self.engine.url)
|
|
|
|
# Override keystone's context manager to be oslo.db's global context
|
|
# manager.
|
|
sql.core._TESTING_USE_GLOBAL_CONTEXT_MANAGER = True
|
|
self.addCleanup(setattr,
|
|
sql.core, '_TESTING_USE_GLOBAL_CONTEXT_MANAGER', False)
|
|
self.addCleanup(sql.cleanup)
|
|
|
|
def expand(self):
|
|
"""Expand database schema."""
|
|
upgrades.expand_schema(engine=self.engine)
|
|
|
|
def contract(self):
|
|
"""Contract database schema."""
|
|
upgrades.contract_schema(engine=self.engine)
|
|
|
|
@property
|
|
def metadata(self):
|
|
"""A collection of tables and their associated schemas."""
|
|
return sqlalchemy.MetaData()
|
|
|
|
def load_table(self, name):
|
|
table = sqlalchemy.Table(
|
|
name, self.metadata, autoload_with=self.engine,
|
|
)
|
|
return table
|
|
|
|
def assertTableDoesNotExist(self, table_name):
|
|
"""Assert that a given table exists cannot be selected by name."""
|
|
# Switch to a different metadata otherwise you might still
|
|
# detect renamed or dropped tables
|
|
try:
|
|
sqlalchemy.Table(
|
|
table_name, self.metadata, autoload_with=self.engine,
|
|
)
|
|
except sqlalchemy.exc.NoSuchTableError:
|
|
pass
|
|
else:
|
|
raise AssertionError('Table "%s" already exists' % table_name)
|
|
|
|
def assertTableColumns(self, table_name, expected_cols):
|
|
"""Assert that the table contains the expected set of columns."""
|
|
table = self.load_table(table_name)
|
|
actual_cols = [col.name for col in table.columns]
|
|
# Check if the columns are equal, but allow for a different order,
|
|
# which might occur after an upgrade followed by a downgrade
|
|
self.assertCountEqual(expected_cols, actual_cols,
|
|
'%s table' % table_name)
|
|
|
|
def test_db_sync_check(self):
|
|
checker = cli.DbSync()
|
|
|
|
# If the expand repository doesn't exist yet, then we need to make sure
|
|
# we advertise that `--expand` must be run first.
|
|
log_info = self.useFixture(fixtures.FakeLogger(level=log.INFO))
|
|
status = checker.check_db_sync_status()
|
|
self.assertIn("keystone-manage db_sync --expand", log_info.output)
|
|
self.assertEqual(status, 2)
|
|
|
|
# Assert the correct message is printed when migrate is ahead of
|
|
# contract
|
|
self.expand()
|
|
log_info = self.useFixture(fixtures.FakeLogger(level=log.INFO))
|
|
status = checker.check_db_sync_status()
|
|
self.assertIn("keystone-manage db_sync --contract", log_info.output)
|
|
self.assertEqual(status, 4)
|
|
|
|
# Assert the correct message gets printed when all commands are on
|
|
# the same version
|
|
self.contract()
|
|
log_info = self.useFixture(fixtures.FakeLogger(level=log.INFO))
|
|
status = checker.check_db_sync_status()
|
|
self.assertIn("All db_sync commands are upgraded", log_info.output)
|
|
self.assertEqual(status, 0)
|
|
|
|
def test_upgrade_add_initial_tables(self):
|
|
self.expand()
|
|
for table in INITIAL_TABLE_STRUCTURE:
|
|
self.assertTableColumns(table, INITIAL_TABLE_STRUCTURE[table])
|
|
|
|
|
|
class FullMigrationSQLite(MigrateBase, unit.TestCase):
|
|
pass
|
|
|
|
|
|
class FullMigrationMySQL(MigrateBase, unit.TestCase):
|
|
FIXTURE = db_fixtures.MySQLOpportunisticFixture
|
|
|
|
|
|
class FullMigrationPostgreSQL(MigrateBase, unit.TestCase):
|
|
FIXTURE = db_fixtures.PostgresqlOpportunisticFixture
|