Merge "Add Tempest gate job for stable/2023.1"
diff --git a/doc/source/keystone_scopes_and_roles_support.rst b/doc/source/keystone_scopes_and_roles_support.rst
index f446f8c..4d70565 100644
--- a/doc/source/keystone_scopes_and_roles_support.rst
+++ b/doc/source/keystone_scopes_and_roles_support.rst
@@ -203,6 +203,10 @@
                  cls.az_p_reader_client = (
                      cls.os_project_reader.availability_zone_client)
 
+     .. note::
+         'primary', 'project_admin', 'project_member', and 'project_reader'
+         credentials will be created under same project.
+
   #. Project alternate Admin: This is supported and can be requested and used from
      the test as below:
 
@@ -248,6 +252,10 @@
                  cls.az_p_alt_reader_client = (
                      cls.os_project_alt_reader.availability_zone_client)
 
+     .. note::
+         'alt', 'project_alt_admin', 'project_alt_member', and
+         'project_alt_reader' credentials will be created under same project.
+
   #. Project other roles: This is supported and can be requested and used from
      the test as below:
 
@@ -269,6 +277,16 @@
                  cls.az_role2_client = (
                      cls.os_project_my_role2.availability_zone_client)
 
+  .. note::
+      'admin' credenatials is considered and kept as legacy admin and
+      will be created under new project. If any test want to test with
+      admin role in projectA and non-admin/admin in projectB then test
+      can request projectA admin using 'admin' or 'project_alt_admin'
+      and non-admin in projectB using 'primary', 'project_member',
+      or 'project_reader'/admin in projectB using 'project_admin'. Many
+      existing tests using the 'admin' with new project to assert on the
+      resource list so we are keeping 'admin' a kind of legacy admin.
+
 Pre-Provisioned Credentials
 ---------------------------
 
