From f9ded3536646bb3998325f3417ef4ed588e05b7d Mon Sep 17 00:00:00 2001 From: ghanshyam Date: Tue, 12 Apr 2016 17:03:01 +0900 Subject: [PATCH] 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 --- ...jsonschema-validator-2377ba131e12d3c7.yaml | 5 ++ .../response/compute/v2_1/aggregates.py | 12 +-- .../compute/v2_1/availability_zone.py | 4 +- .../response/compute/v2_1/extensions.py | 7 +- .../response/compute/v2_1/images.py | 4 +- .../response/compute/v2_1/keypairs.py | 8 +- .../response/compute/v2_1/migrations.py | 6 +- .../response/compute/v2_1/parameter_types.py | 19 ++++- .../response/compute/v2_1/servers.py | 10 +-- .../response/compute/v2_1/services.py | 4 +- .../response/compute/v2_1/snapshots.py | 4 +- .../response/compute/v2_1/tenant_usages.py | 15 ++-- .../response/compute/v2_1/versions.py | 4 +- .../response/compute/v2_1/volumes.py | 6 +- tempest/lib/common/jsonschema_validator.py | 39 +++++++++ tempest/lib/common/rest_client.py | 5 +- .../lib/common/test_jsonschema_validator.py | 83 +++++++++++++++++++ .../services/compute/test_servers_client.py | 6 +- 18 files changed, 197 insertions(+), 44 deletions(-) create mode 100644 releasenotes/notes/jsonschema-validator-2377ba131e12d3c7.yaml create mode 100644 tempest/lib/common/jsonschema_validator.py create mode 100644 tempest/tests/lib/common/test_jsonschema_validator.py diff --git a/releasenotes/notes/jsonschema-validator-2377ba131e12d3c7.yaml b/releasenotes/notes/jsonschema-validator-2377ba131e12d3c7.yaml new file mode 100644 index 0000000000..8817ed438b --- /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 1a9fe41cdd..3289a34a79 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 @@ aggregate_set_metadata = get_aggregate # 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 d9aebce7b9..f7b77a16ea 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 @@ detail = { '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 a6a455c1b8..b5962d7529 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 @@ list_extensions = { '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 f65b9d89fc..156ff4ac62 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 @@ common_image_schema = { '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 9c04c79b49..28280972b1 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 @@ get_keypair = { '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 b7d66ea486..c50286d27e 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 @@ list_migrations = { '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 3cc5ca476e..a3c9099ea8 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 @@ addresses = { } } +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 @@ response_header = { '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 1264416765..4ccca6f70a 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 @@ common_show_server = { }, '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 @@ instance_actions = { '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 @@ instance_action_events = { '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 ddef7b2291..6949f86265 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 @@ list_services = { '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 01a524bbc9..826f85413b 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 @@ common_snapshot_info = { '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 d51ef12e26..b531d2e751 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 @@ _tenant_usage_list = { '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 08a9fab5a8..7f56239286 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 @@ _version = { } }, '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 bb34acb17b..c35dae9810 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 @@ create_get_volume = { '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 @@ list_volumes = { '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 0000000000..bbdf38255a --- /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 2c36f55a49..d0e21ff4a2 100644 --- a/tempest/lib/common/rest_client.py +++ b/tempest/lib/common/rest_client.py @@ -25,6 +25,7 @@ from oslo_serialization import jsonutils as json 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_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207) 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 0000000000..8694f3d01c --- /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 93550fdeee..adfaaf2ffa 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 @@ class TestServersClient(base.BaseServiceTest): "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 @@ class TestServersClient(base.BaseServiceTest): 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" }