<NME> test_sql_upgrade.py
<BEF> # 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 migrate.versioning import script
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',
    ],
    '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()

        # NOTE(dstanek): Clear out sqlalchemy-migrate's script cache to allow
        # us to have multiple repos (expand, migrate, contract) where the
        # modules have the same name (001_awesome.py).
        self.addCleanup(script.PythonScript.clear)

        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(self.engine)

    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
                                 'enabled', 'domain_id', 'parent_id',
                                 'is_domain'])

    def populate_user_table(self, with_pass_enab=False,
                            with_pass_enab_domain=False):
        # Populate the appropriate fields in the user

<MSG> Merge "Relax newly imposed sql driver restriction for domain config"
<DFF> @@ -636,6 +636,13 @@ class SqlUpgradeTests(SqlMigrateBase):
                                  'enabled', 'domain_id', 'parent_id',
                                  'is_domain'])
 
+    def test_add_config_registration(self):
+        config_registration = 'config_register'
+        self.upgrade(74)
+        self.assertTableDoesNotExist(config_registration)
+        self.upgrade(75)
+        self.assertTableColumns(config_registration, ['type', 'domain_id'])
+
     def populate_user_table(self, with_pass_enab=False,
                             with_pass_enab_domain=False):
         # Populate the appropriate fields in the user

