From a1d65edb3f0cc3814b315a945629a88e21fb090f Mon Sep 17 00:00:00 2001 From: Taku Fukushima Date: Wed, 12 Aug 2015 19:00:10 +0200 Subject: [PATCH] Add the validation for /NetworkDriver.CreateNetwork This patch adds the validation for /NetworkDriver.CreateNetwork with the JSON schema. This patch also introdueces the basic foundation for the following validations with JSON schemata. The common part of the scheamta is separated in commons.py and it's injected into each schema. Change-Id: I1cd849db2eeadb03b4488e1842fbd22e62fff169 Signed-off-by: Taku Fukushima --- kuryr/controllers.py | 5 +- kuryr/schemata/__init__.py | 13 ++++ kuryr/schemata/commons.py | 112 ++++++++++++++++++++++++++++++ kuryr/schemata/network_create.py | 40 +++++++++++ kuryr/tests/test_kuryr_network.py | 11 +++ kuryr/utils.py | 13 ++-- requirements.txt | 1 + 7 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 kuryr/schemata/__init__.py create mode 100644 kuryr/schemata/commons.py create mode 100644 kuryr/schemata/network_create.py diff --git a/kuryr/controllers.py b/kuryr/controllers.py index 9cf1be65..50f959a9 100644 --- a/kuryr/controllers.py +++ b/kuryr/controllers.py @@ -14,12 +14,14 @@ import os from flask import jsonify from flask import request +from jsonschema import validate import netaddr from neutronclient.common import exceptions as n_exceptions from kuryr import app from kuryr.constants import SCHEMA from kuryr import exceptions +from kuryr import schemata from kuryr import utils @@ -198,7 +200,8 @@ def network_driver_create_network(): json_data = request.get_json(force=True) app.logger.debug("Received JSON data {0} for /NetworkDriver.CreateNetwork" .format(json_data)) - # TODO(tfukushima): Add a validation of the JSON data for the network. + validate(json_data, schemata.NETWORK_CREATE_SCHEMA) + neutron_network_name = json_data['NetworkID'] network = app.neutron.create_network( diff --git a/kuryr/schemata/__init__.py b/kuryr/schemata/__init__.py new file mode 100644 index 00000000..92ea06e0 --- /dev/null +++ b/kuryr/schemata/__init__.py @@ -0,0 +1,13 @@ +# 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 kuryr.schemata.network_create import NETWORK_CREATE_SCHEMA # noqa diff --git a/kuryr/schemata/commons.py b/kuryr/schemata/commons.py new file mode 100644 index 00000000..487b05d0 --- /dev/null +++ b/kuryr/schemata/commons.py @@ -0,0 +1,112 @@ +# 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. + +COMMONS = { + u'description': u'Common data schemata shared among other schemata.', + u'links': [], + u'title': u'Kuryr Common Data Schema Definitions', + u'properties': { + u'options': {u'$ref': u'/schemata/commons#/definitions/options'}, + u'mac': {u'$ref': u'/schemata/commons#/definitions/mac'}, + u'cidrv6': {u'$ref': u'/schemata/commons#/definitions/cidrv6'}, + u'interface': {u'$ref': u'/schemata/commons#/definitions/interface'}, + u'cidr': {u'$ref': u'/schemata/commons#/definitions/cidr'}, + u'id': {u'$ref': u'/schemata/commons#/definitions/id'} + }, + u'definitions': { + u'options': { + u'type': u'object', + u'description': u'Options.', + u'example': {} + }, + u'mac': { + u'pattern': (u'^((?:[0-9a-f]{2}:){5}[0-9a-f]{2}|' + u'(?:[0-9A-F]{2}:){5}[0-9A-F]{2})$'), + u'type': u'string', + u'description': u'A MAC address.', + u'example': u'aa:bb:cc:dd:ee:ff' + }, + u'cidrv6': { + u'pattern': (u'^((' + u'([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|' + u'([0-9a-fA-F]{1,4}:){1,7}:|' + u'([0-9a-fA-F]{1,4}:){1,6}\:[0-9a-fA-F]{1,4}|' + u'([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|' + u'([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|' + u'([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|' + u'([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|' + u'[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|' + u':((:[0-9a-fA-F]{1,4}){1,7}|:)|' + u'fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|' + u'::(ffff(:0{1,4}){0,1}:){0,1}' + u'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}' + u'(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|' + u'([0-9a-fA-F]{1,4}:){1,4}:' + u'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}' + u'(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))' + u'(/(1[0-2][0-8]|[1-9]?[0-9])))$'), + u'type': u'string', + u'description': u'A IPv6 CIDR of the subnet' + }, + u'interface': { + u'properties': { + u'ID': { + u'description': u'Index of the interface', + u'type': u'number', + }, + u'AddressIPv6': { + u'description': u'IPv6 CIDR', + u'$ref': u'#/definitions/commons/definitions/cidrv6' + }, + u'MacAddress': { + u'description': u'MAC address', + u'$ref': u'#/definitions/commons/definitions/mac' + }, + u'Address': { + u'description': u'IPv4 CIDR', + u'$ref': u'#/definitions/commons/definitions/cidr' + } + }, + u'type': u'object', + u'description': u'Interface used in requests against Endpoints.', + u'example': { + u'AddressIPv6': u'fe80::f816:3eff:fe20:57c3/64', + u'MacAddress': u'fa:16:3e:20:57:c3', + u'Address': u'192.168.1.42/24' + } + }, + u'cidr': { + u'pattern': (u'^((25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\.){3}' + u'(25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])' + u'/(3[0-2]|((1|2)?[0-9]))$'), + u'type': u'string', + u'description': u'A IPv4 CIDR of the subnet.', + u'example': u'10.0.0.0/24' + }, + u'id': { + u'pattern': u'^([0-9a-f]{64})$', + u'type': u'string', + u'description': u'256 bits ID value of Docker.', + u'example': + u'51c75a2515d47edecc3f720bb541e287224416fb66715eb7802011d6ffd499f1' + }, + u'sandbox_key': { + u'pattern': u'^(/var/run/docker/netns/[0-9a-f]{12})$', + u'type': u'string', + u'description': u'Sandbox information of netns.', + u'example': '/var/run/docker/netns/12bbda391ed0' + } + }, + u'$schema': u'http://json-schema.org/draft-04/hyper-schema', + u'type': u'object', + u'id': u'schemata/commons' +} diff --git a/kuryr/schemata/network_create.py b/kuryr/schemata/network_create.py new file mode 100644 index 00000000..a8216409 --- /dev/null +++ b/kuryr/schemata/network_create.py @@ -0,0 +1,40 @@ +# 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 kuryr.schemata.commons import COMMONS + +NETWORK_CREATE_SCHEMA = { + u'links': [{ + u'method': u'POST', + u'href': u'/NetworkDriver.CreateNetwork', + u'description': u'Create a Network', + u'rel': u'self', + u'title': u'Create' + }], + u'title': u'Create network', + u'required': [u'NetworkID'], + u'definitions': {u'commons': {}}, + u'$schema': u'http://json-schema.org/draft-04/hyper-schema', + u'type': u'object', + u'properties': { + u'NetworkID': { + u'description': u'ID of a Network to be created', + u'$ref': u'#/definitions/commons/definitions/id' + }, + u'Options': + {u'type': u'object', + u'description': u'Options', + u'example': {}} + } +} + +NETWORK_CREATE_SCHEMA[u'definitions'][u'commons'] = COMMONS diff --git a/kuryr/tests/test_kuryr_network.py b/kuryr/tests/test_kuryr_network.py index ced7c525..2b94e2c6 100644 --- a/kuryr/tests/test_kuryr_network.py +++ b/kuryr/tests/test_kuryr_network.py @@ -61,6 +61,17 @@ class TestKuryrNetworkCreateFailures(base.TestKuryrFailures): self.assertEqual( {'Err': exceptions.Unauthorized.message}, decoded_json) + def test_create_network_bad_request(self): + invalid_docker_network_id = 'id-should-be-hexdigits' + response = self._invoke_create_request(invalid_docker_network_id) + + self.assertEqual(400, response.status_code) + decoded_json = jsonutils.loads(response.data) + self.assertTrue('Err' in decoded_json) + # TODO(tfukushima): Add the better error message validation. + self.assertTrue(invalid_docker_network_id in decoded_json['Err']) + self.assertTrue('NetworkID' in decoded_json['Err']) + @ddt class TestKuryrNetworkDeleteFailures(base.TestKuryrFailures): diff --git a/kuryr/utils.py b/kuryr/utils.py index a2af17ba..c92c5984 100644 --- a/kuryr/utils.py +++ b/kuryr/utils.py @@ -11,6 +11,7 @@ # under the License. from flask import Flask, jsonify +from jsonschema import ValidationError from neutronclient.common.exceptions import NeutronClientException from neutronclient.neutron import client from neutronclient.v2_0 import client as client_v2 @@ -54,12 +55,16 @@ def make_json_app(import_name, **kwargs): app = Flask(import_name, **kwargs) @app.errorhandler(NeutronClientException) + @app.errorhandler(ValidationError) def make_json_error(ex): response = jsonify({"Err": str(ex)}) - response.status_code = (ex.code if isinstance(ex, HTTPException) - else ex.status_code - if isinstance(ex, NeutronClientException) - else 500) + response.status_code = 500 + if isinstance(ex, HTTPException): + response.status_code = ex.code + elif isinstance(ex, NeutronClientException): + response.status_code = ex.status_code + elif isinstance(ex, ValidationError): + response.status_code = 400 content_type = 'application/vnd.docker.plugins.v1+json; charset=utf-8' response.headers['Content-Type'] = content_type return response diff --git a/requirements.txt b/requirements.txt index 978fe1cb..c027621a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ pbr<2.0,>=1.3 Babel>=1.3 Flask>=0.10,<1.0 +jsonschema!=2.5.0,<3.0.0,>=2.0.0 netaddr>=0.7.12 oslo.serialization>=1.4.0 # Apache-2.0 python-neutronclient>=2.3.11,<3