Add tenant isolation for scenario tests
Currently the scenario tests have a race condition between tests
that create a new security group rule to allow ssh on the default
security group. A security group rule can only be added to the same
security group once and when running tempest in parallel this
will cause a possible error because the scenario tests were running
in the same tenant which have the same default security group. This
commit fixes this by adding tenant isolation to the scenario tests
so that each scenario test will have it's own tenant and thus it's
own default security group.
To add tenant isolation for the scenario tests and use the official
python clients without duplicating code the isolated_credential code
was broken out as a separate class in a new file in tempest/common.
This class will either use the tempest identity client or
python-keystone client depending on whether it for an api test or a
scenario test.
Part of blueprint speed-up-tempest
Change-Id: Icaffd0c40f55d94014ab4758b392bf5c38f0b0f6
diff --git a/tempest/api/compute/admin/test_quotas.py b/tempest/api/compute/admin/test_quotas.py
index d8d162e..4e42dd0 100644
--- a/tempest/api/compute/admin/test_quotas.py
+++ b/tempest/api/compute/admin/test_quotas.py
@@ -39,7 +39,8 @@
# NOTE(afazekas): these test cases should always create and use a new
# tenant most of them should be skipped if we can't do that
if cls.config.compute.allow_tenant_isolation:
- cls.demo_tenant_id = cls.isolated_creds[0][0]['tenantId']
+ cls.demo_tenant_id = cls.isolated_creds.get_primary_user().get(
+ 'tenantId')
else:
cls.demo_tenant_id = [tnt['id'] for tnt in tenants if tnt['name']
== cls.config.identity.tenant_name][0]
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index d40b0e0..15e28fd 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -19,6 +19,7 @@
from tempest.api import compute
from tempest import clients
+from tempest.common import isolated_creds
from tempest.common.utils.data_utils import parse_image_id
from tempest.common.utils.data_utils import rand_name
from tempest.openstack.common import log as logging
@@ -38,10 +39,10 @@
if not cls.config.service_available.nova:
skip_msg = ("%s skipped as nova is not available" % cls.__name__)
raise cls.skipException(skip_msg)
- cls.isolated_creds = []
+ cls.isolated_creds = isolated_creds.IsolatedCreds(cls.__name__)
if cls.config.compute.allow_tenant_isolation:
- creds = cls._get_isolated_creds()
+ creds = cls.isolated_creds.get_primary_creds()
username, tenant_name, password = creds
os = clients.Manager(username=username,
password=password,
@@ -108,7 +109,8 @@
def tearDownClass(cls):
cls.clear_images()
cls.clear_servers()
- cls._clear_isolated_creds()
+ cls.isolated_creds.clear_isolated_creds()
+ super(BaseComputeTest, cls).tearDownClass()
@classmethod
def create_server(cls, **kwargs):
@@ -187,7 +189,7 @@
"in configuration.")
raise cls.skipException(msg)
if cls.config.compute.allow_tenant_isolation:
- creds = cls._get_isolated_creds(admin=True)
+ creds = cls.isolated_creds.get_admin_creds()
admin_username, admin_tenant_name, admin_password = creds
cls.os_adm = clients.Manager(username=admin_username,
password=admin_password,
diff --git a/tempest/api/compute/images/test_images.py b/tempest/api/compute/images/test_images.py
index 4f9364b..2f0ed6b 100644
--- a/tempest/api/compute/images/test_images.py
+++ b/tempest/api/compute/images/test_images.py
@@ -39,7 +39,7 @@
if compute.MULTI_USER:
if cls.config.compute.allow_tenant_isolation:
- creds = cls._get_isolated_creds()
+ creds = cls.isolated_creds.get_alt_creds()
username, tenant_name, password = creds
cls.alt_manager = clients.Manager(username=username,
password=password,
diff --git a/tempest/api/compute/images/test_images_oneserver.py b/tempest/api/compute/images/test_images_oneserver.py
index 14eced2..0052a30 100644
--- a/tempest/api/compute/images/test_images_oneserver.py
+++ b/tempest/api/compute/images/test_images_oneserver.py
@@ -54,7 +54,7 @@
if compute.MULTI_USER:
if cls.config.compute.allow_tenant_isolation:
- creds = cls._get_isolated_creds()
+ creds = cls.isolated_creds.get_alt_creds()
username, tenant_name, password = creds
cls.alt_manager = clients.Manager(username=username,
password=password,
diff --git a/tempest/api/compute/servers/test_list_servers_negative.py b/tempest/api/compute/servers/test_list_servers_negative.py
index bad4a11..14ea174 100644
--- a/tempest/api/compute/servers/test_list_servers_negative.py
+++ b/tempest/api/compute/servers/test_list_servers_negative.py
@@ -35,7 +35,7 @@
if compute.MULTI_USER:
if cls.config.compute.allow_tenant_isolation:
- creds = cls._get_isolated_creds()
+ creds = cls.isolated_creds.get_alt_creds()
username, tenant_name, password = creds
cls.alt_manager = clients.Manager(username=username,
password=password,
diff --git a/tempest/api/compute/test_authorization.py b/tempest/api/compute/test_authorization.py
index 55dba97..60297a9 100644
--- a/tempest/api/compute/test_authorization.py
+++ b/tempest/api/compute/test_authorization.py
@@ -41,7 +41,7 @@
cls.security_client = cls.os.security_groups_client
if cls.config.compute.allow_tenant_isolation:
- creds = cls._get_isolated_creds()
+ creds = cls.isolated_creds.get_alt_creds()
username, tenant_name, password = creds
cls.alt_manager = clients.Manager(username=username,
password=password,
diff --git a/tempest/api/image/base.py b/tempest/api/image/base.py
index 086c50e..4e61495 100644
--- a/tempest/api/image/base.py
+++ b/tempest/api/image/base.py
@@ -15,6 +15,7 @@
# under the License.
from tempest import clients
+from tempest.common import isolated_creds
from tempest.common.utils.data_utils import rand_name
from tempest import exceptions
from tempest.openstack.common import log as logging
@@ -28,14 +29,14 @@
@classmethod
def setUpClass(cls):
- cls.isolated_creds = []
cls.created_images = []
cls._interface = 'json'
+ cls.isolated_creds = isolated_creds.IsolatedCreds(cls.__name__)
if not cls.config.service_available.glance:
skip_msg = ("%s skipped as glance is not available" % cls.__name__)
raise cls.skipException(skip_msg)
if cls.config.compute.allow_tenant_isolation:
- creds = cls._get_isolated_creds()
+ creds = cls.isolated_creds.get_primary_creds()
username, tenant_name, password = creds
cls.os = clients.Manager(username=username,
password=password,
@@ -53,7 +54,8 @@
for image_id in cls.created_images:
cls.client.wait_for_resource_deletion(image_id)
- cls._clear_isolated_creds()
+ cls.isolated_creds.clear_isolated_creds()
+ super(BaseImageTest, cls).tearDownClass()
@classmethod
def create_image(cls, **kwargs):
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index 379baa2..52ab5b7 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -18,6 +18,7 @@
import time
from tempest import clients
+from tempest.common import isolated_creds
from tempest.openstack.common import log as logging
import tempest.test
@@ -30,14 +31,14 @@
@classmethod
def setUpClass(cls):
- cls.isolated_creds = []
+ cls.isolated_creds = isolated_creds.IsolatedCreds(cls.__name__)
if not cls.config.service_available.cinder:
skip_msg = ("%s skipped as Cinder is not available" % cls.__name__)
raise cls.skipException(skip_msg)
if cls.config.compute.allow_tenant_isolation:
- creds = cls._get_isolated_creds()
+ creds = cls.isolated_creds.get_primary_creds()
username, tenant_name, password = creds
os = clients.Manager(username=username,
password=password,
@@ -67,7 +68,8 @@
def tearDownClass(cls):
cls.clear_snapshots()
cls.clear_volumes()
- cls._clear_isolated_creds()
+ cls.isolated_creds.clear_isolated_creds()
+ super(BaseVolumeTest, cls).tearDownClass()
@classmethod
def create_snapshot(cls, volume_id=1, **kwargs):
@@ -149,7 +151,7 @@
"in configuration.")
raise cls.skipException(msg)
if cls.config.compute.allow_tenant_isolation:
- creds = cls._get_isolated_creds(admin=True)
+ creds = cls.isolated_creds.get_admin_creds()
admin_username, admin_tenant_name, admin_password = creds
cls.os_adm = clients.Manager(username=admin_username,
password=admin_password,
diff --git a/tempest/common/isolated_creds.py b/tempest/common/isolated_creds.py
new file mode 100644
index 0000000..22e1bd2
--- /dev/null
+++ b/tempest/common/isolated_creds.py
@@ -0,0 +1,248 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 IBM Corp.
+#
+# 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.
+
+import keystoneclient.v2_0.client
+
+from tempest import clients
+from tempest.common.utils.data_utils import rand_name
+from tempest import config
+from tempest import exceptions
+from tempest.openstack.common import log as logging
+
+LOG = logging.getLogger(__name__)
+
+
+class IsolatedCreds(object):
+
+ def __init__(self, name, tempest_client=True, interface='json',
+ password='pass'):
+ self.isolated_creds = {}
+ self.name = name
+ self.config = config.TempestConfig()
+ self.tempest_client = tempest_client
+ self.interface = interface
+ self.password = password
+ self.admin_client = self._get_identity_admin_client()
+
+ def _get_keystone_client(self):
+ username = self.config.identity.admin_username
+ password = self.config.identity.admin_password
+ tenant_name = self.config.identity.admin_tenant_name
+ auth_url = self.config.identity.uri
+ dscv = self.config.identity.disable_ssl_certificate_validation
+ return keystoneclient.v2_0.client.Client(username=username,
+ password=password,
+ tenant_name=tenant_name,
+ auth_url=auth_url,
+ insecure=dscv)
+
+ def _get_identity_admin_client(self):
+ """
+ Returns an instance of the Identity Admin API client
+ """
+ if self.tempest_client:
+ os = clients.AdminManager(interface=self.interface)
+ admin_client = os.identity_client
+ else:
+ admin_client = self._get_keystone_client()
+ return admin_client
+
+ def _create_tenant(self, name, description):
+ if self.tempest_client:
+ resp, tenant = self.admin_client.create_tenant(
+ name=name, description=description)
+ else:
+ tenant = self.admin_client.tenants.create(name,
+ description=description)
+ return tenant
+
+ def _get_tenant_by_name(self, name):
+ if self.tempest_client:
+ resp, tenant = self.admin_client.get_tenant_by_name(name)
+ else:
+ tenants = self.admin_client.tenants.list()
+ for ten in tenants:
+ if ten['name'] == name:
+ tenant = ten
+ raise exceptions.NotFound('No such tenant')
+ return tenant
+
+ def _create_user(self, username, password, tenant, email):
+ if self.tempest_client:
+ resp, user = self.admin_client.create_user(username, password,
+ tenant['id'], email)
+ else:
+ user = self.admin_client.users.create(username, password, email,
+ tenant_id=tenant.id)
+ return user
+
+ def _get_user(self, tenant, username):
+ if self.tempest_client:
+ resp, user = self.admin_client.get_user_by_username(tenant['id'],
+ username)
+ else:
+ user = self.admin_client.users.get(username)
+ return user
+
+ def _list_roles(self):
+ if self.tempest_client:
+ resp, roles = self.admin_client.list_roles()
+ else:
+ roles = self.admin_client.roles.list()
+ return roles
+
+ def _assign_user_role(self, tenant, user, role):
+ if self.tempest_client:
+ self.admin_client.assign_user_role(tenant, user, role)
+ else:
+ self.admin_client.roles.add_user_role(user, role, tenant=tenant)
+
+ def _delete_user(self, user):
+ if self.tempest_client:
+ self.admin_client.delete_user(user)
+ else:
+ self.admin_client.users.delete(user)
+
+ def _delete_tenant(self, tenant):
+ if self.tempest_client:
+ self.admin_client.delete_tenant(tenant)
+ else:
+ self.admin_client.tenants.delete(tenant)
+
+ def _create_creds(self, suffix=None, admin=False):
+ rand_name_root = rand_name(self.name)
+ if suffix:
+ rand_name_root += suffix
+ tenant_name = rand_name_root + "-tenant"
+ tenant_desc = tenant_name + "-desc"
+ rand_name_root = rand_name(self.name)
+ tenant = self._create_tenant(name=tenant_name,
+ description=tenant_desc)
+ if suffix:
+ rand_name_root += suffix
+ username = rand_name_root + "-user"
+ email = rand_name_root + "@example.com"
+ user = self._create_user(username, self.password,
+ tenant, email)
+ if admin:
+ role = None
+ try:
+ roles = self._list_roles()
+ if self.tempest_client:
+ role = next(r for r in roles if r['name'] == 'admin')
+ else:
+ role = next(r for r in roles if r.name == 'admin')
+ except StopIteration:
+ msg = "No admin role found"
+ raise exceptions.NotFound(msg)
+ if self.tempest_client:
+ self._assign_user_role(tenant['id'], user['id'], role['id'])
+ else:
+ self._assign_user_role(tenant.id, user.id, role.id)
+ return user, tenant
+
+ def _get_cred_names(self, user, tenant):
+ if self.tempest_client:
+ username = user.get('name')
+ tenant_name = tenant.get('name')
+ else:
+ username = user.name
+ tenant_name = tenant.name
+ return username, tenant_name
+
+ def get_primary_tenant(self):
+ return self.isolated_creds.get('primary')[1]
+
+ def get_primary_user(self):
+ return self.isolated_creds.get('primary')[0]
+
+ def get_alt_tenant(self):
+ return self.isolated_creds.get('alt')[1]
+
+ def get_alt_user(self):
+ return self.isolated_creds.get('alt')[0]
+
+ def get_admin_tenant(self):
+ return self.isolated_creds.get('admin')[1]
+
+ def get_admin_user(self):
+ return self.isolated_creds.get('admin')[0]
+
+ def get_primary_creds(self):
+ if self.isolated_creds.get('primary'):
+ user, tenant = self.isolated_creds['primary']
+ username, tenant_name = self._get_cred_names(user, tenant)
+ else:
+ user, tenant = self._create_creds()
+ username, tenant_name = self._get_cred_names(user, tenant)
+ self.isolated_creds['primary'] = (user, tenant)
+ LOG.info("Aquired isolated creds:\n user: %s, tenant: %s"
+ % (username, tenant_name))
+ return username, tenant_name, self.password
+
+ def get_admin_creds(self):
+ if self.isolated_creds.get('admin'):
+ user, tenant = self.isolated_creds['admin']
+ username, tenant_name = self._get_cred_names(user, tenant)
+ else:
+ user, tenant = self._create_creds(admin=True)
+ username, tenant_name = self._get_cred_names(user, tenant)
+ self.isolated_creds['admin'] = (user, tenant)
+ LOG.info("Aquired admin isolated creds:\n user: %s, tenant: %s"
+ % (username, tenant_name))
+ return username, tenant_name, self.password
+
+ def get_alt_creds(self):
+ if self.isolated_creds.get('alt'):
+ user, tenant = self.isolated_creds['alt']
+ username, tenant_name = self._get_cred_names(user, tenant)
+ else:
+ user, tenant = self._create_creds()
+ username, tenant_name = self._get_cred_names(user, tenant)
+ self.isolated_creds['alt'] = (user, tenant)
+ LOG.info("Aquired alt isolated creds:\n user: %s, tenant: %s"
+ % (username, tenant_name))
+ return username, tenant_name, self.password
+
+ def clear_isolated_creds(self):
+ if not self.isolated_creds:
+ return
+ for cred in self.isolated_creds:
+ user, tenant = self.isolated_creds.get(cred)
+ try:
+ if self.tempest_client:
+ self._delete_user(user['id'])
+ else:
+ self._delete_user(user.id)
+ except exceptions.NotFound:
+ if self.tempest_client:
+ name = user['name']
+ else:
+ name = user.name
+ LOG.warn("user with name: %s not found for delete" % name)
+ pass
+ try:
+ if self.tempest_client:
+ self._delete_tenant(tenant['id'])
+ else:
+ self._delete_tenant(tenant.id)
+ except exceptions.NotFound:
+ if self.tempest_client:
+ name = tenant['name']
+ else:
+ name = tenant.name
+ LOG.warn("tenant with name: %s not found for delete" % name)
+ pass
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index 09b87b2..759ab81 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -166,7 +166,8 @@
elif resp.status == 401:
raise exceptions.AuthenticationFailure(user=user,
- password=password)
+ password=password,
+ tenant=tenant_name)
raise exceptions.IdentityError('Unexpected status code {0}'.format(
resp.status))
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 448fbdf..62bd8cf 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -105,7 +105,7 @@
class AuthenticationFailure(RestClientException):
message = ("Authentication with user %(user)s and password "
- "%(password)s failed")
+ "%(password)s failed auth using tenant %(tenant)s.")
class EndpointNotFound(TempestException):
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index c95e867..4447da0 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -29,6 +29,7 @@
from tempest.api.network import common as net_common
+from tempest.common import isolated_creds
from tempest.common import ssh
from tempest.common.utils.data_utils import rand_name
import tempest.manager
@@ -48,26 +49,24 @@
NOVACLIENT_VERSION = '2'
CINDERCLIENT_VERSION = '1'
- def __init__(self):
+ def __init__(self, username, password, tenant_name):
super(OfficialClientManager, self).__init__()
- self.compute_client = self._get_compute_client()
+ self.compute_client = self._get_compute_client(username,
+ password,
+ tenant_name)
+ self.identity_client = self._get_identity_client(username,
+ password,
+ tenant_name)
self.image_client = self._get_image_client()
- self.identity_client = self._get_identity_client()
self.network_client = self._get_network_client()
- self.volume_client = self._get_volume_client()
+ self.volume_client = self._get_volume_client(username,
+ password,
+ tenant_name)
- def _get_compute_client(self, username=None, password=None,
- tenant_name=None):
+ def _get_compute_client(self, username, password, tenant_name):
# Novaclient will not execute operations for anyone but the
# identified user, so a new client needs to be created for
# each user that operations need to be performed for.
- if not username:
- username = self.config.identity.username
- if not password:
- password = self.config.identity.password
- if not tenant_name:
- tenant_name = self.config.identity.tenant_name
-
self._validate_credentials(username, password, tenant_name)
auth_url = self.config.identity.uri
@@ -84,23 +83,14 @@
insecure=dscv)
def _get_image_client(self):
- keystone = self._get_identity_client()
- token = keystone.auth_token
- endpoint = keystone.service_catalog.url_for(service_type='image',
- endpoint_type='publicURL')
+ token = self.identity_client.auth_token
+ endpoint = self.identity_client.service_catalog.url_for(
+ service_type='image', endpoint_type='publicURL')
dscv = self.config.identity.disable_ssl_certificate_validation
return glanceclient.Client('1', endpoint=endpoint, token=token,
insecure=dscv)
- def _get_volume_client(self, username=None, password=None,
- tenant_name=None):
- if not username:
- username = self.config.identity.username
- if not password:
- password = self.config.identity.password
- if not tenant_name:
- tenant_name = self.config.identity.tenant_name
-
+ def _get_volume_client(self, username, password, tenant_name):
auth_url = self.config.identity.uri
return cinderclient.client.Client(self.CINDERCLIENT_VERSION,
username,
@@ -108,17 +98,9 @@
tenant_name,
auth_url)
- def _get_identity_client(self, username=None, password=None,
- tenant_name=None):
+ def _get_identity_client(self, username, password, tenant_name):
# This identity client is not intended to check the security
# of the identity service, so use admin credentials by default.
- if not username:
- username = self.config.identity.admin_username
- if not password:
- password = self.config.identity.admin_password
- if not tenant_name:
- tenant_name = self.config.identity.admin_tenant_name
-
self._validate_credentials(username, password, tenant_name)
auth_url = self.config.identity.uri
@@ -168,7 +150,17 @@
@classmethod
def setUpClass(cls):
- cls.manager = OfficialClientManager()
+ cls.isolated_creds = isolated_creds.IsolatedCreds(
+ __name__, tempest_client=False)
+ if cls.config.compute.allow_tenant_isolation:
+ creds = cls.isolated_creds.get_primary_creds()
+ username, tenant_name, password = creds
+ else:
+ username = cls.config.identity.username
+ password = cls.config.identity.password
+ tenant_name = cls.config.identity.tenant_name
+
+ cls.manager = OfficialClientManager(username, password, tenant_name)
cls.compute_client = cls.manager.compute_client
cls.image_client = cls.manager.image_client
cls.identity_client = cls.manager.identity_client
@@ -216,6 +208,8 @@
# Block until resource deletion has completed or timed-out
tempest.test.call_until_true(is_deletion_complete, 10, 1)
+ cls.isolated_creds.clear_isolated_creds()
+ super(OfficialClientTest, cls).tearDownClass()
@classmethod
def set_resource(cls, key, thing):
diff --git a/tempest/test.py b/tempest/test.py
index 96360ff..0cd0b08 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -24,9 +24,7 @@
import testtools
from tempest import clients
-from tempest.common.utils.data_utils import rand_name
from tempest import config
-from tempest import exceptions
from tempest.openstack.common import log as logging
LOG = logging.getLogger(__name__)
@@ -143,85 +141,6 @@
cls.config.identity.uri
)
- @classmethod
- def _get_isolated_creds(cls, admin=False):
- """
- Creates a new set of user/tenant/password credentials for a
- **regular** user of the Compute API so that a test case can
- operate in an isolated tenant container.
- """
- admin_client = cls._get_identity_admin_client()
- password = "pass"
-
- while True:
- try:
- rand_name_root = rand_name(cls.__name__)
- if cls.isolated_creds:
- # Main user already created. Create the alt or admin one...
- if admin:
- rand_name_root += '-admin'
- else:
- rand_name_root += '-alt'
- tenant_name = rand_name_root + "-tenant"
- tenant_desc = tenant_name + "-desc"
-
- resp, tenant = admin_client.create_tenant(
- name=tenant_name, description=tenant_desc)
- break
- except exceptions.Duplicate:
- if cls.config.compute.allow_tenant_reuse:
- tenant = admin_client.get_tenant_by_name(tenant_name)
- LOG.info('Re-using existing tenant %s', tenant)
- break
-
- while True:
- try:
- rand_name_root = rand_name(cls.__name__)
- if cls.isolated_creds:
- # Main user already created. Create the alt one...
- rand_name_root += '-alt'
- username = rand_name_root + "-user"
- email = rand_name_root + "@example.com"
- resp, user = admin_client.create_user(username,
- password,
- tenant['id'],
- email)
- break
- except exceptions.Duplicate:
- if cls.config.compute.allow_tenant_reuse:
- user = admin_client.get_user_by_username(tenant['id'],
- username)
- LOG.info('Re-using existing user %s', user)
- break
- # Store the complete creds (including UUID ids...) for later
- # but return just the username, tenant_name, password tuple
- # that the various clients will use.
- cls.isolated_creds.append((user, tenant))
-
- # Assign admin role if this is for admin creds
- if admin:
- _, roles = admin_client.list_roles()
- role = None
- try:
- _, roles = admin_client.list_roles()
- role = next(r for r in roles if r['name'] == 'admin')
- except StopIteration:
- msg = "No admin role found"
- raise exceptions.NotFound(msg)
- admin_client.assign_user_role(tenant['id'], user['id'], role['id'])
-
- return username, tenant_name, password
-
- @classmethod
- def _clear_isolated_creds(cls):
- if not cls.isolated_creds:
- return
- admin_client = cls._get_identity_admin_client()
-
- for user, tenant in cls.isolated_creds:
- admin_client.delete_user(user['id'])
- admin_client.delete_tenant(tenant['id'])
-
def call_until_true(func, duration, sleep_for):
"""