diff --git a/releasenotes/notes/fix-bug-1964509-b742f2c95d854980.yaml b/releasenotes/notes/fix-bug-1964509-b742f2c95d854980.yaml
new file mode 100644
index 0000000..db627de
--- /dev/null
+++ b/releasenotes/notes/fix-bug-1964509-b742f2c95d854980.yaml
@@ -0,0 +1,19 @@
+---
+fixes:
+  - |
+    There was a bug (bug#1964509) in dynamic credentials creation where
+    project credentials with different roles are created with the new
+    projects. Credential of different role of projects must be created
+    within the same project. For exmaple, 'project_admin', 'project_member',
+    'project_reader', and 'primary', credentials will be created in the
+    same projects. 'alt', 'project_alt_admin', 'project_alt_member',
+    'project_alt_reader' will be created within the same project.
+
+    'admin' credenatials is considered and kept as legacy admin and
+    will be created under new project. If any test want to test with
+    admin role in projectA and non-admin/admin in projectB then test
+    can request projectA admin using 'admin' or 'project_alt_admin'
+    and non-admin in projectB using 'primary', 'project_member',
+    or 'project_reader'/admin in projectB using 'project_admin'. Many
+    existing tests using the 'admin' with new project to assert on the
+    resource list so we are keeping 'admin' a kind of legacy admin.
diff --git a/releasenotes/notes/tempest-2023-1-release-b18a240afadae8c9.yaml b/releasenotes/notes/tempest-2023-1-release-b18a240afadae8c9.yaml
new file mode 100644
index 0000000..092f4e3
--- /dev/null
+++ b/releasenotes/notes/tempest-2023-1-release-b18a240afadae8c9.yaml
@@ -0,0 +1,17 @@
+---
+prelude: |
+    This release is to tag Tempest for OpenStack 2023.1 release.
+    This release marks the start of 2023.1 release support in Tempest.
+    After this release, Tempest will support below OpenStack Releases:
+
+    * 2023.1
+    * Zed
+    * Yoga
+    * Xena
+
+    Current development of Tempest is for OpenStack 2023.2 development
+    cycle. Every Tempest commit is also tested against master during
+    the 2023.2 cycle. However, this does not necessarily mean that using
+    Tempest as of this tag will work against a 2023.2 (or future release)
+    cloud.
+    To be on safe side, use this tag to test the OpenStack 2023.1 release.
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 1cb8004..f7c0dd9 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -202,7 +202,8 @@
         volume = self.create_volume()
 
         # Attach the volume to the server
-        self.attach_volume(server, volume, device='/dev/xvdb')
+        self.attach_volume(server, volume, device='/dev/xvdb',
+                           wait_for_detach=False)
         server = self.admin_servers_client.show_server(server_id)['server']
         volume_id1 = server["os-extended-volumes:volumes_attached"][0]["id"]
         self._live_migrate(server_id, target_host, 'ACTIVE')
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index ea1cddc..260d4e0 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -568,7 +568,8 @@
             # is already detached.
             pass
 
-    def attach_volume(self, server, volume, device=None, tag=None):
+    def attach_volume(self, server, volume, device=None, tag=None,
+                      wait_for_detach=True):
         """Attaches volume to server and waits for 'in-use' volume status.
 
         The volume will be detached when the test tears down.
@@ -605,7 +606,7 @@
         # the contents of the console log. The final check of the volume state
         # should be a no-op by this point and is just added for completeness
         # when detaching non-multiattach volumes.
-        if not volume['multiattach']:
+        if not volume['multiattach'] and wait_for_detach:
             self.addCleanup(
                 waiters.wait_for_volume_resource_status, self.volumes_client,
                 volume['id'], 'available')
diff --git a/tempest/api/compute/servers/test_multiple_create.py b/tempest/api/compute/servers/test_multiple_create.py
index 10c76bb..b464c45 100644
--- a/tempest/api/compute/servers/test_multiple_create.py
+++ b/tempest/api/compute/servers/test_multiple_create.py
@@ -15,6 +15,7 @@
 
 from tempest.api.compute import base
 from tempest.common import compute
+from tempest.common import waiters
 from tempest.lib import decorators
 
 
@@ -34,8 +35,15 @@
             wait_until='ACTIVE',
             min_count=2,
             tenant_network=tenant_network)
+
+        for server in servers:
+            self.addCleanup(waiters.wait_for_server_termination,
+                            self.servers_client,
+                            server['id'])
+
         for server in servers:
             self.addCleanup(self.servers_client.delete_server, server['id'])
+
         # NOTE(maurosr): do status response check and also make sure that
         # reservation_id is not in the response body when the request send
         # contains return_reservation_id=False
diff --git a/tempest/api/compute/servers/test_servers.py b/tempest/api/compute/servers/test_servers.py
index 1c839eb..388b9b0 100644
--- a/tempest/api/compute/servers/test_servers.py
+++ b/tempest/api/compute/servers/test_servers.py
@@ -28,10 +28,16 @@
     """Test servers API"""
     create_default_network = True
 
+    credentials = ['primary', 'project_reader']
+
     @classmethod
     def setup_clients(cls):
         super(ServersTestJSON, cls).setup_clients()
         cls.client = cls.servers_client
+        if CONF.enforce_scope.nova:
+            cls.reader_client = cls.os_project_reader.servers_client
+        else:
+            cls.reader_client = cls.client
 
     @decorators.idempotent_id('b92d5ec7-b1dd-44a2-87e4-45e888c46ef0')
     @testtools.skipUnless(CONF.compute_feature_enabled.
@@ -64,9 +70,9 @@
         id2 = server['id']
         self.addCleanup(self.delete_server, id2)
         self.assertNotEqual(id1, id2, "Did not create a new server")
-        server = self.client.show_server(id1)['server']
+        server = self.reader_client.show_server(id1)['server']
         name1 = server['name']
-        server = self.client.show_server(id2)['server']
+        server = self.reader_client.show_server(id2)['server']
         name2 = server['name']
         self.assertEqual(name1, name2)
 
@@ -80,7 +86,7 @@
         server = self.create_test_server(key_name=key_name,
                                          wait_until='ACTIVE')
         self.addCleanup(self.delete_server, server['id'])
-        server = self.client.show_server(server['id'])['server']
+        server = self.reader_client.show_server(server['id'])['server']
         self.assertEqual(key_name, server['key_name'])
 
     def _update_server_name(self, server_id, status, prefix_name='server'):
@@ -93,7 +99,7 @@
         waiters.wait_for_server_status(self.client, server_id, status)
 
         # Verify the name of the server has changed
-        server = self.client.show_server(server_id)['server']
+        server = self.reader_client.show_server(server_id)['server']
         self.assertEqual(new_name, server['name'])
         return server
 
@@ -128,7 +134,7 @@
         waiters.wait_for_server_status(self.client, server['id'], 'ACTIVE')
 
         # Verify the access addresses have been updated
-        server = self.client.show_server(server['id'])['server']
+        server = self.reader_client.show_server(server['id'])['server']
         self.assertEqual('1.1.1.1', server['accessIPv4'])
         self.assertEqual('::babe:202:202', server['accessIPv6'])
 
@@ -138,7 +144,7 @@
         server = self.create_test_server(accessIPv6='2001:2001::3',
                                          wait_until='ACTIVE')
         self.addCleanup(self.delete_server, server['id'])
-        server = self.client.show_server(server['id'])['server']
+        server = self.reader_client.show_server(server['id'])['server']
         self.assertEqual('2001:2001::3', server['accessIPv6'])
 
     @decorators.related_bug('1730756')
@@ -169,12 +175,22 @@
     # also. 2.47 APIs schema are on top of 2.9->2.19->2.26 schema so
     # below tests cover all of the schema.
 
+    credentials = ['primary', 'project_reader']
+
+    @classmethod
+    def setup_clients(cls):
+        super(ServerShowV247Test, cls).setup_clients()
+        if CONF.enforce_scope.nova:
+            cls.reader_client = cls.os_project_reader.servers_client
+        else:
+            cls.reader_client = cls.servers_client
+
     @decorators.idempotent_id('88b0bdb2-494c-11e7-a919-92ebcb67fe33')
     def test_show_server(self):
         """Test getting server detail"""
         server = self.create_test_server()
         # All fields will be checked by API schema
-        self.servers_client.show_server(server['id'])
+        self.reader_client.show_server(server['id'])
 
     @decorators.idempotent_id('8de397c2-57d0-4b90-aa30-e5d668f21a8b')
     def test_update_rebuild_list_server(self):
@@ -198,6 +214,16 @@
     min_microversion = '2.63'
     max_microversion = 'latest'
 
+    credentials = ['primary', 'project_reader']
+
+    @classmethod
+    def setup_clients(cls):
+        super(ServerShowV263Test, cls).setup_clients()
+        if CONF.enforce_scope.nova:
+            cls.reader_client = cls.os_project_reader.servers_client
+        else:
+            cls.reader_client = cls.servers_client
+
     @testtools.skipUnless(CONF.compute.certified_image_ref,
                           '``[compute]/certified_image_ref`` required to test '
                           'image certificate validation.')
@@ -214,7 +240,7 @@
             wait_until='ACTIVE')
 
         # Check show API response schema
-        self.servers_client.show_server(server['id'])['server']
+        self.reader_client.show_server(server['id'])['server']
 
         # Check update API response schema
         self.servers_client.update_server(server['id'])
diff --git a/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py b/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py
index 8aed37d..b36c9d6 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py
@@ -30,7 +30,7 @@
 
 mac_address = {
     'type': 'string',
-    'pattern': '(?:[a-f0-9]{2}:){5}[a-f0-9]{2}'
+    'pattern': '(?:[a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}'
 }
 
 ip_address = {
diff --git a/tempest/lib/cli/base.py b/tempest/lib/cli/base.py
index c661d21..c9cffd2 100644
--- a/tempest/lib/cli/base.py
+++ b/tempest/lib/cli/base.py
@@ -97,6 +97,10 @@
     :type identity_api_version: string
     """
 
+    CLIENTS_WITHOUT_IDENTITY_VERSION = ['nova', 'nova_manage', 'keystone',
+                                        'glance', 'ceilometer', 'heat',
+                                        'cinder', 'neutron', 'sahara']
+
     def __init__(self, username='', password='', tenant_name='', uri='',
                  cli_dir='', insecure=False, prefix='', user_domain_name=None,
                  user_domain_id=None, project_domain_name=None,
@@ -377,8 +381,9 @@
                   self.password,
                   self.uri))
         if self.identity_api_version:
-            creds += ' --os-identity-api-version %s' % (
-                self.identity_api_version)
+            if cmd not in self.CLIENTS_WITHOUT_IDENTITY_VERSION:
+                creds += ' --os-identity-api-version %s' % (
+                    self.identity_api_version)
         if self.user_domain_name is not None:
             creds += ' --os-user-domain-name %s' % self.user_domain_name
         if self.user_domain_id is not None:
diff --git a/tempest/lib/common/cred_client.py b/tempest/lib/common/cred_client.py
index f13d6d0..69798a4 100644
--- a/tempest/lib/common/cred_client.py
+++ b/tempest/lib/common/cred_client.py
@@ -58,6 +58,10 @@
     def create_project(self, name, description):
         pass
 
+    @abc.abstractmethod
+    def show_project(self, project_id):
+        pass
+
     def _check_role_exists(self, role_name):
         try:
             roles = self._list_roles()
@@ -118,6 +122,9 @@
             name=name, description=description)['tenant']
         return tenant
 
+    def show_project(self, project_id):
+        return self.projects_client.show_tenant(project_id)['tenant']
+
     def delete_project(self, project_id):
         self.projects_client.delete_tenant(project_id)
 
@@ -159,6 +166,9 @@
             domain_id=self.creds_domain['id'])['project']
         return project
 
+    def show_project(self, project_id):
+        return self.projects_client.show_project(project_id)['project']
+
     def delete_project(self, project_id):
         self.projects_client.delete_project(project_id)
 
diff --git a/tempest/lib/common/dynamic_creds.py b/tempest/lib/common/dynamic_creds.py
index d687eb5..99647d4 100644
--- a/tempest/lib/common/dynamic_creds.py
+++ b/tempest/lib/common/dynamic_creds.py
@@ -163,7 +163,8 @@
                     os.network.PortsClient(),
                     os.network.SecurityGroupsClient())
 
-    def _create_creds(self, admin=False, roles=None, scope='project'):
+    def _create_creds(self, admin=False, roles=None, scope='project',
+                      project_id=None):
         """Create credentials with random name.
 
         Creates user and role assignments on a project, domain, or system. When
@@ -177,6 +178,8 @@
         :type roles: list
         :param str scope: The scope for the role assignment, may be one of
                           'project', 'domain', or 'system'.
+        :param str project_id: The project id of already created project
+                               for credentials under same project.
         :return: Readonly Credentials with network resources
         :raises: Exception if scope is invalid
         """
@@ -190,12 +193,20 @@
             'system': None
         }
         if scope == 'project':
-            project_name = data_utils.rand_name(
-                root, prefix=self.resource_prefix)
-            project_desc = project_name + '-desc'
-            project = self.creds_client.create_project(
-                name=project_name, description=project_desc)
-
+            if not project_id:
+                project_name = data_utils.rand_name(
+                    root, prefix=self.resource_prefix)
+                project_desc = project_name + '-desc'
+                project = self.creds_client.create_project(
+                    name=project_name, description=project_desc)
+            else:
+                # NOTE(gmann) This is the case where creds are requested
+                # from the existing creds within same project. We should
+                # not create the new project in this case.
+                project = self.creds_client.show_project(project_id)
+                project_name = project['name']
+                LOG.info("Using the existing project %s for scope %s and "
+                         "roles: %s", project['id'], scope, roles)
             # NOTE(andreaf) User and project can be distinguished from the
             # context, having the same ID in both makes it easier to match them
             # and debug.
@@ -372,48 +383,78 @@
         self.routers_admin_client.add_router_interface(router_id,
                                                        subnet_id=subnet_id)
 
-    def get_credentials(self, credential_type, scope=None):
-        if not scope and self._creds.get(str(credential_type)):
-            credentials = self._creds[str(credential_type)]
-        elif scope and (
-                self._creds.get("%s_%s" % (scope, str(credential_type)))):
-            credentials = self._creds["%s_%s" % (scope, str(credential_type))]
+    def _get_project_id(self, credential_type, scope):
+        same_creds = [['admin'], ['member'], ['reader']]
+        same_alt_creds = [['alt_admin'], ['alt_member'], ['alt_reader']]
+        search_in = []
+        if credential_type in same_creds:
+            search_in = same_creds
+        elif credential_type in same_alt_creds:
+            search_in = same_alt_creds
+        for cred in search_in:
+            found_cred = self._creds.get("%s_%s" % (scope, str(cred)))
+            if found_cred:
+                project_id = found_cred.get("%s_%s" % (scope, 'id'))
+                LOG.debug("Reusing existing project %s from creds: %s ",
+                          project_id, found_cred)
+                return project_id
+        return None
+
+    def get_credentials(self, credential_type, scope=None, by_role=False):
+        cred_prefix = ''
+        if by_role:
+            cred_prefix = 'role_'
+        if not scope and self._creds.get(
+                "%s%s" % (cred_prefix, str(credential_type))):
+            credentials = self._creds[
+                "%s%s" % (cred_prefix, str(credential_type))]
+        elif scope and (self._creds.get(
+                "%s%s_%s" % (cred_prefix, scope, str(credential_type)))):
+            credentials = self._creds[
+                "%s%s_%s" % (cred_prefix, scope, str(credential_type))]
         else:
             LOG.debug("Creating new dynamic creds for scope: %s and "
                       "credential_type: %s", scope, credential_type)
