Merge "Allow multiple instances on create_test_server"
diff --git a/releasenotes/notes/add-image-cache-apis-as-tempest-clients-fbcd186927a85e2f.yaml b/releasenotes/notes/add-image-cache-apis-as-tempest-clients-fbcd186927a85e2f.yaml
new file mode 100644
index 0000000..38cc9ac
--- /dev/null
+++ b/releasenotes/notes/add-image-cache-apis-as-tempest-clients-fbcd186927a85e2f.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    The following ``image_cache`` tempest client for glance v2 image
+    caching API is implemented in this release.
+
diff --git a/tempest/api/compute/admin/test_servers_on_multinodes.py b/tempest/api/compute/admin/test_servers_on_multinodes.py
index f440428..9082306 100644
--- a/tempest/api/compute/admin/test_servers_on_multinodes.py
+++ b/tempest/api/compute/admin/test_servers_on_multinodes.py
@@ -16,6 +16,7 @@
 
 from tempest.api.compute import base
 from tempest.common import compute
+from tempest.common import waiters
 from tempest import config
 from tempest.lib import decorators
 
@@ -125,3 +126,47 @@
         hostnames = list(hosts.values())
         self.assertEqual(hostnames[0], hostnames[1],
                          'Servers are on the different hosts: %s' % hosts)
+
+
+class UnshelveToHostMultiNodesTest(base.BaseV2ComputeAdminTest):
+    """Test to unshelve server in between hosts."""
+    min_microversion = '2.91'
+    max_microversion = 'latest'
+
+    @classmethod
+    def skip_checks(cls):
+        super(UnshelveToHostMultiNodesTest, cls).skip_checks()
+
+        if CONF.compute.min_compute_nodes < 2:
+            raise cls.skipException(
+                "Less than 2 compute nodes, skipping multi-nodes test.")
+
+    def _shelve_offload_then_unshelve_to_host(self, server, host):
+        compute.shelve_server(self.servers_client, server['id'],
+                              force_shelve_offload=True)
+
+        self.os_admin.servers_client.unshelve_server(
+            server['id'],
+            body={'unshelve': {'host': host}}
+            )
+        waiters.wait_for_server_status(self.servers_client, server['id'],
+                                       'ACTIVE')
+
+    @decorators.idempotent_id('b5cc0889-50c2-46a0-b8ff-b5fb4c3a6e20')
+    def test_unshelve_to_specific_host(self):
+        """Test unshelve to a specific host, new behavior introduced in
+        microversion 2.91.
+        1. Shelve offload server.
+        2. Request unshelve to original host and verify server land on it.
+        3. Shelve offload server again.
+        4. Request unshelve to the other host and verify server land on it.
+        """
+        server = self.create_test_server(wait_until='ACTIVE')
+        host = self.get_host_for_server(server['id'])
+        otherhost = self.get_host_other_than(server['id'])
+
+        self._shelve_offload_then_unshelve_to_host(server, host)
+        self.assertEqual(host, self.get_host_for_server(server['id']))
+
+        self._shelve_offload_then_unshelve_to_host(server, otherhost)
+        self.assertEqual(otherhost, self.get_host_for_server(server['id']))
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 0ed73a8..a69dbb3 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -43,7 +43,7 @@
         super(ServerActionsTestJSON, self).setUp()
         # Check if the server is in a clean state after test
         try:
-            validation_resources = self.get_class_validation_resources(
+            self.validation_resources = self.get_class_validation_resources(
                 self.os_primary)
             # _test_rebuild_server test compares ip address attached to the
             # server before and after the rebuild, in order to avoid
@@ -53,18 +53,18 @@
             waiters.wait_for_server_floating_ip(
                 self.client,
                 self.client.show_server(self.server_id)['server'],
-                validation_resources['floating_ip'])
+                self.validation_resources['floating_ip'])
             waiters.wait_for_server_status(self.client,
                                            self.server_id, 'ACTIVE')
         except lib_exc.NotFound:
             # The server was deleted by previous test, create a new one
             # Use class level validation resources to avoid them being
             # deleted once a test is over
