Move verification of response attributes into service client

Moves the verification of service/volume response body attributes to
the service client rather than in each test which uses the service
client.

Partially implements blueprint nova-api-attribute-test

Change-Id: Ie829e2beb1e065a2804dab93835c3d1933fd419d
diff --git a/tempest/api/compute/admin/test_services.py b/tempest/api/compute/admin/test_services.py
index 9dd429b..2feb825 100644
--- a/tempest/api/compute/admin/test_services.py
+++ b/tempest/api/compute/admin/test_services.py
@@ -14,7 +14,6 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.api.compute.api_schema import services as schema
 from tempest.api.compute import base
 from tempest import test
 
@@ -33,7 +32,7 @@
     @test.attr(type='gate')
     def test_list_services(self):
         resp, services = self.client.list_services()
-        self.validate_response(schema.list_services, resp, services)
+        self.assertEqual(200, resp.status)
         self.assertNotEqual(0, len(services))
 
     @test.attr(type='gate')
@@ -41,7 +40,7 @@
         binary_name = 'nova-compute'
         params = {'binary': binary_name}
         resp, services = self.client.list_services(params)
-        self.validate_response(schema.list_services, resp, services)
+        self.assertEqual(200, resp.status)
         self.assertNotEqual(0, len(services))
         for service in services:
             self.assertEqual(binary_name, service['binary'])
@@ -49,14 +48,12 @@
     @test.attr(type='gate')
     def test_get_service_by_host_name(self):
         resp, services = self.client.list_services()
-        self.validate_response(schema.list_services, resp, services)
         host_name = services[0]['host']
         services_on_host = [service for service in services if
                             service['host'] == host_name]
         params = {'host': host_name}
 
         resp, services = self.client.list_services(params)
-        self.validate_response(schema.list_services, resp, services)
 
         # we could have a periodic job checkin between the 2 service
         # lookups, so only compare binary lists.
@@ -70,13 +67,12 @@
     @test.attr(type='gate')
     def test_get_service_by_service_and_host_name(self):
         resp, services = self.client.list_services()
-        self.validate_response(schema.list_services, resp, services)
         host_name = services[0]['host']
         binary_name = services[0]['binary']
         params = {'host': host_name, 'binary': binary_name}
 
         resp, services = self.client.list_services(params)
-        self.validate_response(schema.list_services, resp, services)
+        self.assertEqual(200, resp.status)
         self.assertEqual(1, len(services))
         self.assertEqual(host_name, services[0]['host'])
         self.assertEqual(binary_name, services[0]['binary'])
diff --git a/tempest/api/compute/api_schema/services.py b/tempest/api/compute/api_schema/services.py
deleted file mode 100644
index ef5868c..0000000
--- a/tempest/api/compute/api_schema/services.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright 2014 NEC 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.
-
-list_services = {
-    'status_code': [200],
-    'response_body': {
-        'type': 'array',
-        'items': {
-            'type': 'object',
-            'properties': {
-                # NOTE: Now the type of 'id' is integer, but here allows
-                # 'string' also because we will be able to change it to
-                # 'uuid' in the future.
-                'id': {'type': ['integer', 'string']},
-                'zone': {'type': 'string'},
-                'host': {'type': 'string'},
-                'state': {'type': 'string'},
-                'binary': {'type': 'string'},
-                'status': {'type': 'string'},
-                'updated_at': {'type': 'string'},
-                'disabled_reason': {'type': ['string', 'null']},
-            },
-            'required': ['id', 'zone', 'host', 'state', 'binary', 'status',
-                         'updated_at', 'disabled_reason'],
-        },
-    }
-}
diff --git a/tempest/api/compute/api_schema/v2/volumes.py b/tempest/api/compute/api_schema/v2/volumes.py
deleted file mode 100644
index 446a446..0000000
--- a/tempest/api/compute/api_schema/v2/volumes.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# Copyright 2014 NEC 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.
-
-get_volume = {
-    'status_code': [200],
-    'response_body': {
-        'type': 'object',
-        'properties': {
-            # NOTE: Now the type of 'id' is integer, but here allows
-            # 'string' also because we will be able to change it to
-            # 'uuid' in the future.
-            'id': {'type': ['integer', 'string']},
-            'status': {'type': 'string'},
-            'displayName': {'type': ['string', 'null']},
-            'availabilityZone': {'type': 'string'},
-            'createdAt': {'type': 'string'},
-            'displayDescription': {'type': ['string', 'null']},
-            'volumeType': {'type': 'string'},
-            'snapshotId': {'type': ['string', 'null']},
-            'metadata': {'type': 'object'},
-            'size': {'type': 'integer'},
-            'attachments': {
-                'type': 'array',
-                'items': {
-                    'type': 'object',
-                    'properties': {
-                        'id': {'type': ['integer', 'string']},
-                        'device': {'type': 'string'},
-                        'volumeId': {'type': ['integer', 'string']},
-                        'serverId': {'type': ['integer', 'string']},
-                    },
-                },
-            },
-        },
-        'required': ['id', 'status', 'displayName', 'availabilityZone',
-                     'createdAt', 'displayDescription', 'volumeType',
-                     'snapshotId', 'metadata', 'size', 'attachments'],
-    },
-}
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index b2f3117..abd36a6 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -15,8 +15,6 @@
 
 import time
 
