Merge "Test only openstack official tempest plugins in plugin sanity job"
diff --git a/releasenotes/notes/support-srbac-in-pre-provisioned-creds-f1b817e85c1fb171.yaml b/releasenotes/notes/support-srbac-in-pre-provisioned-creds-f1b817e85c1fb171.yaml
new file mode 100644
index 0000000..c264cad
--- /dev/null
+++ b/releasenotes/notes/support-srbac-in-pre-provisioned-creds-f1b817e85c1fb171.yaml
@@ -0,0 +1,22 @@
+---
+prelude: >
+    The pre-provisioned credentials supports the new SRBAC personas.
+features:
+  - |
+    Add support for SRBAC personas as well as system scope in the
+    ``tempest account-generator`` command as well as in pre
+    provisioned credentials. The below SRBAC personas are supported:
+
+    * ``system admin``
+    * ``system member``
+    * ``system reader``
+    * ``project manager``
+    * ``project member``
+    * ``project reader``
+fixes:
+  - |
+    Fixed a bug#2143564 in the pre-provisioned credentials provider where
+    ``project_member`` and ``project_reader`` were not guaranteed to be
+    allocated from the same project. The ``PreProvisionedCredentialProvider``
+    now stores accounts by project name and gets the accounts based on the
+    project name if they are requested from the same test class.
diff --git a/tempest/api/compute/floating_ips/base.py b/tempest/api/compute/floating_ips/base.py
index d6c302d..d39e955 100644
--- a/tempest/api/compute/floating_ips/base.py
+++ b/tempest/api/compute/floating_ips/base.py
@@ -41,3 +41,8 @@
     def setup_clients(cls):
         super(BaseFloatingIPsTest, cls).setup_clients()
         cls.client = cls.floating_ips_client
+        if CONF.enforce_scope.nova and hasattr(cls, 'os_project_reader'):
+            cls.reader_floating_ips_client = (
+                cls.os_project_reader.compute_floating_ips_client)
+        else:
+            cls.reader_floating_ips_client = cls.client
diff --git a/tempest/api/compute/floating_ips/test_floating_ips_actions.py b/tempest/api/compute/floating_ips/test_floating_ips_actions.py
index 6097bbc..8d569c3 100644
--- a/tempest/api/compute/floating_ips/test_floating_ips_actions.py
+++ b/tempest/api/compute/floating_ips/test_floating_ips_actions.py
@@ -37,10 +37,11 @@
         floating_ip_id_allocated = body['id']
         self.addCleanup(self.client.delete_floating_ip,
                         floating_ip_id_allocated)
-        floating_ip_details = self.client.show_floating_ip(
+        floating_ip_details = self.reader_floating_ips_client.show_floating_ip(
             floating_ip_id_allocated)['floating_ip']
         # Checking if the details of allocated IP is in list of floating IP
-        body = self.client.list_floating_ips()['floating_ips']
+        body = self.reader_floating_ips_client.list_floating_ips()[
+            'floating_ips']
         self.assertIn(floating_ip_details, body)
 
     @decorators.idempotent_id('de45e989-b5ca-4a9b-916b-04a52e7bbb8b')
@@ -87,8 +88,8 @@
             self.server_id)
 
         # Check instance_id in the floating_ip body
-        body = (self.client.show_floating_ip(self.floating_ip_id)
-                ['floating_ip'])
+        body = (self.reader_floating_ips_client.show_floating_ip(
+                self.floating_ip_id)['floating_ip'])
         self.assertEqual(self.server_id, body['instance_id'])
 
         # Disassociation of floating IP that was associated in this method
diff --git a/tempest/api/compute/floating_ips/test_floating_ips_actions_negative.py b/tempest/api/compute/floating_ips/test_floating_ips_actions_negative.py
index e99e218..4e98cbe 100644
--- a/tempest/api/compute/floating_ips/test_floating_ips_actions_negative.py
+++ b/tempest/api/compute/floating_ips/test_floating_ips_actions_negative.py
@@ -34,7 +34,8 @@
         super(FloatingIPsNegativeTestJSON, cls).resource_setup()
 
         # Generating a nonexistent floatingIP id
-        body = cls.client.list_floating_ips()['floating_ips']
+        body = cls.reader_floating_ips_client.list_floating_ips()[
+            'floating_ips']
         floating_ip_ids = [floating_ip['id'] for floating_ip in body]
         while True:
             if CONF.service_available.neutron:
diff --git a/tempest/api/compute/floating_ips/test_list_floating_ips.py b/tempest/api/compute/floating_ips/test_list_floating_ips.py
index fcbea2f..3eecab3 100644
--- a/tempest/api/compute/floating_ips/test_list_floating_ips.py
+++ b/tempest/api/compute/floating_ips/test_list_floating_ips.py
@@ -39,7 +39,8 @@
     @decorators.idempotent_id('16db31c3-fb85-40c9-bbe2-8cf7b67ff99f')
     def test_list_floating_ips(self):
         """Test listing floating ips"""
-        body = self.client.list_floating_ips()['floating_ips']
+        body = self.reader_floating_ips_client.list_floating_ips()[
+            'floating_ips']
         floating_ips = body
         self.assertNotEmpty(floating_ips,
                             "Expected floating IPs. Got zero.")
@@ -58,7 +59,8 @@
         floating_ip_instance_id = body['instance_id']
         floating_ip_ip = body['ip']
         floating_ip_fixed_ip = body['fixed_ip']