-            validation_resources = self.get_class_validation_resources(
+            self.validation_resources = self.get_class_validation_resources(
                 self.os_primary)
             server = self.create_test_server(
                 validatable=True,
-                validation_resources=validation_resources,
+                validation_resources=self.validation_resources,
                 wait_until='SSHABLE')
             self.__class__.server_id = server['id']
         except Exception:
@@ -106,11 +106,9 @@
         """
         # Since this test messes with the password and makes the
         # server unreachable, it should create its own server
-        validation_resources = self.get_test_validation_resources(
-            self.os_primary)
         newserver = self.create_test_server(
             validatable=True,
-            validation_resources=validation_resources,
+            validation_resources=self.validation_resources,
             wait_until='ACTIVE')
         self.addCleanup(self.delete_server, newserver['id'])
         # The server's password should be set to the provided password
@@ -122,7 +120,7 @@
             # Verify that the user can authenticate with the new password
             server = self.client.show_server(newserver['id'])['server']
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server, validation_resources),
+                self.get_server_ip(server, self.validation_resources),
                 self.ssh_user,
                 new_password,
                 server=server,
@@ -131,15 +129,13 @@
 
     def _test_reboot_server(self, reboot_type):
         if CONF.validation.run_validation:
-            validation_resources = self.get_class_validation_resources(
-                self.os_primary)
             # Get the time the server was last rebooted,
             server = self.client.show_server(self.server_id)['server']
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server, validation_resources),
+                self.get_server_ip(server, self.validation_resources),
                 self.ssh_user,
                 self.password,
-                validation_resources['keypair']['private_key'],
+                self.validation_resources['keypair']['private_key'],
                 server=server,
                 servers_client=self.client)
             boot_time = linux_client.get_boot_time()
@@ -153,10 +149,10 @@
         if CONF.validation.run_validation:
             # Log in and verify the boot time has changed
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server, validation_resources),
+                self.get_server_ip(server, self.validation_resources),
                 self.ssh_user,
                 self.password,
-                validation_resources['keypair']['private_key'],
+                self.validation_resources['keypair']['private_key'],
                 server=server,
                 servers_client=self.client)
             new_boot_time = linux_client.get_boot_time()
@@ -185,10 +181,18 @@
         server = self.client.show_server(server['id'])['server']
         self.assertNotIn('security_groups', server)
 
-    def _rebuild_server_and_check(self, image_ref):
-        rebuilt_server = (self.client.rebuild_server(self.server_id, image_ref)
+    def _rebuild_server_and_check(self, image_ref, server):
+        rebuilt_server = (self.client.rebuild_server(server['id'], image_ref)
                           ['server'])
-        waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
+        if CONF.validation.run_validation:
+            tenant_network = self.get_tenant_network()
+            compute.wait_for_ssh_or_ping(
+                server, self.os_primary, tenant_network,
+                True, self.validation_resources, "SSHABLE", True)
+        else:
+            waiters.wait_for_server_status(self.client, self.server['id'],
+                                           'ACTIVE')
+
         msg = ('Server was not rebuilt to the original image. '
                'The original image: {0}. The current image: {1}'
                .format(image_ref, rebuilt_server['image']['id']))
@@ -212,7 +216,8 @@
         # If the server was rebuilt on a different image, restore it to the
         # original image once the test ends
         if self.image_ref_alt != self.image_ref:
-            self.addCleanup(self._rebuild_server_and_check, self.image_ref)
+            self.addCleanup(self._rebuild_server_and_check, self.image_ref,
+                            rebuilt_server)
 
         # Verify the properties in the initial response are correct
         self.assertEqual(self.server_id, rebuilt_server['id'])
@@ -230,8 +235,6 @@
         self.assertEqual(original_addresses, server['addresses'])
 
         if CONF.validation.run_validation:
-            validation_resources = self.get_class_validation_resources(
-                self.os_primary)
             # Authentication is attempted in the following order of priority:
             # 1.The key passed in, if one was passed in.
             # 2.Any key we can find through an SSH agent (if allowed).
@@ -239,10 +242,10 @@
             #   ~/.ssh/ (if allowed).
             # 4.Plain username/password auth, if a password was given.
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(rebuilt_server, validation_resources),
+                self.get_server_ip(rebuilt_server, self.validation_resources),
                 self.ssh_alt_user,
                 password,
-                validation_resources['keypair']['private_key'],
+                self.validation_resources['keypair']['private_key'],
                 server=rebuilt_server,
                 servers_client=self.client)
             linux_client.validate_authentication()
@@ -273,7 +276,7 @@
         # If the server was rebuilt on a different image, restore it to the
         # original image once the test ends
         if self.image_ref_alt != self.image_ref:
-            self.addCleanup(self._rebuild_server_and_check, old_image)
+            self.addCleanup(self._rebuild_server_and_check, old_image, server)
 
         # Verify the properties in the initial response are correct
         self.assertEqual(self.server_id, rebuilt_server['id'])
@@ -318,13 +321,11 @@
         self.assertEqual(self.server_id,
                          vol_after_rebuild['attachments'][0]['server_id'])
         if CONF.validation.run_validation:
-            validation_resources = self.get_class_validation_resources(
-                self.os_primary)
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server, validation_resources),
+                self.get_server_ip(server, self.validation_resources),
                 self.ssh_alt_user,
                 password=None,
-                pkey=validation_resources['keypair']['private_key'],
+                pkey=self.validation_resources['keypair']['private_key'],
                 server=server,
                 servers_client=self.client)
             linux_client.validate_authentication()
@@ -376,10 +377,8 @@
         kwargs = {'volume_backed': True,
                   'wait_until': 'ACTIVE'}
         if CONF.validation.run_validation:
-            validation_resources = self.get_test_validation_resources(
-                self.os_primary)
             kwargs.update({'validatable': True,
-                           'validation_resources': validation_resources})
+                           'validation_resources': self.validation_resources})
         server = self.create_test_server(**kwargs)
 
         # NOTE(mgoddard): Get detailed server to ensure addresses are present
@@ -395,10 +394,10 @@
             self.client.get_console_output(server['id'])
         if CONF.validation.run_validation:
             linux_client = remote_client.RemoteClient(
-                self.get_server_ip(server, validation_resources),
+                self.get_server_ip(server, self.validation_resources),
                 self.ssh_user,
                 password=None,
-                pkey=validation_resources['keypair']['private_key'],
+                pkey=self.validation_resources['keypair']['private_key'],
                 server=server,
                 servers_client=self.client)
             linux_client.validate_authentication()
diff --git a/tempest/api/image/v2/admin/test_image_caching.py b/tempest/api/image/v2/admin/test_image_caching.py
new file mode 100644
index 0000000..11dcc80
--- /dev/null
+++ b/tempest/api/image/v2/admin/test_image_caching.py
@@ -0,0 +1,153 @@
+# 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 io
+
+from oslo_log import log as logging
+from tempest.api.image import base
+from tempest.common import waiters
+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
+
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class ImageCachingTest(base.BaseV2ImageTest):
+    """Here we test the caching operations for image"""
+    credentials = ['primary', 'admin']
+
+    def setUp(self):
+        super(ImageCachingTest, self).setUp()
+        # NOTE(abhishekk): As caching is enabled instance boot or volume
+        # boot or image download can also cache image, so we are going to
+        # maintain our caching information to avoid disturbing other tests
+        self.cached_info = {}
+
+    def tearDown(self):
+        # Delete all from cache/queue if we exit abruptly
+        for image_id in self.cached_info:
+            self.os_admin.image_cache_client.cache_delete(
+                image_id)
+        super(ImageCachingTest, self).tearDown()
+
+    @classmethod
+    def skip_checks(cls):
+        super(ImageCachingTest, cls).skip_checks()
+        # Check to see if we should even be running these tests.
+        if not CONF.image.image_caching_enabled:
+            raise cls.skipException('Target system is not configured with '
+                                    'glance caching')
+
+    def image_create_and_upload(self, upload=True, **kwargs):
+        """Wrapper that returns a test image."""
+        if 'name' not in kwargs:
+            name = data_utils.rand_name(self.__name__ + "-image")
+            kwargs['name'] = name
+
+        params = dict(kwargs)
+        image = self.create_image(**params)
+        self.assertEqual('queued', image['status'])
+        if not upload:
+            return image
+
+        file_content = data_utils.random_bytes()
+        image_file = io.BytesIO(file_content)
+        self.client.store_image_file(image['id'], image_file)
+
+        image = self.client.show_image(image['id'])
+        return image
+
+    def _assertCheckQueues(self, queued_images):
+        for image in self.cached_info:
+            if self.cached_info[image] == 'queued':
+                self.assertIn(image, queued_images)
+
+    def _assertCheckCache(self, cached_images):
+        cached_list = []
+        for image in cached_images:
+            cached_list.append(image['image_id'])
+
+        for image in self.cached_info:
+            if self.cached_info[image] == 'cached':
+                self.assertIn(image, cached_list)
+
+    @decorators.idempotent_id('4bf6adba-2f9f-47e9-a6d5-37f21ad4387c')
+    def test_image_caching_cycle(self):
+        """Test image cache APIs"""
+        # Ensure that non-admin user is not allowed to perform caching
+        # operations
+        self.assertRaises(lib_exc.Forbidden,
+                          self.os_primary.image_cache_client.list_cache)
+
+        # Check there is nothing is queued for cached by us
+        output = self.os_admin.image_cache_client.list_cache()
+        self._assertCheckQueues(output['queued_images'])
+        self._assertCheckCache(output['cached_images'])
+
+        # Non-existing image should raise NotFound exception
+        self.assertRaises(lib_exc.NotFound,
+                          self.os_admin.image_cache_client.cache_queue,
+                          'non-existing-image-id')
+
+        # Verify that we can not use queued image for queueing
+        image = self.image_create_and_upload(name='queued', upload=False)
+        self.assertRaises(lib_exc.BadRequest,
+                          self.os_admin.image_cache_client.cache_queue,
+                          image['id'])
+
+        # Create one image
+        image = self.image_create_and_upload(name='first',
+                                             container_format='bare',
+                                             disk_format='raw',
+                                             visibility='private')
+        self.assertEqual('active', image['status'])
+
+        # Queue image for caching
+        self.os_admin.image_cache_client.cache_queue(image['id'])
+        self.cached_info[image['id']] = 'queued'
+        # Verify that we have 1 image for queueing and 0 for caching
+        output = self.os_admin.image_cache_client.list_cache()
+        self._assertCheckQueues(output['queued_images'])
+        self._assertCheckCache(output['cached_images'])
+
+        # Wait for image caching
+        LOG.info("Waiting for image %s to get cached", image['id'])
+        caching = waiters.wait_for_caching(
+            self.client,
+            self.os_admin.image_cache_client,
+            image['id'])
+
+        self.cached_info[image['id']] = 'cached'
+        # verify that we have image in cache and not in queued
+        self._assertCheckQueues(caching['queued_images'])
+        self._assertCheckCache(caching['cached_images'])
+
+        # Verify that we can delete images from caching and queueing with
+        # api call.
+        self.os_admin.image_cache_client.cache_clear()
+        output = self.os_admin.image_cache_client.list_cache()
+        self.assertEqual(0, len(output['queued_images']))
+        self.assertEqual(0, len(output['cached_images']))
+
+        # Verify that invalid header value for target returns 400 response
+        self.assertRaises(lib_exc.BadRequest,
+                          self.os_admin.image_cache_client.cache_clear,
+                          target="invalid")
+        # Remove all data from local information
+        self.cached_info = {}
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index d283ab3..7e647dd 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -49,12 +49,12 @@
             raise cls.skipException('Server does not support '
                                     'any import method')
 
-    def _create_image(self):
+    def _create_image(self, disk_format=None, container_format=None):
         # Create image
         uuid = '00000000-1111-2222-3333-444455556666'
         image_name = data_utils.rand_name('image')
-        container_format = CONF.image.container_formats[0]
-        disk_format = CONF.image.disk_formats[0]
+        container_format = container_format or CONF.image.container_formats[0]
+        disk_format = disk_format or CONF.image.disk_formats[0]
         image = self.create_image(name=image_name,
                                   container_format=container_format,
                                   disk_format=disk_format,
@@ -131,9 +131,144 @@
         # import image from web to backend
         image_uri = CONF.image.http_image
         self.client.image_import(image['id'], method='web-download',
-                                 image_uri=image_uri)
+                                 import_params={'uri': image_uri})
         waiters.wait_for_image_imported_to_stores(self.client, image['id'])
 
+    @decorators.idempotent_id('8876c818-c40e-4b90-9742-31d231616305')
+    def test_image_glance_download_import_success(self):
+        # We use glance-direct initially, then glance-download for test
+        self._require_import_method('glance-direct')
+        self._require_import_method('glance-download')
+
+        # Create an image via the normal import process to be our source
+        src = self._stage_and_check()
+        self.client.image_import(src, method='glance-direct')
+        waiters.wait_for_image_imported_to_stores(self.client, src)
+
+        # Add some properties to it that will be copied by the default
+        # config (and one that won't)
+        self.client.update_image(src, [
+            {'add': '/hw_cpu_cores', 'value': '5'},
+            {'add': '/trait:STORAGE_DISK_SSD', 'value': 'required'},
+            {'add': '/os_distro', 'value': 'rhel'},
+            {'add': '/speed', 'value': '88mph'},
+        ])
+
+        # Make sure our properties stuck on the source image
+        src_image = self.client.show_image(src)
+        self.assertEqual('5', src_image['hw_cpu_cores'])
+        self.assertEqual('required', src_image['trait:STORAGE_DISK_SSD'])
+        self.assertEqual('rhel', src_image['os_distro'])
+        self.assertEqual('88mph', src_image['speed'])
+
+        # Create a new image which we will fill from another glance image
+        dst = self._create_image(container_format='ovf',
+                                 disk_format='iso')['id']
+
+        # Set some values that will conflict to make sure we get the
+        # new ones and confirm they stuck before the import.
+        self.client.update_image(dst, [
+            {'add': '/hw_cpu_cores', 'value': '1'},
+            {'add': '/os_distro', 'value': 'windows'},
+        ])
+        dst_image = self.client.show_image(dst)
+        self.assertEqual('1', dst_image['hw_cpu_cores'])
+        self.assertEqual('windows', dst_image['os_distro'])
+
+        params = {
+            'glance_image_id': src,
+            'glance_region': self.client.region,
+            'glance_service_interface': 'public',
+        }
+        self.client.image_import(dst, method='glance-download',
+                                 import_params=params)
+        waiters.wait_for_image_tasks_status(self.client, dst, 'success')
+
+        # Make sure the new image has all the keys imported from the
+        # original image that we expect
+        dst_image = self.client.show_image(dst)
+        self.assertEqual(src_image['disk_format'], dst_image['disk_format'])
+        self.assertEqual(src_image['container_format'],
+                         dst_image['container_format'])
+        self.assertEqual('5', dst_image['hw_cpu_cores'])
+        self.assertEqual('required', dst_image['trait:STORAGE_DISK_SSD'])
+        self.assertEqual('rhel', dst_image['os_distro'])
+        self.assertNotIn('speed', dst_image)
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('36d4b546-64a2-4bb9-bdd0-ba676aa48f2c')
+    def test_image_glance_download_import_bad_uuid(self):
+        self._require_import_method('glance-download')
+        image_id = self._create_image()['id']
+        params = {
+            'glance_image_id': 'foo',
+            'glance_region': self.client.region,
+            'glance_service_interface': 'public',
+        }
+
+        # A non-UUID-like image id should make us fail immediately
+        e = self.assertRaises(lib_exc.BadRequest,
+                              self.client.image_import,
+                              image_id, method='glance-download',
+                              import_params=params)
+        self.assertIn('image id does not look like a UUID', str(e))
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('77644240-dbbe-4744-ae28-09b2ac12e218')
+    def test_image_glance_download_import_bad_endpoint(self):
+        self._require_import_method('glance-download')
+        image_id = self._create_image()['id']
+
+        # Set some properties before the import to make sure they are
+        # undisturbed
+        self.client.update_image(image_id, [
+            {'add': '/hw_cpu_cores', 'value': '1'},
+            {'add': '/os_distro', 'value': 'windows'},
+        ])
+        image = self.client.show_image(image_id)
+        self.assertEqual('1', image['hw_cpu_cores'])
+        self.assertEqual('windows', image['os_distro'])
+
+        params = {
+            'glance_image_id': '36d4b546-64a2-4bb9-bdd0-ba676aa48f2c',
+            'glance_region': 'not a region',
+            'glance_service_interface': 'not an interface',
+        }
+
+        # A bad region or interface will cause us to fail when we
+        # contact the remote glance.
+        self.client.image_import(image_id, method='glance-download',
+                                 import_params=params)
+        waiters.wait_for_image_tasks_status(self.client, image_id, 'failure')
+
+        # Make sure we reverted the image status to queued on failure, and that
+        # our extra properties are still in place.
+        image = self.client.show_image(image_id)
+        self.assertEqual('queued', image['status'])
+        self.assertEqual('1', image['hw_cpu_cores'])
+        self.assertEqual('windows', image['os_distro'])
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('c7edec8e-24b5-416a-9d42-b3e773bab62c')
+    def test_image_glance_download_import_bad_missing_image(self):
+        self._require_import_method('glance-download')
+        image_id = self._create_image()['id']
+        params = {
+            'glance_image_id': '36d4b546-64a2-4bb9-bdd0-ba676aa48f2c',
+            'glance_region': self.client.region,
+            'glance_service_interface': 'public',
+        }
+
+        # A non-existent image will cause us to fail when we
+        # contact the remote glance.
+        self.client.image_import(image_id, method='glance-download',
+                                 import_params=params)
+        waiters.wait_for_image_tasks_status(self.client, image_id, 'failure')
+
+        # Make sure we reverted the image status to queued on failure
+        image = self.client.show_image(image_id)
+        self.assertEqual('queued', image['status'])
+
     @decorators.idempotent_id('e04761a1-22af-42c2-b8bc-a34a3f12b585')
     def test_remote_import(self):
         """Test image import against a different worker than stage.
diff --git a/tempest/api/image/v2/test_images_negative.py b/tempest/api/image/v2/test_images_negative.py
index a3802a9..80c01a5 100644
--- a/tempest/api/image/v2/test_images_negative.py
+++ b/tempest/api/image/v2/test_images_negative.py
@@ -206,7 +206,7 @@
         # import image from web to backend
         image_uri = 'http://does-not.exist/no/possible/way'
         self.client.image_import(image['id'], method='web-download',
-                                 image_uri=image_uri,
+                                 import_params={'uri': image_uri},
                                  stores=[stores[0]['id']])
 
         start_time = int(time.time())
diff --git a/tempest/api/object_storage/base.py b/tempest/api/object_storage/base.py
index 8d8039b..7107dc4 100644
--- a/tempest/api/object_storage/base.py
+++ b/tempest/api/object_storage/base.py
@@ -16,6 +16,7 @@
 import time
 
 from tempest.common import custom_matchers
+from tempest.common import waiters
 from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import exceptions as lib_exc
@@ -124,6 +125,9 @@
                                                 object_name,
                                                 data,
                                                 metadata=metadata)
