Merge "workaround: Wait for instance.host=None when shelving"
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index 20ace9e..33e75ff 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -454,6 +454,10 @@
 
   .. _2.86: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id79
 
+  * `2.96`_
+
+  .. _2.96: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-2024-1-caracal-and-2024-2-dalmatian
+
 * Volume
 
   * `3.3`_
diff --git a/doc/source/supported_version.rst b/doc/source/supported_version.rst
index c4631d8..0adfebd 100644
--- a/doc/source/supported_version.rst
+++ b/doc/source/supported_version.rst
@@ -12,7 +12,6 @@
 * 2024.2
 * 2024.1
 * 2023.2
-* 2023.1
 
 For older OpenStack Release:
 
@@ -33,7 +32,7 @@
 
 Tempest master supports the below python versions:
 
-* Python 3.8
 * Python 3.9
 * Python 3.10
 * Python 3.11
+* Python 3.12
diff --git a/releasenotes/notes/change-volume-catalog_type-default-fbcb2be6ebc42818.yaml b/releasenotes/notes/change-volume-catalog_type-default-fbcb2be6ebc42818.yaml
new file mode 100644
index 0000000..a507bd7
--- /dev/null
+++ b/releasenotes/notes/change-volume-catalog_type-default-fbcb2be6ebc42818.yaml
@@ -0,0 +1,6 @@
+---
+upgrade:
+  - |
+    The default for ``[volume] catalog_type``, which is used to determine the
+    service type to use to identify the block storage service in the service
+    catalog, has changed from ``volumev3`` to ``block-storage``.
diff --git a/releasenotes/notes/drop-python38-support-c0a696af00110602.yaml b/releasenotes/notes/drop-python38-support-c0a696af00110602.yaml
new file mode 100644
index 0000000..035f628
--- /dev/null
+++ b/releasenotes/notes/drop-python38-support-c0a696af00110602.yaml
@@ -0,0 +1,8 @@
+---
+prelude: >
+    Tempest dropped the Python 3.8 support.
+upgrade:
+  - |
+    Python 3.8 support has been dropped. Last release of Tempest
+    to support python 3.8 is Temepst 41.0.0. The minimum version
+    of Python now supported by Tempest is Python 3.9.
diff --git a/releasenotes/notes/end-of-support-of-2023-1-ddec1dac59700063.yaml b/releasenotes/notes/end-of-support-of-2023-1-ddec1dac59700063.yaml
new file mode 100644
index 0000000..d52b54e
--- /dev/null
+++ b/releasenotes/notes/end-of-support-of-2023-1-ddec1dac59700063.yaml
@@ -0,0 +1,12 @@
+---
+prelude: >
+    This is an intermediate release during the 2025.1 development cycle to
+    mark the end of support for 2023.1 release in Tempest.
+    After this release, Tempest will support below OpenStack Releases:
+
+    * 2024.2
+    * 2024.1
+    * 2023.2
+
+    Current development of Tempest is for OpenStack 2025.1 development
+    cycle.
diff --git a/releasenotes/notes/image-enforcement-config-0bc67791a40bac56.yaml b/releasenotes/notes/image-enforcement-config-0bc67791a40bac56.yaml
new file mode 100644
index 0000000..2bbc82b
--- /dev/null
+++ b/releasenotes/notes/image-enforcement-config-0bc67791a40bac56.yaml
@@ -0,0 +1,8 @@
+---
+features:
+  - |
+    Add a new config option
+    `[image_feature_enabled]/image_format_enforcement` which tells tempest
+    that glance will do image format inspection and enforcement on upload. This
+    will disable tests that require glance to accept a bad image in order to
+    test another service (i.e. nova).
diff --git a/releasenotes/notes/image-wait-multiple-79c55305b584b1ba.yaml b/releasenotes/notes/image-wait-multiple-79c55305b584b1ba.yaml
new file mode 100644
index 0000000..6f63ebd
--- /dev/null
+++ b/releasenotes/notes/image-wait-multiple-79c55305b584b1ba.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    The wait_for_image_status() waiter now allows a list of status values
+    instead of just a string, and returns the state the image was in when we
+    stopped waiting.
diff --git a/requirements.txt b/requirements.txt
index b0df18b..a1eff53 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,3 @@
-# The order of packages is significant, because pip processes them in the order
-# of appearance. Changing the order has an impact on the overall integration
-# process, which may cause wedges in the gate later.
 pbr!=2.1.0,>=2.0.0 # Apache-2.0
 cliff!=2.9.0,>=2.8.0 # Apache-2.0
 jsonschema>=3.2.0 # MIT
@@ -13,7 +10,7 @@
 oslo.log>=3.36.0 # Apache-2.0
 stestr>=1.0.0 # Apache-2.0
 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
-oslo.utils>=4.7.0 # Apache-2.0
+oslo.utils>=7.0.0 # Apache-2.0
 fixtures>=3.0.0 # Apache-2.0/BSD
 PyYAML>=3.12 # MIT
 python-subunit>=1.0.0 # Apache-2.0/BSD
diff --git a/roles/run-tempest/tasks/main.yaml b/roles/run-tempest/tasks/main.yaml
index 29409c0..15b1743 100644
--- a/roles/run-tempest/tasks/main.yaml
+++ b/roles/run-tempest/tasks/main.yaml
@@ -25,11 +25,11 @@
     target_branch: "{{ zuul.override_checkout }}"
   when: zuul.override_checkout is defined
 
-- name: Use stable branch upper-constraints till Wallaby
+- name: Use stable branch upper-constraints till 2023.1
   set_fact:
     # TOX_CONSTRAINTS_FILE is new name, UPPER_CONSTRAINTS_FILE is old one, best to set both
     tempest_tox_environment: "{{ tempest_tox_environment | combine({'UPPER_CONSTRAINTS_FILE': stable_constraints_file}) | combine({'TOX_CONSTRAINTS_FILE': stable_constraints_file}) }}"
-  when: target_branch in ["stable/ocata", "stable/pike", "stable/queens", "stable/rocky", "stable/stein", "stable/train", "stable/ussuri", "unmaintained/victoria", "unmaintained/wallaby"]
+  when: target_branch in ["stable/ocata", "stable/pike", "stable/queens", "stable/rocky", "stable/stein", "stable/train", "stable/ussuri", "stable/2023.1", "unmaintained/victoria", "unmaintained/wallaby", "unmaintained/xena", "unmaintained/yoga", "unmaintained/zed", "unmaintained/2023.1"]
 
 - name: Use Configured upper-constraints for non-master Tempest
   set_fact:
diff --git a/setup.cfg b/setup.cfg
index bb1ced5..67555f4 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,7 +6,6 @@
 author = OpenStack
 author_email = openstack-discuss@lists.openstack.org
 home_page = https://docs.openstack.org/tempest/latest/
-python_requires = >=3.8
 classifier =
     Intended Audience :: Information Technology
     Intended Audience :: System Administrators
@@ -15,10 +14,10 @@
     Operating System :: POSIX :: Linux
     Programming Language :: Python
     Programming Language :: Python :: 3
-    Programming Language :: Python :: 3.8
     Programming Language :: Python :: 3.9
     Programming Language :: Python :: 3.10
     Programming Language :: Python :: 3.11
+    Programming Language :: Python :: 3.12
     Programming Language :: Python :: 3 :: Only
     Programming Language :: Python :: Implementation :: CPython
 
diff --git a/tempest/api/compute/flavors/test_flavors_negative.py b/tempest/api/compute/flavors/test_flavors_negative.py
index 09f54b5..efd9cdd 100644
--- a/tempest/api/compute/flavors/test_flavors_negative.py
+++ b/tempest/api/compute/flavors/test_flavors_negative.py
@@ -47,7 +47,7 @@
             'name': data_utils.rand_name(
                 prefix=CONF.resource_name_prefix, name='image'),
             'container_format': CONF.image.container_formats[0],
