Merge "Fix dsvm typo to dscv"
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 7e4503d..c2df0b6 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -135,7 +135,7 @@
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
-#html_use_smartypants = True
+html_use_smartypants = False
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index 32cd3ef..e4b104f 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/doc/source/library.rst b/doc/source/library.rst
index a89512c..6a2fb83 100644
--- a/doc/source/library.rst
+++ b/doc/source/library.rst
@@ -66,3 +66,4 @@
library/rest_client
library/utils
library/api_microversion_testing
+ library/auth
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/cmd/account_generator.py b/tempest/cmd/account_generator.py
index 9e1d391..0a5a41b 100755
--- a/tempest/cmd/account_generator.py
+++ b/tempest/cmd/account_generator.py
@@ -38,17 +38,17 @@
of your cloud to operate properly. The corresponding info can be given either
through CLI options or environment variables.
-You're probably familiar with these, but just to remind::
+You're probably familiar with these, but just to remind:
- +----------+---------------------------+----------------------+
- | Param | CLI | Environment Variable |
- +----------+---------------------------+----------------------+
- | Username | --os-username | OS_USERNAME |
- | Password | --os-password | OS_PASSWORD |
- | Project | --os-project-name | OS_PROJECT_NAME |
- | Tenant | --os-tenant-name (depr.) | OS_TENANT_NAME |
- | Domain | --os-domain-name | OS_DOMAIN_NAME |
- +----------+---------------------------+----------------------+
+======== ======================== ====================
+Param CLI Environment Variable
+======== ======================== ====================
+Username --os-username OS_USERNAME
+Password --os-password OS_PASSWORD
+Project --os-project-name OS_PROJECT_NAME
+Tenant --os-tenant-name (depr.) OS_TENANT_NAME
+Domain --os-domain-name OS_DOMAIN_NAME
+======== ======================== ====================
Optional Arguments
------------------
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 8a388be..425758f 100644
--- a/tempest/lib/auth.py
+++ b/tempest/lib/auth.py
@@ -180,7 +180,7 @@
:param headers: HTTP headers of the request
:param body: HTTP body in case of POST / PUT
:param filters: select a base URL out of the catalog
- :returns a Tuple (url, headers, body)
+ :return: a Tuple (url, headers, body)
"""
orig_req = dict(url=url, headers=headers, body=body)
@@ -224,6 +224,7 @@
Configure auth provider to provide alt authentication data
on a part of the *next* auth_request. If credentials are None,
set invalid data.
+
:param request_part: request part to contain invalid auth: url,
headers, body
:param auth_data: alternative auth_data from which to get the
@@ -370,16 +371,19 @@
"""Base URL from catalog
:param filters: Used to filter results
- Filters can be:
- - service: service type name such as compute, image, etc.
- - region: service region name
- - name: service name, only if service exists
- - endpoint_type: type of endpoint such as
- adminURL, publicURL, internalURL
- - api_version: the version of api used to replace catalog version
- - skip_path: skips the suffix path of the url and uses base URL
- :rtype string
- :return url with filters applied
+
+ Filters can be:
+
+ - service: service type name such as compute, image, etc.
+ - region: service region name
+ - name: service name, only if service exists
+ - endpoint_type: type of endpoint such as
+ adminURL, publicURL, internalURL
+ - api_version: the version of api used to replace catalog version
+ - skip_path: skips the suffix path of the url and uses base URL
+
+ :rtype: string
+ :return: url with filters applied
"""
if auth_data is None:
auth_data = self.get_auth()
@@ -499,16 +503,19 @@
'identity', we can use the original auth URL to build the base_url.
:param filters: Used to filter results
- Filters can be:
- - service: service type name such as compute, image, etc.
- - region: service region name
- - name: service name, only if service exists
- - endpoint_type: type of endpoint such as
- adminURL, publicURL, internalURL
- - api_version: the version of api used to replace catalog version
- - skip_path: skips the suffix path of the url and uses base URL
- :rtype string
- :return url with filters applied
+
+ Filters can be:
+
+ - service: service type name such as compute, image, etc.
+ - region: service region name
+ - name: service name, only if service exists
+ - endpoint_type: type of endpoint such as
+ adminURL, publicURL, internalURL
+ - api_version: the version of api used to replace catalog version
+ - skip_path: skips the suffix path of the url and uses base URL
+
+ :rtype: string
+ :return: url with filters applied
"""
if auth_data is None:
auth_data = self.get_auth()
@@ -525,6 +532,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:
@@ -542,10 +550,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]