+                waiters.wait_for_object_create(cls.object_client,
+                                               container_name,
+                                               object_name)
                 return object_name, data
             # after bucket creation we might see Conflict
             except lib_exc.Conflict as e:
diff --git a/tempest/api/object_storage/test_container_quotas.py b/tempest/api/object_storage/test_container_quotas.py
index 7977a7a..fb67fb4 100644
--- a/tempest/api/object_storage/test_container_quotas.py
+++ b/tempest/api/object_storage/test_container_quotas.py
@@ -15,6 +15,7 @@
 
 from tempest.api.object_storage import base
 from tempest.common import utils
+from tempest.common import waiters
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
@@ -91,6 +92,9 @@
         for _ in range(QUOTA_COUNT):
             name = data_utils.rand_name(name="TestObject")
             self.object_client.create_object(self.container_name, name, "")
+            waiters.wait_for_object_create(self.object_client,
+                                           self.container_name,
+                                           name)
 
         nbefore = self._get_object_count()
         self.assertEqual(nbefore, QUOTA_COUNT)
diff --git a/tempest/api/object_storage/test_container_sync.py b/tempest/api/object_storage/test_container_sync.py
index 6b1f849..b31ff76 100644
--- a/tempest/api/object_storage/test_container_sync.py
+++ b/tempest/api/object_storage/test_container_sync.py
@@ -126,7 +126,7 @@
                 self.assertEqual(object_content, obj_name[::-1].encode())
 
     @decorators.attr(type='slow')
-    @decorators.unstable_test(bug='1317133')
+    @decorators.skip_because(bug='1317133')
     @decorators.idempotent_id('be008325-1bba-4925-b7dd-93b58f22ce9b')
     @testtools.skipIf(
         not CONF.object_storage_feature_enabled.container_sync,
diff --git a/tempest/api/volume/admin/test_encrypted_volumes_extend.py b/tempest/api/volume/admin/test_encrypted_volumes_extend.py
index 7339179..e85a00d 100644
--- a/tempest/api/volume/admin/test_encrypted_volumes_extend.py
+++ b/tempest/api/volume/admin/test_encrypted_volumes_extend.py
@@ -31,5 +31,18 @@
         "Attached encrypted volume extend is disabled.")
     @utils.services('compute')
     def test_extend_attached_encrypted_volume_luksv1(self):
+        """LUKs v1 decrypts and extends through libvirt."""
         volume = self.create_encrypted_volume(encryption_provider="luks")
         self._test_extend_attached_volume(volume)