-            'disk_format': CONF.image.disk_formats[0],
+            'disk_format': 'raw',
             'min_ram': min_img_ram,
             'visibility': 'private'
         }
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index 8984d1d..eddfd73 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -316,6 +316,7 @@
             _, servers = compute.create_test_server(
                 self.os_primary, tenant_network=network,
                 validatable=True,
+                wait_until='ACTIVE',
                 validation_resources=validation_resources)
             return servers[0]
 
diff --git a/tempest/api/compute/servers/test_servers.py b/tempest/api/compute/servers/test_servers.py
index c72b74e..e7e84d6 100644
--- a/tempest/api/compute/servers/test_servers.py
+++ b/tempest/api/compute/servers/test_servers.py
@@ -263,3 +263,22 @@
         servers = self.servers_client.list_servers(
             detail=True, **params)['servers']
         self.assertNotEmpty(servers)
+
+
+class ServersListShow296Test(base.BaseV2ComputeTest):
+    """Test compute server with microversion >= than 2.96
+
+    This test tests the Server APIs response schema for 2.96 microversion.
+    No specific assert or behaviour verification is needed.
+    """
+
+    min_microversion = '2.96'
+    max_microversion = 'latest'
+
+    @decorators.idempotent_id('4eee1ffe-9e00-4c99-a431-0d3e0f323a8f')
+    def test_list_show_server_296(self):
+        server = self.create_test_server()
+        # Checking list API response schema.
+        self.servers_client.list_servers(detail=True)
+        # Checking show API response schema
+        self.servers_client.show_server(server['id'])
diff --git a/tempest/api/identity/admin/v3/test_groups.py b/tempest/api/identity/admin/v3/test_groups.py
index b5b3c5d..96218bb 100644
--- a/tempest/api/identity/admin/v3/test_groups.py
+++ b/tempest/api/identity/admin/v3/test_groups.py
@@ -128,7 +128,7 @@
         for g in user_groups:
             if 'membership_expires_at' in g:
                 self.assertIsNone(g['membership_expires_at'])
-                del(g['membership_expires_at'])
+                del g['membership_expires_at']
         self.assertEqual(sorted(groups, key=lambda k: k['name']),
                          sorted(user_groups, key=lambda k: k['name']))
         self.assertEqual(2, len(user_groups))
diff --git a/tempest/api/image/v2/admin/test_images.py b/tempest/api/image/v2/admin/test_images.py
index 2b1c4fb..2c2e9a8 100644
--- a/tempest/api/image/v2/admin/test_images.py
+++ b/tempest/api/image/v2/admin/test_images.py
@@ -112,7 +112,7 @@
         image_name = data_utils.rand_name(
             prefix=CONF.resource_name_prefix, name='copy-image')
         container_format = CONF.image.container_formats[0]
