keystone/keystone/tests/unit/test_sql_upgrade.py

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