Merge "compute: Move device tagging tests to use wait_until=SSHABLE"
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..ff6237e 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -418,6 +418,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
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_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/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/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_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/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/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/integrated-gate.yaml b/zuul.d/integrated-gate.yaml
index fad17dd..d35e25d 100644
--- a/zuul.d/integrated-gate.yaml
+++ b/zuul.d/integrated-gate.yaml
@@ -335,6 +335,8 @@
     check:
       jobs:
         - grenade
+        - grenade-skip-level:
+            voting: false
         - tempest-integrated-networking
         - openstacksdk-functional-devstack
     gate:
@@ -352,6 +354,8 @@
       run on Nova gate only.
     check:
       jobs:
+        - grenade-skip-level:
+            voting: false
         - tempest-integrated-compute
         - tempest-integrated-compute-centos-8-stream
         - openstacksdk-functional-devstack
@@ -371,6 +375,8 @@
     check:
       jobs:
         - grenade
+        - grenade-skip-level:
+            voting: false
         - tempest-integrated-placement
         - openstacksdk-functional-devstack
     gate:
@@ -389,6 +395,8 @@
     check:
       jobs:
         - grenade
+        - grenade-skip-level:
+            voting: false
         - tempest-integrated-storage
         - openstacksdk-functional-devstack
     gate:
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: |