V3 jsonschema validation: Volume-types

This patch adds jsonschema validation for below Volume-types API's
* POST /v3/{project_id}/types
* PUT /v3/{project_id}/types/{volume_type_id}

Made changes to unit tests to pass body as keyword argument as wsgi
calls action method [1] and passes body as keyword argument.

[1] https://github.com/openstack/cinder/blob/master/cinder/api/openstack/wsgi.py#L997

Change-Id: Ibacf0acd16d3a3210289244fe2b99ebf5c5dd4ec
Partial-Implements: bp json-schema-validation
This commit is contained in:
Neha Alhat 2017-11-06 15:05:32 +05:30
parent 004bc27046
commit 2dedaf1fab
4 changed files with 117 additions and 80 deletions

View File

@ -15,12 +15,15 @@
"""The volume types manage extension."""
from oslo_utils import strutils
import six
from six.moves import http_client
import webob
from cinder.api import extensions
from cinder.api.openstack import wsgi
from cinder.api.schemas import volume_types as volume_types_schema
from cinder.api import validation
from cinder.api.views import types as views_types
from cinder import exception
from cinder.i18n import _
@ -48,32 +51,17 @@ class VolumeTypesManageController(wsgi.Controller):
rpc.get_notifier('volumeType').info(context, method, payload)
@wsgi.action("create")
@validation.schema(volume_types_schema.create)
def _create(self, req, body):
"""Creates a new volume type."""
context = req.environ['cinder.context']
context.authorize(policy.MANAGE_POLICY)
self.assert_valid_body(body, 'volume_type')
vol_type = body['volume_type']
name = vol_type.get('name', None)
name = vol_type['name']
description = vol_type.get('description')
specs = vol_type.get('extra_specs', {})
utils.validate_dictionary_string_length(specs)
is_public = utils.get_bool_param('os-volume-type-access:is_public',
vol_type, True)
if name is None or len(name.strip()) == 0:
msg = _("Volume type name can not be empty.")
raise webob.exc.HTTPBadRequest(explanation=msg)
utils.check_string_length(name, 'Type name',
min_length=1, max_length=255)
if description is not None:
utils.check_string_length(description, 'Type description',
min_length=0, max_length=255)
is_public = vol_type.get('os-volume-type-access:is_public', True)
is_public = strutils.bool_from_string(is_public, strict=True)
try:
volume_types.create(context,
name,
@ -98,39 +86,18 @@ class VolumeTypesManageController(wsgi.Controller):
return self._view_builder.show(req, vol_type)
@wsgi.action("update")
@validation.schema(volume_types_schema.update)
def _update(self, req, id, body):
# Update description for a given volume type.
context = req.environ['cinder.context']
context.authorize(policy.MANAGE_POLICY)
self.assert_valid_body(body, 'volume_type')
vol_type = body['volume_type']
description = vol_type.get('description')
name = vol_type.get('name')
is_public = vol_type.get('is_public')
# Name and description can not be both None.
# If name specified, name can not be empty.
if name and len(name.strip()) == 0:
msg = _("Volume type name can not be empty.")
raise webob.exc.HTTPBadRequest(explanation=msg)
if name is None and description is None and is_public is None:
msg = _("Specify volume type name, description, is_public or "
"a combination thereof.")
raise webob.exc.HTTPBadRequest(explanation=msg)
if is_public is not None:
is_public = utils.get_bool_param('is_public', vol_type)
if name:
utils.check_string_length(name, 'Type name',
min_length=1, max_length=255)
if description is not None:
utils.check_string_length(description, 'Type description',
min_length=0, max_length=255)
is_public = strutils.bool_from_string(is_public, strict=True)
try:
volume_types.update(context, id, name, description,

View File

@ -0,0 +1,61 @@
# Copyright 2017 NTT DATA
# 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 cinder.api.validation import parameter_types
create = {
'type': 'object',
'properties': {
'type': 'object',
'volume_type': {
'type': 'object',
'properties': {
'name': parameter_types.name,
'description': parameter_types.description,
'extra_specs': parameter_types.extra_specs_with_null,
'os-volume-type-access:is_public': parameter_types.boolean,
},
'required': ['name'],
'additionalProperties': False,
},
},
'required': ['volume_type'],
'additionalProperties': False,
}
update = {
'type': 'object',
'properties': {
'type': 'object',
'volume_type': {
'type': 'object',
'properties': {
'name': parameter_types.name,
'description': parameter_types.description,
'is_public': parameter_types.boolean,
},
'additionalProperties': False,
'anyOf': [
{'required': ['name']},
{'required': ['description']},
{'required': ['is_public']}
]
},
},
'required': ['volume_type'],
'additionalProperties': False,
}

View File

@ -18,6 +18,7 @@ Common parameter types for validating request Body.
"""
import copy
import re
import unicodedata
@ -151,6 +152,12 @@ extra_specs = {
'additionalProperties': False
}
group_snapshot_status = {
'type': 'string', 'format': 'group_snapshot_status'
}
extra_specs_with_null = copy.deepcopy(extra_specs)
extra_specs_with_null['patternProperties'][
'^[a-zA-Z0-9-_:. ]{1,255}$']['type'] = ['string', 'null']

View File

@ -221,7 +221,7 @@ class VolumeTypesManageApiTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/%s/types' % fake.PROJECT_ID)
self.assertEqual(0, len(self.notifier.notifications))
res_dict = self.controller._create(req, body)
res_dict = self.controller._create(req, body=body)
self.assertEqual(1, len(self.notifier.notifications))
id = res_dict['volume_type']['id']
@ -245,7 +245,7 @@ class VolumeTypesManageApiTest(test.TestCase):
"extra_specs": {"key1": "value1"}}}
req = fakes.HTTPRequest.blank('/v2/%s/types' % fake.PROJECT_ID)
res_dict = self.controller._create(req, body)
res_dict = self.controller._create(req, body=body)
self._check_test_results(res_dict, {
'expected_name': 'vol_type_1', 'expected_desc': ''})
@ -255,8 +255,8 @@ class VolumeTypesManageApiTest(test.TestCase):
body = {"volume_type": {"name": type_name,
"extra_specs": {"key1": "value1"}}}
req = fakes.HTTPRequest.blank('/v2/%s/types' % fake.PROJECT_ID)
self.assertRaises(exception.InvalidInput,
self.controller._create, req, body)
self.assertRaises(exception.ValidationError,
self.controller._create, req, body=body)
def test_create_type_with_description_too_long(self):
type_description = 'a' * 256
@ -264,8 +264,8 @@ class VolumeTypesManageApiTest(test.TestCase):
"description": type_description,
"extra_specs": {"key1": "value1"}}}
req = fakes.HTTPRequest.blank('/v2/%s/types' % fake.PROJECT_ID)
self.assertRaises(exception.InvalidInput,
self.controller._create, req, body)
self.assertRaises(exception.ValidationError,
self.controller._create, req, body=body)
def test_create_duplicate_type_fail(self):
self.mock_object(volume_types, 'create',
@ -277,7 +277,7 @@ class VolumeTypesManageApiTest(test.TestCase):
"extra_specs": {"key1": "value1"}}}
req = fakes.HTTPRequest.blank('/v2/%s/types' % fake.PROJECT_ID)
self.assertRaises(webob.exc.HTTPConflict,
self.controller._create, req, body)
self.controller._create, req, body=body)
def test_create_type_with_invalid_is_public(self):
body = {"volume_type": {"name": "vol_type_1",
@ -285,8 +285,8 @@ class VolumeTypesManageApiTest(test.TestCase):
"description": "test description",
"extra_specs": {"key1": "value1"}}}
req = fakes.HTTPRequest.blank('/v2/%s/types' % fake.PROJECT_ID)
self.assertRaises(exception.InvalidParameterValue,
self.controller._create, req, body)
self.assertRaises(exception.ValidationError,
self.controller._create, req, body=body)
@ddt.data('0', 'f', 'false', 'off', 'n', 'no', '1', 't', 'true', 'on',
'y', 'yes')
@ -305,7 +305,7 @@ class VolumeTypesManageApiTest(test.TestCase):
"extra_specs": {"key1": "value1"}}}
req = fakes.HTTPRequest.blank('/v2/%s/types' % fake.PROJECT_ID)
req.environ['cinder.context'] = ctxt
self.controller._create(req, body)
self.controller._create(req, body=body)
mock_create.assert_called_once_with(
ctxt, 'vol_type_1', {'key1': 'value1'},
boolean_is_public, description=None)
@ -313,8 +313,8 @@ class VolumeTypesManageApiTest(test.TestCase):
def _create_volume_type_bad_body(self, body):
req = fakes.HTTPRequest.blank('/v2/%s/types' % fake.PROJECT_ID)
req.method = 'POST'
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._create, req, body)
self.assertRaises(exception.ValidationError,
self.controller._create, req, body=body)
def test_create_no_body(self):
self._create_volume_type_bad_body(body=None)
@ -349,7 +349,7 @@ class VolumeTypesManageApiTest(test.TestCase):
use_admin_context=False)
self.assertEqual(0, len(self.notifier.notifications))
res_dict = self.controller._create(req, body)
res_dict = self.controller._create(req, body=body)
self.assertEqual(1, len(self.notifier.notifications))
self._check_test_results(res_dict, {
@ -360,7 +360,7 @@ class VolumeTypesManageApiTest(test.TestCase):
exception.PolicyNotAuthorized(action='type_create'))
self.assertRaises(exception.PolicyNotAuthorized,
self.controller._create,
req, body)
req, body=body)
@ddt.data({'a' * 256: 'a'},
{'a': 'a' * 256},
@ -373,8 +373,8 @@ class VolumeTypesManageApiTest(test.TestCase):
"description": "test description"}}
body['volume_type']['extra_specs'] = value
req = fakes.HTTPRequest.blank('/v2/%s/types' % fake.PROJECT_ID)
self.assertRaises(exception.InvalidInput,
self.controller._create, req, body)
self.assertRaises(exception.ValidationError,
self.controller._create, req, body=body)
@mock.patch('cinder.volume.volume_types.update')
@mock.patch('cinder.volume.volume_types.get_volume_type')
@ -387,7 +387,7 @@ class VolumeTypesManageApiTest(test.TestCase):
req.method = 'PUT'
self.assertEqual(0, len(self.notifier.notifications))
res_dict = self.controller._update(req, DEFAULT_VOLUME_TYPE, body)
res_dict = self.controller._update(req, DEFAULT_VOLUME_TYPE, body=body)
self.assertEqual(1, len(self.notifier.notifications))
self._check_test_results(
res_dict,
@ -414,7 +414,7 @@ class VolumeTypesManageApiTest(test.TestCase):
req.environ['cinder.context'] = ctxt
boolean_is_public = strutils.bool_from_string(is_public)
self.controller._update(req, DEFAULT_VOLUME_TYPE, body)
self.controller._update(req, DEFAULT_VOLUME_TYPE, body=body)
mock_update.assert_called_once_with(
ctxt, DEFAULT_VOLUME_TYPE, None, None,
is_public=boolean_is_public)
@ -433,7 +433,7 @@ class VolumeTypesManageApiTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/%s/types/%s' % (
fake.PROJECT_ID, DEFAULT_VOLUME_TYPE))
req.method = 'PUT'
resp = self.controller._update(req, DEFAULT_VOLUME_TYPE, body)
resp = self.controller._update(req, DEFAULT_VOLUME_TYPE, body=body)
self._check_test_results(resp,
{'expected_desc': '',
'expected_name': 'vol_type_1'})
@ -445,9 +445,9 @@ class VolumeTypesManageApiTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/%s/types/%s' % (
fake.PROJECT_ID, DEFAULT_VOLUME_TYPE))
req.method = 'PUT'
self.assertRaises(exception.InvalidInput,
self.assertRaises(exception.ValidationError,
self.controller._update, req,
DEFAULT_VOLUME_TYPE, body)
DEFAULT_VOLUME_TYPE, body=body)
def test_update_type_with_description_too_long(self):
type_description = 'a' * 256
@ -455,9 +455,9 @@ class VolumeTypesManageApiTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/%s/types/%s' % (
fake.PROJECT_ID, DEFAULT_VOLUME_TYPE))
req.method = 'PUT'
self.assertRaises(exception.InvalidInput,
self.assertRaises(exception.ValidationError,
self.controller._update, req,
DEFAULT_VOLUME_TYPE, body)
DEFAULT_VOLUME_TYPE, body=body)
@mock.patch('cinder.volume.volume_types.get_volume_type')
@mock.patch('cinder.volume.volume_types.update')
@ -473,7 +473,7 @@ class VolumeTypesManageApiTest(test.TestCase):
self.assertEqual(0, len(self.notifier.notifications))
self.assertRaises(exception.VolumeTypeNotFound,
self.controller._update, req,
NOT_FOUND_VOLUME_TYPE, body)
NOT_FOUND_VOLUME_TYPE, body=body)
self.assertEqual(1, len(self.notifier.notifications))
@mock.patch('cinder.volume.volume_types.get_volume_type')
@ -493,7 +493,7 @@ class VolumeTypesManageApiTest(test.TestCase):
self.assertEqual(0, len(self.notifier.notifications))
self.assertRaises(webob.exc.HTTPInternalServerError,
self.controller._update, req,
DEFAULT_VOLUME_TYPE, body)
DEFAULT_VOLUME_TYPE, body=body)
self.assertEqual(1, len(self.notifier.notifications))
def test_update_no_name_no_description(self):
@ -502,9 +502,9 @@ class VolumeTypesManageApiTest(test.TestCase):
fake.PROJECT_ID, DEFAULT_VOLUME_TYPE))
req.method = 'PUT'
self.assertRaises(webob.exc.HTTPBadRequest,
self.assertRaises(exception.ValidationError,
self.controller._update, req,
DEFAULT_VOLUME_TYPE, body)
DEFAULT_VOLUME_TYPE, body=body)
def test_update_empty_name(self):
body = {"volume_type": {"name": " ",
@ -513,9 +513,9 @@ class VolumeTypesManageApiTest(test.TestCase):
fake.PROJECT_ID, DEFAULT_VOLUME_TYPE))
req.method = 'PUT'
self.assertRaises(webob.exc.HTTPBadRequest,
self.assertRaises(exception.ValidationError,
self.controller._update, req,
DEFAULT_VOLUME_TYPE, body)
DEFAULT_VOLUME_TYPE, body=body)
@mock.patch('cinder.volume.volume_types.get_volume_type')
@mock.patch('cinder.db.volume_type_update')
@ -537,7 +537,8 @@ class VolumeTypesManageApiTest(test.TestCase):
req.environ['cinder.context'] = ctxt
self.assertEqual(0, len(self.notifier.notifications))
res_dict = self.controller._update(req, UPDATE_NAME_ONLY_TYPE, body)
res_dict = self.controller._update(req, UPDATE_NAME_ONLY_TYPE,
body=body)
self.assertEqual(1, len(self.notifier.notifications))
mock_update_quota.assert_called_once_with(ctxt, updated_name, name)
self._check_test_results(res_dict,
@ -558,7 +559,8 @@ class VolumeTypesManageApiTest(test.TestCase):
req.method = 'PUT'
self.assertEqual(0, len(self.notifier.notifications))
res_dict = self.controller._update(req, UPDATE_DESC_ONLY_TYPE, body)
res_dict = self.controller._update(req, UPDATE_DESC_ONLY_TYPE,
body=body)
self.assertEqual(1, len(self.notifier.notifications))
self._check_test_results(res_dict,
{'expected_name': name,
@ -580,7 +582,7 @@ class VolumeTypesManageApiTest(test.TestCase):
req.method = 'PUT'
self.assertEqual(0, len(self.notifier.notifications))
res_dict = self.controller._update(req, DEFAULT_VOLUME_TYPE, body)
res_dict = self.controller._update(req, DEFAULT_VOLUME_TYPE, body=body)
self.assertEqual(1, len(self.notifier.notifications))
self._check_test_results(res_dict,
{'expected_name': updated_name,
@ -595,9 +597,9 @@ class VolumeTypesManageApiTest(test.TestCase):
fake.PROJECT_ID, DEFAULT_VOLUME_TYPE))
req.method = 'PUT'
self.assertRaises(exception.InvalidParameterValue,
self.assertRaises(exception.ValidationError,
self.controller._update, req,
DEFAULT_VOLUME_TYPE, body)
DEFAULT_VOLUME_TYPE, body=body)
@mock.patch('cinder.volume.volume_types.update')
@mock.patch('cinder.volume.volume_types.get_volume_type')
@ -619,7 +621,7 @@ class VolumeTypesManageApiTest(test.TestCase):
self.assertEqual(0, len(self.notifier.notifications))
self.assertRaises(webob.exc.HTTPConflict,
self.controller._update, req,
UPDATE_NAME_AFTER_DELETE_TYPE, body)
UPDATE_NAME_AFTER_DELETE_TYPE, body=body)
self.assertEqual(1, len(self.notifier.notifications))
@ -643,7 +645,7 @@ class VolumeTypesManageApiTest(test.TestCase):
self.notifier.reset()
self.assertEqual(0, len(self.notifier.notifications))
res_dict = self.controller._update(req, UPDATE_NAME_AFTER_DELETE_TYPE,
body)
body=body)
self._check_test_results(res_dict,
{'expected_name': name,
'expected_desc': desc})
@ -673,7 +675,7 @@ class VolumeTypesManageApiTest(test.TestCase):
req.method = 'PUT'
self.assertEqual(0, len(self.notifier.notifications))
res_dict = self.controller._update(req, DEFAULT_VOLUME_TYPE, body)
res_dict = self.controller._update(req, DEFAULT_VOLUME_TYPE, body=body)
self.assertEqual(1, len(self.notifier.notifications))
self._check_test_results(res_dict,
{'expected_desc': updated_desc,
@ -685,7 +687,7 @@ class VolumeTypesManageApiTest(test.TestCase):
exception.PolicyNotAuthorized(action='type_update'))
self.assertRaises(exception.PolicyNotAuthorized,
self.controller._update,
req, DEFAULT_VOLUME_TYPE, body)
req, DEFAULT_VOLUME_TYPE, body=body)
def _check_test_results(self, results, expected_results):
self.assertEqual(1, len(results))