Merge "Add image "version" metadata in test_snapshot_pattern"
diff --git a/tempest/api/compute/servers/test_server_rescue.py b/tempest/api/compute/servers/test_server_rescue.py
index 17cd443..5a646f9 100644
--- a/tempest/api/compute/servers/test_server_rescue.py
+++ b/tempest/api/compute/servers/test_server_rescue.py
@@ -110,14 +110,14 @@
     create_default_network = True
 
     @classmethod
-    def skip_checks(self):
-        super(BaseServerStableDeviceRescueTest, self).skip_checks()
+    def skip_checks(cls):
+        super(BaseServerStableDeviceRescueTest, cls).skip_checks()
         if not CONF.compute_feature_enabled.rescue:
             msg = "Server rescue not available."
-            raise self.skipException(msg)
+            raise cls.skipException(msg)
         if not CONF.compute_feature_enabled.stable_rescue:
             msg = "Stable rescue not available."
-            raise self.skipException(msg)
+            raise cls.skipException(msg)
 
     def _create_server_and_rescue_image(self, hw_rescue_device=None,
                                         hw_rescue_bus=None,
diff --git a/tempest/lib/api_schema/response/volume/scheduler_stats.py b/tempest/lib/api_schema/response/volume/scheduler_stats.py
new file mode 100644
index 0000000..b5d7d2c
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/scheduler_stats.py
@@ -0,0 +1,79 @@
+# Copyright 2018 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.
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
+get_pools_no_detail = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'pools': {
+                'type': 'array',
+                'items': {
+                    'type': 'object',
+                    'properties': {
+                        'name': {'type': 'string'},
+                    },
+                    'additionalProperties': False,
+                    'required': ['name']
+                }
+            }
+        },
+        'additionalProperties': False,
+        'required': ['pools'],
+    }
+}
+
+get_pools_with_detail = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'pools': {
+                'type': 'array',
+                'items': {
+                    'type': 'object',
+                    'properties': {
+                        'name': {'type': 'string'},
+                        'capabilities': {
+                            'type': ['object', 'null'],
+                            'properties': {
+                                'updated': parameter_types.date_time_or_null,
+                                'QoS_support': {'type': 'boolean'},
+                                'total_capacity_gb': {
+                                    'type': ['number', 'string']
+                                },
+                                'volume_backend_name': {'type': 'string'},
+                                'free_capacity_gb': {
+                                    'type': ['number', 'string']
+                                },
+                                'driver_version': {'type': 'string'},
+                                'reserved_percentage': {'type': 'integer'},
+                                'storage_protocol': {'type': 'string'},
+                                'vendor_name': {'type': 'string'},
+                                'timestamp': parameter_types.date_time_or_null
+                            },
+                            # Because some legacy volumes or backends may not
+                            # support pools, so no required fields here.
+                        },
+                    },
+                    'additionalProperties': False,
+                    'required': ['name', 'capabilities']
+                }
+            }
+        },
+        'additionalProperties': False,
+        'required': ['pools'],
+    }
+}
diff --git a/tempest/lib/api_schema/response/volume/snapshots.py b/tempest/lib/api_schema/response/volume/snapshots.py
new file mode 100644
index 0000000..e9aeb64
--- /dev/null
+++ b/tempest/lib/api_schema/response/volume/snapshots.py
@@ -0,0 +1,198 @@
+# Copyright 2018 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_1 import parameter_types
+
+metadata = {
+    'type': 'object',
+    'patternProperties': {
+        '^.+$': {'type': 'string'}
+    }
+}
+
+common_snapshot_schema = {
+    'type': 'object',
+    'properties': {
+        'status': {'type': 'string'},
+        'description': {'type': ['string', 'null']},
+        'created_at': parameter_types.date_time,
+        'name': {'type': 'string'},
+        'volume_id': {'type': 'string', 'format': 'uuid'},
+        'metadata': metadata,
+        'id': {'type': 'string', 'format': 'uuid'},
+        'size': {'type': 'integer'},
+        'updated_at': parameter_types.date_time_or_null,
+        # TODO(zhufl): user_id is added in 3.41, we should move it
+        # to the 3.41 schema file when microversion is supported
+        # in volume interfaces
+        # 'user_id': {'type': 'string', 'format': 'uuid'}
+    },
+    'additionalProperties': False,
+    'required': ['status', 'description', 'created_at', 'metadata',
+                 'name', 'volume_id', 'id', 'size', 'updated_at']
+}
+
+common_snapshot_detail_schema = copy.deepcopy(common_snapshot_schema)
+common_snapshot_detail_schema['properties'].update(
+    {'os-extended-snapshot-attributes:progress': {'type': 'string'},
+     'os-extended-snapshot-attributes:project_id': {
+         'type': 'string', 'format': 'uuid'}})
+common_snapshot_detail_schema['required'].extend(
+    ['os-extended-snapshot-attributes:progress',
+     'os-extended-snapshot-attributes:project_id'])
+
+list_snapshots_no_detail = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'snapshots': {
+                'type': 'array',
+                'items': common_snapshot_schema
+            },
+            'snapshots_links': parameter_types.links,
+            # TODO(zhufl): count is added in 3.45, we should move
+            # it to the 3.45 schema file when microversion is
+            # supported in volume interfaces
+            # 'count': {'type': 'integer'}
+        },
+        'additionalProperties': False,
+        'required': ['snapshots'],
+    }
+}
+
+list_snapshots_with_detail = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'snapshots': {
+                'type': 'array',
+                'items': common_snapshot_detail_schema
+            },
+            'snapshots_links': parameter_types.links,
+            # TODO(zhufl): count is added in 3.45, we should move
+            # it to the 3.45 schema file when microversion is
+            # supported in volume interfaces
+            # 'count': {'type': 'integer'},
+        },
+        'additionalProperties': False,
+        'required': ['snapshots'],
+    }
+}
+
+show_snapshot = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'snapshot': common_snapshot_detail_schema
+        },
+        'additionalProperties': False,
+        'required': ['snapshot'],
+    }
+}
+
+create_snapshot = {
+    'status_code': [202],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'snapshot': common_snapshot_schema
+        },
+        'additionalProperties': False,
+        'required': ['snapshot'],
+    }
+}
+
+update_snapshot = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'snapshot': common_snapshot_schema
+        },
+        'additionalProperties': False,
+        'required': ['snapshot'],
+    }
+}
+
+delete_snapshot = {'status_code': [202]}
+reset_snapshot_status = {'status_code': [202]}
+update_snapshot_status = {'status_code': [202]}
+
+create_snapshot_metadata = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'metadata': metadata
+        },
+        'additionalProperties': False,
+        'required': ['metadata'],
+    }
+}
+
+show_snapshot_metadata = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'metadata': metadata
+        },
+        'additionalProperties': False,
+        'required': ['metadata'],
+    }
+}
+
+update_snapshot_metadata = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'metadata': metadata
+        },
+        'additionalProperties': False,
+        'required': ['metadata'],
+    }
+}
+
+show_snapshot_metadata_item = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'meta': metadata
+        },
+        'additionalProperties': False,
+        'required': ['meta'],
+    }
+}
+
+update_snapshot_metadata_item = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'meta': metadata
+        },
+        'additionalProperties': False,
+        'required': ['meta'],
+    }
+}
+
+delete_snapshot_metadata_item = {'status_code': [200]}
+force_delete_snapshot = {'status_code': [202]}
+unmanage_snapshot = {'status_code': [202]}
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index cbf7a8c..3ceecda 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -211,6 +211,9 @@
                                post_body)
         if body:
             body = json.loads(body)
