From 0fb61520fd391f89ad9aaa87eab3ef079e18086d Mon Sep 17 00:00:00 2001 From: Anusha Ramineni Date: Thu, 19 Jan 2017 11:45:15 +0530 Subject: [PATCH] Add API Validation framework for flavors Partially-Implements blueprint validation Change-Id: I9345b6b928353a4db936aeb1f06da9589e902f8c --- requirements.txt | 1 + valence/api/v1/flavors.py | 2 + valence/common/exception.py | 11 +++- valence/tests/unit/validation/__init__.py | 0 .../tests/unit/validation/test_validation.py | 50 +++++++++++++++++ valence/validation/__init__.py | 0 valence/validation/schemas.py | 37 +++++++++++++ valence/validation/validator.py | 53 +++++++++++++++++++ 8 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 valence/tests/unit/validation/__init__.py create mode 100644 valence/tests/unit/validation/test_validation.py create mode 100644 valence/validation/__init__.py create mode 100644 valence/validation/schemas.py create mode 100644 valence/validation/validator.py diff --git a/requirements.txt b/requirements.txt index 7817635..7c24c06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ Flask==0.11.1 Flask-Cors==3.0.2 Flask-RESTful==0.3.5 itsdangerous==0.24 +jsonschema>=2.0.0,<3.0.0,!=2.5.0 # MIT Jinja2==2.8 MarkupSafe==0.23 python-dateutil==2.5.3 diff --git a/valence/api/v1/flavors.py b/valence/api/v1/flavors.py index 943200b..7484275 100644 --- a/valence/api/v1/flavors.py +++ b/valence/api/v1/flavors.py @@ -20,6 +20,7 @@ from six.moves import http_client from valence.common import utils from valence.controller import flavors +from valence.validation import validator LOG = logging.getLogger(__name__) @@ -29,6 +30,7 @@ class Flavors(Resource): def get(self): return utils.make_response(http_client.OK, flavors.list_flavors()) + @validator.check_input('flavor_schema') def post(self): return utils.make_response(http_client.OK, flavors.create_flavor(request.get_json())) diff --git a/valence/common/exception.py b/valence/common/exception.py index 7bff519..0e5f6fb 100644 --- a/valence/common/exception.py +++ b/valence/common/exception.py @@ -99,14 +99,21 @@ class NotFound(ValenceError): class BadRequest(ValenceError): - def __init__(self, detail='bad request', request_id=FAKE_REQUEST_ID): + def __init__(self, detail='bad request', request_id=FAKE_REQUEST_ID, + code=None): self.request_id = request_id self.status = http_client.BAD_REQUEST - self.code = "BadRequest" + self.code = code or "BadRequest" self.title = "Malformed or Missing Payload in Request" self.detail = detail +class ValidationError(BadRequest): + def __init__(self, detail='Validation Error', request_id=None): + super(ValidationError, self).__init__(detail=detail, + code='ValidationError') + + def _error(error_code, http_status, error_title, error_detail, request_id=FAKE_REQUEST_ID): # responseobj - the response object of Requests framework diff --git a/valence/tests/unit/validation/__init__.py b/valence/tests/unit/validation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/tests/unit/validation/test_validation.py b/valence/tests/unit/validation/test_validation.py new file mode 100644 index 0000000..776b7b9 --- /dev/null +++ b/valence/tests/unit/validation/test_validation.py @@ -0,0 +1,50 @@ +import json + +import mock +from oslotest import base + +from valence.api import app as flask_app +from valence.tests.unit.fakes import flavor_fakes + + +class TestApiValidation(base.BaseTestCase): + """Test case base class for all unit tests.""" + + def setUp(self): + super(TestApiValidation, self).setUp() + app = flask_app.get_app() + app.config['TESTING'] = True + self.app = app.test_client() + self.flavor = flavor_fakes.fake_flavor() + + @mock.patch('valence.controller.flavors.create_flavor') + def test_flavor_create(self, mock_create): + flavor = self.flavor + flavor.pop('uuid') + mock_create.return_value = self.flavor + response = self.app.post('/v1/flavors', + content_type='application/json', + data=json.dumps(flavor)) + self.assertEqual(200, response.status_code) + mock_create.assert_called_once_with(flavor) + + def test_flavor_create_incorrect_param(self): + flavor = self.flavor + flavor.pop('uuid') + # Test invalid value + flavor['properties']['memory']['capacity_mib'] = 10 + response = self.app.post('/v1/flavors', + content_type='application/json', + data=json.dumps(self.flavor)) + response = json.loads(response.data.decode()) + self.assertEqual(400, response['status']) + self.assertEqual('ValidationError', response['code']) + + # Test invalid key + flavor['properties']['invalid_key'] = 'invalid' + response = self.app.post('/v1/flavors', + content_type='application/json', + data=json.dumps(self.flavor)) + response = json.loads(response.data.decode()) + self.assertEqual(400, response['status']) + self.assertEqual('ValidationError', response['code']) diff --git a/valence/validation/__init__.py b/valence/validation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/validation/schemas.py b/valence/validation/schemas.py new file mode 100644 index 0000000..18d3fdd --- /dev/null +++ b/valence/validation/schemas.py @@ -0,0 +1,37 @@ +import jsonschema + +flavor_schema = { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'description': {'type': 'string'}, + 'properties': { + 'type': 'object', + 'properties': { + 'memory': { + 'type': 'object', + 'properties': { + 'capacity_mib': {'type': 'string'}, + 'type': {'type': 'string'} + }, + 'additionalProperties': False, + }, + 'processor': { + 'type': 'object', + 'properties': { + 'total_cores': {'type': 'string'}, + 'model': {'type': 'string'}, + }, + 'additionalProperties': False, + }, + }, + 'additionalProperties': False, + }, + }, + 'required': ['name', 'properties'], + 'additionalProperties': False, +} + + +jsonschema.Draft4Validator.check_schema(flavor_schema) +SCHEMAS = {'flavor_schema': flavor_schema} diff --git a/valence/validation/validator.py b/valence/validation/validator.py new file mode 100644 index 0000000..6956b99 --- /dev/null +++ b/valence/validation/validator.py @@ -0,0 +1,53 @@ +# Copyright (c) 2017 NEC, Corp. +# +# 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 functools +import logging + +from flask import request +import jsonschema + +from valence.common import exception +from valence.validation import schemas + +LOG = logging.getLogger(__name__) + + +def check_input(schema_name): + def decorated(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + data = request.get_json() + LOG.debug("validating input %s with schema %s", data, schema_name) + schema_validator = Validator(schema_name) + schema_validator.validate(data) + return f(*args, **kwargs) + return wrapper + return decorated + + +class Validator(object): + def __init__(self, name): + self.name = name + self.schema = schemas.SCHEMAS.get(name) + checker = jsonschema.FormatChecker() + self.validator = jsonschema.Draft4Validator(self.schema, + format_checker=checker) + + def validate(self, data): + try: + self.validator.validate(data) + except jsonschema.ValidationError as e: + LOG.exception("Failed to validate the input") + raise exception.ValidationError(detail=e.message)