Merge "Add API Validation framework for flavors"

This commit is contained in:
Jenkins 2017-04-03 02:28:23 +00:00 committed by Gerrit Code Review
commit 36aabb9011
8 changed files with 152 additions and 2 deletions

View File

@ -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

View File

@ -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()))

View File

@ -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

View File

@ -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'])

View File

View File

@ -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}

View File

@ -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)