-import jsonschema
-
 from tempest import clients
 from tempest.common.utils import data_utils
 from tempest import config
@@ -178,32 +176,6 @@
 
         return resp, body
 
-    @classmethod
-    def validate_response(cls, schema, resp, body):
-        response_code = schema['status_code']
-        if resp.status not in response_code:
-            msg = ("The status code(%s) is different than the expected "
-                   "one(%s)") % (resp.status, response_code)
-            raise exceptions.InvalidHttpSuccessCode(msg)
-        response_schema = schema.get('response_body')
-        if response_schema:
-            if cls._interface == 'xml':
-                # NOTE: xml client of Tempest is broken and cannot get some
-                # keys. The best way is to fix it, but now xml format has been
-                # marked as "deprecated" in Nova API and xml client will be
-                # removed from Tempest.
-                # So now this test does not check attributes if xml.
-                return
-            try:
-                jsonschema.validate(body, response_schema)
-            except jsonschema.ValidationError as ex:
-                msg = ("HTTP response body is invalid (%s)") % ex
-                raise exceptions.InvalidHTTPResponseBody(msg)
-        else:
-            if body:
-                msg = ("HTTP response body should not exist (%s)") % body
-                raise exceptions.InvalidHTTPResponseBody(msg)
-
     def wait_for(self, condition):
         """Repeatedly calls condition() until a timeout."""
         start_time = int(time.time())
diff --git a/tempest/api/compute/v3/admin/test_services.py b/tempest/api/compute/v3/admin/test_services.py
index 0a7c7f1..b367dad 100644
--- a/tempest/api/compute/v3/admin/test_services.py
+++ b/tempest/api/compute/v3/admin/test_services.py
@@ -14,7 +14,6 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.api.compute.api_schema import services as schema
 from tempest.api.compute import base
 from tempest.test import attr
 
@@ -33,7 +32,7 @@
     @attr(type='gate')
     def test_list_services(self):
         resp, services = self.client.list_services()
-        self.validate_response(schema.list_services, resp, services)
+        self.assertEqual(200, resp.status)
         self.assertNotEqual(0, len(services))
 
     @attr(type='gate')
@@ -41,7 +40,7 @@
         binary_name = 'nova-compute'
         params = {'binary': binary_name}
         resp, services = self.client.list_services(params)
-        self.validate_response(schema.list_services, resp, services)
+        self.assertEqual(200, resp.status)
         self.assertNotEqual(0, len(services))
         for service in services:
             self.assertEqual(binary_name, service['binary'])
@@ -49,14 +48,13 @@
     @attr(type='gate')
     def test_get_service_by_host_name(self):
         resp, services = self.client.list_services()
-        self.validate_response(schema.list_services, resp, services)
+        self.assertEqual(200, resp.status)
         host_name = services[0]['host']
         services_on_host = [service for service in services if
                             service['host'] == host_name]
         params = {'host': host_name}
 
         resp, services = self.client.list_services(params)
-        self.validate_response(schema.list_services, resp, services)
 
         # we could have a periodic job checkin between the 2 service
         # lookups, so only compare binary lists.
@@ -70,13 +68,12 @@
     @attr(type='gate')
     def test_get_service_by_service_and_host_name(self):
         resp, services = self.client.list_services()
-        self.validate_response(schema.list_services, resp, services)
         host_name = services[0]['host']
         binary_name = services[0]['binary']
         params = {'host': host_name, 'binary': binary_name}
 
         resp, services = self.client.list_services(params)