+
+    @decorators.idempotent_id('381a2a3a-b2f4-4631-a910-720881f2cc2f')
+    @testtools.skipUnless(
+        CONF.volume_feature_enabled.extend_attached_encrypted_volume,
+        "Attached encrypted volume extend is disabled.")
+    @testtools.skipIf(CONF.volume.storage_protocol == 'ceph',
+                      'Ceph only supports LUKSv2 if doing host attach.')
+    @utils.services('compute')
+    def test_extend_attached_encrypted_volume_luksv2(self):
+        """LUKs v2 decrypts and extends through os-brick."""
+        volume = self.create_encrypted_volume(encryption_provider="luks2")
+        self._test_extend_attached_volume(volume)
diff --git a/tempest/api/volume/test_volume_transfers.py b/tempest/api/volume/test_volume_transfers.py
index f1dec06..62cb203 100644
--- a/tempest/api/volume/test_volume_transfers.py
+++ b/tempest/api/volume/test_volume_transfers.py
@@ -110,9 +110,7 @@
     """Test volume transfer for the "new" Transfers API mv 3.55"""
 
     volume_min_microversion = '3.55'
-    volume_max_microversion = 'latest'
-
-    credentials = ['primary', 'alt', 'admin']
+    volume_max_microversion = '3.56'
 
     @classmethod
     def setup_clients(cls):
@@ -131,3 +129,22 @@
         """Test create, list, delete with volume-transfers API mv 3.55"""
         super(VolumesTransfersV355Test, self). \
             test_create_list_delete_volume_transfer()
+
+
+class VolumesTransfersV357Test(VolumesTransfersV355Test):
+    """Test volume transfer for the "new" Transfers API mv 3.57"""
+
+    volume_min_microversion = '3.57'
+    volume_max_microversion = 'latest'
+
+    @decorators.idempotent_id('d746bd69-bb30-4414-9a1c-577959fac6a1')
+    def test_create_get_list_accept_volume_transfer(self):
+        """Test create, get, list, accept with volume-transfers API mv 3.57"""
+        super(VolumesTransfersV357Test, self). \
+            test_create_get_list_accept_volume_transfer()
+
+    @decorators.idempotent_id('d4b20ec2-e1bb-4068-adcf-6c20020a8e05')
+    def test_create_list_delete_volume_transfer(self):
+        """Test create, list, delete with volume-transfers API mv 3.57"""
+        super(VolumesTransfersV357Test, self). \
+            test_create_list_delete_volume_transfer()
diff --git a/tempest/api/volume/test_volumes_snapshots.py b/tempest/api/volume/test_volumes_snapshots.py
index a58da7e..b3a04f8 100644
--- a/tempest/api/volume/test_volumes_snapshots.py
+++ b/tempest/api/volume/test_volumes_snapshots.py
@@ -45,7 +45,7 @@
     def test_snapshot_create_delete_with_volume_in_use(self):
         """Test create/delete snapshot from volume attached to server"""
         # Create a test instance
-        server = self.create_server()
+        server = self.create_server(wait_until='SSHABLE')
         # NOTE(zhufl) Here we create volume from self.image_ref for adding
         # coverage for "creating snapshot from non-blank volume".
         volume = self.create_volume(imageRef=self.image_ref)
@@ -80,7 +80,7 @@
         snapshot1 = self.create_snapshot(self.volume_origin['id'])
 
         # Create a server and attach it
-        server = self.create_server()
+        server = self.create_server(wait_until='SSHABLE')
         self.attach_volume(server['id'], self.volume_origin['id'])
 
         # Now that the volume is attached, create other snapshots
diff --git a/tempest/clients.py b/tempest/clients.py
index 4c3d875..b7fa54a 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -87,6 +87,7 @@
             self.image_member_client = self.image_v1.ImageMembersClient()
             self.image_client_v2 = self.image_v2.ImagesClient()
             self.image_member_client_v2 = self.image_v2.ImageMembersClient()
+            self.image_cache_client = self.image_v2.ImageCacheClient()
             self.namespaces_client = self.image_v2.NamespacesClient()
             self.resource_types_client = self.image_v2.ResourceTypesClient()
             self.namespace_objects_client = \
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index ab401fb..f207066 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -594,3 +594,35 @@
         except lib_exc.SSHTimeout:
             pass
     raise lib_exc.TimeoutException()
+
+
+def wait_for_caching(client, cache_client, image_id):
+    """Waits until image is cached"""
+    start = int(time.time())
+    while int(time.time()) - start < client.build_timeout:
+        caching = cache_client.list_cache()
+        output = [image['image_id'] for image in caching['cached_images']]
+        if output and image_id in output:
+            return caching
+
+        time.sleep(client.build_interval)
+
+    message = ('Image %s failed to cache in time.' % image_id)
+    caller = test_utils.find_test_caller()
+    if caller:
+        message = '(%s) %s' % (caller, message)
+    raise lib_exc.TimeoutException(message)
+
+
+def wait_for_object_create(object_client, container_name, object_name,
+                           interval=1):
+    """Waits for created object to become available"""
+    start_time = time.time()
+    while time.time() - start_time < object_client.build_timeout:
+        try:
+            return object_client.get_object(container_name, object_name)
+        except lib_exc.NotFound:
+            time.sleep(interval)
+    message = ('Object %s failed to create within the required time (%s s).' %
+               (object_name, object_client.build_timeout))
+    raise lib_exc.TimeoutException(message)
diff --git a/tempest/config.py b/tempest/config.py
index f986ddb..39e7fb3 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -674,6 +674,11 @@
                         'publicURL', 'adminURL', 'internalURL'],
                help=("The endpoint type to use for the alternate image "
                      "service.")),
+    cfg.BoolOpt('image_caching_enabled',
+                default=False,
+                help=("Flag to enable if caching is enabled by image "
+                      "service, operator should set this parameter to True"
+                      "if 'image_cache_dir' is set in glance-api.conf")),
     cfg.StrOpt('http_image',
                default='http://download.cirros-cloud.net/0.3.1/'
                'cirros-0.3.1-x86_64-uec.tar.gz',
@@ -1143,6 +1148,9 @@
                help="One name of cluster which is set in the realm whose name "
                     "is set in 'realm_name' item in this file. Set the "
                     "same cluster name as Swift's container-sync-realms.conf"),
