Merge "V3 jsonschema validation: Volumes"

This commit is contained in:
Zuul 2018-07-17 09:03:23 +00:00 committed by Gerrit Code Review
commit b52d26a361
14 changed files with 397 additions and 219 deletions

View File

@ -12,42 +12,22 @@
# License for the specific language governing permissions and limitations
# under the License.
from cinder.api import extensions
from cinder.api.openstack import wsgi
from cinder.api.schemas import scheduler_hints
from cinder.api import validation
class SchedulerHintsController(wsgi.Controller):
@validation.schema(scheduler_hints.create)
def _extract_scheduler_hints(self, req, body):
hints = {}
attr = '%s:scheduler_hints' % Scheduler_hints.alias
if body.get(attr) is not None:
hints.update(body.get(attr))
return hints
@wsgi.extends
def create(self, req, body):
attr = '%s:scheduler_hints' % Scheduler_hints.alias
def create(req, body):
attr = 'OS-SCH-HNT:scheduler_hints'
if body.get(attr) is not None:
scheduler_hints_body = dict.fromkeys((attr,), body.get(attr))
hints = self._extract_scheduler_hints(req, body=scheduler_hints_body)
if 'volume' in body:
body['volume']['scheduler_hints'] = hints
yield
@validation.schema(scheduler_hints.create)
def _validate_scheduler_hints(req=None, body=None):
# TODO(pooja_jadhav): The scheduler hints schema validation
# should be moved to v3 volume schema directly and this module
# should be deleted at the time of deletion of v2 version code.
pass
class Scheduler_hints(extensions.ExtensionDescriptor):
"""Pass arbitrary key/value pairs to the scheduler."""
name = "SchedulerHints"
alias = "OS-SCH-HNT"
updated = "2013-04-18T00:00:00+00:00"
def get_controller_extensions(self):
controller = SchedulerHintsController()
ext = extensions.ControllerExtension(self, 'volumes', controller)
return [ext]
_validate_scheduler_hints(req=req, body=scheduler_hints_body)
body['volume']['scheduler_hints'] = scheduler_hints_body.get(attr)
return body

View File

@ -31,7 +31,7 @@ import cinder.policy
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
FILES_TO_SKIP = ['resource_common_manage.py']
FILES_TO_SKIP = ['resource_common_manage.py', 'scheduler_hints.py']
class ExtensionDescriptor(object):

View File

@ -143,6 +143,8 @@ BACKUP_AZ = '3.51'
SUPPORT_VOLUME_TYPE_FILTER = '3.52'
SUPPORT_VOLUME_SCHEMA_CHANGES = '3.53'
def get_mv_header(version):
"""Gets a formatted HTTP microversion header.

View File

@ -118,6 +118,13 @@ REST_API_VERSION_HISTORY = """
* 3.52 - ``RESKEY:availability_zones`` is a reserved spec key for AZ
volume type, and filter volume type by ``extra_specs`` is
supported now.
* 3.53 - Add schema validation support for request body using jsonschema
for V2/V3 volume APIs.
1. Modified create volume API to accept only parameters which are
documented in the api-ref otherwise it will return 400 error.
2. Update volume API expects user to pass at least one valid
parameter in the request body in order to update the volume.
Also, additional parameters will not be allowed.
"""
# The minimum and maximum versions of the API supported
@ -125,9 +132,9 @@ REST_API_VERSION_HISTORY = """
# minimum version of the API supported.
# Explicitly using /v2 endpoints will still work
_MIN_API_VERSION = "3.0"
_MAX_API_VERSION = "3.52"
_MAX_API_VERSION = "3.53"
_LEGACY_API_VERSION2 = "2.0"
UPDATED = "2017-09-19T20:18:14Z"
UPDATED = "2018-06-29T05:34:49Z"
# NOTE(cyeoh): min and max versions declared as functions so we can

View File

@ -409,3 +409,27 @@ Add support for cross AZ backups.
----
``RESKEY:availability_zones`` is a reserved spec key for AZ volume type,
and filter volume type by ``extra_specs`` is supported now.
3.53
----
Schema validation support has been added using jsonschema for V2/V3
volume APIs.
- Create volume API
Before 3.53, create volume API used to accept any invalid parameters in the
request body like the ones below were passed by python-cinderclient.
1. user_id
2. project_id
3. status
4. attach_status
But in 3.53, this behavior is updated. If user passes any invalid
parameters to the API which are not documented in api-ref, then
it will raise badRequest error.
- Update volume API
Before 3.53, even if user doesn't pass any valid parameters in the request body,
the volume was updated.
But in 3.53, user will need to pass at least one valid parameter in the request
body otherwise it will return 400 error.

View File