+            project_id = None
             if scope:
-                if credential_type in [['admin'], ['alt_admin']]:
+                if scope == 'project':
+                    project_id = self._get_project_id(
+                        credential_type, 'project')
+                if by_role:
                     credentials = self._create_creds(
-                        admin=True, scope=scope)
+                        roles=credential_type, scope=scope)
+                elif credential_type in [['admin'], ['alt_admin']]:
+                    credentials = self._create_creds(
+                        admin=True, scope=scope, project_id=project_id)
                 elif credential_type in [['alt_member'], ['alt_reader']]:
                     cred_type = credential_type[0][4:]
                     if isinstance(cred_type, str):
                         cred_type = [cred_type]
                     credentials = self._create_creds(
-                        roles=cred_type, scope=scope)
-                else:
+                        roles=cred_type, scope=scope, project_id=project_id)
+                elif credential_type in [['member'], ['reader']]:
                     credentials = self._create_creds(
-                        roles=credential_type, scope=scope)
+                        roles=credential_type, scope=scope,
+                        project_id=project_id)
             elif credential_type in ['primary', 'alt', 'admin']:
                 is_admin = (credential_type == 'admin')
                 credentials = self._create_creds(admin=is_admin)
             else:
                 credentials = self._create_creds(roles=credential_type)
             if scope:
-                self._creds["%s_%s" %
-                            (scope, str(credential_type))] = credentials
+                self._creds["%s%s_%s" % (
+                    cred_prefix, scope, str(credential_type))] = credentials
             else:
-                self._creds[str(credential_type)] = credentials
+                self._creds[
+                    "%s%s" % (cred_prefix, str(credential_type))] = credentials
             # Maintained until tests are ported
             LOG.info("Acquired dynamic creds:\n"
                      " credentials: %s", credentials)
             # NOTE(gmann): For 'domain' and 'system' scoped token, there is no
             # project_id so we are skipping the network creation for both
-            # scope. How these scoped token can create the network, Nova
-            # server or other project mapped resources is one of the open
-            # question and discussed a lot in Xena cycle PTG. Once we sort
-            # out that then if needed we can update the network creation here.
-            if (not scope or scope == 'project'):
+            # scope.
+            # We need to create nework resource once per project.
+            if (not project_id and (not scope or scope == 'project')):
                 if (self.neutron_available and self.create_networks):
                     network, subnet, router = self._create_network_resources(
                         credentials.tenant_id)
@@ -422,24 +463,22 @@
                     LOG.info("Created isolated network resources for:\n"
                              " credentials: %s", credentials)
             else:
-                LOG.info("Network resources are not created for scope: %s",
-                         scope)
+                LOG.info("Network resources are not created for requested "
+                         "scope: %s and credentials: %s", scope, credentials)
         return credentials
 
     # TODO(gmann): Remove this method in favor of get_project_member_creds()
     # after the deprecation phase.
     def get_primary_creds(self):
-        return self.get_credentials('primary')
+        return self.get_project_member_creds()
 
-    # TODO(gmann): Remove this method in favor of get_project_admin_creds()
-    # after the deprecation phase.
     def get_admin_creds(self):
         return self.get_credentials('admin')
 
-    # TODO(gmann): Replace this method with more appropriate name.
-    # like get_project_alt_member_creds()
+    # TODO(gmann): Remove this method in favor of
+    # get_project_alt_member_creds() after the deprecation phase.
     def get_alt_creds(self):
-        return self.get_credentials('alt')
+        return self.get_project_alt_member_creds()
 
     def get_system_admin_creds(self):
         return self.get_credentials(['admin'], scope='system')
@@ -481,9 +520,9 @@
         roles = list(set(roles))
         # The roles list as a str will become the index as the dict key for
         # the created credentials set in the dynamic_creds dict.
-        creds_name = str(roles)
+        creds_name = "role_%s" % str(roles)
         if scope:
-            creds_name = "%s_%s" % (scope, str(roles))
+            creds_name = "role_%s_%s" % (scope, str(roles))
         exist_creds = self._creds.get(creds_name)
         # If force_new flag is True 2 cred sets with the same roles are needed
         # handle this by creating a separate index for old one to store it
@@ -492,7 +531,7 @@
             new_index = creds_name + '-' + str(len(self._creds))
             self._creds[new_index] = exist_creds
             del self._creds[creds_name]
-        return self.get_credentials(roles, scope=scope)
+        return self.get_credentials(roles, scope=scope, by_role=True)
 
     def _clear_isolated_router(self, router_id, router_name):
         client = self.routers_admin_client
@@ -553,31 +592,20 @@
         if not self._creds:
             return
         self._clear_isolated_net_resources()
+        project_ids = set()
         for creds in self._creds.values():
+            # NOTE(gmann): With new RBAC personas, we can have single project
+            # and multiple user created under it, to avoid conflict let's
+            # cleanup the projects at the end.
+            # Adding project if id is not None, means leaving domain and
+            # system creds.
+            if creds.project_id:
+                project_ids.add(creds.project_id)
             try:
                 self.creds_client.delete_user(creds.user_id)
             except lib_exc.NotFound:
                 LOG.warning("user with name: %s not found for delete",
                             creds.username)