+    cfg.IntOpt('build_timeout',
+               default=10,
+               help="Timeout in seconds to wait for objects to create."),
 ]
 
 object_storage_feature_group = cfg.OptGroup(
diff --git a/tempest/lib/api_schema/response/volume/v3_55/__init__.py b/tempest/lib/api_schema/response/volume/v3_55/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/v3_55/__init__.py
diff --git a/tempest/lib/api_schema/response/volume/v3_55/transfers.py b/tempest/lib/api_schema/response/volume/v3_55/transfers.py
new file mode 100644
index 0000000..683c62f
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/v3_55/transfers.py
@@ -0,0 +1,46 @@
+# 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 copy
+
+from tempest.lib.api_schema.response.volume import transfers
+
+# Volume microversion 3.55:
+# Add 'no_snapshots' attribute in 'transfer' responses.
+
+create_volume_transfer = copy.deepcopy(transfers.create_volume_transfer)
+create_volume_transfer['response_body']['properties']['transfer'][
+    'properties'].update({'no_snapshots': {'type': 'boolean'}})
+
+common_show_volume_transfer = copy.deepcopy(
+    transfers.common_show_volume_transfer)
+common_show_volume_transfer['properties'].update(
+    {'no_snapshots': {'type': 'boolean'}})
+
+show_volume_transfer = copy.deepcopy(transfers.show_volume_transfer)
+show_volume_transfer['response_body']['properties'][
+    'transfer'] = common_show_volume_transfer
+
+list_volume_transfers_no_detail = copy.deepcopy(
+    transfers.list_volume_transfers_no_detail)
+
+list_volume_transfers_with_detail = copy.deepcopy(
+    transfers.list_volume_transfers_with_detail)
+list_volume_transfers_with_detail['response_body']['properties']['transfers'][
+    'items'] = common_show_volume_transfer
+
+delete_volume_transfer = copy.deepcopy(transfers.delete_volume_transfer)
+
+accept_volume_transfer = copy.deepcopy(transfers.accept_volume_transfer)
diff --git a/tempest/lib/api_schema/response/volume/v3_57/__init__.py b/tempest/lib/api_schema/response/volume/v3_57/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/v3_57/__init__.py
diff --git a/tempest/lib/api_schema/response/volume/v3_57/transfers.py b/tempest/lib/api_schema/response/volume/v3_57/transfers.py
new file mode 100644
index 0000000..2fcf0aa
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/v3_57/transfers.py
@@ -0,0 +1,61 @@
+# 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 copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+from tempest.lib.api_schema.response.volume.v3_55 import transfers
+
+# Volume microversion 3.57:
+# Add these attributes in 'transfer' responses.
+#   'destination_project_id'
+#   'source_project_id'
+#   'accepted'
+
+create_volume_transfer = copy.deepcopy(transfers.create_volume_transfer)
+create_volume_transfer['response_body']['properties']['transfer'][
+    'properties'].update(
+        {'destination_project_id': parameter_types.uuid_or_null})
+create_volume_transfer['response_body']['properties']['transfer'][
+    'properties'].update(
+        {'source_project_id': {'type': 'string', 'format': 'uuid'}})
+create_volume_transfer['response_body']['properties']['transfer'][
+    'properties'].update(
+        {'accepted': {'type': 'boolean'}})
+
+common_show_volume_transfer = copy.deepcopy(
+    transfers.common_show_volume_transfer)
+common_show_volume_transfer['properties'].update(
+    {'destination_project_id': parameter_types.uuid_or_null})
+common_show_volume_transfer['properties'].update(
+    {'source_project_id': {'type': 'string', 'format': 'uuid'}})
+common_show_volume_transfer['properties'].update(
+    {'accepted': {'type': 'boolean'}})
+
+show_volume_transfer = copy.deepcopy(transfers.show_volume_transfer)
+show_volume_transfer['response_body']['properties'][
+    'transfer'] = common_show_volume_transfer
+
+list_volume_transfers_no_detail = copy.deepcopy(
+    transfers.list_volume_transfers_no_detail)
+
+list_volume_transfers_with_detail = copy.deepcopy(
+    transfers.list_volume_transfers_with_detail)
+list_volume_transfers_with_detail['response_body']['properties']['transfers'][
+    'items'] = common_show_volume_transfer
+
+delete_volume_transfer = copy.deepcopy(transfers.delete_volume_transfer)
+
+accept_volume_transfer = copy.deepcopy(transfers.accept_volume_transfer)
diff --git a/tempest/lib/api_schema/response/volume/v3_65/__init__.py b/tempest/lib/api_schema/response/volume/v3_65/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/v3_65/__init__.py
diff --git a/tempest/lib/api_schema/response/volume/v3_65/volumes.py b/tempest/lib/api_schema/response/volume/v3_65/volumes.py
new file mode 100644
index 0000000..f7d9e1b
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/v3_65/volumes.py
@@ -0,0 +1,65 @@
+# 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 copy
+
+from tempest.lib.api_schema.response.volume.v3_64 import volumes
+
+# Volume microversion 3.65:
+# Add 'consumes_quota' attribute in volume details.
+
+common_show_volume = copy.deepcopy(volumes.common_show_volume)
+common_show_volume['properties'].update(
+    {'consumes_quota': {'type': 'boolean'}})
+
+create_volume = copy.deepcopy(volumes.create_volume)
+create_volume['response_body']['properties']['volume']['properties'].update(
+    {'consumes_quota': {'type': 'boolean'}})
+
+# copy unchanged volumes schema
+attachments = copy.deepcopy(volumes.attachments)
+list_volumes_no_detail = copy.deepcopy(volumes.list_volumes_no_detail)
+# show_volume refers to common_show_volume
+show_volume = copy.deepcopy(volumes.show_volume)
+show_volume['response_body']['properties']['volume'] = common_show_volume
+# list_volumes_detail refers to latest common_show_volume
+list_volumes_detail = copy.deepcopy(common_show_volume)
+list_volumes_with_detail = copy.deepcopy(volumes.list_volumes_with_detail)
+list_volumes_with_detail['response_body']['properties']['volumes']['items'] \
+    = list_volumes_detail
+update_volume = copy.deepcopy(volumes.update_volume)
+delete_volume = copy.deepcopy(volumes.delete_volume)
+show_volume_summary = copy.deepcopy(volumes.show_volume_summary)
+attach_volume = copy.deepcopy(volumes.attach_volume)
+set_bootable_volume = copy.deepcopy(volumes.set_bootable_volume)
+detach_volume = copy.deepcopy(volumes.detach_volume)
+reserve_volume = copy.deepcopy(volumes.reserve_volume)
+unreserve_volume = copy.deepcopy(volumes.unreserve_volume)
+extend_volume = copy.deepcopy(volumes.extend_volume)
+reset_volume_status = copy.deepcopy(volumes.reset_volume_status)
+update_volume_readonly = copy.deepcopy(volumes.update_volume_readonly)
+force_delete_volume = copy.deepcopy(volumes.force_delete_volume)
+retype_volume = copy.deepcopy(volumes.retype_volume)
+force_detach_volume = copy.deepcopy(volumes.force_detach_volume)
+create_volume_metadata = copy.deepcopy(volumes.create_volume_metadata)
+show_volume_metadata = copy.deepcopy(volumes.show_volume_metadata)
+update_volume_metadata = copy.deepcopy(volumes.update_volume_metadata)
+update_volume_metadata_item = copy.deepcopy(
+    volumes.update_volume_metadata_item)
+update_volume_image_metadata = copy.deepcopy(
+    volumes.update_volume_image_metadata)
+delete_volume_image_metadata = copy.deepcopy(
+    volumes.delete_volume_image_metadata)
+unmanage_volume = copy.deepcopy(volumes.unmanage_volume)
diff --git a/tempest/lib/api_schema/response/volume/v3_69/__init__.py b/tempest/lib/api_schema/response/volume/v3_69/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/v3_69/__init__.py
diff --git a/tempest/lib/api_schema/response/volume/v3_69/volumes.py b/tempest/lib/api_schema/response/volume/v3_69/volumes.py
new file mode 100644
index 0000000..e83ef46
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/v3_69/volumes.py
@@ -0,0 +1,65 @@
+# 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 copy
+
+from tempest.lib.api_schema.response.volume.v3_65 import volumes
+
+# Volume microversion 3.69:
+# The 'shared_targets' attribute is now a tristate boolean.
+
+common_show_volume = copy.deepcopy(volumes.common_show_volume)
+common_show_volume['properties'].update(
+    {'shared_targets': {'type': ['boolean', 'null']}})
+
+create_volume = copy.deepcopy(volumes.create_volume)
+create_volume['response_body']['properties']['volume']['properties'].update(
+    {'shared_targets': {'type': ['boolean', 'null']}})
+
+# copy unchanged volumes schema
+attachments = copy.deepcopy(volumes.attachments)
+list_volumes_no_detail = copy.deepcopy(volumes.list_volumes_no_detail)
+# show_volume refers to common_show_volume
+show_volume = copy.deepcopy(volumes.show_volume)
+show_volume['response_body']['properties']['volume'] = common_show_volume
+# list_volumes_detail refers to latest common_show_volume
+list_volumes_detail = copy.deepcopy(common_show_volume)
+list_volumes_with_detail = copy.deepcopy(volumes.list_volumes_with_detail)
+list_volumes_with_detail['response_body']['properties']['volumes']['items'] \
+    = list_volumes_detail
+update_volume = copy.deepcopy(volumes.update_volume)
+delete_volume = copy.deepcopy(volumes.delete_volume)
+show_volume_summary = copy.deepcopy(volumes.show_volume_summary)
+attach_volume = copy.deepcopy(volumes.attach_volume)
+set_bootable_volume = copy.deepcopy(volumes.set_bootable_volume)
+detach_volume = copy.deepcopy(volumes.detach_volume)
+reserve_volume = copy.deepcopy(volumes.reserve_volume)
+unreserve_volume = copy.deepcopy(volumes.unreserve_volume)
+extend_volume = copy.deepcopy(volumes.extend_volume)
+reset_volume_status = copy.deepcopy(volumes.reset_volume_status)
+update_volume_readonly = copy.deepcopy(volumes.update_volume_readonly)
+force_delete_volume = copy.deepcopy(volumes.force_delete_volume)
+retype_volume = copy.deepcopy(volumes.retype_volume)
+force_detach_volume = copy.deepcopy(volumes.force_detach_volume)
+create_volume_metadata = copy.deepcopy(volumes.create_volume_metadata)
+show_volume_metadata = copy.deepcopy(volumes.show_volume_metadata)
+update_volume_metadata = copy.deepcopy(volumes.update_volume_metadata)
+update_volume_metadata_item = copy.deepcopy(
+    volumes.update_volume_metadata_item)
+update_volume_image_metadata = copy.deepcopy(
+    volumes.update_volume_image_metadata)
+delete_volume_image_metadata = copy.deepcopy(
+    volumes.delete_volume_image_metadata)
+unmanage_volume = copy.deepcopy(volumes.unmanage_volume)
diff --git a/tempest/lib/common/dynamic_creds.py b/tempest/lib/common/dynamic_creds.py
index be8c0e8..d687eb5 100644
--- a/tempest/lib/common/dynamic_creds.py
+++ b/tempest/lib/common/dynamic_creds.py
@@ -559,23 +559,24 @@
             except lib_exc.NotFound:
                 LOG.warning("user with name: %s not found for delete",
                             creds.username)
-            # NOTE(zhufl): Only when neutron's security_group ext is
-            # enabled, cleanup_default_secgroup will not raise error. But
-            # here cannot use test_utils.is_extension_enabled for it will cause
-            # "circular dependency". So here just use try...except to
-            # ensure tenant deletion without big changes.
-            try:
-                if self.neutron_available:
-                    self.cleanup_default_secgroup(
-                        self.security_groups_admin_client, creds.tenant_id)
-            except lib_exc.NotFound:
-                LOG.warning("failed to cleanup tenant %s's secgroup",
-                            creds.tenant_name)
-            try:
-                self.creds_client.delete_project(creds.tenant_id)
-            except lib_exc.NotFound:
-                LOG.warning("tenant with name: %s not found for delete",
-                            creds.tenant_name)
+            if creds.tenant_id:
+                # NOTE(zhufl): Only when neutron's security_group ext is
+                # enabled, cleanup_default_secgroup will not raise error. But
+                # here cannot use test_utils.is_extension_enabled for it will
+                # cause "circular dependency". So here just use try...except to
+                # ensure tenant deletion without big changes.
+                try:
+                    if self.neutron_available:
+                        self.cleanup_default_secgroup(
+                            self.security_groups_admin_client, creds.tenant_id)
+                except lib_exc.NotFound:
+                    LOG.warning("failed to cleanup tenant %s's secgroup",
+                                creds.tenant_name)
+                try:
+                    self.creds_client.delete_project(creds.tenant_id)
+                except lib_exc.NotFound:
+                    LOG.warning("tenant with name: %s not found for delete",
+                                creds.tenant_name)
 
             # if cred is domain scoped, delete ephemeral domain
             # do not delete default domain
diff --git a/tempest/lib/services/image/v2/__init__.py b/tempest/lib/services/image/v2/__init__.py
index 99a5321..a2f5bdc 100644
--- a/tempest/lib/services/image/v2/__init__.py
+++ b/tempest/lib/services/image/v2/__init__.py
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+from tempest.lib.services.image.v2.image_cache_client import \
+    ImageCacheClient
 from tempest.lib.services.image.v2.image_members_client import \
     ImageMembersClient
 from tempest.lib.services.image.v2.images_client import ImagesClient
@@ -27,7 +29,7 @@
 from tempest.lib.services.image.v2.schemas_client import SchemasClient
 from tempest.lib.services.image.v2.versions_client import VersionsClient
 
-__all__ = ['ImageMembersClient', 'ImagesClient', 'NamespaceObjectsClient',
-           'NamespacePropertiesClient', 'NamespaceTagsClient',
-           'NamespacesClient', 'ResourceTypesClient', 'SchemasClient',
-           'VersionsClient']
+__all__ = ['ImageMembersClient', 'ImagesClient', 'ImageCacheClient',
+           'NamespaceObjectsClient', 'NamespacePropertiesClient',
+           'NamespaceTagsClient', 'NamespacesClient', 'ResourceTypesClient',
+           'SchemasClient', 'VersionsClient']
diff --git a/tempest/lib/services/image/v2/image_cache_client.py b/tempest/lib/services/image/v2/image_cache_client.py
new file mode 100644
index 0000000..90ff776
--- /dev/null
+++ b/tempest/lib/services/image/v2/image_cache_client.py
@@ -0,0 +1,74 @@
+# 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.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class ImageCacheClient(rest_client.RestClient):
+    api_version = "v2"
+
+    def list_cache(self):
+        """Lists all images in cache or queue. (Since Image API v2.14)
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://docs.openstack.org/api-ref/image/v2/?expanded=query-cache-status-detail#cache-manage
+        """
+        url = 'cache'
+        resp, body = self.get(url)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def cache_queue(self, image_id):
+        """Queues image for caching. (Since Image API v2.14)
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://docs.openstack.org/api-ref/image/v2/?expanded=queue-image-detail#queue-image
+        """
+        url = 'cache/%s' % image_id
+        resp, body = self.put(url, body=None)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body=body)
+
+    def cache_delete(self, image_id):
+        """Deletes a image from cache. (Since Image API v2.14)
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://docs.openstack.org/api-ref/image/v2/?expanded=delete-image-from-cache-detail#delete-image-from-cache
+        """
+        url = 'cache/%s' % image_id
+        resp, _ = self.delete(url)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp)
+
+    def cache_clear(self, target=None):
+        """Clears the cache and its queue. (Since Image API v2.14)
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://docs.openstack.org/api-ref/image/v2/?expanded=clear-images-from-cache-detail#delete-image-from-cache
+        """
+        url = 'cache'
+        headers = {}
+        if target:
+            headers['x-image-cache-clear-target'] = target
+        resp, _ = self.delete(url, headers=headers)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/image/v2/images_client.py b/tempest/lib/services/image/v2/images_client.py
index abf427c..ae6ce25 100644
--- a/tempest/lib/services/image/v2/images_client.py
+++ b/tempest/lib/services/image/v2/images_client.py
@@ -206,7 +206,7 @@
 
     def image_import(self, image_id, method='glance-direct',
                      all_stores_must_succeed=None, all_stores=True,
-                     stores=None, image_uri=None):
+                     stores=None, import_params=None):
         """Import data from staging area to glance store.
 
         For a full list of available parameters, please refer to the official
@@ -222,9 +222,11 @@
                            all available stores (incompatible with stores)
         :param stores: A list of destination store names for the import. Must
                        be None if server does not support multistore.
-        :param image_uri: A URL to be used with the web-download method
+        :param import_params: A dict of import method parameters
         """
         url = 'images/%s/import' % image_id