-        body = self.client.show_floating_ip(floating_ip_id)['floating_ip']
+        body = self.reader_floating_ips_client.show_floating_ip(
+            floating_ip_id)['floating_ip']
         # Comparing the details of floating IP
         self.assertEqual(floating_ip_instance_id,
                          body['instance_id'])
diff --git a/tempest/api/compute/floating_ips/test_list_floating_ips_negative.py b/tempest/api/compute/floating_ips/test_list_floating_ips_negative.py
index aa0320d..3919147 100644
--- a/tempest/api/compute/floating_ips/test_list_floating_ips_negative.py
+++ b/tempest/api/compute/floating_ips/test_list_floating_ips_negative.py
@@ -41,4 +41,5 @@
         else:
             non_exist_id = data_utils.rand_int_id(start=999)
         self.assertRaises(lib_exc.NotFound,
-                          self.client.show_floating_ip, non_exist_id)
+                          self.reader_floating_ips_client.show_floating_ip,
+                          non_exist_id)
diff --git a/tempest/api/object_storage/test_account_services.py b/tempest/api/object_storage/test_account_services.py
index 031ddee..b0e1f22 100644
--- a/tempest/api/object_storage/test_account_services.py
+++ b/tempest/api/object_storage/test_account_services.py
@@ -156,6 +156,8 @@
             self.assertEqual(len(container_list), limit)
 
     @decorators.idempotent_id('638f876d-6a43-482a-bbb3-0840bca101c6')
