Merge "API for list group types by parameter sort/public"
diff --git a/HACKING.rst b/HACKING.rst
index 95bcbb5..dc28e4e 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -26,6 +26,7 @@
 - [T116] Unsupported 'message' Exception attribute in PY3
 - [T117] Check negative tests have ``@decorators.attr(type=['negative'])``
   applied.
+- [T118] LOG.warn is deprecated. Enforce use of LOG.warning.
 
 It is recommended to use ``tox -eautopep8`` before submitting a patch.
 
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index ecf2930..20ace9e 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -374,6 +374,10 @@
 
   .. _2.42: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-ocata
 
+  * `2.45`_
+
+  .. _2.45: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id41
+
   * `2.47`_
 
   .. _2.47: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id43
@@ -386,6 +390,10 @@
 
   .. _2.49: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id45
 
+  * `2.50`_
+
+  .. _2.50: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id46
+
   * `2.53`_
 
   .. _2.53: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-pike
@@ -418,6 +426,10 @@
 
   .. _2.63: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id58
 
+  * `2.64`_
+
+  .. _2.64: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id59
+
   * `2.70`_
 
   .. _2.70: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id64
@@ -430,6 +442,10 @@
 
   .. _2.73: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id66
 
+  * `2.75`_
+
+  .. _2.75: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id68
+
   * `2.79`_
 
   .. _2.79: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-train
diff --git a/releasenotes/notes/tempest-yoga-release-66e8484b9a402e9f.yaml b/releasenotes/notes/tempest-yoga-release-66e8484b9a402e9f.yaml
new file mode 100644
index 0000000..e41e31d
--- /dev/null
+++ b/releasenotes/notes/tempest-yoga-release-66e8484b9a402e9f.yaml
@@ -0,0 +1,18 @@
+---
+prelude: |
+    This release is to tag Tempest for OpenStack Yoga release.
+    This release marks the start of Yoga release support in Tempest.
+    After this release, Tempest will support below OpenStack Releases:
+
+    * Yoga
+    * Xena
+    * Wallaby
+    * Victoria
+    * Ussuri
+
+    Current development of Tempest is for OpenStack Zed development
+    cycle. Every Tempest commit is also tested against master during
+    the Zed cycle. However, this does not necessarily mean that using
+    Tempest as of this tag will work against a Zed (or future release)
+    cloud.
+    To be on safe side, use this tag to test the OpenStack Yoga release.
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
index 6a1f8b4..3bff257 100644
--- a/releasenotes/source/index.rst
+++ b/releasenotes/source/index.rst
@@ -6,6 +6,8 @@
    :maxdepth: 1
 
    unreleased
+   v29.2.0
+   v29.1.0
    v29.0.0
    v28.1.0
    v28.0.0
diff --git a/releasenotes/source/v29.1.0.rst b/releasenotes/source/v29.1.0.rst
new file mode 100644
index 0000000..f8780fd
--- /dev/null
+++ b/releasenotes/source/v29.1.0.rst
@@ -0,0 +1,5 @@
+=====================
+v29.1.0 Release Notes
+=====================
+.. release-notes:: 29.1.0 Release Notes
+   :version: 29.1.0
diff --git a/releasenotes/source/v29.2.0.rst b/releasenotes/source/v29.2.0.rst
new file mode 100644
index 0000000..4f2f2b2
--- /dev/null
+++ b/releasenotes/source/v29.2.0.rst
@@ -0,0 +1,5 @@
+=====================
+v29.2.0 Release Notes
+=====================
+.. release-notes:: 29.2.0 Release Notes
+   :version: 29.2.0
diff --git a/tempest/api/compute/admin/test_quotas.py b/tempest/api/compute/admin/test_quotas.py
index 9d5e0c9..caf4fc1 100644
--- a/tempest/api/compute/admin/test_quotas.py
+++ b/tempest/api/compute/admin/test_quotas.py
@@ -14,14 +14,17 @@
 #    under the License.
 
 from oslo_log import log as logging
+import testtools
 from testtools import matchers
 
 from tempest.api.compute import base
 from tempest.common import identity
 from tempest.common import tempest_fixtures as fixtures
+from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 
+CONF = config.CONF
 LOG = logging.getLogger(__name__)
 
 
@@ -110,6 +113,8 @@
             self.assertIn(quota, quota_set.keys())
 
     @decorators.idempotent_id('55fbe2bf-21a9-435b-bbd2-4162b0ed799a')
+    @testtools.skipIf(CONF.compute_feature_enabled.unified_limits,
+                      'Legacy quota update not available with unified limits')
     def test_update_all_quota_resources_for_tenant(self):
         """Test admin can update all the compute quota limits for a project"""
         default_quota_set = self.adm_client.show_default_quota_set(
@@ -141,11 +146,15 @@
 
     # TODO(afazekas): merge these test cases
     @decorators.idempotent_id('ce9e0815-8091-4abd-8345-7fe5b85faa1d')
+    @testtools.skipIf(CONF.compute_feature_enabled.unified_limits,
+                      'Legacy quota update not available with unified limits')
     def test_get_updated_quotas(self):
         """Test that GET shows the updated quota set of project"""
         self._get_updated_quotas()
 
     @decorators.idempotent_id('389d04f0-3a41-405f-9317-e5f86e3c44f0')
+    @testtools.skipIf(CONF.compute_feature_enabled.unified_limits,
+                      'Legacy quota update not available with unified limits')
     def test_delete_quota(self):
         """Test admin can delete the compute quota set for a project"""
         project_name = data_utils.rand_name('ram_quota_project')
@@ -178,6 +187,8 @@
     min_microversion = '2.36'
 
     @decorators.idempotent_id('4268b5c9-92e5-4adc-acf1-3a2798f3d803')
+    @testtools.skipIf(CONF.compute_feature_enabled.unified_limits,
+                      'Legacy quota update not available with unified limits')
     def test_get_updated_quotas(self):
         """Test compute quotas API with microversion greater than 2.35
 
@@ -197,6 +208,8 @@
     min_microversion = '2.57'
 
     @decorators.idempotent_id('e641e6c6-e86c-41a4-9e5c-9493c0ae47ad')
+    @testtools.skipIf(CONF.compute_feature_enabled.unified_limits,
+                      'Legacy quota update not available with unified limits')
     def test_get_updated_quotas(self):
         """Test compute quotas API with microversion greater than 2.56
 
@@ -228,6 +241,8 @@
     # tests that get run all by themselves at the end under a
     # 'danger' flag.
     @decorators.idempotent_id('7932ab0f-5136-4075-b201-c0e2338df51a')
+    @testtools.skipIf(CONF.compute_feature_enabled.unified_limits,
+                      'Legacy quota update not available with unified limits')
     def test_update_default_quotas(self):
         """Test updating default compute quota class set"""
         # get the current 'default' quota class values
@@ -253,3 +268,14 @@
             'default')['quota_class_set']
         self.assertThat(show_body.items(),
                         matchers.ContainsAll(body.items()))
+
+
+class QuotaClassesAdmin257Test(QuotaClassesAdminTestJSON):
+    """Test compute quotas with microversion greater than 2.56
+
+    # NOTE(gmann): This test tests the Quota class APIs response schema
+    # for 2.57 microversion. No specific assert or behaviour verification
+    # is needed.
+    """
+
+    min_microversion = '2.57'
diff --git a/tempest/api/compute/admin/test_quotas_negative.py b/tempest/api/compute/admin/test_quotas_negative.py
index 04dbc2d..a4120bb 100644
--- a/tempest/api/compute/admin/test_quotas_negative.py
+++ b/tempest/api/compute/admin/test_quotas_negative.py
@@ -12,6 +12,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import testtools
+
 from tempest.api.compute import base
 from tempest.common import utils
 from tempest import config
@@ -68,6 +70,8 @@
     # It can be moved into the setUpClass as well.
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('91058876-9947-4807-9f22-f6eb17140d9b')
+    @testtools.skipIf(CONF.compute_feature_enabled.unified_limits,
+                      'Legacy quota update not available with unified limits')
     def test_create_server_when_cpu_quota_is_full(self):
         """Disallow server creation when tenant's vcpu quota is full"""
         self._update_quota('cores', 0)
@@ -76,6 +80,8 @@
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('6fdd7012-584d-4327-a61c-49122e0d5864')
+    @testtools.skipIf(CONF.compute_feature_enabled.unified_limits,
+                      'Legacy quota update not available with unified limits')
     def test_create_server_when_memory_quota_is_full(self):
         """Disallow server creation when tenant's memory quota is full"""
         self._update_quota('ram', 0)
@@ -84,6 +90,8 @@
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('7c6be468-0274-449a-81c3-ac1c32ee0161')
+    @testtools.skipIf(CONF.compute_feature_enabled.unified_limits,
+                      'Legacy quota update not available with unified limits')
     def test_create_server_when_instances_quota_is_full(self):
         """Once instances quota limit is reached, disallow server creation"""
         self._update_quota('instances', 0)
diff --git a/tempest/api/compute/admin/test_servers.py b/tempest/api/compute/admin/test_servers.py
index ab1b49a..bc00f8c 100644
--- a/tempest/api/compute/admin/test_servers.py
+++ b/tempest/api/compute/admin/test_servers.py
@@ -223,3 +223,32 @@
         }
         self.create_test_server(scheduler_hints=hints,
                                 wait_until='ACTIVE')