-        disk_format = CONF.image.disk_formats[0]
+        disk_format = 'raw'
         image = self.create_image(name=image_name,
                                   container_format=container_format,
                                   disk_format=disk_format,
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index 5bb8eef..9309c76 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -56,7 +56,14 @@
         image_name = data_utils.rand_name(
             prefix=CONF.resource_name_prefix, name='image')
         container_format = container_format or CONF.image.container_formats[0]
-        disk_format = disk_format or CONF.image.disk_formats[0]
+        disk_format = disk_format or 'raw'
+        if disk_format not in CONF.image.disk_formats:
+            # If the test asked for some disk format that is not available,
+            # consider that a programming error. Tests with specific
+            # requirements should be checking to see if it is available and
+            # skipping themselves instead of this helper doing it.
+            raise RuntimeError('Test requires unavailable disk_format %s, '
+                               'but did not skip' % disk_format)
         image = self.create_image(name=image_name,
                                   container_format=container_format,
                                   disk_format=disk_format,
@@ -125,7 +132,7 @@
         """
         self._require_import_method('web-download')
 
-        image = self._create_image()
+        image = self._create_image(disk_format='qcow2')
         # Now try to get image details
         body = self.client.show_image(image['id'])
         self.assertEqual(image['id'], body['id'])
@@ -390,7 +397,7 @@
         image_name = data_utils.rand_name(
             prefix=CONF.resource_name_prefix, name='image')
         container_format = CONF.image.container_formats[0]
-        disk_format = CONF.image.disk_formats[0]
+        disk_format = 'raw'
         image = self.create_image(name=image_name,
                                   container_format=container_format,
                                   disk_format=disk_format,
@@ -529,6 +536,15 @@
                      for container_fmt in container_fmts
                      for disk_fmt in disk_fmts]
 
+        # NOTE(danms): This tests depends on being able to lie about image
+        # content. We can probably improve this in some way, but without a
+        # valid sample of each image format in each container format, there is
+        # no easy solution.
+        if CONF.image_feature_enabled.image_format_enforcement:
+            raise cls.skipException(
+                'Image format enforcement prevents testing with '
+                'bogus image data')
+
         for (container_fmt, disk_fmt) in all_pairs[:6]:
             LOG.debug("Creating an image "
                       "(Container format: %s, Disk format: %s).",
@@ -754,7 +770,7 @@
         # Create an image to be shared using default visibility
         image_file = io.BytesIO(data_utils.random_bytes(2048))
         container_format = CONF.image.container_formats[0]
-        disk_format = CONF.image.disk_formats[0]
+        disk_format = 'raw'
         image = self.create_image(container_format=container_format,
                                   disk_format=disk_format)
         self.client.store_image_file(image['id'], data=image_file)
diff --git a/tempest/api/image/v2/test_images_formats.py b/tempest/api/image/v2/test_images_formats.py
index 48f1325..f0dec90 100644
--- a/tempest/api/image/v2/test_images_formats.py
+++ b/tempest/api/image/v2/test_images_formats.py
@@ -166,6 +166,12 @@
             # a properly-formatted image for it, so skip it.
             self.skipTest(
                 'Format %s not allowed by config' % self.imgdef['format'])
+        if CONF.image_feature_enabled.image_format_enforcement:
+            # If glance rejects bad images during upload, we cannot get them
+            # registered so that we can test nova.
+            self.skipTest(
+                'Unable to test compute image formats if glance does not '
+                'allow them to be uploaded')
 
         # VMDK with footer was not supported by earlier service versions,
         # so we need to tolerate it passing and failing (skip for the latter).
@@ -191,6 +197,12 @@
     @decorators.idempotent_id('ffe21610-e801-4992-9b81-e2d646e2e2e9')
     def test_compute_rejects_format_mismatch(self):
         """Make sure compute rejects any image with a format mismatch."""
+        if CONF.image_feature_enabled.image_format_enforcement:
+            # If glance rejects bad images during upload, we cannot get them
+            # registered so that we can test nova.
+            self.skipTest(
+                'Unable to test compute image formats if glance does not '
+                'allow them to be uploaded')
         # Lying about the disk_format should always fail
         override_fmt = (
             self.imgdef['format'] in ('raw', 'gpt') and 'qcow2' or 'raw')
diff --git a/tempest/api/network/admin/test_dhcp_agent_scheduler.py b/tempest/api/network/admin/test_dhcp_agent_scheduler.py
index b4bfc61..8b4766c 100644
--- a/tempest/api/network/admin/test_dhcp_agent_scheduler.py
+++ b/tempest/api/network/admin/test_dhcp_agent_scheduler.py
@@ -48,12 +48,13 @@
 
     @decorators.idempotent_id('f164801e-1dd8-4b8b-b5d3-cc3ac77cfaa5')
     def test_dhcp_port_status_active(self):
-        ports = self.admin_ports_client.list_ports(
+        dhcp_ports = self.admin_ports_client.list_ports(
+            device_owner='network:dhcp',
             network_id=self.network['id'])['ports']
-        for port in ports:
+        for dhcp_port in dhcp_ports:
             waiters.wait_for_port_status(
                 client=self.admin_ports_client,
-                port_id=port['id'],
+                port_id=dhcp_port['id'],
                 status='ACTIVE')
 
     @decorators.idempotent_id('5032b1fe-eb42-4a64-8f3b-6e189d8b5c7d')
diff --git a/tempest/api/network/test_routers.py b/tempest/api/network/test_routers.py
index 0dd7c70..aaedba2 100644
--- a/tempest/api/network/test_routers.py
+++ b/tempest/api/network/test_routers.py
@@ -18,6 +18,7 @@
 
 from tempest.api.network import base
 from tempest.common import utils
+from tempest.common import waiters
 from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib.common.utils import test_utils
@@ -33,11 +34,17 @@
         interface = self.routers_client.add_router_interface(
             router_id, subnet_id=subnet_id)
         self.addCleanup(self._remove_router_interface_with_subnet_id,
-                        router_id, subnet_id)
+                        router_id, subnet_id, interface['port_id'])
         self.assertEqual(subnet_id, interface['subnet_id'])
         return interface
 
-    def _remove_router_interface_with_subnet_id(self, router_id, subnet_id):
+    def _remove_router_interface_with_subnet_id(self, router_id, subnet_id,
+                                                port_id):
+        # NOTE: with DVR and without a VM port, it is not possible to know
+        # what agent will host the router interface thus won't be bound.
+        if not utils.is_extension_enabled('dvr', 'network'):
+            waiters.wait_for_port_status(client=self.ports_client,
+                                         port_id=port_id, status='ACTIVE')
         body = self.routers_client.remove_router_interface(router_id,
                                                            subnet_id=subnet_id)
         self.assertEqual(subnet_id, body['subnet_id'])
@@ -107,7 +114,7 @@
         interface = self.routers_client.add_router_interface(
             router['id'], subnet_id=subnet['id'])
         self.addCleanup(self._remove_router_interface_with_subnet_id,
-                        router['id'], subnet['id'])
+                        router['id'], subnet['id'], interface['port_id'])
         self.assertIn('subnet_id', interface.keys())
         self.assertIn('port_id', interface.keys())
         # Verify router id is equal to device id in port details
@@ -183,9 +190,10 @@
             next_cidr = next_cidr.next()
 
             # Add router interface with subnet id
-            self.create_router_interface(router['id'], subnet['id'])
+            interface = self.create_router_interface(router['id'],
+                                                     subnet['id'])
             self.addCleanup(self._remove_router_interface_with_subnet_id,
-                            router['id'], subnet['id'])
+                            router['id'], subnet['id'], interface['port_id'])
             cidr = netaddr.IPNetwork(subnet['cidr'])
             next_hop = str(cidr[2])
             destination = str(subnet['cidr'])
diff --git a/tempest/api/volume/test_volumes_negative.py b/tempest/api/volume/test_volumes_negative.py
index d8480df..754b676 100644
--- a/tempest/api/volume/test_volumes_negative.py
+++ b/tempest/api/volume/test_volumes_negative.py
@@ -45,7 +45,7 @@
         image = self.images_client.create_image(
             name=image_name,
             container_format=CONF.image.container_formats[0],
-            disk_format=CONF.image.disk_formats[0],
+            disk_format='raw',
             visibility='private',
             min_disk=CONF.volume.volume_size + CONF.volume.volume_size_extend)
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
index dd18190..79cc09c 100644
--- a/tempest/common/utils/linux/remote_client.py
+++ b/tempest/common/utils/linux/remote_client.py
@@ -59,14 +59,14 @@
         output = self.exec_command(command)
         selected = []
         pos = None
-        for l in output.splitlines():
-            if pos is None and l.find("TYPE") > 0:
-                pos = l.find("TYPE")
+        for line in output.splitlines():
+            if pos is None and line.find("TYPE") > 0:
+                pos = line.find("TYPE")
                 # Show header line too
-                selected.append(l)
+                selected.append(line)
             # lsblk lists disk type in a column right-aligned with TYPE
-            elif pos is not None and pos > 0 and l[pos:pos + 4] == "disk":
-                selected.append(l)
+            elif pos is not None and pos > 0 and line[pos:pos + 4] == "disk":
+                selected.append(line)
 
         if selected:
             return "\n".join(selected)
@@ -121,9 +121,9 @@
     def _get_dns_servers(self):
         cmd = 'cat /etc/resolv.conf'
         resolve_file = self.exec_command(cmd).strip().split('\n')
-        entries = (l.split() for l in resolve_file)
-        dns_servers = [l[1] for l in entries
-                       if len(l) and l[0] == 'nameserver']
+        entries = (line.split() for line in resolve_file)
+        dns_servers = [line[1] for line in entries
+                       if len(line) and line[0] == 'nameserver']
         return dns_servers
 
     def get_dns_servers(self, timeout=5):
diff --git a/tempest/common/utils/net_downtime.py b/tempest/common/utils/net_downtime.py
index 9675ec8..ec1a4c8 100644
--- a/tempest/common/utils/net_downtime.py
+++ b/tempest/common/utils/net_downtime.py
@@ -22,12 +22,38 @@
 
 LOG = log.getLogger(__name__)
 
+PASSED = 'PASSED'
+FAILED = 'FAILED'
+METADATA_SCRIPT_PATH = '/tmp/metadata_meter_script.sh'
+METADATA_RESULTS_PATH = '/tmp/metadata_meter.log'
+METADATA_PID_PATH = '/tmp/metadata_meter.pid'
+# /proc/uptime is used because it include two decimals in cirros, while
+# `date +%s.%N` does not work in cirros (min granularity is seconds)
+METADATA_SCRIPT = """#!/bin/sh
+echo $$ > %(metadata_meter_pidfile)s
+old_time=$(cut -d" " -f1 /proc/uptime)
+while true; do
+    curl http://169.254.169.254/latest/meta-data/hostname 2>/dev/null | \
+grep -q `hostname`
+    result=$?
+    new_time=$(cut -d" " -f1 /proc/uptime)
+    runtime=$(awk -v new=$new_time -v old=$old_time "BEGIN {print new-old}")
+    old_time=$new_time
+    if [ $result -eq 0 ]; then
+        echo "PASSED $runtime"
+    else
+        echo "FAILED $runtime"
+    fi
+    sleep %(interval)s
+done
+"""
+
 
 class NetDowntimeMeter(fixtures.Fixture):
-    def __init__(self, dest_ip, interval='0.2'):
+    def __init__(self, dest_ip, interval=0.2):
         self.dest_ip = dest_ip
         # Note: for intervals lower than 0.2 ping requires root privileges
-        self.interval = interval
+        self.interval = float(interval)
         self.ping_process = None
 
     def _setUp(self):
@@ -35,18 +61,18 @@
 
     def start_background_pinger(self):
         cmd = ['ping', '-q', '-s1']
-        cmd.append('-i{}'.format(self.interval))
+        cmd.append('-i%g' % self.interval)
         cmd.append(self.dest_ip)
-        LOG.debug("Starting background pinger to '{}' with interval {}".format(
-            self.dest_ip, self.interval))
+        LOG.debug("Starting background pinger to '%s' with interval %g",
+                  self.dest_ip, self.interval)
         self.ping_process = subprocess.Popen(
             cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         self.addCleanup(self.cleanup)
 
     def cleanup(self):
         if self.ping_process and self.ping_process.poll() is None:
-            LOG.debug('Terminating background pinger with pid {}'.format(
-                self.ping_process.pid))
+            LOG.debug('Terminating background pinger with pid %d',
+                      self.ping_process.pid)
             self.ping_process.terminate()
         self.ping_process = None
 
@@ -57,7 +83,68 @@
         output = self.ping_process.stderr.readline().strip().decode('utf-8')
         if output and len(output.split()[0].split('/')) == 2:
             succ, total = output.split()[0].split('/')
-            return (int(total) - int(succ)) * float(self.interval)
+            return (int(total) - int(succ)) * self.interval
         else:
             LOG.warning('Unexpected output obtained from the pinger: %s',
                         output)
+
+
+class MetadataDowntimeMeter(fixtures.Fixture):
+    def __init__(self, ssh_client,
+                 interval='0.2', script_path=METADATA_SCRIPT_PATH,
+                 output_path=METADATA_RESULTS_PATH,
+                 pidfile_path=METADATA_PID_PATH):
+        self.ssh_client = ssh_client
+        self.interval = interval
+        self.script_path = script_path
+        self.output_path = output_path
+        self.pidfile_path = pidfile_path
+        self.pid = None
+
+    def _setUp(self):
+        self.addCleanup(self.cleanup)
+        self.upload_metadata_script()
+        self.run_metadata_script()
+
+    def upload_metadata_script(self):
+        metadata_script = METADATA_SCRIPT % {
+            'metadata_meter_pidfile': self.pidfile_path,
+            'interval': self.interval}
+        echo_cmd = "echo '{}' > {}".format(
+            metadata_script, self.script_path)
+        chmod_cmd = 'chmod +x {}'.format(self.script_path)
+        self.ssh_client.exec_command(';'.join((echo_cmd, chmod_cmd)))
+        LOG.debug('script created: %s', self.script_path)
+        output = self.ssh_client.exec_command(
+            'cat {}'.format(self.script_path))
+        LOG.debug('script content: %s', output)
+
+    def run_metadata_script(self):
+        self.ssh_client.exec_command('{} > {} &'.format(self.script_path,
+                                                        self.output_path))
+        self.pid = self.ssh_client.exec_command(
+            'cat {}'.format(self.pidfile_path)).strip()
+        LOG.debug('running metadata downtime meter script in background with '
+                  'PID = %s', self.pid)
+
+    def get_results(self):
+        output = self.ssh_client.exec_command(
+            'cat {}'.format(self.output_path))
+        results = {}
+        results['successes'] = output.count(PASSED)
+        results['failures'] = output.count(FAILED)
+        downtime = {PASSED: 0.0, FAILED: 0.0}
+        for line in output.splitlines():
+            key, value = line.strip().split()
+            downtime[key] += float(value)
+
+        results['downtime'] = downtime
+        LOG.debug('metadata downtime meter results: %r', results)
+        return results
+
+    def cleanup(self):
+        if self.pid:
+            self.ssh_client.exec_command('kill {}'.format(self.pid))
+            LOG.debug('killed metadata downtime script with PID %s', self.pid)
+        else:
+            LOG.debug('No metadata downtime script found')
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index 9e97f47..b4312b7 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -154,13 +154,21 @@
 
 
 def wait_for_image_status(client, image_id, status):
-    """Waits for an image to reach a given status.
+    """Waits for an image to reach a given status (or list of them).
 
     The client should have a show_image(image_id) method to get the image.
     The client should also have build_interval and build_timeout attributes.
+
+    status can be either a string or a list of strings that constitute a
+    terminal state that we will return.
     """
     show_image = client.show_image
 
+    if isinstance(status, str):
+        terminal_status = [status]
+    else:
+        terminal_status = status
+
     current_status = 'An unknown status'
     start = int(time.time())
     while int(time.time()) - start < client.build_timeout:
@@ -171,8 +179,8 @@
             image = image['image']
 
         current_status = image['status']
-        if current_status == status:
-            return
+        if current_status in terminal_status:
+            return current_status
         if current_status.lower() == 'killed':
             raise exceptions.ImageKilledException(image_id=image_id,
                                                   status=status)
@@ -184,7 +192,7 @@
     message = ('Image %(image_id)s failed to reach %(status)s state '
                '(current state %(current_status)s) within the required '
                'time (%(timeout)s s).' % {'image_id': image_id,
-                                          'status': status,
+                                          'status': ','.join(terminal_status),
                                           'current_status': current_status,
                                           'timeout': client.build_timeout})
     caller = test_utils.find_test_caller()
@@ -327,8 +335,7 @@
     # Check if image have last store location
     if len(available_stores) == 1:
         exc_cls = lib_exc.OtherRestClientException
-        message = ('Delete from last store location not allowed'
-                   % (image, image_store_deleted))
+        message = 'Delete from last store location not allowed'
         raise exc_cls(message)
     start = int(time.time())
     while int(time.time()) - start < client.build_timeout:
@@ -548,7 +555,7 @@
     interface_status = body['port_state']
     start = int(time.time())
 
-    while(interface_status != status):
+    while interface_status != status:
         time.sleep(client.build_interval)
         body = (client.show_interface(server_id, port_id)
                 ['interfaceAttachment'])
diff --git a/tempest/config.py b/tempest/config.py
index 471782b..36f0152 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -728,6 +728,11 @@
     cfg.BoolOpt('image_conversion',
                 default=False,
                 help=('Is image_conversion enabled in glance.')),
+    cfg.BoolOpt('image_format_enforcement',
+                default=True,
+                help=('Indicates that image format is enforced by glance, '
+                      'such that we should not expect to be able to upload '
+                      'bad images for testing other services.')),
 ]
 
 network_group = cfg.OptGroup(name='network',
@@ -964,6 +969,12 @@
                       "migration, in seconds. "
                       "When the measured downtime exceeds this value, an "
                       "exception is raised."),
+    cfg.FloatOpt('allowed_metadata_downtime',
+                 default=6.0,
+                 help="Allowed VM metadata connection downtime during live "
+                      "migration, in seconds. "
+                      "When the measured downtime exceeds this value, an "
+                      "exception is raised."),
 ]
 
 volume_group = cfg.OptGroup(name='volume',
@@ -978,7 +989,7 @@
                help='Timeout in seconds to wait for a volume to become '
                     'available.'),
     cfg.StrOpt('catalog_type',
-               default='volumev3',
+               default='block-storage',
                help="Catalog type of the Volume Service"),
     cfg.StrOpt('region',
                default='',
@@ -1343,9 +1354,9 @@
 
 The best use case is investigating used resources of one test.
 A test can be run as follows:
- $ stestr run --pdb TEST_ID
+$ stestr run --pdb TEST_ID
 or
- $ python -m testtools.run TEST_ID"""),
+$ python -m testtools.run TEST_ID"""),
     cfg.StrOpt('resource_name_prefix',
                default='tempest',
                help="Define the prefix name for the resources created by "
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index 1c9c55b..c81ec03 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -16,7 +16,6 @@
 import re
 
 from hacking import core
-import pycodestyle
 
 
 PYTHON_CLIENTS = ['cinder', 'glance', 'keystone', 'nova', 'swift', 'neutron',
@@ -40,22 +39,22 @@
 
 
 @core.flake8ext
-def import_no_clients_in_api_and_scenario_tests(physical_line, filename):
+def import_no_clients_in_api_and_scenario_tests(logical_line, filename):
     """Check for client imports from tempest/api & tempest/scenario tests
 
     T102: Cannot import OpenStack python clients
     """
 
     if "tempest/api" in filename or "tempest/scenario" in filename:
-        res = PYTHON_CLIENT_RE.match(physical_line)
+        res = PYTHON_CLIENT_RE.match(logical_line)
         if res:
-            return (physical_line.find(res.group(1)),
+            return (logical_line.find(res.group(1)),
                     ("T102: python clients import not allowed"
                      " in tempest/api/* or tempest/scenario/* tests"))
 
 
 @core.flake8ext
-def scenario_tests_need_service_tags(physical_line, filename,
+def scenario_tests_need_service_tags(logical_line, filename,
                                      previous_logical):
     """Check that scenario tests have service tags
 
@@ -63,28 +62,28 @@
     """
 
     if 'tempest/scenario/' in filename and '/test_' in filename:
-        if TEST_DEFINITION.match(physical_line):
+        if TEST_DEFINITION.match(logical_line):
             if not SCENARIO_DECORATOR.match(previous_logical):
-                return (physical_line.find('def'),
+                return (logical_line.find('def'),
                         "T104: Scenario tests require a service decorator")
 
 
 @core.flake8ext
-def no_setup_teardown_class_for_tests(physical_line, filename):
+def no_setup_teardown_class_for_tests(logical_line, filename, noqa):
 
-    if pycodestyle.noqa(physical_line):
+    if noqa:
         return
 
     if 'tempest/test.py' in filename or 'tempest/lib/' in filename:
         return
 
-    if SETUP_TEARDOWN_CLASS_DEFINITION.match(physical_line):
-        return (physical_line.find('def'),
+    if SETUP_TEARDOWN_CLASS_DEFINITION.match(logical_line):
+        return (logical_line.find('def'),
                 "T105: (setUp|tearDown)Class can not be used in tests")
 
 
 @core.flake8ext
-def service_tags_not_in_module_path(physical_line, filename):
+def service_tags_not_in_module_path(logical_line, filename):
     """Check that a service tag isn't in the module path
 
     A service tag should only be added if the service name isn't already in
@@ -96,14 +95,14 @@
     # created for services like heat which would cause false negatives for
     # those tests, so just exclude the scenario tests.
     if 'tempest/scenario' not in filename:
-        matches = SCENARIO_DECORATOR.match(physical_line)
+        matches = SCENARIO_DECORATOR.match(logical_line)
         if matches:
             services = matches.group(1).split(',')
             for service in services:
                 service_name = service.strip().strip("'")
                 modulepath = os.path.split(filename)[0]
                 if service_name in modulepath:
-                    return (physical_line.find(service_name),
+                    return (logical_line.find(service_name),
                             "T107: service tag should not be in path")
 
 
@@ -140,28 +139,27 @@
                "decorators.skip_because from tempest.lib")
 
 
-def _common_service_clients_check(logical_line, physical_line, filename):
+def _common_service_clients_check(logical_line, filename, noqa):
+    if noqa:
+        return False
+
     if not re.match('tempest/(lib/)?services/.*', filename):
         return False
 
-    if not METHOD.match(physical_line):
-        return False
-
-    if pycodestyle.noqa(physical_line):
+    if not METHOD.match(logical_line):
         return False
 
     return True
 
 
 @core.flake8ext
-def get_resources_on_service_clients(physical_line, logical_line, filename,
-                                     line_number, lines):
+def get_resources_on_service_clients(logical_line, filename,
+                                     line_number, lines, noqa):
     """Check that service client names of GET should be consistent
 
     T110
     """
-    if not _common_service_clients_check(logical_line, physical_line,
-                                         filename):
+    if not _common_service_clients_check(logical_line, filename, noqa):
         return
 
     for line in lines[line_number:]:
@@ -182,14 +180,13 @@
 
 
 @core.flake8ext
-def delete_resources_on_service_clients(physical_line, logical_line, filename,
-                                        line_number, lines):
+def delete_resources_on_service_clients(logical_line, filename,
+                                        line_number, lines, noqa):
     """Check that service client names of DELETE should be consistent
 
     T111
     """
-    if not _common_service_clients_check(logical_line, physical_line,
-                                         filename):
+    if not _common_service_clients_check(logical_line, filename, noqa):
         return
 
     for line in lines[line_number:]:
@@ -262,7 +259,7 @@
             'oslo_config' in logical_line):
         msg = ('T114: tempest.lib can not have any dependency on tempest '
                'config.')
-        yield(0, msg)
+        yield (0, msg)
 
 
 @core.flake8ext
@@ -281,7 +278,7 @@
 
     if not re.match(r'.\/tempest\/api\/.*\/admin\/.*', filename):
         msg = 'T115: All admin tests should exist under admin path.'
-        yield(0, msg)
+        yield (0, msg)
 
 
 @core.flake8ext
@@ -293,11 +290,11 @@
     result = EX_ATTRIBUTE.search(logical_line)
     msg = ("[T116] Unsupported 'message' Exception attribute in PY3")
     if result:
-        yield(0, msg)
+        yield (0, msg)
 
 
 @core.flake8ext
-def negative_test_attribute_always_applied_to_negative_tests(physical_line,
+def negative_test_attribute_always_applied_to_negative_tests(logical_line,
                                                              filename):
     """Check ``@decorators.attr(type=['negative'])`` applied to negative tests.
 
@@ -307,13 +304,13 @@
 
     if re.match(r'.\/tempest\/api\/.*_negative.*', filename):
 
-        if NEGATIVE_TEST_DECORATOR.match(physical_line):
+        if NEGATIVE_TEST_DECORATOR.match(logical_line):
             _HAVE_NEGATIVE_DECORATOR = True
             return
 
-        if TEST_DEFINITION.match(physical_line):
+        if TEST_DEFINITION.match(logical_line):
             if not _HAVE_NEGATIVE_DECORATOR:
-                return (
+                yield (
                     0, "T117: Must apply `@decorators.attr(type=['negative'])`"
                        " to all negative API tests"
                 )
diff --git a/tempest/lib/api_schema/response/compute/v2_96/__init__.py b/tempest/lib/api_schema/response/compute/v2_96/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_96/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_96/servers.py b/tempest/lib/api_schema/response/compute/v2_96/servers.py
new file mode 100644
index 0000000..7036a11
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_96/servers.py
@@ -0,0 +1,62 @@
+#    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_89 import servers as servers289
+
+
+###########################################################################
+#
+# 2.96:
+#
+# The attachment_id and bdm_uuid parameter is now returned in the response body
+# of the following calls:
+# The pinned_availability_zone parameter is now returned in the response body
+# of the following calls:
+#
+# - GET /servers/detail
+# - GET /servers/{server_id}
+###########################################################################
+
+get_server = copy.deepcopy(servers289.get_server)
+get_server['response_body']['properties']['server'][
+    'properties'].update(
+        {'pinned_availability_zone': {'type': ['string', 'null']}})
+
+list_servers_detail = copy.deepcopy(servers289.list_servers_detail)
+list_servers_detail['response_body']['properties']['servers']['items'][
+    'properties'].update(
+        {'pinned_availability_zone': {'type': ['string', 'null']}})
+
+# NOTE(zhufl): Below are the unchanged schema in this microversion. We
+# need to keep this schema in this file to have the generic way to select the
+# right schema based on self.schema_versions_info mapping in service client.
+# ****** Schemas unchanged since microversion 2.89***
+attach_volume = copy.deepcopy(servers289.attach_volume)
+show_volume_attachment = copy.deepcopy(servers289.show_volume_attachment)
+list_volume_attachments = copy.deepcopy(servers289.list_volume_attachments)
+rebuild_server = copy.deepcopy(servers289.rebuild_server)
+rebuild_server_with_admin_pass = copy.deepcopy(
+    servers289.rebuild_server_with_admin_pass)
+update_server = copy.deepcopy(servers289.update_server)
+list_servers = copy.deepcopy(servers289.list_servers)
+show_server_diagnostics = copy.deepcopy(servers289.show_server_diagnostics)
+get_remote_consoles = copy.deepcopy(servers289.get_remote_consoles)
+list_tags = copy.deepcopy(servers289.list_tags)
+update_all_tags = copy.deepcopy(servers289.update_all_tags)
+delete_all_tags = copy.deepcopy(servers289.delete_all_tags)
+check_tag_existence = copy.deepcopy(servers289.check_tag_existence)
+update_tag = copy.deepcopy(servers289.update_tag)
+delete_tag = copy.deepcopy(servers289.delete_tag)
+show_instance_action = copy.deepcopy(servers289.show_instance_action)
+create_backup = copy.deepcopy(servers289.create_backup)
diff --git a/tempest/lib/auth.py b/tempest/lib/auth.py
index 8bdf98e..12fffdb 100644
--- a/tempest/lib/auth.py
+++ b/tempest/lib/auth.py
@@ -21,6 +21,7 @@
 from urllib import parse as urlparse
 
 from oslo_log import log as logging
+from oslo_utils import timeutils
 
 from tempest.lib import exceptions
 from tempest.lib.services.identity.v2 import token_client as json_v2id
@@ -419,8 +420,7 @@
     def is_expired(self, auth_data):
         _, access = auth_data
         expiry = self._parse_expiry_time(access['token']['expires'])
-        return (expiry - self.token_expiry_threshold <=
-                datetime.datetime.utcnow())
+        return (expiry - self.token_expiry_threshold <= timeutils.utcnow())
 
 
 class KeystoneV3AuthProvider(KeystoneAuthProvider):
@@ -595,8 +595,7 @@
     def is_expired(self, auth_data):
         _, access = auth_data
         expiry = self._parse_expiry_time(access['expires_at'])
-        return (expiry - self.token_expiry_threshold <=
-                datetime.datetime.utcnow())
+        return (expiry - self.token_expiry_threshold <= timeutils.utcnow())
 
 
 def is_identity_version_supported(identity_version):
diff --git a/tempest/lib/common/http.py b/tempest/lib/common/http.py
index d163968..5bdcecd 100644
--- a/tempest/lib/common/http.py
+++ b/tempest/lib/common/http.py
@@ -60,6 +60,14 @@
             retry = urllib3.util.Retry(redirect=False)
         r = super(ClosingProxyHttp, self).request(method, url, retries=retry,
                                                   *args, **new_kwargs)
+
+        # Clearing the pool is necessary to free memory that holds certificates
+        # loaded by the HTTPConnection class in urllib3. This line can be
+        # removed once we require a newer version of urllib3 (e.g., 2.2.3) that
+        # does not retain certificates in memory for each HTTPConnection
+        # managed by the PoolManager.
+        self.clear()
+
         if not kwargs.get('preload_content', True):
             # This means we asked urllib3 for streaming content, so we
             # need to return the raw response and not read any data yet
@@ -114,6 +122,14 @@
             retry = urllib3.util.Retry(redirect=False)
         r = super(ClosingHttp, self).request(method, url, retries=retry,
                                              *args, **new_kwargs)
+
+        # Clearing the pool is necessary to free memory that holds certificates
+        # loaded by the HTTPConnection class in urllib3. This line can be
+        # removed once we require a newer version of urllib3 (e.g., 2.2.3) that
+        # does not retain certificates in memory for each HTTPConnection
+        # managed by the PoolManager.
+        self.clear()
+
         if not kwargs.get('preload_content', True):
             # This means we asked urllib3 for streaming content, so we
             # need to return the raw response and not read any data yet
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index b656b7a..b360569 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -881,6 +881,11 @@
                 resp_body = self._parse_resp(resp_body)
             raise exceptions.Gone(resp_body, resp=resp)
 
+        if resp.status == 406:
+            if parse_resp:
+                resp_body = self._parse_resp(resp_body)
+            raise exceptions.NotAcceptable(resp_body, resp=resp)
+
         if resp.status == 409:
             if parse_resp:
                 resp_body = self._parse_resp(resp_body)
diff --git a/tempest/lib/exceptions.py b/tempest/lib/exceptions.py
index dd7885e..0242de2 100644
--- a/tempest/lib/exceptions.py
+++ b/tempest/lib/exceptions.py
@@ -94,6 +94,11 @@
     message = "Object not found"
 
 
+class NotAcceptable(ClientRestClientException):
+    status_code = 406
+    message = "Not Acceptable"
+
+
 class Conflict(ClientRestClientException):
     status_code = 409
     message = "Conflict with state of target resource"
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index 1b93f91..e91c87a 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -45,6 +45,7 @@
 from tempest.lib.api_schema.response.compute.v2_8 import servers as schemav28
 from tempest.lib.api_schema.response.compute.v2_89 import servers as schemav289
 from tempest.lib.api_schema.response.compute.v2_9 import servers as schemav29
+from tempest.lib.api_schema.response.compute.v2_96 import servers as schemav296
 from tempest.lib.common import rest_client
 from tempest.lib.services.compute import base_compute_client
 
@@ -75,7 +76,8 @@
         {'min': '2.73', 'max': '2.74', 'schema': schemav273},
         {'min': '2.75', 'max': '2.78', 'schema': schemav275},
         {'min': '2.79', 'max': '2.88', 'schema': schemav279},
-        {'min': '2.89', 'max': None, 'schema': schemav289}]
+        {'min': '2.89', 'max': '2.95', 'schema': schemav289},
+        {'min': '2.96', 'max': None, 'schema': schemav296}]
 
     def __init__(self, auth_provider, service, region,
                  enable_instance_password=True, **kwargs):
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 01c42c8..369efcc 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -851,7 +851,7 @@
                     kernel_img_path = os.path.join(extract_dir, fname)
                 elif re.search(r'(.*-initrd.*|ari-.*/image$)', fname):
                     ramdisk_img_path = os.path.join(extract_dir, fname)
-                elif re.search(f'(.*\\.img$|ami-.*/image$)', fname):
+                elif re.search(r'(.*\\.img$|ami-.*/image$)', fname):
                     img_path = os.path.join(extract_dir, fname)
             # Create the kernel image.
             kparams = {
@@ -1561,8 +1561,8 @@
             floating_ip = (self.floating_ips_client.
                            show_floatingip(floatingip_id)['floatingip'])
             if status == floating_ip['status']:
-                LOG.info("FloatingIP: {fp} is at status: {st}"
-                         .format(fp=floating_ip, st=status))
+                LOG.info("FloatingIP: %s is at status: %s",
+                         floating_ip, status)
             return status == floating_ip['status']
 
         if not test_utils.call_until_true(refresh,
diff --git a/tempest/scenario/test_network_advanced_server_ops.py b/tempest/scenario/test_network_advanced_server_ops.py
index 911ff42..f4ee98d 100644
--- a/tempest/scenario/test_network_advanced_server_ops.py
+++ b/tempest/scenario/test_network_advanced_server_ops.py
@@ -17,6 +17,7 @@
 
 from oslo_log import log
 from tempest.common import utils
+from tempest.common.utils.linux import remote_client
 from tempest.common.utils import net_downtime
 from tempest.common import waiters
 from tempest import config
@@ -189,6 +190,12 @@
             floating_ip['floating_ip_address'])
         self.useFixture(downtime_meter)
 
+        metadata_downtime_meter = net_downtime.MetadataDowntimeMeter(
+            remote_client.RemoteClient(floating_ip['floating_ip_address'],
+                                       CONF.validation.image_ssh_user,
+                                       pkey=keypair['private_key']))
+        self.useFixture(metadata_downtime_meter)
+
         migration_kwargs = {'host': None, 'block_migration': block_migration}
 
         # check if microversion is less than 2.25 because of
@@ -230,6 +237,18 @@
             "Downtime of {} seconds is higher than expected '{}'".format(
                 downtime, allowed_downtime))
 
+        metadata_downtime_results = metadata_downtime_meter.get_results()
+        self.assertGreater(metadata_downtime_results['successes'], 0)
+        LOG.debug("Metadata Downtime seconds measured = %r",
+                  metadata_downtime_results['downtime'])
+        allowed_metadata_downtime = CONF.validation.allowed_metadata_downtime
+        metadata_downtime_failed = \
+            metadata_downtime_results['downtime']['FAILED']
+        self.assertLessEqual(
+            metadata_downtime_failed, allowed_metadata_downtime,
+            "Metadata downtime: {} seconds is higher than expected: {}".format(
+                metadata_downtime_failed, allowed_metadata_downtime))
+
     def _test_server_connectivity_cold_migration_revert(self, source_host=None,
                                                         dest_host=None):
         keypair = self.create_keypair(client=self.keypairs_client)
diff --git a/tempest/scenario/test_unified_limits.py b/tempest/scenario/test_unified_limits.py
index 6e194f9..7e8f8b2 100644
--- a/tempest/scenario/test_unified_limits.py
+++ b/tempest/scenario/test_unified_limits.py
@@ -32,6 +32,13 @@
     credentials = ['primary', 'system_admin']
 
     @classmethod
+    def skip_checks(cls):
+        super(ImageQuotaTest, cls).skip_checks()
+        if not CONF.service_available.glance:
+            skip_msg = ("%s skipped as glance is not available" % cls.__name__)
+            raise cls.skipException(skip_msg)
+
+    @classmethod
     def resource_setup(cls):
         super(ImageQuotaTest, cls).resource_setup()
 
diff --git a/tempest/tests/cmd/test_verify_tempest_config.py b/tempest/tests/cmd/test_verify_tempest_config.py
index fa43e58..3df9f19 100644
--- a/tempest/tests/cmd/test_verify_tempest_config.py
+++ b/tempest/tests/cmd/test_verify_tempest_config.py
@@ -498,7 +498,7 @@
                 return ('token',
                         {'serviceCatalog': [{'type': 'compute'},
                                             {'type': 'image'},
-                                            {'type': 'volumev3'},
+                                            {'type': 'block-storage'},
                                             {'type': 'network'},
                                             {'type': 'object-store'}]})
 
diff --git a/tempest/tests/lib/test_auth.py b/tempest/tests/lib/test_auth.py
index 3edb122..4e5ec48 100644
--- a/tempest/tests/lib/test_auth.py
+++ b/tempest/tests/lib/test_auth.py
@@ -17,6 +17,7 @@
 import datetime
 
 import fixtures
+from oslo_utils import timeutils
 import testtools
 
 from tempest.lib import auth
@@ -509,15 +510,15 @@
         self._test_base_url_helper(expected, filters, ('token', auth_data))
 
     def test_token_not_expired(self):
-        expiry_data = datetime.datetime.utcnow() + datetime.timedelta(days=1)
+        expiry_data = timeutils.utcnow() + datetime.timedelta(days=1)
         self._verify_expiry(expiry_data=expiry_data, should_be_expired=False)
 
     def test_token_expired(self):
-        expiry_data = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
+        expiry_data = timeutils.utcnow() - datetime.timedelta(hours=1)
         self._verify_expiry(expiry_data=expiry_data, should_be_expired=True)
 
     def test_token_not_expired_to_be_renewed(self):
-        expiry_data = (datetime.datetime.utcnow() +
+        expiry_data = (timeutils.utcnow() +
                        self.auth_provider.token_expiry_threshold / 2)
         self._verify_expiry(expiry_data=expiry_data, should_be_expired=True)
 
diff --git a/tempest/tests/test_hacking.py b/tempest/tests/test_hacking.py
index 464e66a..3f603e8 100644
--- a/tempest/tests/test_hacking.py
+++ b/tempest/tests/test_hacking.py
@@ -51,25 +51,34 @@
 
     def test_no_setup_teardown_class_for_tests(self):
         self.assertTrue(checks.no_setup_teardown_class_for_tests(
-            "  def setUpClass(cls):", './tempest/tests/fake_test.py'))
+            "  def setUpClass(cls):", './tempest/tests/fake_test.py', False))
         self.assertIsNone(checks.no_setup_teardown_class_for_tests(
-            "  def setUpClass(cls): # noqa", './tempest/tests/fake_test.py'))
+            "  def setUpClass(cls):", './tempest/tests/fake_test.py',
+            True))
         self.assertTrue(checks.no_setup_teardown_class_for_tests(
-            "  def setUpClass(cls):", './tempest/api/fake_test.py'))
+            "  def setUpClass(cls):", './tempest/api/fake_test.py',
+            False))
         self.assertTrue(checks.no_setup_teardown_class_for_tests(
-            "  def setUpClass(cls):", './tempest/scenario/fake_test.py'))
+            "  def setUpClass(cls):", './tempest/scenario/fake_test.py',
+            False))
         self.assertFalse(checks.no_setup_teardown_class_for_tests(
-            "  def setUpClass(cls):", './tempest/test.py'))
+            "  def setUpClass(cls):", './tempest/test.py',
+            False))
         self.assertTrue(checks.no_setup_teardown_class_for_tests(
-            "  def tearDownClass(cls):", './tempest/tests/fake_test.py'))
+            "  def tearDownClass(cls):", './tempest/tests/fake_test.py',
+            False))
         self.assertIsNone(checks.no_setup_teardown_class_for_tests(
-            "  def tearDownClass(cls): # noqa", './tempest/tests/fake_test.py'))
+            "  def tearDownClass(cls):", './tempest/tests/fake_test.py',
+            True))
         self.assertTrue(checks.no_setup_teardown_class_for_tests(
-            "  def tearDownClass(cls):", './tempest/api/fake_test.py'))
+            "  def tearDownClass(cls):", './tempest/api/fake_test.py',
+            False))
         self.assertTrue(checks.no_setup_teardown_class_for_tests(
-            "  def tearDownClass(cls):", './tempest/scenario/fake_test.py'))
+            "  def tearDownClass(cls):", './tempest/scenario/fake_test.py',
+            False))
         self.assertFalse(checks.no_setup_teardown_class_for_tests(
-            "  def tearDownClass(cls):", './tempest/test.py'))
+            "  def tearDownClass(cls):", './tempest/test.py',
+            False))
 
     def test_import_no_clients_in_api_and_scenario_tests(self):
         for client in checks.PYTHON_CLIENTS:
@@ -198,22 +207,26 @@
             # arbitrarily many decorators. These insert decorators above the
             # @decorators.attr(type=['negative']) decorator.
             for decorator in other_decorators:
-                self.assertIsNone(check(" %s" % decorator, filename))
+                self.assertFalse(
+                    list(check(" %s" % decorator, filename)))
         if with_negative_decorator:
-            self.assertIsNone(
-                check("@decorators.attr(type=['negative'])", filename))
+            self.assertFalse(
+                list(check("@decorators.attr(type=['negative'])", filename)))
         if with_other_decorators:
             # Include multiple decorators to verify that this check works with
             # arbitrarily many decorators. These insert decorators between
             # the test and the @decorators.attr(type=['negative']) decorator.
             for decorator in other_decorators:
-                self.assertIsNone(check(" %s" % decorator, filename))
-        final_result = check(" def test_some_negative_case", filename)
+                self.assertFalse(
+                    list(check(" %s" % decorator, filename)))
+        final_result = list(check(" def test_some_negative_case", filename))
         if expected_success:
-            self.assertIsNone(final_result)
+            self.assertFalse(final_result)
         else:
-            self.assertIsInstance(final_result, tuple)
-            self.assertFalse(final_result[0])
+            self.assertEqual(1, len(final_result))
+            self.assertIsInstance(final_result[0], tuple)
+            self.assertEqual(0, final_result[0][0])
+            self.assertTrue(final_result[0][1])
 
     def test_no_negatve_test_attribute_applied_to_negative_test(self):
         # Check negative filename, negative decorator passes