+    @testtools.skipIf(not CONF.auth.use_dynamic_credentials,
+                      'Skipped because of bug#2147232.')
     def test_list_containers_with_marker(self):
         """Test listing containers with marker parameter
 
diff --git a/tempest/cmd/account_generator.py b/tempest/cmd/account_generator.py
index f4f4b17..a7253a1 100755
--- a/tempest/cmd/account_generator.py
+++ b/tempest/cmd/account_generator.py
@@ -155,12 +155,16 @@
     # Create the list of resources to be provisioned for each process
     # NOTE(andreaf) get_credentials expects a string for types or a list for
     # roles. Adding all required inputs to the spec list.
-    spec = ['primary', 'alt', 'project_reader']
+    spec = ['alt', 'project_manager', 'project_member', 'project_reader']
+    if cred_provider.identity_version == 'v3':
+        spec += ['system_member', 'system_reader']
     if CONF.service_available.swift:
         spec.append([CONF.object_storage.operator_role])
         spec.append([CONF.object_storage.reseller_admin_role])
     if admin:
         spec.append('admin')
+        if cred_provider.identity_version == 'v3':
+            spec.append('system_admin')
     resources = []
     for cred_type in spec:
         scope = None
@@ -175,6 +179,17 @@
 
 def dump_accounts(resources, identity_version, account_file):
     accounts = []
+    # NOTE(gmaan): When accounts are created under same project (project
+    # manager, member, and reader), only the first account creation has the
+    # network resource and other accounts will not have it in the generated
+    # account file. To solve that, we need to build a project & network name
+    # map so that all accounts sharing a project get the network resource.
+    project_network_map = {}
+    for _, test_resource in resources:
+        proj = test_resource.project_name or test_resource.tenant_name
+        if proj and test_resource.network:
+            project_network_map.setdefault(proj, test_resource.network['name'])
+
     for resource in resources:
         cred_type, test_resource = resource
         account = {
@@ -182,8 +197,11 @@
             'password': test_resource.password
         }
         if identity_version == 3:
-            account['project_name'] = test_resource.project_name
-            account['domain_name'] = test_resource.domain_name
+            if test_resource.system:
+                account['system'] = test_resource.system
+            else:
+                account['project_name'] = test_resource.project_name
+                account['domain_name'] = test_resource.domain_name
         else:
             account['project_name'] = test_resource.tenant_name
 
@@ -194,9 +212,13 @@
         elif cred_type not in ['primary', 'alt']:
             account['roles'] = cred_type
 
+        net_name = None
         if test_resource.network:
-            account['resources'] = {}
-            account['resources']['network'] = test_resource.network['name']
+            net_name = test_resource.network['name']
+        elif test_resource.project_name:
+            net_name = project_network_map.get(test_resource.project_name)
+        if net_name:
+            account['resources'] = {'network': net_name}
         accounts.append(account)
     if os.path.exists(account_file):
         os.rename(account_file, '.'.join((account_file, 'bak')))
diff --git a/tempest/lib/common/cred_provider.py b/tempest/lib/common/cred_provider.py
index 9be7c5e..a20e68d 100644
--- a/tempest/lib/common/cred_provider.py
+++ b/tempest/lib/common/cred_provider.py
@@ -47,6 +47,26 @@
             raise exceptions.InvalidIdentityVersion(
                 identity_version=self.identity_version)
 
+    def _get_project_id(self, credential_type, scope, return_name=False):
+        same_creds = [['admin'], ['manager'], ['member'], ['reader']]
+        same_alt_creds = [['alt_admin'], ['alt_manager'],
+                          ['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))) or
+                          self._creds.get("%s_%s" % (scope, '_'.join(cred))))
+            if found_cred:
+                field = 'name' if return_name else 'id'
+                project_id = found_cred.get("%s_%s" % (scope, field))
+                LOG.debug("Reusing existing project %s from creds: %s ",
+                          project_id, found_cred)
+                return project_id
+        return None
+
     @abc.abstractmethod
     def get_primary_creds(self):
         return
diff --git a/tempest/lib/common/dynamic_creds.py b/tempest/lib/common/dynamic_creds.py
index 11e7215..be98100 100644
--- a/tempest/lib/common/dynamic_creds.py
+++ b/tempest/lib/common/dynamic_creds.py
@@ -383,24 +383,6 @@
         self.routers_admin_client.add_router_interface(router_id,
                                                        subnet_id=subnet_id)
 
-    def _get_project_id(self, credential_type, scope):
-        same_creds = [['admin'], ['manager'], ['member'], ['reader']]
-        same_alt_creds = [['alt_admin'], ['alt_manager'],
-                          ['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:
diff --git a/tempest/lib/common/preprov_creds.py b/tempest/lib/common/preprov_creds.py
index e685c2c..b52a131 100644
--- a/tempest/lib/common/preprov_creds.py
+++ b/tempest/lib/common/preprov_creds.py
@@ -114,7 +114,7 @@
                       object_storage_operator_role=None,
                       object_storage_reseller_admin_role=None):
         hash_dict = {'roles': {}, 'creds': {}, 'networks': {},
-                     'scoped_roles': {}}
+                     'scoped_roles': {}, 'project_names': {}}
 
         # Loop over the accounts read from the yaml file
         for account in accounts:
@@ -128,7 +128,7 @@
                 types = account.pop('types')
             if 'resources' in account:
                 resources = account.pop('resources')
-            if 'project_name' in account:
+            if 'project_name' in account or 'tenant_name' in account:
                 scope = 'project'
             elif 'domain_name' in account:
                 scope = 'domain'
@@ -140,9 +140,17 @@
             temp_hash.update(str(account_for_hash).encode('utf-8'))
             temp_hash_key = temp_hash.hexdigest()
             hash_dict['creds'][temp_hash_key] = account
+            if 'project_name' in account or 'tenant_name' in account:
+                field = ('tenant_name' if 'tenant_name' in account else
+                         'project_name')
+                hash_dict['project_names'].setdefault(
+                    account[field], [])
+                hash_dict['project_names'][account[field]].append(
+                    temp_hash_key)
             for role in roles:
-                hash_dict = cls._append_role(role, temp_hash_key,
-                                             hash_dict)
+                if not scope or scope != 'system':
+                    hash_dict = cls._append_role(role, temp_hash_key,
+                                                 hash_dict)
                 if scope:
                     hash_dict = cls._append_scoped_role(
                         scope, role, temp_hash_key, hash_dict)
@@ -180,6 +188,7 @@
                         'Unknown resource type %s, ignoring this field',
                         resource
                     )
+        LOG.debug('Pre provisioned hash dict: %s', hash_dict)
         return hash_dict
 
     def is_multi_user(self):
@@ -218,7 +227,7 @@
                'the credentials for this allocation request' % ','.join(names))
         raise lib_exc.InvalidCredentials(msg)
 
-    def _get_match_hash_list(self, roles=None, scope=None):
+    def _get_match_hash_list(self, roles=None, scope=None, project_name=None):
         hashes = []
         if roles:
             # Loop over all the creds for each role in the subdict and generate
@@ -238,12 +247,27 @@
                             "No credentials with role: %s specified in the "
                             "accounts file" % role)
                 hashes.append(temp_hashes)
-            # Take the list of lists and do a boolean and between each list to
-            # find the creds which fall under all the specified roles
+            # Take the list of lists and do a boolean and between each
+            # list to find the creds which fall under all the specified
+            # roles
             temp_list = set(hashes[0])
             for hash_list in hashes[1:]:
                 temp_list = temp_list & set(hash_list)
             hashes = temp_list
+
+            if project_name:
+                p_hashes = self.hash_dict['project_names'].get(
+                    project_name, [])
+                if not p_hashes:
+                    raise lib_exc.InvalidCredentials(
+                        "No credentials with project_name: %s specified in "
+                        "the accounts file" % project_name)
+                hashes = set(hashes) & set(p_hashes)
+                if not hashes:
+                    raise lib_exc.InvalidCredentials(
+                        "No credentials with project_name: %s, scope %s, and "
+                        "role: %s specified in the accounts file" % (
+                            project_name, scope, role))
         else:
             hashes = self.hash_dict['creds'].keys()
         # NOTE(mtreinish): admin is a special case because of the increased
@@ -257,6 +281,34 @@
             useable_hashes = [x for x in hashes if x not in admin_hashes]
         else:
             useable_hashes = hashes
+        # (gmaan): When a test requests its first credentials, prefer the
+        # project with the largest set of role accounts. This helps ensure
+        # that subsequent credential requests can be fulfilled from the same
+        # project.
+        if (scope == 'project' and not project_name and
+                roles in [['manager'], ['member'], ['reader'],
+                          ['alt_manager'], ['alt_member'], ['alt_reader']]):
+            if roles in [['manager'], ['member'], ['reader']]:
+                personas = ['manager', 'member', 'reader']
+            else:
+                personas = ['alt_manager', 'alt_member', 'alt_reader']
+            scoped_hashes = [
+                set(self.hash_dict['scoped_roles'].get(
+                    'project_%s' % persona, []))
+                for persona in personas
+            ]
+            scored = []
+            for hash in useable_hashes:
+                creds = self.hash_dict['creds'][hash]
+                proj = (creds.get('project_name') or
+                        creds.get('tenant_name'))
+                proj_hashes = set(
+                    self.hash_dict['project_names'].get(proj, []))
+                score = sum(1 for s in scoped_hashes if proj_hashes & s)
+                scored.append((score, hash))
+            scored.sort(reverse=True)
+            useable_hashes = [hash for _, hash in scored]
+        LOG.info('Pre provisioned useable hashes: %s', useable_hashes)
         return useable_hashes
 
     def _sanitize_creds(self, creds):
@@ -265,7 +317,12 @@
         return temp_creds
 
     def _get_creds(self, roles=None, scope=None):
-        useable_hashes = self._get_match_hash_list(roles, scope)
+        project_name = None
+        if scope == 'project' and roles in [['manager'], ['member'],
+                                            ['reader'], ['alt_manager'],
+                                            ['alt_member'], ['alt_reader']]:
+            project_name = self._get_project_id(roles, scope, return_name=True)
+        useable_hashes = self._get_match_hash_list(roles, scope, project_name)
         if not useable_hashes:
             msg = 'No users configured for type/roles %s' % roles
             raise lib_exc.InvalidCredentials(msg)
@@ -310,11 +367,7 @@
     # TODO(gmann): Remove this method in favor of get_project_member_creds()
     # after the deprecation phase.
     def get_primary_creds(self):
-        if self._creds.get('primary'):
-            return self._creds.get('primary')
-        net_creds = self._get_creds()
-        self._creds['primary'] = net_creds
-        return net_creds
+        return self.get_project_member_creds()
 
     # TODO(gmann): Replace this method with more appropriate name.
     # like get_project_alt_member_creds()
@@ -469,11 +522,25 @@
                                              identity_uri=self.identity_uri)
         networks_client = net_clients.network.NetworksClient()
         net_name = self.hash_dict['networks'].get(hash, None)
+        if not net_name:
+            # NOTE(gmaan): If no network is found for this hash, try to use
+            # the network from another account in the same project, since
+            # project manager/member/reader accounts share the same project
+            # network.
+            project = (creds_dict.get('project_name') or
+                       creds_dict.get('tenant_name'))
+            if project:
+                for project_hash in self.hash_dict['project_names'].get(
+                        project, []):
+                    if project_hash in self.hash_dict['networks']:
+                        net_name = self.hash_dict['networks'][project_hash]
+                        break
         try:
             network = fixed_network.get_network_from_name(
                 net_name, networks_client)
         except lib_exc.InvalidTestResource:
             network = {}
+        LOG.info('Network %s allocated for hash %s', network, hash)
         net_creds.set_resources(network=network)
         return net_creds
 
diff --git a/tempest/tests/cmd/test_account_generator.py b/tempest/tests/cmd/test_account_generator.py
index 9647467..30d5b71 100644
--- a/tempest/tests/cmd/test_account_generator.py
+++ b/tempest/tests/cmd/test_account_generator.py
@@ -54,6 +54,9 @@
             self.cred_client + '.create_project',
             return_value=fake_resource))
         self.useFixture(fixtures.MockPatch(
+            self.cred_client + '.show_project',
+            return_value=fake_resource))
+        self.useFixture(fixtures.MockPatch(
             self.cred_client + '.assign_user_role'))
         self.useFixture(fixtures.MockPatch(
             self.cred_client + '._check_role_exists',
@@ -80,6 +83,8 @@
             return_value=fake_domain_list))
         self.useFixture(fixtures.MockPatch(
             self.cred_client + '.assign_user_role_on_domain'))
+        self.useFixture(fixtures.MockPatch(
+            self.cred_client + '.assign_user_role_on_system'))
 
 
 class TestAccountGeneratorV2(base.TestCase, MockHelpersMixin):
@@ -136,6 +141,10 @@
     cred_client = 'tempest.lib.common.cred_client.V2CredsClient'
     dynamic_creds = ('tempest.lib.common.dynamic_creds.'
                      'DynamicCredentialProvider')
+    count_no_admin_no_swift = 4
+    count_admin_no_swift = 5
+    count_swift_admin = 7
+    count_swift_no_admin = 6
 
     def setUp(self):
         super(TestGenerateResourcesV2, self).setUp()
@@ -153,21 +162,17 @@
         resources = account_generator.generate_resources(
             self.cred_provider, admin=False)
         resource_types = [k for k, _ in resources]
-        # No admin, no swift, expect three credentials only
-        self.assertEqual(3, len(resources))
-        # Ensure create_user was invoked three times (three distinct users)
-        self.assertEqual(3, self.user_create_fixture.mock.call_count)
-        self.assertIn('primary', resource_types)
+        self.assertEqual(self.count_no_admin_no_swift, len(resources))
+        self.assertEqual(self.count_no_admin_no_swift,
+                         self.user_create_fixture.mock.call_count)
+        self.assertIn(['manager'], resource_types)
+        self.assertIn(['member'], resource_types)
         self.assertIn('alt', resource_types)
         self.assertNotIn('admin', resource_types)
         self.assertIn(['reader'], resource_types)
         self.assertNotIn(['fake_operator'], resource_types)
         self.assertNotIn(['fake_reseller'], resource_types)
         self.assertNotIn(['fake_owner'], resource_types)
-        for resource in resources:
-            self.assertIsNotNone(resource[1].network)
-            self.assertIsNotNone(resource[1].router)
-            self.assertIsNotNone(resource[1].subnet)
 
     def test_generate_resources_admin(self):
         cfg.CONF.set_default('swift', False, group='service_available')
@@ -178,21 +183,17 @@
         resources = account_generator.generate_resources(
             self.cred_provider, admin=True)
         resource_types = [k for k, _ in resources]
-        # Admin, no swift, expect three credentials only
-        self.assertEqual(4, len(resources))
-        # Ensure create_user was invoked 4 times (4 distinct users)
-        self.assertEqual(4, self.user_create_fixture.mock.call_count)
-        self.assertIn('primary', resource_types)
+        self.assertEqual(self.count_admin_no_swift, len(resources))
+        self.assertEqual(self.count_admin_no_swift,
+                         self.user_create_fixture.mock.call_count)
+        self.assertIn(['manager'], resource_types)
+        self.assertIn(['member'], resource_types)
         self.assertIn('alt', resource_types)
         self.assertIn('admin', resource_types)
         self.assertIn(['reader'], resource_types)
         self.assertNotIn(['fake_operator'], resource_types)
         self.assertNotIn(['fake_reseller'], resource_types)
         self.assertNotIn(['fake_owner'], resource_types)
-        for resource in resources:
-            self.assertIsNotNone(resource[1].network)
-            self.assertIsNotNone(resource[1].router)
-            self.assertIsNotNone(resource[1].subnet)
 
     def test_generate_resources_swift_admin(self):
         cfg.CONF.set_default('swift', True, group='service_available')
@@ -203,20 +204,16 @@
         resources = account_generator.generate_resources(
             self.cred_provider, admin=True)
         resource_types = [k for k, _ in resources]
-        # all options on, expect six credentials
-        self.assertEqual(6, len(resources))
-        # Ensure create_user was invoked 6 times (6 distinct users)
-        self.assertEqual(6, self.user_create_fixture.mock.call_count)
-        self.assertIn('primary', resource_types)
+        self.assertEqual(self.count_swift_admin, len(resources))
+        self.assertEqual(self.count_swift_admin,
+                         self.user_create_fixture.mock.call_count)
+        self.assertIn(['manager'], resource_types)
+        self.assertIn(['member'], resource_types)
         self.assertIn('alt', resource_types)
         self.assertIn('admin', resource_types)
         self.assertIn(['reader'], resource_types)
         self.assertIn(['fake_operator'], resource_types)
         self.assertIn(['fake_reseller'], resource_types)
-        for resource in resources:
-            self.assertIsNotNone(resource[1].network)
-            self.assertIsNotNone(resource[1].router)
-            self.assertIsNotNone(resource[1].subnet)
 
     def test_generate_resources_swift_no_admin(self):
         cfg.CONF.set_default('swift', True, group='service_available')
@@ -227,27 +224,27 @@
         resources = account_generator.generate_resources(
             self.cred_provider, admin=False)
         resource_types = [k for k, _ in resources]
-        # No Admin, swift, expect five credentials only
-        self.assertEqual(5, len(resources))
-        # Ensure create_user was invoked 5 times (5 distinct users)
-        self.assertEqual(5, self.user_create_fixture.mock.call_count)
-        self.assertIn('primary', resource_types)
+        self.assertEqual(self.count_swift_no_admin, len(resources))
+        self.assertEqual(self.count_swift_no_admin,
+                         self.user_create_fixture.mock.call_count)
+        self.assertIn(['manager'], resource_types)
+        self.assertIn(['member'], resource_types)
         self.assertIn('alt', resource_types)
         self.assertNotIn('admin', resource_types)
         self.assertIn(['reader'], resource_types)
         self.assertIn(['fake_operator'], resource_types)
         self.assertIn(['fake_reseller'], resource_types)
         self.assertNotIn(['fake_owner'], resource_types)
-        for resource in resources:
-            self.assertIsNotNone(resource[1].network)
-            self.assertIsNotNone(resource[1].router)
-            self.assertIsNotNone(resource[1].subnet)
 
 
 class TestGenerateResourcesV3(TestGenerateResourcesV2):
 
     identity_version = 3
     cred_client = 'tempest.lib.common.cred_client.V3CredsClient'
+    count_no_admin_no_swift = 6
+    count_admin_no_swift = 8
+    count_swift_admin = 10
+    count_swift_no_admin = 8
 
     def setUp(self):
         self.mock_domains()
@@ -261,6 +258,9 @@
     dynamic_creds = ('tempest.lib.common.dynamic_creds.'
                      'DynamicCredentialProvider')
     domain_is_in = False
+    expected_accounts = 7
+    expected_roles = 5
+    expected_accounts_with_network = 7
 
     def setUp(self):
         super(TestDumpAccountsV2, self).setUp()
@@ -272,6 +272,24 @@
         self.resources = account_generator.generate_resources(
             self.cred_provider, admin=True)
 
+    def _assert_accounts(self, accounts, handle, yaml_dump_mock):
+        _, f = yaml_dump_mock.call_args[0]
+        self.assertEqual(handle, f)
+        self.assertEqual(self.expected_accounts, len(accounts))
+        if self.domain_is_in:
+            self.assertIn('domain_name', accounts[0].keys())
+        else:
+            self.assertNotIn('domain_name', accounts[0].keys())
+        self.assertEqual(1, len([x for x in accounts if
+                                 x.get('types') == ['admin']]))
+        self.assertEqual(self.expected_roles,
+                         len([x for x in accounts if 'roles' in x]))
+        accounts_with_resources = [x for x in accounts if 'resources' in x]
+        self.assertEqual(self.expected_accounts_with_network,
+                         len(accounts_with_resources))
+        for account in accounts_with_resources:
+            self.assertIn('network', account.get('resources'))
+
     def test_dump_accounts(self):
         self.useFixture(fixtures.MockPatch('os.path.exists',
                                            return_value=False))
@@ -285,20 +303,8 @@
                                                 self.opts.accounts)
         mocked_open.assert_called_once_with(self.opts.accounts, 'w')
         handle = mocked_open()
-        # Ordered args in [0], keyword args in [1]
-        accounts, f = yaml_dump_mock.call_args[0]
-        self.assertEqual(handle, f)
-        self.assertEqual(6, len(accounts))
-        if self.domain_is_in:
-            self.assertIn('domain_name', accounts[0].keys())
-        else:
-            self.assertNotIn('domain_name', accounts[0].keys())
-        self.assertEqual(1, len([x for x in accounts if
-                                 x.get('types') == ['admin']]))
-        self.assertEqual(3, len([x for x in accounts if 'roles' in x]))
-        for account in accounts:
-            self.assertIn('resources', account)
-            self.assertIn('network', account.get('resources'))
+        accounts, _ = yaml_dump_mock.call_args[0]
+        self._assert_accounts(accounts, handle, yaml_dump_mock)
 
     def test_dump_accounts_existing_file(self):
         self.useFixture(fixtures.MockPatch('os.path.exists',
@@ -316,20 +322,8 @@
         rename_mock.assert_called_once_with(self.opts.accounts, backup_file)
         mocked_open.assert_called_once_with(self.opts.accounts, 'w')
         handle = mocked_open()
-        # Ordered args in [0], keyword args in [1]
-        accounts, f = yaml_dump_mock.call_args[0]
-        self.assertEqual(handle, f)
-        self.assertEqual(6, len(accounts))
-        if self.domain_is_in:
-            self.assertIn('domain_name', accounts[0].keys())
-        else:
-            self.assertNotIn('domain_name', accounts[0].keys())
-        self.assertEqual(1, len([x for x in accounts if
-                                 x.get('types') == ['admin']]))
-        self.assertEqual(3, len([x for x in accounts if 'roles' in x]))
-        for account in accounts:
-            self.assertIn('resources', account)
-            self.assertIn('network', account.get('resources'))
+        accounts, _ = yaml_dump_mock.call_args[0]
+        self._assert_accounts(accounts, handle, yaml_dump_mock)
 
 
 class TestDumpAccountsV3(TestDumpAccountsV2):
@@ -337,6 +331,9 @@
     identity_version = 3
     cred_client = 'tempest.lib.common.cred_client.V3CredsClient'
     domain_is_in = True
+    expected_accounts = 10
+    expected_roles = 8
+    expected_accounts_with_network = 7
 
     def setUp(self):
         self.mock_domains()
diff --git a/tempest/tests/lib/common/test_preprov_creds.py b/tempest/tests/lib/common/test_preprov_creds.py
index c5eaf7e..d3b6f88 100644
--- a/tempest/tests/lib/common/test_preprov_creds.py
+++ b/tempest/tests/lib/common/test_preprov_creds.py
@@ -33,6 +33,9 @@
 from tempest.tests.lib import fake_identity
 from tempest.tests.lib.services import registry_fixture
 
+# Store the real os.path.isfile before any test mocking replaces it
+original_isfile = os.path.isfile
+
 
 class TestPreProvisionedCredentials(base.TestCase):
 
@@ -53,9 +56,9 @@
     def _fake_accounts(cls, admin_role):
         return [
             {'username': 'test_user1', 'tenant_name': 'test_tenant1',
-             'password': 'p'},
+             'password': 'p', 'roles': ['member']},
             {'username': 'test_user2', 'project_name': 'test_tenant2',
-             'password': 'p'},
+             'password': 'p', 'roles': ['member']},
             {'username': 'test_user3', 'tenant_name': 'test_tenant3',
              'password': 'p'},
             {'username': 'test_user4', 'project_name': 'test_tenant4',
@@ -457,6 +460,123 @@
             # Get one more
             test_accounts_class.get_project_manager_creds()
 
+    def test_get_project_member_reader_creds_in_same_project(self):
+        # Verify that project_member and project_reader creds are allocated
+        # from the same project when both are available in the accounts file.
+        test_accounts = [
+            {'username': 'test_project_member1',
+             'project_name': 'test_project_shared1',
+             'password': 'p', 'roles': ['member']},
+            {'username': 'test_project_member3',
+             'project_name': 'test_project_shared3',
+             'password': 'p', 'roles': ['member']},
+            {'username': 'test_project_reader1',
+             'project_name': 'test_project_shared1',
+             'password': 'p', 'roles': ['reader']},
+        ]
+        self.useFixture(fixtures.MockPatch(
+            'tempest.lib.common.preprov_creds.read_accounts_yaml',
+            return_value=test_accounts))
+        # Use a real temp dir and real os.path.isfile so the hash-file locking
+        # mechanism works correctly for multiple credential allocations.
+        tmp_dir = self.useFixture(fixtures.TempDir())
+        self.patchobject(os.path, 'isfile', original_isfile)
+        params = dict(self.fixed_params)
+        params['accounts_lock_dir'] = tmp_dir.path
+        test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+            **params)
+        member_creds = test_accounts_class.get_project_member_creds()
+        reader_creds = test_accounts_class.get_project_reader_creds()
+        self.assertEqual(member_creds.project_name, reader_creds.project_name)
+
+    def test_get_match_hash_list_select_project_with_more_roles(self):
+        test_accounts = [
+            {'username': 'test_member_proj_1',
+             'project_name': 'project_1',
+             'password': 'p', 'roles': ['member']},
+            {'username': 'test_member_proj_2',
+             'project_name': 'project_2',
+             'password': 'p', 'roles': ['member']},
+            {'username': 'test_reader_proj_2',
+             'project_name': 'project_2',
+             'password': 'p', 'roles': ['reader']},
+        ]
+        self.useFixture(fixtures.MockPatch(
+            'tempest.lib.common.preprov_creds.read_accounts_yaml',
+            return_value=test_accounts))
+        test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+            **self.fixed_params)
+        hash_list = self._get_hash_list(test_accounts)
+        result = test_accounts_class._get_match_hash_list(
+            roles=['member'], scope='project')
+        self.assertEqual(2, len(result))
+        # First hash in selected one is from project_2 which has the member
+        # as well as reader role.
+        self.assertEqual(hash_list[1], result[0])
+        self.assertEqual(hash_list[0], result[1])
+
+    def test_get_match_hash_list_with_three_roles_in_project_account(self):
+        test_accounts = [
+            {'username': 'test_manager_proj_1',
+             'project_name': 'project_1',
+             'password': 'p', 'roles': ['manager']},
+            {'username': 'test_manager_proj_2',
+             'project_name': 'project_2',
+             'password': 'p', 'roles': ['manager']},
+            {'username': 'test_member_proj_2',
+             'project_name': 'project_2',
+             'password': 'p', 'roles': ['member']},
+            {'username': 'test_manager_proj_3',
+             'project_name': 'project_3',
+             'password': 'p', 'roles': ['manager']},
+            {'username': 'test_member_proj_3',
+             'project_name': 'project_3',
+             'password': 'p', 'roles': ['member']},
+            {'username': 'test_reader_proj_3',
+             'project_name': 'project_3',
+             'password': 'p', 'roles': ['reader']},
+        ]
+        self.useFixture(fixtures.MockPatch(
+            'tempest.lib.common.preprov_creds.read_accounts_yaml',
+            return_value=test_accounts))
+        test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+            **self.fixed_params)
+        hash_list = self._get_hash_list(test_accounts)
+        result = test_accounts_class._get_match_hash_list(
+            roles=['manager'], scope='project')
+        self.assertEqual(3, len(result))
+        # Hashes selection should be project_3 (with three roles),
+        # project_2 (with two roles), and then project_1 (with one role)
+        self.assertEqual(hash_list[3], result[0])
+        self.assertEqual(hash_list[1], result[1])
+        self.assertEqual(hash_list[0], result[2])
+
+    def test_get_match_hash_list_with_project_name(self):
+        test_accounts = [
+            {'username': 'test_member_proj_1',
+             'project_name': 'project_1',
+             'password': 'p', 'roles': ['member']},
+            {'username': 'test_reader_proj_1',
+             'project_name': 'project_1',
+             'password': 'p', 'roles': ['reader']},
+            {'username': 'test_member_proj_2',
+             'project_name': 'project_2',
+             'password': 'p', 'roles': ['member']},
+            {'username': 'test_reader_proj_2',
+             'project_name': 'project_2',
+             'password': 'p', 'roles': ['reader']},
+        ]
+        self.useFixture(fixtures.MockPatch(
+            'tempest.lib.common.preprov_creds.read_accounts_yaml',
+            return_value=test_accounts))
+        test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+            **self.fixed_params)
+        hash_list = self._get_hash_list(test_accounts)
+        result = test_accounts_class._get_match_hash_list(
+            roles=['reader'], scope='project', project_name='project_1')
+        self.assertEqual(1, len(result))
+        self.assertIn(hash_list[1], result)
+
 
 class TestPreProvisionedCredentialsV3(TestPreProvisionedCredentials):
 
@@ -477,9 +597,9 @@
     def _fake_accounts(cls, admin_role):
         return [
             {'username': 'test_user1', 'project_name': 'test_project1',
-             'domain_name': 'domain', 'password': 'p'},
+             'domain_name': 'domain', 'password': 'p', 'roles': ['member']},
             {'username': 'test_user2', 'project_name': 'test_project2',
-             'domain_name': 'domain', 'password': 'p'},
+             'domain_name': 'domain', 'password': 'p', 'roles': ['member']},
             {'username': 'test_user3', 'project_name': 'test_project3',
              'domain_name': 'domain', 'password': 'p'},
             {'username': 'test_user4', 'project_name': 'test_project4',
@@ -532,3 +652,27 @@
         with testtools.ExpectedException(lib_exc.InvalidCredentials):
             # Get one more
             test_accounts_class.get_domain_manager_creds()
+
+    def test_domain_creds_not_constrained_by_project(self):
+        test_accounts = [
+            {'username': 'test_domain_manager1',
+             'domain_name': 'test_domain', 'password': 'p',
+             'roles': ['manager']},
+            {'username': 'test_domain_member1',
+             'domain_name': 'test_domain', 'password': 'p',
+             'roles': ['member']},
+        ]
+        self.useFixture(fixtures.MockPatch(
+            'tempest.lib.common.preprov_creds.read_accounts_yaml',
+            return_value=test_accounts))
+        tmp_dir = self.useFixture(fixtures.TempDir())
+        self.patchobject(os.path, 'isfile', original_isfile)
+        params = dict(self.fixed_params)
+        params['accounts_lock_dir'] = tmp_dir.path
+        test_accounts_class = preprov_creds.PreProvisionedCredentialProvider(
+            **params)
+        test_accounts_class.get_domain_manager_creds()
+        # get_domain_member_creds should pass as get_domain_manager_creds
+        # restrict the member creds to be in same project as manager creds
+        domain_member = test_accounts_class.get_domain_member_creds()
+        self.assertIn('test_domain_member', domain_member.username)
diff --git a/zuul.d/integrated-gate.yaml b/zuul.d/integrated-gate.yaml
index fb710ee..fa5b8d7 100644
--- a/zuul.d/integrated-gate.yaml
+++ b/zuul.d/integrated-gate.yaml
@@ -108,6 +108,16 @@
         horizon: false
 
 - job:
+    name: tempest-full-preprov-creds
+    parent: tempest-full-py3
+    description: |
+      This job runs the full set of tempest tests using pre-provisioned
+      credentials.
+    vars:
+      devstack_localrc:
+        TEMPEST_USE_TEST_ACCOUNTS: True
+
+- job:
     name: tempest-full-centos-9-stream
     parent: tempest-full-py3
     nodeset: devstack-single-node-centos-9-stream
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index e5a25c2..6a77f8a 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -41,6 +41,11 @@
             irrelevant-files: *tempest-irrelevant-files
         - tempest-full-2024-2:
             irrelevant-files: *tempest-irrelevant-files
+        # NOTE(gmaan): cinder-tempest-lvm-multibackend-2024-2
+        # is added to cover the stable release testing with tempest
+        # plugin.
+        - cinder-tempest-lvm-multibackend-2024-2:
+            irrelevant-files: *tempest-irrelevant-files
         - tempest-multinode-full-py3:
             irrelevant-files: *tempest-irrelevant-files
         - tempest-tox-plugin-sanity-check:
@@ -113,10 +118,9 @@
         - neutron-ovs-tempest-dvr:
             voting: false
             irrelevant-files: *tempest-irrelevant-files
-        - tempest-full-test-account-py3:
-            voting: false
+        - tempest-full-preprov-creds:
             irrelevant-files: *tempest-irrelevant-files
-        - ironic-tempest-bios-ipmi-direct:
+        - ironic-tempest-bios-ipmi-autodetect:
             irrelevant-files: *tempest-irrelevant-files
         - openstack-tox-bashate:
             irrelevant-files: *tempest-irrelevant-files-2
@@ -133,6 +137,8 @@
             irrelevant-files: *tempest-irrelevant-files
         - tempest-full-py3:
             irrelevant-files: *tempest-irrelevant-files
+        - tempest-full-preprov-creds:
+            irrelevant-files: *tempest-irrelevant-files
         - tempest-extra-tests:
             irrelevant-files: *tempest-irrelevant-files
         - grenade:
@@ -149,7 +155,7 @@
             irrelevant-files: *tempest-irrelevant-files
         - nova-live-migration:
             irrelevant-files: *tempest-irrelevant-files
-        - ironic-tempest-bios-ipmi-direct:
+        - ironic-tempest-bios-ipmi-autodetect:
             irrelevant-files: *tempest-irrelevant-files
     experimental:
       jobs:
diff --git a/zuul.d/stable-jobs.yaml b/zuul.d/stable-jobs.yaml
index 74c0b3f..217a1f4 100644
--- a/zuul.d/stable-jobs.yaml
+++ b/zuul.d/stable-jobs.yaml
@@ -24,6 +24,14 @@
     nodeset: openstack-single-node-jammy
     override-checkout: stable/2024.2
 
+# NOTE(gmaan): Keep moving this job to oldest supported release so that we
+# can cover the tempest plugin testing on stable releases.
+- job:
+    name: cinder-tempest-lvm-multibackend-2024-2
+    parent: cinder-tempest-lvm-multibackend
+    nodeset: openstack-single-node-jammy
+    override-checkout: stable/2024.2
+
 - job:
     name: tempest-full-2026-1-extra-tests
     parent: tempest-extra-tests
diff --git a/zuul.d/tempest-specific.yaml b/zuul.d/tempest-specific.yaml
index 11c755e..d382bad 100644
--- a/zuul.d/tempest-specific.yaml
+++ b/zuul.d/tempest-specific.yaml
@@ -88,27 +88,8 @@
     timeout: 5000
 
 - job:
-    name: tempest-full-test-account-py3
-    parent: tempest-full-py3
-    description: |
-      This job runs the full set of tempest tests using pre-provisioned
-      credentials instead of dynamic credentials and py3.
-      Former names for this job were:
-        - legacy-tempest-dsvm-full-test-accounts
-        - legacy-tempest-dsvm-neutron-full-test-accounts
-        - legacy-tempest-dsvm-identity-v3-test-accounts
-    vars:
-      devstack_localrc:
-        TEMPEST_USE_TEST_ACCOUNTS: True
-        # FIXME(gmann): Nova and Glance have enabled the new defaults and scope
-        # by default in devstack and pre provisioned account code and testing
-        # needs to be move to new RBAC design testing. Until we do that, let's
-        # run these jobs with old defaults.
-        NOVA_ENFORCE_SCOPE: false
-        GLANCE_ENFORCE_SCOPE: false
-- job:
     name: tempest-full-test-account-no-admin-py3
-    parent: tempest-full-test-account-py3
+    parent: tempest-full-preprov-creds
     description: |
       This job runs the full set of tempest tests using pre-provisioned
       credentials and py3 without having an admin account.