dict(new_consistencygroup.iteritems()))
return retval
+ @wsgi.serializers(xml=ConsistencyGroupTemplate)
+ def update(self, req, id, body):
+ """Update the consistency group.
+
+ Expected format of the input parameter 'body':
+ {
+ "consistencygroup":
+ {
+ "name": "my_cg",
+ "description": "My consistency group",
+ "add_volumes": "volume-uuid-1,volume-uuid-2,..."
+ "remove_volumes": "volume-uuid-8,volume-uuid-9,..."
+ }
+ }
+ """
+ LOG.debug('Update called for consistency group %s.', id)
+
+ if not body:
+ msg = _("Missing request body.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ if not self.is_valid_body(body, 'consistencygroup'):
+ msg = _("Incorrect request body format.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ context = req.environ['cinder.context']
+
+ consistencygroup = body.get('consistencygroup', None)
+ name = consistencygroup.get('name', None)
+ description = consistencygroup.get('description', None)
+ add_volumes = consistencygroup.get('add_volumes', None)
+ remove_volumes = consistencygroup.get('remove_volumes', None)
+
+ if (not name and not description 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(_LI("Updating consistency group %(id)s with name %(name)s "
+ "description: %(description)s add_volumes: "
+ "%(add_volumes)s remove_volumes: %(remove_volumes)s."),
+ {'id': id, 'name': name,
+ 'description': description,
+ 'add_volumes': add_volumes,
+ 'remove_volumes': remove_volumes},
+ context=context)
+
+ try:
+ group = self.consistencygroup_api.get(context, id)
+ self.consistencygroup_api.update(
+ context, group, name, description,
+ add_volumes, remove_volumes)
+ except exception.ConsistencyGroupNotFound:
+ msg = _("Consistency group %s could not be found.") % id
+ raise exc.HTTPNotFound(explanation=msg)
+ except exception.InvalidConsistencyGroup as error:
+ raise exc.HTTPBadRequest(explanation=error.msg)
+
+ return webob.Response(status_int=202)
+
class Consistencygroups(extensions.ExtensionDescriptor):
"""consistency groups support."""
res = extensions.ResourceExtension(
Consistencygroups.alias, ConsistencyGroupsController(),
collection_actions={'detail': 'GET'},
- member_actions={'delete': 'POST'})
+ member_actions={'delete': 'POST', 'update': 'PUT'})
resources.append(res)
return resources
from cinder.scheduler import rpcapi as scheduler_rpcapi
from cinder.volume import api as volume_api
from cinder.volume import rpcapi as volume_rpcapi
+from cinder.volume import utils as vol_utils
from cinder.volume import volume_types
LOG = logging.getLogger(__name__)
CGQUOTAS = quota.CGQUOTAS
+VALID_REMOVE_VOL_FROM_CG_STATUS = ('available', 'in-use',)
def wrap_check_policy(func):
self.volume_rpcapi.delete_consistencygroup(context, group)
- @wrap_check_policy
- def update(self, context, group, fields):
+ def update(self, context, group, name, description,
+ add_volumes, remove_volumes):
+ """Update consistency group."""
+ if group['status'] not in ["available"]:
+ msg = _("Consistency group status must be available, "
+ "but current status is: %s.") % group['status']
+ raise exception.InvalidConsistencyGroup(reason=msg)
+
+ add_volumes_list = []
+ remove_volumes_list = []
+ if add_volumes:
+ add_volumes = add_volumes.strip(',')
+ add_volumes_list = add_volumes.split(',')
+ if remove_volumes:
+ remove_volumes = remove_volumes.strip(',')
+ remove_volumes_list = remove_volumes.split(',')
+
+ invalid_uuids = []
+ for uuid in add_volumes_list:
+ if uuid in remove_volumes_list:
+ invalid_uuids.append(uuid)
+ if invalid_uuids:
+ msg = _("UUIDs %s are in both add and remove volume "
+ "list.") % invalid_uuids
+ raise exception.InvalidVolume(reason=msg)
+
+ volumes = self.db.volume_get_all_by_group(context, group['id'])
+
+ # Validate name.
+ if not name or name == group['name']:
+ name = None
+
+ # Validate description.
+ if not description or description == group['description']:
+ description = None
+
+ # Validate volumes in add_volumes and remove_volumes.
+ add_volumes_new = ""
+ remove_volumes_new = ""
+ if add_volumes_list:
+ add_volumes_new = self._validate_add_volumes(
+ context, volumes, add_volumes_list, group)
+ if remove_volumes_list:
+ remove_volumes_new = self._validate_remove_volumes(
+ volumes, remove_volumes_list, group)
+
+ if (not name and not description and not add_volumes_new and
+ not remove_volumes_new):
+ msg = (_("Cannot update consistency group %(group_id)s "
+ "because no valid name, description, add_volumes, "
+ "or remove_volumes were provided.") %
+ {'group_id': group['id']})
+ raise exception.InvalidConsistencyGroup(reason=msg)
+
+ now = timeutils.utcnow()
+ fields = {'updated_at': now}
+
+ # Update name and description in db now. No need to
+ # to send them over thru an RPC call.
+ if name:
+ fields['name'] = name
+ if description:
+ fields['description'] = description
+ if not add_volumes_new and not remove_volumes_new:
+ # Only update name or description. Set status to available.
+ fields['status'] = 'available'
+ else:
+ fields['status'] = 'updating'
+
self.db.consistencygroup_update(context, group['id'], fields)
+ # Do an RPC call only if the update request includes
+ # adding/removing volumes. add_volumes_new and remove_volumes_new
+ # are strings of volume UUIDs separated by commas with no spaces
+ # in between.
+ if add_volumes_new or remove_volumes_new:
+ self.volume_rpcapi.update_consistencygroup(
+ context, group,
+ add_volumes=add_volumes_new,
+ remove_volumes=remove_volumes_new)
+
+ def _validate_remove_volumes(self, volumes, remove_volumes_list, group):
+ # Validate volumes in remove_volumes.
+ remove_volumes_new = ""
+ for volume in volumes:
+ if volume['id'] in remove_volumes_list:
+ if volume['status'] not in VALID_REMOVE_VOL_FROM_CG_STATUS:
+ msg = (_("Cannot remove volume %(volume_id)s from "
+ "consistency group %(group_id)s because volume "
+ "is in an invalid state: %(status)s. Valid "
+ "states are: %(valid)s.") %
+ {'volume_id': volume['id'],
+ 'group_id': group['id'],
+ 'status': volume['status'],
+ 'valid': VALID_REMOVE_VOL_FROM_CG_STATUS})
+ raise exception.InvalidVolume(reason=msg)
+ # Volume currently in CG. It will be removed from CG.
+ if remove_volumes_new:
+ remove_volumes_new += ","
+ remove_volumes_new += volume['id']
+
+ for rem_vol in remove_volumes_list:
+ if rem_vol not in remove_volumes_new:
+ msg = (_("Cannot remove volume %(volume_id)s from "
+ "consistency group %(group_id)s because it "
+ "is not in the group.") %
+ {'volume_id': rem_vol,
+ 'group_id': group['id']})
+ raise exception.InvalidVolume(reason=msg)
+
+ return remove_volumes_new
+
+ def _validate_add_volumes(self, context, volumes, add_volumes_list, group):
+ add_volumes_new = ""
+ for volume in volumes:
+ if volume['id'] in add_volumes_list:
+ # Volume already in CG. Remove from add_volumes.
+ add_volumes_list.remove(volume['id'])
+
+ for add_vol in add_volumes_list:
+ try:
+ add_vol_ref = self.db.volume_get(context, add_vol)
+ except exception.VolumeNotFound:
+ msg = (_("Cannot add volume %(volume_id)s to consistency "
+ "group %(group_id)s because volume cannot be "
+ "found.") %
+ {'volume_id': add_vol,
+ 'group_id': group['id']})
+ raise exception.InvalidVolume(reason=msg)
+ if add_vol_ref:
+ add_vol_type_id = add_vol_ref.get('volume_type_id', None)
+ if not add_vol_type_id:
+ msg = (_("Cannot add volume %(volume_id)s to consistency "
+ "group %(group_id)s because it has no volume "
+ "type.") %
+ {'volume_id': add_vol_ref['id'],
+ 'group_id': group['id']})
+ raise exception.InvalidVolume(reason=msg)
+ if add_vol_type_id not in group['volume_type_id']:
+ msg = (_("Cannot add volume %(volume_id)s to consistency "
+ "group %(group_id)s because volume type "
+ "%(volume_type)s is not supported by the "
+ "group.") %
+ {'volume_id': add_vol_ref['id'],
+ 'group_id': group['id'],
+ 'volume_type': add_vol_type_id})
+ raise exception.InvalidVolume(reason=msg)
+ if (add_vol_ref['status'] not in
+ VALID_REMOVE_VOL_FROM_CG_STATUS):
+ msg = (_("Cannot add volume %(volume_id)s to consistency "
+ "group %(group_id)s because volume is in an "
+ "invalid state: %(status)s. Valid states are: "
+ "%(valid)s.") %
+ {'volume_id': add_vol_ref['id'],
+ 'group_id': group['id'],
+ 'status': add_vol_ref['status'],
+ 'valid': VALID_REMOVE_VOL_FROM_CG_STATUS})
+ raise exception.InvalidVolume(reason=msg)
+
+ # group['host'] and add_vol_ref['host'] are in this format:
+ # 'host@backend#pool'. Extract host (host@backend) before
+ # doing comparison.
+ vol_host = vol_utils.extract_host(add_vol_ref['host'])
+ group_host = vol_utils.extract_host(group['host'])
+ if group_host != vol_host:
+ raise exception.InvalidVolume(
+ reason=_("Volume is not local to this node."))
+
+ # Volume exists. It will be added to CG.
+ if add_volumes_new:
+ add_volumes_new += ","
+ add_volumes_new += add_vol_ref['id']
+
+ else:
+ msg = (_("Cannot add volume %(volume_id)s to consistency "
+ "group %(group_id)s because volume does not exist.") %
+ {'volume_id': add_vol_ref['id'],
+ 'group_id': group['id']})
+ raise exception.InvalidVolume(reason=msg)
+
+ return add_volumes_new
+
def get(self, context, group_id):
rv = self.db.consistencygroup_get(context, group_id)
group = dict(rv.iteritems())
return groups
- def get_group(self, context, group_id):
- check_policy(context, 'get_group')
- rv = self.db.consistencygroup_get(context, group_id)
- return dict(rv.iteritems())
-
def create_cgsnapshot(self, context,
group, name,
description):
return []
-@require_admin_context
+@require_context
def volume_get_all_by_group(context, group_id, filters=None):
"""Retrieves all volumes associated with the group_id.
from cinder.i18n import _
from cinder import test
from cinder.tests.api import fakes
+from cinder.tests import utils
class ConsistencyGroupsAPITestCase(test.TestCase):
msg = (_('volume_types must be provided to create '
'consistency group %s.') % name)
self.assertEqual(msg, res_dict['badRequest']['message'])
+
+ def test_update_consistencygroup_success(self):
+ volume_type_id = '123456'
+ ctxt = context.RequestContext('fake', 'fake')
+ consistencygroup_id = self._create_consistencygroup(status='available',
+ host='test_host')
+ remove_volume_id = utils.create_volume(
+ ctxt,
+ volume_type_id=volume_type_id,
+ consistencygroup_id=consistencygroup_id)['id']
+ remove_volume_id2 = utils.create_volume(
+ ctxt,
+ volume_type_id=volume_type_id,
+ consistencygroup_id=consistencygroup_id)['id']
+
+ self.assertEqual('available',
+ self._get_consistencygroup_attrib(consistencygroup_id,
+ 'status'))
+
+ cg_volumes = db.volume_get_all_by_group(ctxt.elevated(),
+ consistencygroup_id)
+ cg_vol_ids = [cg_vol['id'] for cg_vol in cg_volumes]
+ self.assertIn(remove_volume_id, cg_vol_ids)
+ self.assertIn(remove_volume_id2, cg_vol_ids)
+
+ add_volume_id = utils.create_volume(
+ ctxt,
+ volume_type_id=volume_type_id)['id']
+ add_volume_id2 = utils.create_volume(
+ ctxt,
+ volume_type_id=volume_type_id)['id']
+ req = webob.Request.blank('/v2/fake/consistencygroups/%s/update' %
+ consistencygroup_id)
+ req.method = 'PUT'
+ req.headers['Content-Type'] = 'application/json'
+ name = 'newcg'
+ description = 'New Consistency Group Description'
+ add_volumes = add_volume_id + "," + add_volume_id2
+ remove_volumes = remove_volume_id + "," + remove_volume_id2
+ body = {"consistencygroup": {"name": name,
+ "description": description,
+ "add_volumes": add_volumes,
+ "remove_volumes": remove_volumes, }}
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+
+ self.assertEqual(202, res.status_int)
+ self.assertEqual('updating',
+ self._get_consistencygroup_attrib(consistencygroup_id,
+ 'status'))
+
+ db.consistencygroup_destroy(ctxt.elevated(), consistencygroup_id)
+
+ def test_update_consistencygroup_add_volume_not_found(self):
+ ctxt = context.RequestContext('fake', 'fake')
+ consistencygroup_id = self._create_consistencygroup(status='available')
+ req = webob.Request.blank('/v2/fake/consistencygroups/%s/update' %
+ consistencygroup_id)
+ req.method = 'PUT'
+ req.headers['Content-Type'] = 'application/json'
+ body = {"consistencygroup": {"name": None,
+ "description": None,
+ "add_volumes": "fake-volume-uuid",
+ "remove_volumes": None, }}
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ self.assertEqual(400, res.status_int)
+ self.assertEqual(400, res_dict['badRequest']['code'])
+ msg = (_("Invalid volume: Cannot add volume fake-volume-uuid "
+ "to consistency group %(group_id)s because volume cannot "
+ "be found.") %
+ {'group_id': consistencygroup_id})
+ self.assertEqual(msg, res_dict['badRequest']['message'])
+
+ db.consistencygroup_destroy(ctxt.elevated(), consistencygroup_id)
+
+ def test_update_consistencygroup_remove_volume_not_found(self):
+ ctxt = context.RequestContext('fake', 'fake')
+ consistencygroup_id = self._create_consistencygroup(status='available')
+ req = webob.Request.blank('/v2/fake/consistencygroups/%s/update' %
+ consistencygroup_id)
+ req.method = 'PUT'
+ req.headers['Content-Type'] = 'application/json'
+ body = {"consistencygroup": {"name": None,
+ "description": "new description",
+ "add_volumes": None,
+ "remove_volumes": "fake-volume-uuid", }}
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ self.assertEqual(400, res.status_int)
+ self.assertEqual(400, res_dict['badRequest']['code'])
+ msg = (_("Invalid volume: Cannot remove volume fake-volume-uuid "
+ "from consistency group %(group_id)s because it is not "
+ "in the group.") %
+ {'group_id': consistencygroup_id})
+ self.assertEqual(msg, res_dict['badRequest']['message'])
+
+ db.consistencygroup_destroy(ctxt.elevated(), consistencygroup_id)
+
+ def test_update_consistencygroup_empty_parameters(self):
+ ctxt = context.RequestContext('fake', 'fake')
+ consistencygroup_id = self._create_consistencygroup(status='available')
+ req = webob.Request.blank('/v2/fake/consistencygroups/%s/update' %
+ consistencygroup_id)
+ req.method = 'PUT'
+ req.headers['Content-Type'] = 'application/json'
+ body = {"consistencygroup": {"name": "",
+ "description": "",
+ "add_volumes": None,
+ "remove_volumes": None, }}
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ self.assertEqual(400, res.status_int)
+ self.assertEqual(400, res_dict['badRequest']['code'])
+ self.assertEqual('Name, description, add_volumes, and remove_volumes '
+ 'can not be all empty in the request body.',
+ res_dict['badRequest']['message'])
+
+ db.consistencygroup_destroy(ctxt.elevated(), consistencygroup_id)
+
+ def test_update_consistencygroup_add_volume_invalid_state(self):
+ volume_type_id = '123456'
+ ctxt = context.RequestContext('fake', 'fake')
+ consistencygroup_id = self._create_consistencygroup(status='available')
+ add_volume_id = utils.create_volume(
+ ctxt,
+ volume_type_id=volume_type_id,
+ status='wrong_status')['id']
+ req = webob.Request.blank('/v2/fake/consistencygroups/%s/update' %
+ consistencygroup_id)
+ req.method = 'PUT'
+ req.headers['Content-Type'] = 'application/json'
+ add_volumes = add_volume_id
+ body = {"consistencygroup": {"name": "",
+ "description": "",
+ "add_volumes": add_volumes,
+ "remove_volumes": None, }}
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ self.assertEqual(400, res.status_int)
+ self.assertEqual(400, res_dict['badRequest']['code'])
+ msg = (_("Invalid volume: Cannot add volume %(volume_id)s "
+ "to consistency group %(group_id)s because volume is in an "
+ "invalid state: %(status)s. Valid states are: ('available', "
+ "'in-use').") %
+ {'volume_id': add_volume_id,
+ 'group_id': consistencygroup_id,
+ 'status': 'wrong_status'})
+ self.assertEqual(msg, res_dict['badRequest']['message'])
+
+ db.consistencygroup_destroy(ctxt.elevated(), consistencygroup_id)
+
+ def test_update_consistencygroup_add_volume_invalid_volume_type(self):
+ ctxt = context.RequestContext('fake', 'fake')
+ consistencygroup_id = self._create_consistencygroup(status='available')
+ wrong_type = 'wrong-volume-type-id'
+ add_volume_id = utils.create_volume(
+ ctxt,
+ volume_type_id=wrong_type)['id']
+ req = webob.Request.blank('/v2/fake/consistencygroups/%s/update' %
+ consistencygroup_id)
+ req.method = 'PUT'
+ req.headers['Content-Type'] = 'application/json'
+ add_volumes = add_volume_id
+ body = {"consistencygroup": {"name": "",
+ "description": "",
+ "add_volumes": add_volumes,
+ "remove_volumes": None, }}
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ self.assertEqual(400, res.status_int)
+ self.assertEqual(400, res_dict['badRequest']['code'])
+ msg = (_("Invalid volume: Cannot add volume %(volume_id)s "
+ "to consistency group %(group_id)s because volume type "
+ "%(volume_type)s is not supported by the group.") %
+ {'volume_id': add_volume_id,
+ 'group_id': consistencygroup_id,
+ 'volume_type': wrong_type})
+ self.assertEqual(msg, res_dict['badRequest']['message'])
+
+ db.consistencygroup_destroy(ctxt.elevated(), consistencygroup_id)
+
+ def test_update_consistencygroup_invalid_state(self):
+ ctxt = context.RequestContext('fake', 'fake')
+ wrong_status = 'wrong_status'
+ consistencygroup_id = self._create_consistencygroup(
+ status=wrong_status)
+ req = webob.Request.blank('/v2/fake/consistencygroups/%s/update' %
+ consistencygroup_id)
+ req.method = 'PUT'
+ req.headers['Content-Type'] = 'application/json'
+ body = {"consistencygroup": {"name": "new name",
+ "description": None,
+ "add_volumes": None,
+ "remove_volumes": None, }}
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ self.assertEqual(400, res.status_int)
+ self.assertEqual(400, res_dict['badRequest']['code'])
+ msg = _("Invalid ConsistencyGroup: Consistency group status must be "
+ "available, but current status is: %s.") % wrong_status
+ self.assertEqual(msg, res_dict['badRequest']['message'])
+
+ db.consistencygroup_destroy(ctxt.elevated(), consistencygroup_id)
"consistencygroup:create" : "",
"consistencygroup:delete": "",
+ "consistencygroup:update": "",
"consistencygroup:get": "",
"consistencygroup:get_all": "",
# clean up
self.volume.delete_volume(self.context, volume['id'])
- def test_create_delete_consistencygroup(self):
+ @mock.patch.object(CGQUOTAS, "reserve",
+ return_value=["RESERVATION"])
+ @mock.patch.object(CGQUOTAS, "commit")
+ @mock.patch.object(CGQUOTAS, "rollback")
+ @mock.patch.object(driver.VolumeDriver,
+ "create_consistencygroup",
+ return_value={'status': 'available'})
+ @mock.patch.object(driver.VolumeDriver,
+ "delete_consistencygroup",
+ return_value=({'status': 'deleted'}, []))
+ def test_create_delete_consistencygroup(self, fake_delete_cg,
+ fake_create_cg, fake_rollback,
+ fake_commit, fake_reserve):
"""Test consistencygroup can be created and deleted."""
- # Need to stub out reserve, commit, and rollback
- def fake_reserve(context, expire=None, project_id=None, **deltas):
- return ["RESERVATION"]
-
- def fake_commit(context, reservations, project_id=None):
- pass
-
- def fake_rollback(context, reservations, project_id=None):
- pass
-
- self.stubs.Set(CGQUOTAS, "reserve", fake_reserve)
- self.stubs.Set(CGQUOTAS, "commit", fake_commit)
- self.stubs.Set(CGQUOTAS, "rollback", fake_rollback)
-
- rval = {'status': 'available'}
- driver.VolumeDriver.create_consistencygroup = \
- mock.Mock(return_value=rval)
-
- rval = {'status': 'deleted'}, []
- driver.VolumeDriver.delete_consistencygroup = \
- mock.Mock(return_value=rval)
-
group = tests_utils.create_consistencygroup(
self.context,
availability_zone=CONF.storage_availability_zone,
self.context,
group_id)
+ @mock.patch.object(CGQUOTAS, "reserve",
+ return_value=["RESERVATION"])
+ @mock.patch.object(CGQUOTAS, "commit")
+ @mock.patch.object(CGQUOTAS, "rollback")
+ @mock.patch.object(driver.VolumeDriver,
+ "create_consistencygroup",
+ return_value={'status': 'available'})
+ @mock.patch.object(driver.VolumeDriver,
+ "update_consistencygroup")
+ def test_update_consistencygroup(self, fake_update_cg,
+ fake_create_cg, fake_rollback,
+ fake_commit, fake_reserve):
+ """Test consistencygroup can be updated."""
+ group = tests_utils.create_consistencygroup(
+ self.context,
+ availability_zone=CONF.storage_availability_zone,
+ volume_type='type1,type2')
+ group_id = group['id']
+ self.volume.create_consistencygroup(self.context, group_id)
+
+ volume = tests_utils.create_volume(
+ self.context,
+ consistencygroup_id=group_id,
+ **self.volume_params)
+ volume_id = volume['id']
+ self.volume.create_volume(self.context, volume_id)
+
+ volume2 = tests_utils.create_volume(
+ self.context,
+ consistencygroup_id=None,
+ **self.volume_params)
+ volume_id2 = volume2['id']
+ self.volume.create_volume(self.context, volume_id2)
+
+ fake_update_cg.return_value = (
+ {'status': 'available'},
+ [{'id': volume_id2, 'status': 'available'}],
+ [{'id': volume_id, 'status': 'available'}])
+
+ self.volume.update_consistencygroup(self.context, group_id,
+ add_volumes=volume_id2,
+ remove_volumes=volume_id)
+ cg = db.consistencygroup_get(
+ self.context,
+ group_id)
+ expected = {
+ 'status': 'available',
+ 'name': 'test_cg',
+ 'availability_zone': 'nova',
+ 'tenant_id': 'fake',
+ 'created_at': 'DONTCARE',
+ 'user_id': 'fake',
+ 'consistencygroup_id': group_id
+ }
+ self.assertEqual('available', cg['status'])
+ self.assertEqual(10, len(fake_notifier.NOTIFICATIONS))
+ msg = fake_notifier.NOTIFICATIONS[6]
+ self.assertEqual('consistencygroup.update.start', msg['event_type'])
+ self.assertDictMatch(expected, msg['payload'])
+ msg = fake_notifier.NOTIFICATIONS[8]
+ self.assertEqual('consistencygroup.update.end', msg['event_type'])
+ self.assertDictMatch(expected, msg['payload'])
+ cgvolumes = db.volume_get_all_by_group(self.context, group_id)
+ cgvol_ids = [cgvol['id'] for cgvol in cgvolumes]
+ # Verify volume is removed.
+ self.assertNotIn(volume_id, cgvol_ids)
+ # Verify volume is added.
+ self.assertIn(volume_id2, cgvol_ids)
+
+ self.volume_params['status'] = 'wrong-status'
+ volume3 = tests_utils.create_volume(
+ self.context,
+ consistencygroup_id=None,
+ **self.volume_params)
+ volume_id3 = volume3['id']
+
+ volume_get_orig = self.volume.db.volume_get
+ self.volume.db.volume_get = mock.Mock(
+ return_value={'status': 'wrong_status',
+ 'id': volume_id3})
+ # Try to add a volume in wrong status
+ self.assertRaises(exception.InvalidVolume,
+ self.volume.update_consistencygroup,
+ self.context,
+ group_id,
+ add_volumes=volume_id3,
+ remove_volumes=None)
+ self.volume.db.volume_get.reset_mock()
+ self.volume.db.volume_get = volume_get_orig
+
@staticmethod
def _create_cgsnapshot(group_id, volume_id, size='0'):
"""Create a cgsnapshot object."""
"""Deletes a consistency group."""
raise NotImplementedError()
+ def update_consistencygroup(self, context, group,
+ add_volumes=None, remove_volumes=None):
+ """Updates a consistency group.
+
+ :param context: the context of the caller.
+ :param group: the dictionary of the consistency group to be updated.
+ :param add_volumes: a list of volume dictionaries to be added.
+ :param remove_volumes: a list of volume dictionaries to be removed.
+ :return model_update, add_volumes_update, remove_volumes_update
+
+ model_update is a dictionary that the driver wants the manager
+ to update upon a successful return. If None is returned, the manager
+ will set the status to 'available'.
+
+ add_volumes_update and remove_volumes_update are lists of dictionaries
+ that the driver wants the manager to update upon a successful return.
+ Note that each entry requires a {'id': xxx} so that the correct
+ volume entry can be updated. If None is returned, the volume will
+ remain its original status. Also note that you cannot directly
+ assign add_volumes to add_volumes_update as add_volumes is a list of
+ cinder.db.sqlalchemy.models.Volume objects and cannot be used for
+ db update directly. Same with remove_volumes.
+
+ If the driver throws an exception, the status of the group as well as
+ those of the volumes to be added/removed will be set to 'error'.
+ """
+ raise NotImplementedError()
+
def create_cgsnapshot(self, context, cgsnapshot):
"""Creates a cgsnapshot."""
raise NotImplementedError()
QUOTAS = quota.QUOTAS
CGQUOTAS = quota.CGQUOTAS
+VALID_REMOVE_VOL_FROM_CG_STATUS = ('available', 'in-use',)
volume_manager_opts = [
cfg.StrOpt('volume_driver',
class VolumeManager(manager.SchedulerDependentManager):
"""Manages attachable block storage devices."""
- RPC_API_VERSION = '1.19'
+ RPC_API_VERSION = '1.21'
target = messaging.Target(version=RPC_API_VERSION)
context,
group,
event_suffix,
+ volumes=None,
extra_usage_info=None):
vol_utils.notify_about_consistencygroup_usage(
context, group, event_suffix,
extra_usage_info=extra_usage_info, host=self.host)
- volumes = self.db.volume_get_all_by_group(context, group['id'])
+ if not volumes:
+ volumes = self.db.volume_get_all_by_group(context, group['id'])
if volumes:
for volume in volumes:
vol_utils.notify_about_volume_usage(
context,
cgsnapshot,
event_suffix,
+ snapshots=None,
extra_usage_info=None):
vol_utils.notify_about_cgsnapshot_usage(
context, cgsnapshot, event_suffix,
extra_usage_info=extra_usage_info, host=self.host)
- snapshots = self.db.snapshot_get_all_for_cgsnapshot(context,
- cgsnapshot['id'])
+ if not snapshots:
+ snapshots = self.db.snapshot_get_all_for_cgsnapshot(
+ context, cgsnapshot['id'])
if snapshots:
for snapshot in snapshots:
vol_utils.notify_about_snapshot_usage(
LOG.info(_LI("Consistency group %s: deleted successfully."),
group_id)
self._notify_about_consistencygroup_usage(
- context, group_ref, "delete.end")
+ context, group_ref, "delete.end", volumes)
self.publish_service_capabilities(context)
return True
+ def update_consistencygroup(self, context, group_id,
+ add_volumes=None, remove_volumes=None):
+ """Updates consistency group.
+
+ Update consistency group by adding volumes to the group,
+ or removing volumes from the group.
+ """
+ LOG.info(_LI("Consistency group %s: updating"), group_id)
+ group = self.db.consistencygroup_get(context, group_id)
+
+ add_volumes_ref = []
+ remove_volumes_ref = []
+ add_volumes_list = []
+ remove_volumes_list = []
+ if add_volumes:
+ add_volumes_list = add_volumes.split(',')
+ if remove_volumes:
+ remove_volumes_list = remove_volumes.split(',')
+ for add_vol in add_volumes_list:
+ try:
+ add_vol_ref = self.db.volume_get(context, add_vol)
+ except exception.VolumeNotFound:
+ LOG.error(_LE("Cannot add volume %(volume_id)s to consistency "
+ "group %(group_id)s because volume cannot be "
+ "found."),
+ {'volume_id': add_vol_ref['id'],
+ 'group_id': group_id})
+ raise
+ if add_vol_ref['status'] not in ['in-use', 'available']:
+ msg = (_("Cannot add volume %(volume_id)s to consistency "
+ "group %(group_id)s because volume is in an invalid "
+ "state: %(status)s. Valid states are: %(valid)s.") %
+ {'volume_id': add_vol_ref['id'],
+ 'group_id': group_id,
+ 'status': add_vol_ref['status'],
+ 'valid': VALID_REMOVE_VOL_FROM_CG_STATUS})
+ raise exception.InvalidVolume(reason=msg)
+ # self.host is 'host@backend'
+ # volume_ref['host'] is 'host@backend#pool'
+ # Extract host before doing comparison
+ new_host = vol_utils.extract_host(add_vol_ref['host'])
+ if new_host != self.host:
+ raise exception.InvalidVolume(
+ reason=_("Volume is not local to this node."))
+ add_volumes_ref.append(add_vol_ref)
+
+ for remove_vol in remove_volumes_list:
+ try:
+ remove_vol_ref = self.db.volume_get(context, remove_vol)
+ except exception.VolumeNotFound:
+ LOG.error(_LE("Cannot remove volume %(volume_id)s from "
+ "consistency group %(group_id)s because volume "
+ "cannot be found."),
+ {'volume_id': remove_vol_ref['id'],
+ 'group_id': group_id})
+ raise
+ remove_volumes_ref.append(remove_vol_ref)
+
+ self._notify_about_consistencygroup_usage(
+ context, group, "update.start")
+
+ try:
+ utils.require_driver_initialized(self.driver)
+
+ LOG.debug("Consistency group %(group_id)s: updating",
+ {'group_id': group['id']})
+
+ model_update, add_volumes_update, remove_volumes_update = (
+ self.driver.update_consistencygroup(
+ context, group,
+ add_volumes=add_volumes_ref,
+ remove_volumes=remove_volumes_ref))
+
+ if add_volumes_update:
+ for update in add_volumes_update:
+ self.db.volume_update(context, update['id'], update)
+
+ if remove_volumes_update:
+ for update in remove_volumes_update:
+ self.db.volume_update(context, update['id'], update)
+
+ if model_update:
+ if model_update['status'] in ['error']:
+ msg = (_('Error occurred when updating consistency group '
+ '%s.') % group_id)
+ LOG.exception(msg)
+ raise exception.VolumeDriverException(message=msg)
+ self.db.consistencygroup_update(context, group_id,
+ model_update)
+
+ except exception.VolumeDriverException:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE("Error occurred in the volume driver when "
+ "updating consistency group %(group_id)s."),
+ {'group_id': group_id})
+ self.db.consistencygroup_update(context, group_id,
+ {'status': 'error'})
+ for add_vol in add_volumes_ref:
+ self.db.volume_update(context, add_vol['id'],
+ {'status': 'error'})
+ for rem_vol in remove_volumes_ref:
+ self.db.volume_update(context, rem_vol['id'],
+ {'status': 'error'})
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE("Error occurred when updating consistency "
+ "group %(group_id)s."),
+ {'group_id': group['id']})
+ self.db.consistencygroup_update(context, group_id,
+ {'status': 'error'})
+ for add_vol in add_volumes_ref:
+ self.db.volume_update(context, add_vol['id'],
+ {'status': 'error'})
+ for rem_vol in remove_volumes_ref:
+ self.db.volume_update(context, rem_vol['id'],
+ {'status': 'error'})
+
+ now = timeutils.utcnow()
+ self.db.consistencygroup_update(context, group_id,
+ {'status': 'available',
+ 'updated_at': now})
+ for add_vol in add_volumes_ref:
+ self.db.volume_update(context, add_vol['id'],
+ {'consistencygroup_id': group_id,
+ 'updated_at': now})
+ for rem_vol in remove_volumes_ref:
+ self.db.volume_update(context, rem_vol['id'],
+ {'consistencygroup_id': None,
+ 'updated_at': now})
+
+ LOG.info(_LI("Consistency group %s: updated successfully."),
+ group_id)
+ self._notify_about_consistencygroup_usage(
+ context, group, "update.end")
+
+ return True
+
def create_cgsnapshot(self, context, group_id, cgsnapshot_id):
"""Creates the cgsnapshot."""
caller_context = context
LOG.info(_LI("cgsnapshot %s: deleted successfully"),
cgsnapshot_ref['id'])
self._notify_about_cgsnapshot_usage(
- context, cgsnapshot_ref, "delete.end")
+ context, cgsnapshot_ref, "delete.end", snapshots)
return True
1.19 - Adds update_migrated_volume
1.20 - Adds support for sending objects over RPC in create_snapshot()
and delete_snapshot()
+ 1.21 - Adds update_consistencygroup.
'''
BASE_RPC_API_VERSION = '1.0'
target = messaging.Target(topic=CONF.volume_topic,
version=self.BASE_RPC_API_VERSION)
serializer = objects_base.CinderObjectSerializer()
- self.client = rpc.get_client(target, '1.20', serializer=serializer)
+ self.client = rpc.get_client(target, '1.21', serializer=serializer)
def create_consistencygroup(self, ctxt, group, host):
new_host = utils.extract_host(host)
cctxt.cast(ctxt, 'delete_consistencygroup',
group_id=group['id'])
+ def update_consistencygroup(self, ctxt, group, add_volumes=None,
+ remove_volumes=None):
+ host = utils.extract_host(group['host'])
+ cctxt = self.client.prepare(server=host, version='1.21')
+ cctxt.cast(ctxt, 'update_consistencygroup',
+ group_id=group['id'],
+ add_volumes=add_volumes,
+ remove_volumes=remove_volumes)
+
def create_cgsnapshot(self, ctxt, group, cgsnapshot):
host = utils.extract_host(group['host'])
"consistencygroup:create" : "group:nobody",
"consistencygroup:delete": "group:nobody",
+ "consistencygroup:update": "group:nobody",
"consistencygroup:get": "group:nobody",
"consistencygroup:get_all": "group:nobody",