Fix date-time format checking in response schema
Currently datetime attributes in response schema like
'created_at' etc are being validated against type 'string' only
not with ISO 8601 date time format.
Another issue is with jsonschema validation for built-in 'date-time'
format. It needs 'strict_rfc3339' or 'isodate' module to be installed
for proper date-time validation as per rfc3339.
Otherwise it returns True wihtout doing any validation.
This patch define the new format checker for 'iso8601-date-time' format
which checks the format as per ISO 8601 with help of oslo_utils.timeutils
and validate all the date time attributes against JSON schema
'iso8601-date-time' format.
NOTE: date in image API header is returned in different format than
ISO 8601 date time format which is not consistent with other date-time
format in nova. So validating this as string only.
This API is already deprecated so not worth to fix on nova side.
Change-Id: Ief7729975daea373dcfa54a23ec76c3ec7754a70
Closes-Bug: #1567640
diff --git a/releasenotes/notes/jsonschema-validator-2377ba131e12d3c7.yaml b/releasenotes/notes/jsonschema-validator-2377ba131e12d3c7.yaml
new file mode 100644
index 0000000..8817ed4
--- /dev/null
+++ b/releasenotes/notes/jsonschema-validator-2377ba131e12d3c7.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Added customized JSON schema format checker for 'date-time' format.
+ Compute response schema will be validated against customized format
+ checker.
diff --git a/tempest/lib/api_schema/response/compute/v2_1/aggregates.py b/tempest/lib/api_schema/response/compute/v2_1/aggregates.py
index 1a9fe41..3289a34 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/aggregates.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/aggregates.py
@@ -14,17 +14,19 @@
import copy
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
# create-aggregate api doesn't have 'hosts' and 'metadata' attributes.
aggregate_for_create = {
'type': 'object',
'properties': {
'availability_zone': {'type': ['string', 'null']},
- 'created_at': {'type': 'string'},
+ 'created_at': parameter_types.date_time,
'deleted': {'type': 'boolean'},
- 'deleted_at': {'type': ['string', 'null']},
+ 'deleted_at': parameter_types.date_time_or_null,
'id': {'type': 'integer'},
'name': {'type': 'string'},
- 'updated_at': {'type': ['string', 'null']}
+ 'updated_at': parameter_types.date_time_or_null
},
'additionalProperties': False,
'required': ['availability_zone', 'created_at', 'deleted',
@@ -69,9 +71,7 @@
# The 'updated_at' attribute of 'update_aggregate' can't be null.
update_aggregate = copy.deepcopy(get_aggregate)
update_aggregate['response_body']['properties']['aggregate']['properties'][
- 'updated_at'] = {
- 'type': 'string'
- }
+ 'updated_at'] = parameter_types.date_time
delete_aggregate = {
'status_code': [200]
diff --git a/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py b/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py
index d9aebce..f7b77a1 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py
@@ -14,6 +14,8 @@
import copy
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
base = {
'status_code': [200],
@@ -61,7 +63,7 @@
'properties': {
'available': {'type': 'boolean'},
'active': {'type': 'boolean'},
- 'updated_at': {'type': ['string', 'null']}
+ 'updated_at': parameter_types.date_time_or_null
},
'additionalProperties': False,
'required': ['available', 'active', 'updated_at']
diff --git a/tempest/lib/api_schema/response/compute/v2_1/extensions.py b/tempest/lib/api_schema/response/compute/v2_1/extensions.py
index a6a455c..b5962d7 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/extensions.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/extensions.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
list_extensions = {
'status_code': [200],
'response_body': {
@@ -22,10 +24,7 @@
'items': {
'type': 'object',
'properties': {
- 'updated': {
- 'type': 'string',
- 'format': 'data-time'
- },
+ 'updated': parameter_types.date_time,
'name': {'type': 'string'},
'links': {'type': 'array'},
'namespace': {
diff --git a/tempest/lib/api_schema/response/compute/v2_1/images.py b/tempest/lib/api_schema/response/compute/v2_1/images.py
index f65b9d8..156ff4a 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/images.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/images.py
@@ -26,10 +26,10 @@
'properties': {
'id': {'type': 'string'},
'status': {'enum': image_status_enums},
- 'updated': {'type': 'string'},
+ 'updated': parameter_types.date_time,
'links': image_links,
'name': {'type': ['string', 'null']},
- 'created': {'type': 'string'},
+ 'created': parameter_types.date_time,
'minDisk': {'type': 'integer'},
'minRam': {'type': 'integer'},
'progress': {'type': 'integer'},
diff --git a/tempest/lib/api_schema/response/compute/v2_1/keypairs.py b/tempest/lib/api_schema/response/compute/v2_1/keypairs.py
index 9c04c79..2828097 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/keypairs.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/keypairs.py
@@ -12,6 +12,8 @@
# 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_keypair = {
'status_code': [200],
'response_body': {
@@ -25,9 +27,9 @@
'fingerprint': {'type': 'string'},
'user_id': {'type': 'string'},
'deleted': {'type': 'boolean'},
- 'created_at': {'type': 'string'},
- 'updated_at': {'type': ['string', 'null']},
- 'deleted_at': {'type': ['string', 'null']},
+ 'created_at': parameter_types.date_time,
+ 'updated_at': parameter_types.date_time_or_null,
+ 'deleted_at': parameter_types.date_time_or_null,
'id': {'type': 'integer'}
},
diff --git a/tempest/lib/api_schema/response/compute/v2_1/migrations.py b/tempest/lib/api_schema/response/compute/v2_1/migrations.py
index b7d66ea..c50286d 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/migrations.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/migrations.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
list_migrations = {
'status_code': [200],
'response_body': {
@@ -32,8 +34,8 @@
'dest_host': {'type': ['string', 'null']},
'old_instance_type_id': {'type': ['integer', 'null']},
'new_instance_type_id': {'type': ['integer', 'null']},
- 'created_at': {'type': 'string'},
- 'updated_at': {'type': ['string', 'null']}
+ 'created_at': parameter_types.date_time,
+ 'updated_at': parameter_types.date_time_or_null
},
'additionalProperties': False,
'required': [
diff --git a/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py b/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py
index 3cc5ca4..a3c9099 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py
@@ -81,6 +81,16 @@
}
}
+date_time = {
+ 'type': 'string',
+ 'format': 'iso8601-date-time'
+}
+
+date_time_or_null = {
+ 'type': ['string', 'null'],
+ 'format': 'iso8601-date-time'
+}
+
response_header = {
'connection': {'type': 'string'},
'content-length': {'type': 'string'},
@@ -89,9 +99,14 @@
'x-compute-request-id': {'type': 'string'},
'vary': {'type': 'string'},
'x-openstack-nova-api-version': {'type': 'string'},
+ # NOTE(gmann): Validating this as string only as this
+ # date in header is returned in different format than
+ # ISO 8601 date time format which is not consistent with
+ # other date-time format in nova.
+ # This API is already deprecated so not worth to fix
+ # on nova side.
'date': {
- 'type': 'string',
- 'format': 'data-time'
+ 'type': 'string'
}
}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/servers.py b/tempest/lib/api_schema/response/compute/v2_1/servers.py
index 1264416..4ccca6f 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/servers.py
@@ -118,8 +118,8 @@
},
'user_id': {'type': 'string'},
'tenant_id': {'type': 'string'},
- 'created': {'type': 'string'},
- 'updated': {'type': 'string'},
+ 'created': parameter_types.date_time,
+ 'updated': parameter_types.date_time,
'progress': {'type': 'integer'},
'metadata': {'type': 'object'},
'links': parameter_types.links,
@@ -402,7 +402,7 @@
'request_id': {'type': 'string'},
'user_id': {'type': 'string'},
'project_id': {'type': 'string'},
- 'start_time': {'type': 'string'},
+ 'start_time': parameter_types.date_time,
'message': {'type': ['string', 'null']},
'instance_uuid': {'type': 'string'}
},
@@ -417,8 +417,8 @@
'type': 'object',
'properties': {
'event': {'type': 'string'},
- 'start_time': {'type': 'string'},
- 'finish_time': {'type': 'string'},
+ 'start_time': parameter_types.date_time,
+ 'finish_time': parameter_types.date_time,
'result': {'type': 'string'},
'traceback': {'type': ['string', 'null']}
},
diff --git a/tempest/lib/api_schema/response/compute/v2_1/services.py b/tempest/lib/api_schema/response/compute/v2_1/services.py
index ddef7b2..6949f86 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/services.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/services.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
list_services = {
'status_code': [200],
'response_body': {
@@ -29,7 +31,7 @@
'state': {'type': 'string'},
'binary': {'type': 'string'},
'status': {'type': 'string'},
- 'updated_at': {'type': ['string', 'null']},
+ 'updated_at': parameter_types.date_time_or_null,
'disabled_reason': {'type': ['string', 'null']}
},
'additionalProperties': False,
diff --git a/tempest/lib/api_schema/response/compute/v2_1/snapshots.py b/tempest/lib/api_schema/response/compute/v2_1/snapshots.py
index 01a524b..826f854 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/snapshots.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/snapshots.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
common_snapshot_info = {
'type': 'object',
'properties': {
@@ -20,7 +22,7 @@
'volumeId': {'type': 'string'},
'status': {'type': 'string'},
'size': {'type': 'integer'},
- 'createdAt': {'type': 'string'},
+ 'createdAt': parameter_types.date_time,
'displayName': {'type': ['string', 'null']},
'displayDescription': {'type': ['string', 'null']}
},
diff --git a/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py b/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py
index d51ef12..b531d2e 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py
@@ -14,24 +14,21 @@
import copy
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
_server_usages = {
'type': 'array',
'items': {
'type': 'object',
'properties': {
- 'ended_at': {
- 'oneOf': [
- {'type': 'string'},
- {'type': 'null'}
- ]
- },
+ 'ended_at': parameter_types.date_time_or_null,
'flavor': {'type': 'string'},
'hours': {'type': 'number'},
'instance_id': {'type': 'string'},
'local_gb': {'type': 'integer'},
'memory_mb': {'type': 'integer'},
'name': {'type': 'string'},
- 'started_at': {'type': 'string'},
+ 'started_at': parameter_types.date_time,
'state': {'type': 'string'},
'tenant_id': {'type': 'string'},
'uptime': {'type': 'integer'},
@@ -47,8 +44,8 @@
'type': 'object',
'properties': {
'server_usages': _server_usages,
- 'start': {'type': 'string'},
- 'stop': {'type': 'string'},
+ 'start': parameter_types.date_time,
+ 'stop': parameter_types.date_time,
'tenant_id': {'type': 'string'},
'total_hours': {'type': 'number'},
'total_local_gb_usage': {'type': 'number'},
diff --git a/tempest/lib/api_schema/response/compute/v2_1/versions.py b/tempest/lib/api_schema/response/compute/v2_1/versions.py
index 08a9fab..7f56239 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/versions.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/versions.py
@@ -14,6 +14,8 @@
import copy
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
_version = {
'type': 'object',
@@ -33,7 +35,7 @@
}
},
'status': {'type': 'string'},
- 'updated': {'type': 'string', 'format': 'date-time'},
+ 'updated': parameter_types.date_time,
'version': {'type': 'string'},
'min_version': {'type': 'string'},
'media-types': {
diff --git a/tempest/lib/api_schema/response/compute/v2_1/volumes.py b/tempest/lib/api_schema/response/compute/v2_1/volumes.py
index bb34acb..c35dae9 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/volumes.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/volumes.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
create_get_volume = {
'status_code': [200],
'response_body': {
@@ -24,7 +26,7 @@
'status': {'type': 'string'},
'displayName': {'type': ['string', 'null']},
'availabilityZone': {'type': 'string'},
- 'createdAt': {'type': 'string'},
+ 'createdAt': parameter_types.date_time,
'displayDescription': {'type': ['string', 'null']},
'volumeType': {'type': ['string', 'null']},
'snapshotId': {'type': ['string', 'null']},
@@ -75,7 +77,7 @@
'status': {'type': 'string'},
'displayName': {'type': ['string', 'null']},
'availabilityZone': {'type': 'string'},
- 'createdAt': {'type': 'string'},
+ 'createdAt': parameter_types.date_time,
'displayDescription': {'type': ['string', 'null']},
'volumeType': {'type': ['string', 'null']},
'snapshotId': {'type': ['string', 'null']},
diff --git a/tempest/lib/common/jsonschema_validator.py b/tempest/lib/common/jsonschema_validator.py
new file mode 100644
index 0000000..bbdf382
--- /dev/null
+++ b/tempest/lib/common/jsonschema_validator.py
@@ -0,0 +1,39 @@
+# Copyright 2016 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.
+
+import jsonschema
+from oslo_utils import timeutils
+
+# JSON Schema validator and format checker used for JSON Schema validation
+JSONSCHEMA_VALIDATOR = jsonschema.Draft4Validator
+FORMAT_CHECKER = jsonschema.draft4_format_checker
+
+
+# NOTE(gmann): Add customized format checker for 'date-time' format because:
+# 1. jsonschema needs strict_rfc3339 or isodate module to be installed
+# for proper date-time checking as per rfc3339.
+# 2. Nova or other OpenStack components handle the date time format as
+# ISO 8601 which is defined in oslo_utils.timeutils
+# so this checker will validate the date-time as defined in
+# oslo_utils.timeutils
+@FORMAT_CHECKER.checks('iso8601-date-time')
+def _validate_datetime_format(instance):
+ try:
+ if isinstance(instance, jsonschema.compat.str_types):
+ timeutils.parse_isotime(instance)
+ except ValueError:
+ return False
+ else:
+ return True
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index 2c36f55..d0e21ff 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -25,6 +25,7 @@
import six
from tempest.lib.common import http
+from tempest.lib.common import jsonschema_validator
from tempest.lib.common.utils import test_utils
from tempest.lib import exceptions
@@ -38,8 +39,8 @@
HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307)
# JSON Schema validator and format checker used for JSON Schema validation
-JSONSCHEMA_VALIDATOR = jsonschema.Draft4Validator
-FORMAT_CHECKER = jsonschema.draft4_format_checker
+JSONSCHEMA_VALIDATOR = jsonschema_validator.JSONSCHEMA_VALIDATOR
+FORMAT_CHECKER = jsonschema_validator.FORMAT_CHECKER
class RestClient(object):
diff --git a/tempest/tests/lib/common/test_jsonschema_validator.py b/tempest/tests/lib/common/test_jsonschema_validator.py
new file mode 100644
index 0000000..8694f3d
--- /dev/null
+++ b/tempest/tests/lib/common/test_jsonschema_validator.py
@@ -0,0 +1,83 @@
+# Copyright 2016 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.
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions
+from tempest.tests import base
+from tempest.tests.lib import fake_http
+
+
+class TestJSONSchemaDateTimeFormat(base.TestCase):
+ date_time_schema = [
+ {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'date-time': parameter_types.date_time
+ }
+ }
+ },
+ {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'date-time': parameter_types.date_time_or_null
+ }
+ }
+ }
+ ]
+
+ def test_valid_date_time_format(self):
+ valid_instances = ['2016-10-02T10:00:00-05:00',
+ '2016-10-02T10:00:00+09:00',
+ '2016-10-02T15:00:00Z',
+ '2016-10-02T15:00:00.05Z']
+ resp = fake_http.fake_http_response('', status=200)
+ for instance in valid_instances:
+ body = {'date-time': instance}
+ for schema in self.date_time_schema:
+ rest_client.RestClient.validate_response(schema, resp, body)
+
+ def test_invalid_date_time_format(self):
+ invalid_instances = ['2016-10-02 T10:00:00-05:00',
+ '2016-10-02T 15:00:00',
+ '2016-10-02T15:00:00.05 Z',
+ '2016-10-02:15:00:00.05Z',
+ 'T15:00:00.05Z',
+ '2016:10:02T15:00:00',
+ '2016-10-02T15-00-00',
+ '2016-10-02T15.05Z',
+ '09MAR2015 11:15',
+ '13 Oct 2015 05:55:36 GMT',
+ '']
+ resp = fake_http.fake_http_response('', status=200)
+ for instance in invalid_instances:
+ body = {'date-time': instance}
+ for schema in self.date_time_schema:
+ self.assertRaises(exceptions.InvalidHTTPResponseBody,
+ rest_client.RestClient.validate_response,
+ schema, resp, body)
+
+ def test_date_time_or_null_format(self):
+ instance = None
+ resp = fake_http.fake_http_response('', status=200)
+ body = {'date-time': instance}
+ rest_client.RestClient.validate_response(self.date_time_schema[1],
+ resp, body)
+ self.assertRaises(exceptions.InvalidHTTPResponseBody,
+ rest_client.RestClient.validate_response,
+ self.date_time_schema[0], resp, body)
diff --git a/tempest/tests/lib/services/compute/test_servers_client.py b/tempest/tests/lib/services/compute/test_servers_client.py
index 93550fd..adfaaf2 100644
--- a/tempest/tests/lib/services/compute/test_servers_client.py
+++ b/tempest/tests/lib/services/compute/test_servers_client.py
@@ -154,7 +154,7 @@
"request_id": "16fb98f-46ca-475e-917e-2563e5a8cd19",
"user_id": "16fb98f-46ca-475e-917e-2563e5a8cd12",
"project_id": "16fb98f-46ca-475e-917e-2563e5a8cd34",
- "start_time": "09MAR2015 11:15",
+ "start_time": "2016-10-02T10:00:00-05:00",
"message": "fake-msg",
"instance_uuid": "16fb98f-46ca-475e-917e-2563e5a8cd12"
}
@@ -166,8 +166,8 @@
FAKE_INSTANCE_ACTION_EVENTS = {
"event": "fake-event",
- "start_time": "09MAR2015 11:15",
- "finish_time": "09MAR2015 11:15",
+ "start_time": "2016-10-02T10:00:00-05:00",
+ "finish_time": "2016-10-02T10:00:00-05:00",
"result": "fake-result",
"traceback": "fake-trace-back"
}