+        else:
+            if isinstance(body, bytes):
+                body = body.decode('utf-8')
         self.validate_response(schema, resp, body)
         return rest_client.ResponseBody(resp, body)
 
diff --git a/tempest/lib/services/volume/v3/scheduler_stats_client.py b/tempest/lib/services/volume/v3/scheduler_stats_client.py
index 2ae8600..e18980d 100644
--- a/tempest/lib/services/volume/v3/scheduler_stats_client.py
+++ b/tempest/lib/services/volume/v3/scheduler_stats_client.py
@@ -15,6 +15,7 @@
 
 from oslo_serialization import jsonutils as json
 
+from tempest.lib.api_schema.response.volume import scheduler_stats as schema
 from tempest.lib.common import rest_client
 
 
@@ -28,9 +29,11 @@
         https://docs.openstack.org/api-ref/block-storage/v3/index.html#list-all-back-end-storage-pools
         """
         url = 'scheduler-stats/get_pools'
+        schema_get_pools = schema.get_pools_no_detail
         if detail:
             url += '?detail=True'
+            schema_get_pools = schema.get_pools_with_detail
         resp, body = self.get(url)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(schema_get_pools, resp, body)
         return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/snapshots_client.py b/tempest/lib/services/volume/v3/snapshots_client.py
index 264381d..8ca2044 100644
--- a/tempest/lib/services/volume/v3/snapshots_client.py
+++ b/tempest/lib/services/volume/v3/snapshots_client.py
@@ -16,6 +16,7 @@
 from oslo_serialization import jsonutils as json
 from six.moves.urllib import parse as urllib
 
+from tempest.lib.api_schema.response.volume import snapshots as schema
 from tempest.lib.common import rest_client
 from tempest.lib import exceptions as lib_exc
 
@@ -32,14 +33,16 @@
         https://docs.openstack.org/api-ref/block-storage/v3/index.html#list-snapshots-and-details
         """
         url = 'snapshots'