{'commit': 'diff --git a/keystone/common/sql/migrate_repo/versions/075_confirm_config_registration.py b/keystone/common/sql/migrate_repo/versions/075_confirm_config_registration.py\nnew file mode 100644\nindex 000000000..576842c69\n--- /dev/null\n+++ b/keystone/common/sql/migrate_repo/versions/075_confirm_config_registration.py\n@@ -0,0 +1,29 @@\n+#    Licensed under the Apache License, Version 2.0 (the "License"); you may\n+#    not use this file except in compliance with the License. You may obtain\n+#    a copy of the License at\n+#\n+#         http://www.apache.org/licenses/LICENSE-2.0\n+#\n+#    Unless required by applicable law or agreed to in writing, software\n+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\n+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n+#    License for the specific language governing permissions and limitations\n+#    under the License.\n+\n+import sqlalchemy as sql\n+\n+REGISTRATION_TABLE = \'config_register\'\n+\n+\n+def upgrade(migrate_engine):\n+    meta = sql.MetaData()\n+    meta.bind = migrate_engine\n+\n+    registration_table = sql.Table(\n+        REGISTRATION_TABLE,\n+        meta,\n+        sql.Column(\'type\', sql.String(64), primary_key=True),\n+        sql.Column(\'domain_id\', sql.String(64), nullable=False),\n+        mysql_engine=\'InnoDB\',\n+        mysql_charset=\'utf8\')\n+    registration_table.create(migrate_engine, checkfirst=True)\ndiff --git a/keystone/exception.py b/keystone/exception.py\nindex 2940d5083..f5c0e1cdf 100644\n--- a/keystone/exception.py\n+++ b/keystone/exception.py\n@@ -357,6 +357,13 @@ class DomainConfigNotFound(NotFound):\n                        \'configuration for domain %(domain_id)s\')\n \n \n+class ConfigRegistrationNotFound(Exception):\n+    # This is used internally between the domain config backend and the\n+    # manager, so should not escape to the client.  If it did, it is a coding\n+    # error on our part, and would end up, appropriately, as a 500 error.\n+    pass\n+\n+\n class Conflict(Error):\n     message_format = _("Conflict occurred attempting to store %(type)s -"\n                        " %(details)s")\ndiff --git a/keystone/identity/core.py b/keystone/identity/core.py\nindex a619e770d..061b82e12 100644\n--- a/keystone/identity/core.py\n+++ b/keystone/identity/core.py\n@@ -44,6 +44,14 @@ MEMOIZE = cache.get_memoization_decorator(section=\'identity\')\n DOMAIN_CONF_FHEAD = \'keystone.\'\n DOMAIN_CONF_FTAIL = \'.conf\'\n \n+# The number of times we will attempt to register a domain to use the SQL\n+# driver, if we find that another process is in the middle of registering or\n+# releasing at the same time as us.\n+REGISTRATION_ATTEMPTS = 10\n+\n+# Config Registration Types\n+SQL_DRIVER = \'SQL\'\n+\n \n def filter_user(user_ref):\n     """Filter out private items in a user dict.\n@@ -67,7 +75,7 @@ def filter_user(user_ref):\n     return user_ref\n \n \n-@dependency.requires(\'domain_config_api\')\n+@dependency.requires(\'domain_config_api\', \'resource_api\')\n class DomainConfigs(dict):\n     """Discover, store and provide access to domain specific configs.\n \n@@ -95,7 +103,7 @@ class DomainConfigs(dict):\n \n     def _assert_no_more_than_one_sql_driver(self, domain_id, new_config,\n                                             config_file=None):\n-        """Ensure there is more than one sql driver.\n+        """Ensure there is no more than one sql driver.\n \n         Check to see if the addition of the driver in this new config\n         would cause there to now be more than one sql driver.\n@@ -108,6 +116,13 @@ class DomainConfigs(dict):\n                 (self.driver.is_sql or self._any_sql)):\n             # The addition of this driver would cause us to have more than\n             # one sql driver, so raise an exception.\n+\n+            # TODO(henry-nash): This method is only used in the file-based\n+            # case, so has no need to worry about the database/API case. The\n+            # code that overrides config_file below is therefore never used\n+            # and should be removed, and this method perhaps moved inside\n+            # _load_config_from_file(). This is raised as bug #1466772.\n+\n             if not config_file:\n                 config_file = _(\'Database at /domains/%s/config\') % domain_id\n             raise exception.MultipleSQLDriversInConfig(source=config_file)\n@@ -177,19 +192,93 @@ class DomainConfigs(dict):\n \n     def _load_config_from_database(self, domain_id, specific_config):\n \n-        def _assert_not_sql_driver(domain_id, new_config):\n-            """Ensure this is not an sql driver.\n+        def _assert_no_more_than_one_sql_driver(domain_id, new_config):\n+            """Ensure adding driver doesn\'t push us over the limit of 1\n \n-            Due to multi-threading safety concerns, we do not currently support\n-            the setting of a specific identity driver to sql via the Identity\n-            API.\n+            The checks we make in this method need to take into account that\n+            we may be in a multiple process configuration and ensure that\n+            any race conditions are avoided.\n \n             """\n-            if new_config[\'driver\'].is_sql:\n-                reason = _(\'Domain specific sql drivers are not supported via \'\n-                           \'the Identity API. One is specified in \'\n-                           \'/domains/%s/config\') % domain_id\n-                raise exception.InvalidDomainConfig(reason=reason)\n+            if not new_config[\'driver\'].is_sql:\n+                self.domain_config_api.release_registration(domain_id)\n+                return\n+\n+            # To ensure the current domain is the only SQL driver, we attempt\n+            # to register our use of SQL. If we get it we know we are good,\n+            # if we fail to register it then we should:\n+            #\n+            # - First check if another process has registered for SQL for our\n+            #   domain, in which case we are fine\n+            # - If a different domain has it, we should check that this domain\n+            #   is still valid, in case, for example, domain deletion somehow\n+            #   failed to remove its registration (i.e. we self heal for these\n+            #   kinds of issues).\n+\n+            domain_registered = \'Unknown\'\n+            for attempt in range(REGISTRATION_ATTEMPTS):\n+                if self.domain_config_api.obtain_registration(\n+                        domain_id, SQL_DRIVER):\n+                    LOG.debug(\'Domain %s successfully registered to use the \'\n+                              \'SQL driver.\', domain_id)\n+                    return\n+\n+                # We failed to register our use, let\'s find out who is using it\n+                try:\n+                    domain_registered = (\n+                        self.domain_config_api.read_registration(\n+                            SQL_DRIVER))\n+                except exception.ConfigRegistrationNotFound:\n+                    msg = (\'While attempting to register domain %(domain)s to \'\n+                           \'use the SQL driver, another process released it, \'\n+                           \'retrying (attempt %(attempt)s).\')\n+                    LOG.debug(msg, {\'domain\': domain_id,\n+                                    \'attempt\': attempt + 1})\n+                    continue\n+\n+                if domain_registered == domain_id:\n+                    # Another process already registered it for us, so we are\n+                    # fine. In the race condition when another process is\n+                    # in the middle of deleting this domain, we know the domain\n+                    # is already disabled and hence telling the caller that we\n+                    # are registered is benign.\n+                    LOG.debug(\'While attempting to register domain %s to use \'\n+                              \'the SQL driver, found that another process had \'\n+                              \'already registered this domain. This is normal \'\n+                              \'in multi-process configurations.\', domain_id)\n+                    return\n+\n+                # So we don\'t have it, but someone else does...let\'s check that\n+                # this domain is still valid\n+                try:\n+                    self.resource_api.get_domain(domain_registered)\n+                except exception.DomainNotFound:\n+                    msg = (\'While attempting to register domain %(domain)s to \'\n+                           \'use the SQL driver, found that it was already \'\n+                           \'registered to a domain that no longer exists \'\n+                           \'(%(old_domain)s). Removing this stale \'\n+                           \'registration and retrying (attempt %(attempt)s).\')\n+                    LOG.debug(msg, {\'domain\': domain_id,\n+                                    \'old_domain\': domain_registered,\n+                                    \'attempt\': attempt + 1})\n+                    self.domain_config_api.release_registration(\n+                        domain_registered, type=SQL_DRIVER)\n+                    continue\n+\n+                # The domain is valid, so we really do have an attempt at more\n+                # than one SQL driver.\n+                details = (\n+                    _(\'Config API entity at /domains/%s/config\') % domain_id)\n+                raise exception.MultipleSQLDriversInConfig(source=details)\n+\n+            # We fell out of the loop without either registering our domain or\n+            # being able to find who has it...either we were very very very\n+            # unlucky or something is awry.\n+            msg = _(\'Exceeded attempts to register domain %(domain)s to use \'\n+                    \'the SQL driver, the last  domain that appears to have \'\n+                    \'had it is %(last_domain)s, giving up\') % {\n+                        \'domain\': domain_id, \'last_domain\': domain_registered}\n+            raise exception.UnexpectedError(msg)\n \n         domain_config = {}\n         domain_config[\'cfg\'] = cfg.ConfigOpts()\n@@ -207,7 +296,7 @@ class DomainConfigs(dict):\n \n         domain_config[\'cfg_overrides\'] = specific_config\n         domain_config[\'driver\'] = self._load_driver(domain_config)\n-        _assert_not_sql_driver(domain_id, domain_config)\n+        _assert_no_more_than_one_sql_driver(domain_id, domain_config)\n         self[domain_id] = domain_config\n \n     def _setup_domain_drivers_from_database(self, standard_driver,\ndiff --git a/keystone/resource/config_backends/sql.py b/keystone/resource/config_backends/sql.py\nindex 846423b2d..7c296074a 100644\n--- a/keystone/resource/config_backends/sql.py\n+++ b/keystone/resource/config_backends/sql.py\n@@ -42,6 +42,12 @@ class SensitiveConfig(sql.ModelBase, sql.ModelDictMixin):\n         return d\n \n \n+class ConfigRegister(sql.ModelBase, sql.ModelDictMixin):\n+    __tablename__ = \'config_register\'\n+    type = sql.Column(sql.String(64), primary_key=True)\n+    domain_id = sql.Column(sql.String(64), nullable=False)\n+\n+\n class DomainConfig(resource.DomainConfigDriverV8):\n \n     def choose_table(self, sensitive):\n@@ -117,3 +123,30 @@ class DomainConfig(resource.DomainConfigDriverV8):\n                 if option:\n                     query = query.filter_by(option=option)\n             query.delete(False)\n+\n+    def obtain_registration(self, domain_id, type):\n+        try:\n+            with sql.transaction() as session:\n+                ref = ConfigRegister(type=type, domain_id=domain_id)\n+                session.add(ref)\n+            return True\n+        except sql.DBDuplicateEntry:\n+            pass\n+        return False\n+\n+    def read_registration(self, type):\n+        with sql.transaction() as session:\n+            ref = session.query(ConfigRegister).get(type)\n+            if not ref:\n+                raise exception.ConfigRegistrationNotFound()\n+        return ref.domain_id\n+\n+    def release_registration(self, domain_id, type=None):\n+        """Silently delete anything registered for the domain specified."""\n+\n+        with sql.transaction() as session:\n+            query = session.query(ConfigRegister)\n+            if type:\n+                query = query.filter_by(type=type)\n+            query = query.filter_by(domain_id=domain_id)\n+            query.delete(False)\ndiff --git a/keystone/resource/core.py b/keystone/resource/core.py\nindex f2de22e2a..6015107d2 100644\n--- a/keystone/resource/core.py\n+++ b/keystone/resource/core.py\n@@ -460,6 +460,7 @@ class Manager(manager.Manager):\n         # Delete any database stored domain config\n         self.domain_config_api.delete_config_options(domain_id)\n         self.domain_config_api.delete_config_options(domain_id, sensitive=True)\n+        self.domain_config_api.release_registration(domain_id)\n         # TODO(henry-nash): Although the controller will ensure deletion of\n         # all users & groups within the domain (which will cause all\n         # assignments for those users/groups to also be deleted), there\n@@ -1373,5 +1374,44 @@ class DomainConfigDriverV8(object):\n         """\n         raise exception.NotImplemented()  # pragma: no cover\n \n+    @abc.abstractmethod\n+    def obtain_registration(self, domain_id, type):\n+        """Try and register this domain to use the type specified.\n+\n+        :param domain_id: the domain required\n+        :param type: type of registration\n+        :returns: True if the domain was registered, False otherwise. Failing\n+                  to register means that someone already has it (which could\n+                  even be the domain being requested).\n+\n+        """\n+        raise exception.NotImplemented()  # pragma: no cover\n+\n+    @abc.abstractmethod\n+    def read_registration(self, type):\n+        """Get the domain ID of who is registered to use this type.\n+\n+        :param type: type of registration\n+        :returns: domain_id of who is registered.\n+        :raises: keystone.exception.ConfigRegistrationNotFound: nobody is\n+                 registered.\n+\n+        """\n+        raise exception.NotImplemented()  # pragma: no cover\n+\n+    @abc.abstractmethod\n+    def release_registration(self, domain_id, type=None):\n+        """Release registration if it is held by the domain specified.\n+\n+        If the specified domain is registered for this domain then free it,\n+        if it is not then do nothing - no exception is raised.\n+\n+        :param domain_id: the domain in question\n+        :param type: type of registration, if None then all registrations\n+                     for this domain will be freed\n+\n+        """\n+        raise exception.NotImplemented()  # pragma: no cover\n+\n \n DomainConfigDriver = manager.create_legacy_driver(DomainConfigDriverV8)\ndiff --git a/keystone/tests/unit/backend/domain_config/core.py b/keystone/tests/unit/backend/domain_config/core.py\nindex e0bb44114..7bbbf313e 100644\n--- a/keystone/tests/unit/backend/domain_config/core.py\n+++ b/keystone/tests/unit/backend/domain_config/core.py\n@@ -549,3 +549,53 @@ class DomainConfigTests(object):\n             {},\n             self.domain_config_api.get_config_with_sensitive_info(\n                 self.domain[\'id\']))\n+\n+    def test_config_registration(self):\n+        type = uuid.uuid4().hex\n+        self.domain_config_api.obtain_registration(\n+            self.domain[\'id\'], type)\n+        self.domain_config_api.release_registration(\n+            self.domain[\'id\'], type=type)\n+\n+        # Make sure that once someone has it, nobody else can get it.\n+        # This includes the domain who already has it.\n+        self.domain_config_api.obtain_registration(\n+            self.domain[\'id\'], type)\n+        self.assertFalse(\n+            self.domain_config_api.obtain_registration(\n+                self.domain[\'id\'], type))\n+\n+        # Make sure we can read who does have it\n+        self.assertEqual(\n+            self.domain[\'id\'],\n+            self.domain_config_api.read_registration(type))\n+\n+        # Make sure releasing it is silent if the domain specified doesn\'t\n+        # have the registration\n+        domain2 = {\'id\': uuid.uuid4().hex, \'name\': uuid.uuid4().hex}\n+        self.resource_api.create_domain(domain2[\'id\'], domain2)\n+        self.domain_config_api.release_registration(\n+            domain2[\'id\'], type=type)\n+\n+        # If nobody has the type registered, then trying to read it should\n+        # raise ConfigRegistrationNotFound\n+        self.domain_config_api.release_registration(\n+            self.domain[\'id\'], type=type)\n+        self.assertRaises(exception.ConfigRegistrationNotFound,\n+                          self.domain_config_api.read_registration,\n+                          type)\n+\n+        # Finally check multiple registrations are cleared if you free the\n+        # registration without specifying the type\n+        type2 = uuid.uuid4().hex\n+        self.domain_config_api.obtain_registration(\n+            self.domain[\'id\'], type)\n+        self.domain_config_api.obtain_registration(\n+            self.domain[\'id\'], type2)\n+        self.domain_config_api.release_registration(self.domain[\'id\'])\n+        self.assertRaises(exception.ConfigRegistrationNotFound,\n+                          self.domain_config_api.read_registration,\n+                          type)\n+        self.assertRaises(exception.ConfigRegistrationNotFound,\n+                          self.domain_config_api.read_registration,\n+                          type2)\ndiff --git a/keystone/tests/unit/test_backend_ldap.py b/keystone/tests/unit/test_backend_ldap.py\nindex 8bdc8145b..16d54973e 100644\n--- a/keystone/tests/unit/test_backend_ldap.py\n+++ b/keystone/tests/unit/test_backend_ldap.py\n@@ -2943,15 +2943,100 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity):\n             domain_cfgs.get_domain_conf(CONF.identity.default_domain_id))\n         self.assertEqual(CONF.ldap.url, default_config.ldap.url)\n \n-    def test_setting_sql_driver_raises_exception(self):\n-        """Ensure setting of domain specific sql driver is prevented."""\n+    def test_setting_multiple_sql_driver_raises_exception(self):\n+        """Ensure setting multiple domain specific sql drivers is prevented."""\n \n         new_config = {\'identity\': {\'driver\': \'sql\'}}\n         self.domain_config_api.create_config(\n             CONF.identity.default_domain_id, new_config)\n-        self.assertRaises(exception.InvalidDomainConfig,\n+        self.identity_api.domain_configs.get_domain_conf(\n+            CONF.identity.default_domain_id)\n+        self.domain_config_api.create_config(self.domains[\'domain1\'][\'id\'],\n+                                             new_config)\n+        self.assertRaises(exception.MultipleSQLDriversInConfig,\n                           self.identity_api.domain_configs.get_domain_conf,\n-                          CONF.identity.default_domain_id)\n+                          self.domains[\'domain1\'][\'id\'])\n+\n+    def test_same_domain_gets_sql_driver(self):\n+        """Ensure we can set an SQL driver if we have had it before."""\n+\n+        new_config = {\'identity\': {\'driver\': \'sql\'}}\n+        self.domain_config_api.create_config(\n+            CONF.identity.default_domain_id, new_config)\n+        self.identity_api.domain_configs.get_domain_conf(\n+            CONF.identity.default_domain_id)\n+\n+        # By using a slightly different config, we cause the driver to be\n+        # reloaded...and hence check if we can reuse the sql driver\n+        new_config = {\'identity\': {\'driver\': \'sql\'},\n+                      \'ldap\': {\'url\': \'fake://memory1\'}}\n+        self.domain_config_api.create_config(\n+            CONF.identity.default_domain_id, new_config)\n+        self.identity_api.domain_configs.get_domain_conf(\n+            CONF.identity.default_domain_id)\n+\n+    def test_delete_domain_clears_sql_registration(self):\n+        """Ensure registration is deleted when a domain is deleted."""\n+\n+        domain = {\'id\': uuid.uuid4().hex, \'name\': uuid.uuid4().hex}\n+        domain = self.resource_api.create_domain(domain[\'id\'], domain)\n+        new_config = {\'identity\': {\'driver\': \'sql\'}}\n+        self.domain_config_api.create_config(domain[\'id\'], new_config)\n+        self.identity_api.domain_configs.get_domain_conf(domain[\'id\'])\n+\n+        # First show that trying to set SQL for another driver fails\n+        self.domain_config_api.create_config(self.domains[\'domain1\'][\'id\'],\n+                                             new_config)\n+        self.assertRaises(exception.MultipleSQLDriversInConfig,\n+                          self.identity_api.domain_configs.get_domain_conf,\n+                          self.domains[\'domain1\'][\'id\'])\n+        self.domain_config_api.delete_config(self.domains[\'domain1\'][\'id\'])\n+\n+        # Now we delete the domain\n+        domain[\'enabled\'] = False\n+        self.resource_api.update_domain(domain[\'id\'], domain)\n+        self.resource_api.delete_domain(domain[\'id\'])\n+\n+        # The registration should now be available\n+        self.domain_config_api.create_config(self.domains[\'domain1\'][\'id\'],\n+                                             new_config)\n+        self.identity_api.domain_configs.get_domain_conf(\n+            self.domains[\'domain1\'][\'id\'])\n+\n+    def test_orphaned_registration_does_not_prevent_getting_sql_driver(self):\n+        """Ensure we self heal an orphaned sql registration."""\n+\n+        domain = {\'id\': uuid.uuid4().hex, \'name\': uuid.uuid4().hex}\n+        domain = self.resource_api.create_domain(domain[\'id\'], domain)\n+        new_config = {\'identity\': {\'driver\': \'sql\'}}\n+        self.domain_config_api.create_config(domain[\'id\'], new_config)\n+        self.identity_api.domain_configs.get_domain_conf(domain[\'id\'])\n+\n+        # First show that trying to set SQL for another driver fails\n+        self.domain_config_api.create_config(self.domains[\'domain1\'][\'id\'],\n+                                             new_config)\n+        self.assertRaises(exception.MultipleSQLDriversInConfig,\n+                          self.identity_api.domain_configs.get_domain_conf,\n+                          self.domains[\'domain1\'][\'id\'])\n+\n+        # Now we delete the domain by using the backend driver directly,\n+        # which causes the domain to be deleted without any of the cleanup\n+        # that is in the manager (this is simulating a server process crashing\n+        # in the middle of a delete domain operation, and somehow leaving the\n+        # domain config settings in place, but the domain is deleted). We\n+        # should still be able to set another domain to SQL, since we should\n+        # self heal this issue.\n+\n+        self.resource_api.driver.delete_domain(domain[\'id\'])\n+        # Invalidate cache (so we will see the domain has gone)\n+        self.resource_api.get_domain.invalidate(\n+            self.resource_api, domain[\'id\'])\n+\n+        # The registration should now be available\n+        self.domain_config_api.create_config(self.domains[\'domain1\'][\'id\'],\n+                                             new_config)\n+        self.identity_api.domain_configs.get_domain_conf(\n+            self.domains[\'domain1\'][\'id\'])\n \n \n class DomainSpecificLDAPandSQLIdentity(\ndiff --git a/keystone/tests/unit/test_sql_upgrade.py b/keystone/tests/unit/test_sql_upgrade.py\nindex 7b14fc677..d617d445c 100644\n--- a/keystone/tests/unit/test_sql_upgrade.py\n+++ b/keystone/tests/unit/test_sql_upgrade.py\n@@ -636,6 +636,13 @@ class SqlUpgradeTests(SqlMigrateBase):\n                                  \'enabled\', \'domain_id\', \'parent_id\',\n                                  \'is_domain\'])\n \n+    def test_add_config_registration(self):\n+        config_registration = \'config_register\'\n+        self.upgrade(74)\n+        self.assertTableDoesNotExist(config_registration)\n+        self.upgrade(75)\n+        self.assertTableColumns(config_registration, [\'type\', \'domain_id\'])\n+\n     def populate_user_table(self, with_pass_enab=False,\n                             with_pass_enab_domain=False):\n         # Populate the appropriate fields in the user\n', 'commit_subject': 'Merge "Relax newly imposed sql driver restriction for domain config"', 'repo_name': 'openstack/keystone', 'committer': {'name': 'Gerrit Code Review', 'email': '61e62b213a1a56f7695845df4fc372a10cb0a73e@openstack.org', 'time_sec': '1442888986', 'tz_offset': '0', 'date': {'seconds': '1442888986'}}, 'commit_tree': '0c8de50a98f7c5d685863fe8fbfbd3658f02537f', 'commit_message': 'Merge "Relax newly imposed sql driver restriction for domain config"', 'language_name': 'Shell', 'license': 'apache-2.0', 'hunks': "@@ -636,6 +636,13 @@ class SqlUpgradeTests(SqlMigrateBase):\n                                  'enabled', 'domain_id', 'parent_id',\n                                  'is_domain'])\n \n+    def test_add_config_registration(self):\n+        config_registration = 'config_register'\n+        self.upgrade(74)\n+        self.assertTableDoesNotExist(config_registration)\n+        self.upgrade(75)\n+        self.assertTableColumns(config_registration, ['type', 'domain_id'])\n+\n     def populate_user_table(self, with_pass_enab=False,\n                             with_pass_enab_domain=False):\n         # Populate the appropriate fields in the user\n", 'addition_count': 7, 'deletion_count': 0, 'src_file': 'a/keystone/tests/unit/test_sql_upgrade.py', 'tgt_file': 'b/keystone/tests/unit/test_sql_upgrade.py', 'file_extension': '.py', 'before_file': '# Copyright 2012 OpenStack Foundation\n#\n# Licensed under the Apache License, Version 2.0 (the "License"); you may\n# not use this file except in compliance with the License. You may obtain\n# a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\n# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n# License for the specific language governing permissions and limitations\n# under the License.\n\n"""\nTest for SQL migration extensions.\n\nTo run these tests against a live database:\n\n1. Set up a blank, live database.\n2. Export database information to environment variable\n   ``OS_TEST_DBAPI_ADMIN_CONNECTION``. For example::\n\n    export OS_TEST_DBAPI_ADMIN_CONNECTION=postgresql://localhost/postgres?host=\n    /var/folders/7k/pwdhb_mj2cv4zyr0kyrlzjx40000gq/T/tmpMGqN8C&port=9824\n\n3. Run the tests using::\n\n    tox -e py39 -- keystone.tests.unit.test_sql_upgrade\n\nFor further information, see `oslo.db documentation\n<https://docs.openstack.org/oslo.db/latest/contributor/index.html#how-to-run-unit-tests>`_.\n\n.. warning::\n\n    Your database will be wiped.\n\n    Do not do this against a database with valuable data as\n    all data will be lost.\n"""\n\nimport fixtures\nfrom migrate.versioning import script\nfrom oslo_db import options as db_options\nfrom oslo_db.sqlalchemy import enginefacade\nfrom oslo_db.sqlalchemy import test_fixtures as db_fixtures\nfrom oslo_log import fixture as log_fixture\nfrom oslo_log import log\nimport sqlalchemy.exc\n\nfrom keystone.cmd import cli\nfrom keystone.common import sql\nfrom keystone.common.sql import upgrades\nimport keystone.conf\nfrom keystone.tests import unit\nfrom keystone.tests.unit import ksfixtures\n\nCONF = keystone.conf.CONF\n\n# NOTE(morganfainberg): This should be updated when each DB migration collapse\n# is done to mirror the expected structure of the DB in the format of\n# { <DB_TABLE_NAME>: [<COLUMN>, <COLUMN>, ...], ... }\nINITIAL_TABLE_STRUCTURE = {\n    \'config_register\': [\n        \'type\', \'domain_id\',\n    ],\n    \'credential\': [\n        \'id\', \'user_id\', \'project_id\', \'type\', \'extra\', \'key_hash\',\n        \'encrypted_blob\',\n    ],\n    \'endpoint\': [\n        \'id\', \'legacy_endpoint_id\', \'interface\', \'region_id\', \'service_id\',\n        \'url\', \'enabled\', \'extra\',\n    ],\n    \'group\': [\n        \'id\', \'domain_id\', \'name\', \'description\', \'extra\',\n    ],\n    \'policy\': [\n        \'id\', \'type\', \'blob\', \'extra\',\n    ],\n    \'project\': [\n        \'id\', \'name\', \'extra\', \'description\', \'enabled\', \'domain_id\',\n        \'parent_id\', \'is_domain\',\n    ],\n    \'project_option\': [\n        \'project_id\', \'option_id\', \'option_value\',\n    ],\n    \'project_tag\': [\n        \'project_id\', \'name\',\n    ],\n    \'role\': [\n        \'id\', \'name\', \'extra\', \'domain_id\', \'description\',\n    ],\n    \'role_option\': [\n        \'role_id\', \'option_id\', \'option_value\',\n    ],\n    \'service\': [\n        \'id\', \'type\', \'extra\', \'enabled\',\n    ],\n    \'token\': [\n        \'id\', \'expires\', \'extra\', \'valid\', \'trust_id\', \'user_id\',\n    ],\n    \'trust\': [\n        \'id\', \'trustor_user_id\', \'trustee_user_id\', \'project_id\',\n        \'impersonation\', \'deleted_at\', \'expires_at\', \'remaining_uses\', \'extra\',\n        \'expires_at_int\', \'redelegated_trust_id\', \'redelegation_count\',\n    ],\n    \'trust_role\': [\n        \'trust_id\', \'role_id\',\n    ],\n    \'user\': [\n        \'id\', \'extra\', \'enabled\', \'default_project_id\', \'created_at\',\n        \'last_active_at\', \'domain_id\',\n    ],\n    \'user_option\': [\n        \'user_id\', \'option_id\', \'option_value\',\n    ],\n    \'user_group_membership\': [\n        \'user_id\', \'group_id\',\n    ],\n    \'region\': [\n        \'id\', \'description\', \'parent_region_id\', \'extra\',\n    ],\n    \'assignment\': [\n        \'type\', \'actor_id\', \'target_id\', \'role_id\', \'inherited\',\n    ],\n    \'id_mapping\': [\n        \'public_id\', \'domain_id\', \'local_id\', \'entity_type\',\n    ],\n    \'whitelisted_config\': [\n        \'domain_id\', \'group\', \'option\', \'value\',\n    ],\n    \'sensitive_config\': [\n        \'domain_id\', \'group\', \'option\', \'value\',\n    ],\n    \'policy_association\': [\n        \'id\', \'policy_id\', \'endpoint_id\', \'service_id\', \'region_id\',\n    ],\n    \'identity_provider\': [\n        \'id\', \'enabled\', \'description\', \'domain_id\', \'authorization_ttl\',\n    ],\n    \'federation_protocol\': [\n        \'id\', \'idp_id\', \'mapping_id\', \'remote_id_attribute\',\n    ],\n    \'mapping\': [\n        \'id\', \'rules\',\n    ],\n    \'service_provider\': [\n        \'auth_url\', \'id\', \'enabled\', \'description\', \'sp_url\',\n        \'relay_state_prefix\',\n    ],\n    \'idp_remote_ids\': [\n        \'idp_id\', \'remote_id\',\n    ],\n    \'consumer\': [\n        \'id\', \'description\', \'secret\', \'extra\',\n    ],\n    \'request_token\': [\n        \'id\', \'request_secret\', \'verifier\', \'authorizing_user_id\',\n        \'requested_project_id\', \'role_ids\', \'consumer_id\', \'expires_at\',\n    ],\n    \'access_token\': [\n        \'id\', \'access_secret\', \'authorizing_user_id\', \'project_id\', \'role_ids\',\n        \'consumer_id\', \'expires_at\',\n    ],\n    \'revocation_event\': [\n        \'id\', \'domain_id\', \'project_id\', \'user_id\', \'role_id\', \'trust_id\',\n        \'consumer_id\', \'access_token_id\', \'issued_before\', \'expires_at\',\n        \'revoked_at\', \'audit_id\', \'audit_chain_id\',\n    ],\n    \'project_endpoint\': [\n        \'endpoint_id\', \'project_id\'\n    ],\n    \'endpoint_group\': [\n        \'id\', \'name\', \'description\', \'filters\',\n    ],\n    \'project_endpoint_group\': [\n        \'endpoint_group_id\', \'project_id\',\n    ],\n    \'implied_role\': [\n        \'prior_role_id\', \'implied_role_id\',\n    ],\n    \'local_user\': [\n        \'id\', \'user_id\', \'domain_id\', \'name\', \'failed_auth_count\',\n        \'failed_auth_at\',\n    ],\n    \'password\': [\n        \'id\', \'local_user_id\', \'created_at\', \'expires_at\',\n        \'self_service\', \'password_hash\', \'created_at_int\', \'expires_at_int\',\n    ],\n    \'federated_user\': [\n        \'id\', \'user_id\', \'idp_id\', \'protocol_id\', \'unique_id\', \'display_name\',\n    ],\n    \'nonlocal_user\': [\n        \'domain_id\', \'name\', \'user_id\',\n    ],\n    \'system_assignment\': [\n        \'type\', \'actor_id\', \'target_id\', \'role_id\', \'inherited\',\n    ],\n    \'registered_limit\': [\n        \'internal_id\', \'id\', \'service_id\', \'region_id\', \'resource_name\',\n        \'default_limit\', \'description\',\n    ],\n    \'limit\': [\n        \'internal_id\', \'id\', \'project_id\', \'resource_limit\', \'description\',\n        \'registered_limit_id\', \'domain_id\',\n    ],\n    \'application_credential\': [\n        \'internal_id\', \'id\', \'name\', \'secret_hash\', \'description\', \'user_id\',\n        \'project_id\', \'expires_at\', \'system\', \'unrestricted\',\n    ],\n    \'application_credential_role\': [\n        \'application_credential_id\', \'role_id\',\n    ],\n    \'access_rule\': [\n        \'id\', \'service\', \'path\', \'method\', \'external_id\', \'user_id\',\n    ],\n    \'application_credential_access_rule\': [\n        \'application_credential_id\', \'access_rule_id\',\n    ],\n    \'expiring_user_group_membership\': [\n        \'user_id\', \'group_id\', \'idp_id\', \'last_verified\',\n    ],\n}\n\n\nclass MigrateBase(\n    db_fixtures.OpportunisticDBTestMixin,\n):\n    """Test complete orchestration between all database phases."""\n\n    def setUp(self):\n        super().setUp()\n\n        self.useFixture(log_fixture.get_logging_handle_error_fixture())\n        self.stdlog = self.useFixture(ksfixtures.StandardLogging())\n        self.useFixture(ksfixtures.WarningsFixture())\n\n        self.engine = enginefacade.writer.get_engine()\n        self.sessionmaker = enginefacade.writer.get_sessionmaker()\n\n        # NOTE(dstanek): Clear out sqlalchemy-migrate\'s script cache to allow\n        # us to have multiple repos (expand, migrate, contract) where the\n        # modules have the same name (001_awesome.py).\n        self.addCleanup(script.PythonScript.clear)\n\n        db_options.set_defaults(CONF, connection=self.engine.url)\n\n        # Override keystone\'s context manager to be oslo.db\'s global context\n        # manager.\n        sql.core._TESTING_USE_GLOBAL_CONTEXT_MANAGER = True\n        self.addCleanup(setattr,\n                        sql.core, \'_TESTING_USE_GLOBAL_CONTEXT_MANAGER\', False)\n        self.addCleanup(sql.cleanup)\n\n    def expand(self):\n        """Expand database schema."""\n        upgrades.expand_schema(engine=self.engine)\n\n    def contract(self):\n        """Contract database schema."""\n        upgrades.contract_schema(engine=self.engine)\n\n    @property\n    def metadata(self):\n        """A collection of tables and their associated schemas."""\n        return sqlalchemy.MetaData(self.engine)\n\n    def load_table(self, name):\n        table = sqlalchemy.Table(\n            name, self.metadata, autoload_with=self.engine,\n        )\n        return table\n\n    def assertTableDoesNotExist(self, table_name):\n        """Assert that a given table exists cannot be selected by name."""\n        # Switch to a different metadata otherwise you might still\n        # detect renamed or dropped tables\n        try:\n            sqlalchemy.Table(\n                table_name, self.metadata, autoload_with=self.engine,\n            )\n        except sqlalchemy.exc.NoSuchTableError:\n            pass\n        else:\n            raise AssertionError(\'Table "%s" already exists\' % table_name)\n\n    def assertTableColumns(self, table_name, expected_cols):\n        """Assert that the table contains the expected set of columns."""\n        table = self.load_table(table_name)\n        actual_cols = [col.name for col in table.columns]\n        # Check if the columns are equal, but allow for a different order,\n        # which might occur after an upgrade followed by a downgrade\n        self.assertCountEqual(expected_cols, actual_cols,\n                              \'%s table\' % table_name)\n\n    def test_db_sync_check(self):\n        checker = cli.DbSync()\n\n        # If the expand repository doesn\'t exist yet, then we need to make sure\n        # we advertise that `--expand` must be run first.\n        log_info = self.useFixture(fixtures.FakeLogger(level=log.INFO))\n        status = checker.check_db_sync_status()\n        self.assertIn("keystone-manage db_sync --expand", log_info.output)\n        self.assertEqual(status, 2)\n\n        # Assert the correct message is printed when migrate is ahead of\n        # contract\n        self.expand()\n        log_info = self.useFixture(fixtures.FakeLogger(level=log.INFO))\n        status = checker.check_db_sync_status()\n        self.assertIn("keystone-manage db_sync --contract", log_info.output)\n        self.assertEqual(status, 4)\n\n        # Assert the correct message gets printed when all commands are on\n        # the same version\n        self.contract()\n        log_info = self.useFixture(fixtures.FakeLogger(level=log.INFO))\n        status = checker.check_db_sync_status()\n        self.assertIn("All db_sync commands are upgraded", log_info.output)\n        self.assertEqual(status, 0)\n\n    def test_upgrade_add_initial_tables(self):\n        self.expand()\n        for table in INITIAL_TABLE_STRUCTURE:\n            self.assertTableColumns(table, INITIAL_TABLE_STRUCTURE[table])\n\n\nclass FullMigrationSQLite(MigrateBase, unit.TestCase):\n    pass\n\n\nclass FullMigrationMySQL(MigrateBase, unit.TestCase):\n    FIXTURE = db_fixtures.MySQLOpportunisticFixture\n\n\nclass FullMigrationPostgreSQL(MigrateBase, unit.TestCase):\n    FIXTURE = db_fixtures.PostgresqlOpportunisticFixture\n                                 \'enabled\', \'domain_id\', \'parent_id\',\n                                 \'is_domain\'])\n\n    def populate_user_table(self, with_pass_enab=False,\n                            with_pass_enab_domain=False):\n        # Populate the appropriate fields in the user\n', 'message': 'Merge "Relax newly imposed sql driver restriction for domain config"', 'author_name': 'Gerrit Code Review', 'lang': 'py'}