Merge "[CI] Disable glance RBAC enforcement in generic jobs"
diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py
index 56a5b5a..0cd07d3 100644
--- a/manila_tempest_tests/config.py
+++ b/manila_tempest_tests/config.py
@@ -40,7 +40,7 @@
                     "This value is only used to validate the versions "
                     "response from Manila."),
     cfg.StrOpt("max_api_microversion",
-               default="2.74",
+               default="2.81",
                help="The maximum api microversion is configured to be the "
                     "value of the latest microversion supported by Manila."),
     cfg.StrOpt("region",
diff --git a/manila_tempest_tests/services/share/v2/json/shares_client.py b/manila_tempest_tests/services/share/v2/json/shares_client.py
index bf944d4..fe3e31c 100644
--- a/manila_tempest_tests/services/share/v2/json/shares_client.py
+++ b/manila_tempest_tests/services/share/v2/json/shares_client.py
@@ -2149,3 +2149,68 @@
         self.expected_success(200, resp.status)
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
+
+#################
+
+    def create_resource_lock(self, resource_id, resource_type,
+                             resource_action='delete', lock_reason=None,
+                             version=LATEST_MICROVERSION):
+        body = {
+            "resource_lock": {
+                'resource_id': resource_id,
+                'resource_type': resource_type,
+                'resource_action': resource_action,
+                'lock_reason': lock_reason,
+            },
+        }
+        body = json.dumps(body)
+        resp, body = self.post("resource-locks", body, version=version)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def get_resource_lock(self, lock_id, version=LATEST_MICROVERSION):
+        resp, body = self.get("resource-locks/%s" % lock_id, version=version)
+
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_resource_locks(self, filters=None, version=LATEST_MICROVERSION):
+        uri = (
+            "resource-locks?%s" % parse.urlencode(filters)
+            if filters else "resource-locks"
+        )
+
+        resp, body = self.get(uri, version=version)
+
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def update_resource_lock(self,
+                             lock_id,
+                             resource_action=None,
+                             lock_reason=None,
+                             version=LATEST_MICROVERSION):
+        uri = 'resource-locks/%s' % lock_id
+        post_body = {}
+        if resource_action:
+            post_body['resource_action'] = resource_action
+        if lock_reason:
+            post_body['lock_reason'] = lock_reason
+        body = json.dumps({'resource_lock': post_body})
+
+        resp, body = self.put(uri, body, version=version)
+
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_resource_lock(self, lock_id, version=LATEST_MICROVERSION):
+        uri = "resource-locks/%s" % lock_id
+
+        resp, body = self.delete(uri, version=version)
+
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py
index c5282d9..18562e5 100755
--- a/manila_tempest_tests/tests/api/base.py
+++ b/manila_tempest_tests/tests/api/base.py
@@ -792,6 +792,30 @@
         return security_service
 
     @classmethod
+    def create_resource_lock(cls, resource_id, resource_type='share',
+                             resource_action='delete', lock_reason=None,
+                             client=None, version=LATEST_MICROVERSION,
+                             cleanup_in_class=True):
+        lock_reason = lock_reason or "locked by tempest tests"
+        client = client or cls.shares_v2_client
+
+        lock = client.create_resource_lock(resource_id,
+                                           resource_type,
+                                           resource_action=resource_action,
+                                           lock_reason=lock_reason,
+                                           version=version)['resource_lock']
+        resource = {
+            "type": "resource_lock",
+            "id": lock["id"],
+            "client": client,
+        }
+        if cleanup_in_class:
+            cls.class_resources.insert(0, resource)
+        else:
+            cls.method_resources.insert(0, resource)
+        return lock
+
+    @classmethod
     def update_share_type(cls, share_type_id, name=None,
                           is_public=None, description=None,
                           client=None):
@@ -904,6 +928,8 @@
                     elif res["type"] == "quotas":
                         user_id = res.get('user_id')
                         client.reset_quotas(res_id, user_id=user_id)
+                    elif res["type"] == "resource_lock":
+                        client.delete_resource_lock(res_id)
                     else:
                         LOG.warning("Provided unsupported resource type for "
                                     "cleanup '%s'. Skipping.", res["type"])
diff --git a/manila_tempest_tests/tests/api/test_resource_locks.py b/manila_tempest_tests/tests/api/test_resource_locks.py
new file mode 100644
index 0000000..4f88d72
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_resource_locks.py
@@ -0,0 +1,291 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import datetime
+
+from oslo_utils import timeutils
+from oslo_utils import uuidutils
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from manila_tempest_tests.tests.api import base
+from manila_tempest_tests import utils
+
+CONF = config.CONF
+
+LOCKS_MIN_API_VERSION = '2.81'
+
+RESOURCE_LOCK_FIELDS = {
+    'id',
+    'resource_id',
+    'resource_action',
+    'resource_type',
+    'user_id',
+    'project_id',
+    'lock_context',
+    'created_at',
+    'updated_at',
+    'lock_reason',
+    'links',
+}
+
+
+class ResourceLockCRUTest(base.BaseSharesMixedTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(ResourceLockCRUTest, cls).skip_checks()
+        utils.check_skip_if_microversion_not_supported(LOCKS_MIN_API_VERSION)
+
+    @classmethod
+    def resource_setup(cls):
+        super(ResourceLockCRUTest, cls).resource_setup()
+        # create share type
+        share_type = cls.create_share_type()
+        cls.share_type_id = share_type['id']
+
+        # create share and place a "delete" lock on it
+        cls.share = cls.create_share(share_type_id=cls.share_type_id)
+        cls.lock = cls.create_resource_lock(cls.share['id'])
+
+    @decorators.attr(type=[base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND])
+    @decorators.idempotent_id('f3d162a6-2ab4-433b-b8e7-6bf4f0bb6b0e')
+    def test_list_resource_locks(self):
+        locks = self.shares_v2_client.list_resource_locks()['resource_locks']
+        self.assertIsInstance(locks, list)
+        self.assertIn(self.lock['id'], [x['id'] for x in locks])
+        lock = locks[0]
+        self.assertEqual(RESOURCE_LOCK_FIELDS, set(lock.keys()))
+
+    @decorators.attr(type=[base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND])
+    @decorators.idempotent_id('72cc0d43-f676-4dd8-8a93-faa71608de98')
+    def test_list_resource_locks_sorted_and_paginated(self):
+        lock_2 = self.create_resource_lock(self.share['id'],
+                                           cleanup_in_class=False)
+        lock_3 = self.create_resource_lock(self.share['id'],
+                                           cleanup_in_class=False)
+
+        expected_order = [self.lock['id'], lock_2['id']]
+
+        filters = {'sort_key': 'created_at', 'sort_dir': 'asc', 'limit': 2}
+        body = self.shares_v2_client.list_resource_locks(filters=filters)
+        # tempest/lib/common/rest_client.py's _parse_resp checks
+        # for number of keys in response's dict, if there is only single
+        # key, it returns directly this key, otherwise it returns
+        # parsed body. If limit param is used, then API returns
+        # multiple keys in response ('resource_locks' and
+        # 'resource_lock_links')
+        locks = body['resource_locks']
+        self.assertIsInstance(locks, list)
+        actual_order = [x['id'] for x in locks]
+        self.assertEqual(2, len(actual_order))
+        self.assertNotIn(lock_3['id'], actual_order)
+        self.assertEqual(expected_order, actual_order)
+
+    @decorators.attr(type=[base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND])
+    @decorators.idempotent_id('22831edc-9d99-432d-a0b6-85af8853db98')
+    def test_list_resource_locks_filtered(self):
+        # Filter by resource_id, resource_action, lock_reason_like,
+        # created_since, created_before
+        share_2 = self.create_share(share_type_id=self.share_type_id)
+        share_1_lock_2 = self.create_resource_lock(
+            self.share['id'],
+            lock_reason="clemson tigers rule",
+            cleanup_in_class=False)
+        share_2_lock = self.create_resource_lock(share_2['id'],
+                                                 cleanup_in_class=False)
+
+        # filter by resource_type
+        expected_locks = sorted([
+            self.lock['id'],
+            share_1_lock_2['id'],
+            share_2_lock['id']
+        ])
+        actual_locks = self.shares_v2_client.list_resource_locks(
+            filters={'resource_type': 'share'})['resource_locks']
+        self.assertEqual(expected_locks,
+                         sorted([lock['id'] for lock in actual_locks]))
+
+        # filter by resource_id
+        expected_locks = sorted([self.lock['id'], share_1_lock_2['id']])
+        actual_locks = self.shares_v2_client.list_resource_locks(
+            filters={'resource_id': self.share['id']})['resource_locks']
+        self.assertEqual(expected_locks,
+                         sorted([lock['id'] for lock in actual_locks]))
+
+        # filter by inexact lock reason
+        actual_locks = self.shares_v2_client.list_resource_locks(
+            filters={'lock_reason~': "clemson"})['resource_locks']
+        self.assertEqual([share_1_lock_2['id']],
+                         [lock['id'] for lock in actual_locks])
+
+        # timestamp filters
+        created_at_1 = timeutils.parse_strtime(self.lock['created_at'])
+        created_at_2 = timeutils.parse_strtime(share_2_lock['created_at'])
+        time_1 = created_at_1 - datetime.timedelta(seconds=1)
+        time_2 = created_at_2 - datetime.timedelta(microseconds=1)
+        filters_1 = {'created_since': str(time_1)}
+
+        # should return all resource locks created by this test including
+        # self.lock
+        actual_locks = self.shares_v2_client.list_resource_locks(
+            filters=filters_1)['resource_locks']
+        actual_lock_ids = [lock['id'] for lock in actual_locks]
+        self.assertGreaterEqual(len(actual_lock_ids), 3)
+        self.assertIn(self.lock['id'], actual_lock_ids)
+        self.assertIn(share_1_lock_2['id'], actual_lock_ids)
+
+        for lock in actual_locks:
+            time_diff_with_created_since = timeutils.delta_seconds(
+                time_1, timeutils.parse_strtime(lock['created_at']))
+            self.assertGreaterEqual(time_diff_with_created_since, 0)
+
+        filters_2 = {
+            'created_since': str(time_1),
+            'created_before': str(time_2),
+        }
+
+        actual_locks = self.shares_v2_client.list_resource_locks(
+            filters=filters_2)['resource_locks']
+        self.assertIsInstance(actual_locks, list)
+        actual_lock_ids = [lock['id'] for lock in actual_locks]
+        self.assertGreaterEqual(len(actual_lock_ids), 2)
+        self.assertIn(self.lock['id'], actual_lock_ids)
+        self.assertIn(share_1_lock_2['id'], actual_lock_ids)
+        self.assertNotIn(share_2_lock['id'], actual_lock_ids)
+
+    @decorators.attr(type=[base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND])
+    @decorators.idempotent_id('8cbf7331-f3a1-4c7b-ab1e-f8b938bf135e')
+    def test_get_resource_lock(self):
+        lock = self.shares_v2_client.get_resource_lock(
+            self.lock['id'])['resource_lock']
+
+        self.assertEqual(set(RESOURCE_LOCK_FIELDS), set(lock.keys()))
+        self.assertTrue(uuidutils.is_uuid_like(lock['id']))
+        self.assertEqual('share', lock['resource_type'])
+        self.assertEqual(self.share['id'], lock['resource_id'])
+        self.assertEqual('delete', lock['resource_action'])
+        self.assertEqual('user', lock['lock_context'])
+        self.assertEqual(self.shares_v2_client.user_id, lock['user_id'])
+        self.assertEqual(self.shares_v2_client.project_id, lock['project_id'])
+
+    @decorators.attr(type=[base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND])
+    @decorators.idempotent_id('a7f0fb6a-05ac-4afa-b8d9-04d20549bbd1')
+    def test_create_resource_lock(self):
+        # testing lock creation by a different user in the same project
+        project = self.os_admin.projects_client.show_project(
+            self.shares_v2_client.project_id)['project']
+        new_user_client = self.create_user_and_get_client(project)
+
+        lock = self.create_resource_lock(
+            self.share['id'],
+            client=new_user_client.shares_v2_client,
+            cleanup_in_class=False)
+
+        self.assertEqual(set(RESOURCE_LOCK_FIELDS), set(lock.keys()))
+        self.assertTrue(uuidutils.is_uuid_like(lock['id']))
+        self.assertEqual('share', lock['resource_type'])
+        self.assertEqual(self.share['id'], lock['resource_id'])
+        self.assertEqual('delete', lock['resource_action'])
+        self.assertEqual('user', lock['lock_context'])
+        self.assertEqual(new_user_client.shares_v2_client.user_id,
+                         lock['user_id'])
+        self.assertEqual(self.shares_v2_client.project_id, lock['project_id'])
+
+        # testing lock creation by admin
+        lock = self.create_resource_lock(
+            self.share['id'],
+            client=self.admin_shares_v2_client,
+            cleanup_in_class=False)
+        self.assertEqual('admin', lock['lock_context'])
+
+    @decorators.attr(type=[base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND])
+    @decorators.idempotent_id('d7b51cde-ff4f-45ce-a237-401e8be5b4e5')
+    def test_update_resource_lock(self):
+        lock = self.shares_v2_client.update_resource_lock(
+            self.lock['id'], lock_reason="new lock reason")['resource_lock']
+
+        # update is synchronous
+        self.assertEqual("new lock reason", lock['lock_reason'])
+
+        # verify get
+        lock = self.shares_v2_client.get_resource_lock(lock['id'])
+        self.assertEqual("new lock reason",
+                         lock['resource_lock']['lock_reason'])
+
+
+class ResourceLockDeleteTest(base.BaseSharesMixedTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(ResourceLockDeleteTest, cls).skip_checks()
+        utils.check_skip_if_microversion_not_supported(LOCKS_MIN_API_VERSION)
+
+    @classmethod
+    def resource_setup(cls):
+        super(ResourceLockDeleteTest, cls).resource_setup()
+        cls.share_type_id = cls.create_share_type()['id']
+
+    @decorators.idempotent_id('835fd617-4600-40a0-9ba1-40e5e0097b01')
+    @decorators.attr(type=[base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND])
+    def test_delete_lock(self):
+        share = self.create_share(share_type_id=self.share_type_id)
+        lock_1 = self.create_resource_lock(share['id'], cleanup_in_class=False)
+        lock_2 = self.create_resource_lock(share['id'], cleanup_in_class=False)
+
+        locks = self.shares_v2_client.list_resource_locks(
+            filters={'resource_id': share['id']})['resource_locks']
+        self.assertEqual(sorted([lock_1['id'], lock_2['id']]),
+                         sorted([lock['id'] for lock in locks]))
+
+        self.shares_v2_client.delete_resource_lock(lock_1['id'])
+        locks = self.shares_v2_client.list_resource_locks(
+            filters={'resource_id': share['id']})['resource_locks']
+        self.assertEqual(1, len(locks))
+        self.assertIn(lock_2['id'], [lock['id'] for lock in locks])
+
+    @decorators.idempotent_id('a96e70c7-0afe-4335-9abc-4b45ef778bd7')
+    @decorators.attr(type=[base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND])
+    def test_delete_locked_resource(self):
+        share = self.create_share(share_type_id=self.share_type_id)
+        lock_1 = self.create_resource_lock(share['id'], cleanup_in_class=False)
+        lock_2 = self.create_resource_lock(share['id'], cleanup_in_class=False)
+
+        # share can't be deleted when a lock exists
+        self.assertRaises(lib_exc.Forbidden,
+                          self.shares_v2_client.delete_share,
+                          share['id'])
+
+        # admin can't do this either
+        self.assertRaises(lib_exc.Forbidden,
+                          self.admin_shares_v2_client.delete_share,
+                          share['id'])
+        # "the force" shouldn't work either
+        self.assertRaises(lib_exc.Forbidden,
+                          self.admin_shares_v2_client.delete_share,
+                          share['id'],
+                          params={'force': True})
+
+        self.shares_v2_client.delete_resource_lock(lock_1['id'])
+
+        # there's at least one lock, share deletion should still fail
+        self.assertRaises(lib_exc.Forbidden,
+                          self.shares_v2_client.delete_share,
+                          share['id'])
+
+        self.shares_v2_client.delete_resource_lock(lock_2['id'])
+
+        # locks are gone, share deletion should be possible
+        self.shares_v2_client.delete_share(share['id'])
+        self.shares_v2_client.wait_for_resource_deletion(
+            share_id=share["id"])
diff --git a/manila_tempest_tests/tests/api/test_resource_locks_negative.py b/manila_tempest_tests/tests/api/test_resource_locks_negative.py
new file mode 100644
index 0000000..9501d6c
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_resource_locks_negative.py
@@ -0,0 +1,127 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from manila_tempest_tests.tests.api import base
+from manila_tempest_tests import utils
+
+CONF = config.CONF
+
+LOCKS_MIN_API_VERSION = '2.81'
+
+
+class ResourceLockNegativeTestAPIOnly(base.BaseSharesMixedTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(ResourceLockNegativeTestAPIOnly, cls).skip_checks()
+        utils.check_skip_if_microversion_not_supported(LOCKS_MIN_API_VERSION)
+
+    @decorators.attr(type=[base.TAG_NEGATIVE, base.TAG_API])
+    @decorators.idempotent_id('dd978cf7-1622-49e8-a6c8-3da4ac6c6f86')
+    def test_create_resource_lock_invalid_resource(self):
+        self.assertRaises(
+            lib_exc.BadRequest,
+            self.shares_v2_client.create_resource_lock,
+            'invalid-share-id',
+            'share'
+        )
+
+    @decorators.attr(type=[base.TAG_NEGATIVE, base.TAG_API])
+    @decorators.idempotent_id('d5600bdc-72c8-43fd-9900-c112aa6c87fa')
+    def test_delete_resource_lock_invalid(self):
+        self.assertRaises(
+            lib_exc.NotFound,
+            self.shares_v2_client.delete_resource_lock,
+            'invalid-lock-id'
+        )
+
+
+class ResourceLockNegativeTestWithShares(base.BaseSharesMixedTest):
+    @classmethod
+    def skip_checks(cls):
+        super(ResourceLockNegativeTestWithShares, cls).skip_checks()
+        utils.check_skip_if_microversion_not_supported(LOCKS_MIN_API_VERSION)
+
+    @classmethod
+    def resource_setup(cls):
+        super(ResourceLockNegativeTestWithShares, cls).resource_setup()
+        share_type = cls.create_share_type()
+        cls.share = cls.create_share(share_type_id=share_type['id'])
+        cls.user_project = cls.os_admin.projects_client.show_project(
+            cls.shares_v2_client.project_id)['project']
+
+    @decorators.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
+    @decorators.idempotent_id('658297a8-d675-471d-8a19-3d9e9af3a352')
+    def test_create_resource_lock_invalid_resource_action(self):
+        self.assertRaises(
+            lib_exc.BadRequest,
+            self.shares_v2_client.create_resource_lock,
+            self.share['id'],
+            'share',
+            resource_action='invalid-action'
+        )
+
+    @decorators.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
+    @decorators.idempotent_id('0057b3e7-c250-492d-805b-e355dff954ed')
+    def test_create_resource_lock_invalid_lock_reason_too_long(self):
+        self.assertRaises(
+            lib_exc.BadRequest,
+            self.shares_v2_client.create_resource_lock,
+            self.share['id'],
+            'share',
+            resource_action='delete',
+            lock_reason='invalid' * 150,
+        )
+
+    @decorators.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
+    @decorators.idempotent_id('a2db3d29-b42f-4c0b-b484-afd32f91f747')
+    def test_update_resource_lock_invalid_param(self):
+        lock = self.create_resource_lock(self.share['id'])
+        self.assertRaises(
+            lib_exc.BadRequest,
+            self.shares_v2_client.update_resource_lock,
+            lock['id'],
+            resource_action='invalid-action'
+        )
+        self.assertRaises(
+            lib_exc.BadRequest,
+            self.shares_v2_client.update_resource_lock,
+            lock['id'],
+            lock_reason='invalid' * 150,
+        )
+
+    @decorators.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
+    @decorators.idempotent_id('45b12120-0fc3-461f-8776-fdb92e599394')
+    def test_update_resource_lock_created_by_different_user(self):
+        lock = self.create_resource_lock(self.share['id'])
+        new_user = self.create_user_and_get_client(project=self.user_project)
+        self.assertRaises(
+            lib_exc.Forbidden,
+            new_user.shares_v2_client.update_resource_lock,
+            lock['id'],
+            lock_reason="I shouldn't be able to do this",
+        )
+
+    @decorators.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
+    @decorators.idempotent_id('00a8ef2b-8769-4aad-aefc-43fc579492f7')
+    def test_delete_resource_lock_created_by_different_user(self):
+        lock = self.create_resource_lock(self.share['id'])
+        new_user = self.create_user_and_get_client(project=self.user_project)
+        self.assertRaises(
+            lib_exc.Forbidden,
+            new_user.shares_v2_client.delete_resource_lock,
+            lock['id'],
+        )
diff --git a/manila_tempest_tests/tests/rbac/base.py b/manila_tempest_tests/tests/rbac/base.py
index 06759ff..d3c63de 100644
--- a/manila_tempest_tests/tests/rbac/base.py
+++ b/manila_tempest_tests/tests/rbac/base.py
@@ -129,6 +129,20 @@
         return share_group_type
 
     @classmethod
+    def create_share_group(cls, client, share_group_type_id, share_type_ids):
+        name = data_utils.rand_name('share-group')
+        share_group = client.create_share_group(
+            name=name, share_group_type_id=share_group_type_id,
+            share_type_ids=share_type_ids)['share_group']
+        waiters.wait_for_resource_status(
+            client, share_group['id'], 'available',
+            resource_name='share_group')
+        cls.addClassResourceCleanup(
+            cls.delete_resource, client,
+            share_group_id=share_group['id'])
+        return share_group
+
+    @classmethod
     def get_share_type(cls):
         return cls.shares_v2_client.get_default_share_type()['share_type']
 
diff --git a/manila_tempest_tests/tests/rbac/test_share_groups.py b/manila_tempest_tests/tests/rbac/test_share_groups.py
new file mode 100644
index 0000000..b09a5e4
--- /dev/null
+++ b/manila_tempest_tests/tests/rbac/test_share_groups.py
@@ -0,0 +1,474 @@
+# Copyright 2022 Red Hat, Inc.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import abc
+
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+from testtools import testcase as tc
+
+from manila_tempest_tests.common import waiters
+from manila_tempest_tests.tests.api import base
+from manila_tempest_tests.tests.rbac import base as rbac_base
+
+CONF = config.CONF
+
+
+class ShareRbacShareGroupsTests(rbac_base.ShareRbacBaseTests,
+                                metaclass=abc.ABCMeta):
+
+    @classmethod
+    def skip_checks(cls):
+        super(ShareRbacShareGroupsTests, cls).skip_checks()
+        if cls.protocol not in CONF.share.enable_protocols:
+            message = "%s tests are disabled" % cls.protocol
+            raise cls.skipException(message)
+
+    @classmethod
+    def setup_clients(cls):
+        super(ShareRbacShareGroupsTests, cls).setup_clients()
+        cls.persona = getattr(cls, 'os_%s' % cls.credentials[0])
+        cls.client = cls.persona.share_v2.SharesV2Client()
+        cls.admin_shares_v2_client = (
+            cls.os_project_admin.share_v2.SharesV2Client())
+        cls.alt_project_share_v2_client = (
+            cls.os_project_alt_member.share_v2.SharesV2Client())
+
+    @classmethod
+    def resource_setup(cls):
+        super(ShareRbacShareGroupsTests, cls).resource_setup()
+        cls.share_type = cls.create_share_type()
+        cls.share_group_type = cls.create_share_group_type(
+            cls.share_type['id'])
+
+    def share_group(self, share_group_type_id, share_type_ids):
+        share_group = {}
+        share_group['name'] = data_utils.rand_name('share_group')
+        share_group['share_group_type_id'] = share_group_type_id
+        share_group['share_type_ids'] = [share_type_ids]
+        return share_group
+
+    @abc.abstractmethod
+    def test_get_share_group(self):
+        pass
+
+    @abc.abstractmethod
+    def test_list_share_groups(self):
+        pass
+
+    @abc.abstractmethod
+    def test_create_share_group(self):
+        pass
+
+    @abc.abstractmethod
+    def test_delete_share_group(self):
+        pass
+
+    @abc.abstractmethod
+    def test_force_delete_share_group(self):
+        pass
+
+    @abc.abstractmethod
+    def test_update_share_group(self):
+        pass
+
+    @abc.abstractmethod
+    def test_reset_share_group(self):
+        pass
+
+
+class TestProjectAdminTestsNFS(ShareRbacShareGroupsTests, base.BaseSharesTest):
+
+    credentials = ['project_admin', 'project_alt_member']
+    protocol = 'nfs'
+
+    @classmethod
+    def setup_clients(cls):
+        super(TestProjectAdminTestsNFS, cls).setup_clients()
+        project_member = cls.setup_user_client(
+            cls.persona, project_id=cls.persona.credentials.project_id)
+        cls.share_member_client = project_member.share_v2.SharesV2Client()
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('0de993c5-8389-4997-8f7f-345e27f563f1')
+    def test_get_share_group(self):
+        share_group = self.create_share_group(
+            self.share_member_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'get_share_group', expected_status=200,
+            share_group_id=share_group['id'])
+
+        alt_share_group = self.create_share_group(
+            self.alt_project_share_v2_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'get_share_group', expected_status=200,
+            share_group_id=alt_share_group['id'])
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('3b277a44-dcae-46da-a58c-f5281d8abc84')
+    def test_list_share_groups(self):
+        share_group = self.create_share_group(
+            self.share_member_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        alt_share_group = self.create_share_group(
+            self.alt_project_share_v2_client, self.share_group_type['id'],
+            [self.share_type['id']])
+
+        params = {"all_tenants": 1}
+        share_group_list = self.do_request(
+            'list_share_groups', expected_status=200,
+            params=params)['share_groups']
+        share_group_id_list = [
+            s['id'] for s in share_group_list
+        ]
+
+        self.assertIn(share_group['id'], share_group_id_list)
+        self.assertIn(alt_share_group['id'], share_group_id_list)
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('d060996e-c5f2-4dff-820b-6892a096a425')
+    def test_create_share_group(self):
+        share_group = self.do_request(
+            'create_share_group', expected_status=202,
+            **self.share_group(self.share_group_type['id'],
+                               self.share_type['id']))['share_group']
+        waiters.wait_for_resource_status(
+            self.client, share_group['id'], 'available',
+            resource_name='share_group')
+        self.addCleanup(self.client.wait_for_resource_deletion,
+                        share_group_id=share_group['id'])
+        self.addCleanup(self.client.delete_share_group, share_group['id'])
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('ea6cbb78-057e-4fbc-86bf-125b033cb76f')
+    def test_delete_share_group(self):
+        share_group = self.create_share_group(
+            self.share_member_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'delete_share_group', expected_status=202,
+            share_group_id=share_group['id'])
+        self.client.wait_for_resource_deletion(
+            share_group_id=share_group['id'])
+
+        alt_share_group = self.create_share_group(
+            self.alt_project_share_v2_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'delete_share_group', expected_status=202,
+            share_group_id=alt_share_group['id'])
+        self.client.wait_for_resource_deletion(
+            share_group_id=alt_share_group['id'])
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('2cb00ffb-47e3-495e-853c-007752c9e679')
+    def test_force_delete_share_group(self):
+        share_group = self.create_share_group(
+            self.share_member_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'share_group_force_delete', expected_status=202,
+            share_group_id=share_group['id'])
+        self.client.wait_for_resource_deletion(
+            share_group_id=share_group['id'])
+
+        alt_share_group = self.create_share_group(
+            self.alt_project_share_v2_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'share_group_force_delete', expected_status=202,
+            share_group_id=alt_share_group['id'])
+        self.client.wait_for_resource_deletion(
+            share_group_id=alt_share_group['id'])
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('1bab40d5-bdba-4a23-9300-807fe513bf15')
+    def test_update_share_group(self):
+        share_group = self.create_share_group(
+            self.share_member_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        name = data_utils.rand_name('rename_share')
+        self.do_request(
+            'update_share_group', expected_status=200,
+            share_group_id=share_group['id'], name=name)
+
+        alt_share_group = self.create_share_group(
+            self.share_member_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        name = data_utils.rand_name('rename_share')
+        self.do_request(
+            'update_share_group', expected_status=200,
+            share_group_id=alt_share_group['id'], name=name)
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('069bc68e-6411-44b8-abe9-399885f0eee5')
+    def test_reset_share_group(self):
+        share_group = self.create_share_group(
+            self.share_member_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'share_group_reset_state', expected_status=202,
+            share_group_id=share_group['id'], status='error')
+
+        alt_share_group = self.create_share_group(
+            self.share_member_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'share_group_reset_state', expected_status=202,
+            share_group_id=alt_share_group['id'], status='error')
+
+
+class TestProjectMemberTestsNFS(ShareRbacShareGroupsTests,
+                                base.BaseSharesTest):
+
+    credentials = ['project_member', 'project_admin', 'project_alt_member']
+    protocol = 'nfs'
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('a29e1a68-220e-40fc-98ea-9092fd256d07')
+    def test_get_share_group(self):
+        share_client = getattr(self, 'share_member_client', self.client)
+        share_group = self.create_share_group(
+            share_client, self.share_group_type['id'], [self.share_type['id']])
+        self.do_request(
+            'get_share_group', expected_status=200,
+            share_group_id=share_group['id'])
+
+        alt_share_group = self.create_share_group(
+            self.alt_project_share_v2_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'get_share_group', expected_status=lib_exc.NotFound,
+            share_group_id=alt_share_group['id'])
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('d9c04932-c47e-46e0-bfcf-79c2af32c4c7')
+    def test_list_share_groups(self):
+        share_client = getattr(self, 'share_member_client', self.client)
+        share_group = self.create_share_group(
+            share_client, self.share_group_type['id'], [self.share_type['id']])
+        alt_share_group = self.create_share_group(
+            self.alt_project_share_v2_client, self.share_group_type['id'],
+            [self.share_type['id']])
+
+        params = {"all_tenants": 1}
+        share_group_list = self.do_request(
+            'list_share_groups', expected_status=200,
+            params=params)['share_groups']
+        share_group_id_list = [
+            s['id'] for s in share_group_list
+        ]
+
+        self.assertIn(share_group['id'], share_group_id_list)
+        self.assertNotIn(alt_share_group['id'], share_group_id_list)
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('ebad2242-1fb5-4d99-9a5a-281c1944e03d')
+    def test_create_share_group(self):
+        share_group = self.do_request(
+            'create_share_group', expected_status=202,
+            **self.share_group(self.share_group_type['id'],
+                               self.share_type['id']))['share_group']
+        waiters.wait_for_resource_status(
+            self.client, share_group['id'], 'available',
+            resource_name='share_group')
+        self.addCleanup(self.client.wait_for_resource_deletion,
+                        share_group_id=share_group['id'])
+        self.addCleanup(self.client.delete_share_group, share_group['id'])
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('f5c243e4-5128-4a1c-9a15-8c9f0a44437e')
+    def test_delete_share_group(self):
+        share_group = self.create_share_group(
+            self.client, self.share_group_type['id'], [self.share_type['id']])
+        self.do_request(
+            'delete_share_group', expected_status=202,
+            share_group_id=share_group['id'])
+        self.client.wait_for_resource_deletion(
+            share_group_id=share_group['id'])
+
+        alt_share_group = self.create_share_group(
+            self.alt_project_share_v2_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'delete_share_group', expected_status=lib_exc.NotFound,
+            share_group_id=alt_share_group['id'])
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('36a58d50-1257-479f-80a2-f9b7a00814e2')
+    def test_force_delete_share_group(self):
+        share_client = getattr(self, 'share_member_client', self.client)
+        share_group = self.create_share_group(
+            share_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'share_group_force_delete', expected_status=lib_exc.Forbidden,
+            share_group_id=share_group['id'])
+
+        alt_share_group = self.create_share_group(
+            self.alt_project_share_v2_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'share_group_force_delete', expected_status=lib_exc.Forbidden,
+            share_group_id=alt_share_group['id'])
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('cf9e34b6-6c04-4920-a811-2dbcf07ba14e')
+    def test_update_share_group(self):
+        share_group = self.create_share_group(
+            self.client, self.share_group_type['id'], [self.share_type['id']])
+        name = data_utils.rand_name('rename_share')
+        self.do_request(
+            'update_share_group', expected_status=200,
+            share_group_id=share_group['id'], name=name)
+
+        alt_share_group = self.create_share_group(
+            self.alt_project_share_v2_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        name = data_utils.rand_name('rename_share')
+        self.do_request(
+            'update_share_group', expected_status=lib_exc.NotFound,
+            share_group_id=alt_share_group['id'], name=name)
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('2108c4cd-74e0-467f-823a-e44cf8686afa')
+    def test_reset_share_group(self):
+        share_client = getattr(self, 'share_member_client', self.client)
+        share_group = self.create_share_group(
+            share_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'share_group_reset_state', expected_status=lib_exc.Forbidden,
+            share_group_id=share_group['id'], status='error')
+
+        alt_share_group = self.create_share_group(
+            self.alt_project_share_v2_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'share_group_reset_state', expected_status=lib_exc.Forbidden,
+            share_group_id=alt_share_group['id'], status='error')
+
+
+class TestProjectReaderTestsNFS(TestProjectMemberTestsNFS):
+    """Test suite for basic share group operations by reader user
+
+    In order to test certain share operations we must create a share group
+    resource for this. Since reader user is limited in resources creation, we
+    are forced to use admin credentials, so we can test other share operations.
+    In this class we use admin user to create a member user within reader
+    project. That way we can perform a reader actions on this resource.
+    """
+
+    credentials = ['project_reader', 'project_admin', 'project_alt_member']
+
+    @classmethod
+    def setup_clients(cls):
+        super(TestProjectReaderTestsNFS, cls).setup_clients()
+        project_member = cls.setup_user_client(
+            cls.os_project_admin,
+            project_id=cls.persona.credentials.project_id)
+        cls.share_member_client = project_member.share_v2.SharesV2Client()
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('ec0ecbb0-5d45-4624-bb26-8b2e140e2ea9')
+    def test_get_share_group(self):
+        super(TestProjectReaderTestsNFS, self).test_get_share_group()
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('4ac87837-5bdf-4253-ab50-dd6efdcea285')
+    def test_list_share_groups(self):
+        super(TestProjectReaderTestsNFS, self).test_list_share_groups()
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('526dcd91-e789-48f8-b209-c384d77e5803')
+    def test_create_share_group(self):
+        self.do_request(
+            'create_share_group', expected_status=lib_exc.Forbidden,
+            **self.share_group(self.share_group_type['id'],
+                               self.share_type['id']))
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('fdf4d49e-a576-441f-9a3c-e2d58c0d8679')
+    def test_delete_share_group(self):
+        share_group = self.create_share_group(
+            self.share_member_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'delete_share_group', expected_status=lib_exc.Forbidden,
+            share_group_id=share_group['id'])
+
+        alt_share_group = self.create_share_group(
+            self.alt_project_share_v2_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        self.do_request(
+            'delete_share_group', expected_status=lib_exc.Forbidden,
+            share_group_id=alt_share_group['id'])
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('eddca093-e3a1-4a79-a8c7-8fd04c77b02f')
+    def test_force_delete_share_group(self):
+        super(TestProjectReaderTestsNFS, self).test_force_delete_share_group()
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('4530c19d-0aa5-402e-ac83-a3f2333f6c71')
+    def test_update_share_group(self):
+        share_group = self.create_share_group(
+            self.share_member_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        name = data_utils.rand_name('rename_share')
+        self.do_request(
+            'update_share_group', expected_status=lib_exc.Forbidden,
+            share_group_id=share_group['id'], name=name)
+
+        alt_share_group = self.create_share_group(
+            self.alt_project_share_v2_client, self.share_group_type['id'],
+            [self.share_type['id']])
+        name = data_utils.rand_name('rename_share')
+        self.do_request(
+            'update_share_group', expected_status=lib_exc.Forbidden,
+            share_group_id=alt_share_group['id'], name=name)
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    @decorators.idempotent_id('37f23531-69b5-418d-bd91-7913341586ec')
+    def test_reset_share_group(self):
+        super(TestProjectReaderTestsNFS, self).test_reset_share_group()
+
+
+class TestProjectAdminTestsCEPHFS(TestProjectAdminTestsNFS):
+    protocol = 'cephfs'
+
+
+class TestProjectMemberTestsCEPHFS(TestProjectMemberTestsNFS):
+    protocol = 'cephfs'
+
+
+class TestProjectReaderTestsCEPHFS(TestProjectReaderTestsNFS):
+    protocol = 'cephfs'
+
+
+class TestProjectAdminTestsCIFS(TestProjectAdminTestsNFS):
+    protocol = 'cifs'
+
+
+class TestProjectMemberTestsCIFS(TestProjectMemberTestsNFS):
+    protocol = 'cifs'
+
+
+class TestProjectReaderTestsCIFS(TestProjectReaderTestsNFS):
+    protocol = 'cifs'
diff --git a/manila_tempest_tests/tests/scenario/manager.py b/manila_tempest_tests/tests/scenario/manager.py
index a73ab44..fe2833a 100644
--- a/manila_tempest_tests/tests/scenario/manager.py
+++ b/manila_tempest_tests/tests/scenario/manager.py
@@ -16,7 +16,6 @@
 
 from oslo_log import log
 from oslo_utils import uuidutils
-from tempest.common import image as common_image
 from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib.common.utils import test_utils
@@ -45,24 +44,16 @@
             'name': name,
             'container_format': fmt,
             'disk_format': disk_format or fmt,
+            'visibility': 'private'
         }
-        if CONF.image_feature_enabled.api_v1:
-            params['is_public'] = 'False'
-            params['properties'] = properties
-            params = {'headers': common_image.image_meta_to_headers(**params)}
-        else:
-            params['visibility'] = 'private'
-            # Additional properties are flattened out in the v2 API.
-            params.update(properties)
+        # Additional properties are flattened out in the v2 API.
+        params.update(properties)
         body = self.image_client.create_image(**params)
         image = body['image'] if 'image' in body else body
         self.addCleanup(self.image_client.delete_image, image['id'])
         self.assertEqual("queued", image['status'])
         with open(path, 'rb') as image_file:
-            if CONF.image_feature_enabled.api_v1:
-                self.image_client.update_image(image['id'], data=image_file)
-            else:
-                self.image_client.store_image_file(image['id'], image_file)
+            self.image_client.store_image_file(image['id'], image_file)
         return image['id']
 
     def glance_image_create(self):
diff --git a/playbooks/manila-tempest-plugin-standalone/run.yaml b/playbooks/manila-tempest-plugin-standalone/run.yaml
index 8df9205..26ad69a 100644
--- a/playbooks/manila-tempest-plugin-standalone/run.yaml
+++ b/playbooks/manila-tempest-plugin-standalone/run.yaml
@@ -5,7 +5,6 @@
 - hosts: tempest
   roles:
     - setup-tempest-run-dir
-    - set-tempest-config
     - setup-tempest-data-dir
     - acl-devstack-files
     - run-tempest
diff --git a/roles/set-tempest-config/README.rst b/roles/set-tempest-config/README.rst
deleted file mode 100644
index 9402d3c..0000000
--- a/roles/set-tempest-config/README.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-set-tempest-config
-==================
-
-This is a workaround for the `merge_config_file <https://opendev
-.org/openstack/devstack/src/commit/76d7d7c90c3979c72404fddd31ee884c8bfdb1ec
-/inc/meta-config#L82>`_ routine that doesn't working correctly on jobs based on
-the "devstack-minimal" profile.
-
-**Role Variables**
-
-.. zuul:rolevar:: devstack_base_dir
-   :default: /opt/stack
-
-   The devstack base directory.
-
-.. zuul:rolevar:: devstack_local_conf_path
-   :default: "{{ devstack_base_dir }}/devstack/local.conf"
-
-   Where to find the local.conf file
diff --git a/roles/set-tempest-config/defaults/main.yml b/roles/set-tempest-config/defaults/main.yml
deleted file mode 100644
index 5cc7ca6..0000000
--- a/roles/set-tempest-config/defaults/main.yml
+++ /dev/null
@@ -1,3 +0,0 @@
----
-devstack_base_dir: /opt/stack
-devstack_local_conf_path: "{{ devstack_base_dir }}/devstack/local.conf"
diff --git a/roles/set-tempest-config/tasks/main.yml b/roles/set-tempest-config/tasks/main.yml
deleted file mode 100644
index 3572ff5..0000000
--- a/roles/set-tempest-config/tasks/main.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-- name: Install required pip packages
-  pip:
-    name: devstack-tools
-    state: "latest"
-    virtualenv: /var/tmp/venv
-
-- name: Copy tempest config
-  shell: >-
-    . /var/tmp/venv/bin/activate && \
-    dsconf extract {{ devstack_local_conf_path }} \
-      test-config \
-      '$TEMPEST_CONFIG' \
-      {{ devstack_base_dir }}/tempest/etc/tempest.conf
-  become: yes
diff --git a/zuul.d/manila-tempest-jobs.yaml b/zuul.d/manila-tempest-jobs.yaml
index 20b0804..202e0f0 100644
--- a/zuul.d/manila-tempest-jobs.yaml
+++ b/zuul.d/manila-tempest-jobs.yaml
@@ -162,11 +162,12 @@
     name: manila-tempest-plugin-zfsonlinux
     description: Test ZFSOnLinux multibackend (DHSS=False) with postgresql db
     parent: manila-tempest-plugin-zfsonlinux-base
-    branches: &ubuntu_jammy_test_image_branches ^(?!stable/(yoga|xena|wallaby|victoria|ussuri)).*$
+    branches: &ubuntu_jammy_test_image_branches
+      regex: ^stable/(yoga|xena|wallaby|victoria|ussuri)$
+      negate: true
 
 - job:
     name: manila-tempest-plugin-lvm-base
-    nodeset: openstack-single-node-focal
     description: |
       Test LVM multibackend (DHSS=False) in a 4+6 (dual-stack) devstack
       environment with IPv6 control plane endpoints.
@@ -176,6 +177,7 @@
       - openstack/neutron-dynamic-routing
     vars:
       tempest_test_regex: '(^manila_tempest_tests.tests)(?=.*\[.*\bbackend\b.*\])'
+      tempest_exclude_regex: "(^manila_tempest_tests.tests.scenario.*)"
       devstack_services: &devstack-with-ovs
         # NOTE(gouthamr): LP#1940324 prevents bgp usage with OVN, disable OVN
         br-ex-tcpdump: false
@@ -230,13 +232,25 @@
               multi_backend: true
               image_password: manila
 
+# NOTE(carloss): Nova bumped libvirt to a version available only on Ubuntu
+# Jammy. We are then forced to migrate this job to use Jammy. When LP Bug
+#1998489 is fixed, we will be able to unify the job above with this.
 - job:
     name: manila-tempest-plugin-lvm
     description: |
-      Test LVM multibackend (DHSS=False) in a 4+6 (dual-stack) devstack
-      environment with IPv6 control plane endpoints.
-    branches: *ubuntu_jammy_test_image_branches
+      Test LVM multibackend (DHSS=False) in a IPv4 environment.
+    branches:
+      regex: ^stable/(2023.1|zed|yoga|xena|wallaby|victoria|ussuri)$
+      negate: true
     parent: manila-tempest-plugin-lvm-base
+    vars:
+      devstack_localrc:
+        MANILA_SETUP_IPV6: false
+      devstack_local_conf:
+        test-config:
+          $TEMPEST_CONFIG:
+            share:
+              run_ipv6_tests: false
 
 - job:
     name: manila-tempest-plugin-container
@@ -394,7 +408,6 @@
 
 - job:
     name: manila-tempest-plugin-cephfs-native
-    nodeset: openstack-single-node-focal
     description: Test CephFS Native (DHSS=False)
     parent: manila-tempest-plugin-cephfs-native-base
     branches: *ubuntu_jammy_test_image_branches
@@ -620,7 +633,6 @@
     description: |
       Test the GlusterFS driver (DHSS=False) with the native GlusterFS protocol
     parent: manila-tempest-plugin-standalone-base
-    nodeset: openstack-single-node-focal
     required-projects:
       - x/devstack-plugin-glusterfs
     vars:
@@ -653,7 +665,6 @@
     description: |
       Test the GlusterFS driver (DHSS=False) with the native NFS protocol
     parent: manila-tempest-plugin-standalone-base
-    nodeset: openstack-single-node-focal
     required-projects:
       - x/devstack-plugin-glusterfs
     vars:
@@ -724,7 +735,9 @@
 - job:
     name: manila-tempest-plugin-lvm-fips
     parent: manila-tempest-plugin-lvm-fips-base
-    branches: ^(?!stable/(yoga|xena|wallaby|victoria|ussuri)).*$
+    branches:
+      regex: ^stable/(yoga|xena|wallaby|victoria|ussuri)$
+      negate: true
 
 - project-template:
     name: manila-tempest-plugin-jobs-using-service-image
diff --git a/zuul.d/manila-tempest-stable-jobs.yaml b/zuul.d/manila-tempest-stable-jobs.yaml
index 28db229..0fb38ab 100644
--- a/zuul.d/manila-tempest-stable-jobs.yaml
+++ b/zuul.d/manila-tempest-stable-jobs.yaml
@@ -7,7 +7,7 @@
       Test the scenario test cases on the generic driver multibackend
       (DHSS=True) with NFS and CIFS
     parent: manila-tempest-plugin-generic-scenario-base
-    branches: &manila_tempest_image_pinned_branches ^(stable/(zed|yoga|xena)).*$
+    branches: &manila_tempest_image_pinned_branches ^stable/(2023.1|zed|yoga|xena)$
     vars: &manila_tempest_image_pinned_vars
       devstack_localrc:
         # NOTE(carloss): Pinning manila service image to a Focal version,
@@ -27,15 +27,12 @@
 
 - job:
     name: manila-tempest-plugin-lvm-stable
-    # NOTE(carloss): we are aware that focal is the current default, but
-    # in order to avoid breakages when devstack-minimal switches to a newer
-    # branch, we are pinning focal here.
-    nodeset: openstack-single-node-focal
     description: |
       Test LVM multibackend (DHSS=False) in a 4+6 (dual-stack) devstack
       environment with IPv6 control plane endpoints.
     branches: *manila_tempest_image_pinned_branches
     parent: manila-tempest-plugin-lvm-base
+    nodeset: openstack-single-node-focal
     vars: *manila_tempest_image_pinned_vars
 
 - job:
@@ -68,20 +65,18 @@
 - job:
     name: manila-tempest-plugin-lvm-fips-stable
     parent: manila-tempest-plugin-lvm-fips-base
-    branches: ^(stable/(yoga|xena)).*$
+    branches: ^stable/(yoga|xena)$
     vars: *manila_tempest_image_pinned_vars
 
 - job:
     name: manila-tempest-plugin-lvm-yoga
     parent: manila-tempest-plugin-lvm-base
-    nodeset: openstack-single-node-focal
     override-checkout: stable/yoga
     vars: *manila_tempest_image_pinned_vars
 
 - job:
     name: manila-tempest-plugin-lvm-xena
     parent: manila-tempest-plugin-lvm-base
-    nodeset: openstack-single-node-focal
     override-checkout: stable/xena
     vars: *manila_tempest_image_pinned_vars