Merge "V3 jsonschema validation: types_extra_specs"

This commit is contained in:
Zuul 2018-03-24 02:47:40 +00:00 committed by Gerrit Code Review
commit 6f6094b833
5 changed files with 91 additions and 64 deletions

View File

@ -68,22 +68,6 @@ ATTRIBUTE_CONVERTERS = {'name~': 'display_name~',
METADATA_TYPES = enum.Enum('METADATA_TYPES', 'user image')
# Regex that matches alphanumeric characters, periods, hyphens,
# colons and underscores:
# ^ assert position at start of the string
# [\w\.\-\:\_] match expression
# $ assert position at end of the string
VALID_KEY_NAME_REGEX = re.compile(r"^[\w\.\-\:\_]+$", re.UNICODE)
def validate_key_names(key_names_list):
"""Validate each item of the list to match key name regex."""
for key_name in key_names_list:
if not VALID_KEY_NAME_REGEX.match(key_name):
return False
return True
def get_pagination_params(params, max_limit=None):
"""Return marker, limit, offset tuple from request.

View File

@ -21,16 +21,16 @@ from oslo_log import versionutils
from six.moves import http_client
import webob
from cinder.api import common
from cinder.api import extensions
from cinder.api.openstack import wsgi
from cinder.api.schemas import types_extra_specs
from cinder.api import validation
from cinder import context as ctxt
from cinder import db
from cinder import exception
from cinder.i18n import _
from cinder.policies import type_extra_specs as policy
from cinder import rpc
from cinder import utils
from cinder.volume import volume_types
LOG = logging.getLogger(__name__)
@ -86,17 +86,14 @@ class VolumeTypeExtraSpecsController(wsgi.Controller):
versionutils.report_deprecated_feature(LOG, msg)
return
def create(self, req, type_id, body=None):
@validation.schema(types_extra_specs.create)
def create(self, req, type_id, body):
context = req.environ['cinder.context']
context.authorize(policy.CREATE_POLICY)
self._allow_update(context, type_id)
self.assert_valid_body(body, 'extra_specs')
self._check_type(context, type_id)
specs = body['extra_specs']
self._check_key_names(specs.keys())
utils.validate_dictionary_string_length(specs)
db.volume_type_extra_specs_update_or_create(context,
type_id,
@ -111,23 +108,16 @@ class VolumeTypeExtraSpecsController(wsgi.Controller):
notifier_info)
return body
def update(self, req, type_id, id, body=None):
@validation.schema(types_extra_specs.update)
def update(self, req, type_id, id, body):
context = req.environ['cinder.context']
context.authorize(policy.UPDATE_POLICY)
self._allow_update(context, type_id)
if not body:
expl = _('Request body empty')
raise webob.exc.HTTPBadRequest(explanation=expl)
self._check_type(context, type_id)
if id not in body:
expl = _('Request body and URI mismatch')
raise webob.exc.HTTPBadRequest(explanation=expl)
if len(body) > 1:
expl = _('Request body contains too many items')
raise webob.exc.HTTPBadRequest(explanation=expl)
self._check_key_names(body.keys())
utils.validate_dictionary_string_length(body)
db.volume_type_extra_specs_update_or_create(context,
type_id,
@ -177,13 +167,6 @@ class VolumeTypeExtraSpecsController(wsgi.Controller):
'volume_type_extra_specs.delete',
notifier_info)
def _check_key_names(self, keys):
if not common.validate_key_names(keys):
expl = _('Key names can only contain alphanumeric characters, '
'underscores, periods, colons and hyphens.')
raise webob.exc.HTTPBadRequest(explanation=expl)
class Types_extra_specs(extensions.ExtensionDescriptor):
"""Type extra specs support."""

View File

@ -0,0 +1,39 @@
# Copyright (C) 2018 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.
"""
Schema for V3 types_extra_specs API.
"""
import copy
from cinder.api.validation import parameter_types
create = {
'type': 'object',
'properties': {
'extra_specs': parameter_types.extra_specs_with_no_spaces_key
},
'required': ['extra_specs'],
'additionalProperties': False,
}
update = copy.deepcopy(parameter_types.extra_specs_with_no_spaces_key)
update.update({
'minProperties': 1,
'maxProperties': 1
})

View File

@ -153,6 +153,17 @@ extra_specs = {
}
extra_specs_with_no_spaces_key = {
'type': 'object',
'patternProperties': {
'^[a-zA-Z0-9-_:.]{1,255}$': {
'type': ['string', 'null'], 'minLength': 0, 'maxLength': 255
}
},
'additionalProperties': False
}
group_snapshot_status = {
'type': 'string', 'format': 'group_snapshot_status'
}

View File

@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import ddt
import mock
from oslo_config import cfg
from oslo_utils import timeutils
@ -63,6 +64,7 @@ def fake_volume_type_extra_specs():
return specs
@ddt.ddt
class VolumeTypesExtraSpecsTest(test.TestCase):
def setUp(self):
@ -130,8 +132,7 @@ class VolumeTypesExtraSpecsTest(test.TestCase):
self.assertRaises(exception.VolumeTypeExtraSpecsNotFound,
self.controller.delete, req, fake.VOLUME_ID, 'key6')
@mock.patch('cinder.utils.check_string_length')
def test_create(self, mock_check):
def test_create(self):
self.mock_object(cinder.db,
'volume_type_extra_specs_update_or_create',
return_create_volume_type_extra_specs)
@ -139,17 +140,15 @@ class VolumeTypesExtraSpecsTest(test.TestCase):
self.assertEqual(0, len(self.notifier.notifications))
req = fakes.HTTPRequest.blank(self.api_path)
res_dict = self.controller.create(req, fake.VOLUME_ID, body)
res_dict = self.controller.create(req, fake.VOLUME_ID, body=body)
self.assertEqual(1, len(self.notifier.notifications))
self.assertIn('created_at', self.notifier.notifications[0]['payload'])
self.assertIn('updated_at', self.notifier.notifications[0]['payload'])
self.assertTrue(mock_check.called)
self.assertEqual('value1', res_dict['extra_specs']['key1'])
@mock.patch.object(cinder.db, 'volume_type_extra_specs_update_or_create')
@mock.patch('cinder.utils.check_string_length')
def test_create_key_allowed_chars(
self, mock_check, volume_type_extra_specs_update_or_create):
self, volume_type_extra_specs_update_or_create):
mock_return_value = {"key1": "value1",
"key2": "value2",
"key3": "value3",
@ -163,16 +162,14 @@ class VolumeTypesExtraSpecsTest(test.TestCase):
self.assertEqual(0, len(self.notifier.notifications))
req = fakes.HTTPRequest.blank(self.api_path)
res_dict = self.controller.create(req, fake.VOLUME_ID, body)
res_dict = self.controller.create(req, fake.VOLUME_ID, body=body)
self.assertEqual(1, len(self.notifier.notifications))
self.assertTrue(mock_check.called)
self.assertEqual('value1',
res_dict['extra_specs']['other_alphanum.-_:'])
@mock.patch.object(cinder.db, 'volume_type_extra_specs_update_or_create')
@mock.patch('cinder.utils.check_string_length')
def test_create_too_many_keys_allowed_chars(
self, mock_check, volume_type_extra_specs_update_or_create):
self, volume_type_extra_specs_update_or_create):
mock_return_value = {"key1": "value1",
"key2": "value2",
"key3": "value3",
@ -188,9 +185,8 @@ class VolumeTypesExtraSpecsTest(test.TestCase):
self.assertEqual(0, len(self.notifier.notifications))
req = fakes.HTTPRequest.blank(self.api_path)
res_dict = self.controller.create(req, fake.VOLUME_ID, body)
res_dict = self.controller.create(req, fake.VOLUME_ID, body=body)
self.assertEqual(1, len(self.notifier.notifications))
self.assertTrue(mock_check.called)
self.assertEqual('value1',
res_dict['extra_specs']['other_alphanum.-_:'])
self.assertEqual('value2',
@ -198,8 +194,7 @@ class VolumeTypesExtraSpecsTest(test.TestCase):
self.assertEqual('value3',
res_dict['extra_specs']['other3_alphanum.-_:'])
@mock.patch('cinder.utils.check_string_length')
def test_update_item(self, mock_check):
def test_update_item(self):
self.mock_object(cinder.db,
'volume_type_extra_specs_update_or_create',
return_create_volume_type_extra_specs)
@ -207,11 +202,11 @@ class VolumeTypesExtraSpecsTest(test.TestCase):
self.assertEqual(0, len(self.notifier.notifications))
req = fakes.HTTPRequest.blank(self.api_path + '/key1')
res_dict = self.controller.update(req, fake.VOLUME_ID, 'key1', body)
res_dict = self.controller.update(req, fake.VOLUME_ID, 'key1',
body=body)
self.assertEqual(1, len(self.notifier.notifications))
self.assertIn('created_at', self.notifier.notifications[0]['payload'])
self.assertIn('updated_at', self.notifier.notifications[0]['payload'])
self.assertTrue(mock_check.called)
self.assertEqual('value1', res_dict['key1'])
@ -222,8 +217,8 @@ class VolumeTypesExtraSpecsTest(test.TestCase):
body = {"key1": "value1", "key2": "value2"}
req = fakes.HTTPRequest.blank(self.api_path + '/key1')
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
req, fake.VOLUME_ID, 'key1', body)
self.assertRaises(exception.ValidationError, self.controller.update,
req, fake.VOLUME_ID, 'key1', body=body)
def test_update_item_body_uri_mismatch(self):
self.mock_object(cinder.db,
@ -233,15 +228,16 @@ class VolumeTypesExtraSpecsTest(test.TestCase):
req = fakes.HTTPRequest.blank(self.api_path + '/bad')
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
req, fake.VOLUME_ID, 'bad', body)
req, fake.VOLUME_ID, 'bad', body=body)
def _extra_specs_empty_update(self, body):
req = fakes.HTTPRequest.blank('/v2/%s/types/%s/extra_specs' % (
fake.PROJECT_ID, fake.VOLUME_TYPE_ID))
req.method = 'POST'
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update, req, fake.VOLUME_ID, body)
self.assertRaises(exception.ValidationError,
self.controller.update, req, fake.VOLUME_ID,
body=body)
def test_update_no_body(self):
self._extra_specs_empty_update(body=None)
@ -254,8 +250,9 @@ class VolumeTypesExtraSpecsTest(test.TestCase):
fake.PROJECT_ID, fake.VOLUME_TYPE_ID))
req.method = 'POST'
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, req, fake.VOLUME_ID, body)
self.assertRaises(exception.ValidationError,
self.controller.create, req, fake.VOLUME_ID,
body=body)
def test_create_no_body(self):
self._extra_specs_create_bad_body(body=None)
@ -295,10 +292,23 @@ class VolumeTypesExtraSpecsTest(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
req,
fake.VOLUME_ID, body)
fake.VOLUME_ID, body=body)
# Again but with conf set to allow modification
CONF.set_default('allow_inuse_volume_type_modification', True)
res_dict = self.controller.create(req, fake.VOLUME_ID, body)
res_dict = self.controller.create(req, fake.VOLUME_ID, body=body)
self.assertEqual({'extra_specs': {'key1': 'value1'}},
res_dict)
@ddt.data({'extra_specs': {'a' * 256: 'a'}},
{'extra_specs': {'a': 'a' * 256}},
{'extra_specs': {'': 'a'}},
{'extra_specs': {' ': 'a'}})
def test_create_with_invalid_extra_specs(self, body):
req = fakes.HTTPRequest.blank('/v2/%s/types/%s/extra_specs' % (
fake.PROJECT_ID, fake.VOLUME_TYPE_ID))
req.method = 'POST'
self.assertRaises(exception.ValidationError,
self.controller.create, req, fake.VOLUME_ID,
body=body)