-            if creds.tenant_id:
-                # NOTE(zhufl): Only when neutron's security_group ext is
-                # enabled, cleanup_default_secgroup will not raise error. But
-                # here cannot use test_utils.is_extension_enabled for it will
-                # cause "circular dependency". So here just use try...except to
-                # ensure tenant deletion without big changes.
-                try:
-                    if self.neutron_available:
-                        self.cleanup_default_secgroup(
-                            self.security_groups_admin_client, creds.tenant_id)
-                except lib_exc.NotFound:
-                    LOG.warning("failed to cleanup tenant %s's secgroup",
-                                creds.tenant_name)
-                try:
-                    self.creds_client.delete_project(creds.tenant_id)
-                except lib_exc.NotFound:
-                    LOG.warning("tenant with name: %s not found for delete",
-                                creds.tenant_name)
-
             # if cred is domain scoped, delete ephemeral domain
             # do not delete default domain
             if (hasattr(creds, 'domain_id') and
@@ -587,6 +615,28 @@
                 except lib_exc.NotFound:
                     LOG.warning("domain with name: %s not found for delete",
                                 creds.domain_name)
+        for project_id in project_ids:
+            # NOTE(zhufl): Only when neutron's security_group ext is
+            # enabled, cleanup_default_secgroup will not raise error. But
+            # here cannot use test_utils.is_extension_enabled for it will
+            # cause "circular dependency". So here just use try...except to
+            # ensure tenant deletion without big changes.
+            LOG.info("Deleting project and security group for project: %s",
+                     project_id)
+
+            try:
+                if self.neutron_available:
+                    self.cleanup_default_secgroup(
+                        self.security_groups_admin_client, project_id)
+            except lib_exc.NotFound:
+                LOG.warning("failed to cleanup tenant %s's secgroup",
+                            project_id)
+            try:
+                self.creds_client.delete_project(project_id)
+            except lib_exc.NotFound:
+                LOG.warning("tenant with id: %s not found for delete",
+                            project_id)
+
         self._creds = {}
 
     def is_multi_user(self):
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index bf3f62f..db0aa5a 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -325,13 +325,15 @@
 
     def create_volume(self, size=None, name=None, snapshot_id=None,
                       imageRef=None, volume_type=None, wait_until='available',
-                      **kwargs):
+                      client=None, **kwargs):
         """Creates volume
 
         This wrapper utility creates volume and waits for volume to be
         in 'available' state by default. If wait_until is None, means no wait.
         This method returns the volume's full representation by GET request.
         """
+        if client is None:
+            client = self.volumes_client
 
         if size is None:
             size = CONF.volume.volume_size
@@ -355,19 +357,20 @@
             kwargs.setdefault('availability_zone',
                               CONF.compute.compute_volume_common_az)
 
-        volume = self.volumes_client.create_volume(**kwargs)['volume']
+        volume = client.create_volume(**kwargs)['volume']
 
-        self.addCleanup(self.volumes_client.wait_for_resource_deletion,
+        self.addCleanup(client.wait_for_resource_deletion,
                         volume['id'])
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
-                        self.volumes_client.delete_volume, volume['id'])
+                        client.delete_volume, volume['id'])
         self.assertEqual(name, volume['name'])
         if wait_until:
-            waiters.wait_for_volume_resource_status(self.volumes_client,
+            waiters.wait_for_volume_resource_status(client,
                                                     volume['id'], wait_until)
             # The volume retrieved on creation has a non-up-to-date status.
             # Retrieval after it becomes active ensures correct details.
-            volume = self.volumes_client.show_volume(volume['id'])['volume']
+            volume = client.show_volume(volume['id'])['volume']
+
         return volume
 
     def create_backup(self, volume_id, name=None, description=None,
@@ -858,32 +861,43 @@
                   image_name, server['name'])
         return snapshot_image
 
-    def nova_volume_attach(self, server, volume_to_attach, **kwargs):
+    def nova_volume_attach(self, server, volume_to_attach,
+                           volumes_client=None, servers_client=None,
+                           **kwargs):
         """Compute volume attach
 
         This utility attaches volume from compute and waits for the
         volume status to be 'in-use' state.
         """
-        volume = self.servers_client.attach_volume(
+        if volumes_client is None:
+            volumes_client = self.volumes_client
+        if servers_client is None:
+            servers_client = self.servers_client
+
+        volume = servers_client.attach_volume(
             server['id'], volumeId=volume_to_attach['id'],
             **kwargs)['volumeAttachment']
         self.assertEqual(volume_to_attach['id'], volume['id'])
-        waiters.wait_for_volume_resource_status(self.volumes_client,
+        waiters.wait_for_volume_resource_status(volumes_client,
                                                 volume['id'], 'in-use')
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
-                        self.nova_volume_detach, server, volume)
+                        self.nova_volume_detach, server, volume,
+                        servers_client)
         # Return the updated volume after the attachment
-        return self.volumes_client.show_volume(volume['id'])['volume']
+        return volumes_client.show_volume(volume['id'])['volume']
 
-    def nova_volume_detach(self, server, volume):
+    def nova_volume_detach(self, server, volume, servers_client=None):
         """Compute volume detach
 
         This utility detaches the volume from the server and checks whether the
         volume attachment has been removed from Nova.
         """
-        self.servers_client.detach_volume(server['id'], volume['id'])
+        if servers_client is None:
+            servers_client = self.servers_client
+
+        servers_client.detach_volume(server['id'], volume['id'])
         waiters.wait_for_volume_attachment_remove_from_server(
-            self.servers_client, server['id'], volume['id'])
+            servers_client, server['id'], volume['id'])
 
     def ping_ip_address(self, ip_address, should_succeed=True,
                         ping_timeout=None, mtu=None, server=None):
diff --git a/tempest/tests/lib/common/test_dynamic_creds.py b/tempest/tests/lib/common/test_dynamic_creds.py
index b4b1b91..d3d01c0 100644
--- a/tempest/tests/lib/common/test_dynamic_creds.py
+++ b/tempest/tests/lib/common/test_dynamic_creds.py
@@ -60,6 +60,7 @@
     fake_response = fake_identity._fake_v2_response
     tenants_client_class = tenants_client.TenantsClient
     delete_tenant = 'delete_tenant'
+    create_tenant = 'create_tenant'
 
     def setUp(self):
         super(TestDynamicCredentialProvider, self).setUp()
