Use scope in v3 identity client
Certain identity v3 API calls require a domain scoped token when
policy.v3cloudsample.json is used.
Introduce a new config flag to tell Tempest that keystone in the
targe cloud requires domain scoped tokens for admin actions.
Modify the v3 client managers used to obtain v3 admin identity
clients to request the domain scope when the flag is turned on.
Co-authored by: Roxana Gherle <roxana.gherle@hp.com>
Change-Id: I91ca907992428a5a14fb8d48a4fad105d2906e27
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index 9a7ce15..743b575 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -26,6 +26,11 @@
- Run tests for admin APIs
- Generate test credentials on the fly (see `Dynamic Credentials`_)
+When keystone uses a policy that requires domain scoped tokens for admin
+actions, the flag ``admin_domain_scope`` must be set to ``True``.
+The admin user configured, if any, must have a role assigned to the domain to
+be usable.
+
Tempest allows for configuring pre-provisioned test credentials as well.
This can be done using the accounts.yaml file (see
`Pre-Provisioned Credentials`_). This file is used to specify an arbitrary
@@ -87,6 +92,14 @@
by dynamic credentials. This option will not have any effect when Tempest is not
configured to use dynamic credentials.
+When the ``admin_domain_scope`` option is set to ``True``, provisioned admin
+accounts will be assigned a role on domain configured in
+``default_credentials_domain_name``. This will make the accounts provisioned
+usable in a cloud where domain scoped tokens are required by keystone for
+admin operations. Note that the the initial pre-provision admin accounts,
+configured in tempest.conf, must have a role on the same domain as well, for
+Dynamic Credentials to work.
+
Pre-Provisioned Credentials
"""""""""""""""""""""""""""
@@ -124,6 +137,18 @@
to the tests using the credentials, and failure to do this will likely cause
unexpected failures in some tests.
+When the keystone in the target cloud requires domain scoped tokens to
+perform admin actions, all pre-provisioned admin users must have a role
+assigned on the domain where test accounts a provisioned.
+The option ``admin_domain_scope`` is used to tell tempest that domain scoped
+tokens shall be used. ``default_credentials_domain_name`` is the domain where
+test accounts are expected to be provisioned if no domain is specified.
+
+Note that if credentials are pre-provisioned via ``tempest account-generator``
+the role on the domain will be assigned automatically for you, as long as
+``admin_domain_scope`` as ``default_credentials_domain_name`` are configured
+properly in tempest.conf.
+
Pre-Provisioned Credentials are also know as accounts.yaml or accounts file.
Compute
diff --git a/tempest/api/identity/admin/v3/test_trusts.py b/tempest/api/identity/admin/v3/test_trusts.py
index 09ae468..9c8f1f6 100644
--- a/tempest/api/identity/admin/v3/test_trusts.py
+++ b/tempest/api/identity/admin/v3/test_trusts.py
@@ -98,7 +98,8 @@
password=self.trustor_password,
user_domain_id='default',
tenant_name=self.trustor_project_name,
- project_domain_id='default')
+ project_domain_id='default',
+ domain_id='default')
os = clients.Manager(credentials=creds)
self.trustor_client = os.trusts_client
@@ -266,7 +267,18 @@
@test.attr(type='smoke')
@test.idempotent_id('4773ebd5-ecbf-4255-b8d8-b63e6f72b65d')
def test_get_trusts_all(self):
+
+ # Simple function that can be used for cleanup
+ def set_scope(auth_provider, scope):
+ auth_provider.scope = scope
+
self.create_trust()
+ # Listing trusts can be done by trustor, by trustee, or without
+ # any filter if scoped to a project, so we must ensure token scope is
+ # project for this test.
+ original_scope = self.os_adm.auth_provider.scope
+ set_scope(self.os_adm.auth_provider, 'project')
+ self.addCleanup(set_scope, self.os_adm.auth_provider, original_scope)
trusts_get = self.trusts_client.list_trusts()['trusts']
trusts = [t for t in trusts_get
if t['id'] == self.trust_id]
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 2d4ff17..31420d1 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -162,6 +162,12 @@
cls.creds_client = cls.os_adm.credentials_client
cls.groups_client = cls.os_adm.groups_client
cls.projects_client = cls.os_adm.projects_client
+ if CONF.identity.admin_domain_scope:
+ # NOTE(andreaf) When keystone policy requires it, the identity
+ # admin clients for these tests shall use 'domain' scoped tokens.
+ # As the client manager is already created by the base class,
+ # we set the scope for the inner auth provider.
+ cls.os_adm.auth_provider.scope = 'domain'
@classmethod
def resource_setup(cls):
diff --git a/tempest/common/credentials_factory.py b/tempest/common/credentials_factory.py
index 4873fcf..c22afc1 100644
--- a/tempest/common/credentials_factory.py
+++ b/tempest/common/credentials_factory.py
@@ -193,17 +193,21 @@
# Wrapper around auth.get_credentials to use the configured identity version
-# is none is specified
+# if none is specified
def get_credentials(fill_in=True, identity_version=None, **kwargs):
params = dict(DEFAULT_PARAMS, **kwargs)
identity_version = identity_version or CONF.identity.auth_version
# In case of "v3" add the domain from config if not specified
+ # To honour the "default_credentials_domain_name", if not domain
+ # field is specified at all, add it the credential dict.
if identity_version == 'v3':
domain_fields = set(x for x in auth.KeystoneV3Credentials.ATTRIBUTES
if 'domain' in x)
if not domain_fields.intersection(kwargs.keys()):
domain_name = CONF.auth.default_credentials_domain_name
- params['user_domain_name'] = domain_name
+ # NOTE(andreaf) Setting domain_name implicitly sets user and
+ # project domain names, if they are None
+ params['domain_name'] = domain_name
auth_url = CONF.identity.uri_v3
else:
diff --git a/tempest/common/dynamic_creds.py b/tempest/common/dynamic_creds.py
index 3a3e3c2..0b92c15 100644
--- a/tempest/common/dynamic_creds.py
+++ b/tempest/common/dynamic_creds.py
@@ -96,8 +96,15 @@
os.networks_client, os.routers_client, os.subnets_client,
os.ports_client, os.security_groups_client)
else:
- return (os.identity_v3_client, os.projects_client,
- os.users_v3_client, os.roles_v3_client, os.domains_client,
+ # We use a dedicated client manager for identity client in case we
+ # need a different token scope for them.
+ scope = 'domain' if CONF.identity.admin_domain_scope else 'project'
+ identity_os = clients.Manager(self.default_admin_creds,
+ scope=scope)
+ return (identity_os.identity_v3_client,
+ identity_os.projects_client,
+ identity_os.users_v3_client, identity_os.roles_v3_client,
+ identity_os.domains_client,
os.networks_client, os.routers_client,
os.subnets_client, os.ports_client,
os.security_groups_client)
@@ -136,7 +143,8 @@
self.creds_client.assign_user_role(user, project,
self.admin_role)
role_assigned = True
- if self.identity_version == 'v3':
+ if (self.identity_version == 'v3' and
+ CONF.identity.admin_domain_scope):
self.creds_client.assign_user_role_on_domain(
user, CONF.identity.admin_role)
# Add roles specified in config file
diff --git a/tempest/config.py b/tempest/config.py
index 3810ceb..1f88871 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -166,6 +166,10 @@
cfg.StrOpt('default_domain_id',
default='default',
help="ID of the default domain"),
+ cfg.BoolOpt('admin_domain_scope',
+ default=False,
+ help="Whether keystone identity v3 policy required "
+ "a domain scoped token to use admin APIs")
]
identity_feature_group = cfg.OptGroup(name='identity-feature-enabled',
diff --git a/tempest/lib/auth.py b/tempest/lib/auth.py
index 974ba82..213ab8a 100644
--- a/tempest/lib/auth.py
+++ b/tempest/lib/auth.py
@@ -510,6 +510,7 @@
endpoint_type = endpoint_type.replace('URL', '')
_base_url = None
catalog = _auth_data.get('catalog', [])
+
# Select entries with matching service type
service_catalog = [ep for ep in catalog if ep['type'] == service]
if len(service_catalog) > 0:
@@ -519,10 +520,20 @@
# NOTE(andreaf) If there's no catalog at all and the service
# is identity, it's a valid use case. Having a non-empty
# catalog with no identity in it is not valid instead.
+ msg = ('Got an empty catalog. Scope: %s. '
+ 'Falling back to configured URL for %s: %s')
+ LOG.debug(msg, self.scope, service, self.auth_url)
return apply_url_filters(self.auth_url, filters)
else:
# No matching service
- raise exceptions.EndpointNotFound(service)
+ msg = ('No matching service found in the catalog.\n'
+ 'Scope: %s, Credentials: %s\n'
+ 'Auth data: %s\n'
+ 'Service: %s, Region: %s, endpoint_type: %s\n'
+ 'Catalog: %s')
+ raise exceptions.EndpointNotFound(msg % (
+ self.scope, self.credentials, _auth_data, service, region,
+ endpoint_type, catalog))
# Filter by endpoint type (interface)
filtered_catalog = [ep for ep in service_catalog if
ep['interface'] == endpoint_type]