@ -0,0 +1,121 @@
# 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 Volumes API.
"""
import copy
from cinder.api.validation import parameter_types
create = {
'type': 'object',
'properties': {
'volume': {
'type': 'object',
'properties': {
'name': {'type': ['string', 'null'],
'format': 'name_non_mandatory_remove_white_spaces'},
'description': {
'type': ['string', 'null'],
'format': 'description_non_mandatory_remove_white_spaces'},
'display_name': {
'type': ['string', 'null'],
'format': 'name_non_mandatory_remove_white_spaces'},
'display_description': {
'type': ['string', 'null'],
'format':
'description_non_mandatory_remove_white_spaces'},
# volume_type accepts 'id' as well as 'name' so do lazy schema
# validation for it.
'volume_type': parameter_types.name_allow_zero_min_length,
'metadata': parameter_types.metadata_allows_null,
'snapshot_id': parameter_types.optional_uuid,
'source_volid': parameter_types.optional_uuid,
'consistencygroup_id': parameter_types.optional_uuid,
'size': parameter_types.volume_size,
'availability_zone': parameter_types.availability_zone,
'multiattach': parameter_types.optional_boolean,
'image_id': {'type': ['string', 'null'], 'minLength': 0,
'maxLength': 255},
'imageRef': {'type': ['string', 'null'], 'minLength': 0,
'maxLength': 255},
},
'required': ['size'],
'additionalProperties': True,
},
'OS-SCH-HNT:scheduler_hints': {
'type': ['object', 'null']
},
},
'required': ['volume'],
'additionalProperties': False,
}
create_volume_v313 = copy.deepcopy(create)
create_volume_v313['properties']['volume']['properties'][
'group_id'] = {'type': ['string', 'null'], 'minLength': 0,
'maxLength': 255}
create_volume_v347 = copy.deepcopy(create_volume_v313)
create_volume_v347['properties']['volume']['properties'][
'backup_id'] = parameter_types.optional_uuid
create_volume_v353 = copy.deepcopy(create_volume_v347)
create_volume_v353['properties']['volume']['additionalProperties'] = False
update = {
'type': 'object',
'properties': {
'volume': {
'type': 'object',
'properties': {
# The 'name' and 'description' are required to be compatible
# with v2.
'name': {
'type': ['string', 'null'],
'format': 'name_non_mandatory_remove_white_spaces'},
'description': {
'type': ['string', 'null'],
'format':
'description_non_mandatory_remove_white_spaces'},
'display_name': {
'type': ['string', 'null'],
'format': 'name_non_mandatory_remove_white_spaces'},
'display_description': {
'type': ['string', 'null'],
'format':
'description_non_mandatory_remove_white_spaces'},
'metadata': parameter_types.extra_specs,
},
'additionalProperties': False,
},
},
'required': ['volume'],
'additionalProperties': False,
}
update_volume_v353 = copy.deepcopy(update)
update_volume_v353['properties']['volume']['anyOf'] = [
{'required': ['name']},
{'required': ['description']},
{'required': ['display_name']},
{'required': ['display_description']},
{'required': ['metadata']}]

View File

@ -25,8 +25,11 @@ import webob
from webob import exc
from cinder.api import common
from cinder.api.contrib import scheduler_hints
from cinder.api.openstack import wsgi
from cinder.api.schemas import volumes
from cinder.api.v2.views import volumes as volume_views
from cinder.api import validation
from cinder import exception
from cinder import group as group_api
from cinder.i18n import _
@ -172,24 +175,23 @@ class VolumeController(wsgi.Controller):
raise exc.HTTPBadRequest(explanation=msg)
@wsgi.response(http_client.ACCEPTED)
@validation.schema(volumes.create, '2.0')
def create(self, req, body):
"""Creates a new volume."""
self.assert_valid_body(body, 'volume')
LOG.debug('Create volume request body: %s', body)
context = req.environ['cinder.context']
# NOTE (pooja_jadhav) To fix bug 1774155, scheduler hints is not
# loaded as a standard extension. If user passes
# OS-SCH-HNT:scheduler_hints in the request body, then it will be
# validated in the create method and this method will add
# scheduler_hints in body['volume'].
body = scheduler_hints.create(req, body)
volume = body['volume']
# Check up front for legacy replication parameters to quick fail
source_replica = volume.get('source_replica')
if source_replica:
msg = _("Creating a volume from a replica source was part of the "
"replication v1 implementation which is no longer "
"available.")
raise exception.InvalidInput(reason=msg)
kwargs = {}
self.validate_name_and_description(volume)
self.validate_name_and_description(volume, check_length=False)
# NOTE(thingee): v2 API allows name instead of display_name
if 'name' in volume:
@ -213,9 +215,6 @@ class VolumeController(wsgi.Controller):
snapshot_id = volume.get('snapshot_id')
if snapshot_id is not None:
if not uuidutils.is_uuid_like(snapshot_id):
msg = _("Snapshot ID must be in UUID form.")
raise exc.HTTPBadRequest(explanation=msg)
# Not found exception will be handled at the wsgi level
kwargs['snapshot'] = self.volume_api.get_snapshot(context,
snapshot_id)
@ -224,10 +223,6 @@ class VolumeController(wsgi.Controller):
source_volid = volume.get('source_volid')
if source_volid is not None:
if not uuidutils.is_uuid_like(source_volid):
msg = _("Source volume ID '%s' must be a "
"valid UUID.") % source_volid
raise exc.HTTPBadRequest(explanation=msg)
# Not found exception will be handled at the wsgi level
kwargs['source_volume'] = \
self.volume_api.get_volume(context,
@ -239,10 +234,6 @@ class VolumeController(wsgi.Controller):
kwargs['consistencygroup'] = None
consistencygroup_id = volume.get('consistencygroup_id')
if consistencygroup_id is not None:
if not uuidutils.is_uuid_like(consistencygroup_id):
msg = _("Consistency group ID '%s' must be a "
"valid UUID.") % consistencygroup_id
raise exc.HTTPBadRequest(explanation=msg)
# Not found exception will be handled at the wsgi level
kwargs['group'] = self.group_api.get(context, consistencygroup_id)
@ -285,34 +276,14 @@ class VolumeController(wsgi.Controller):
"""Return volume search options allowed by non-admin."""
return CONF.query_volume_filters
@validation.schema(volumes.update, '2.0', '3.52')
@validation.schema(volumes.update_volume_v353, '3.53')
def update(self, req, id, body):
"""Update a volume."""
context = req.environ['cinder.context']
update_dict = body['volume']
if not body:
msg = _("Missing request body")
raise exc.HTTPBadRequest(explanation=msg)
if 'volume' not in body:
msg = _("Missing required element '%s' in request body") % 'volume'
raise exc.HTTPBadRequest(explanation=msg)
volume = body['volume']
update_dict = {}
valid_update_keys = (
'name',
'description',
'display_name',
'display_description',
'metadata',
)
for key in valid_update_keys:
if key in volume:
update_dict[key] = volume[key]
self.validate_name_and_description(update_dict)
self.validate_name_and_description(update_dict, check_length=False)
# NOTE(thingee): v2 API allows name instead of display_name
if 'name' in update_dict:

View File

@ -15,17 +15,19 @@
from oslo_log import log as logging
from oslo_log import versionutils
from oslo_utils import uuidutils
import six
from six.moves import http_client
import webob
from webob import exc
from cinder.api import common
from cinder.api.contrib import scheduler_hints
from cinder.api import microversions as mv
from cinder.api.openstack import wsgi
from cinder.api.schemas import volumes
from cinder.api.v2 import volumes as volumes_v2
from cinder.api.v3.views import volumes as volume_views_v3
from cinder.api import validation
from cinder.backup import api as backup_api
from cinder import exception
from cinder import group as group_api
@ -224,6 +226,10 @@ class VolumeController(volumes_v2.VolumeController):
return image_snapshot
@wsgi.response(http_client.ACCEPTED)
@validation.schema(volumes.create, '3.0', '3.12')
@validation.schema(volumes.create_volume_v313, '3.13', '3.46')
@validation.schema(volumes.create_volume_v347, '3.47', '3.52')
@validation.schema(volumes.create_volume_v353, '3.53')
def create(self, req, body):
"""Creates a new volume.
@ -232,39 +238,21 @@ class VolumeController(volumes_v2.VolumeController):
:returns: dict -- the new volume dictionary
:raises HTTPNotFound, HTTPBadRequest:
"""
self.assert_valid_body(body, 'volume')
LOG.debug('Create volume request body: %s', body)
context = req.environ['cinder.context']
req_version = req.api_version_request
# Remove group_id from body if max version is less than GROUP_VOLUME.
if req_version.matches(None, mv.get_prior_version(mv.GROUP_VOLUME)):
# NOTE(xyang): The group_id is from a group created with a
# group_type. So with this group_id, we've got a group_type
# for this volume. Also if group_id is passed in, that means
# we already know which backend is hosting the group and the
# volume will be created on the same backend as well. So it
# won't go through the scheduler again if a group_id is
# passed in.
try:
body.get('volume', {}).pop('group_id', None)
except AttributeError:
msg = (_("Invalid body provided for creating volume. "
"Request API version: %s.") % req_version)
raise exc.HTTPBadRequest(explanation=msg)
# NOTE (pooja_jadhav) To fix bug 1774155, scheduler hints is not
# loaded as a standard extension. If user passes
# OS-SCH-HNT:scheduler_hints in the request body, then it will be
# validated in the create method and this method will add
# scheduler_hints in body['volume'].
body = scheduler_hints.create(req, body)
volume = body['volume']
kwargs = {}
self.validate_name_and_description(volume)
# Check up front for legacy replication parameters to quick fail
source_replica = volume.get('source_replica')
if source_replica:
msg = _("Creating a volume from a replica source was part of the "
"replication v1 implementation which is no longer "
"available.")
raise exception.InvalidInput(reason=msg)
self.validate_name_and_description(volume, check_length=False)
# NOTE(thingee): v2 API allows name instead of display_name
if 'name' in volume:
@ -288,9 +276,6 @@ class VolumeController(volumes_v2.VolumeController):
snapshot_id = volume.get('snapshot_id')
if snapshot_id is not None:
if not uuidutils.is_uuid_like(snapshot_id):
msg = _("Snapshot ID must be in UUID form.")
raise exc.HTTPBadRequest(explanation=msg)
# Not found exception will be handled at the wsgi level
kwargs['snapshot'] = self.volume_api.get_snapshot(context,
snapshot_id)
@ -299,10 +284,6 @@ class VolumeController(volumes_v2.VolumeController):
source_volid = volume.get('source_volid')
if source_volid is not None:
if not uuidutils.is_uuid_like(source_volid):
msg = _("Source volume ID '%s' must be a "
"valid UUID.") % source_volid
raise exc.HTTPBadRequest(explanation=msg)
# Not found exception will be handled at the wsgi level
kwargs['source_volume'] = (
self.volume_api.get_volume(context,
@ -314,10 +295,6 @@ class VolumeController(volumes_v2.VolumeController):
kwargs['consistencygroup'] = None
consistencygroup_id = volume.get('consistencygroup_id')
if consistencygroup_id is not None:
if not uuidutils.is_uuid_like(consistencygroup_id):
msg = _("Consistency group ID '%s' must be a "
"valid UUID.") % consistencygroup_id
raise exc.HTTPBadRequest(explanation=msg)
# Not found exception will be handled at the wsgi level
kwargs['group'] = self.group_api.get(context, consistencygroup_id)
@ -338,18 +315,10 @@ class VolumeController(volumes_v2.VolumeController):
else:
kwargs['image_id'] = image_uuid
# Add backup if min version is greater than or equal
# to VOLUME_CREATE_FROM_BACKUP.
if req_version.matches(mv.VOLUME_CREATE_FROM_BACKUP, None):
backup_id = volume.get('backup_id')
if backup_id:
if not uuidutils.is_uuid_like(backup_id):
msg = _("Backup ID must be in UUID form.")
raise exc.HTTPBadRequest(explanation=msg)
kwargs['backup'] = self.backup_api.get(context,
backup_id=backup_id)
else:
kwargs['backup'] = None
backup_id = volume.get('backup_id')
if backup_id:
kwargs['backup'] = self.backup_api.get(context,
backup_id=backup_id)
size = volume.get('size', None)
if size is None and kwargs['snapshot'] is not None:

View File

@ -208,7 +208,8 @@ nullable_string = {
volume_size = {
'type': ['integer', 'string'],
'pattern': '^[0-9]+$',
'minimum': 1
'minimum': 1,
'maximum': constants.DB_MAX_INT
}
@ -262,3 +263,11 @@ key_size = {'type': ['string', 'integer', 'null'],
'minimum': 0,
'maximum': constants.DB_MAX_INT,
'format': 'key_size'}
availability_zone = {
'type': ['string', 'null'], 'minLength': 1, 'maxLength': 255
}
optional_boolean = {'oneOf': [{'type': 'null'}, boolean]}

View File

@ -219,6 +219,24 @@ def _validate_disabled_reason(param_value):
return True
@jsonschema.FormatChecker.cls_checks(
'name_non_mandatory_remove_white_spaces')
def _validate_name_non_mandatory_remove_white_spaces(param_value):
_validate_string_length(param_value, 'name',
mandatory=False, min_length=0, max_length=255,
remove_whitespaces=True)
return True
@jsonschema.FormatChecker.cls_checks(
'description_non_mandatory_remove_white_spaces')
def _validate_description_non_mandatory_remove_white_spaces(param_value):
_validate_string_length(param_value, 'description',
mandatory=False, min_length=0, max_length=255,
remove_whitespaces=True)
return True
@jsonschema.FormatChecker.cls_checks('quota_set')
def _validate_quota_set(quota_set):
bad_keys = []

View File

@ -137,7 +137,7 @@ class VolumeMetaDataTest(test.TestCase):
"metadata": {}}
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
self.volume_controller.create(req, body)
self.volume_controller.create(req, body=body)
def test_index(self):
req = fakes.HTTPRequest.blank(self.url)

View File

@ -74,7 +74,7 @@ class VolumeApiTest(test.TestCase):
vol = self._vol_in_request_body()
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
ex = self._expected_vol_from_controller()
self.assertEqual(ex, res_dict)
self.assertTrue(mock_validate.called)
@ -100,19 +100,19 @@ class VolumeApiTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/volumes')
# Raise 404 when type name isn't valid
self.assertRaises(exception.VolumeTypeNotFoundByName,
self.controller.create, req, body)
self.controller.create, req, body=body)
# Use correct volume type name
vol.update(dict(volume_type=CONF.default_volume_type))
vol = self._vol_in_request_body(volume_type=CONF.default_volume_type)
body.update(dict(volume=vol))
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
volume_id = res_dict['volume']['id']
self.assertEqual(1, len(res_dict))
# Use correct volume type id
vol.update(dict(volume_type=db_vol_type['id']))
vol = self._vol_in_request_body(volume_type=db_vol_type['id'])
body.update(dict(volume=vol))
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
volume_id = res_dict['volume']['id']
self.assertEqual(1, len(res_dict))
@ -241,7 +241,7 @@ class VolumeApiTest(test.TestCase):
vol = self._vol_in_request_body(snapshot_id=snapshot_id)
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
ex = self._expected_vol_from_controller(snapshot_id=snapshot_id)
self.assertEqual(ex, res_dict)
@ -268,7 +268,7 @@ class VolumeApiTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/volumes')
# Raise 404 when snapshot cannot be found.
self.assertRaises(exception.SnapshotNotFound, self.controller.create,
req, body)
req, body=body)
context = req.environ['cinder.context']
get_snapshot.assert_called_once_with(self.controller.volume_api,
context, snapshot_id)
@ -282,8 +282,8 @@ class VolumeApiTest(test.TestCase):
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
# Raise 400 when snapshot has not uuid type.
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
req, body)
self.assertRaises(exception.ValidationError, self.controller.create,
req, body=body)
@mock.patch.object(db.sqlalchemy.api, '_volume_type_get_full',
autospec=True)
@ -299,7 +299,7 @@ class VolumeApiTest(test.TestCase):
vol = self._vol_in_request_body(source_volid=source_volid)
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
ex = self._expected_vol_from_controller(source_volid=source_volid)
self.assertEqual(ex, res_dict)
@ -329,7 +329,7 @@ class VolumeApiTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/volumes')
# Raise 404 when source volume cannot be found.
self.assertRaises(exception.VolumeNotFound, self.controller.create,
req, body)
req, body=body)
context = req.environ['cinder.context']
get_volume.assert_called_once_with(self.controller.volume_api,
@ -345,8 +345,8 @@ class VolumeApiTest(test.TestCase):
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
# Raise 400 for resource requested with invalid uuids.
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
req, body)
self.assertRaises(exception.ValidationError, self.controller.create,
req, body=body)
@mock.patch.object(groupAPI.API, 'get', autospec=True)
def test_volume_creation_fails_with_invalid_consistency_group(self,
@ -361,7 +361,7 @@ class VolumeApiTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/volumes')
# Raise 404 when consistency group is not found.
self.assertRaises(exception.GroupNotFound,
self.controller.create, req, body)
self.controller.create, req, body=body)
context = req.environ['cinder.context']
get_cg.assert_called_once_with(self.controller.group_api,
@ -371,10 +371,10 @@ class VolumeApiTest(test.TestCase):
vol = self._vol_in_request_body(size="")
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
self.assertRaises(exception.InvalidInput,
self.assertRaises(exception.ValidationError,
self.controller.create,
req,
body)
body=body)
def test_volume_creation_fails_with_bad_availability_zone(self):
vol = self._vol_in_request_body(availability_zone="zonen:hostn")
@ -382,7 +382,7 @@ class VolumeApiTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/volumes')
self.assertRaises(exception.InvalidAvailabilityZone,
self.controller.create,
req, body)
req, body=body)
@mock.patch(
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
@ -399,7 +399,7 @@ class VolumeApiTest(test.TestCase):
ex = self._expected_vol_from_controller(availability_zone="nova")
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
self.assertEqual(ex, res_dict)
self.assertTrue(mock_validate.called)
@ -410,10 +410,10 @@ class VolumeApiTest(test.TestCase):
image_ref=1234)
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
self.assertRaises(webob.exc.HTTPBadRequest,
self.assertRaises(exception.ValidationError,
self.controller.create,
req,
body)
body=body)
def test_volume_create_with_image_ref_not_uuid_format(self):
self.mock_object(volume_api.API, "create", v2_fakes.fake_volume_create)
@ -428,7 +428,7 @@ class VolumeApiTest(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
req,
body)
body=body)
def test_volume_create_with_image_ref_with_empty_string(self):
self.mock_object(volume_api.API, "create", v2_fakes.fake_volume_create)
@ -443,7 +443,7 @@ class VolumeApiTest(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
req,
body)
body=body)
@mock.patch(
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
@ -460,7 +460,7 @@ class VolumeApiTest(test.TestCase):
ex = self._expected_vol_from_controller(availability_zone="nova")
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
self.assertEqual(ex, res_dict)
self.assertTrue(mock_validate.called)
@ -471,10 +471,10 @@ class VolumeApiTest(test.TestCase):
image_id=1234)
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
self.assertRaises(webob.exc.HTTPBadRequest,
self.assertRaises(exception.ValidationError,
self.controller.create,
req,
body)
body=body)
def test_volume_create_with_image_id_not_uuid_format(self):
self.mock_object(volume_api.API, "create", v2_fakes.fake_volume_create)
@ -489,7 +489,7 @@ class VolumeApiTest(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
req,
body)
body=body)
def test_volume_create_with_image_id_with_empty_string(self):
self.mock_object(volume_api.API, "create", v2_fakes.fake_volume_create)
@ -504,7 +504,7 @@ class VolumeApiTest(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
req,
body)
body=body)
@mock.patch(
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
@ -524,9 +524,8 @@ class VolumeApiTest(test.TestCase):
ex = self._expected_vol_from_controller(availability_zone="nova")
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
self.assertEqual(ex, res_dict)
self.assertTrue(mock_validate.called)
def test_volume_create_with_image_name_has_multiple(self):
self.mock_object(db, 'volume_get', v2_fakes.fake_volume_get_db)
@ -544,7 +543,7 @@ class VolumeApiTest(test.TestCase):
self.assertRaises(webob.exc.HTTPConflict,
self.controller.create,
req,
body)
body=body)
def test_volume_create_with_image_name_no_match(self):
self.mock_object(db, 'volume_get', v2_fakes.fake_volume_get_db)
@ -562,17 +561,17 @@ class VolumeApiTest(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
req,
body)
body=body)
def test_volume_create_with_invalid_multiattach(self):
vol = self._vol_in_request_body(multiattach="InvalidBool")
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
self.assertRaises(exception.InvalidParameterValue,
self.assertRaises(exception.ValidationError,
self.controller.create,
req,
body)
body=body)
@mock.patch.object(volume_api.API, 'create', autospec=True)
@mock.patch.object(volume_api.API, 'get', autospec=True)
@ -591,7 +590,7 @@ class VolumeApiTest(test.TestCase):
ex = self._expected_vol_from_controller(multiattach=True)
req = fakes.HTTPRequest.blank('/v2/volumes')
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
self.assertEqual(ex, res_dict)
@ -605,36 +604,36 @@ class VolumeApiTest(test.TestCase):
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
if len(list(value.keys())[0]) == 0 or list(value.values())[0] is None:
exc = exception.InvalidVolumeMetadata
else:
exc = exception.InvalidVolumeMetadataSize
self.assertRaises(exc,
self.assertRaises(exception.ValidationError,
self.controller.create,
req,
body)
body=body)
@mock.patch(
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
def test_volume_update(self, mock_validate):
@ddt.data({"name": "Updated Test Name",
"description": "Updated Test Description"},
{"name": " test name ",
"description": " test description "})
def test_volume_update(self, body):
self.mock_object(volume_api.API, 'get', v2_fakes.fake_volume_api_get)
self.mock_object(volume_api.API, "update", v2_fakes.fake_volume_update)
self.mock_object(db.sqlalchemy.api, '_volume_type_get_full',
v2_fakes.fake_volume_type_get)
updates = {
"name": "Updated Test Name",
"name": body['name'],
"description": body['description']
}
body = {"volume": updates}
req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID)
self.assertEqual(0, len(self.notifier.notifications))
res_dict = self.controller.update(req, fake.VOLUME_ID, body)
name = updates["name"].strip()
description = updates["description"].strip()
expected = self._expected_vol_from_controller(
availability_zone=v2_fakes.DEFAULT_AZ, name="Updated Test Name",
availability_zone=v2_fakes.DEFAULT_AZ, name=name,
description=description,
metadata={'attached_mode': 'rw', 'readonly': 'False'})
res_dict = self.controller.update(req, fake.VOLUME_ID, body=body)
self.assertEqual(expected, res_dict)
self.assertEqual(2, len(self.notifier.notifications))
self.assertTrue(mock_validate.called)
@mock.patch(
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
@ -651,7 +650,7 @@ class VolumeApiTest(test.TestCase):
body = {"volume": updates}
req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID)
self.assertEqual(0, len(self.notifier.notifications))
res_dict = self.controller.update(req, fake.VOLUME_ID, body)
res_dict = self.controller.update(req, fake.VOLUME_ID, body=body)
expected = self._expected_vol_from_controller(
availability_zone=v2_fakes.DEFAULT_AZ, name="Updated Test Name",
description="Updated Test Description",
@ -678,7 +677,7 @@ class VolumeApiTest(test.TestCase):
body = {"volume": updates}
req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID)
self.assertEqual(0, len(self.notifier.notifications))
res_dict = self.controller.update(req, fake.VOLUME_ID, body)
res_dict = self.controller.update(req, fake.VOLUME_ID, body=body)
expected = self._expected_vol_from_controller(
availability_zone=v2_fakes.DEFAULT_AZ,
name="New Name", description="New Description",
@ -701,7 +700,7 @@ class VolumeApiTest(test.TestCase):
body = {"volume": updates}
req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID)
self.assertEqual(0, len(self.notifier.notifications))
res_dict = self.controller.update(req, fake.VOLUME_ID, body)
res_dict = self.controller.update(req, fake.VOLUME_ID, body=body)
expected = self._expected_vol_from_controller(
availability_zone=v2_fakes.DEFAULT_AZ,
metadata={'attached_mode': 'rw', 'readonly': 'False',
@ -742,7 +741,7 @@ class VolumeApiTest(test.TestCase):
self.assertEqual(0, len(self.notifier.notifications))
admin_ctx = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True)
req.environ['cinder.context'] = admin_ctx
res_dict = self.controller.update(req, fake.VOLUME_ID, body)
res_dict = self.controller.update(req, fake.VOLUME_ID, body=body)
expected = self._expected_vol_from_controller(
availability_zone=v2_fakes.DEFAULT_AZ, volume_type=None,
status='in-use', name='Updated Test Name',
@ -776,29 +775,36 @@ class VolumeApiTest(test.TestCase):
body = {"volume": updates}
req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID)
if len(list(value.keys())[0]) == 0 or list(value.values())[0] is None:
exc = exception.InvalidVolumeMetadata
else:
exc = webob.exc.HTTPRequestEntityTooLarge
self.assertRaises(exc,
self.assertRaises(exception.ValidationError,
self.controller.update,
req, fake.VOLUME_ID, body)
req, fake.VOLUME_ID, body=body)
def test_update_empty_body(self):
body = {}
req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID)
self.assertRaises(webob.exc.HTTPBadRequest,
self.assertRaises(exception.ValidationError,
self.controller.update,
req, fake.VOLUME_ID, body)
req, fake.VOLUME_ID, body=body)
def test_update_invalid_body(self):
body = {
'name': 'missing top level volume key'
}
req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID)
self.assertRaises(webob.exc.HTTPBadRequest,
self.assertRaises(exception.ValidationError,
self.controller.update,
req, fake.VOLUME_ID, body)
req, fake.VOLUME_ID, body=body)
@ddt.data({'name': 'a' * 256},
{'description': 'a' * 256},
{'display_name': 'a' * 256},
{'display_description': 'a' * 256})
def test_update_exceeds_length_name_description(self, vol):
req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID)
body = {'volume': vol}
self.assertRaises(exception.InvalidInput,
self.controller.update,
req, fake.VOLUME_ID, body=body)
def test_update_not_found(self):
self.mock_object(volume_api.API, "get",
@ -810,7 +816,7 @@ class VolumeApiTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID)
self.assertRaises(exception.VolumeNotFound,
self.controller.update,
req, fake.VOLUME_ID, body)
req, fake.VOLUME_ID, body=body)
def test_volume_list_summary(self):
self.mock_object(volume_api.API, 'get_all',
@ -1476,8 +1482,8 @@ class VolumeApiTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v2/%s/volumes' % 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_bad_request(body=None)

View File

@ -163,7 +163,7 @@ class VolumeMetaDataTest(test.TestCase):
"metadata": {}}
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
self.volume_controller.create(req, body)
self.volume_controller.create(req, body=body)
def test_index(self):
req = fakes.HTTPRequest.blank(self.url, version=mv.ETAGS)

View File

@ -16,7 +16,9 @@ import ddt
import iso8601
import mock
from oslo_serialization import jsonutils
from oslo_utils import strutils
from six.moves import http_client
import webob
from cinder.api import extensions
@ -284,7 +286,7 @@ class VolumeApiTest(test.TestCase):
req = fakes.HTTPRequest.blank('/v3/volumes')
req.headers = mv.get_mv_header(mv.SUPPORT_NOVA_IMAGE)
req.api_version_request = mv.get_api_version(mv.SUPPORT_NOVA_IMAGE)
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
self.assertEqual(ex, res_dict)
context = req.environ['cinder.context']
get_snapshot.assert_called_once_with(self.controller.volume_api,
@ -314,7 +316,7 @@ class VolumeApiTest(test.TestCase):
availability_zone="nova")
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v3/volumes')
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
req = self._fake_volumes_summary_request()
res_dict = self.controller.summary(req)
@ -498,25 +500,34 @@ class VolumeApiTest(test.TestCase):
return volume
@ddt.data(mv.GROUP_VOLUME, mv.get_prior_version(mv.GROUP_VOLUME))
@mock.patch(
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
def test_volume_create(self, max_ver, mock_validate):
@ddt.data((mv.GROUP_VOLUME,
{'display_name': ' test name ',
'display_description': ' test desc ',
'size': 1}),
(mv.get_prior_version(mv.GROUP_VOLUME),
{'name': ' test name ',
'description': ' test desc ',
'size': 1}))
@ddt.unpack
def test_volume_create(self, max_ver, volume_body):
self.mock_object(volume_api.API, 'get', v2_fakes.fake_volume_get)
self.mock_object(volume_api.API, "create",
v2_fakes.fake_volume_api_create)
self.mock_object(db.sqlalchemy.api, '_volume_type_get_full',
v2_fakes.fake_volume_type_get)
vol = self._vol_in_request_body()
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v3/volumes')
req.api_version_request = mv.get_api_version(max_ver)
res_dict = self.controller.create(req, body)
body = {'volume': volume_body}
res_dict = self.controller.create(req, body=body)
ex = self._expected_vol_from_controller(
req_version=req.api_version_request)
self.assertEqual(ex, res_dict)
self.assertTrue(mock_validate.called)
req_version=req.api_version_request, name='test name',
description='test desc')
self.assertEqual(ex['volume']['name'],
res_dict['volume']['name'])
self.assertEqual(ex['volume']['description'],
res_dict['volume']['description'])
@ddt.data(mv.GROUP_SNAPSHOTS, mv.get_prior_version(mv.GROUP_SNAPSHOTS))
@mock.patch.object(group_api.API, 'get')
@ -542,7 +553,7 @@ class VolumeApiTest(test.TestCase):
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v3/volumes')
req.api_version_request = mv.get_api_version(max_ver)
res_dict = self.controller.create(req, body)
res_dict = self.controller.create(req, body=body)
ex = self._expected_vol_from_controller(
snapshot_id=snapshot_id,
req_version=req.api_version_request)
@ -574,11 +585,14 @@ class VolumeApiTest(test.TestCase):
volume_type_get.side_effect = v2_fakes.fake_volume_type_get
backup_id = fake.BACKUP_ID
vol = self._vol_in_request_body(backup_id=backup_id)
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v3/volumes')
req.api_version_request = mv.get_api_version(max_ver)
res_dict = self.controller.create(req, body)
if max_ver == mv.VOLUME_CREATE_FROM_BACKUP:
vol = self._vol_in_request_body(backup_id=backup_id)
else:
vol = self._vol_in_request_body()
body = {"volume": vol}
res_dict = self.controller.create(req, body=body)
ex = self._expected_vol_from_controller(
req_version=req.api_version_request)
self.assertEqual(ex, res_dict)
@ -597,6 +611,63 @@ class VolumeApiTest(test.TestCase):
v2_fakes.DEFAULT_VOL_DESCRIPTION,
**kwargs)
def test_volume_creation_with_scheduler_hints(self):
vol = self._vol_in_request_body(availability_zone=None)
vol.pop('group_id')
body = {"volume": vol,
"OS-SCH-HNT:scheduler_hints": {
'different_host': [fake.UUID1, fake.UUID2]}}
req = webob.Request.blank('/v3/%s/volumes' % fake.PROJECT_ID)
req.method = 'POST'
req.headers['Content-Type'] = 'application/json'
req.body = jsonutils.dump_as_bytes(body)
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.ctxt))
res_dict = jsonutils.loads(res.body)
self.assertEqual(http_client.ACCEPTED, res.status_int)
self.assertIn('id', res_dict['volume'])
@ddt.data('fake_host', '', 1234, ' ')
def test_volume_creation_invalid_scheduler_hints(self, invalid_hints):
vol = self._vol_in_request_body()
vol.pop('group_id')
body = {"volume": vol,
"OS-SCH-HNT:scheduler_hints": {
'different_host': invalid_hints}}
req = fakes.HTTPRequest.blank('/v3/volumes')
self.assertRaises(exception.ValidationError, self.controller.create,
req, body=body)
@ddt.data({'size': 'a'},
{'size': ''},
{'size': 0},
{'size': 2 ** 31},
{'size': None},
{})
def test_volume_creation_fails_with_invalid_parameters(
self, vol_body):
body = {"volume": vol_body}
req = fakes.HTTPRequest.blank('/v3/volumes')
self.assertRaises(exception.ValidationError, self.controller.create,
req, body=body)
def test_volume_creation_fails_with_additional_properties(self):
body = {"volume": {"size": 1, "user_id": fake.USER_ID,
"project_id": fake.PROJECT_ID}}
req = fakes.HTTPRequest.blank('/v3/volumes')
req.api_version_request = mv.get_api_version(
mv.SUPPORT_VOLUME_SCHEMA_CHANGES)
self.assertRaises(exception.ValidationError, self.controller.create,
req, body=body)
def test_volume_update_without_vol_data(self):
body = {"volume": {}}
req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID)
req.api_version_request = mv.get_api_version(
mv.SUPPORT_VOLUME_SCHEMA_CHANGES)
self.assertRaises(exception.ValidationError, self.controller.update,
req, fake.VOLUME_ID, body=body)
@ddt.data({'s': 'ea895e29-8485-4930-bbb8-c5616a309c0e'},
['ea895e29-8485-4930-bbb8-c5616a309c0e'],
42)
@ -606,8 +677,8 @@ class VolumeApiTest(test.TestCase):
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v3/volumes')
# Raise 400 when snapshot has not uuid type.
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
req, body)
self.assertRaises(exception.ValidationError, self.controller.create,
req, body=body)
@ddt.data({'source_volid': 1},
{'source_volid': []},
@ -619,8 +690,8 @@ class VolumeApiTest(test.TestCase):
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
# Raise 400 for resource requested with invalid uuids.
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
req, body)
self.assertRaises(exception.ValidationError, self.controller.create,
req, body=body)
@ddt.data(mv.get_prior_version(mv.RESOURCE_FILTER), mv.RESOURCE_FILTER,
mv.LIKE_FILTER)