@@ -140,7 +141,9 @@
             return_value=(rest_client.ResponseBody
                           (200, {'roles': [
                               {'id': '1', 'name': 'FakeRole'},
-                              {'id': '2', 'name': 'member'}]}))))
+                              {'id': '2', 'name': 'member'},
+                              {'id': '3', 'name': 'reader'},
+                              {'id': '4', 'name': 'admin'}]}))))
         return roles_fix
 
     def _mock_list_ec2_credentials(self, user_id, tenant_id):
@@ -191,6 +194,205 @@
         self.assertEqual(primary_creds.tenant_id, '1234')
         self.assertEqual(primary_creds.user_id, '1234')
 
+    def _request_and_check_second_creds(
+            self, creds_obj, func, creds_to_compare,
+            show_mock, sm_count=1, sm_count_in_diff_project=0,
+            same_project_request=True, **func_kwargs):
+        self._mock_user_create('111', 'fake_user')
+        with mock.patch.object(creds_obj.creds_client,
+                               'create_project') as create_mock:
+            create_mock.return_value = {'id': '22', 'name': 'fake_project'}
+            new_creds = func(**func_kwargs)
+        if same_project_request:
+            # Check that with second creds request, create_project is not
+            # called and show_project is called. Which means new project is
+            # not created for the second requested creds instead new user is
+            # created under existing project.
+            self.assertEqual(len(create_mock.mock_calls), 0)
+            self.assertEqual(len(show_mock.mock_calls), sm_count)
+            # Verify project name and id is same as creds_to_compare
+            self.assertEqual(creds_to_compare.tenant_name,
+                             new_creds.tenant_name)
+            self.assertEqual(creds_to_compare.tenant_id,
+                             new_creds.tenant_id)
+        else:
+            # Check that with different project creds request, create_project
+            # is called and show_project is not called. Which means new project
+            # is created for this new creds request.
+            self.assertEqual(len(create_mock.mock_calls), 1)
+            self.assertEqual(len(show_mock.mock_calls),
+                             sm_count_in_diff_project)
+            # Verify project name and id is not same as creds_to_compare
+            self.assertNotEqual(creds_to_compare.tenant_name,
+                                new_creds.tenant_name)
+            self.assertNotEqual(creds_to_compare.tenant_id,
+                                new_creds.tenant_id)
+            self.assertEqual(new_creds.tenant_name, 'fake_project')
+            self.assertEqual(new_creds.tenant_id, '22')
+        # Verify new user name and id
+        self.assertEqual(new_creds.username, 'fake_user')
+        self.assertEqual(new_creds.user_id, '111')
+        return new_creds
+
+    @mock.patch('tempest.lib.common.rest_client.RestClient')
+    def _creds_within_same_project(self, MockRestClient, test_alt_creds=False):
+        creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
+        if test_alt_creds:
+            admin_func = creds.get_project_alt_admin_creds
+            member_func = creds.get_project_alt_member_creds
+            reader_func = creds.get_project_alt_reader_creds
+        else:
+            admin_func = creds.get_project_admin_creds
+            member_func = creds.get_project_member_creds
+            reader_func = creds.get_project_reader_creds
+        self._mock_assign_user_role()
+        self._mock_list_role()
+        self._mock_user_create('11', 'fake_user1')
+        show_mock = self.patchobject(creds.creds_client, 'show_project')
+        show_mock.return_value = {'id': '21', 'name': 'fake_project1'}
+        with mock.patch.object(creds.creds_client,
+                               'create_project') as create_mock:
+            create_mock.return_value = {'id': '21', 'name': 'fake_project1'}
+            member_creds = member_func()
+        # Check that with first creds request, create_project is called and
+        # show_project is not called. Which means new project is created for
+        # the requested creds.
+        self.assertEqual(len(create_mock.mock_calls), 1)
+        self.assertEqual(len(show_mock.mock_calls), 0)
+        # Verify project, user name and IDs
+        self.assertEqual(member_creds.username, 'fake_user1')
+        self.assertEqual(member_creds.tenant_name, 'fake_project1')
+        self.assertEqual(member_creds.tenant_id, '21')
+        self.assertEqual(member_creds.user_id, '11')
+
+        # Now request for the project reader creds which should not create new
+        # project instead should use the project_id of member_creds already
+        # created project.
+        self._request_and_check_second_creds(
+            creds, reader_func, member_creds, show_mock)
+
+        # Now request for the project admin creds which should not create new
+        # project instead should use the project_id of member_creds already
+        # created project.
+        self._request_and_check_second_creds(
+            creds, admin_func, member_creds, show_mock, sm_count=2)
+
+    def test_creds_within_same_project(self):
+        self._creds_within_same_project()
+
+    def test_alt_creds_within_same_project(self):
+        self._creds_within_same_project(test_alt_creds=True)
+
+    @mock.patch('tempest.lib.common.rest_client.RestClient')
+    def test_creds_in_different_project(self, MockRestClient):
+        creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
+        self._mock_assign_user_role()
+        self._mock_list_role()
+        self._mock_user_create('11', 'fake_user1')
+        show_mock = self.patchobject(creds.creds_client, 'show_project')
+        show_mock.return_value = {'id': '21', 'name': 'fake_project1'}
+        with mock.patch.object(creds.creds_client,
+                               'create_project') as create_mock:
+            create_mock.return_value = {'id': '21', 'name': 'fake_project1'}
+            member_creds = creds.get_project_member_creds()
+        # Check that with first creds request, create_project is called and
+        # show_project is not called. Which means new project is created for
+        # the requested creds.
+        self.assertEqual(len(create_mock.mock_calls), 1)
+        self.assertEqual(len(show_mock.mock_calls), 0)
+        # Verify project, user name and IDs
+        self.assertEqual(member_creds.username, 'fake_user1')
+        self.assertEqual(member_creds.tenant_name, 'fake_project1')
+        self.assertEqual(member_creds.tenant_id, '21')
+        self.assertEqual(member_creds.user_id, '11')
+
+        # Now request for the project alt reader creds which should create
+        # new project as this request is for alt creds.
+        alt_reader_creds = self._request_and_check_second_creds(
+            creds, creds.get_project_alt_reader_creds,
+            member_creds, show_mock, same_project_request=False)
+
+        # Check that with second creds request, create_project is not called
+        # and show_project is called. Which means new project is not created
+        # for the second requested creds instead new user is created under
+        # existing project.
+        self._request_and_check_second_creds(
+            creds, creds.get_project_reader_creds, member_creds, show_mock)
+
+        # Now request for the project alt member creds which should not create
+        # new project instead use the alt project already created for
+        # alt_reader creds.
+        show_mock.return_value = {
+            'id': alt_reader_creds.tenant_id,
+            'name': alt_reader_creds.tenant_name}
+        self._request_and_check_second_creds(
+            creds, creds.get_project_alt_member_creds,
+            alt_reader_creds, show_mock, sm_count=2,
+            same_project_request=True)
+
+    @mock.patch('tempest.lib.common.rest_client.RestClient')
+    def test_creds_by_role_in_different_project(self, MockRestClient):
+        creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
+        self._mock_assign_user_role()
+        self._mock_list_role()
+        self._mock_user_create('11', 'fake_user1')
+        show_mock = self.patchobject(creds.creds_client, 'show_project')
+        show_mock.return_value = {'id': '21', 'name': 'fake_project1'}
+        with mock.patch.object(creds.creds_client,
+                               'create_project') as create_mock:
+            create_mock.return_value = {'id': '21', 'name': 'fake_project1'}
+            member_creds = creds.get_project_member_creds()
+        # Check that with first creds request, create_project is called and
+        # show_project is not called. Which means new project is created for
+        # the requested creds.
+        self.assertEqual(len(create_mock.mock_calls), 1)
+        self.assertEqual(len(show_mock.mock_calls), 0)
+        # Verify project, user name and IDs
+        self.assertEqual(member_creds.username, 'fake_user1')
+        self.assertEqual(member_creds.tenant_name, 'fake_project1')
+        self.assertEqual(member_creds.tenant_id, '21')
+        self.assertEqual(member_creds.user_id, '11')
+        # Check that with second creds request, create_project is not called
+        # and show_project is called. Which means new project is not created
+        # for the second requested creds instead new user is created under
+        # existing project.
+        self._request_and_check_second_creds(
+            creds, creds.get_project_reader_creds, member_creds, show_mock)
+        # Now request the creds by role which should create new project.
+        self._request_and_check_second_creds(
+            creds, creds.get_creds_by_roles, member_creds, show_mock,
+            sm_count_in_diff_project=1, same_project_request=False,
+            roles=['member'], scope='project')
+
+    @mock.patch('tempest.lib.common.rest_client.RestClient')
+    def test_legacy_admin_creds_in_different_project(self, MockRestClient):
+        creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
+        self._mock_assign_user_role()
+        self._mock_list_role()
+        self._mock_user_create('11', 'fake_user1')
+        show_mock = self.patchobject(creds.creds_client, 'show_project')
+        show_mock.return_value = {'id': '21', 'name': 'fake_project1'}
+        with mock.patch.object(creds.creds_client,
+                               'create_project') as create_mock:
+            create_mock.return_value = {'id': '21', 'name': 'fake_project1'}
+            member_creds = creds.get_project_member_creds()
+        # Check that with first creds request, create_project is called and
+        # show_project is not called. Which means new project is created for
+        # the requested creds.
+        self.assertEqual(len(create_mock.mock_calls), 1)
+        self.assertEqual(len(show_mock.mock_calls), 0)
+        # Verify project, user name and IDs
+        self.assertEqual(member_creds.username, 'fake_user1')
+        self.assertEqual(member_creds.tenant_name, 'fake_project1')
+        self.assertEqual(member_creds.tenant_id, '21')
+        self.assertEqual(member_creds.user_id, '11')
+
+        # Now request for the legacy admin creds which should create
+        # new project instead of using project member creds project.
+        self._request_and_check_second_creds(
+            creds, creds.get_admin_creds,
+            member_creds, show_mock, same_project_request=False)
+
     @mock.patch('tempest.lib.common.rest_client.RestClient')
     def test_admin_creds(self, MockRestClient):
         creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