+        list_schema = schema.list_snapshots_no_detail
         if detail:
             url += '/detail'
+            list_schema = schema.list_snapshots_with_detail
         if params:
             url += '?%s' % urllib.urlencode(params)
 
         resp, body = self.get(url)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(list_schema, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def show_snapshot(self, snapshot_id):
@@ -52,7 +55,7 @@
         url = "snapshots/%s" % snapshot_id
         resp, body = self.get(url)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(schema.show_snapshot, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def create_snapshot(self, **kwargs):
@@ -65,7 +68,7 @@
         post_body = json.dumps({'snapshot': kwargs})
         resp, body = self.post('snapshots', post_body)
         body = json.loads(body)
-        self.expected_success(202, resp.status)
+        self.validate_response(schema.create_snapshot, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def update_snapshot(self, snapshot_id, **kwargs):
@@ -78,7 +81,7 @@
         put_body = json.dumps({'snapshot': kwargs})
         resp, body = self.put('snapshots/%s' % snapshot_id, put_body)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(schema.update_snapshot, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def delete_snapshot(self, snapshot_id):
@@ -89,7 +92,7 @@
         https://docs.openstack.org/api-ref/block-storage/v3/index.html#delete-a-snapshot
         """
         resp, body = self.delete("snapshots/%s" % snapshot_id)
-        self.expected_success(202, resp.status)
+        self.validate_response(schema.delete_snapshot, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def is_resource_deleted(self, id):
@@ -108,7 +111,7 @@
         """Reset the specified snapshot's status."""
         post_body = json.dumps({'os-reset_status': {"status": status}})
         resp, body = self.post('snapshots/%s/action' % snapshot_id, post_body)
-        self.expected_success(202, resp.status)
+        self.validate_response(schema.reset_snapshot_status, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def update_snapshot_status(self, snapshot_id, **kwargs):
@@ -121,7 +124,7 @@
         post_body = json.dumps({'os-update_snapshot_status': kwargs})
         url = 'snapshots/%s/action' % snapshot_id
         resp, body = self.post(url, post_body)
-        self.expected_success(202, resp.status)
+        self.validate_response(schema.update_snapshot_status, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def create_snapshot_metadata(self, snapshot_id, metadata):
@@ -135,7 +138,7 @@
         url = "snapshots/%s/metadata" % snapshot_id
         resp, body = self.post(url, put_body)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(schema.create_snapshot_metadata, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def show_snapshot_metadata(self, snapshot_id):
@@ -148,7 +151,7 @@
         url = "snapshots/%s/metadata" % snapshot_id
         resp, body = self.get(url)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(schema.show_snapshot_metadata, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def update_snapshot_metadata(self, snapshot_id, **kwargs):
@@ -162,7 +165,7 @@
         url = "snapshots/%s/metadata" % snapshot_id
         resp, body = self.put(url, put_body)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(schema.update_snapshot_metadata, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def show_snapshot_metadata_item(self, snapshot_id, id):
@@ -170,7 +173,7 @@
         url = "snapshots/%s/metadata/%s" % (snapshot_id, id)
         resp, body = self.get(url)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(schema.show_snapshot_metadata_item, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def update_snapshot_metadata_item(self, snapshot_id, id, **kwargs):
@@ -184,21 +187,23 @@
         url = "snapshots/%s/metadata/%s" % (snapshot_id, id)
         resp, body = self.put(url, put_body)
         body = json.loads(body)
-        self.expected_success(200, resp.status)
+        self.validate_response(
+            schema.update_snapshot_metadata_item, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def delete_snapshot_metadata_item(self, snapshot_id, id):
         """Delete metadata item for the snapshot."""
         url = "snapshots/%s/metadata/%s" % (snapshot_id, id)
         resp, body = self.delete(url)
-        self.expected_success(200, resp.status)
+        self.validate_response(
+            schema.delete_snapshot_metadata_item, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def force_delete_snapshot(self, snapshot_id):
         """Force Delete Snapshot."""
         post_body = json.dumps({'os-force_delete': {}})
         resp, body = self.post('snapshots/%s/action' % snapshot_id, post_body)
-        self.expected_success(202, resp.status)
+        self.validate_response(schema.force_delete_snapshot, resp, body)
         return rest_client.ResponseBody(resp, body)
 
     def unmanage_snapshot(self, snapshot_id):
@@ -206,5 +211,5 @@
         post_body = json.dumps({'os-unmanage': {}})
         url = 'snapshots/%s/action' % (snapshot_id)
         resp, body = self.post(url, post_body)
-        self.expected_success(202, resp.status)
+        self.validate_response(schema.unmanage_snapshot, resp, body)
         return rest_client.ResponseBody(resp, body)
diff --git a/tempest/tests/cmd/test_cleanup_services.py b/tempest/tests/cmd/test_cleanup_services.py
index 8366290..7bf7315 100644
--- a/tempest/tests/cmd/test_cleanup_services.py
+++ b/tempest/tests/cmd/test_cleanup_services.py
@@ -274,19 +274,22 @@
                     "name": "test"
                 },
                 "name": "test-volume-snapshot",
-                "user_id": "40c2102f4a554b848d96b14f3eec39ed",
                 "volume_id": "173f7b48-c4c1-4e70-9acc-086b39073506",
                 "created_at": "2015-11-29T02:25:51.000000",
                 "size": 1,
                 "updated_at": "2015-11-20T05:36:40.000000",
-                "os-extended-snapshot-attributes:progress": "100%",
                 "id": "b1323cda-8e4b-41c1-afc5-2fc791809c8c",
                 "description": "volume snapshot"
             },
             {
                 "status": "available",
                 "name": "saved-snapshot",
+                "metadata": {},
                 "id": "1ad4c789-7e8w-4dwg-afc5",
+                "size": 1,
+                "volume_id": "af7c41be-1ff6-4233-a690-7ed61c34347f",
+                "created_at": "2015-11-20T05:39:40.000000",
+                "updated_at": "2015-11-20T05:39:40.000000",
                 "description": "snapshot in saved state"
             }
         ]
diff --git a/tempest/tests/lib/services/volume/v3/test_scheduler_stats_client.py b/tempest/tests/lib/services/volume/v3/test_scheduler_stats_client.py
index 84c7589..7606a52 100644
--- a/tempest/tests/lib/services/volume/v3/test_scheduler_stats_client.py
+++ b/tempest/tests/lib/services/volume/v3/test_scheduler_stats_client.py
@@ -25,12 +25,14 @@
                 "name": "pool1",
                 "capabilities": {
                     "updated": "2014-10-28T00:00:00-00:00",
-                    "total_capacity": 1024,
-                    "free_capacity": 100,
+                    "total_capacity_gb": 1024,
+                    "free_capacity_gb": 100,
                     "volume_backend_name": "pool1",
                     "reserved_percentage": 0,
                     "driver_version": "1.0.0",
+                    "timestamp": "2014-10-28T00:00:00-00:00",
                     "storage_protocol": "iSCSI",
+                    "vendor_name": "vendor",
                     "QoS_support": False
                 }
             },
@@ -38,12 +40,14 @@
                 "name": "pool2",
                 "capabilities": {
                     "updated": "2014-10-28T00:00:00-00:00",
-                    "total_capacity": 512,
-                    "free_capacity": 200,
+                    "total_capacity_gb": 512,
+                    "free_capacity_gb": 200,
                     "volume_backend_name": "pool2",
                     "reserved_percentage": 0,
                     "driver_version": "1.0.2",
+                    "timestamp": "2014-10-28T00:00:00-00:00",
                     "storage_protocol": "iSER",
+                    "vendor_name": "vendor",
                     "QoS_support": True
                 }
             }