diff --git a/tempest/tests/test_test.py b/tempest/tests/test_test.py
index 80825a4..7fb9bb3 100644
--- a/tempest/tests/test_test.py
+++ b/tempest/tests/test_test.py
@@ -303,7 +303,7 @@
         # [0]: test, err, details [1] -> exc_info
         # Type, Exception, traceback [1] -> MultipleException
         found_exc = log[0][1][1]
-        self.assertTrue(isinstance(found_exc, testtools.MultipleExceptions))
+        self.assertIsInstance(found_exc, testtools.MultipleExceptions)
         self.assertEqual(2, len(found_exc.args))
         # Each arg is exc_info - match messages and order
         self.assertIn('mock3 resource', str(found_exc.args[0][1]))
@@ -332,7 +332,7 @@
         # [0]: test, err, details [1] -> exc_info
         # Type, Exception, traceback [1] -> RuntimeError
         found_exc = log[0][1][1]
-        self.assertTrue(isinstance(found_exc, RuntimeError))
+        self.assertIsInstance(found_exc, RuntimeError)
         self.assertIn(BadResourceCleanup.__name__, str(found_exc))
 
     def test_super_skip_checks_not_invoked(self):
diff --git a/test-requirements.txt b/test-requirements.txt
index 17fa9f1..bd4d772 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,8 +1,4 @@
-# The order of packages is significant, because pip processes them in the order
-# of appearance. Changing the order has an impact on the overall integration
-# process, which may cause wedges in the gate later.
-hacking>=3.0.1,<3.1.0;python_version>='3.5' # Apache-2.0
+hacking>=6.1.0,<6.2.0
 coverage!=4.4,>=4.0 # Apache-2.0
 oslotest>=3.2.0 # Apache-2.0