@@ -321,7 +523,8 @@
 
     @mock.patch('tempest.lib.common.rest_client.RestClient')
     def _test_get_same_role_creds_with_project_scope(self, MockRestClient,
-                                                     scope=None):
+                                                     scope=None,
+                                                     force_new=False):
         creds = dynamic_creds.DynamicCredentialProvider(**self.fixed_params)
         self._mock_list_2_roles()
         self._mock_user_create('1234', 'fake_role_user')
@@ -329,7 +532,7 @@
         with mock.patch.object(self.roles_client.RolesClient,
                                'create_user_role_on_project') as user_mock:
             role_creds = creds.get_creds_by_roles(
-                roles=['role1', 'role2'], scope=scope)
+                roles=['role1', 'role2'], force_new=force_new, scope=scope)
         calls = user_mock.mock_calls
         # Assert that the role creation is called with the 2 specified roles
         self.assertEqual(len(calls), 2)
@@ -338,13 +541,18 @@
         with mock.patch.object(self.roles_client.RolesClient,
                                'create_user_role_on_project') as user_mock1:
             role_creds_new = creds.get_creds_by_roles(
-                roles=['role1', 'role2'], scope=scope)
+                roles=['role1', 'role2'], force_new=force_new, scope=scope)
         calls = user_mock1.mock_calls
+        # With force_new, assert that new creds are created
+        if force_new:
+            self.assertEqual(len(calls), 2)
+            self.assertNotEqual(role_creds, role_creds_new)
         # Assert that previously created creds are return and no call to