-        self.validate_response(schema.list_services, resp, services)
+        self.assertEqual(200, resp.status)
         self.assertEqual(1, len(services))
         self.assertEqual(host_name, services[0]['host'])
         self.assertEqual(binary_name, services[0]['binary'])
diff --git a/tempest/api/compute/volumes/test_volumes_get.py b/tempest/api/compute/volumes/test_volumes_get.py
index e7179cc..c3d6ba6 100644
--- a/tempest/api/compute/volumes/test_volumes_get.py
+++ b/tempest/api/compute/volumes/test_volumes_get.py
@@ -13,7 +13,6 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from tempest.api.compute.api_schema.v2 import volumes as schema
 from tempest.api.compute import base
 from tempest.common.utils import data_utils
 from tempest import config
@@ -44,7 +43,9 @@
                                                  display_name=v_name,
                                                  metadata=metadata)
         self.addCleanup(self.delete_volume, volume['id'])
-        self.validate_response(schema.get_volume, resp, volume)
+        self.assertEqual(200, resp.status)
+        self.assertIn('id', volume)
+        self.assertIn('displayName', volume)
         self.assertEqual(volume['displayName'], v_name,
                          "The created volume name is not equal "
                          "to the requested name")
@@ -54,7 +55,7 @@
         self.client.wait_for_volume_status(volume['id'], 'available')
         # GET Volume
         resp, fetched_volume = self.client.get_volume(volume['id'])
-        self.validate_response(schema.get_volume, resp, fetched_volume)
+        self.assertEqual(200, resp.status)
         # Verification of details of fetched Volume
         self.assertEqual(v_name,
                          fetched_volume['displayName'],
diff --git a/tempest/api/compute/api_schema/__init__.py b/tempest/api_schema/__init__.py
similarity index 100%
rename from tempest/api/compute/api_schema/__init__.py
rename to tempest/api_schema/__init__.py
diff --git a/tempest/api/compute/api_schema/__init__.py b/tempest/api_schema/compute/__init__.py
similarity index 100%
copy from tempest/api/compute/api_schema/__init__.py
copy to tempest/api_schema/compute/__init__.py
diff --git a/tempest/api_schema/compute/services.py b/tempest/api_schema/compute/services.py
new file mode 100644
index 0000000..4793f5a
--- /dev/null
+++ b/tempest/api_schema/compute/services.py
@@ -0,0 +1,44 @@
+# Copyright 2014 NEC 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.
+
+list_services = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'services': {
+                'type': 'array',
+                'items': {
+                    'type': 'object',
+                    'properties': {
+                        # NOTE: Now the type of 'id' is integer, but here
+                        # allows 'string' also because we will be able to
+                        # change it to 'uuid' in the future.
+                        'id': {'type': ['integer', 'string']},
+                        'zone': {'type': 'string'},
+                        'host': {'type': 'string'},
+                        'state': {'type': 'string'},
+                        'binary': {'type': 'string'},
+                        'status': {'type': 'string'},
+                        'updated_at': {'type': 'string'},
+                        'disabled_reason': {'type': ['string', 'null']}
+                    },
+                    'required': ['id', 'zone', 'host', 'state', 'binary',
+                                 'status', 'updated_at', 'disabled_reason']
+                }
+            }
+        },
+        'required': ['services']
+    }
+}
diff --git a/tempest/api/compute/api_schema/v2/__init__.py b/tempest/api_schema/compute/v2/__init__.py
similarity index 100%
rename from tempest/api/compute/api_schema/v2/__init__.py
rename to tempest/api_schema/compute/v2/__init__.py
diff --git a/tempest/api_schema/compute/v2/volumes.py b/tempest/api_schema/compute/v2/volumes.py
new file mode 100644
index 0000000..16ed7c2
--- /dev/null
+++ b/tempest/api_schema/compute/v2/volumes.py
@@ -0,0 +1,56 @@
+# Copyright 2014 NEC 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.
+
+get_volume = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'volume': {
+                'type': 'object',
+                'properties': {
+                    # NOTE: Now the type of 'id' is integer, but here allows
+                    # 'string' also because we will be able to change it to
+                    # 'uuid' in the future.
+                    'id': {'type': ['integer', 'string']},
+                    'status': {'type': 'string'},
+                    'displayName': {'type': ['string', 'null']},
+                    'availabilityZone': {'type': 'string'},
+                    'createdAt': {'type': 'string'},
+                    'displayDescription': {'type': ['string', 'null']},
+                    'volumeType': {'type': 'string'},
+                    'snapshotId': {'type': ['string', 'null']},
+                    'metadata': {'type': 'object'},
+                    'size': {'type': 'integer'},
+                    'attachments': {
+                        'type': 'array',
+                        'items': {
+                            'type': 'object',
+                            'properties': {
+                                'id': {'type': ['integer', 'string']},
+                                'device': {'type': 'string'},
+                                'volumeId': {'type': ['integer', 'string']},
+                                'serverId': {'type': ['integer', 'string']}
+                            }
+                        }
+                    }
+                },
+                'required': ['id', 'status', 'displayName', 'availabilityZone',
+                             'createdAt', 'displayDescription', 'volumeType',
+                             'snapshotId', 'metadata', 'size', 'attachments']
+            }
+        },
+        'required': ['volume']
+    }
+}
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index 36ddb40..88dbe58 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -21,6 +21,8 @@
 import re
 import time
 
