Merge "Add --worker-file option in tempest"
diff --git a/HACKING.rst b/HACKING.rst
index 204b3c7..f8be0ad 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -12,7 +12,6 @@
   tempest/scenario tests
 - [T104] Scenario tests require a services decorator
 - [T105] Tests cannot use setUpClass/tearDownClass
-- [T106] vim configuration should not be kept in source files.
 - [T107] Check that a service tag isn't in the module path
 - [T108] Check no hyphen at the end of rand_name() argument
 - [T109] Cannot use testtools.skip decorator; instead use
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index b4f06e3..50fd4f2 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -414,6 +414,10 @@
 
   .. _2.71: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id64
 
+  * `2.73`_
+
+  .. _2.73: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id66
+
 * Volume
 
   * `3.3`_
diff --git a/setup.cfg b/setup.cfg
index d246c68..04511e1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,7 +6,7 @@
 author = OpenStack
 author-email = openstack-discuss@lists.openstack.org
 home-page = https://docs.openstack.org/tempest/latest/
-requires-python = >=3.6
+python-requires = >=3.6
 classifier =
     Intended Audience :: Information Technology
     Intended Audience :: System Administrators
@@ -49,5 +49,3 @@
 oslo.config.opts =
     tempest.config = tempest.config:list_opts
 
-[wheel]
-universal = 1
diff --git a/tempest/api/compute/admin/test_aggregates_negative.py b/tempest/api/compute/admin/test_aggregates_negative.py
index a6e0efa..d5adfed 100644
--- a/tempest/api/compute/admin/test_aggregates_negative.py
+++ b/tempest/api/compute/admin/test_aggregates_negative.py
@@ -144,6 +144,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('19dd44e1-c435-4ee1-a402-88c4f90b5950')
     def test_aggregate_add_existent_host(self):
+        # Adding already existing host to aggregate should fail.
         self.useFixture(fixtures.LockFixture('availability_zone'))
         aggregate = self._create_test_aggregate()
 
@@ -172,6 +173,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('95d6a6fa-8da9-4426-84d0-eec0329f2e4d')
     def test_aggregate_remove_nonexistent_host(self):
+        # Removing not existing host from aggregate should fail.
         aggregate = self._create_test_aggregate()
 
         self.assertRaises(lib_exc.NotFound, self.client.remove_host,
diff --git a/tempest/api/compute/admin/test_flavors_extra_specs.py b/tempest/api/compute/admin/test_flavors_extra_specs.py
index 4d27a22..789965e 100644
--- a/tempest/api/compute/admin/test_flavors_extra_specs.py
+++ b/tempest/api/compute/admin/test_flavors_extra_specs.py
@@ -64,7 +64,7 @@
         # Test to SET, GET, UPDATE, SHOW, UNSET flavor extra
         # spec as a user with admin privileges.
         # Assigning extra specs values that are to be set
-        specs = {"key1": "value1", "key2": "value2"}
+        specs = {'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
         # SET extra specs to the flavor created in setUp
         set_body = self.admin_flavors_client.set_flavor_extra_spec(
             self.flavor['id'], **specs)['extra_specs']
@@ -74,30 +74,33 @@
             self.flavor['id'])['extra_specs'])
         self.assertEqual(get_body, specs)
 
-        # UPDATE the value of the extra specs key1
-        update_body = \
-            self.admin_flavors_client.update_flavor_extra_spec(
-                self.flavor['id'], "key1", key1="value")
-        self.assertEqual({"key1": "value"}, update_body)
+        # UPDATE the value of the extra specs 'hw:numa_nodes'
+        update_body = self.admin_flavors_client.update_flavor_extra_spec(
+            self.flavor['id'], "hw:numa_nodes", **{'hw:numa_nodes': '2'})
+        self.assertEqual({'hw:numa_nodes': '2'}, update_body)
 
-        # GET extra specs and verify the value of the key2
+        # GET extra specs and verify the value of the 'hw:cpu_policy'
         # is the same as before
         get_body = self.admin_flavors_client.list_flavor_extra_specs(
             self.flavor['id'])['extra_specs']
-        self.assertEqual(get_body, {"key1": "value", "key2": "value2"})
+        self.assertEqual(
+            get_body, {'hw:numa_nodes': '2', 'hw:cpu_policy': 'shared'}
+        )
 
         # UNSET extra specs that were set in this test
-        self.admin_flavors_client.unset_flavor_extra_spec(self.flavor['id'],
-                                                          "key1")
-        self.admin_flavors_client.unset_flavor_extra_spec(self.flavor['id'],
-                                                          "key2")
+        self.admin_flavors_client.unset_flavor_extra_spec(
+            self.flavor['id'], 'hw:numa_nodes'
+        )
+        self.admin_flavors_client.unset_flavor_extra_spec(
+            self.flavor['id'], 'hw:cpu_policy'
+        )
         get_body = self.admin_flavors_client.list_flavor_extra_specs(
             self.flavor['id'])['extra_specs']
         self.assertEmpty(get_body)
 
     @decorators.idempotent_id('a99dad88-ae1c-4fba-aeb4-32f898218bd0')
     def test_flavor_non_admin_get_all_keys(self):
-        specs = {"key1": "value1", "key2": "value2"}
+        specs = {'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
         self.admin_flavors_client.set_flavor_extra_spec(self.flavor['id'],
                                                         **specs)
         body = (self.flavors_client.list_flavor_extra_specs(
@@ -108,11 +111,14 @@
 
     @decorators.idempotent_id('12805a7f-39a3-4042-b989-701d5cad9c90')
     def test_flavor_non_admin_get_specific_key(self):
+        specs = {'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
         body = self.admin_flavors_client.set_flavor_extra_spec(
-            self.flavor['id'], key1="value1", key2="value2")['extra_specs']
-        self.assertEqual(body['key1'], 'value1')
-        self.assertIn('key2', body)
+            self.flavor['id'], **specs
+        )['extra_specs']
+        self.assertEqual(body['hw:numa_nodes'], '1')
+        self.assertIn('hw:cpu_policy', body)
+
         body = self.flavors_client.show_flavor_extra_spec(
-            self.flavor['id'], 'key1')
-        self.assertEqual(body['key1'], 'value1')
-        self.assertNotIn('key2', body)
+            self.flavor['id'], 'hw:numa_nodes')
+        self.assertEqual(body['hw:numa_nodes'], '1')
+        self.assertNotIn('hw:cpu_policy', body)
diff --git a/tempest/api/compute/admin/test_flavors_extra_specs_negative.py b/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
index 5cde39e..9f89293 100644
--- a/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
+++ b/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
@@ -68,32 +68,36 @@
         self.assertRaises(lib_exc.Forbidden,
                           self.flavors_client.set_flavor_extra_spec,
                           self.flavor['id'],
-                          key1="value1", key2="value2")
+                          **{'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'})
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('1ebf4ef8-759e-48fe-a801-d451d80476fb')
     def test_flavor_non_admin_update_specific_key(self):
         # non admin user is not allowed to update flavor extra spec
         body = self.admin_flavors_client.set_flavor_extra_spec(
-            self.flavor['id'], key1="value1", key2="value2")['extra_specs']
-        self.assertEqual(body['key1'], 'value1')
+            self.flavor['id'],
+            **{'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
+        )['extra_specs']
+        self.assertEqual(body['hw:numa_nodes'], '1')
         self.assertRaises(lib_exc.Forbidden,
                           self.flavors_client.
                           update_flavor_extra_spec,
                           self.flavor['id'],
-                          'key1',
-                          key1='value1_new')
+                          'hw:numa_nodes',
+                          **{'hw:numa_nodes': '1'})
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('28f12249-27c7-44c1-8810-1f382f316b11')
     def test_flavor_non_admin_unset_keys(self):
         self.admin_flavors_client.set_flavor_extra_spec(
-            self.flavor['id'], key1="value1", key2="value2")
+            self.flavor['id'],
+            **{'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
+        )
 
         self.assertRaises(lib_exc.Forbidden,
                           self.flavors_client.unset_flavor_extra_spec,
                           self.flavor['id'],
-                          'key1')
+                          'hw:numa_nodes')
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('440b9f3f-3c7f-4293-a106-0ceda350f8de')
@@ -101,7 +105,7 @@
         self.assertRaises(lib_exc.NotFound,
                           self.admin_flavors_client.unset_flavor_extra_spec,
                           self.flavor['id'],
-                          'nonexistent_key')
+                          'hw:cpu_thread_policy')
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('329a7be3-54b2-48be-8052-bf2ce4afd898')
@@ -109,7 +113,7 @@
         self.assertRaises(lib_exc.NotFound,
                           self.flavors_client.show_flavor_extra_spec,
                           self.flavor['id'],
-                          "nonexistent_key")
+                          'hw:cpu_thread_policy')
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('25b822b8-9f49-44f6-80de-d99f0482e5cb')
@@ -118,8 +122,8 @@
         self.assertRaises(lib_exc.BadRequest,
                           self.admin_flavors_client.update_flavor_extra_spec,
                           self.flavor['id'],
-                          "key2",
-                          key1="value")
+                          'hw:numa_nodes',
+                          **{'hw:cpu_policy': 'shared'})
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('f5889590-bf66-41cc-b4b1-6e6370cfd93f')
@@ -128,6 +132,5 @@
         self.assertRaises(lib_exc.BadRequest,
                           self.admin_flavors_client.update_flavor_extra_spec,
                           self.flavor['id'],
-                          "key1",
-                          key1="value",
-                          key2="value")
+                          'hw:numa_nodes',
+                          **{'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'})
diff --git a/tempest/api/compute/admin/test_hosts.py b/tempest/api/compute/admin/test_hosts.py
index c246685..31fe2b5 100644
--- a/tempest/api/compute/admin/test_hosts.py
+++ b/tempest/api/compute/admin/test_hosts.py
@@ -29,11 +29,13 @@
 
     @decorators.idempotent_id('9bfaf98d-e2cb-44b0-a07e-2558b2821e4f')
     def test_list_hosts(self):