+
+
+class ServersAdmin275Test(base.BaseV2ComputeAdminTest):
+    """Test compute server with microversion greater than 2.75
+
+    # NOTE(gmann): This test tests the Server APIs response schema
+    # for 2.75 microversion. No specific assert or behaviour verification
+    # is needed.
+    """
+
+    min_microversion = '2.75'
+
+    @decorators.idempotent_id('bf2b4a00-73a3-4d53-81fa-acbcd97d6339')
+    def test_rebuild_update_server_275(self):
+        server = self.create_test_server()
+        # Checking update response schema.
+        self.servers_client.update_server(server['id'])
+        waiters.wait_for_server_status(self.servers_client, server['id'],
+                                       'ACTIVE')
+        # Checking rebuild API response schema
+        self.servers_client.rebuild_server(server['id'], self.image_ref_alt)
+        waiters.wait_for_server_status(self.servers_client,
+                                       server['id'], 'ACTIVE')
+        # Checking rebuild server with admin response schema.
+        self.os_admin.servers_client.rebuild_server(
+            server['id'], self.image_ref)
+        self.addCleanup(waiters.wait_for_server_status,
+                        self.os_admin.servers_client,
+                        server['id'], 'ACTIVE')
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index a110eb4..e16afaf 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -306,10 +306,18 @@
     def create_test_server_group(cls, name="", policy=None):
         if not name:
             name = data_utils.rand_name(cls.__name__ + "-Server-Group")
-        if policy is None:
-            policy = ['affinity']
+        if cls.is_requested_microversion_compatible('2.63'):
+            policy = policy or ['affinity']
+            if not isinstance(policy, list):
+                policy = [policy]
+            kwargs = {'policies': policy}
+        else:
+            policy = policy or 'affinity'
+            if isinstance(policy, list):
+                policy = policy[0]
+            kwargs = {'policy': policy}
         body = cls.server_groups_client.create_server_group(
-            name=name, policies=policy)['server_group']
+            name=name, **kwargs)['server_group']
         cls.addClassResourceCleanup(
             test_utils.call_and_ignore_notfound_exc,
             cls.server_groups_client.delete_server_group,
diff --git a/tempest/api/compute/servers/test_device_tagging.py b/tempest/api/compute/servers/test_device_tagging.py
index f5c9080..d099fce 100644
--- a/tempest/api/compute/servers/test_device_tagging.py
+++ b/tempest/api/compute/servers/test_device_tagging.py
@@ -212,7 +212,7 @@
 
         server = self.create_test_server(
             validatable=True,
-            wait_until='ACTIVE',
+            wait_until='SSHABLE',
             validation_resources=validation_resources,
             config_drive=config_drive_enabled,
             name=data_utils.rand_name('device-tagging-server'),
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index c415c00..870c6f5 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -792,3 +792,28 @@
         self.assertEqual('novnc', body['type'])
         self.assertNotEqual('', body['url'])
         self._validate_url(body['url'])
+
+
+class ServersAaction247Test(base.BaseV2ComputeTest):
+    """Test compute server with microversion greater than 2.47
+
+    # NOTE(gmann): This test tests the Server create backup APIs
+    # response schema for 2.47 microversion. No specific assert
+    # or behaviour verification is needed.
+    """
+
+    min_microversion = '2.47'
+
+    @testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
+                          'Snapshotting not available, backup not possible.')
+    @utils.services('image')
+    @decorators.idempotent_id('252a4bdd-6366-4dae-9994-8c30aa660f23')
+    def test_create_backup(self):
+        server = self.create_test_server(wait_until='ACTIVE')
+
+        backup1 = data_utils.rand_name('backup-1')
+        # Just check create_back to verify the schema with 2.47
+        self.servers_client.create_backup(server['id'],
+                                          backup_type='daily',
+                                          rotation=2,
+                                          name=backup1)
diff --git a/tempest/api/compute/servers/test_server_group.py b/tempest/api/compute/servers/test_server_group.py
index 4c0d021..4811a7b 100644
--- a/tempest/api/compute/servers/test_server_group.py
+++ b/tempest/api/compute/servers/test_server_group.py
@@ -44,9 +44,21 @@
         cls.client = cls.server_groups_client
 
     @classmethod
+    def _set_policy(cls, policy):
+        if not cls.is_requested_microversion_compatible('2.63'):
+            return policy[0]
+        else:
+            return policy
+
+    @classmethod
     def resource_setup(cls):
         super(ServerGroupTestJSON, cls).resource_setup()
-        cls.policy = ['affinity']
+        if cls.is_requested_microversion_compatible('2.63'):
+            cls.policy_field = 'policies'
+            cls.policy = ['affinity']
+        else:
+            cls.policy_field = 'policy'
+            cls.policy = 'affinity'
 
     def setUp(self):
         super(ServerGroupTestJSON, self).setUp()
@@ -61,9 +73,9 @@
 
     def _create_server_group(self, name, policy):
         # create the test server-group with given policy
-        server_group = {'name': name, 'policies': policy}
+        server_group = {'name': name, self.policy_field: policy}
         body = self.create_test_server_group(name, policy)
-        for key in ['name', 'policies']:
+        for key in ['name', self.policy_field]:
             self.assertEqual(server_group[key], body[key])
         return body
 
@@ -88,7 +100,7 @@
     @decorators.idempotent_id('3645a102-372f-4140-afad-13698d850d23')
     def test_create_delete_server_group_with_anti_affinity_policy(self):
         """Test Create/Delete the server-group with anti-affinity policy"""
-        policy = ['anti-affinity']
+        policy = self._set_policy(['anti-affinity'])
         self._create_delete_server_group(policy)
 
     @decorators.idempotent_id('154dc5a4-a2fe-44b5-b99e-f15806a4a113')
@@ -99,7 +111,7 @@
         for _ in range(0, 2):
             server_groups.append(self._create_server_group(server_group_name,
                                                            self.policy))
-        for key in ['name', 'policies']:
+        for key in ['name', self.policy_field]:
             self.assertEqual(server_groups[0][key], server_groups[1][key])
         self.assertNotEqual(server_groups[0]['id'], server_groups[1]['id'])
 
@@ -134,3 +146,24 @@
         server_group = (self.server_groups_client.show_server_group(
             self.created_server_group['id'])['server_group'])
         self.assertIn(server['id'], server_group['members'])
+
+
+class ServerGroup264TestJSON(base.BaseV2ComputeTest):
+    """These tests check for the server-group APIs 2.64 microversion.
+
+    This tests is only to verify the POST, GET server-groups APIs response
+    schema with 2.64 microversion
+    """
+    create_default_network = True
+    min_microversion = '2.64'
+
+    @decorators.idempotent_id('b52f09dd-2133-4037-9a5d-bdb260096a88')
+    def test_create_get_server_group(self):
+        # create, get the test server-group with given policy
+        server_group = self.create_test_server_group(
+            name='server-group', policy='affinity')
+        self.addCleanup(
+            self.server_groups_client.delete_server_group,
+            server_group['id'])
+        self.server_groups_client.list_server_groups()
+        self.server_groups_client.show_server_group(server_group['id'])
diff --git a/tempest/api/compute/volumes/test_attach_volume.py b/tempest/api/compute/volumes/test_attach_volume.py
index 4c7c234..e4ec209 100644
--- a/tempest/api/compute/volumes/test_attach_volume.py
+++ b/tempest/api/compute/volumes/test_attach_volume.py
@@ -49,7 +49,7 @@
         server = self.create_test_server(
             validatable=True,
             validation_resources=validation_resources,
-            wait_until='ACTIVE',
+            wait_until='SSHABLE',
             adminPass=self.image_ssh_password)
         self.addCleanup(self.delete_server, server['id'])
         # Record addresses so that we can ssh later
diff --git a/tempest/api/compute/volumes/test_attach_volume_negative.py b/tempest/api/compute/volumes/test_attach_volume_negative.py
index 516f599..43b4bf5 100644
--- a/tempest/api/compute/volumes/test_attach_volume_negative.py
+++ b/tempest/api/compute/volumes/test_attach_volume_negative.py
@@ -12,7 +12,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.api.compute import base
+from tempest.api.compute.volumes import test_attach_volume
 from tempest import config
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
@@ -20,24 +20,15 @@
 CONF = config.CONF
 
 
-class AttachVolumeNegativeTest(base.BaseV2ComputeTest):
+class AttachVolumeNegativeTest(test_attach_volume.BaseAttachVolumeTest):
     """Negative tests of volume attaching"""
 
-    create_default_network = True
-
-    @classmethod
-    def skip_checks(cls):
-        super(AttachVolumeNegativeTest, cls).skip_checks()
-        if not CONF.service_available.cinder:
-            skip_msg = ("%s skipped as Cinder is not available" % cls.__name__)
-            raise cls.skipException(skip_msg)
-
     @decorators.attr(type=['negative'])
     @decorators.related_bug('1630783', status_code=500)
     @decorators.idempotent_id('a313b5cd-fbd0-49cc-94de-870e99f763c7')
     def test_delete_attached_volume(self):
         """Test deleting attachemd volume should fail"""
-        server = self.create_test_server(wait_until='ACTIVE')
+        server, validation_resources = self._create_server()
         volume = self.create_volume()
         self.attach_volume(server, volume)
 
@@ -54,7 +45,7 @@
         depending on whether or not cinder v3.27 is being used to attach
         the volume to the instance.
         """
-        server = self.create_test_server(wait_until='ACTIVE')
+        server, validation_resources = self._create_server()
         volume = self.create_volume()
 
         self.attach_volume(server, volume)
@@ -66,12 +57,12 @@
     @decorators.idempotent_id('ee37a796-2afb-11e7-bc0f-fa163e65f5ce')
     def test_attach_attached_volume_to_different_server(self):
         """Test attaching attached volume to different server should fail"""
-        server1 = self.create_test_server(wait_until='ACTIVE')
+        server1, validation_resources = self._create_server()
         volume = self.create_volume()
 
         self.attach_volume(server1, volume)
 
         # Create server2 and attach in-use volume
-        server2 = self.create_test_server(wait_until='ACTIVE')
+        server2, validation_resources = self._create_server()
         self.assertRaises(lib_exc.BadRequest,
                           self.attach_volume, server2, volume)
diff --git a/tempest/cmd/verify_tempest_config.py b/tempest/cmd/verify_tempest_config.py
index 0db1ab1..421afd3 100644
--- a/tempest/cmd/verify_tempest_config.py
+++ b/tempest/cmd/verify_tempest_config.py
@@ -130,7 +130,7 @@
             msg = ('Glance is available in the catalog, but no known version, '
                    '(v1.x or v2.x) of Glance could be found, so Glance should '
                    'be configured as not available')
-            LOG.warn(msg)
+            LOG.warning(msg)
             print_and_or_update('glance', 'service-available', False, update)
             return
 
diff --git a/tempest/common/compute.py b/tempest/common/compute.py
index d34cd6d..43e30ad 100644
--- a/tempest/common/compute.py
+++ b/tempest/common/compute.py
@@ -23,6 +23,7 @@
 from oslo_log import log as logging
 from oslo_utils import excutils
 
+from tempest.common.utils.linux import remote_client
 from tempest.common import waiters
 from tempest import config
 from tempest import exceptions
@@ -98,7 +99,9 @@
         server. Include a keypair, a security group and an IP.
     :param tenant_network: Tenant network to be used for creating a server.
     :param wait_until: Server status to wait for the server to reach after
-        its creation.
+        its creation. Additionally PINGABLE and SSHABLE states are also
+        accepted when the server is both validatable and has the required
+        validation_resources provided.
     :param volume_backed: Whether the server is volume backed or not.
         If this is true, a volume will be created and create server will be
         requested with 'block_device_mapping_v2' populated with below values:
@@ -125,8 +128,6 @@
     :returns: a tuple
     """
 
-    # TODO(jlanoux) add support of wait_until PINGABLE/SSHABLE
-
     if name is None:
         name = data_utils.rand_name(__name__ + "-instance")
     if flavor is None:
@@ -259,18 +260,50 @@
                 server_id=servers[0]['id'])
 
     if wait_until:
+
+        # NOTE(lyarwood): PINGABLE and SSHABLE both require the instance to
+        # go ACTIVE initially before we can setup the fip(s) etc so stash
+        # this additional wait state for later use.
+        wait_until_extra = None
+        if wait_until in ['PINGABLE', 'SSHABLE']:
+            wait_until_extra = wait_until
+            wait_until = 'ACTIVE'
+
         for server in servers:
             try:
                 waiters.wait_for_server_status(
                     clients.servers_client, server['id'], wait_until,
                     request_id=request_id)
 
-                # Multiple validatable servers are not supported for now. Their
-                # creation will fail with the condition above.
                 if CONF.validation.run_validation and validatable:
+
                     if CONF.validation.connect_method == 'floating':
                         _setup_validation_fip()
 
+                    server_ip = get_server_ip(
+                        server, validation_resources=validation_resources)
+
+                    if wait_until_extra == 'PINGABLE':
+                        waiters.wait_for_ping(
+                            server_ip,
+                            clients.servers_client.build_timeout,
+                            clients.servers_client.build_interval
+                        )
+
+                    if wait_until_extra == 'SSHABLE':
+                        pkey = validation_resources['keypair']['private_key']
+                        ssh_client = remote_client.RemoteClient(
+                            server_ip,
+                            CONF.validation.image_ssh_user,
+                            pkey=pkey,
+                            server=server,
+                            servers_client=clients.servers_client
+                        )
+                        waiters.wait_for_ssh(
+                            ssh_client,
+                            clients.servers_client.build_timeout
+                        )
+
             except Exception:
                 with excutils.save_and_reraise_exception():
                     for server in servers:
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index fbc8698..ab401fb 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -10,6 +10,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import os
 import re
 import time
 
@@ -570,3 +571,26 @@
                        'in time.' % (floating_ip, server['id']))
             raise lib_exc.TimeoutException(msg)
         time.sleep(servers_client.build_interval)
+
+
+def wait_for_ping(server_ip, timeout=30, interval=1):
+    """Waits for an address to become pingable"""
+    start_time = int(time.time())
+    while int(time.time()) - start_time < timeout:
+        response = os.system("ping -c 1 " + server_ip)
+        if response == 0:
+            return
+        time.sleep(interval)
+    raise lib_exc.TimeoutException()
+
+
+def wait_for_ssh(ssh_client, timeout=30):
+    """Waits for SSH connection to become usable"""
+    start_time = int(time.time())
+    while int(time.time()) - start_time < timeout:
+        try:
+            ssh_client.validate_authentication()
+            return
+        except lib_exc.SSHTimeout:
+            pass
+    raise lib_exc.TimeoutException()
diff --git a/tempest/config.py b/tempest/config.py
index 03ddbf5..0f509fb 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -653,6 +653,9 @@
                 default=True,
                 help='Does the test environment support attaching devices '
                      'using an IDE bus to the instance?'),
+    cfg.BoolOpt('unified_limits',
+                default=False,
+                help='Does the test environment support unified limits?'),
 ]
 
 
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index c1e6b2d..1c9c55b 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -318,3 +318,16 @@
                        " to all negative API tests"
                 )
             _HAVE_NEGATIVE_DECORATOR = False
+
+
+@core.flake8ext
+def no_log_warn(logical_line):
+    """Disallow 'LOG.warn('
+
+    Use LOG.warning() instead of Deprecated LOG.warn().
+    https://docs.python.org/3/library/logging.html#logging.warning
+    """
+
+    msg = ("T118: LOG.warn is deprecated, please use LOG.warning!")
+    if "LOG.warn(" in logical_line:
+        yield (0, msg)
diff --git a/tempest/lib/api_schema/response/compute/v2_1/servers.py b/tempest/lib/api_schema/response/compute/v2_1/servers.py
index 3300298..bd42afd 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/servers.py
@@ -506,6 +506,10 @@
     }
 }
 
+create_backup = {
+    'status_code': [202]
+}
+
 server_actions_common_schema = {
     'status_code': [202]
 }
diff --git a/tempest/lib/api_schema/response/compute/v2_16/servers.py b/tempest/lib/api_schema/response/compute/v2_16/servers.py
index dcd64cf..2b3ce38 100644
--- a/tempest/lib/api_schema/response/compute/v2_16/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_16/servers.py
@@ -172,3 +172,4 @@
 show_volume_attachment = copy.deepcopy(servers.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers.show_instance_action)
