V3 json schema validation: generic volume groups

This patch adds jsonschema validation for below volume groups API's
* POST /v3/{project_id}/groups (create)
* PUT  /v3/{project_id}/groups/{group_id} (update)
* POST /v3/{project_id}/groups/action (create from source)
* POST /v3/{project_id}/groups/{group_id}/action (delete)
* POST /v3/{project_id}/groups/{group_id}/action (reset status)
* POST /v3/{project_id}/groups/{group_id}/action (failover replication)
* POST /v3/{project_id}/groups/{group_id}/action (enable replication)
* POST /v3/{project_id}/groups/{group_id}/action (disable replication)
* POST /v3/{project_id}/groups/{group_id}/action (list replication)

Change-Id: Ie91a52cc7f0245e5ecb3a9382691d78f5f92aa4f
Partial-Implements: bp json-schema-validation
This commit is contained in:
pooja jadhav 2017-11-10 17:08:38 +05:30 committed by Pooja Jadhav
parent 0b934710ae
commit 57983ba67c
5 changed files with 351 additions and 146 deletions

View File

@ -0,0 +1,173 @@
# 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 Generic Volume Groups API.
"""
from cinder.api.validation import parameter_types
create = {
'type': 'object',
'properties': {
'group': {
'type': 'object',
'properties': {
'description': parameter_types.description,
'group_type': {
'type': 'string', 'format': 'group_type'
},
'name': parameter_types.name_allow_zero_min_length,
'volume_types': {
'type': 'array', 'minItems': 1,
'items': {
'type': 'string', 'maxLength': 255,
},
'uniqueItems': True
},
'availability_zone': {
'type': ['string', 'null'], 'format': 'availability_zone'
},
},
'required': ['group_type', 'volume_types'],
'additionalProperties': False,
},
},
'required': ['group'],
'additionalProperties': False,
}
create_from_source = {
'type': 'object',
'properties': {
'create-from-src': {
'type': 'object',
'properties': {
'description': parameter_types.description,
'name': parameter_types.name_allow_zero_min_length,
'source_group_id': parameter_types.uuid,
'group_snapshot_id': parameter_types.uuid,
},
'oneOf': [
{'required': ['group_snapshot_id']},
{'required': ['source_group_id']}
],
'additionalProperties': False,
},
},
'required': ['create-from-src'],
'additionalProperties': False,
}
delete = {
'type': 'object',
'properties': {
'delete': {
'type': 'object',
'properties': {
'delete-volumes': parameter_types.boolean,
},
'additionalProperties': False,
},
},
'required': ['delete'],
'additionalProperties': False,
}
reset_status = {
'type': 'object',
'properties': {
'reset_status': {
'type': 'object',
'properties': {
'status': {
'type': 'string', 'format': 'group_status'
},
},
'required': ['status'],
'additionalProperties': False,
},
},
'required': ['reset_status'],
'additionalProperties': False,
}
update = {
'type': 'object',
'properties': {
'group': {
'type': 'object',
'properties': {
'description': parameter_types.description,
'name': parameter_types.name_allow_zero_min_length,
'add_volumes': parameter_types.description,
'remove_volumes': parameter_types.description,
},
'anyOf': [
{'required': ['name']},
{'required': ['description']},
{'required': ['add_volumes']},
{'required': ['remove_volumes']},
],
'additionalProperties': False,
},
},
'required': ['group'],
'additionalProperties': False,
}
failover_replication = {
'type': 'object',
'properties': {
'failover_replication': {
'type': 'object',
'properties': {
'allow_attached_volume': parameter_types.boolean,
'secondary_backend_id': parameter_types.nullable_string,
},
'additionalProperties': False,
},
},
'required': ['failover_replication'],
'additionalProperties': False,
}
list_replication = {
'type': 'object',
'properties': {
'list_replication_targets': {'type': 'object'}
},
'required': ['list_replication_targets'],
'additionalProperties': False,
}
enable_replication = {
'type': 'object',
'properties': {
'enable_replication': {'type': 'object'}
},
'required': ['enable_replication'],
'additionalProperties': False,
}
disable_replication = {
'type': 'object',
'properties': {
'disable_replication': {'type': 'object'}
},
'required': ['disable_replication'],
'additionalProperties': False,
}

View File

@ -24,7 +24,9 @@ from webob import exc
from cinder.api import common from cinder.api import common
from cinder.api import microversions as mv from cinder.api import microversions as mv
from cinder.api.openstack import wsgi from cinder.api.openstack import wsgi
from cinder.api.schemas import groups as group
from cinder.api.v3.views import groups as views_groups from cinder.api.v3.views import groups as views_groups
from cinder.api import validation
from cinder import exception from cinder import exception
from cinder import group as group_api from cinder import group as group_api
from cinder.i18n import _ from cinder.i18n import _
@ -67,6 +69,7 @@ class GroupsController(wsgi.Controller):
@wsgi.Controller.api_version(mv.GROUP_VOLUME_RESET_STATUS) @wsgi.Controller.api_version(mv.GROUP_VOLUME_RESET_STATUS)
@wsgi.action("reset_status") @wsgi.action("reset_status")
@validation.schema(group.reset_status)
def reset_status(self, req, id, body): def reset_status(self, req, id, body):
return self._reset_status(req, id, body) return self._reset_status(req, id, body)
@ -74,10 +77,7 @@ class GroupsController(wsgi.Controller):
"""Reset status on generic group.""" """Reset status on generic group."""
context = req.environ['cinder.context'] context = req.environ['cinder.context']
try: status = body['reset_status']['status'].lower()
status = body['reset_status']['status'].lower()
except (TypeError, KeyError):
raise exc.HTTPBadRequest(explanation=_("Must specify 'status'"))
LOG.debug("Updating group '%(id)s' with " LOG.debug("Updating group '%(id)s' with "
"'%(update)s'", {'id': id, "'%(update)s'", {'id': id,
@ -108,6 +108,7 @@ class GroupsController(wsgi.Controller):
@wsgi.Controller.api_version(mv.GROUP_VOLUME) @wsgi.Controller.api_version(mv.GROUP_VOLUME)
@wsgi.action("delete") @wsgi.action("delete")
@validation.schema(group.delete)
def delete_group(self, req, id, body): def delete_group(self, req, id, body):
return self._delete(req, id, body) return self._delete(req, id, body)
@ -115,19 +116,9 @@ class GroupsController(wsgi.Controller):
"""Delete a group.""" """Delete a group."""
LOG.debug('delete called for group %s', id) LOG.debug('delete called for group %s', id)
context = req.environ['cinder.context'] context = req.environ['cinder.context']
del_vol = False grp_body = body['delete']
if body: del_vol = strutils.bool_from_string(grp_body.get(
self.assert_valid_body(body, 'delete') 'delete-volumes', False))
grp_body = body['delete']
try:
del_vol = strutils.bool_from_string(
grp_body.get('delete-volumes', False),
strict=True)
except ValueError:
msg = (_("Invalid value '%s' for delete-volumes flag.")
% del_vol)
raise exc.HTTPBadRequest(explanation=msg)
LOG.info('Delete group with id: %s', id, LOG.info('Delete group with id: %s', id,
context=context) context=context)
@ -193,31 +184,25 @@ class GroupsController(wsgi.Controller):
@wsgi.Controller.api_version(mv.GROUP_VOLUME) @wsgi.Controller.api_version(mv.GROUP_VOLUME)
@wsgi.response(http_client.ACCEPTED) @wsgi.response(http_client.ACCEPTED)
@validation.schema(group.create)
def create(self, req, body): def create(self, req, body):
"""Create a new group.""" """Create a new group."""
LOG.debug('Creating new group %s', body) LOG.debug('Creating new group %s', body)
self.assert_valid_body(body, 'group')
context = req.environ['cinder.context'] context = req.environ['cinder.context']
group = body['group'] group = body['group']
self.validate_name_and_description(group)
name = group.get('name') name = group.get('name')
description = group.get('description') description = group.get('description')
group_type = group.get('group_type') if name:
if not group_type: name = name.strip()
msg = _("group_type must be provided to create " if description:
"group %(name)s.") % {'name': name} description = description.strip()
raise exc.HTTPBadRequest(explanation=msg) group_type = group['group_type']
if not uuidutils.is_uuid_like(group_type): if not uuidutils.is_uuid_like(group_type):
req_group_type = group_types.get_group_type_by_name(context, req_group_type = group_types.get_group_type_by_name(context,
group_type) group_type)
group_type = req_group_type['id'] group_type = req_group_type['id']
self._check_default_cgsnapshot_type(group_type) self._check_default_cgsnapshot_type(group_type)
volume_types = group.get('volume_types') volume_types = group['volume_types']
if not volume_types:
msg = _("volume_types must be provided to create "
"group %(name)s.") % {'name': name}
raise exc.HTTPBadRequest(explanation=msg)
availability_zone = group.get('availability_zone') availability_zone = group.get('availability_zone')
LOG.info("Creating group %(name)s.", LOG.info("Creating group %(name)s.",
@ -240,6 +225,7 @@ class GroupsController(wsgi.Controller):
@wsgi.Controller.api_version(mv.GROUP_SNAPSHOTS) @wsgi.Controller.api_version(mv.GROUP_SNAPSHOTS)
@wsgi.action("create-from-src") @wsgi.action("create-from-src")
@wsgi.response(http_client.ACCEPTED) @wsgi.response(http_client.ACCEPTED)
@validation.schema(group.create_from_source)
def create_from_src(self, req, body): def create_from_src(self, req, body):
"""Create a new group from a source. """Create a new group from a source.
@ -248,26 +234,17 @@ class GroupsController(wsgi.Controller):
"create" API above. "create" API above.
""" """
LOG.debug('Creating new group %s.', body) LOG.debug('Creating new group %s.', body)
self.assert_valid_body(body, 'create-from-src')
context = req.environ['cinder.context'] context = req.environ['cinder.context']
group = body['create-from-src'] group = body['create-from-src']
self.validate_name_and_description(group) name = group.get('name')
name = group.get('name', None) description = group.get('description')
description = group.get('description', None) if name:
name = name.strip()
if description:
description = description.strip()
group_snapshot_id = group.get('group_snapshot_id', None) group_snapshot_id = group.get('group_snapshot_id', None)
source_group_id = group.get('source_group_id', None) source_group_id = group.get('source_group_id', None)
if not group_snapshot_id and not source_group_id:
msg = (_("Either 'group_snapshot_id' or 'source_group_id' must be "
"provided to create group %(name)s from source.")
% {'name': name})
raise exc.HTTPBadRequest(explanation=msg)
if group_snapshot_id and source_group_id:
msg = _("Cannot provide both 'group_snapshot_id' and "
"'source_group_id' to create group %(name)s from "
"source.") % {'name': name}
raise exc.HTTPBadRequest(explanation=msg)
group_type_id = None group_type_id = None
if group_snapshot_id: if group_snapshot_id:
@ -303,6 +280,7 @@ class GroupsController(wsgi.Controller):
return retval return retval
@wsgi.Controller.api_version(mv.GROUP_VOLUME) @wsgi.Controller.api_version(mv.GROUP_VOLUME)
@validation.schema(group.update)
def update(self, req, id, body): def update(self, req, id, body):
"""Update the group. """Update the group.
@ -323,27 +301,18 @@ class GroupsController(wsgi.Controller):
""" """
LOG.debug('Update called for group %s.', id) LOG.debug('Update called for group %s.', id)
if not body:
msg = _("Missing request body.")
raise exc.HTTPBadRequest(explanation=msg)
self.assert_valid_body(body, 'group')
context = req.environ['cinder.context'] context = req.environ['cinder.context']
group = body.get('group') group = body['group']
self.validate_name_and_description(group)
name = group.get('name') name = group.get('name')
description = group.get('description') description = group.get('description')
if name:
name = name.strip()
if description:
description = description.strip()
add_volumes = group.get('add_volumes') add_volumes = group.get('add_volumes')
remove_volumes = group.get('remove_volumes') remove_volumes = group.get('remove_volumes')
# Allow name or description to be changed to an empty string ''.
if (name is None and description is None and not add_volumes
and not remove_volumes):
msg = _("Name, description, add_volumes, and remove_volumes "
"can not be all empty in the request body.")
raise exc.HTTPBadRequest(explanation=msg)
LOG.info("Updating group %(id)s with name %(name)s " LOG.info("Updating group %(id)s with name %(name)s "
"description: %(description)s add_volumes: " "description: %(description)s add_volumes: "
"%(add_volumes)s remove_volumes: %(remove_volumes)s.", "%(add_volumes)s remove_volumes: %(remove_volumes)s.",
@ -369,11 +338,10 @@ class GroupsController(wsgi.Controller):
@wsgi.Controller.api_version(mv.GROUP_REPLICATION) @wsgi.Controller.api_version(mv.GROUP_REPLICATION)
@wsgi.action("enable_replication") @wsgi.action("enable_replication")
@validation.schema(group.enable_replication)
def enable_replication(self, req, id, body): def enable_replication(self, req, id, body):
"""Enables replications for a group.""" """Enables replications for a group."""
context = req.environ['cinder.context'] context = req.environ['cinder.context']
if body:
self.assert_valid_body(body, 'enable_replication')
LOG.info('Enable replication group with id: %s.', id, LOG.info('Enable replication group with id: %s.', id,
context=context) context=context)
@ -390,11 +358,10 @@ class GroupsController(wsgi.Controller):
@wsgi.Controller.api_version(mv.GROUP_REPLICATION) @wsgi.Controller.api_version(mv.GROUP_REPLICATION)
@wsgi.action("disable_replication") @wsgi.action("disable_replication")
@validation.schema(group.disable_replication)
def disable_replication(self, req, id, body): def disable_replication(self, req, id, body):
"""Disables replications for a group.""" """Disables replications for a group."""
context = req.environ['cinder.context'] context = req.environ['cinder.context']
if body:
self.assert_valid_body(body, 'disable_replication')
LOG.info('Disable replication group with id: %s.', id, LOG.info('Disable replication group with id: %s.', id,
context=context) context=context)
@ -411,22 +378,16 @@ class GroupsController(wsgi.Controller):
@wsgi.Controller.api_version(mv.GROUP_REPLICATION) @wsgi.Controller.api_version(mv.GROUP_REPLICATION)
@wsgi.action("failover_replication") @wsgi.action("failover_replication")
@validation.schema(group.failover_replication)
def failover_replication(self, req, id, body): def failover_replication(self, req, id, body):
"""Fails over replications for a group.""" """Fails over replications for a group."""
context = req.environ['cinder.context'] context = req.environ['cinder.context']
if body:
self.assert_valid_body(body, 'failover_replication')
grp_body = body['failover_replication'] grp_body = body['failover_replication']
try:
allow_attached = strutils.bool_from_string( allow_attached = strutils.bool_from_string(
grp_body.get('allow_attached_volume', False), grp_body.get('allow_attached_volume', False))
strict=True) secondary_backend_id = grp_body.get('secondary_backend_id')
except ValueError:
msg = (_("Invalid value '%s' for allow_attached_volume flag.")
% grp_body)
raise exc.HTTPBadRequest(explanation=msg)
secondary_backend_id = grp_body.get('secondary_backend_id')
LOG.info('Failover replication group with id: %s.', id, LOG.info('Failover replication group with id: %s.', id,
context=context) context=context)
@ -444,11 +405,10 @@ class GroupsController(wsgi.Controller):
@wsgi.Controller.api_version(mv.GROUP_REPLICATION) @wsgi.Controller.api_version(mv.GROUP_REPLICATION)
@wsgi.action("list_replication_targets") @wsgi.action("list_replication_targets")
@validation.schema(group.list_replication)
def list_replication_targets(self, req, id, body): def list_replication_targets(self, req, id, body):
"""List replication targets for a group.""" """List replication targets for a group."""
context = req.environ['cinder.context'] context = req.environ['cinder.context']
if body:
self.assert_valid_body(body, 'list_replication_targets')
LOG.info('List replication targets for group with id: %s.', id, LOG.info('List replication targets for group with id: %s.', id,
context=context) context=context)

View File

@ -253,6 +253,42 @@ def _validate_quota_class_set(instance):
return True return True
@jsonschema.FormatChecker.cls_checks(
'group_status', webob.exc.HTTPBadRequest)
def _validate_group_status(param_value):
if param_value is None:
msg = _("The 'status' can not be None.")
raise webob.exc.HTTPBadRequest(explanation=msg)
if len(param_value.strip()) == 0:
msg = _("The 'status' can not be empty.")
raise exception.InvalidGroupStatus(reason=msg)
if param_value.lower() not in c_fields.GroupSnapshotStatus.ALL:
msg = _("Group status: %(status)s is invalid, valid status "
"are: %(valid)s.") % {'status': param_value,
'valid': c_fields.GroupStatus.ALL}
raise exception.InvalidGroupStatus(reason=msg)
return True
@jsonschema.FormatChecker.cls_checks('availability_zone')
def _validate_availability_zone(param_value):
if param_value is None:
return True
_validate_string_length(param_value, "availability_zone",
mandatory=True, min_length=1,
max_length=255, remove_whitespaces=True)
return True
@jsonschema.FormatChecker.cls_checks(
'group_type', (webob.exc.HTTPBadRequest, exception.InvalidInput))
def _validate_group_type(param_value):
_validate_string_length(param_value, 'group_type',
mandatory=True, min_length=1, max_length=255,
remove_whitespaces=True)
return True
class FormatChecker(jsonschema.FormatChecker): class FormatChecker(jsonschema.FormatChecker):
"""A FormatChecker can output the message from cause exception """A FormatChecker can output the message from cause exception

View File

@ -827,11 +827,6 @@ class API(base.Base):
def reset_status(self, context, group, status): def reset_status(self, context, group, status):
"""Reset status of generic group""" """Reset status of generic group"""
context.authorize(gp_action_policy.RESET_STATUS, target_obj=group) context.authorize(gp_action_policy.RESET_STATUS, target_obj=group)
if status not in c_fields.GroupStatus.ALL:
msg = _("Group status: %(status)s is invalid, valid status "
"are: %(valid)s.") % {'status': status,
'valid': c_fields.GroupStatus.ALL}
raise exception.InvalidGroupStatus(reason=msg)
field = {'updated_at': timeutils.utcnow(), field = {'updated_at': timeutils.utcnow(),
'status': status} 'status': status}
group.update(field) group.update(field)

View File

@ -459,9 +459,7 @@ class GroupsAPITestCase(test.TestCase):
index += 1 index += 1
@ddt.data(False, True) @ddt.data(False, True)
@mock.patch( def test_create_group_json(self, use_group_type_name):
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
def test_create_group_json(self, use_group_type_name, mock_validate):
# Create volume types and group type # Create volume types and group type
vol_type = 'test' vol_type = 'test'
vol_type_id = db.volume_type_create( vol_type_id = db.volume_type_create(
@ -480,11 +478,10 @@ class GroupsAPITestCase(test.TestCase):
"Group 1", }} "Group 1", }}
req = fakes.HTTPRequest.blank('/v3/%s/groups' % fake.PROJECT_ID, req = fakes.HTTPRequest.blank('/v3/%s/groups' % fake.PROJECT_ID,
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
res_dict = self.controller.create(req, body) res_dict = self.controller.create(req, body=body)
self.assertEqual(1, len(res_dict)) self.assertEqual(1, len(res_dict))
self.assertIn('id', res_dict['group']) self.assertIn('id', res_dict['group'])
self.assertTrue(mock_validate.called)
group_id = res_dict['group']['id'] group_id = res_dict['group']['id']
objects.Group.get_by_id(self.ctxt, group_id) objects.Group.get_by_id(self.ctxt, group_id)
@ -493,8 +490,31 @@ class GroupsAPITestCase(test.TestCase):
# omit body from the request # omit body from the request
req = fakes.HTTPRequest.blank('/v3/%s/groups' % fake.PROJECT_ID, req = fakes.HTTPRequest.blank('/v3/%s/groups' % fake.PROJECT_ID,
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, self.assertRaises(exception.ValidationError, self.controller.create,
req, None) req, body=None)
@ddt.data(("", webob.exc.HTTPBadRequest),
(" ", exception.InvalidInput),
("a" * 256, exception.InvalidInput))
@ddt.unpack
def test_create_group_with_invalid_availability_zone(
self, az_name, exceptions):
vol_type = 'test'
vol_type_id = db.volume_type_create(
self.ctxt,
{'name': vol_type, 'extra_specs': {}}).get('id')
grp_type_name = 'test_grp_type'
grp_type = db.group_type_create(
self.ctxt,
{'name': grp_type_name, 'group_specs': {}}).get('id')
body = {"group": {"name": "group1",
"volume_types": [vol_type_id],
"group_type": grp_type,
"availability_zone": az_name}}
req = fakes.HTTPRequest.blank('/v3/%s/groups' % fake.PROJECT_ID,
version=mv.GROUP_VOLUME)
self.assertRaises(exceptions, self.controller.create,
req, body=body)
def test_delete_group_available(self): def test_delete_group_available(self):
self.group1.status = fields.GroupStatus.AVAILABLE self.group1.status = fields.GroupStatus.AVAILABLE
@ -504,7 +524,7 @@ class GroupsAPITestCase(test.TestCase):
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
body = {"delete": {"delete-volumes": False}} body = {"delete": {"delete-volumes": False}}
res_dict = self.controller.delete_group( res_dict = self.controller.delete_group(
req, self.group1.id, body) req, self.group1.id, body=body)
group = objects.Group.get_by_id( group = objects.Group.get_by_id(
self.ctxt, self.group1.id) self.ctxt, self.group1.id)
@ -519,7 +539,7 @@ class GroupsAPITestCase(test.TestCase):
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
body = {"delete": {"delete-volumes": False}} body = {"delete": {"delete-volumes": False}}
res_dict = self.controller.delete_group( res_dict = self.controller.delete_group(
req, self.group1.id, body) req, self.group1.id, body=body)
group = objects.Group.get_by_id( group = objects.Group.get_by_id(
self.ctxt, self.group1.id) self.ctxt, self.group1.id)
@ -535,7 +555,7 @@ class GroupsAPITestCase(test.TestCase):
body = {"delete": {"delete-volumes": False}} body = {"delete": {"delete-volumes": False}}
self.assertRaises(exception.GroupNotFound, self.assertRaises(exception.GroupNotFound,
self.controller.delete_group, self.controller.delete_group,
req, fake.WILL_NOT_BE_FOUND_ID, body) req, fake.WILL_NOT_BE_FOUND_ID, body=body)
def test_delete_group_with_invalid_group(self): def test_delete_group_with_invalid_group(self):
req = fakes.HTTPRequest.blank('/v3/%s/groups/%s/action' % req = fakes.HTTPRequest.blank('/v3/%s/groups/%s/action' %
@ -545,7 +565,7 @@ class GroupsAPITestCase(test.TestCase):
body = {"delete": {"delete-volumes": False}} body = {"delete": {"delete-volumes": False}}
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.delete_group, self.controller.delete_group,
req, self.group1.id, body) req, self.group1.id, body=body)
def test_delete_group_invalid_delete_volumes(self): def test_delete_group_invalid_delete_volumes(self):
req = fakes.HTTPRequest.blank('/v3/%s/groups/%s/action' % req = fakes.HTTPRequest.blank('/v3/%s/groups/%s/action' %
@ -554,7 +574,7 @@ class GroupsAPITestCase(test.TestCase):
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
body = {"delete": {"delete-volumes": True}} body = {"delete": {"delete-volumes": True}}
res_dict = self.controller.delete_group( res_dict = self.controller.delete_group(
req, self.group1.id, body) req, self.group1.id, body=body)
group = objects.Group.get_by_id( group = objects.Group.get_by_id(
self.ctxt, self.group1.id) self.ctxt, self.group1.id)
@ -571,7 +591,7 @@ class GroupsAPITestCase(test.TestCase):
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
body = {"delete": {"delete-volumes": True}} body = {"delete": {"delete-volumes": True}}
res_dict = self.controller.delete_group( res_dict = self.controller.delete_group(
req, self.group1.id, body) req, self.group1.id, body=body)
self.assertEqual(http_client.ACCEPTED, res_dict.status_int) self.assertEqual(http_client.ACCEPTED, res_dict.status_int)
group = objects.Group.get_by_id( group = objects.Group.get_by_id(
@ -626,7 +646,7 @@ class GroupsAPITestCase(test.TestCase):
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
ex = self.assertRaises(exception.GroupLimitExceeded, ex = self.assertRaises(exception.GroupLimitExceeded,
self.controller.create, self.controller.create,
req, body) req, body=body)
self.assertEqual(http_client.REQUEST_ENTITY_TOO_LARGE, ex.code) self.assertEqual(http_client.REQUEST_ENTITY_TOO_LARGE, ex.code)
def test_delete_group_with_invalid_body(self): def test_delete_group_with_invalid_body(self):
@ -636,9 +656,9 @@ class GroupsAPITestCase(test.TestCase):
(fake.PROJECT_ID, self.group1.id), (fake.PROJECT_ID, self.group1.id),
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
body = {"invalid_request_element": {"delete-volumes": False}} body = {"invalid_request_element": {"delete-volumes": False}}
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(exception.ValidationError,
self.controller.delete_group, self.controller.delete_group,
req, self.group1.id, body) req, self.group1.id, body=body)
def test_delete_group_with_invalid_delete_volumes_value_in_body(self): def test_delete_group_with_invalid_delete_volumes_value_in_body(self):
self.group1.status = fields.GroupStatus.AVAILABLE self.group1.status = fields.GroupStatus.AVAILABLE
@ -647,9 +667,9 @@ class GroupsAPITestCase(test.TestCase):
(fake.PROJECT_ID, self.group1.id), (fake.PROJECT_ID, self.group1.id),
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
body = {"delete": {"delete-volumes": "abcd"}} body = {"delete": {"delete-volumes": "abcd"}}
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(exception.ValidationError,
self.controller.delete_group, self.controller.delete_group,
req, self.group1.id, body) req, self.group1.id, body=body)
def test_delete_group_with_empty_delete_volumes_value_in_body(self): def test_delete_group_with_empty_delete_volumes_value_in_body(self):
self.group1.status = fields.GroupStatus.AVAILABLE self.group1.status = fields.GroupStatus.AVAILABLE
@ -658,9 +678,9 @@ class GroupsAPITestCase(test.TestCase):
(fake.PROJECT_ID, self.group1.id), (fake.PROJECT_ID, self.group1.id),
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
body = {"delete": {"delete-volumes": ""}} body = {"delete": {"delete-volumes": ""}}
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(exception.ValidationError,
self.controller.delete_group, self.controller.delete_group,
req, self.group1.id, body) req, self.group1.id, body=body)
def test_delete_group_with_group_snapshot(self): def test_delete_group_with_group_snapshot(self):
self.group1.status = fields.GroupStatus.AVAILABLE self.group1.status = fields.GroupStatus.AVAILABLE
@ -673,12 +693,12 @@ class GroupsAPITestCase(test.TestCase):
body = {"delete": {"delete-volumes": True}} body = {"delete": {"delete-volumes": True}}
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.delete_group, self.controller.delete_group,
req, self.group1.id, body) req, self.group1.id, body=body)
g_snapshot.destroy() g_snapshot.destroy()
res_dict = self.controller.delete_group( res_dict = self.controller.delete_group(
req, self.group1.id, body) req, self.group1.id, body=body)
group = objects.Group.get_by_id( group = objects.Group.get_by_id(
self.ctxt, self.group1.id) self.ctxt, self.group1.id)
@ -694,7 +714,7 @@ class GroupsAPITestCase(test.TestCase):
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
body = {"delete": {"delete-volumes": True}} body = {"delete": {"delete-volumes": True}}
res_dict = self.controller.delete_group( res_dict = self.controller.delete_group(
req, self.group1.id, body) req, self.group1.id, body=body)
group = objects.Group.get_by_id( group = objects.Group.get_by_id(
self.ctxt, self.group1.id) self.ctxt, self.group1.id)
@ -714,7 +734,7 @@ class GroupsAPITestCase(test.TestCase):
body = {"delete": {"delete-volumes": True}} body = {"delete": {"delete-volumes": True}}
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.delete_group, self.controller.delete_group,
req, self.group1.id, body) req, self.group1.id, body=body)
vol.destroy() vol.destroy()
@ -729,7 +749,7 @@ class GroupsAPITestCase(test.TestCase):
body = {"delete": {"delete-volumes": True}} body = {"delete": {"delete-volumes": True}}
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.delete_group, self.controller.delete_group,
req, self.group1.id, body) req, self.group1.id, body=body)
vol.destroy() vol.destroy()
@ -745,7 +765,7 @@ class GroupsAPITestCase(test.TestCase):
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
body = {"delete": {"delete-volumes": True}} body = {"delete": {"delete-volumes": True}}
res_dict = self.controller.delete_group( res_dict = self.controller.delete_group(
req, self.group1.id, body) req, self.group1.id, body=body)
group = objects.Group.get_by_id( group = objects.Group.get_by_id(
self.ctxt, self.group1.id) self.ctxt, self.group1.id)
@ -762,9 +782,21 @@ class GroupsAPITestCase(test.TestCase):
"Group 1", }} "Group 1", }}
req = fakes.HTTPRequest.blank('/v3/%s/groups' % fake.PROJECT_ID, req = fakes.HTTPRequest.blank('/v3/%s/groups' % fake.PROJECT_ID,
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(exception.ValidationError,
self.controller.create, self.controller.create,
req, body) req, body=body)
@ddt.data(None, "", " ", "a" * 256)
def test_create_group_failed_invalid_group_type(self, group_type):
name = 'group1'
body = {"group": {"volume_types": [fake.VOLUME_TYPE_ID],
"name": name,
"description": "Group 1",
"group_type": group_type}}
req = fakes.HTTPRequest.blank('/v3/%s/groups' % fake.PROJECT_ID,
version=mv.GROUP_VOLUME)
self.assertRaises(exception.ValidationError, self.controller.create,
req, body=body)
def test_create_group_failed_no_volume_types(self): def test_create_group_failed_no_volume_types(self):
name = 'group1' name = 'group1'
@ -774,13 +806,11 @@ class GroupsAPITestCase(test.TestCase):
"Group 1", }} "Group 1", }}
req = fakes.HTTPRequest.blank('/v3/%s/groups' % fake.PROJECT_ID, req = fakes.HTTPRequest.blank('/v3/%s/groups' % fake.PROJECT_ID,
version=mv.GROUP_VOLUME) version=mv.GROUP_VOLUME)
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(exception.ValidationError,
self.controller.create, self.controller.create,
req, body) req, body=body)
@mock.patch( def test_update_group_success(self):
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
def test_update_group_success(self, mock_validate):
volume_type_id = fake.VOLUME_TYPE_ID volume_type_id = fake.VOLUME_TYPE_ID
self.group1.status = fields.GroupStatus.AVAILABLE self.group1.status = fields.GroupStatus.AVAILABLE
self.group1.host = 'test_host' self.group1.host = 'test_host'
@ -834,12 +864,11 @@ class GroupsAPITestCase(test.TestCase):
"add_volumes": add_volumes, "add_volumes": add_volumes,
"remove_volumes": remove_volumes, }} "remove_volumes": remove_volumes, }}
res_dict = self.controller.update( res_dict = self.controller.update(
req, self.group1.id, body) req, self.group1.id, body=body)
group = objects.Group.get_by_id( group = objects.Group.get_by_id(
self.ctxt, self.group1.id) self.ctxt, self.group1.id)
self.assertEqual(http_client.ACCEPTED, res_dict.status_int) self.assertEqual(http_client.ACCEPTED, res_dict.status_int)
self.assertTrue(mock_validate.called)
self.assertEqual(fields.GroupStatus.UPDATING, self.assertEqual(fields.GroupStatus.UPDATING,
group.status) group.status)
@ -861,7 +890,7 @@ class GroupsAPITestCase(test.TestCase):
"remove_volumes": None, }} "remove_volumes": None, }}
res_dict = self.controller.update( res_dict = self.controller.update(
req, self.group1.id, body) req, self.group1.id, body=body)
self.assertEqual(http_client.ACCEPTED, res_dict.status_int) self.assertEqual(http_client.ACCEPTED, res_dict.status_int)
group = objects.Group.get_by_id(self.ctxt, self.group1.id) group = objects.Group.get_by_id(self.ctxt, self.group1.id)
@ -881,7 +910,7 @@ class GroupsAPITestCase(test.TestCase):
self.assertRaises(exception.InvalidVolume, self.assertRaises(exception.InvalidVolume,
self.controller.update, self.controller.update,
req, self.group1.id, body) req, self.group1.id, body=body)
def test_update_group_remove_volume_not_found(self): def test_update_group_remove_volume_not_found(self):
self.group1.status = fields.GroupStatus.AVAILABLE self.group1.status = fields.GroupStatus.AVAILABLE
@ -896,7 +925,7 @@ class GroupsAPITestCase(test.TestCase):
self.assertRaises(exception.InvalidVolume, self.assertRaises(exception.InvalidVolume,
self.controller.update, self.controller.update,
req, self.group1.id, body) req, self.group1.id, body=body)
def test_update_group_empty_parameters(self): def test_update_group_empty_parameters(self):
self.group1.status = fields.GroupStatus.AVAILABLE self.group1.status = fields.GroupStatus.AVAILABLE
@ -911,7 +940,7 @@ class GroupsAPITestCase(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update, self.controller.update,
req, self.group1.id, body) req, self.group1.id, body=body)
def test_update_group_add_volume_invalid_state(self): def test_update_group_add_volume_invalid_state(self):
self.group1.status = fields.GroupStatus.AVAILABLE self.group1.status = fields.GroupStatus.AVAILABLE
@ -931,7 +960,7 @@ class GroupsAPITestCase(test.TestCase):
self.assertRaises(exception.InvalidVolume, self.assertRaises(exception.InvalidVolume,
self.controller.update, self.controller.update,
req, self.group1.id, body) req, self.group1.id, body=body)
add_volume.destroy() add_volume.destroy()
@ -953,7 +982,7 @@ class GroupsAPITestCase(test.TestCase):
self.assertRaises(exception.InvalidVolume, self.assertRaises(exception.InvalidVolume,
self.controller.update, self.controller.update,
req, self.group1.id, body) req, self.group1.id, body=body)
add_volume.destroy() add_volume.destroy()
@ -974,7 +1003,7 @@ class GroupsAPITestCase(test.TestCase):
self.assertRaises(exception.InvalidVolume, self.assertRaises(exception.InvalidVolume,
self.controller.update, self.controller.update,
req, self.group1.id, body) req, self.group1.id, body=body)
add_volume.destroy() add_volume.destroy()
@ -996,7 +1025,7 @@ class GroupsAPITestCase(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update, self.controller.update,
req, self.group1.id, body) req, self.group1.id, body=body)
vol = objects.Volume.get_by_id(self.ctxt, add_volume.id) vol = objects.Volume.get_by_id(self.ctxt, add_volume.id)
self.assertEqual(add_volume.status, vol.status) self.assertEqual(add_volume.status, vol.status)
@ -1013,7 +1042,10 @@ class GroupsAPITestCase(test.TestCase):
exception.GroupNotFound), exception.GroupNotFound),
(mv.GROUP_VOLUME_RESET_STATUS, None, (mv.GROUP_VOLUME_RESET_STATUS, None,
'invalid_test_status', 'invalid_test_status',
webob.exc.HTTPBadRequest), exception.InvalidGroupStatus),
(mv.GROUP_VOLUME_RESET_STATUS, 'fake_group_001',
None,
exception.ValidationError)
) )
@ddt.unpack @ddt.unpack
def test_reset_group_status_illegal(self, version, group_id, def test_reset_group_status_illegal(self, version, group_id,
@ -1027,7 +1059,17 @@ class GroupsAPITestCase(test.TestCase):
}} }}
self.assertRaises(exceptions, self.assertRaises(exceptions,
self.controller.reset_status, self.controller.reset_status,
req, g_id, body) req, g_id, body=body)
def test_reset_group_without_status(self):
g_id = self.group2.id
req = fakes.HTTPRequest.blank('/v3/%s/groups/%s/action' %
(fake.PROJECT_ID, g_id),
version=mv.GROUP_VOLUME_RESET_STATUS)
body = {"reset_status": {}}
self.assertRaises(exception.ValidationError,
self.controller.reset_status,
req, g_id, body=body)
def test_reset_group_status(self): def test_reset_group_status(self):
req = fakes.HTTPRequest.blank('/v3/%s/groups/%s/action' % req = fakes.HTTPRequest.blank('/v3/%s/groups/%s/action' %
@ -1037,18 +1079,15 @@ class GroupsAPITestCase(test.TestCase):
"status": fields.GroupStatus.AVAILABLE "status": fields.GroupStatus.AVAILABLE
}} }}
response = self.controller.reset_status(req, response = self.controller.reset_status(req,
self.group2.id, body) self.group2.id, body=body)
group = objects.Group.get_by_id(self.ctxt, self.group2.id) group = objects.Group.get_by_id(self.ctxt, self.group2.id)
self.assertEqual(http_client.ACCEPTED, response.status_int) self.assertEqual(http_client.ACCEPTED, response.status_int)
self.assertEqual(fields.GroupStatus.AVAILABLE, group.status) self.assertEqual(fields.GroupStatus.AVAILABLE, group.status)
@ddt.data(True, False) @ddt.data(True, False)
@mock.patch(
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
@mock.patch('cinder.scheduler.rpcapi.SchedulerAPI.validate_host_capacity') @mock.patch('cinder.scheduler.rpcapi.SchedulerAPI.validate_host_capacity')
def test_create_group_from_src_snap(self, valid_host, mock_validate_host, def test_create_group_from_src_snap(self, valid_host, mock_validate_host):
mock_validate):
self.mock_object(volume_api.API, "create", v3_fakes.fake_volume_create) self.mock_object(volume_api.API, "create", v3_fakes.fake_volume_create)
group = utils.create_group(self.ctxt, group = utils.create_group(self.ctxt,
@ -1077,16 +1116,15 @@ class GroupsAPITestCase(test.TestCase):
fake.PROJECT_ID, fake.PROJECT_ID,
version=mv.GROUP_SNAPSHOTS) version=mv.GROUP_SNAPSHOTS)
if valid_host: if valid_host:
res_dict = self.controller.create_from_src(req, body) res_dict = self.controller.create_from_src(req, body=body)
self.assertIn('id', res_dict['group']) self.assertIn('id', res_dict['group'])
self.assertEqual(test_grp_name, res_dict['group']['name']) self.assertEqual(test_grp_name, res_dict['group']['name'])
self.assertTrue(mock_validate.called)
grp_ref = objects.Group.get_by_id( grp_ref = objects.Group.get_by_id(
self.ctxt.elevated(), res_dict['group']['id']) self.ctxt.elevated(), res_dict['group']['id'])
else: else:
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create_from_src, req, body) self.controller.create_from_src, req, body=body)
groups = objects.GroupList.get_all_by_project(self.ctxt, groups = objects.GroupList.get_all_by_project(self.ctxt,
fake.PROJECT_ID) fake.PROJECT_ID)
grp_ref = objects.Group.get_by_id( grp_ref = objects.Group.get_by_id(
@ -1119,7 +1157,7 @@ class GroupsAPITestCase(test.TestCase):
fake.PROJECT_ID, fake.PROJECT_ID,
version=mv.GROUP_SNAPSHOTS) version=mv.GROUP_SNAPSHOTS)
if host_valid: if host_valid:
res_dict = self.controller.create_from_src(req, body) res_dict = self.controller.create_from_src(req, body=body)
self.assertIn('id', res_dict['group']) self.assertIn('id', res_dict['group'])
self.assertEqual(test_grp_name, res_dict['group']['name']) self.assertEqual(test_grp_name, res_dict['group']['name'])
grp = objects.Group.get_by_id( grp = objects.Group.get_by_id(
@ -1129,7 +1167,7 @@ class GroupsAPITestCase(test.TestCase):
source_grp.destroy() source_grp.destroy()
else: else:
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create_from_src, req, body) self.controller.create_from_src, req, body=body)
groups = objects.GroupList.get_all_by_project(self.ctxt, groups = objects.GroupList.get_all_by_project(self.ctxt,
fake.PROJECT_ID) fake.PROJECT_ID)
grp = objects.Group.get_by_id( grp = objects.Group.get_by_id(
@ -1150,7 +1188,8 @@ class GroupsAPITestCase(test.TestCase):
self.group3.save() self.group3.save()
body = {"enable_replication": {}} body = {"enable_replication": {}}
response = self.controller.enable_replication(req, response = self.controller.enable_replication(req,
self.group3.id, body) self.group3.id,
body=body)
group = objects.Group.get_by_id(self.ctxt, self.group3.id) group = objects.Group.get_by_id(self.ctxt, self.group3.id)
self.assertEqual(202, response.status_int) self.assertEqual(202, response.status_int)
@ -1176,7 +1215,7 @@ class GroupsAPITestCase(test.TestCase):
body = {"enable_replication": {}} body = {"enable_replication": {}}
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.enable_replication, self.controller.enable_replication,
req, self.group3.id, body) req, self.group3.id, body=body)
@mock.patch('cinder.volume.utils.is_replicated_spec', @mock.patch('cinder.volume.utils.is_replicated_spec',
return_value=False) return_value=False)
@ -1192,7 +1231,7 @@ class GroupsAPITestCase(test.TestCase):
body = {"enable_replication": {}} body = {"enable_replication": {}}
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.enable_replication, self.controller.enable_replication,
req, self.group3.id, body) req, self.group3.id, body=body)
@mock.patch('cinder.volume.utils.is_replicated_spec', @mock.patch('cinder.volume.utils.is_replicated_spec',
return_value=True) return_value=True)
@ -1225,7 +1264,7 @@ class GroupsAPITestCase(test.TestCase):
body = {"enable_replication": {}} body = {"enable_replication": {}}
self.assertRaises(exceptions, self.assertRaises(exceptions,
self.controller.enable_replication, self.controller.enable_replication,
req, group_id, body) req, group_id, body=body)
@mock.patch('cinder.volume.utils.is_replicated_spec', @mock.patch('cinder.volume.utils.is_replicated_spec',
return_value=True) return_value=True)
@ -1240,7 +1279,8 @@ class GroupsAPITestCase(test.TestCase):
self.group3.save() self.group3.save()
body = {"disable_replication": {}} body = {"disable_replication": {}}
response = self.controller.disable_replication(req, response = self.controller.disable_replication(req,
self.group3.id, body) self.group3.id,
body=body)
group = objects.Group.get_by_id(self.ctxt, self.group3.id) group = objects.Group.get_by_id(self.ctxt, self.group3.id)
self.assertEqual(202, response.status_int) self.assertEqual(202, response.status_int)
@ -1288,7 +1328,7 @@ class GroupsAPITestCase(test.TestCase):
body = {"disable_replication": {}} body = {"disable_replication": {}}
self.assertRaises(exceptions, self.assertRaises(exceptions,
self.controller.disable_replication, self.controller.disable_replication,
req, group_id, body) req, group_id, body=body)
@mock.patch('cinder.volume.utils.is_replicated_spec', @mock.patch('cinder.volume.utils.is_replicated_spec',
return_value=True) return_value=True)
@ -1303,7 +1343,8 @@ class GroupsAPITestCase(test.TestCase):
self.group3.save() self.group3.save()
body = {"failover_replication": {}} body = {"failover_replication": {}}
response = self.controller.failover_replication(req, response = self.controller.failover_replication(req,
self.group3.id, body) self.group3.id,
body=body)
group = objects.Group.get_by_id(self.ctxt, self.group3.id) group = objects.Group.get_by_id(self.ctxt, self.group3.id)
self.assertEqual(202, response.status_int) self.assertEqual(202, response.status_int)
@ -1351,7 +1392,7 @@ class GroupsAPITestCase(test.TestCase):
body = {"failover_replication": {}} body = {"failover_replication": {}}
self.assertRaises(exceptions, self.assertRaises(exceptions,
self.controller.failover_replication, self.controller.failover_replication,
req, group_id, body) req, group_id, body=body)
@mock.patch('cinder.volume.utils.is_replicated_spec', @mock.patch('cinder.volume.utils.is_replicated_spec',
return_value=True) return_value=True)
@ -1373,7 +1414,7 @@ class GroupsAPITestCase(test.TestCase):
self.group3.save() self.group3.save()
body = {"list_replication_targets": {}} body = {"list_replication_targets": {}}
response = self.controller.list_replication_targets( response = self.controller.list_replication_targets(
req, self.group3.id, body) req, self.group3.id, body=body)
self.assertIn('replication_targets', response) self.assertIn('replication_targets', response)
self.assertEqual('lvm_backend_1', self.assertEqual('lvm_backend_1',