+        # Listing hosts.
         hosts = self.client.list_hosts()['hosts']
         self.assertGreaterEqual(len(hosts), 2, str(hosts))
 
     @decorators.idempotent_id('5dc06f5b-d887-47a2-bb2a-67762ef3c6de')
     def test_list_hosts_with_zone(self):
+        # Listing hosts with specified availability zone
         self.useFixture(fixtures.LockFixture('availability_zone'))
         hosts = self.client.list_hosts()['hosts']
         host = hosts[0]
@@ -43,6 +45,7 @@
 
     @decorators.idempotent_id('9af3c171-fbf4-4150-a624-22109733c2a6')
     def test_list_hosts_with_a_blank_zone(self):
+        # Listing hosts with blank availability zone.
         # If send the request with a blank zone, the request will be successful
         # and it will return all the hosts list
         hosts = self.client.list_hosts(zone='')['hosts']
@@ -50,6 +53,7 @@
 
     @decorators.idempotent_id('c6ddbadb-c94e-4500-b12f-8ffc43843ff8')
     def test_list_hosts_with_nonexistent_zone(self):
+        # Listing hosts with not existing availability zone.
         # If send the request with a nonexistent zone, the request will be
         # successful and no hosts will be returned
         hosts = self.client.list_hosts(zone='xxx')['hosts']
@@ -57,6 +61,7 @@
 
     @decorators.idempotent_id('38adbb12-aee2-4498-8aec-329c72423aa4')
     def test_show_host_detail(self):
+        # Showing host details.
         hosts = self.client.list_hosts()['hosts']
 
         hosts = [host for host in hosts if host['service'] == 'compute']
diff --git a/tempest/api/compute/admin/test_hosts_negative.py b/tempest/api/compute/admin/test_hosts_negative.py
index 8a91ae2..e8733c8 100644
--- a/tempest/api/compute/admin/test_hosts_negative.py
+++ b/tempest/api/compute/admin/test_hosts_negative.py
@@ -39,18 +39,21 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('dd032027-0210-4d9c-860e-69b1b8deed5f')
     def test_list_hosts_with_non_admin_user(self):
+        # Non admin user is not allowed to list hosts.
         self.assertRaises(lib_exc.Forbidden,
                           self.non_admin_client.list_hosts)
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('e75b0a1a-041f-47a1-8b4a-b72a6ff36d3f')
     def test_show_host_detail_with_nonexistent_hostname(self):
+        # Showing host detail with not existing hostname should fail.
         self.assertRaises(lib_exc.NotFound,
                           self.client.show_host, 'nonexistent_hostname')
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('19ebe09c-bfd4-4b7c-81a2-e2e0710f59cc')
     def test_show_host_detail_with_non_admin_user(self):
+        # Non admin user is not allowed to show host details.
         self.assertRaises(lib_exc.Forbidden,
                           self.non_admin_client.show_host,
                           self.hostname)
@@ -58,6 +61,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('e40c72b1-0239-4ed6-ba21-81a184df1f7c')
     def test_update_host_with_non_admin_user(self):
