Add API schema for v2.1/v3 cells API

By defining the API schema, it is possible to separate the validation
code from the API method. The API method can be more simple.
In addition, a response of API validation error can be consistent for
the whole Nova API.

Partially implements blueprint v3-api-schema

Change-Id: Ic400a31fe2c05e07785a2dc6c4fd864e684f4965
This commit is contained in:
Ken'ichi Ohmichi 2014-04-08 14:07:20 +09:00
parent 2cc4f27526
commit 3efb8070da
5 changed files with 187 additions and 83 deletions

View File

@ -22,14 +22,15 @@ import six
from webob import exc
from nova.api.openstack import common
from nova.api.openstack.compute.schemas.v3 import cells
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova.api import validation
from nova.cells import rpcapi as cells_rpcapi
from nova.compute import api as compute
from nova import exception
from nova.i18n import _
from nova.openstack.common import strutils
from nova.openstack.common import timeutils
from nova import rpc
@ -187,21 +188,6 @@ class CellsController(object):
raise exc.HTTPNotFound(
explanation=_("Cell %s doesn't exist.") % id)
def _validate_cell_name(self, cell_name):
"""Validate cell name is not empty and doesn't contain '!' or '.'."""
if not cell_name:
msg = _("Cell name cannot be empty")
raise exc.HTTPBadRequest(explanation=msg)
if '!' in cell_name or '.' in cell_name:
msg = _("Cell name cannot contain '!' or '.'")
raise exc.HTTPBadRequest(explanation=msg)
def _validate_cell_type(self, cell_type):
"""Validate cell_type is 'parent' or 'child'."""
if cell_type not in ['parent', 'child']:
msg = _("Cell type must be 'parent' or 'child'")
raise exc.HTTPBadRequest(explanation=msg)
def _normalize_cell(self, cell, existing=None):
"""Normalize input cell data. Normalizations include:
@ -211,7 +197,6 @@ class CellsController(object):
# Start with the cell type conversion
if 'type' in cell:
self._validate_cell_type(cell['type'])
cell['is_parent'] = cell['type'] == 'parent'
del cell['type']
# Avoid cell type being overwritten to 'child'
@ -249,6 +234,7 @@ class CellsController(object):
@extensions.expected_errors((400, 403, 501))
@common.check_cells_enabled
@wsgi.response(201)
@validation.schema(cells.create)
def create(self, req, body):
"""Create a child cell entry."""
context = req.environ['nova.context']
@ -256,14 +242,7 @@ class CellsController(object):
authorize(context)
authorize(context, action="create")
if 'cell' not in body:
msg = _("No cell information in request")
raise exc.HTTPBadRequest(explanation=msg)
cell = body['cell']
if 'name' not in cell:
msg = _("No cell name in request")
raise exc.HTTPBadRequest(explanation=msg)
self._validate_cell_name(cell['name'])
self._normalize_cell(cell)
try:
cell = self.cells_rpcapi.cell_create(context, cell)
@ -273,6 +252,7 @@ class CellsController(object):
@extensions.expected_errors((400, 403, 404, 501))
@common.check_cells_enabled
@validation.schema(cells.update)
def update(self, req, id, body):
"""Update a child cell entry. 'id' is the cell name to update."""
context = req.environ['nova.context']
@ -280,13 +260,9 @@ class CellsController(object):
authorize(context)
authorize(context, action="update")
if 'cell' not in body:
msg = _("No cell information in request")
raise exc.HTTPBadRequest(explanation=msg)
cell = body['cell']
cell.pop('id', None)
if 'name' in cell:
self._validate_cell_name(cell['name'])
try:
# NOTE(Vek): There is a race condition here if multiple
# callers are trying to update the cell
@ -309,6 +285,7 @@ class CellsController(object):
@extensions.expected_errors((400, 501))
@common.check_cells_enabled
@wsgi.response(204)
@validation.schema(cells.sync_instances)
def sync_instances(self, req, body):
"""Tell all cells to sync instance info."""
context = req.environ['nova.context']
@ -319,21 +296,8 @@ class CellsController(object):
project_id = body.pop('project_id', None)
deleted = body.pop('deleted', False)
updated_since = body.pop('updated_since', None)
if body:
msg = _("Only 'updated_since', 'project_id' and 'deleted' are "
"understood.")
raise exc.HTTPBadRequest(explanation=msg)
if isinstance(deleted, six.string_types):
try:
deleted = strutils.bool_from_string(deleted, strict=True)
except ValueError as err:
raise exc.HTTPBadRequest(explanation=str(err))
if updated_since:
try:
timeutils.parse_isotime(updated_since)
except ValueError:
msg = _('Invalid changes-since value')
raise exc.HTTPBadRequest(explanation=msg)
deleted = strutils.bool_from_string(deleted, strict=True)
self.cells_rpcapi.sync_instances(context, project_id=project_id,
updated_since=updated_since, deleted=deleted)

View File

@ -0,0 +1,99 @@
# Copyright 2014 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 nova.api.validation import parameter_types
create = {
'type': 'object',
'properties': {
'cell': {
'type': 'object',
'properties': {
'name': parameter_types.name,
'type': {
'type': 'string',
'enum': ['parent', 'child'],
},
# NOTE: In unparse_transport_url(), a url consists of the
# following parameters:
# "qpid://<username>:<password>@<rpc_host>:<rpc_port>/"
# or
# "rabiit://<username>:<password>@<rpc_host>:<rpc_port>/"
# Then the url is stored into transport_url of cells table
# which is defined with String(255).
'username': {
'type': 'string', 'maxLength': 255,
'pattern': '^[a-zA-Z0-9-_]*$'
},
'password': {
# Allow to specify any string for strong password.
'type': 'string', 'maxLength': 255,
},
'rpc_host': parameter_types.hostname_or_ip_address,
'rpc_port': parameter_types.tcp_udp_port,
'rpc_virtual_host': parameter_types.hostname_or_ip_address,
},
'required': ['name'],
'additionalProperties': False,
},
},
'required': ['cell'],
'additionalProperties': False,
}
update = {
'type': 'object',
'properties': {
'cell': {
'type': 'object',
'properties': {
'name': parameter_types.name,
'type': {
'type': 'string',
'enum': ['parent', 'child'],
},
'username': {
'type': 'string', 'maxLength': 255,
'pattern': '^[a-zA-Z0-9-_]*$'
},
'password': {
'type': 'string', 'maxLength': 255,
},
'rpc_host': parameter_types.hostname_or_ip_address,
'rpc_port': parameter_types.tcp_udp_port,
'rpc_virtual_host': parameter_types.hostname_or_ip_address,
},
'additionalProperties': False,
},
},
'required': ['cell'],
'additionalProperties': False,
}
sync_instances = {
'type': 'object',
'properties': {
'project_id': parameter_types.project_id,
'deleted': parameter_types.boolean,
'updated_since': {
'type': 'string',
'format': 'date-time',
},
},
'additionalProperties': False,
}

View File

@ -21,9 +21,20 @@ import six
from nova import exception
from nova.i18n import _
from nova.openstack.common import timeutils
from nova.openstack.common import uuidutils
@jsonschema.FormatChecker.cls_checks('date-time')
def _validate_datetime_format(instance):
try:
timeutils.parse_isotime(instance)
except ValueError:
return False
else:
return True
@jsonschema.FormatChecker.cls_checks('uuid')
def _validate_uuid_format(instance):
return uuidutils.is_uuid_like(instance)

View File

@ -180,13 +180,11 @@ class CellsTest(BaseCellsTest):
'username': 'fred',
'password': 'fubar',
'rpc_host': 'r3.example.org',
'type': 'parent',
# Also test this is ignored/stripped
'is_parent': False}}
'type': 'parent'}}
req = self._get_request("cells")
req.environ['nova.context'] = self.context
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
cell = res_dict['cell']
self.assertEqual(self.controller.create.wsgi_code, 201)
self.assertEqual(cell['name'], 'meow')
@ -194,7 +192,6 @@ class CellsTest(BaseCellsTest):
self.assertEqual(cell['rpc_host'], 'r3.example.org')
self.assertEqual(cell['type'], 'parent')
self.assertNotIn('password', cell)
self.assertNotIn('is_parent', cell)
def test_cell_create_parent(self):
# Test create with just cells policy
@ -215,7 +212,7 @@ class CellsTest(BaseCellsTest):
req = self._get_request("cells")
req.environ['nova.context'] = self.context
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
cell = res_dict['cell']
self.assertEqual(self.controller.create.wsgi_code, 201)
self.assertEqual(cell['name'], 'meow')
@ -243,8 +240,8 @@ class CellsTest(BaseCellsTest):
req = self._get_request("cells")
req.environ['nova.context'] = self.context
self.assertRaises(exc.HTTPBadRequest,
self.controller.create, req, body)
self.assertRaises(exception.ValidationError,
self.controller.create, req, body=body)
def test_cell_create_name_empty_string_raises(self):
body = {'cell': {'name': '',
@ -255,8 +252,8 @@ class CellsTest(BaseCellsTest):
req = self._get_request("cells")
req.environ['nova.context'] = self.context
self.assertRaises(exc.HTTPBadRequest,
self.controller.create, req, body)
self.assertRaises(exception.ValidationError,
self.controller.create, req, body=body)
def test_cell_create_name_with_bang_raises(self):
body = {'cell': {'name': 'moo!cow',
@ -267,20 +264,8 @@ class CellsTest(BaseCellsTest):
req = self._get_request("cells")
req.environ['nova.context'] = self.context
self.assertRaises(exc.HTTPBadRequest,
self.controller.create, req, body)
def test_cell_create_name_with_dot_raises(self):
body = {'cell': {'name': 'moo.cow',
'username': 'fred',
'password': 'secret',
'rpc_host': 'r3.example.org',
'type': 'parent'}}
req = self._get_request("cells")
req.environ['nova.context'] = self.context
self.assertRaises(exc.HTTPBadRequest,
self.controller.create, req, body)
self.assertRaises(exception.ValidationError,
self.controller.create, req, body=body)
def test_cell_create_name_with_invalid_type_raises(self):
body = {'cell': {'name': 'moocow',
@ -291,8 +276,8 @@ class CellsTest(BaseCellsTest):
req = self._get_request("cells")
req.environ['nova.context'] = self.context
self.assertRaises(exc.HTTPBadRequest,
self.controller.create, req, body)
self.assertRaises(exception.ValidationError,
self.controller.create, req, body=body)
def test_cell_create_fails_for_invalid_policy(self):
body = {'cell': {'name': 'fake'}}
@ -300,7 +285,7 @@ class CellsTest(BaseCellsTest):
req.environ['nova.context'] = self.context
req.environ['nova.context'].is_admin = False
self.assertRaises(exception.PolicyNotAuthorized,
self.controller.create, req, body)
self.controller.create, req, body=body)
def _cell_update(self):
body = {'cell': {'username': 'zeb',
@ -308,7 +293,7 @@ class CellsTest(BaseCellsTest):
req = self._get_request("cells/cell1")
req.environ['nova.context'] = self.context
res_dict = self.controller.update(req, 'cell1', body)
res_dict = self.controller.update(req, 'cell1', body=body)
cell = res_dict['cell']
self.assertEqual(cell['name'], 'cell1')
@ -332,7 +317,7 @@ class CellsTest(BaseCellsTest):
req.environ['nova.context'] = self.context
req.environ['nova.context'].is_admin = False
self.assertRaises(exception.PolicyNotAuthorized,
self.controller.create, req, body)
self.controller.create, req, body=body)
def test_cell_update_empty_name_raises(self):
body = {'cell': {'name': '',
@ -341,8 +326,8 @@ class CellsTest(BaseCellsTest):
req = self._get_request("cells/cell1")
req.environ['nova.context'] = self.context
self.assertRaises(exc.HTTPBadRequest,
self.controller.update, req, 'cell1', body)
self.assertRaises(exception.ValidationError,
self.controller.update, req, 'cell1', body=body)
def test_cell_update_invalid_type_raises(self):
body = {'cell': {'username': 'zeb',
@ -351,15 +336,15 @@ class CellsTest(BaseCellsTest):
req = self._get_request("cells/cell1")
req.environ['nova.context'] = self.context
self.assertRaises(exc.HTTPBadRequest,
self.controller.update, req, 'cell1', body)
self.assertRaises(exception.ValidationError,
self.controller.update, req, 'cell1', body=body)
def test_cell_update_without_type_specified(self):
body = {'cell': {'username': 'wingwj'}}
req = self._get_request("cells/cell1")
req.environ['nova.context'] = self.context
res_dict = self.controller.update(req, 'cell1', body)
res_dict = self.controller.update(req, 'cell1', body=body)
cell = res_dict['cell']
self.assertEqual(cell['name'], 'cell1')
@ -373,12 +358,12 @@ class CellsTest(BaseCellsTest):
req1 = self._get_request("cells/cell1")
req1.environ['nova.context'] = self.context
res_dict1 = self.controller.update(req1, 'cell1', body1)
res_dict1 = self.controller.update(req1, 'cell1', body=body1)
cell1 = res_dict1['cell']
req2 = self._get_request("cells/cell2")
req2.environ['nova.context'] = self.context
res_dict2 = self.controller.update(req2, 'cell2', body2)
res_dict2 = self.controller.update(req2, 'cell2', body=body2)
cell2 = res_dict2['cell']
self.assertEqual(cell1['name'], 'cell1')
@ -500,7 +485,7 @@ class CellsTest(BaseCellsTest):
self.assertEqual(call_info['updated_since'], expected)
body = {'updated_since': 'skjdfkjsdkf'}
self.assertRaises(exc.HTTPBadRequest,
self.assertRaises(exception.ValidationError,
self.controller.sync_instances, req, body=body)
body = {'deleted': False}
@ -522,11 +507,11 @@ class CellsTest(BaseCellsTest):
self.assertEqual(call_info['deleted'], True)
body = {'deleted': 'foo'}
self.assertRaises(exc.HTTPBadRequest,
self.assertRaises(exception.ValidationError,
self.controller.sync_instances, req, body=body)
body = {'foo': 'meow'}
self.assertRaises(exc.HTTPBadRequest,
self.assertRaises(exception.ValidationError,
self.controller.sync_instances, req, body=body)
def test_sync_instances_fails_for_invalid_policy(self):
@ -541,7 +526,7 @@ class CellsTest(BaseCellsTest):
body = {}
self.assertRaises(exception.PolicyNotAuthorized,
self.controller.sync_instances, req, body)
self.controller.sync_instances, req, body=body)
def test_cells_disabled(self):
self.flags(enable=False, group='cells')

View File

@ -602,6 +602,51 @@ class TcpUdpPortTestCase(APIValidationTestCase):
expected_detail=detail)
class DatetimeTestCase(APIValidationTestCase):
def setUp(self):
super(DatetimeTestCase, self).setUp()
schema = {
'type': 'object',
'properties': {
'foo': {
'type': 'string',
'format': 'date-time',
},
},
}
@validation.schema(schema)
def post(body):
return 'Validation succeeded.'
self.post = post
def test_validate_datetime(self):
self.assertEqual('Validation succeeded.',
self.post(
body={'foo': '2014-01-14T01:00:00Z'}
))
def test_validate_datetime_fails(self):
detail = ("Invalid input for field/attribute foo."
" Value: 2014-13-14T01:00:00Z."
" '2014-13-14T01:00:00Z' is not a 'date-time'")
self.check_validation_error(self.post,
body={'foo': '2014-13-14T01:00:00Z'},
expected_detail=detail)
detail = ("Invalid input for field/attribute foo."
" Value: bar. 'bar' is not a 'date-time'")
self.check_validation_error(self.post, body={'foo': 'bar'},
expected_detail=detail)
detail = ("Invalid input for field/attribute foo. Value: 1."
" '1' is not a 'date-time'")
self.check_validation_error(self.post, body={'foo': '1'},
expected_detail=detail)
class UuidTestCase(APIValidationTestCase):
def setUp(self):