Add support of schema versioning for microversion

For API microversion testing, service clients can be
requested with any supported API microversions.
Compute service clients does response validation using response
schema.

To validate the corresponding microversion response with appropriate
schema, we need to add support of versioning in schema.

Each service client having multiple schema for microversions,
needs to define a list of dict with schema versions information.
Based on that information and requested microversion, schema
will be selected.

Example-

from tempest.api_schema.response.compute.v2_1 import xyz as schemav21
from tempest.api_schema.response.compute.v2_2 import xyz as schemav22
from tempest.api_schema.response.compute.v2_10 import abc as schemav210
.
.
.
    schema_versions_info = [
        {'min': None, 'max': '2.1', 'schema': schemav21},
        {'min': '2.2', 'max': '2.9', 'schema': schemav22},
        {'min': '2.10', 'max': None, 'schema': schemav210}]

This can be defined at class level or method level depends on how
schema differ for that service client.

This commits add the logic to select the valid schema for requested API
microversion.

Partially implements blueprint api-microversions-testing-support

Change-Id: I7c87287ca9f6f60a66aab8290031632945c03d59
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 1d725af..931737d 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -181,6 +181,11 @@
                "be of format MajorNum.MinorNum or string 'latest'.")
 
 
+class JSONSchemaNotFound(TempestException):
+    message = ("JSON Schema for %(version)s is not found in \n"
+               " %(schema_versions_info)s")
+
+
 class CommandFailed(Exception):
     def __init__(self, returncode, cmd, output, stderr):
         super(CommandFailed, self).__init__()
diff --git a/tempest/services/compute/json/base.py b/tempest/services/compute/json/base.py
index ea1a696..40d3056 100644
--- a/tempest/services/compute/json/base.py
+++ b/tempest/services/compute/json/base.py
@@ -14,6 +14,9 @@
 
 from tempest_lib.common import rest_client
 
+from tempest.common import api_version_request
+from tempest import exceptions
+
 
 class BaseComputeClient(rest_client.RestClient):
     api_microversion = None
@@ -26,3 +29,36 @@
 
     def set_api_microversion(self, microversion):
         self.api_microversion = microversion
+
+    def get_schema(self, schema_versions_info):
+        """Get JSON schema
+
+        This method provides the matching schema for requested
+        microversion (self.api_microversion).
+        :param schema_versions_info: List of dict which provides schema
+        information with range of valid versions.
+        Example -
+        schema_versions_info = [
+            {'min': None, 'max': '2.1', 'schema': schemav21},
+            {'min': '2.2', 'max': '2.9', 'schema': schemav22},
+            {'min': '2.10', 'max': None, 'schema': schemav210}]
+        """
+        schema = None
+        version = api_version_request.APIVersionRequest(self.api_microversion)
+        for items in schema_versions_info:
+            min_version = api_version_request.APIVersionRequest(items['min'])
+            max_version = api_version_request.APIVersionRequest(items['max'])
+            # This is case where self.api_microversion is None, which means
+            # request without microversion So select base v2.1 schema.
+            if version.is_null() and items['min'] is None:
+                schema = items['schema']
+                break
+            # else select appropriate schema as per self.api_microversion
+            elif version.matches(min_version, max_version):
+                schema = items['schema']
+                break
+        if schema is None:
+            raise exceptions.JSONSchemaNotFound(
+                version=version.get_string(),
+                schema_versions_info=schema_versions_info)
+        return schema
diff --git a/tempest/tests/services/compute/test_base_compute_client.py b/tempest/tests/services/compute/test_base_compute_client.py
index 13461e4..134fe39 100644
--- a/tempest/tests/services/compute/test_base_compute_client.py
+++ b/tempest/tests/services/compute/test_base_compute_client.py
@@ -16,6 +16,7 @@
 import mock
 from tempest_lib.common import rest_client
 
+from tempest import exceptions
 from tempest.services.compute.json import base as base_compute_client
 from tempest.tests import fake_auth_provider
 from tempest.tests.services.compute import base
@@ -70,3 +71,82 @@
                                'raw_request') as mock_get:
             mock_get.side_effect = raw_request
             self.client.get('fake_url')
+
+
+class DummyServiceClient1(base_compute_client.BaseComputeClient):
+    schema_versions_info = [
+        {'min': None, 'max': '2.1', 'schema': 'schemav21'},
+        {'min': '2.2', 'max': '2.9', 'schema': 'schemav22'},
+        {'min': '2.10', 'max': None, 'schema': 'schemav210'}]
+
+    def return_selected_schema(self):
+        return self.get_schema(self.schema_versions_info)
+
+
+class TestSchemaVersionsNone(base.BaseComputeServiceTest):
+    api_microversion = None
+    expected_schema = 'schemav21'
+
+    def setUp(self):
+        super(TestSchemaVersionsNone, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = DummyServiceClient1(fake_auth, 'compute', 'regionOne')
+        self.client.api_microversion = self.api_microversion
+
+    def test_schema(self):
+        self.assertEqual(self.expected_schema,
+                         self.client.return_selected_schema())
+
+
+class TestSchemaVersionsV21(TestSchemaVersionsNone):
+    api_microversion = '2.1'
+    expected_schema = 'schemav21'
+
+
+class TestSchemaVersionsV22(TestSchemaVersionsNone):
+    api_microversion = '2.2'
+    expected_schema = 'schemav22'
+
+
+class TestSchemaVersionsV25(TestSchemaVersionsNone):
+    api_microversion = '2.5'
+    expected_schema = 'schemav22'
+
+
+class TestSchemaVersionsV29(TestSchemaVersionsNone):
+    api_microversion = '2.9'
+    expected_schema = 'schemav22'
+
+
+class TestSchemaVersionsV210(TestSchemaVersionsNone):
+    api_microversion = '2.10'
+    expected_schema = 'schemav210'
+
+
+class TestSchemaVersionsLatest(TestSchemaVersionsNone):
+    api_microversion = 'latest'
+    expected_schema = 'schemav210'
+
+
+class DummyServiceClient2(base_compute_client.BaseComputeClient):
+    schema_versions_info = [
+        {'min': None, 'max': '2.1', 'schema': 'schemav21'},
+        {'min': '2.2', 'max': '2.9', 'schema': 'schemav22'}]
+
+    def return_selected_schema(self):
+        return self.get_schema(self.schema_versions_info)
+
+
+class TestSchemaVersionsNotFound(base.BaseComputeServiceTest):
+    api_microversion = '2.10'
+    expected_schema = 'schemav210'
+
+    def setUp(self):
+        super(TestSchemaVersionsNotFound, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = DummyServiceClient2(fake_auth, 'compute', 'regionOne')
+        self.client.api_microversion = self.api_microversion
+
+    def test_schema(self):
+        self.assertRaises(exceptions.JSONSchemaNotFound,
+                          self.client.return_selected_schema)