+        # Non admin user is not allowed to update host.
         self.assertRaises(lib_exc.Forbidden,
                           self.non_admin_client.update_host,
                           self.hostname,
@@ -67,7 +71,8 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('fbe2bf3e-3246-4a95-a59f-94e4e298ec77')
     def test_update_host_with_invalid_status(self):
-        # 'status' can only be 'enable' or 'disable'
+        # Updating host to invalid status should fail,
+        # 'status' can only be 'enable' or 'disable'.
         self.assertRaises(lib_exc.BadRequest,
                           self.client.update_host,
                           self.hostname,
@@ -77,7 +82,8 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('ab1e230e-5e22-41a9-8699-82b9947915d4')
     def test_update_host_with_invalid_maintenance_mode(self):
-        # 'maintenance_mode' can only be 'enable' or 'disable'
+        # Updating host to invalid maintenance mode should fail,
+        # 'maintenance_mode' can only be 'enable' or 'disable'.
         self.assertRaises(lib_exc.BadRequest,
                           self.client.update_host,
                           self.hostname,
@@ -87,7 +93,8 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('0cd85f75-6992-4a4a-b1bd-d11e37fd0eee')
     def test_update_host_without_param(self):
-        # 'status' or 'maintenance_mode' needed for host update
+        # Updating host without param should fail,
+        # 'status' or 'maintenance_mode' is needed for host update.
         self.assertRaises(lib_exc.BadRequest,
                           self.client.update_host,
                           self.hostname)
@@ -95,6 +102,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('23c92146-2100-4d68-b2d6-c7ade970c9c1')
     def test_update_nonexistent_host(self):
+        # Updating not existing host should fail.
         self.assertRaises(lib_exc.NotFound,
                           self.client.update_host,
                           'nonexistent_hostname',
@@ -104,6 +112,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('0d981ac3-4320-4898-b674-82b61fbb60e4')
     def test_startup_nonexistent_host(self):
+        # Starting up not existing host should fail.
         self.assertRaises(lib_exc.NotFound,
                           self.client.startup_host,
                           'nonexistent_hostname')
@@ -111,6 +120,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('9f4ebb7e-b2ae-4e5b-a38f-0fd1bb0ddfca')
     def test_startup_host_with_non_admin_user(self):
+        # Non admin user is not allowed to startup host.
         self.assertRaises(lib_exc.Forbidden,
                           self.non_admin_client.startup_host,
                           self.hostname)
@@ -118,6 +128,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('9e637444-29cf-4244-88c8-831ae82c31b6')
     def test_shutdown_nonexistent_host(self):
+        # Shutting down not existing host should fail.
         self.assertRaises(lib_exc.NotFound,
                           self.client.shutdown_host,
                           'nonexistent_hostname')
@@ -125,6 +136,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('a803529c-7e3f-4d3c-a7d6-8e1c203d27f6')
     def test_shutdown_host_with_non_admin_user(self):
+        # Non admin user is not allowed to shutdown host.
         self.assertRaises(lib_exc.Forbidden,
                           self.non_admin_client.shutdown_host,
                           self.hostname)
@@ -132,6 +144,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('f86bfd7b-0b13-4849-ae29-0322e83ee58b')
     def test_reboot_nonexistent_host(self):
+        # Rebooting not existing host should fail.
         self.assertRaises(lib_exc.NotFound,
                           self.client.reboot_host,
                           'nonexistent_hostname')
@@ -139,6 +152,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('02d79bb9-eb57-4612-abf6-2cb38897d2f8')
     def test_reboot_host_with_non_admin_user(self):
+        # Non admin user is not allowed to reboot host.
         self.assertRaises(lib_exc.Forbidden,
                           self.non_admin_client.reboot_host,
                           self.hostname)
diff --git a/tempest/api/compute/admin/test_hypervisor.py b/tempest/api/compute/admin/test_hypervisor.py
index 9822c26..e45aac5 100644
--- a/tempest/api/compute/admin/test_hypervisor.py
+++ b/tempest/api/compute/admin/test_hypervisor.py
@@ -134,6 +134,7 @@
 
     @decorators.idempotent_id('d7e1805b-3b14-4a3b-b6fd-50ec6d9f361f')
     def test_search_hypervisor(self):
+        # Searching for hypervisors by its name.
         hypers = self._list_hypervisors()
         self.assertNotEmpty(hypers, "No hypervisors found.")
         hypers = self.client.search_hypervisor(
diff --git a/tempest/api/compute/admin/test_hypervisor_negative.py b/tempest/api/compute/admin/test_hypervisor_negative.py
index 0056376..723b93c 100644
--- a/tempest/api/compute/admin/test_hypervisor_negative.py
+++ b/tempest/api/compute/admin/test_hypervisor_negative.py
@@ -40,8 +40,8 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('c136086a-0f67-4b2b-bc61-8482bd68989f')
     def test_show_nonexistent_hypervisor(self):
+        # Showing not existing hypervisor should fail.
         nonexistent_hypervisor_id = data_utils.rand_uuid()
-
         self.assertRaises(
             lib_exc.NotFound,
             self.client.show_hypervisor,
@@ -50,6 +50,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('51e663d0-6b89-4817-a465-20aca0667d03')
     def test_show_hypervisor_with_non_admin_user(self):
+        # Non admin user is not allowed to show hypervisor.
         hypers = self._list_hypervisors()
         self.assertNotEmpty(hypers)
 
@@ -61,6 +62,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('e2b061bb-13f9-40d8-9d6e-d5bf17595849')
     def test_get_hypervisor_stats_with_non_admin_user(self):
+        # Non admin user is not allowed to get hypervisor stats.
         self.assertRaises(
             lib_exc.Forbidden,
             self.non_adm_client.show_hypervisor_statistics)
@@ -68,6 +70,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('f60aa680-9a3a-4c7d-90e1-fae3a4891303')
     def test_get_nonexistent_hypervisor_uptime(self):
+        # Getting uptime of not existing hypervisor should fail.
         nonexistent_hypervisor_id = data_utils.rand_uuid()
 
         self.assertRaises(
@@ -78,6 +81,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('6c3461f9-c04c-4e2a-bebb-71dc9cb47df2')
     def test_get_hypervisor_uptime_with_non_admin_user(self):
+        # Non admin user is not allowed to get hypervisor uptime.
         hypers = self._list_hypervisors()
         self.assertNotEmpty(hypers)
 
@@ -97,7 +101,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('dc02db05-e801-4c5f-bc8e-d915290ab345')
     def test_get_hypervisor_list_details_with_non_admin_user(self):
-        # List of hypervisor details and available services with non admin user
+        # Non admin user is not allowed to list hypervisor details.
         self.assertRaises(
             lib_exc.Forbidden,
             self.non_adm_client.list_hypervisors, detail=True)
@@ -109,6 +113,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('2a0a3938-832e-4859-95bf-1c57c236b924')
     def test_show_servers_with_non_admin_user(self):
+        # Non admin user is not allowed to show servers on hypervisor.
         hypers = self._list_hypervisors()
         self.assertNotEmpty(hypers)
 
@@ -120,6 +125,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('02463d69-0ace-4d33-a4a8-93d7883a2bba')
     def test_show_servers_with_nonexistent_hypervisor(self):
+        # Showing servers on not existing hypervisor should fail.
         nonexistent_hypervisor_id = data_utils.rand_uuid()
 
         self.assertRaises(
@@ -130,6 +136,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('5b6a6c79-5dc1-4fa5-9c58-9c8085948e74')
     def test_search_hypervisor_with_non_admin_user(self):
+        # Non admin user is not allowed to search hypervisor.
         hypers = self._list_hypervisors()
         self.assertNotEmpty(hypers)
 
@@ -141,6 +148,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('19a45cc1-1000-4055-b6d2-28e8b2ec4faa')
     def test_search_nonexistent_hypervisor(self):
+        # Searching not existing hypervisor should fail.
         self.assertRaises(
             lib_exc.NotFound,
             self.client.search_hypervisor,
diff --git a/tempest/api/compute/admin/test_services.py b/tempest/api/compute/admin/test_services.py
index 73e191b..bf846e5 100644
--- a/tempest/api/compute/admin/test_services.py
+++ b/tempest/api/compute/admin/test_services.py
@@ -28,11 +28,13 @@
 
     @decorators.idempotent_id('5be41ef4-53d1-41cc-8839-5c2a48a1b283')
     def test_list_services(self):
+        # Listing nova services
         services = self.client.list_services()['services']
         self.assertNotEmpty(services)
 
     @decorators.idempotent_id('f345b1ec-bc6e-4c38-a527-3ca2bc00bef5')
     def test_get_service_by_service_binary_name(self):
+        # Listing nova services by binary name.
         binary_name = 'nova-compute'
         services = self.client.list_services(binary=binary_name)['services']
         self.assertNotEmpty(services)
@@ -41,6 +43,7 @@
 
     @decorators.idempotent_id('affb42d5-5b4b-43c8-8b0b-6dca054abcca')
     def test_get_service_by_host_name(self):
+        # Listing nova services by host name.
         services = self.client.list_services()['services']
         host_name = services[0]['host']
         services_on_host = [service for service in services if
diff --git a/tempest/api/compute/admin/test_volumes_negative.py b/tempest/api/compute/admin/test_volumes_negative.py
index aa932ee..133f4bc 100644
--- a/tempest/api/compute/admin/test_volumes_negative.py
+++ b/tempest/api/compute/admin/test_volumes_negative.py
@@ -13,6 +13,7 @@
 #    under the License.
 
 from tempest.api.compute import base
+from tempest.common import utils
 from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
@@ -58,3 +59,66 @@
                           self.admin_servers_client.update_attached_volume,
                           self.server['id'], volume['id'],
                           volumeId=nonexistent_volume)
+
+
+class UpdateMultiattachVolumeNegativeTest(base.BaseV2ComputeAdminTest):
+
+    min_microversion = '2.60'
+    volume_min_microversion = '3.27'
+
+    @classmethod
+    def skip_checks(self):
+        super(UpdateMultiattachVolumeNegativeTest, self).skip_checks()
+        if not CONF.compute_feature_enabled.volume_multiattach:
+            raise self.skipException('Volume multi-attach is not available.')
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('7576d497-b7c6-44bd-9cc5-c5b4e50fec71')
+    @utils.services('volume')
+    def test_multiattach_rw_volume_update_failure(self):
+
+        # Create two multiattach capable volumes.
+        vol1 = self.create_volume(multiattach=True)
+        vol2 = self.create_volume(multiattach=True)
+
+        # Create two instances.
+        server1 = self.create_test_server(wait_until='ACTIVE')
+        server2 = self.create_test_server(wait_until='ACTIVE')
+
+        # Attach vol1 to both of these instances.
+        vol1_attachment1 = self.attach_volume(server1, vol1)
+        vol1_attachment2 = self.attach_volume(server2, vol1)
+
+        # Assert that we now have two attachments.
+        vol1 = self.volumes_client.show_volume(vol1['id'])['volume']
+        self.assertEqual(2, len(vol1['attachments']))
+
+        # By default both of these attachments should have an attach_mode of
+        # read-write, assert that here to ensure the following calls to update
+        # the volume will be rejected.
+        for volume_attachment in vol1['attachments']:
+            attachment_id = volume_attachment['attachment_id']
+            attachment = self.attachments_client.show_attachment(
+                attachment_id)['attachment']
+            self.assertEqual('rw', attachment['attach_mode'])
+
+        # Assert that a BadRequest is raised when we attempt to update volume1
+        # to volume2 on server1 or server2.
+        self.assertRaises(lib_exc.BadRequest,
+                          self.admin_servers_client.update_attached_volume,
+                          server1['id'], vol1['id'], volumeId=vol2['id'])
+        self.assertRaises(lib_exc.BadRequest,
+                          self.admin_servers_client.update_attached_volume,
+                          server2['id'], vol1['id'], volumeId=vol2['id'])
+
+        # Fetch the volume 1 to check the current attachments.
+        vol1 = self.volumes_client.show_volume(vol1['id'])['volume']
+        vol1_attachment_ids = [a['id'] for a in vol1['attachments']]
+
+        # Assert that volume 1 is still attached to both server 1 and 2.
+        self.assertIn(vol1_attachment1['id'], vol1_attachment_ids)
+        self.assertIn(vol1_attachment2['id'], vol1_attachment_ids)
+
+        # Assert that volume 2 has no attachments.
+        vol2 = self.volumes_client.show_volume(vol2['id'])['volume']
+        self.assertEqual([], vol2['attachments'])
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 8d0962d..eab2a8d 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -108,6 +108,7 @@
         cls.versions_client = cls.os_primary.compute_versions_client
         if CONF.service_available.cinder:
             cls.volumes_client = cls.os_primary.volumes_client_latest
+            cls.attachments_client = cls.os_primary.attachments_client_latest
         if CONF.service_available.glance:
             if CONF.image_feature_enabled.api_v1:
                 cls.images_client = cls.os_primary.image_client
@@ -226,7 +227,7 @@
 
     @classmethod
     def create_test_server(cls, validatable=False, volume_backed=False,
-                           validation_resources=None, **kwargs):
+                           validation_resources=None, clients=None, **kwargs):
         """Wrapper utility that returns a test server.
 
         This wrapper utility calls the common create test server and
@@ -238,6 +239,7 @@
         :param volume_backed: Whether the instance is volume backed or not.
         :param validation_resources: Dictionary of validation resources as
             returned by `get_class_validation_resources`.
+        :param clients: Client manager, defaults to os_primary.
         :param kwargs: Extra arguments are passed down to the
             `compute.create_test_server` call.
         """
@@ -254,8 +256,11 @@
             not tenant_network):
             kwargs['networks'] = 'none'
 
+        if clients is None:
+            clients = cls.os_primary
+
         body, servers = compute.create_test_server(
-            cls.os_primary,
+            clients,
             validatable,
             validation_resources=validation_resources,
             tenant_network=tenant_network,
@@ -266,11 +271,11 @@
         # and then wait for all
         for server in servers:
             cls.addClassResourceCleanup(waiters.wait_for_server_termination,
-                                        cls.servers_client, server['id'])
+                                        clients.servers_client, server['id'])
         for server in servers:
             cls.addClassResourceCleanup(
                 test_utils.call_and_ignore_notfound_exc,
-                cls.servers_client.delete_server, server['id'])
+                clients.servers_client.delete_server, server['id'])
 
         return body
 
diff --git a/tempest/api/compute/servers/test_multiple_create.py b/tempest/api/compute/servers/test_multiple_create.py
index e176251..dcadace 100644
--- a/tempest/api/compute/servers/test_multiple_create.py
+++ b/tempest/api/compute/servers/test_multiple_create.py
@@ -23,6 +23,7 @@
 
     @decorators.idempotent_id('61e03386-89c3-449c-9bb1-a06f423fd9d1')
     def test_multiple_create(self):
+        # Creating server with min_count=2, 2 servers will be created.
         tenant_network = self.get_tenant_network()
         body, servers = compute.create_test_server(
             self.os_primary,
@@ -39,6 +40,8 @@
 
     @decorators.idempotent_id('864777fb-2f1e-44e3-b5b9-3eb6fa84f2f7')
     def test_multiple_create_with_reservation_return(self):
+        # Creating multiple servers with return_reservation_id=True,
+        # reservation_id will be returned.
         body = self.create_test_server(wait_until='ACTIVE',
                                        min_count=1,
                                        max_count=2,
diff --git a/tempest/api/compute/servers/test_multiple_create_negative.py b/tempest/api/compute/servers/test_multiple_create_negative.py
index 422510f..6bdf83b 100644
--- a/tempest/api/compute/servers/test_multiple_create_negative.py
+++ b/tempest/api/compute/servers/test_multiple_create_negative.py
@@ -23,6 +23,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('daf29d8d-e928-4a01-9a8c-b129603f3fc0')
     def test_min_count_less_than_one(self):
+        # Creating server with min_count=0 should fail.
         invalid_min_count = 0
         self.assertRaises(lib_exc.BadRequest, self.create_test_server,
                           min_count=invalid_min_count)
@@ -30,6 +31,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('999aa722-d624-4423-b813-0d1ac9884d7a')
     def test_min_count_non_integer(self):
+        # Creating server with non-integer min_count should fail.
         invalid_min_count = 2.5
         self.assertRaises(lib_exc.BadRequest, self.create_test_server,
                           min_count=invalid_min_count)
@@ -37,6 +39,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('a6f9c2ab-e060-4b82-b23c-4532cb9390ff')
     def test_max_count_less_than_one(self):
+        # Creating server with max_count < 1 shoudld fail.
         invalid_max_count = 0
         self.assertRaises(lib_exc.BadRequest, self.create_test_server,
                           max_count=invalid_max_count)
@@ -44,6 +47,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('9c5698d1-d7af-4c80-b971-9d403135eea2')
     def test_max_count_non_integer(self):
+        # Creating server with non-integer max_count should fail.
         invalid_max_count = 2.5
         self.assertRaises(lib_exc.BadRequest, self.create_test_server,
                           max_count=invalid_max_count)
@@ -51,6 +55,7 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('476da616-f1ef-4271-a9b1-b9fc87727cdf')
     def test_max_count_less_than_min_count(self):
+        # Creating server with max_count less than min_count should fail.
         min_count = 3
         max_count = 2
         self.assertRaises(lib_exc.BadRequest, self.create_test_server,
diff --git a/tempest/api/identity/admin/v3/test_groups.py b/tempest/api/identity/admin/v3/test_groups.py
index df0d79d..2dd1fe2 100644
--- a/tempest/api/identity/admin/v3/test_groups.py
+++ b/tempest/api/identity/admin/v3/test_groups.py
@@ -114,6 +114,13 @@
             self.groups_client.add_group_user(group['id'], user['id'])
         # list groups which user belongs to
         user_groups = self.users_client.list_user_groups(user['id'])['groups']
+        # The `membership_expires_at` attribute is present when listing user
+        # group memberships, and is not an attribute of the groups themselves.
+        # Therefore we remove it from the comparison.
+        for g in user_groups:
+            if 'membership_expires_at' in g:
+                self.assertIsNone(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/network/test_tags.py b/tempest/api/network/test_tags.py
index 85f6896..2b9719a 100644
--- a/tempest/api/network/test_tags.py
+++ b/tempest/api/network/test_tags.py
@@ -103,9 +103,10 @@
         List tags.
         Remove a tag.
 
-    v2.0 of the Neutron API is assumed. The tag-ext extension allows users to
-    set tags on the following resources: subnets, ports, routers and
-    subnetpools.
+    v2.0 of the Neutron API is assumed. The tag-ext or standard-attr-tag
+    extension allows users to set tags on the following resources: subnets,
+    ports, routers and subnetpools.
+    from stein release the tag-ext has been renamed to standard-attr-tag
     """
 
     # NOTE(felipemonteiro): The supported resource names are plural. Use
@@ -115,8 +116,12 @@
     @classmethod
     def skip_checks(cls):
         super(TagsExtTest, cls).skip_checks()
-        if not utils.is_extension_enabled('tag-ext', 'network'):
-            msg = "tag-ext extension not enabled."
+        # Added condition to support backward compatiblity since
+        # tag-ext has been renamed to standard-attr-tag
+        if not (utils.is_extension_enabled('tag-ext', 'network') or
+                utils.is_extension_enabled('standard-attr-tag', 'network')):
+            msg = ("neither tag-ext nor standard-attr-tag extensions "
+                   "are enabled.")
             raise cls.skipException(msg)
 
     @classmethod
diff --git a/tempest/api/volume/admin/test_group_snapshots.py b/tempest/api/volume/admin/test_group_snapshots.py
index f695f51..c57766e 100644
--- a/tempest/api/volume/admin/test_group_snapshots.py
+++ b/tempest/api/volume/admin/test_group_snapshots.py
@@ -113,7 +113,8 @@
         self._delete_group_snapshot(group_snapshot)
         group_snapshots = self.group_snapshots_client.list_group_snapshots()[
             'group_snapshots']
-        self.assertEmpty(group_snapshots)
+        self.assertNotIn((group_snapshot['name'], group_snapshot['id']),
+                         [(m['name'], m['id']) for m in group_snapshots])
 
     @decorators.idempotent_id('eff52c70-efc7-45ed-b47a-4ad675d09b81')
     def test_create_group_from_group_snapshot(self):
diff --git a/tempest/api/volume/test_volume_absolute_limits.py b/tempest/api/volume/test_volume_absolute_limits.py
index 00a3375..4d64a95 100644
--- a/tempest/api/volume/test_volume_absolute_limits.py
+++ b/tempest/api/volume/test_volume_absolute_limits.py
@@ -23,7 +23,7 @@
 # NOTE(zhufl): This inherits from BaseVolumeAdminTest because
 # it requires force_tenant_isolation=True, which need admin
 # credentials to create non-admin users for the tests.
-class AbsoluteLimitsTests(base.BaseVolumeAdminTest):  # noqa
+class AbsoluteLimitsTests(base.BaseVolumeAdminTest):  # noqa: T115
 
     # avoid existing volumes of pre-defined tenant
     force_tenant_isolation = True
diff --git a/tempest/api/volume/test_volumes_snapshots_list.py b/tempest/api/volume/test_volumes_snapshots_list.py
index 8a416ea..f4f039c 100644
--- a/tempest/api/volume/test_volumes_snapshots_list.py
+++ b/tempest/api/volume/test_volumes_snapshots_list.py
@@ -109,7 +109,7 @@
         snap_list = self.snapshots_client.list_snapshots(
             sort_key=sort_key, sort_dir=sort_dir)['snapshots']
         self.assertNotEmpty(snap_list)
-        if sort_key is 'display_name':
+        if sort_key == 'display_name':
             sort_key = 'name'
         # Note: On Cinder API, 'display_name' works as a sort key
         # on a request, a volume name appears as 'name' on the response.
diff --git a/tempest/cmd/cleanup_service.py b/tempest/cmd/cleanup_service.py
index 9b591bc..469b214 100644
--- a/tempest/cmd/cleanup_service.py
+++ b/tempest/cmd/cleanup_service.py
@@ -1048,6 +1048,8 @@
         project_associated_services.append(NovaQuotaService)
     if IS_CINDER:
         project_associated_services.append(VolumeQuotaService)
+    if IS_NEUTRON:
+        project_associated_services.append(NetworkQuotaService)
     return project_associated_services
 
 
@@ -1076,7 +1078,6 @@
         resource_cleanup_services.append(NetworkService)
         resource_cleanup_services.append(NetworkSecGroupService)
         resource_cleanup_services.append(NetworkSubnetPoolsService)
-        resource_cleanup_services.append(NetworkQuotaService)
     if IS_CINDER:
         resource_cleanup_services.append(SnapshotService)
         resource_cleanup_services.append(VolumeService)
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index b547cc6..e242301 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -124,6 +124,12 @@
             raise lib_exc.DeleteErrorException(
                 "Server %s failed to delete and is in ERROR status" %
                 server_id)
+        if server_status == 'SOFT_DELETED':
+            # Soft-deleted instances need to be forcibly deleted to
+            # prevent some test cases from failing.
+            LOG.debug("Automatically force-deleting soft-deleted server %s",
+                      server_id)
+            client.force_delete_server(server_id)
 
         if int(time.time()) - start_time >= client.build_timeout:
             raise lib_exc.TimeoutException
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index 2c40cb1..6a97a00 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -15,6 +15,7 @@
 import os
 import re
 
+from hacking import core
 import pycodestyle
 
 
@@ -25,7 +26,6 @@
 TEST_DEFINITION = re.compile(r'^\s*def test.*')
 SETUP_TEARDOWN_CLASS_DEFINITION = re.compile(r'^\s+def (setUp|tearDown)Class')
 SCENARIO_DECORATOR = re.compile(r'\s*@.*services\((.*)\)')
-VI_HEADER_RE = re.compile(r"^#\s+vim?:.+")
 RAND_NAME_HYPHEN_RE = re.compile(r".*rand_name\(.+[\-\_][\"\']\)")
 mutable_default_args = re.compile(r"^\s*def .+\((.+=\{\}|.+=\[\])")
 TESTTOOLS_SKIP_DECORATOR = re.compile(r'\s*@testtools\.skip\((.*)\)')
@@ -39,6 +39,7 @@
 _HAVE_NEGATIVE_DECORATOR = False
 
 
+@core.flake8ext
 def import_no_clients_in_api_and_scenario_tests(physical_line, filename):
     """Check for client imports from tempest/api & tempest/scenario tests
 
@@ -53,6 +54,7 @@
                      " in tempest/api/* or tempest/scenario/* tests"))
 
 
+@core.flake8ext
 def scenario_tests_need_service_tags(physical_line, filename,
                                      previous_logical):
     """Check that scenario tests have service tags
@@ -67,6 +69,7 @@
                         "T104: Scenario tests require a service decorator")
 
 
+@core.flake8ext
 def no_setup_teardown_class_for_tests(physical_line, filename):
 
     if pycodestyle.noqa(physical_line):
@@ -80,20 +83,7 @@
                 "T105: (setUp|tearDown)Class can not be used in tests")
 
 
-def no_vi_headers(physical_line, line_number, lines):
-    """Check for vi editor configuration in source files.
-
-    By default vi modelines can only appear in the first or
-    last 5 lines of a source file.
-
-    T106
-    """
-    # NOTE(gilliard): line_number is 1-indexed
-    if line_number <= 5 or line_number > len(lines) - 5:
-        if VI_HEADER_RE.match(physical_line):
-            return 0, "T106: Don't put vi configuration in source files"
-
-
+@core.flake8ext
 def service_tags_not_in_module_path(physical_line, filename):
     """Check that a service tag isn't in the module path
 
@@ -117,6 +107,7 @@
                             "T107: service tag should not be in path")
 
 
+@core.flake8ext
 def no_hyphen_at_end_of_rand_name(logical_line, filename):
     """Check no hyphen at the end of rand_name() argument
 
@@ -127,6 +118,7 @@
         return 0, msg
 
 
+@core.flake8ext
 def no_mutable_default_args(logical_line):
     """Check that mutable object isn't used as default argument
 
@@ -137,6 +129,7 @@
         yield (0, msg)
 
 
+@core.flake8ext
 def no_testtools_skip_decorator(logical_line):
     """Check that methods do not have the testtools.skip decorator
 
@@ -170,7 +163,8 @@
     return True
 
 
-def get_resources_on_service_clients(logical_line, physical_line, filename,
+@core.flake8ext
+def get_resources_on_service_clients(physical_line, logical_line, filename,
                                      line_number, lines):
     """Check that service client names of GET should be consistent
 
@@ -197,7 +191,8 @@
         yield (0, msg)
 
 
-def delete_resources_on_service_clients(logical_line, physical_line, filename,
+@core.flake8ext
+def delete_resources_on_service_clients(physical_line, logical_line, filename,
                                         line_number, lines):
     """Check that service client names of DELETE should be consistent
 
@@ -223,6 +218,7 @@
         yield (0, msg)
 
 
+@core.flake8ext
 def dont_import_local_tempest_into_lib(logical_line, filename):
     """Check that tempest.lib should not import local tempest code
 
@@ -244,6 +240,7 @@
     yield (0, msg)
 
 
+@core.flake8ext
 def use_rand_uuid_instead_of_uuid4(logical_line, filename):
     """Check that tests use data_utils.rand_uuid() instead of uuid.uuid4()
 
@@ -260,6 +257,7 @@
     yield (0, msg)
 
 
+@core.flake8ext
 def dont_use_config_in_tempest_lib(logical_line, filename):
     """Check that tempest.lib doesn't use tempest config
 
@@ -277,7 +275,8 @@
         yield(0, msg)
 
 
-def dont_put_admin_tests_on_nonadmin_path(logical_line, physical_line,
+@core.flake8ext
+def dont_put_admin_tests_on_nonadmin_path(logical_line,
                                           filename):
     """Check admin tests should exist under admin path
 
@@ -287,9 +286,6 @@
     if 'tempest/api/' not in filename:
         return
 
-    if pycodestyle.noqa(physical_line):
-        return
-
     if not re.match(r'class .*Test.*\(.*Admin.*\):', logical_line):
         return
 
@@ -298,6 +294,7 @@
         yield(0, msg)
 
 
+@core.flake8ext
 def unsupported_exception_attribute_PY3(logical_line):
     """Check Unsupported 'message' exception attribute in PY3
 
@@ -309,6 +306,7 @@
         yield(0, msg)
 
 
+@core.flake8ext
 def negative_test_attribute_always_applied_to_negative_tests(physical_line,
                                                              filename):
     """Check ``@decorators.attr(type=['negative'])`` applied to negative tests.
@@ -330,22 +328,3 @@
                        " to all negative API tests"
                 )
             _HAVE_NEGATIVE_DECORATOR = False
-
-
-def factory(register):
-    register(import_no_clients_in_api_and_scenario_tests)
-    register(scenario_tests_need_service_tags)
-    register(no_setup_teardown_class_for_tests)
-    register(no_vi_headers)
-    register(service_tags_not_in_module_path)
-    register(no_hyphen_at_end_of_rand_name)
-    register(no_mutable_default_args)
-    register(no_testtools_skip_decorator)
-    register(get_resources_on_service_clients)
-    register(delete_resources_on_service_clients)
-    register(dont_import_local_tempest_into_lib)
-    register(dont_use_config_in_tempest_lib)
-    register(use_rand_uuid_instead_of_uuid4)
-    register(dont_put_admin_tests_on_nonadmin_path)
-    register(unsupported_exception_attribute_PY3)
-    register(negative_test_attribute_always_applied_to_negative_tests)
diff --git a/tempest/lib/api_schema/response/compute/v2_73/__init__.py b/tempest/lib/api_schema/response/compute/v2_73/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_73/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_73/servers.py b/tempest/lib/api_schema/response/compute/v2_73/servers.py
new file mode 100644
index 0000000..6e491e9
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_73/servers.py
@@ -0,0 +1,78 @@
+#    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_71 import servers as servers271
+
+
+###########################################################################
+#
+# 2.73:
+#
+# The locked_reason parameter is now returned in the response body of the
+# following calls:
+#
+# - POST /servers/{server_id}/action (where the action is rebuild)
+# - PUT /servers/{server_id} (update)
+# - GET /servers/{server_id} (show)
+# - GET /servers/detail (list)
+#
+###########################################################################
+
+# The "locked_reason" parameter will either be a string or None.
+locked_reason = {'type': ['string', 'null']}
+
+rebuild_server = copy.deepcopy(servers271.rebuild_server)
+rebuild_server['response_body']['properties']['server'][
+    'properties'].update({'locked_reason': locked_reason})
+rebuild_server['response_body']['properties']['server'][
+    'required'].append('locked_reason')
+
+rebuild_server_with_admin_pass = copy.deepcopy(
+    servers271.rebuild_server_with_admin_pass)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+    'properties'].update({'locked_reason': locked_reason})
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+    'required'].append('locked_reason')
+
+update_server = copy.deepcopy(servers271.update_server)
+update_server['response_body']['properties']['server'][
+    'properties'].update({'locked_reason': locked_reason})
+update_server['response_body']['properties']['server'][
+    'required'].append('locked_reason')
+
+get_server = copy.deepcopy(servers271.get_server)
+get_server['response_body']['properties']['server'][
+    'properties'].update({'locked_reason': locked_reason})
+get_server['response_body']['properties']['server'][
+    'required'].append('locked_reason')
+
+list_servers_detail = copy.deepcopy(servers271.list_servers_detail)
+list_servers_detail['response_body']['properties']['servers']['items'][
+    'properties'].update({'locked_reason': locked_reason})
+list_servers_detail['response_body']['properties']['servers']['items'][
+    'required'].append('locked_reason')
+
+# NOTE(lajoskatona): 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.71 ***
+list_servers = copy.deepcopy(servers271.list_servers)
+show_server_diagnostics = copy.deepcopy(servers271.show_server_diagnostics)
+get_remote_consoles = copy.deepcopy(servers271.get_remote_consoles)
+list_tags = copy.deepcopy(servers271.list_tags)
+update_all_tags = copy.deepcopy(servers271.update_all_tags)
+delete_all_tags = copy.deepcopy(servers271.delete_all_tags)
+check_tag_existence = copy.deepcopy(servers271.check_tag_existence)
+update_tag = copy.deepcopy(servers271.update_tag)
+delete_tag = copy.deepcopy(servers271.delete_tag)
diff --git a/tempest/lib/auth.py b/tempest/lib/auth.py
index 8e6d3d5..3fee489 100644
--- a/tempest/lib/auth.py
+++ b/tempest/lib/auth.py
@@ -684,7 +684,7 @@
 
     def __str__(self):
         """Represent only attributes included in self.ATTRIBUTES"""
-        attrs = [attr for attr in self.ATTRIBUTES if attr is not 'password']
+        attrs = [attr for attr in self.ATTRIBUTES if attr != 'password']
         _repr = dict((k, getattr(self, k)) for k in attrs)
         return str(_repr)
 
@@ -741,7 +741,7 @@
 
     def __str__(self):
         """Represent only attributes included in self.ATTRIBUTES"""
-        attrs = [attr for attr in self.ATTRIBUTES if attr is not 'password']
+        attrs = [attr for attr in self.ATTRIBUTES if attr != 'password']
         _repr = dict((k, getattr(self, k)) for k in attrs)
         return str(_repr)
 
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index a687137..cbf7a8c 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -35,6 +35,7 @@
 from tempest.lib.api_schema.response.compute.v2_63 import servers as schemav263
 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_8 import servers as schemav28
 from tempest.lib.api_schema.response.compute.v2_9 import servers as schemav29
 from tempest.lib.common import rest_client
@@ -59,7 +60,8 @@
         {'min': '2.57', 'max': '2.62', 'schema': schemav257},
         {'min': '2.63', 'max': '2.69', 'schema': schemav263},
         {'min': '2.70', 'max': '2.70', 'schema': schemav270},
-        {'min': '2.71', 'max': None, 'schema': schemav271}]
+        {'min': '2.71', 'max': '2.72', 'schema': schemav271},
+        {'min': '2.73', 'max': None, 'schema': schemav273}]
 
     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 99dd653..efdfe8e 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -966,18 +966,21 @@
         # A port can have more than one IP address in some cases.
         # If the network is dual-stack (IPv4 + IPv6), this port is associated
         # with 2 subnets
-        p_status = ['ACTIVE']
-        # NOTE(vsaienko) With Ironic, instances live on separate hardware
-        # servers. Neutron does not bind ports for Ironic instances, as a
-        # result the port remains in the DOWN state.
-        # TODO(vsaienko) remove once bug: #1599836 is resolved.
-        if getattr(CONF.service_available, 'ironic', False):
-            p_status.append('DOWN')
+
+        def _is_active(port):
+            # NOTE(vsaienko) With Ironic, instances live on separate hardware
+            # servers. Neutron does not bind ports for Ironic instances, as a
+            # result the port remains in the DOWN state. This has been fixed
+            # with the introduction of the networking-baremetal plugin but
+            # it's not mandatory (and is not used on all stable branches).
+            return (port['status'] == 'ACTIVE' or
+                    port.get('binding:vnic_type') == 'baremetal')
+
         port_map = [(p["id"], fxip["ip_address"])
                     for p in ports
                     for fxip in p["fixed_ips"]
                     if (netutils.is_valid_ipv4(fxip["ip_address"]) and
-                        p['status'] in p_status)]
+                        _is_active(p))]
         inactive = [p for p in ports if p['status'] != 'ACTIVE']
         if inactive:
             LOG.warning("Instance has ports that are not ACTIVE: %s", inactive)
diff --git a/tempest/test_discover/plugins.py b/tempest/test_discover/plugins.py
index 7a037eb..b20b60e 100644
--- a/tempest/test_discover/plugins.py
+++ b/tempest/test_discover/plugins.py
@@ -194,11 +194,14 @@
     def get_plugin_load_tests_tuple(self):
         load_tests_dict = {}
         for plug in self.ext_plugins:
+            LOG.info('Loading tests from Tempest plugin: %s', plug.name)
             load_tests_dict[plug.name] = plug.obj.load_tests()
         return load_tests_dict
 
     def register_plugin_opts(self, conf):
         for plug in self.ext_plugins:
+            LOG.info('Register additional config options from Tempest '
+                     'plugin: %s', plug.name)
             try:
                 plug.obj.register_opts(conf)
             except Exception:
@@ -209,6 +212,9 @@
         plugin_options = []
         for plug in self.ext_plugins:
             opt_list = plug.obj.get_opt_lists()
+            LOG.info('List additional config options registered by '
+                     'Tempest plugin: %s', plug.name)
+
             if opt_list:
                 plugin_options.extend(opt_list)
         return plugin_options
diff --git a/tempest/tests/lib/services/image/v2/test_namespaces_client.py b/tempest/tests/lib/services/image/v2/test_namespaces_client.py
index 3b057ad..db1ffae 100644
--- a/tempest/tests/lib/services/image/v2/test_namespaces_client.py
+++ b/tempest/tests/lib/services/image/v2/test_namespaces_client.py
@@ -18,16 +18,64 @@
 
 
 class TestNamespacesClient(base.BaseServiceTest):
-    FAKE_CREATE_SHOW_NAMESPACE = {
-        "namespace": "OS::Compute::Hypervisor",
-        "visibility": "public",
-        "description": "Tempest",
-        "display_name": u"\u2740(*\xb4\u25e1`*)\u2740",
-        "protected": True
+    FAKE_CREATE_NAMESPACE = {
+        "created_at": "2016-05-19T16:05:48Z",
+        "description": "A metadata definitions namespace.",
+        "display_name": "An Example Namespace",
+        "namespace": "FredCo::SomeCategory::Example",
+        "owner": "c60b1d57c5034e0d86902aedf8c49be0",
+        "protected": True,
+        "schema": "/v2/schemas/metadefs/namespace",
+        "self": "/v2/metadefs/namespaces/"
+                "FredCo::SomeCategory::Example",
+        "updated_at": "2016-05-19T16:05:48Z",
+        "visibility": "public"
+    }
+
+    FAKE_SHOW_NAMESPACE = {
+        "created_at": "2016-06-28T14:57:10Z",
+        "description": "The libvirt compute driver options.",
+        "display_name": "libvirt Driver Options",
+        "namespace": "OS::Compute::Libvirt",
+        "owner": "admin",
+        "properties": {
+            "boot_menu": {
+                "description": "If true, enables the BIOS bootmenu.",
+                "enum": [
+                    "true",
+                    "false"
+                ],
+                "title": "Boot Menu",
+                "type": "string"
+            },
+            "serial_port_count": {
+                "description": "Specifies the count of serial ports.",
+                "minimum": 0,
+                "title": "Serial Port Count",
+                "type": "integer"
+            }
+        },
+        "protected": True,
+        "resource_type_associations": [
+            {
+                "created_at": "2016-06-28T14:57:10Z",
+                "name": "OS::Glance::Image",
+                "prefix": "hw_"
+            },
+            {
+                "created_at": "2016-06-28T14:57:10Z",
+                "name": "OS::Nova::Flavor",
+                "prefix": "hw:"
+            }
+        ],
+        "schema": "/v2/schemas/metadefs/namespace",
+        "self": "/v2/metadefs/namespaces/OS::Compute::Libvirt",
+        "visibility": "public"
     }
 
     FAKE_LIST_NAMESPACES = {
-        "first": "/v2/metadefs/namespaces?sort_key=created_at&sort_dir=asc",
+        "first": "/v2/metadefs/namespaces?sort_key=created_at&"
+                 "sort_dir=asc",
         "namespaces": [
             {
                 "created_at": "2014-08-28T17:13:06Z",
@@ -89,7 +137,7 @@
         self.check_service_client_function(
             self.client.show_namespace,
             'tempest.lib.common.rest_client.RestClient.get',
-            self.FAKE_CREATE_SHOW_NAMESPACE,
+            self.FAKE_SHOW_NAMESPACE,
             bytes_body,
             namespace="OS::Compute::Hypervisor")
 
@@ -104,7 +152,7 @@
         self.check_service_client_function(
             self.client.create_namespace,
             'tempest.lib.common.rest_client.RestClient.post',
-            self.FAKE_CREATE_SHOW_NAMESPACE,
+            self.FAKE_CREATE_NAMESPACE,
             bytes_body,
             namespace="OS::Compute::Hypervisor",
             visibility="public", description="Tempest",
diff --git a/tempest/tests/lib/services/image/v2/test_resource_types_client.py b/tempest/tests/lib/services/image/v2/test_resource_types_client.py
index 74e1c36..089e62e 100644
--- a/tempest/tests/lib/services/image/v2/test_resource_types_client.py
+++ b/tempest/tests/lib/services/image/v2/test_resource_types_client.py
@@ -48,6 +48,28 @@
         ]
     }
 
+    FAKE_CREATE_RESOURCE_TYPE_ASSOCIATION = {
+        "created_at": "2020-03-07T18:20:44Z",
+        "name": "OS::Glance::Image",
+        "prefix": "hw:",
+        "updated_at": "2020-03-07T18:20:44Z"
+    }
+
+    FAKE_LIST_RESOURCE_TYPE_ASSOCIATION = {
+        "resource_type_associations": [
+            {
+                "created_at": "2020-03-07T18:20:44Z",
+                "name": "OS::Nova::Flavor",
+                "prefix": "hw:"
+            },
+            {
+                "created_at": "2020-03-07T18:20:44Z",
+                "name": "OS::Glance::Image",
+                "prefix": "hw_"
+            }
+        ]
+    }
+
     def setUp(self):
         super(TestResourceTypesClient, self).setUp()
         fake_auth = fake_auth_provider.FakeAuthProvider()
@@ -62,6 +84,25 @@
             self.FAKE_LIST_RESOURCETYPES,
             bytes_body)
 
+    def _test_create_resource_type_association(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_resource_type_association,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_CREATE_RESOURCE_TYPE_ASSOCIATION,
+            bytes_body, status=201,
+            namespace_id="OS::Compute::Hypervisor",
+            name="OS::Glance::Image", prefix="hw_",
+            )
+
+    def _test_list_resource_type_association(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_resource_type_association,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_RESOURCE_TYPE_ASSOCIATION,
+            bytes_body,
+            namespace_id="OS::Compute::Hypervisor",
+            )
+
     def test_list_resource_types_with_str_body(self):
         self._test_list_resource_types()
 
@@ -76,3 +117,15 @@
             namespace_id="OS::Compute::Hypervisor",
             resource_name="OS::Glance::Image",
             )
+
+    def test_create_resource_type_association_with_str_body(self):
+        self._test_create_resource_type_association()
+
+    def test_create_resource_type_association_with_bytes_body(self):
+        self._test_create_resource_type_association(bytes_body=True)
+
+    def test_list_resource_type_association_with_str_body(self):
+        self._test_list_resource_type_association()
+
+    def test_list_resource_type_association_with_bytes_body(self):
+        self._test_list_resource_type_association(bytes_body=True)
diff --git a/tempest/tests/lib/services/object_storage/test_object_client.py b/tempest/tests/lib/services/object_storage/test_object_client.py
index a16d1d7..1749b03 100644
--- a/tempest/tests/lib/services/object_storage/test_object_client.py
+++ b/tempest/tests/lib/services/object_storage/test_object_client.py
@@ -69,7 +69,7 @@
 
         # If the expected initial status is not 100, then an exception
         # should be thrown and the connection closed
-        if initial_status is 100:
+        if initial_status == 100:
             status, reason = \
                 self.object_client.create_object_continue(cnt, obj, req_data)
         else:
@@ -91,7 +91,7 @@
         mock_poc.return_value.endheaders.assert_called_once_with()
 
         # The following steps are only taken if the initial status is 100
-        if initial_status is 100:
+        if initial_status == 100:
             # Verify that the method returned what it was supposed to
             self.assertEqual(status, 201)
 
diff --git a/tempest/tests/test_hacking.py b/tempest/tests/test_hacking.py
index 83c1abb..7c31185 100644
--- a/tempest/tests/test_hacking.py
+++ b/tempest/tests/test_hacking.py
@@ -101,17 +101,6 @@
             'def test_fake:', './tempest/scenario/orchestration/test_fake.py',
             "\n"))
 
-    def test_no_vi_headers(self):
-        # NOTE(mtreinish)  The lines parameter is used only for finding the
-        # line location in the file. So these tests just pass a list of an
-        # arbitrary length to use for verifying the check function.
-        self.assertTrue(checks.no_vi_headers(
-            '# vim: tabstop=4 shiftwidth=4 softtabstop=4', 1, range(250)))
-        self.assertTrue(checks.no_vi_headers(
-            '# vim: tabstop=4 shiftwidth=4 softtabstop=4', 249, range(250)))
-        self.assertFalse(checks.no_vi_headers(
-            '# vim: tabstop=4 shiftwidth=4 softtabstop=4', 149, range(250)))
-
     def test_service_tags_not_in_module_path(self):
         self.assertTrue(checks.service_tags_not_in_module_path(
             "@utils.services('compute')",
diff --git a/test-requirements.txt b/test-requirements.txt
index 196387c..a50905f 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,7 +1,7 @@
 # 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>=1.1.0,<1.2.0 # Apache-2.0
+hacking>=3.0,<3.1.0;python_version>='3.5' # Apache-2.0
 mock>=2.0.0 # BSD
 coverage!=4.4,>=4.0 # Apache-2.0
 oslotest>=3.2.0 # Apache-2.0
diff --git a/tools/generate-tempest-plugins-list.py b/tools/generate-tempest-plugins-list.py
index 64adcbe..5ffef3e 100644
--- a/tools/generate-tempest-plugins-list.py
+++ b/tools/generate-tempest-plugins-list.py
@@ -36,6 +36,7 @@
 # when the patches are merged.
 BLACKLIST = [
     'x/gce-api',  # It looks gce-api doesn't support python3 yet.
+    'x/glare',  # To avoid sanity-job failure
     'x/group-based-policy',  # It looks this doesn't support python3 yet.
     'x/intel-nfv-ci-tests',  # https://review.opendev.org/#/c/634640/
     'openstack/networking-generic-switch',
@@ -48,7 +49,9 @@
     'openstack/neutron-dynamic-routing',
     # https://review.opendev.org/#/c/637718/
     'openstack/neutron-vpnaas',  # https://review.opendev.org/#/c/637719/
+    'x/tap-as-a-service',  # To avoid sanity-job failure
     'x/valet',  # https://review.opendev.org/#/c/638339/
+    'x/kingbird',  # https://bugs.launchpad.net/kingbird/+bug/1869722
 ]
 
 url = 'https://review.opendev.org/projects/'
diff --git a/tox.ini b/tox.ini
index 64921ef..e861c84 100644
--- a/tox.ini
+++ b/tox.ini
@@ -313,7 +313,6 @@
     check-uuid --fix
 
 [hacking]
-local-check-factory = tempest.hacking.checks.factory
 import_exceptions = tempest.services
 
 [flake8]
@@ -327,6 +326,26 @@
 enable-extensions = H106,H203,H904
 import-order-style = pep8
 
+[flake8:local-plugins]
+extension =
+  T102 = checks:import_no_clients_in_api_and_scenario_tests
+  T104 = checks:scenario_tests_need_service_tags
+  T105 = checks:no_setup_teardown_class_for_tests
+  T107 = checks:service_tags_not_in_module_path
+  T108 = checks:no_hyphen_at_end_of_rand_name
+  N322 = checks:no_mutable_default_args
+  T109 = checks:no_testtools_skip_decorator
+  T110 = checks:get_resources_on_service_clients
+  T111 = checks:delete_resources_on_service_clients
+  T112 = checks:dont_import_local_tempest_into_lib
+  T113 = checks:use_rand_uuid_instead_of_uuid4
+  T114 = checks:dont_use_config_in_tempest_lib
+  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
+paths =
+  ./tempest/hacking
+
 [testenv:releasenotes]
 deps =
   -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}