-pycodestyle>=2.0.0,<2.6.0 # MIT
-flake8-import-order==0.11 # LGPLv3
+flake8-import-order>=0.18.0,<0.19.0 # LGPLv3
diff --git a/tox.ini b/tox.ini
index e3c8fcf..d9d2bad 100644
--- a/tox.ini
+++ b/tox.ini
@@ -411,7 +411,8 @@
 # E129 skipped because it is too limiting when combined with other rules
 # W504 skipped because it is overeager and unnecessary
 # H405 skipped because it arbitrarily forces doctring "title" lines
-ignore = E125,E123,E129,W504,H405
+# I201 and I202 skipped because the rule does not allow new line between 3rd party modules and own modules
+ignore = E125,E123,E129,W504,H405,I201,I202,T117
 show-source = True
 exclude = .git,.venv,.tox,dist,doc,*egg,build
 enable-extensions = H106,H203,H904
diff --git a/zuul.d/integrated-gate.yaml b/zuul.d/integrated-gate.yaml
index 56c65c0..fb08297 100644
--- a/zuul.d/integrated-gate.yaml
+++ b/zuul.d/integrated-gate.yaml
@@ -330,6 +330,7 @@
       devstack_localrc:
         CINDER_ENABLED_BACKENDS: lvm:lvmdriver-1,lvm:lvmdriver-2
         ENABLE_VOLUME_MULTIATTACH: true