+import jsonschema
+
 from tempest.common import http
 from tempest import config
 from tempest import exceptions
@@ -502,6 +504,31 @@
                    % self.__class__.__name__)
         raise NotImplementedError(message)
 
+    @classmethod
+    def validate_response(cls, schema, resp, body):
+        # Only check the response if the status code is a success code
+        # TODO(cyeoh): Eventually we should be able to verify that a failure
+        # code if it exists is something that we expect. This is explicitly
+        # declared in the V3 API and so we should be able to export this in
+        # the response schema. For now we'll ignore it.
+        if str(resp.status).startswith('2'):
+            response_code = schema['status_code']
+            if resp.status not in response_code:
+                msg = ("The status code(%s) is different than the expected "
+                       "one(%s)") % (resp.status, response_code)
+                raise exceptions.InvalidHttpSuccessCode(msg)
+            response_schema = schema.get('response_body')
+            if response_schema:
+                try:
+                    jsonschema.validate(body, response_schema)
+                except jsonschema.ValidationError as ex:
+                    msg = ("HTTP response body is invalid (%s)") % ex
+                    raise exceptions.InvalidHTTPResponseBody(msg)
+            else:
+                if body:
+                    msg = ("HTTP response body should not exist (%s)") % body
+                    raise exceptions.InvalidHTTPResponseBody(msg)
+
 
 class NegativeRestClient(RestClient):
     """
diff --git a/tempest/services/compute/json/services_client.py b/tempest/services/compute/json/services_client.py
index 1ab25ec..0f7d4cb 100644
--- a/tempest/services/compute/json/services_client.py
+++ b/tempest/services/compute/json/services_client.py
@@ -17,6 +17,7 @@
 import json
 import urllib
 
+from tempest.api_schema.compute import services as schema
 from tempest.common import rest_client
 from tempest import config
 
@@ -36,6 +37,7 @@
 
         resp, body = self.get(url)
         body = json.loads(body)
+        self.validate_response(schema.list_services, resp, body)
         return resp, body['services']
 
     def enable_service(self, host_name, binary):
diff --git a/tempest/services/compute/json/volumes_extensions_client.py b/tempest/services/compute/json/volumes_extensions_client.py
index 5ef11ed..451dbac 100644
--- a/tempest/services/compute/json/volumes_extensions_client.py
+++ b/tempest/services/compute/json/volumes_extensions_client.py
@@ -17,6 +17,7 @@
 import time
 import urllib
 
+from tempest.api_schema.compute.v2 import volumes as schema
 from tempest.common import rest_client
 from tempest import config
 from tempest import exceptions
@@ -58,6 +59,7 @@
         url = "os-volumes/%s" % str(volume_id)
         resp, body = self.get(url)
         body = json.loads(body)
+        self.validate_response(schema.get_volume, resp, body)
         return resp, body['volume']
 
     def create_volume(self, size, **kwargs):
diff --git a/tempest/services/compute/v3/json/services_client.py b/tempest/services/compute/v3/json/services_client.py
index b4e65a0..88c4d16 100644
--- a/tempest/services/compute/v3/json/services_client.py
+++ b/tempest/services/compute/v3/json/services_client.py
@@ -17,6 +17,7 @@
 import json
 import urllib
 
+from tempest.api_schema.compute import services as schema
 from tempest.common import rest_client
 from tempest import config
 
@@ -36,6 +37,7 @@
 
         resp, body = self.get(url)
         body = json.loads(body)
+        self.validate_response(schema.list_services, resp, body)
         return resp, body['services']
 
     def enable_service(self, host_name, binary):