Fix creation of requested creds within the same project

We have a bug in dynamic creds creation where project creds
with different roles are created under a new projects. Creds
of different role of projects must be created within the same
project.

Fixing the creation of 'project_admin', 'project_member',
'project_reader', 'primary' creds in same projects. All the alt
creds will be created under same projects. but main and alt creds
will use different project, for example, 'project_alt_member'
and 'project_member' creds will be created in different project.

'admin' creds will continue be in new project as many test
use it as legacy admin.

Closes-Bug: #1964509
Change-Id: I9af005e2900195c42ecbbf7434facae2d3952f30
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):