+create_backup = copy.deepcopy(servers.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_19/servers.py b/tempest/lib/api_schema/response/compute/v2_19/servers.py
index 0e4bd5c..ba3d787 100644
--- a/tempest/lib/api_schema/response/compute/v2_19/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_19/servers.py
@@ -62,3 +62,4 @@
 show_volume_attachment = copy.deepcopy(serversv216.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(serversv216.list_volume_attachments)
 show_instance_action = copy.deepcopy(serversv216.show_instance_action)
+create_backup = copy.deepcopy(serversv216.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_26/servers.py b/tempest/lib/api_schema/response/compute/v2_26/servers.py
index 74c08f1..123eb72 100644
--- a/tempest/lib/api_schema/response/compute/v2_26/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_26/servers.py
@@ -105,3 +105,4 @@
 show_volume_attachment = copy.deepcopy(servers219.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers219.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers219.show_instance_action)
+create_backup = copy.deepcopy(servers219.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_3/servers.py b/tempest/lib/api_schema/response/compute/v2_3/servers.py
index 435e3ac..d19f1ad 100644
--- a/tempest/lib/api_schema/response/compute/v2_3/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_3/servers.py
@@ -177,3 +177,4 @@
 show_volume_attachment = copy.deepcopy(servers.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers.show_instance_action)
+create_backup = copy.deepcopy(servers.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_45/servers.py b/tempest/lib/api_schema/response/compute/v2_45/servers.py
new file mode 100644
index 0000000..cb0fc13
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_45/servers.py
@@ -0,0 +1,49 @@
+#    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_26 import servers as servers226
+
+create_backup = {
+    'status_code': [202],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'image_id': {'type': 'string', 'format': 'uuid'}
+        },
+        'additionalProperties': False,
+        'required': ['image_id']
+    }
+}
+# NOTE(gmann): 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.26 ***
+get_server = copy.deepcopy(servers226.get_server)
+list_servers_detail = copy.deepcopy(servers226.list_servers_detail)
+update_server = copy.deepcopy(servers226.update_server)
+rebuild_server = copy.deepcopy(servers226.rebuild_server)
+rebuild_server_with_admin_pass = copy.deepcopy(
+    servers226.rebuild_server_with_admin_pass)
+show_server_diagnostics = copy.deepcopy(servers226.show_server_diagnostics)
+get_remote_consoles = copy.deepcopy(servers226.get_remote_consoles)
+list_tags = copy.deepcopy(servers226.list_tags)
+update_all_tags = copy.deepcopy(servers226.update_all_tags)
+delete_all_tags = copy.deepcopy(servers226.delete_all_tags)
+check_tag_existence = copy.deepcopy(servers226.check_tag_existence)
+update_tag = copy.deepcopy(servers226.update_tag)
+delete_tag = copy.deepcopy(servers226.delete_tag)
+list_servers = copy.deepcopy(servers226.list_servers)
+attach_volume = copy.deepcopy(servers226.attach_volume)
+show_volume_attachment = copy.deepcopy(servers226.show_volume_attachment)
+list_volume_attachments = copy.deepcopy(servers226.list_volume_attachments)
diff --git a/tempest/lib/api_schema/response/compute/v2_47/servers.py b/tempest/lib/api_schema/response/compute/v2_47/servers.py
index 7050602..1399c2d 100644
--- a/tempest/lib/api_schema/response/compute/v2_47/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_47/servers.py
@@ -13,6 +13,7 @@
 import copy
 
 from tempest.lib.api_schema.response.compute.v2_26 import servers as servers226
+from tempest.lib.api_schema.response.compute.v2_45 import servers as servers245
 
 flavor = {
     'type': 'object',
@@ -34,39 +35,40 @@
     'required': ['original_name', 'disk', 'ephemeral', 'ram', 'swap', 'vcpus']
 }
 
-get_server = copy.deepcopy(servers226.get_server)
+get_server = copy.deepcopy(servers245.get_server)
 get_server['response_body']['properties']['server'][
     'properties'].update({'flavor': flavor})
-list_servers_detail = copy.deepcopy(servers226.list_servers_detail)
+list_servers_detail = copy.deepcopy(servers245.list_servers_detail)
 list_servers_detail['response_body']['properties']['servers']['items'][
     'properties'].update({'flavor': flavor})
 
-update_server = copy.deepcopy(servers226.update_server)
+update_server = copy.deepcopy(servers245.update_server)
 update_server['response_body']['properties']['server'][
     'properties'].update({'flavor': flavor})
 
-rebuild_server = copy.deepcopy(servers226.rebuild_server)
+rebuild_server = copy.deepcopy(servers245.rebuild_server)
 rebuild_server['response_body']['properties']['server'][
     'properties'].update({'flavor': flavor})
 
 rebuild_server_with_admin_pass = copy.deepcopy(
-    servers226.rebuild_server_with_admin_pass)
+    servers245.rebuild_server_with_admin_pass)
 rebuild_server_with_admin_pass['response_body']['properties']['server'][
     'properties'].update({'flavor': flavor})
 
 # 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.
-show_server_diagnostics = copy.deepcopy(servers226.show_server_diagnostics)
-get_remote_consoles = copy.deepcopy(servers226.get_remote_consoles)
-list_tags = copy.deepcopy(servers226.list_tags)
-update_all_tags = copy.deepcopy(servers226.update_all_tags)
-delete_all_tags = copy.deepcopy(servers226.delete_all_tags)
-check_tag_existence = copy.deepcopy(servers226.check_tag_existence)
-update_tag = copy.deepcopy(servers226.update_tag)
-delete_tag = copy.deepcopy(servers226.delete_tag)
-list_servers = copy.deepcopy(servers226.list_servers)
-attach_volume = copy.deepcopy(servers226.attach_volume)
-show_volume_attachment = copy.deepcopy(servers226.show_volume_attachment)
-list_volume_attachments = copy.deepcopy(servers226.list_volume_attachments)
+show_server_diagnostics = copy.deepcopy(servers245.show_server_diagnostics)
+get_remote_consoles = copy.deepcopy(servers245.get_remote_consoles)
+list_tags = copy.deepcopy(servers245.list_tags)
+update_all_tags = copy.deepcopy(servers245.update_all_tags)
+delete_all_tags = copy.deepcopy(servers245.delete_all_tags)
+check_tag_existence = copy.deepcopy(servers245.check_tag_existence)
+update_tag = copy.deepcopy(servers245.update_tag)
+delete_tag = copy.deepcopy(servers245.delete_tag)
+list_servers = copy.deepcopy(servers245.list_servers)
+attach_volume = copy.deepcopy(servers245.attach_volume)
+show_volume_attachment = copy.deepcopy(servers245.show_volume_attachment)
+list_volume_attachments = copy.deepcopy(servers245.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers226.show_instance_action)
+create_backup = copy.deepcopy(servers245.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_48/servers.py b/tempest/lib/api_schema/response/compute/v2_48/servers.py
index af6344b..5b53906 100644
--- a/tempest/lib/api_schema/response/compute/v2_48/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_48/servers.py
@@ -133,3 +133,4 @@
 show_volume_attachment = copy.deepcopy(servers247.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers247.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers247.show_instance_action)
+create_backup = copy.deepcopy(servers247.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_50/__init__.py b/tempest/lib/api_schema/response/compute/v2_50/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_50/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_50/quota_classes.py b/tempest/lib/api_schema/response/compute/v2_50/quota_classes.py
new file mode 100644
index 0000000..4ee845f
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_50/quota_classes.py
@@ -0,0 +1,48 @@
+# 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 quota_classes \
+    as quota_classesv21
+
+# Compute microversion 2.50:
+# 1. fixed_ips, floating_ips, security_group_rules and security_groups
+#    are removed from:
+#      * GET /os-quota-class-sets/{id}
+#      * PUT /os-quota-class-sets/{id}
+# 2. server_groups and server_group_members are added to:
+#      * GET /os-quota-class-sets/{id}
+#      * PUT /os-quota-class-sets/{id}
+
+get_quota_class_set = copy.deepcopy(quota_classesv21.get_quota_class_set)
+update_quota_class_set = copy.deepcopy(quota_classesv21.update_quota_class_set)
+for field in ['fixed_ips', 'floating_ips', 'security_group_rules',
+              'security_groups']:
+    get_quota_class_set['response_body']['properties']['quota_class_set'][
+        'properties'].pop(field, None)
+    get_quota_class_set['response_body']['properties']['quota_class_set'][
+        'required'].remove(field)
+    update_quota_class_set['response_body']['properties']['quota_class_set'][
+        'properties'].pop(field, None)
+    update_quota_class_set['response_body']['properties'][
+        'quota_class_set']['required'].remove(field)
+for field in ['server_groups', 'server_group_members']:
+    get_quota_class_set['response_body']['properties']['quota_class_set'][
+        'properties'].update({field: {'type': 'integer'}})
+    get_quota_class_set['response_body']['properties']['quota_class_set'][
+        'required'].append(field)
+    update_quota_class_set['response_body']['properties']['quota_class_set'][
+        'properties'].update({field: {'type': 'integer'}})
+    update_quota_class_set['response_body']['properties']['quota_class_set'][
+        'required'].append(field)
diff --git a/tempest/lib/api_schema/response/compute/v2_51/servers.py b/tempest/lib/api_schema/response/compute/v2_51/servers.py
index e603287..50d6aaa 100644
--- a/tempest/lib/api_schema/response/compute/v2_51/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_51/servers.py
@@ -40,3 +40,4 @@
 attach_volume = copy.deepcopy(servers248.attach_volume)
 show_volume_attachment = copy.deepcopy(servers248.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers248.list_volume_attachments)
+create_backup = copy.deepcopy(servers248.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_54/servers.py b/tempest/lib/api_schema/response/compute/v2_54/servers.py
index 135b381..9de3016 100644
--- a/tempest/lib/api_schema/response/compute/v2_54/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_54/servers.py
@@ -59,3 +59,4 @@
 show_volume_attachment = copy.deepcopy(servers251.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers251.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers251.show_instance_action)
+create_backup = copy.deepcopy(servers251.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_57/quota_classes.py b/tempest/lib/api_schema/response/compute/v2_57/quota_classes.py
new file mode 100644
index 0000000..396ed66
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_57/quota_classes.py
@@ -0,0 +1,37 @@
+# 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_50 import quota_classes \
+    as quota_classesv250
+
+# Compute microversion 2.57:
+# 1. injected_file_content_bytes, injected_file_path_bytes, injected_files
+#    are removed from:
+#      * GET /os-quota-class-sets/{id}
+#      * PUT /os-quota-class-sets/{id}
+
+get_quota_class_set = copy.deepcopy(quota_classesv250.get_quota_class_set)
+update_quota_class_set = copy.deepcopy(
+    quota_classesv250.update_quota_class_set)
+for field in ['injected_file_content_bytes', 'injected_file_path_bytes',
+              'injected_files']:
+    get_quota_class_set['response_body']['properties']['quota_class_set'][
+        'properties'].pop(field, None)
+    get_quota_class_set['response_body']['properties']['quota_class_set'][
+        'required'].remove(field)
+    update_quota_class_set['response_body']['properties']['quota_class_set'][
+        'properties'].pop(field, None)
+    update_quota_class_set['response_body']['properties'][
+        'quota_class_set']['required'].remove(field)
diff --git a/tempest/lib/api_schema/response/compute/v2_57/servers.py b/tempest/lib/api_schema/response/compute/v2_57/servers.py
index bdff74b..ee91391 100644
--- a/tempest/lib/api_schema/response/compute/v2_57/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_57/servers.py
@@ -63,3 +63,4 @@
 show_volume_attachment = copy.deepcopy(servers254.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers254.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers254.show_instance_action)
+create_backup = copy.deepcopy(servers254.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_58/servers.py b/tempest/lib/api_schema/response/compute/v2_58/servers.py
index 62239cf..637b765 100644
--- a/tempest/lib/api_schema/response/compute/v2_58/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_58/servers.py
@@ -42,3 +42,4 @@
 attach_volume = copy.deepcopy(servers257.attach_volume)
 show_volume_attachment = copy.deepcopy(servers257.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers257.list_volume_attachments)
+create_backup = copy.deepcopy(servers257.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_6/servers.py b/tempest/lib/api_schema/response/compute/v2_6/servers.py
index 6103b7c..e6b2c32 100644
--- a/tempest/lib/api_schema/response/compute/v2_6/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_6/servers.py
@@ -32,6 +32,7 @@
 show_volume_attachment = copy.deepcopy(servers.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers.show_instance_action)
+create_backup = copy.deepcopy(servers.create_backup)
 
 # NOTE: The consolidated remote console API got introduced with v2.6
 # with bp/consolidate-console-api. See Nova commit 578bafeda
diff --git a/tempest/lib/api_schema/response/compute/v2_62/servers.py b/tempest/lib/api_schema/response/compute/v2_62/servers.py
index 23eebbb..d761fe9 100644
--- a/tempest/lib/api_schema/response/compute/v2_62/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_62/servers.py
@@ -45,3 +45,4 @@
 attach_volume = copy.deepcopy(servers258.attach_volume)
 show_volume_attachment = copy.deepcopy(servers258.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers258.list_volume_attachments)
+create_backup = copy.deepcopy(servers258.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_63/servers.py b/tempest/lib/api_schema/response/compute/v2_63/servers.py
index db713b1..865b4fd 100644
--- a/tempest/lib/api_schema/response/compute/v2_63/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_63/servers.py
@@ -77,3 +77,4 @@
 show_volume_attachment = copy.deepcopy(servers262.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers262.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers262.show_instance_action)
+create_backup = copy.deepcopy(servers262.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_64/__init__.py b/tempest/lib/api_schema/response/compute/v2_64/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_64/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_64/server_groups.py b/tempest/lib/api_schema/response/compute/v2_64/server_groups.py
new file mode 100644
index 0000000..1402de5
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_64/server_groups.py
@@ -0,0 +1,56 @@
+# Copyright 2020 ZTE Corporation.  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_13 import server_groups as \
+    server_groupsv213
+
+# Compute microversion 2.64:
+# 1. change policies to policy in:
+#   * GET /os-server-groups
+#   * POST /os-server-groups
+#   * GET /os-server-groups/{server_group_id}
+# 2. add rules in:
+#   * GET /os-server-groups
+#   * POST /os-server-groups
+#   * GET /os-server-groups/{server_group_id}
+# 3. remove metadata from:
+#   * GET /os-server-groups
+#   * POST /os-server-groups
+#   * GET /os-server-groups/{server_group_id}
+
+common_server_group = copy.deepcopy(server_groupsv213.common_server_group)
+common_server_group['properties']['policy'] = {'type': 'string'}
+common_server_group['properties']['rules'] = {'type': 'object'}
+common_server_group['properties'].pop('policies')
+common_server_group['properties'].pop('metadata')
+common_server_group['required'].append('policy')
+common_server_group['required'].append('rules')
+common_server_group['required'].remove('policies')
+common_server_group['required'].remove('metadata')
+
+create_show_server_group = copy.deepcopy(
+    server_groupsv213.create_show_server_group)
+create_show_server_group['response_body']['properties'][
+    'server_group'] = common_server_group
+
+list_server_groups = copy.deepcopy(server_groupsv213.list_server_groups)
+list_server_groups['response_body']['properties']['server_groups'][
+    'items'] = common_server_group
+
+# 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.
+delete_server_group = copy.deepcopy(server_groupsv213.delete_server_group)
diff --git a/tempest/lib/api_schema/response/compute/v2_70/servers.py b/tempest/lib/api_schema/response/compute/v2_70/servers.py
index 6103923..6bb688a 100644
--- a/tempest/lib/api_schema/response/compute/v2_70/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_70/servers.py
@@ -79,3 +79,4 @@
 update_tag = copy.deepcopy(servers263.update_tag)
 delete_tag = copy.deepcopy(servers263.delete_tag)
 show_instance_action = copy.deepcopy(servers263.show_instance_action)
+create_backup = copy.deepcopy(servers263.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_71/servers.py b/tempest/lib/api_schema/response/compute/v2_71/servers.py
index 3e55c1c..b1c202b 100644
--- a/tempest/lib/api_schema/response/compute/v2_71/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_71/servers.py
@@ -83,3 +83,4 @@
 show_volume_attachment = copy.deepcopy(servers270.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers270.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers270.show_instance_action)
+create_backup = copy.deepcopy(servers270.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_73/servers.py b/tempest/lib/api_schema/response/compute/v2_73/servers.py
index e7a1d87..89f100d 100644
--- a/tempest/lib/api_schema/response/compute/v2_73/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_73/servers.py
@@ -80,3 +80,4 @@
 show_volume_attachment = copy.deepcopy(servers271.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers271.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers271.show_instance_action)
+create_backup = copy.deepcopy(servers271.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_75/__init__.py b/tempest/lib/api_schema/response/compute/v2_75/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_75/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_75/servers.py b/tempest/lib/api_schema/response/compute/v2_75/servers.py
new file mode 100644
index 0000000..6b3e93d
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_75/servers.py
@@ -0,0 +1,64 @@
+#    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_57 import servers as servers257
+from tempest.lib.api_schema.response.compute.v2_73 import servers as servers273
+
+
+###########################################################################
+#
+# 2.75:
+#
+# Server representation is made consistent among GET, PUT
+# and Rebuild serevr APIs response.
+#
+###########################################################################
+
+rebuild_server = copy.deepcopy(servers273.get_server)
+rebuild_server['response_body']['properties']['server'][
+    'properties'].pop('OS-EXT-SRV-ATTR:user_data')
+rebuild_server['status_code'] = [202]
+rebuild_server['response_body']['properties']['server'][
+    'properties'].update({'user_data': servers257.user_data})
+rebuild_server['response_body']['properties']['server'][
+    'required'].append('user_data')
+
+rebuild_server_with_admin_pass = copy.deepcopy(rebuild_server)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+    'properties'].update({'adminPass': {'type': 'string'}})
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+    'required'].append('adminPass')
+
+update_server = copy.deepcopy(servers273.get_server)
+
+# NOTE(gmann): 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.73 ***
+get_server = copy.deepcopy(servers273.get_server)
+list_servers = copy.deepcopy(servers273.list_servers)
+list_servers_detail = copy.deepcopy(servers273.list_servers_detail)
+show_server_diagnostics = copy.deepcopy(servers273.show_server_diagnostics)
+get_remote_consoles = copy.deepcopy(servers273.get_remote_consoles)
+list_tags = copy.deepcopy(servers273.list_tags)
+update_all_tags = copy.deepcopy(servers273.update_all_tags)
+delete_all_tags = copy.deepcopy(servers273.delete_all_tags)
+check_tag_existence = copy.deepcopy(servers273.check_tag_existence)
+update_tag = copy.deepcopy(servers273.update_tag)
+delete_tag = copy.deepcopy(servers273.delete_tag)
+attach_volume = copy.deepcopy(servers273.attach_volume)
+show_volume_attachment = copy.deepcopy(servers273.show_volume_attachment)
+list_volume_attachments = copy.deepcopy(servers273.list_volume_attachments)
+show_instance_action = copy.deepcopy(servers273.show_instance_action)
+create_backup = copy.deepcopy(servers273.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_79/servers.py b/tempest/lib/api_schema/response/compute/v2_79/servers.py
index b5507f9..77d9beb 100644
--- a/tempest/lib/api_schema/response/compute/v2_79/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_79/servers.py
@@ -12,7 +12,7 @@
 
 import copy
 
-from tempest.lib.api_schema.response.compute.v2_73 import servers as servers273
+from tempest.lib.api_schema.response.compute.v2_75 import servers as servers275
 
 
 ###########################################################################
@@ -27,19 +27,19 @@
 # - POST /servers/{server_id}/os-volume_attachments
 ###########################################################################
 
-attach_volume = copy.deepcopy(servers273.attach_volume)
+attach_volume = copy.deepcopy(servers275.attach_volume)
 attach_volume['response_body']['properties']['volumeAttachment'][
     'properties'].update({'delete_on_termination': {'type': 'boolean'}})
 attach_volume['response_body']['properties']['volumeAttachment'][
     'required'].append('delete_on_termination')
 
-show_volume_attachment = copy.deepcopy(servers273.show_volume_attachment)
+show_volume_attachment = copy.deepcopy(servers275.show_volume_attachment)
 show_volume_attachment['response_body']['properties']['volumeAttachment'][
     'properties'].update({'delete_on_termination': {'type': 'boolean'}})
 show_volume_attachment['response_body']['properties'][
     'volumeAttachment']['required'].append('delete_on_termination')
 
-list_volume_attachments = copy.deepcopy(servers273.list_volume_attachments)
+list_volume_attachments = copy.deepcopy(servers275.list_volume_attachments)
 list_volume_attachments['response_body']['properties']['volumeAttachments'][
     'items']['properties'].update(
         {'delete_on_termination': {'type': 'boolean'}})
@@ -49,20 +49,21 @@
 # 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.73 ***
-rebuild_server = copy.deepcopy(servers273.rebuild_server)
+# ****** Schemas unchanged since microversion 2.75 ***
+rebuild_server = copy.deepcopy(servers275.rebuild_server)
 rebuild_server_with_admin_pass = copy.deepcopy(
-    servers273.rebuild_server_with_admin_pass)
-update_server = copy.deepcopy(servers273.update_server)
-get_server = copy.deepcopy(servers273.get_server)
-list_servers_detail = copy.deepcopy(servers273.list_servers_detail)
-list_servers = copy.deepcopy(servers273.list_servers)
-show_server_diagnostics = copy.deepcopy(servers273.show_server_diagnostics)
-get_remote_consoles = copy.deepcopy(servers273.get_remote_consoles)
-list_tags = copy.deepcopy(servers273.list_tags)
-update_all_tags = copy.deepcopy(servers273.update_all_tags)
-delete_all_tags = copy.deepcopy(servers273.delete_all_tags)
-check_tag_existence = copy.deepcopy(servers273.check_tag_existence)
-update_tag = copy.deepcopy(servers273.update_tag)
-delete_tag = copy.deepcopy(servers273.delete_tag)
-show_instance_action = copy.deepcopy(servers273.show_instance_action)
+    servers275.rebuild_server_with_admin_pass)
+update_server = copy.deepcopy(servers275.update_server)
+get_server = copy.deepcopy(servers275.get_server)
+list_servers_detail = copy.deepcopy(servers275.list_servers_detail)
+list_servers = copy.deepcopy(servers275.list_servers)
+show_server_diagnostics = copy.deepcopy(servers275.show_server_diagnostics)
+get_remote_consoles = copy.deepcopy(servers275.get_remote_consoles)
+list_tags = copy.deepcopy(servers275.list_tags)
+update_all_tags = copy.deepcopy(servers275.update_all_tags)
+delete_all_tags = copy.deepcopy(servers275.delete_all_tags)
+check_tag_existence = copy.deepcopy(servers275.check_tag_existence)
+update_tag = copy.deepcopy(servers275.update_tag)
+delete_tag = copy.deepcopy(servers275.delete_tag)
+show_instance_action = copy.deepcopy(servers275.show_instance_action)
+create_backup = copy.deepcopy(servers275.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_8/servers.py b/tempest/lib/api_schema/response/compute/v2_8/servers.py
index 119d8e2..366fb1b 100644
--- a/tempest/lib/api_schema/response/compute/v2_8/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_8/servers.py
@@ -39,3 +39,4 @@
 show_volume_attachment = copy.deepcopy(servers.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers.show_instance_action)
+create_backup = copy.deepcopy(servers.create_backup)
diff --git a/tempest/lib/api_schema/response/compute/v2_9/servers.py b/tempest/lib/api_schema/response/compute/v2_9/servers.py
index 9258eec..b4c7865 100644
--- a/tempest/lib/api_schema/response/compute/v2_9/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_9/servers.py
@@ -58,3 +58,4 @@
 show_volume_attachment = copy.deepcopy(servers.show_volume_attachment)
 list_volume_attachments = copy.deepcopy(servers.list_volume_attachments)
 show_instance_action = copy.deepcopy(servers.show_instance_action)
+create_backup = copy.deepcopy(servers.create_backup)
diff --git a/tempest/lib/services/compute/quota_classes_client.py b/tempest/lib/services/compute/quota_classes_client.py
index 9b64099..5f220a7 100644
--- a/tempest/lib/services/compute/quota_classes_client.py
+++ b/tempest/lib/services/compute/quota_classes_client.py
@@ -16,20 +16,30 @@
 from oslo_serialization import jsonutils as json
 
 from tempest.lib.api_schema.response.compute.v2_1\
-    import quota_classes as classes_schema
+    import quota_classes as schema
+from tempest.lib.api_schema.response.compute.v2_50 import quota_classes \
+    as schemav250
+from tempest.lib.api_schema.response.compute.v2_57 import quota_classes \
+    as schemav257
 from tempest.lib.common import rest_client
 from tempest.lib.services.compute import base_compute_client
 
 
 class QuotaClassesClient(base_compute_client.BaseComputeClient):
 
+    schema_versions_info = [
+        {'min': None, 'max': '2.49', 'schema': schema},
+        {'min': '2.50', 'max': '2.56', 'schema': schemav250},
+        {'min': '2.57', 'max': None, 'schema': schemav257}]
+
     def show_quota_class_set(self, quota_class_id):
         """List the quota class set for a quota class."""
 
         url = 'os-quota-class-sets/%s' % quota_class_id
         resp, body = self.get(url)
         body = json.loads(body)
-        self.validate_response(classes_schema.get_quota_class_set, resp, body)
+        _schema = self.get_schema(self.schema_versions_info)
+        self.validate_response(_schema.get_quota_class_set, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def update_quota_class_set(self, quota_class_id, **kwargs):
@@ -45,6 +55,7 @@
                               post_body)
 
         body = json.loads(body)
-        self.validate_response(classes_schema.update_quota_class_set,
+        _schema = self.get_schema(self.schema_versions_info)
+        self.validate_response(_schema.update_quota_class_set,
                                resp, body)
         return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/compute/server_groups_client.py b/tempest/lib/services/compute/server_groups_client.py
index 89ad2d9..9895653 100644
--- a/tempest/lib/services/compute/server_groups_client.py
+++ b/tempest/lib/services/compute/server_groups_client.py
@@ -20,6 +20,8 @@
     as schema
 from tempest.lib.api_schema.response.compute.v2_13 import server_groups \
     as schemav213
+from tempest.lib.api_schema.response.compute.v2_64 import server_groups \
+    as schemav264
 from tempest.lib.common import rest_client
 from tempest.lib.services.compute import base_compute_client
 
@@ -28,7 +30,8 @@
 
     schema_versions_info = [
         {'min': None, 'max': '2.12', 'schema': schema},
-        {'min': '2.13', 'max': None, 'schema': schemav213}]
+        {'min': '2.13', 'max': '2.63', 'schema': schemav213},
+        {'min': '2.64', 'max': None, 'schema': schemav264}]
 
     def create_server_group(self, **kwargs):
         """Create the server group.
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index ed3d4c0..d2bdb6e 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -27,6 +27,7 @@
 from tempest.lib.api_schema.response.compute.v2_19 import servers as schemav219
 from tempest.lib.api_schema.response.compute.v2_26 import servers as schemav226
 from tempest.lib.api_schema.response.compute.v2_3 import servers as schemav23
+from tempest.lib.api_schema.response.compute.v2_45 import servers as schemav245
 from tempest.lib.api_schema.response.compute.v2_47 import servers as schemav247
 from tempest.lib.api_schema.response.compute.v2_48 import servers as schemav248
 from tempest.lib.api_schema.response.compute.v2_51 import servers as schemav251
@@ -39,6 +40,7 @@
 from tempest.lib.api_schema.response.compute.v2_70 import servers as schemav270
 from tempest.lib.api_schema.response.compute.v2_71 import servers as schemav271
 from tempest.lib.api_schema.response.compute.v2_73 import servers as schemav273
+from tempest.lib.api_schema.response.compute.v2_75 import servers as schemav275
 from tempest.lib.api_schema.response.compute.v2_79 import servers as schemav279
 from tempest.lib.api_schema.response.compute.v2_8 import servers as schemav28
 from tempest.lib.api_schema.response.compute.v2_9 import servers as schemav29
@@ -57,7 +59,8 @@
         {'min': '2.9', 'max': '2.15', 'schema': schemav29},
         {'min': '2.16', 'max': '2.18', 'schema': schemav216},
         {'min': '2.19', 'max': '2.25', 'schema': schemav219},
-        {'min': '2.26', 'max': '2.46', 'schema': schemav226},
+        {'min': '2.26', 'max': '2.44', 'schema': schemav226},
+        {'min': '2.45', 'max': '2.46', 'schema': schemav245},
         {'min': '2.47', 'max': '2.47', 'schema': schemav247},
         {'min': '2.48', 'max': '2.50', 'schema': schemav248},
         {'min': '2.51', 'max': '2.53', 'schema': schemav251},
@@ -68,7 +71,8 @@
         {'min': '2.63', 'max': '2.69', 'schema': schemav263},
         {'min': '2.70', 'max': '2.70', 'schema': schemav270},
         {'min': '2.71', 'max': '2.72', 'schema': schemav271},
-        {'min': '2.73', 'max': '2.78', 'schema': schemav273},
+        {'min': '2.73', 'max': '2.74', 'schema': schemav273},
+        {'min': '2.75', 'max': '2.78', 'schema': schemav275},
         {'min': '2.79', 'max': None, 'schema': schemav279}]
 
     def __init__(self, auth_provider, service, region,
@@ -235,7 +239,9 @@
         API reference:
         https://docs.openstack.org/api-ref/compute/#create-server-back-up-createbackup-action
         """
-        return self.action(server_id, "createBackup", **kwargs)
+        schema = self.get_schema(self.schema_versions_info)
+        return self.action(server_id, "createBackup",
+                           schema.create_backup, **kwargs)
 
     def change_password(self, server_id, **kwargs):
         """Change the root password for the server.
diff --git a/tempest/scenario/test_compute_unified_limits.py b/tempest/scenario/test_compute_unified_limits.py
new file mode 100644
index 0000000..bacf526
--- /dev/null
+++ b/tempest/scenario/test_compute_unified_limits.py
@@ -0,0 +1,166 @@
+# Copyright 2021 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 testtools
+
+from tempest.common import utils
+from tempest.common import waiters
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+from tempest.scenario import manager
+
+CONF = config.CONF
+
+
+@testtools.skipUnless(CONF.compute_feature_enabled.unified_limits,
+                      'Compute unified limits are not enabled')
+class ComputeProjectQuotaTest(manager.ScenarioTest):
+    """The test base class for compute unified limits tests.
+
+    Dynamic credentials (unique tenants) are created on a per-class basis, so
+    we test different quota limits in separate test classes to prevent a quota
+    limit update in one test class from affecting a test running in another
+    test class in parallel.
+
+    https://docs.openstack.org/tempest/latest/configuration.html#dynamic-credentials
+    """
+    credentials = ['primary', 'system_admin']
+    force_tenant_isolation = True
+
+    @classmethod
+    def resource_setup(cls):
+        super(ComputeProjectQuotaTest, cls).resource_setup()
+
+        # Figure out and record the nova service id
+        services = cls.os_system_admin.identity_services_v3_client.\
+            list_services()
+        nova_services = [x for x in services['services']
+                         if x['name'] == 'nova']
+        cls.nova_service_id = nova_services[0]['id']
+
+        # Pre-create quota limits in subclasses and record their IDs so we can
+        # update them in-place without needing to know which ones have been
+        # created and in which order.
+        cls.limit_ids = {}
+
+    @classmethod
+    def _create_limit(cls, name, value):
+        return cls.os_system_admin.identity_limits_client.create_limit(
+            CONF.identity.region, cls.nova_service_id,
+            cls.servers_client.tenant_id, name, value)['limits'][0]['id']
+
+    def _update_limit(self, name, value):
+        self.os_system_admin.identity_limits_client.update_limit(
+            self.limit_ids[name], value)
+
+
+@testtools.skipUnless(CONF.compute_feature_enabled.unified_limits,
+                      'Compute unified limits are not enabled')
+class ServersQuotaTest(ComputeProjectQuotaTest):
+
+    @classmethod
+    def resource_setup(cls):
+        super(ServersQuotaTest, cls).resource_setup()
+
+        try:
+            cls.limit_ids['servers'] = cls._create_limit(
+                'servers', 5)
+            cls.limit_ids['class:VCPU'] = cls._create_limit(
+                'class:VCPU', 10)
+            cls.limit_ids['class:MEMORY_MB'] = cls._create_limit(
+                'class:MEMORY_MB', 25 * 1024)
+            cls.limit_ids['class:DISK_GB'] = cls._create_limit(
+                'class:DISK_GB', 10)
+        except lib_exc.Forbidden:
+            raise cls.skipException('Target system is not configured with '
+                                    'compute unified limits')
+
+    @decorators.idempotent_id('555d8bbf-d2ed-4e39-858c-4235899402d9')
+    @utils.services('compute')
+    def test_server_count_vcpu_memory_disk_quota(self):
+        # Set a quota on the number of servers for our tenant to one.
+        self._update_limit('servers', 1)
+
+        # Create one server.
+        first = self.create_server(name='first')
+
+        # Second server would put us over quota, so expect failure.
+        # NOTE: In nova, quota exceeded raises 403 Forbidden.
+        self.assertRaises(lib_exc.Forbidden,
+                          self.create_server,
+                          name='second')
+
+        # Update our limit to two.
+        self._update_limit('servers', 2)
+
+        # Now the same create should succeed.
+        second = self.create_server(name='second')
+
+        # Third server would put us over quota, so expect failure.
+        self.assertRaises(lib_exc.Forbidden,
+                          self.create_server,
+                          name='third')
+
+        # Delete the first server to put us under quota.
+        self.servers_client.delete_server(first['id'])
+        waiters.wait_for_server_termination(self.servers_client, first['id'])
+
+        # Now the same create should succeed.
+        third = self.create_server(name='third')
+
+        # Set the servers limit back to 10 to test other resources.
+        self._update_limit('servers', 10)
+
+        # Default flavor has: VCPU=1, MEMORY_MB=512, DISK_GB=1
+        # We are currently using 2 VCPU, set the limit to 2.
+        self._update_limit('class:VCPU', 2)
+
+        # Server create should fail as it would go over quota.
+        self.assertRaises(lib_exc.Forbidden,
+                          self.create_server,
+                          name='fourth')
+
+        # Delete the second server to put us under quota.
+        self.servers_client.delete_server(second['id'])
+        waiters.wait_for_server_termination(self.servers_client, second['id'])
+
+        # Same create should now succeed.
+        fourth = self.create_server(name='fourth')
+
+        # We are currently using 2 DISK_GB. Set limit to 1.
+        self._update_limit('class:DISK_GB', 1)
+
+        # Server create should fail because we're already over (new) quota.
+        self.assertRaises(lib_exc.Forbidden,
+                          self.create_server,
+                          name='fifth')
+
+        # Delete the third server.
+        self.servers_client.delete_server(third['id'])
+        waiters.wait_for_server_termination(self.servers_client, third['id'])
+
+        # Server create should fail again because it would still put us over
+        # quota.
+        self.assertRaises(lib_exc.Forbidden,
+                          self.create_server,
+                          name='fifth')
+
+        # Delete the fourth server.
+        self.servers_client.delete_server(fourth['id'])
+        waiters.wait_for_server_termination(self.servers_client, fourth['id'])
+
+        # Server create should succeed now.
+        self.create_server(name='fifth')
diff --git a/tempest/tests/common/test_waiters.py b/tempest/tests/common/test_waiters.py
index 5b0acfa..1d0ee77 100755
--- a/tempest/tests/common/test_waiters.py
+++ b/tempest/tests/common/test_waiters.py
@@ -523,6 +523,70 @@
         mock_list_volume_attachments.assert_called_once_with(
             mock.sentinel.server_id)
 
+    @mock.patch('os.system')
+    def test_wait_for_ping_host_alive(self, mock_ping):
+        mock_ping.return_value = 0
+        # Assert that nothing is raised as the host is alive
+        waiters.wait_for_ping('127.0.0.1', 10, 1)
+
+    @mock.patch('os.system')
+    def test_wait_for_ping_host_eventually_alive(self, mock_ping):
+        mock_ping.side_effect = [1, 1, 0]
+        # Assert that nothing is raised when the host is eventually alive
+        waiters.wait_for_ping('127.0.0.1', 10, 1)
+
+    @mock.patch('os.system')
+    def test_wait_for_ping_timeout(self, mock_ping):
+        mock_ping.return_value = 1
+        # Assert that TimeoutException is raised when the host is dead
+        self.assertRaises(
+            lib_exc.TimeoutException,
+            waiters.wait_for_ping,
+            '127.0.0.1',
+            .1,
+            .1
+        )
+
+    def test_wait_for_ssh(self):
+        mock_ssh_client = mock.Mock()
+        mock_ssh_client.validate_authentication.return_value = True
+        # Assert that nothing is raised when validate_authentication returns
+        waiters.wait_for_ssh(mock_ssh_client, .1)
+        mock_ssh_client.validate_authentication.assert_called_once()
+
+    def test_wait_for_ssh_eventually_up(self):
+        mock_ssh_client = mock.Mock()
+        timeout = lib_exc.SSHTimeout(
+            host='foo',
+            username='bar',
+            password='fizz'
+        )
+        mock_ssh_client.validate_authentication.side_effect = [
+            timeout,
+            timeout,
+            True
+        ]
+        # Assert that nothing is raised if validate_authentication passes
+        # before the timeout
+        waiters.wait_for_ssh(mock_ssh_client, 10)
+
+    def test_wait_for_ssh_timeout(self):
+        mock_ssh_client = mock.Mock()
+        timeout = lib_exc.SSHTimeout(
+            host='foo',
+            username='bar',
+            password='fizz'
+        )
+        mock_ssh_client.validate_authentication.side_effect = timeout
+        # Assert that TimeoutException is raised when validate_authentication
+        # doesn't pass in time.
+        self.assertRaises(
+            lib_exc.TimeoutException,
+            waiters.wait_for_ssh,
+            mock_ssh_client,
+            .1
+        )
+
 
 class TestServerFloatingIPWaiters(base.TestCase):
 
diff --git a/tempest/tests/lib/services/image/v2/test_schemas_client.py b/tempest/tests/lib/services/image/v2/test_schemas_client.py
index eef5b41..9fb249b 100644
--- a/tempest/tests/lib/services/image/v2/test_schemas_client.py
+++ b/tempest/tests/lib/services/image/v2/test_schemas_client.py
@@ -75,6 +75,293 @@
         }
     }
 
+    FAKE_SHOW_SCHEMA_IMAGE = {
+        "additionalProperties": {
+            "type": "string"
+        },
+        "links": [
+            {
+                "href": "{self}",
+                "rel": "self"
+            },
+            {
+                "href": "{file}",
+                "rel": "enclosure"
+            },
+            {
+                "href": "{schema}",
+                "rel": "describedby"
+            }
+        ],
+        "name": "image",
+        "properties": {
+            "architecture": {
+                "description": "Operating system architecture as "
+                               "specified in https://docs.openstack.org/"
+                               "python-glanceclient/latest/cli"
+                               "/property-keys.html",
+                "is_base": False,
+                "type": "string"
+            },
+            "checksum": {
+                "description": "md5 hash of image contents.",
+                "maxLength": 32,
+                "readOnly": True,
+                "type": [
+                    "null",
+                    "string"
+                ]
+            },
+            "container_format": {
+                "description": "Format of the container",
+                "enum": [
+                    None,
+                    "ami",
+                    "ari",
+                    "aki",
+                    "bare",
+                    "ovf",
+                    "ova",
+                    "docker"
+                ],
+                "type": [
+                    "null",
+                    "string"
+                ]
+            },
+            "created_at": {
+                "description": "Date and time of image registration",
+                "readOnly": True,
+                "type": "string"
+            },
+            "direct_url": {
+                "description": "URL to access the image file "
+                               "kept in external store",
+                "readOnly": True,
+                "type": "string"
+            },
+            "disk_format": {
+                "description": "Format of the disk",
+                "enum": [
+                    None,
+                    "ami",
+                    "ari",
+                    "aki",
+                    "vhd",
+                    "vhdx",
+                    "vmdk",
+                    "raw",
+                    "qcow2",
+                    "vdi",
+                    "iso",
+                    "ploop"
+                ],
+                "type": [
+                    "null",
+                    "string"
+                ]
+            },
+            "file": {
+                "description": "An image file url",
+                "readOnly": True,
+                "type": "string"
+            },
+            "id": {
+                "description": "An identifier for the image",
+                "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F])"
+                           "{4}-([0-9a-fA-F]){4}-([0-9a-fA-F])"
+                           "{4}-([0-9a-fA-F]){12}$",
+                "type": "string"
+            },
+            "instance_uuid": {
+                "description": "Metadata which can be used to record which"
+                               " instance this image is associated with. "
+                               "(Informational only, does not create "
+                               "an instance snapshot.)",
+                "is_base": False,
+                "type": "string"
+            },
+            "kernel_id": {
+                "description": "ID of image stored in Glance that should "
+                               "be used as the kernel when booting an "
+                               "AMI-style image.",
+                "is_base": False,
+                "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-"
+                           "([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-("
+                           "[0-9a-fA-F]){12}$",
+                "type": [
+                    "null",
+                    "string"
+                ]
+            },
+            "locations": {
+                "description": "A set of URLs to access the image file "
+                               "kept in external store",
+                "items": {
+                    "properties": {
+                        "metadata": {
+                            "type": "object"
+                        },
+                        "url": {
+                            "maxLength": 255,
+                            "type": "string"
+                        }
+                    },
+                    "required": [
+                        "url",
+                        "metadata"
+                    ],
+                    "type": "object"
+                },
+                "type": "array"
+            },
+            "min_disk": {
+                "description": "Amount of disk space (in GB) "
+                               "required to boot image.",
+                "type": "integer"
+            },
+            "min_ram": {
+                "description": "Amount of ram (in MB) required "
+                               "to boot image.",
+                "type": "integer"
+            },
+            "name": {
+                "description": "Descriptive name for the image",
+                "maxLength": 255,
+                "type": [
+                    "null",
+                    "string"
+                ]
+            },
+            "os_distro": {
+                "description": "Common name of operating system distribution "
+                               "as specified in https://docs.openstack.org/"
+                               "python-glanceclient/latest/cli/"
+                               "property-keys.html",
+                "is_base": False,
+                "type": "string"
+            },
+            "os_hash_algo": {
+                "description": "Algorithm to calculate the os_hash_value",
+                "maxLength": 64,
+                "readOnly": True,
+                "type": [
+                    "null",
+                    "string"
+                ]
+            },
+            "os_hash_value": {
+                "description": "Hexdigest of the image contents "
+                               "using the algorithm specified by "
+                               "the os_hash_algo",
+                "maxLength": 128,
+                "readOnly": True,
+                "type": [
+                    "null",
+                    "string"
+                ]
+            },
+            "os_hidden": {
+                "description": "If true, image will not appear in default"
+                               " image list response.",
+                "type": "boolean"
+            },
+            "os_version": {
+                "description": "Operating system version as specified by "
+                               "the distributor",
+                "is_base": False,
+                "type": "string"
+            },
+            "owner": {
+                "description": "Owner of the image",
+                "maxLength": 255,
+                "type": [
+                    "null",
+                    "string"
+                ]
+            },
+            "protected": {
+                "description": "If true, image will not be deletable.",
+                "type": "boolean"
+            },
+            "ramdisk_id": {
+                "description": "ID of image stored in Glance that should"
+                               " be used as the ramdisk when booting an "
+                               "AMI-style image.",
+                "is_base": False,
+                "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])"
+                           "{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
+                "type": [
+                    "null",
+                    "string"
+                ]
+            },
+            "schema": {
+                "description": "An image schema url",
+                "readOnly": True,
+                "type": "string"
+            },
+            "self": {
+                "description": "An image self url",
+                "readOnly": True,
+                "type": "string"
+            },
+            "size": {
+                "description": "Size of image file in bytes",
+                "readOnly": True,
+                "type": [
+                    "null",
+                    "integer"
+                ]
+            },
+            "status": {
+                "description": "Status of the image",
+                "enum": [
+                    "queued",
+                    "saving",
+                    "active",
+                    "killed",
+                    "deleted",
+                    "pending_delete",
+                    "deactivated",
+                    "uploading",
+                    "importing"
+                ],
+                "readOnly": True,
+                "type": "string"
+            },
+            "tags": {
+                "description": "List of strings related to the image",
+                "items": {
+                    "maxLength": 255,
+                    "type": "string"
+                },
+                "type": "array"
+            },
+            "updated_at": {
+                "description": "Date and time of the last image modification",
+                "readOnly": True,
+                "type": "string"
+            },
+            "virtual_size": {
+                "description": "Virtual size of image in bytes",
+                "readOnly": True,
+                "type": [
+                    "null",
+                    "integer"
+                ]
+            },
+            "visibility": {
+                "description": "Scope of image accessibility",
+                "enum": [
+                    "public",
+                    "private"
+                ],
+                "type": "string"
+            }
+        }
+    }
+
     def setUp(self):
         super(TestSchemasClient, self).setUp()
         fake_auth = fake_auth_provider.FakeAuthProvider()
@@ -89,6 +376,22 @@
             bytes_body,
             schema="members")
 
+    def _test_show_schema_image(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.show_schema,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_SHOW_SCHEMA_IMAGE,
+            bytes_body,
+            schema="image")
+
+    def _test_show_schema_images(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.show_schema,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_SHOW_SCHEMA_IMAGE,
+            bytes_body,
+            schema="images")
+
     def _test_show_schema(self, bytes_body=False):
         self.check_service_client_function(
             self.client.show_schema,
@@ -103,6 +406,18 @@
     def test_show_schema_members_with_bytes_body(self):
         self._test_show_schema_members(bytes_body=True)
 
+    def test_show_schema_image_with_str_body(self):
+        self._test_show_schema_image()
+
+    def test_show_schema_image_with_bytes_body(self):
+        self._test_show_schema_image(bytes_body=True)
+
+    def test_show_schema_images_with_str_body(self):
+        self._test_show_schema_images()
+
+    def test_show_schema_images_with_bytes_body(self):
+        self._test_show_schema_images(bytes_body=True)
+
     def test_show_schema_with_str_body(self):
         self._test_show_schema()
 
diff --git a/tempest/tests/test_hacking.py b/tempest/tests/test_hacking.py
index 7c31185..464e66a 100644
--- a/tempest/tests/test_hacking.py
+++ b/tempest/tests/test_hacking.py
@@ -240,3 +240,9 @@
             with_other_decorators=True,
             with_negative_decorator=False,
             expected_success=False)
+
+    def test_no_log_warn(self):
+        self.assertFalse(list(checks.no_log_warn(
+            'LOG.warning("LOG.warn is deprecated")')))
+        self.assertTrue(list(checks.no_log_warn(
+            'LOG.warn("LOG.warn is deprecated")')))
diff --git a/tox.ini b/tox.ini
index 18f2aa6..b07fdaf 100644
--- a/tox.ini
+++ b/tox.ini
@@ -369,6 +369,7 @@
   T115 = checks:dont_put_admin_tests_on_nonadmin_path
   T116 = checks:unsupported_exception_attribute_PY3
   T117 = checks:negative_test_attribute_always_applied_to_negative_tests
+  T118 = checks:no_log_warn
 paths =
   ./tempest/hacking
 
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 731a72a..e62f24a 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -122,6 +122,8 @@
             irrelevant-files: *tempest-irrelevant-files-2
         - tempest-full-py3-centos-8-stream:
             irrelevant-files: *tempest-irrelevant-files
+        - tempest-full-centos-9-stream:
+            irrelevant-files: *tempest-irrelevant-files
     gate:
       jobs:
         - openstack-tox-pep8
diff --git a/zuul.d/tempest-specific.yaml b/zuul.d/tempest-specific.yaml
index 5b6b702..7d28e5c 100644
--- a/zuul.d/tempest-specific.yaml
+++ b/zuul.d/tempest-specific.yaml
@@ -83,6 +83,12 @@
       configure_swap_size: 4096
 
 - job:
+    name: tempest-full-centos-9-stream
+    parent: tempest-full-py3-centos-8-stream
+    voting: false
+    nodeset: devstack-single-node-centos-9-stream
+
+- job:
     name: tempest-tox-plugin-sanity-check
     parent: tox
     description: |