-        # role creation.
-        self.assertEqual(len(calls), 0)
+        # role creation
         # Check if previously created creds are returned.
-        self.assertEqual(role_creds, role_creds_new)
+        else:
+            self.assertEqual(len(calls), 0)
+            self.assertEqual(role_creds, role_creds_new)
 
     def test_get_same_role_creds_with_project_scope(self):
         self._test_get_same_role_creds_with_project_scope(scope='project')
@@ -352,6 +560,13 @@
     def test_get_same_role_creds_with_default_scope(self):
         self._test_get_same_role_creds_with_project_scope()
 
+    def test_get_same_role_creds_with_project_scope_force_new(self):
+        self._test_get_same_role_creds_with_project_scope(
+            scope='project', force_new=True)
+
+    def test_get_same_role_creds_with_default_scope_force_new(self):
+        self._test_get_same_role_creds_with_project_scope(force_new=True)
+
     @mock.patch('tempest.lib.common.rest_client.RestClient')
     def _test_get_different_role_creds_with_project_scope(
             self, MockRestClient, scope=None):
@@ -391,8 +606,12 @@
         self._mock_assign_user_role()
         self._mock_list_role()
         self._mock_tenant_create('1234', 'fake_prim_tenant')
-        self._mock_user_create('1234', 'fake_prim_user')
+        show_mock = self.patchobject(creds.creds_client, 'show_project')
+        show_mock.return_value = {'id': '1234', 'name': 'fake_prim_tenant'}
+        self._mock_user_create('1234', 'fake_project1_user')
         creds.get_primary_creds()
+        self._mock_user_create('12341', 'fake_project1_user')
+        creds.get_project_admin_creds()
         self._mock_tenant_create('12345', 'fake_alt_tenant')
         self._mock_user_create('12345', 'fake_alt_user')
         creds.get_alt_creds()
@@ -407,10 +626,11 @@
         creds.clear_creds()
         # Verify user delete calls
         calls = user_mock.mock_calls
-        self.assertEqual(len(calls), 3)
+        self.assertEqual(len(calls), 4)
         args = map(lambda x: x[1][0], calls)
         args = list(args)
         self.assertIn('1234', args)
+        self.assertIn('12341', args)
         self.assertIn('12345', args)
         self.assertIn('123456', args)
         # Verify tenant delete calls
@@ -512,6 +732,9 @@
         self._mock_list_role()
         self._mock_user_create('1234', 'fake_prim_user')
         self._mock_tenant_create('1234', 'fake_prim_tenant')
+        show_mock = self.patchobject(creds.creds_client, 'show_project')
+        show_mock.return_value = {'id': '1234', 'name': 'fake_prim_tenant'}
+        self._mock_user_create('12341', 'fake_project1_user')
         self._mock_network_create(creds, '1234', 'fake_net')
         self._mock_subnet_create(creds, '1234', 'fake_subnet')
         self._mock_router_create('1234', 'fake_router')
@@ -519,6 +742,7 @@
             'tempest.lib.services.network.routers_client.RoutersClient.'
             'add_router_interface')
         creds.get_primary_creds()
+        creds.get_project_admin_creds()
         router_interface_mock.assert_called_once_with('1234', subnet_id='1234')
         router_interface_mock.reset_mock()
         # Create alternate tenant and network
@@ -779,6 +1003,7 @@
     fake_response = fake_identity._fake_v3_response
     tenants_client_class = tenants_client.ProjectsClient
     delete_tenant = 'delete_project'
+    create_tenant = 'create_project'
 
     def setUp(self):
         super(TestDynamicCredentialProviderV3, self).setUp()
diff --git a/zuul.d/integrated-gate.yaml b/zuul.d/integrated-gate.yaml
index d50be5a..f977f82 100644
--- a/zuul.d/integrated-gate.yaml
+++ b/zuul.d/integrated-gate.yaml
@@ -11,10 +11,11 @@
     vars:
       tox_envlist: all
       tempest_test_regex: tempest
-      # TODO(gmann): Enable File injection tests once nova bug is fixed
-      # https://bugs.launchpad.net/nova/+bug/1882421
-      # devstack_localrc:
-      #   ENABLE_FILE_INJECTION: true
+      devstack_localrc:
+        MYSQL_REDUCE_MEMORY: true
+        # TODO(gmann): Enable File injection tests once nova bug is fixed
+        # https://bugs.launchpad.net/nova/+bug/1882421
+        #   ENABLE_FILE_INJECTION: true
 
 - job:
     name: tempest-ipv6-only
@@ -318,6 +319,8 @@
     # This job run slow tests in parallel.
     vars:
       tox_envlist: slow
+      devstack_localrc:
+        MYSQL_REDUCE_MEMORY: true
 
 - job:
     name: tempest-cinder-v2-api
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 97027c5..e39c9e3 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -161,6 +161,11 @@
             irrelevant-files: *tempest-irrelevant-files
         - tempest-all:
             irrelevant-files: *tempest-irrelevant-files
+        - tempest-slow-parallel
+        - tempest-full-parallel
+        - tempest-full-zed-extra-tests
+        - tempest-full-yoga-extra-tests
+        - tempest-full-xena-extra-tests
         - neutron-ovs-tempest-dvr-ha-multinode-full:
             irrelevant-files: *tempest-irrelevant-files
         - nova-tempest-v2-api:
diff --git a/zuul.d/tempest-specific.yaml b/zuul.d/tempest-specific.yaml
index 972123e..a8c29af 100644
--- a/zuul.d/tempest-specific.yaml
+++ b/zuul.d/tempest-specific.yaml
@@ -49,6 +49,7 @@
       run_tempest_dry_cleanup: true
       devstack_localrc:
         DEVSTACK_PARALLEL: True
+        MYSQL_REDUCE_MEMORY: true
 
 - job:
     name: tempest-full-py3-ipv6