+        GLANCE_ENFORCE_IMAGE_FORMAT: false
       devstack_plugins:
         neutron: https://opendev.org/openstack/neutron
       devstack_services:
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index e284487..a7641a6 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -8,10 +8,10 @@
     check:
       jobs:
         - openstack-tox-pep8
-        - openstack-tox-py38
         - openstack-tox-py39
         - openstack-tox-py310
         - openstack-tox-py311
+        - openstack-tox-py312
         - tempest-full-py3:
             # Define list of irrelevant files to use everywhere else
             irrelevant-files: &tempest-irrelevant-files
@@ -37,9 +37,9 @@
         # if things are working in latest and oldest it will work in between
         # stable branches also. If anything is breaking we will be catching
         # those in respective stable branch gate.
-        - tempest-full-2024-1:
+        - tempest-full-2024-2:
             irrelevant-files: *tempest-irrelevant-files
-        - tempest-full-2023-1:
+        - tempest-full-2023-2:
             irrelevant-files: *tempest-irrelevant-files
         - tempest-multinode-full-py3:
             irrelevant-files: *tempest-irrelevant-files
@@ -126,10 +126,10 @@
     gate:
       jobs:
         - openstack-tox-pep8
-        - openstack-tox-py38
         - openstack-tox-py39
         - openstack-tox-py310
         - openstack-tox-py311