+        if import_params is None:
+            import_params = {}
         data = {
             "method": {
                 "name": method
@@ -237,8 +239,8 @@
 
         if all_stores_must_succeed is not None:
             data['all_stores_must_succeed'] = all_stores_must_succeed
-        if image_uri:
-            data['method']['uri'] = image_uri
+        if import_params:
+            data['method'].update(import_params)
         data = json.dumps(data)
         headers = {'Content-Type': 'application/json'}
         resp, _ = self.post(url, data, headers=headers)
diff --git a/tempest/lib/services/volume/v3/transfers_client.py b/tempest/lib/services/volume/v3/transfers_client.py
index cc4e1b2..f85bf21 100644
--- a/tempest/lib/services/volume/v3/transfers_client.py
+++ b/tempest/lib/services/volume/v3/transfers_client.py
@@ -18,12 +18,23 @@
 from oslo_serialization import jsonutils as json
 
 from tempest.lib.api_schema.response.volume import transfers as schema
+from tempest.lib.api_schema.response.volume.v3_55 \
+    import transfers as schemav355
+from tempest.lib.api_schema.response.volume.v3_57 \
+    import transfers as schemav357
 from tempest.lib.common import rest_client
+from tempest.lib.services.volume import base_client
 
 
-class TransfersClient(rest_client.RestClient):
+class TransfersClient(base_client.BaseClient):
     """Client class to send CRUD Volume Transfer API requests"""
 
+    schema_versions_info = [
+        {'min': None, 'max': '3.54', 'schema': schema},
+        {'min': '3.55', 'max': '3.56', 'schema': schemav355},
+        {'min': '3.57', 'max': None, 'schema': schemav357}
+    ]
+
     resource_path = 'os-volume-transfer'
 
     def create_volume_transfer(self, **kwargs):
@@ -36,6 +47,7 @@
         post_body = json.dumps({'transfer': kwargs})
         resp, body = self.post(self.resource_path, post_body)
         body = json.loads(body)
+        schema = self.get_schema(self.schema_versions_info)
         self.validate_response(schema.create_volume_transfer, resp, body)
         return rest_client.ResponseBody(resp, body)
 
@@ -44,6 +56,7 @@
         url = "%s/%s" % (self.resource_path, transfer_id)
         resp, body = self.get(url)
         body = json.loads(body)
+        schema = self.get_schema(self.schema_versions_info)
         self.validate_response(schema.show_volume_transfer, resp, body)
         return rest_client.ResponseBody(resp, body)
 
@@ -56,6 +69,7 @@
         https://docs.openstack.org/api-ref/block-storage/v3/index.html#list-volume-transfers-and-details
         """
         url = self.resource_path
+        schema = self.get_schema(self.schema_versions_info)
         schema_list_transfers = schema.list_volume_transfers_no_detail
         if detail:
             url += '/detail'
@@ -70,6 +84,7 @@
     def delete_volume_transfer(self, transfer_id):
         """Delete a volume transfer."""
         resp, body = self.delete("%s/%s" % (self.resource_path, transfer_id))
+        schema = self.get_schema(self.schema_versions_info)
         self.validate_response(schema.delete_volume_transfer, resp, body)
         return rest_client.ResponseBody(resp, body)
 
@@ -84,6 +99,7 @@
         post_body = json.dumps({'accept': kwargs})
         resp, body = self.post(url, post_body)
         body = json.loads(body)
+        schema = self.get_schema(self.schema_versions_info)
         self.validate_response(schema.accept_volume_transfer, resp, body)
         return rest_client.ResponseBody(resp, body)
 
diff --git a/tempest/lib/services/volume/v3/volumes_client.py b/tempest/lib/services/volume/v3/volumes_client.py
index 9934e47..ad8bd71 100644
--- a/tempest/lib/services/volume/v3/volumes_client.py
+++ b/tempest/lib/services/volume/v3/volumes_client.py
@@ -20,6 +20,8 @@
 from tempest.lib.api_schema.response.volume.v3_61 import volumes as schemav361
 from tempest.lib.api_schema.response.volume.v3_63 import volumes as schemav363
 from tempest.lib.api_schema.response.volume.v3_64 import volumes as schemav364
+from tempest.lib.api_schema.response.volume.v3_65 import volumes as schemav365
+from tempest.lib.api_schema.response.volume.v3_69 import volumes as schemav369
 from tempest.lib.api_schema.response.volume import volumes as schema
 from tempest.lib.common import rest_client
 from tempest.lib import exceptions as lib_exc
@@ -33,7 +35,9 @@
         {'min': None, 'max': '3.60', 'schema': schema},
         {'min': '3.61', 'max': '3.62', 'schema': schemav361},
         {'min': '3.63', 'max': '3.63', 'schema': schemav363},
-        {'min': '3.64', 'max': None, 'schema': schemav364}
+        {'min': '3.64', 'max': '3.64', 'schema': schemav364},
+        {'min': '3.65', 'max': '3.68', 'schema': schemav365},
+        {'min': '3.69', 'max': None, 'schema': schemav369}
         ]
 
     def _prepare_params(self, params):
diff --git a/tempest/scenario/test_encrypted_cinder_volumes.py b/tempest/scenario/test_encrypted_cinder_volumes.py
index 6ee9f28..9788e19 100644
--- a/tempest/scenario/test_encrypted_cinder_volumes.py
+++ b/tempest/scenario/test_encrypted_cinder_volumes.py
@@ -13,6 +13,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import testtools
+
 from tempest.common import utils
 from tempest import config
 from tempest.lib import decorators
@@ -27,7 +29,7 @@
 
     This test is for verifying the functionality of encrypted cinder volumes.
 
-    For both LUKS and cryptsetup encryption types, this test performs
+    For both LUKS (v1 & v2) and cryptsetup encryption types, this test performs
     the following:
 
     * Boots an instance from an image (CONF.compute.image_ref)
@@ -55,11 +57,24 @@
     @decorators.attr(type='slow')
     @utils.services('compute', 'volume', 'image')
     def test_encrypted_cinder_volumes_luks(self):
+        """LUKs v1 decrypts volume through libvirt."""
         server = self.launch_instance()
         volume = self.create_encrypted_volume('luks',
                                               volume_type='luks')
         self.attach_detach_volume(server, volume)
 
+    @decorators.idempotent_id('7abec0a3-61a0-42a5-9e36-ad3138fb38b4')
+    @testtools.skipIf(CONF.volume.storage_protocol == 'ceph',
+                      'Ceph only supports LUKSv2 if doing host attach.')
+    @decorators.attr(type='slow')
+    @utils.services('compute', 'volume', 'image')
+    def test_encrypted_cinder_volumes_luksv2(self):
+        """LUKs v2 decrypts volume through os-brick."""
+        server = self.launch_instance()
+        volume = self.create_encrypted_volume('luks2',
+                                              volume_type='luksv2')
+        self.attach_detach_volume(server, volume)
+
     @decorators.idempotent_id('cbc752ed-b716-4717-910f-956cce965722')
     @decorators.attr(type='slow')
     @utils.services('compute', 'volume', 'image')
diff --git a/tempest/scenario/test_minimum_basic.py b/tempest/scenario/test_minimum_basic.py
index 8cafd1f..90e1bc5 100644
--- a/tempest/scenario/test_minimum_basic.py
+++ b/tempest/scenario/test_minimum_basic.py
@@ -86,20 +86,6 @@
                    '%s' % (secgroup['id'], server['id']))
             raise exceptions.TimeoutException(msg)
 
-    def _get_floating_ip_in_server_addresses(self, floating_ip, server):
-        for addresses in server['addresses'].values():
-            for address in addresses:
-                if (address['OS-EXT-IPS:type'] == 'floating' and
-                        address['addr'] == floating_ip['floating_ip_address']):
-                    return address
-
-    def _is_floating_ip_detached_from_server(self, server, floating_ip):
-        server_info = self.servers_client.show_server(
-            server['id'])['server']
-        address = self._get_floating_ip_in_server_addresses(
-            floating_ip, server_info)
-        return (not address)
-
     @decorators.idempotent_id('bdbb5441-9204-419d-a225-b4fdbfb1a1a8')
     @utils.services('compute', 'volume', 'image', 'network')
     def test_minimum_basic_scenario(self):
@@ -173,15 +159,6 @@
                 self.servers_client, server, floating_ip,
                 wait_for_disassociate=True)
 
-            if not test_utils.call_until_true(
-                    self._is_floating_ip_detached_from_server,
-                    CONF.compute.build_timeout,
-                    CONF.compute.build_interval, server, floating_ip):
-                msg = ("Floating IP '%s' should not be in server addresses: %s"
-                       % (floating_ip['floating_ip_address'],
-                          server['addresses']))
-                raise exceptions.TimeoutException(msg)
-
     @decorators.idempotent_id('a8fd48ec-1d01-4895-b932-02321661ec1e')
     @testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
                           "Cinder volume snapshots are disabled")
@@ -232,15 +209,8 @@
             fip = self.create_floating_ip(server)
             floating_ip = self.associate_floating_ip(
                 fip, server)
-            # fetch the server again to make sure the addresses were refreshed
-            # after associating the floating IP
-            server = self.servers_client.show_server(server['id'])['server']
-            address = self._get_floating_ip_in_server_addresses(
-                floating_ip, server)
-            self.assertIsNotNone(
-                address,
-                "Failed to find floating IP '%s' in server addresses: %s" %
-                (floating_ip['floating_ip_address'], server['addresses']))
+            waiters.wait_for_server_floating_ip(self.servers_client, server,
+                                                floating_ip)
             ssh_ip = floating_ip['floating_ip_address']
         else:
             ssh_ip = self.get_server_ip(server)
@@ -276,12 +246,3 @@
             waiters.wait_for_server_floating_ip(
                 self.servers_client, server, floating_ip,
                 wait_for_disassociate=True)
-
-            if not test_utils.call_until_true(
-                self._is_floating_ip_detached_from_server,
-                    CONF.compute.build_timeout, CONF.compute.build_interval,
-                    server, floating_ip):
-                msg = ("Floating IP '%s' should not be in server addresses: %s"
-                       % (floating_ip['floating_ip_address'],
-                          server['addresses']))
-                raise exceptions.TimeoutException(msg)
diff --git a/tempest/scenario/test_volume_boot_pattern.py b/tempest/scenario/test_volume_boot_pattern.py
index 5a5cc27..2e87c15 100644
--- a/tempest/scenario/test_volume_boot_pattern.py
+++ b/tempest/scenario/test_volume_boot_pattern.py
@@ -246,14 +246,10 @@
         # Assert that the underlying volume is gone.
         self.volumes_client.wait_for_resource_deletion(volume_origin['id'])
 
-    @decorators.idempotent_id('cb78919a-e553-4bab-b73b-10cf4d2eb125')
-    @testtools.skipUnless(CONF.compute_feature_enabled.attach_encrypted_volume,
-                          'Encrypted volume attach is not supported')
-    @utils.services('compute', 'volume')
-    def test_boot_server_from_encrypted_volume_luks(self):
+    def _do_test_boot_server_from_encrypted_volume_luks(self, provider):
         # Create an encrypted volume
-        volume = self.create_encrypted_volume('luks',
-                                              volume_type='luks')
+        volume = self.create_encrypted_volume(provider,
+                                              volume_type=provider)
 
         self.volumes_client.set_bootable_volume(volume['id'], bootable=True)
 
@@ -266,3 +262,21 @@
         server_info = self.servers_client.show_server(server['id'])['server']
         created_volume = server_info['os-extended-volumes:volumes_attached']
         self.assertEqual(volume['id'], created_volume[0]['id'])
+
+    @decorators.idempotent_id('cb78919a-e553-4bab-b73b-10cf4d2eb125')
+    @testtools.skipUnless(CONF.compute_feature_enabled.attach_encrypted_volume,
+                          'Encrypted volume attach is not supported')
+    @utils.services('compute', 'volume')
+    def test_boot_server_from_encrypted_volume_luks(self):
+        """LUKs v1 decrypts volume through libvirt."""
+        self._do_test_boot_server_from_encrypted_volume_luks('luks')
+
+    @decorators.idempotent_id('5ab6100f-1b31-4dd0-a774-68cfd837ef77')
+    @testtools.skipIf(CONF.volume.storage_protocol == 'ceph',
+                      'Ceph only supports LUKSv2 if doing host attach.')
+    @testtools.skipUnless(CONF.compute_feature_enabled.attach_encrypted_volume,
+                          'Encrypted volume attach is not supported')
+    @utils.services('compute', 'volume')
+    def test_boot_server_from_encrypted_volume_luksv2(self):
+        """LUKs v2 decrypts volume through os-brick."""
+        self._do_test_boot_server_from_encrypted_volume_luks('luks2')
diff --git a/tempest/tests/lib/services/image/v2/test_image_cache_client.py b/tempest/tests/lib/services/image/v2/test_image_cache_client.py
new file mode 100644
index 0000000..1a99115
--- /dev/null
+++ b/tempest/tests/lib/services/image/v2/test_image_cache_client.py
@@ -0,0 +1,64 @@
+# 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.
+
+from tempest.lib.services.image.v2 import image_cache_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestImageCacheClient(base.BaseServiceTest):
+    def setUp(self):
+        super(TestImageCacheClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = image_cache_client.ImageCacheClient(
+            fake_auth, 'image', 'regionOne')
+
+    def test_list_cache(self):
+        fake_result = {
+            "cached_images": [{
+                "image_id": "8f332e84-ea60-4501-8e11-5efcddb81f30",
+                "hits": 3,
+                "last_accessed": 1639578364.65118,
+                "last_modified": 1639389612.596718,
+                "size": 16300544
+            }],
+            "queued_images": ['1bea47ed-f6a9-463b-b423-14b9cca9ad27']}
+        self.check_service_client_function(
+            self.client.list_cache,
+            'tempest.lib.common.rest_client.RestClient.get',
+            fake_result,
+            mock_args=['cache'])
+
+    def test_cache_queue(self):
+        self.check_service_client_function(
+            self.client.cache_queue,
+            'tempest.lib.common.rest_client.RestClient.put',
+            {},
+            status=202,
+            image_id="e485aab9-0907-4973-921c-bb6da8a8fcf8")
+
+    def test_cache_delete(self):
+        fake_result = {}
+        self.check_service_client_function(
+            self.client.cache_delete,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            fake_result, image_id="e485aab9-0907-4973-921c-bb6da8a8fcf8",
+            status=204)
+
+    def test_cache_clear_without_target(self):
+        fake_result = {}
+        self.check_service_client_function(
+            self.client.cache_clear,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            fake_result, status=204)
diff --git a/tools/tempest-integrated-gate-networking-exclude-list.txt b/tools/tempest-integrated-gate-networking-exclude-list.txt
index 263b2e4..9d79a35 100644
--- a/tools/tempest-integrated-gate-networking-exclude-list.txt
+++ b/tools/tempest-integrated-gate-networking-exclude-list.txt
@@ -11,9 +11,11 @@
 
 # Skip Cinder, Glance and Swift only scenario tests.
 tempest.scenario.test_encrypted_cinder_volumes.TestEncryptedCinderVolumes.test_encrypted_cinder_volumes_luks
+tempest.scenario.test_encrypted_cinder_volumes.TestEncryptedCinderVolumes.test_encrypted_cinder_volumes_luks2
 tempest.scenario.test_encrypted_cinder_volumes.TestEncryptedCinderVolumes.test_encrypted_cinder_volumes_cryptsetup
 tempest.scenario.test_object_storage_basic_ops.TestObjectStorageBasicOps.test_swift_basic_ops
 tempest.scenario.test_object_storage_basic_ops.TestObjectStorageBasicOps.test_swift_acl_anonymous_download
 tempest.scenario.test_volume_boot_pattern.TestVolumeBootPattern.test_boot_server_from_encrypted_volume_luks
+tempest.scenario.test_volume_boot_pattern.TestVolumeBootPattern.test_boot_server_from_encrypted_volume_luks2
 tempest.scenario.test_volume_boot_pattern.TestVolumeBootPattern.test_image_defined_boot_from_volume
 tempest.scenario.test_volume_boot_pattern.TestVolumeBootPattern.test_create_server_from_volume_snapshot
diff --git a/tools/tempest-integrated-gate-placement-exclude-list.txt b/tools/tempest-integrated-gate-placement-exclude-list.txt
index efba796..eb68b32 100644
--- a/tools/tempest-integrated-gate-placement-exclude-list.txt
+++ b/tools/tempest-integrated-gate-placement-exclude-list.txt
@@ -11,9 +11,11 @@
 
 # Skip Cinder, Glance and Swift only scenario tests.
 tempest.scenario.test_encrypted_cinder_volumes.TestEncryptedCinderVolumes.test_encrypted_cinder_volumes_luks
+tempest.scenario.test_encrypted_cinder_volumes.TestEncryptedCinderVolumes.test_encrypted_cinder_volumes_luks2
 tempest.scenario.test_encrypted_cinder_volumes.TestEncryptedCinderVolumes.test_encrypted_cinder_volumes_cryptsetup
 tempest.scenario.test_object_storage_basic_ops.TestObjectStorageBasicOps.test_swift_basic_ops
 tempest.scenario.test_object_storage_basic_ops.TestObjectStorageBasicOps.test_swift_acl_anonymous_download
 tempest.scenario.test_volume_boot_pattern.TestVolumeBootPattern.test_boot_server_from_encrypted_volume_luks
+tempest.scenario.test_volume_boot_pattern.TestVolumeBootPattern.test_boot_server_from_encrypted_volume_luks2
 tempest.scenario.test_volume_boot_pattern.TestVolumeBootPattern.test_image_defined_boot_from_volume
 tempest.scenario.test_volume_boot_pattern.TestVolumeBootPattern.test_create_server_from_volume_snapshot
diff --git a/zuul.d/integrated-gate.yaml b/zuul.d/integrated-gate.yaml
index 7535ccc..4c08ad9 100644
--- a/zuul.d/integrated-gate.yaml
+++ b/zuul.d/integrated-gate.yaml
@@ -319,11 +319,19 @@
     vars:
       tox_envlist: full
       configure_swap_size: 4096
-      devstack_local_conf:
-        test-config:
-          "$TEMPEST_CONFIG":
-            validation:
-              ssh_key_type: 'ecdsa'
+      nslookup_target: 'opendev.org'
+
+- job:
+    name: tempest-centos9-stream-fips
+    parent: devstack-tempest
+    description: |
+      Integration testing for a FIPS enabled Centos 9 system
+    nodeset: devstack-single-node-centos-9-stream
+    pre-run: playbooks/enable-fips.yaml
+    vars:
+      tox_envlist: full
+      configure_swap_size: 4096
+      nslookup_target: 'opendev.org'
 
 - job:
     name: tempest-pg-full
@@ -383,9 +391,6 @@
         # centos-8-stream is tested from wallaby -> yoga branches
         - tempest-integrated-compute-centos-8-stream:
             branches: ^stable/(wallaby|xena|yoga).*$
-        # centos-9-stream is tested from zed release onwards
-        - tempest-integrated-compute-centos-9-stream:
-            branches: ^(?!stable/(pike|queens|rocky|stein|train|ussuri|victoria|wallaby|xena|yoga)).*$
         # Do not run it on ussuri until below issue is fixed
         # https://storyboard.openstack.org/#!/story/2010057
         - openstacksdk-functional-devstack:
@@ -393,11 +398,13 @@
     gate:
       jobs:
         - tempest-integrated-compute
-        - tempest-integrated-compute-centos-9-stream
-        # Do not run it on ussuri until below issue is fixed
-        # https://storyboard.openstack.org/#!/story/2010057
         - openstacksdk-functional-devstack:
             branches: ^(?!stable/ussuri).*$
+    periodic-weekly:
+      jobs:
+        # centos-9-stream is tested from zed release onwards
+        - tempest-integrated-compute-centos-9-stream:
+            branches: ^(?!stable/(pike|queens|rocky|stein|train|ussuri|victoria|wallaby|xena|yoga)).*$
 
 - project-template:
     name: integrated-gate-placement
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 1f93903..3cc3fda 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -164,7 +164,7 @@
             irrelevant-files: *tempest-irrelevant-files
         - tempest-pg-full:
             irrelevant-files: *tempest-irrelevant-files
-        - tempest-centos8-stream-fips:
+        - tempest-centos9-stream-fips:
             irrelevant-files: *tempest-irrelevant-files
     periodic-stable:
       jobs:
@@ -179,3 +179,4 @@
         - tempest-all
         - tempest-full-oslo-master
         - tempest-stestr-master
+        - tempest-centos9-stream-fips