+        - openstack-tox-py312
         - tempest-slow-py3:
             irrelevant-files: *tempest-irrelevant-files
         - neutron-ovs-grenade-multinode:
@@ -185,15 +185,15 @@
             irrelevant-files: *tempest-irrelevant-files
     periodic-stable:
       jobs:
+        - tempest-full-2024-2
         - tempest-full-2024-1
         - tempest-full-2023-2
-        - tempest-full-2023-1
+        - tempest-slow-2024-2
         - tempest-slow-2024-1
         - tempest-slow-2023-2
-        - tempest-slow-2023-1
+        - tempest-full-2024-2-extra-tests
         - tempest-full-2024-1-extra-tests
         - tempest-full-2023-2-extra-tests
-        - tempest-full-2023-1-extra-tests
     periodic:
       jobs:
         - tempest-all
diff --git a/zuul.d/stable-jobs.yaml b/zuul.d/stable-jobs.yaml
index cb1330f..efa771e 100644
--- a/zuul.d/stable-jobs.yaml
+++ b/zuul.d/stable-jobs.yaml
@@ -1,5 +1,11 @@
 # NOTE(gmann): This file includes all stable release jobs definition.
 - job:
+    name: tempest-full-2024-2
+    parent: tempest-full-py3
+    nodeset: openstack-single-node-jammy
+    override-checkout: stable/2024.2
+
+- job:
     name: tempest-full-2024-1
     parent: tempest-full-py3
     nodeset: openstack-single-node-jammy
@@ -12,10 +18,10 @@
     override-checkout: stable/2023.2
 
 - job:
-    name: tempest-full-2023-1
-    parent: tempest-full-py3
+    name: tempest-full-2024-2-extra-tests
+    parent: tempest-extra-tests
     nodeset: openstack-single-node-jammy
-    override-checkout: stable/2023.1
+    override-checkout: stable/2024.2
 
 - job:
     name: tempest-full-2024-1-extra-tests
@@ -30,10 +36,10 @@
     override-checkout: stable/2023.2
 
 - job:
-    name: tempest-full-2023-1-extra-tests
-    parent: tempest-extra-tests
-    nodeset: openstack-single-node-jammy
-    override-checkout: stable/2023.1
+    name: tempest-slow-2024-2
+    parent: tempest-slow-py3
+    nodeset: openstack-two-node-jammy
+    override-checkout: stable/2024.2
 
 - job:
     name: tempest-slow-2024-1
@@ -48,19 +54,14 @@
     override-checkout: stable/2023.2
 
 - job:
-    name: tempest-slow-2023-1
-    parent: tempest-slow-py3
-    nodeset: openstack-two-node-jammy
-    override-checkout: stable/2023.1
-
-- job:
     name: tempest-full-py3
     parent: devstack-tempest
     # This job version is to use the 'full' tox env which
-    # is available for stable/ussuri to stable/wallaby also.
+    # is available for unmaintained/victoria to unmaintained/xena also.
     branches:
       - ^.*/victoria
       - ^.*/wallaby
+      - ^.*/xena
     description: |
       Base integration test with Neutron networking, horizon, swift enable,
       and py3.
@@ -71,6 +72,10 @@
       - openstack/horizon
     vars:
       tox_envlist: full
+      tempest_exclude_regex: "\
+          (DHCPAgentSchedulersTestJSON)|\
+          (AttachVolumeMultiAttachTest)|\
+          (UpdateMultiattachVolumeNegativeTest)"
       devstack_localrc:
         USE_PYTHON3: true
         FORCE_CONFIG_DRIVE: true
@@ -109,11 +114,30 @@
     name: tempest-multinode-full
     parent: tempest-multinode-full-base
     nodeset: openstack-two-node-focal
-    # This job runs on Focal and on python2. This is for unmaintained/victoria to unmaintained/zed.
+    # This job runs on Focal and on python2. This is for unmaintained/victoria to unmaintained/xena.
     branches:
       - ^.*/victoria
       - ^.*/wallaby
       - ^.*/xena
+    vars:
+      tox_envlist: full
+      tempest_exclude_regex: "\
+          (DHCPAgentSchedulersTestJSON)|\
+          (AttachVolumeMultiAttachTest)|\
+          (UpdateMultiattachVolumeNegativeTest)"
+      devstack_localrc:
+        USE_PYTHON3: False
+    group-vars:
+      subnode:
+        devstack_localrc:
+          USE_PYTHON3: False
+
+- job:
+    name: tempest-multinode-full
+    parent: tempest-multinode-full-base
+    nodeset: openstack-two-node-focal
+    # This job runs on Focal and on python2. This is for unmaintained/yoga to unmaintained/zed.
+    branches:
       - ^.*/yoga
       - ^.*/zed
     vars: