VMAX driver - Fix error handling and checks for generic volume groups

There are several issues with the implementation of generic volume
groups in the VMAX driver. There is no rollback implemented in case
of errors for create group from source or for deleting groups. There
can be issues if source groups have been modified since a snapshot
was taken. The provider_location is also not being updated in the
individual volume snapshot objects of a group snapshot. This patch
rectifies these issues, and also does some general refactoring of
the generic volume group code.

Change-Id: I3e6ac90b145015ccd820af3449b20e377c9f4651
Closes-bug: 1732488
This commit is contained in:
Ciara Stacke 2017-11-17 12:32:47 +00:00 committed by Helen Walsh
parent cf40a001da
commit 826d48e986
5 changed files with 380 additions and 374 deletions

View File

@ -199,10 +199,13 @@ class VMAXCommonData(object):
replication_driver_data=six.text_type(legacy_provider_location2),
host=fake_host, volume_type=test_volume_type)
snapshot_id = '390eeb4d-0f56-4a02-ba14-167167967014'
test_clone_volume = fake_volume.fake_volume_obj(
context=ctx, name='vol1', size=2, provider_auth=None,
provider_location=six.text_type(provider_location2),
host=fake_host)
host=fake_host, source_volid=test_volume.id,
snapshot_id=snapshot_id)
test_volume_snap_manage = fake_volume.fake_volume_obj(
context=ctx, name='vol1', size=2, provider_auth=None,
@ -212,7 +215,6 @@ class VMAXCommonData(object):
replication_driver_data=six.text_type(provider_location4))
snapshot_display_id = 'my_snap'
snapshot_id = '390eeb4d-0f56-4a02-ba14-167167967014'
managed_snap_id = 'OS-390eeb4d-0f56-4a02-ba14-167167967014'
test_snapshot_snap_name = 'OS-' + snapshot_id[:6] + snapshot_id[-9:]
@ -482,10 +484,10 @@ class VMAXCommonData(object):
sg_details_rep = [{"childNames": [],
"numDevicesNonGk": 2,
"isLinkTarget": False,
"rdf": False,
"rdf": True,
"capacityGB": 2.0,
"name": storagegroup_name_source,
"snapVXSnapshots": ['12345'],
"snapVXSnapshots": ['6560405d-752f5a139'],
"symmetrixId": array,
"numSnapVXSnapshots": 1}]
@ -1365,32 +1367,11 @@ class VMAXUtilsTest(test.TestCase):
self.assertEqual(ref_group_name, vol_grp_name)
def test_get_volume_group_utils(self):
group = self.data.test_group_1
array, extraspecs_dict = self.utils.get_volume_group_utils(
group, interval=1, retries=1)
array, intervals_retries = self.utils.get_volume_group_utils(
self.data.test_group_1, interval=1, retries=1)
ref_array = self.data.array
self.assertEqual(ref_array, array)
def test_update_extra_specs_list(self):
extra_specs = self.data.extra_specs
volume_type_id = 'abc'
extraspecs_dict = self.utils._update_extra_specs_list(
extra_specs, volume_type_id, interval=1, retries=1)
self.assertEqual(extra_specs, extraspecs_dict['extra_specs'])
def test_update_intervals_and_retries(self):
extra_specs = self.data.extra_specs
ref_interval = 1
extraspecs = self.utils._update_intervals_and_retries(
extra_specs, interval=1, retries=1)
self.assertEqual(ref_interval, extraspecs['interval'])
def test_get_intervals_retries_dict(self):
ref_value = {'interval': 1, 'retries': 1}
ret_dict = self.utils.get_intervals_retries_dict(
interval=1, retries=1)
self.assertEqual(ref_value, ret_dict)
def test_update_volume_model_updates(self):
volume_model_updates = [{'id': '1', 'status': 'available'}]
volumes = [self.data.test_volume]
@ -2452,6 +2433,16 @@ class VMAXRestTest(test.TestCase):
self.rest.modify_resource.assert_called_once_with(
array, 'replication', 'snapshot', payload_restore,
resource_name=snap_name, private='/private')
# link or unlink, list of volumes
mock_modify.reset_mock()
payload["action"] = "Link"
self.rest.modify_volume_snap(
array, "", "", snap_name,
extra_specs, unlink=False, link=True,
list_volume_pairs=[(source_id, target_id)])
self.rest.modify_resource.assert_called_once_with(
array, 'replication', 'snapshot', payload,
resource_name=snap_name, private='/private')
# none selected
mock_modify.reset_mock()
self.rest.modify_volume_snap(
@ -2976,7 +2967,8 @@ class VMAXProvisionTest(test.TestCase):
(self.provision.rest.modify_volume_snap.
assert_called_once_with(
array, source_device_id, target_device_id,
snap_name, extra_specs, unlink=True))
snap_name, extra_specs,
list_volume_pairs=None, unlink=True))
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
new=test_utils.ZeroIntervalLoopingCall)
@ -2988,7 +2980,7 @@ class VMAXProvisionTest(test.TestCase):
mock_mod.assert_called_once_with(
self.data.array, self.data.device_id, self.data.device_id2,
self.data.snap_location['snap_name'], self.data.extra_specs,
unlink=True)
list_volume_pairs=None, unlink=True)
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
new=test_utils.ZeroIntervalLoopingCall)
@ -3281,19 +3273,6 @@ class VMAXProvisionTest(test.TestCase):
target_group_name, snap_name,
extra_specs, deleteSnapshot)
def test_unlink_group(self):
with mock.patch.object(self.rest,
'modify_storagegroup_snap') as mock_mod:
self.provision._unlink_group(
self.data.array, self.data.storagegroup_name_source,
self.data.target_group_name,
self.data.group_snapshot_name, self.data.extra_specs)
mock_mod.assert_called_once_with(
self.data.array, self.data.storagegroup_name_source,
self.data.target_group_name,
self.data.group_snapshot_name, self.data.extra_specs,
unlink=True)
@mock.patch.object(rest.VMAXRest, 'get_storage_group',
side_effect=[None, VMAXCommonData.sg_details[1]])
@mock.patch.object(provision.VMAXProvision, 'create_volume_group')
@ -3452,7 +3431,7 @@ class VMAXCommonTest(test.TestCase):
self.common.delete_snapshot(self.data.test_snapshot,
self.data.test_volume)
self.provision.delete_volume_snap.assert_called_once_with(
self.data.array, snap_name, sourcedevice_id)
self.data.array, snap_name, [sourcedevice_id])
def test_delete_snapshot_not_found(self):
with mock.patch.object(self.common, '_parse_snap_info',
@ -4838,26 +4817,61 @@ class VMAXCommonTest(test.TestCase):
group, volumes)
self.assertEqual(ref_model_update, model_update)
@mock.patch.object(
common.VMAXCommon, '_get_clone_vol_info',
return_value=(VMAXCommonData.device_id,
VMAXCommonData.extra_specs, 1, 'tgt_vol'))
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True)
@mock.patch.object(volume_utils, 'is_group_a_type',
return_value=False)
def test_create_group_from_src_success(self, mock_type, mock_cg_type):
context = None
group = self.data.test_group_1
group_snapshot = self.data.test_group_snapshot_1
snapshots = []
volumes = [self.data.test_volume]
source_group = None
source_vols = []
def test_create_group_from_src_success(self, mock_type,
mock_cg_type, mock_info):
ref_model_update = {'status': fields.GroupStatus.AVAILABLE}
model_update, volumes_model_update = (
self.common.create_group_from_src(
context, group, volumes,
group_snapshot, snapshots,
source_group, source_vols))
None, self.data.test_group_1, [self.data.test_volume],
self.data.test_group_snapshot_1, [], None, []))
self.assertEqual(ref_model_update, model_update)
@mock.patch.object(
common.VMAXCommon, '_remove_vol_and_cleanup_replication')
@mock.patch.object(
masking.VMAXMasking, 'remove_volumes_from_storage_group')
def test_rollback_create_group_from_src(
self, mock_rm, mock_clean):
rollback_dict = {
'target_group_name': self.data.target_group_name,
'snap_name': 'snap1', 'source_group_name': 'src_grp',
'volumes': (self.data.device_id, self.data.extra_specs,
self.data.test_volume),
'device_ids': [self.data.device_id],
'interval_retries_dict': self.data.extra_specs}
for x in range(0, 2):
self.common._rollback_create_group_from_src(
self.data.array, rollback_dict)
self.assertEqual(2, mock_rm.call_count)
def test_get_snap_src_dev_list(self):
src_dev_ids = self.common._get_snap_src_dev_list(
self.data.array, [self.data.test_snapshot])
ref_dev_ids = [self.data.device_id]
self.assertEqual(ref_dev_ids, src_dev_ids)
def test_get_clone_vol_info(self):
ref_dev_id = self.data.device_id
source_vols = [self.data.test_volume,
self.data.test_attached_volume]
src_snapshots = [self.data.test_snapshot]
src_dev_id1, extra_specs1, vol_size1, tgt_vol_name1 = (
self.common._get_clone_vol_info(
self.data.test_clone_volume, source_vols, []))
src_dev_id2, extra_specs2, vol_size2, tgt_vol_name2 = (
self.common._get_clone_vol_info(
self.data.test_clone_volume, [], src_snapshots))
self.assertEqual(ref_dev_id, src_dev_id1)
self.assertEqual(ref_dev_id, src_dev_id2)
def test_get_attributes_from_cinder_config(self):
kwargs_expected = (
{'RestServerIp': '1.1.1.1',
@ -7249,7 +7263,7 @@ class VMAXCommonReplicationTest(test.TestCase):
model_update['replication_status'])
@mock.patch.object(utils.VMAXUtils, 'get_volume_group_utils',
return_value=(VMAXCommonData.array, []))
return_value=(VMAXCommonData.array, {}))
@mock.patch.object(common.VMAXCommon, '_cleanup_group_replication')
@mock.patch.object(volume_utils, 'is_group_a_type', return_value=True)
def test_delete_replication_group(self, mock_check,

View File

@ -274,6 +274,8 @@ class VMAXCommon(object):
if volume.group_id is not None:
if (volume_utils.is_group_a_cg_snapshot_type(volume.group)
or volume.group.is_replicated):
LOG.debug("Adding volume %(vol_id)s to group %(grp_id)s",
{'vol_id': volume.id, 'grp_id': volume.group_id})
self._add_new_volume_to_volume_group(
volume, volume_dict['device_id'], volume_name,
extra_specs, rep_driver_data)
@ -1007,8 +1009,11 @@ class VMAXCommon(object):
device_id = name['keybindings']['DeviceID']
else:
device_id = None
founddevice_id = self.rest.check_volume_device_id(
array, device_id, volume_name)
try:
founddevice_id = self.rest.check_volume_device_id(
array, device_id, volume_name)
except exception.VolumeBackendAPIException:
pass
if founddevice_id is None:
LOG.debug("Volume %(volume_name)s not found on the array.",
@ -3237,10 +3242,8 @@ class VMAXCommon(object):
vol_grp_name = self.utils.update_volume_group_name(group)
try:
array, __ = self.utils.get_volume_group_utils(
array, interval_retries_dict = self.utils.get_volume_group_utils(
group, self.interval, self.retries)
interval_retries_dict = self.utils.get_intervals_retries_dict(
self.interval, self.retries)
self.provision.create_volume_group(
array, vol_grp_name, interval_retries_dict)
if group.is_replicated:
@ -3288,7 +3291,7 @@ class VMAXCommon(object):
:returns: model_update, volumes_model_update
"""
volumes_model_update = []
array, extraspecs_dict_list = self.utils.get_volume_group_utils(
array, interval_retries_dict = self.utils.get_volume_group_utils(
group, self.interval, self.retries)
vol_grp_name = None
@ -3308,39 +3311,33 @@ class VMAXCommon(object):
vol_grp_name = volume_group['name']
volume_device_ids = self._get_members_of_volume_group(
array, vol_grp_name)
intervals_retries_dict = self.utils.get_intervals_retries_dict(
self.interval, self.retries)
deleted_volume_device_ids = []
# Remove replication for group, if applicable
if group.is_replicated:
self._cleanup_group_replication(
array, vol_grp_name, volume_device_ids,
intervals_retries_dict)
interval_retries_dict)
try:
if volume_device_ids:
# First remove all the volumes from the SG
self.masking.remove_volumes_from_storage_group(
array, volume_device_ids, vol_grp_name,
intervals_retries_dict)
interval_retries_dict)
for vol in volumes:
for extraspecs_dict in extraspecs_dict_list:
if (vol.volume_type_id in
extraspecs_dict['volumeTypeId']):
extraspecs = extraspecs_dict.get(
utils.EXTRA_SPECS)
device_id = self._find_device_on_array(
vol, extraspecs)
if device_id in volume_device_ids:
self.masking.remove_and_reset_members(
array, vol, device_id, vol.name,
extraspecs, False)
self._delete_from_srp(
array, device_id, "group vol", extraspecs)
else:
LOG.debug("Volume not found on the array.")
# Add the device id to the deleted list
deleted_volume_device_ids.append(device_id)
extra_specs = self._initial_setup(vol)
device_id = self._find_device_on_array(
vol, extra_specs)
if device_id in volume_device_ids:
self.masking.remove_and_reset_members(
array, vol, device_id, vol.name,
extra_specs, False)
self._delete_from_srp(
array, device_id, "group vol", extra_specs)
else:
LOG.debug("Volume not found on the array.")
# Add the device id to the deleted list
deleted_volume_device_ids.append(device_id)
# Once all volumes are deleted then delete the SG
self.rest.delete_storage_group(array, vol_grp_name)
model_update = {'status': fields.GroupStatus.DELETED}
@ -3351,27 +3348,28 @@ class VMAXCommon(object):
"Error received: %(e)s", {'e': e})
model_update = {'status': fields.GroupStatus.ERROR_DELETING}
# Update the volumes_model_update
if len(deleted_volume_device_ids) is not 0:
LOG.debug("Device ids: %(dev)s are deleted.",
{'dev': deleted_volume_device_ids})
volumes_not_deleted = []
for vol in volume_device_ids:
if vol not in deleted_volume_device_ids:
volumes_not_deleted.append(vol)
if not deleted_volume_device_ids:
volumes_model_update = self.utils.update_volume_model_updates(
volumes_model_update,
deleted_volume_device_ids,
volumes_model_update, deleted_volume_device_ids,
group.id, status='deleted')
if not volumes_not_deleted:
volumes_model_update = self.utils.update_volume_model_updates(
volumes_model_update,
volumes_not_deleted,
group.id, status='deleted')
volumes_model_update, volumes_not_deleted,
group.id, status='error_deleting')
# As a best effort try to add back the undeleted volumes to sg
# Dont throw any exception in case of failure
# Don't throw any exception in case of failure
try:
if not volumes_not_deleted:
self.masking.add_volumes_to_storage_group(
array, volumes_not_deleted,
vol_grp_name, intervals_retries_dict)
vol_grp_name, interval_retries_dict)
except Exception as ex:
LOG.error("Error in rollback - %(ex)s. "
"Failed to add back volumes to sg %(sg_name)s",
@ -3434,8 +3432,7 @@ class VMAXCommon(object):
try:
snap_name = self.utils.truncate_string(group_snapshot.id, 19)
self._create_group_replica(source_group,
snap_name)
self._create_group_replica(source_group, snap_name)
except Exception as e:
exception_message = (_("Failed to create snapshot for group: "
@ -3446,13 +3443,26 @@ class VMAXCommon(object):
raise exception.VolumeBackendAPIException(data=exception_message)
for snapshot in snapshots:
src_dev_id = self._get_src_device_id_for_group_snap(snapshot)
snapshots_model_update.append(
{'id': snapshot.id,
'provider_location': six.text_type(
{'source_id': src_dev_id, 'snap_name': snap_name}),
'status': fields.SnapshotStatus.AVAILABLE})
model_update = {'status': fields.GroupStatus.AVAILABLE}
return model_update, snapshots_model_update
def _get_src_device_id_for_group_snap(self, snapshot):
"""Get the source device id for the provider_location.
:param snapshot: the snapshot object
:return: src_device_id
"""
volume = snapshot.volume
extra_specs = self._initial_setup(volume)
return self._find_device_on_array(volume, extra_specs)
def _create_group_replica(
self, source_group, snap_name):
"""Create a group replica.
@ -3461,9 +3471,8 @@ class VMAXCommon(object):
:param source_group: the group object
:param snap_name: the name of the snapshot
"""
array, __ = (
self.utils.get_volume_group_utils(
source_group, self.interval, self.retries))
array, interval_retries_dict = self.utils.get_volume_group_utils(
source_group, self.interval, self.retries)
vol_grp_name = None
volume_group = (
self._find_volume_group(array, source_group))
@ -3476,8 +3485,6 @@ class VMAXCommon(object):
{'group_id': source_group.id})
raise exception.VolumeBackendAPIException(
data=exception_message)
interval_retries_dict = self.utils.get_intervals_retries_dict(
self.interval, self.retries)
self.provision.create_group_replica(
array, vol_grp_name,
snap_name, interval_retries_dict)
@ -3503,7 +3510,6 @@ class VMAXCommon(object):
:raises: VolumeBackendApiException, NotImplementedError
"""
snapshots_model_update = []
model_update = {}
source_group = group_snapshot.get('group')
grp_id = group_snapshot.group_id
if not volume_utils.is_group_a_cg_snapshot_type(source_group):
@ -3518,15 +3524,12 @@ class VMAXCommon(object):
vol_grp_name = None
try:
# Get the array serial
array, __ = (
self.utils.get_volume_group_utils(
source_group, self.interval, self.retries))
array, extra_specs = self.utils.get_volume_group_utils(
source_group, self.interval, self.retries)
# Get the volume group dict for getting the group name
volume_group = (
self._find_volume_group(array, source_group))
if volume_group:
if 'name' in volume_group:
vol_grp_name = volume_group['name']
volume_group = (self._find_volume_group(array, source_group))
if volume_group and volume_group.get('name'):
vol_grp_name = volume_group['name']
if vol_grp_name is None:
exception_message = (
_("Cannot find generic volume group %(grp_id)s.") %
@ -3536,9 +3539,9 @@ class VMAXCommon(object):
# Check if the snapshot exists
if 'snapVXSnapshots' in volume_group:
if snap_name in volume_group['snapVXSnapshots']:
self.provision.delete_group_replica(array,
snap_name,
vol_grp_name)
src_devs = self._get_snap_src_dev_list(array, snapshots)
self.provision.delete_group_replica(
array, snap_name, vol_grp_name, src_devs, extra_specs)
else:
# Snapshot has been already deleted, return successfully
LOG.error("Cannot find group snapshot %(snapId)s.",
@ -3556,6 +3559,20 @@ class VMAXCommon(object):
return model_update, snapshots_model_update
def _get_snap_src_dev_list(self, array, snapshots):
"""Get the list of source devices for a list of snapshots.
:param array: the array serial number
:param snapshots: the list of snapshot objects
:return: src_dev_ids
"""
src_dev_ids = []
for snap in snapshots:
src_dev_id, snap_name = self._parse_snap_info(array, snap)
if snap_name:
src_dev_ids.append(src_dev_id)
return src_dev_ids
def _find_volume_group(self, array, group):
"""Finds a volume group given the group.
@ -3603,7 +3620,7 @@ class VMAXCommon(object):
and not group.is_replicated):
raise NotImplementedError()
array, __ = self.utils.get_volume_group_utils(
array, interval_retries_dict = self.utils.get_volume_group_utils(
group, self.interval, self.retries)
model_update = {'status': fields.GroupStatus.AVAILABLE}
add_vols = [vol for vol in add_volumes] if add_volumes else []
@ -3618,8 +3635,6 @@ class VMAXCommon(object):
vol_grp_name = volume_group['name']
if vol_grp_name is None:
raise exception.GroupNotFound(group_id=group.id)
interval_retries_dict = self.utils.get_intervals_retries_dict(
self.interval, self.retries)
# Add volume(s) to the group
if add_device_ids:
self.utils.check_rep_status_enabled(group)
@ -3741,15 +3756,12 @@ class VMAXCommon(object):
"""
if not volume_utils.is_group_a_cg_snapshot_type(group):
raise NotImplementedError()
# Check if we need to create a snapshot
create_snapshot = False
volumes_model_update = []
if group_snapshot:
source_vols_or_snapshots = snapshots
source_id = group_snapshot.id
actual_source_grp = group_snapshot
actual_source_grp = group_snapshot.get('group')
elif source_group:
source_vols_or_snapshots = source_vols
source_id = source_group.id
actual_source_grp = source_group
create_snapshot = True
@ -3759,88 +3771,169 @@ class VMAXCommon(object):
raise exception.VolumeBackendAPIException(
data=exception_message)
tgt_name = self.utils.update_volume_group_name(group)
rollback_dict = {}
array, interval_retries_dict = self.utils.get_volume_group_utils(
group, self.interval, self.retries)
source_sg = self._find_volume_group(array, actual_source_grp)
if source_sg is not None:
src_grp_name = (source_sg['name']
if 'name' in source_sg else None)
rollback_dict['source_group_name'] = src_grp_name
else:
error_msg = (_("Cannot retrieve source volume group %(grp_id)s "
"from the array.")
% {'grp_id': actual_source_grp.id})
LOG.error(error_msg)
raise exception.VolumeBackendAPIException(data=error_msg)
LOG.debug("Enter VMAX create_volume group_from_src. Group to be "
"created: %(grpId)s, Source : %(SourceGrpId)s.",
{'grpId': group.id,
'SourceGrpId': source_id})
{'grpId': group.id, 'SourceGrpId': source_id})
tgt_name = self.utils.update_volume_group_name(group)
self.create_group(context, group)
model_update = {'status': fields.GroupStatus.AVAILABLE}
try:
array, extraspecs_dict_list = (
self.utils.get_volume_group_utils(
group, self.interval, self.retries))
vol_grp_name = ""
self.provision.create_volume_group(
array, tgt_name, interval_retries_dict)
rollback_dict.update({
'target_group_name': tgt_name, 'volumes': [],
'device_ids': [], 'list_volume_pairs': [],
'interval_retries_dict': interval_retries_dict})
model_update = {'status': fields.GroupStatus.AVAILABLE}
# Create the target devices
dict_volume_dicts = {}
target_volume_names = {}
for volume, source_vol_or_snapshot in zip(
volumes, source_vols_or_snapshots):
if 'size' in source_vol_or_snapshot:
volume_size = source_vol_or_snapshot['size']
else:
volume_size = source_vol_or_snapshot['volume_size']
for extraspecs_dict in extraspecs_dict_list:
if volume.volume_type_id in (
extraspecs_dict['volumeTypeId']):
extraspecs = extraspecs_dict.get(utils.EXTRA_SPECS)
target_volume_name = (
self.utils.get_volume_element_name(volume.id))
volume_dict = self.provision.create_volume_from_sg(
array, target_volume_name,
tgt_name, volume_size, extraspecs)
dict_volume_dicts[volume.id] = volume_dict
target_volume_names[volume.id] = target_volume_name
list_volume_pairs = []
for volume in volumes:
src_dev_id, extra_specs, vol_size, tgt_vol_name = (
self._get_clone_vol_info(
volume, source_vols, snapshots))
volume_dict = self._create_volume(
tgt_vol_name, vol_size, extra_specs)
device_id = volume_dict['device_id']
# Add the volume to the volume group SG
self.masking.add_volume_to_storage_group(
extra_specs[utils.ARRAY], device_id, tgt_name,
tgt_vol_name, extra_specs)
# Record relevant information
list_volume_pairs.append((src_dev_id, device_id))
# Add details to rollback dict
rollback_dict['device_ids'].append(device_id)
rollback_dict['list_volume_pairs'].append(
(src_dev_id, device_id))
rollback_dict['volumes'].append(
(device_id, extra_specs, volume))
volumes_model_update.append(
self.utils.get_grp_volume_model_update(
volume, volume_dict, group.id))
if create_snapshot is True:
# We have to create a snapshot of the source group
snap_name = self.utils.truncate_string(group.id, 19)
self._create_group_replica(actual_source_grp, snap_name)
vol_grp_name = self.utils.update_volume_group_name(
source_group)
rollback_dict['snap_name'] = snap_name
else:
# We need to check if the snapshot exists
snap_name = self.utils.truncate_string(source_id, 19)
source_group = actual_source_grp.get('group')
volume_group = self._find_volume_group(array, source_group)
if volume_group is not None:
if 'snapVXSnapshots' in volume_group:
if snap_name in volume_group['snapVXSnapshots']:
LOG.info("Snapshot is present on the array")
if 'name' in volume_group:
vol_grp_name = volume_group['name']
if ('snapVXSnapshots' in source_sg and
snap_name in source_sg['snapVXSnapshots']):
LOG.info("Snapshot is present on the array")
else:
error_msg = (
_("Cannot retrieve source snapshot %(snap_id)s "
"from the array.") % {'snap_id': source_id})
LOG.error(error_msg)
raise exception.VolumeBackendAPIException(data=error_msg)
# Link and break the snapshot to the source group
interval_retries_dict = self.utils.get_intervals_retries_dict(
self.interval, self.retries)
self.provision.link_and_break_replica(
array, vol_grp_name, tgt_name, snap_name,
interval_retries_dict, delete_snapshot=create_snapshot)
except Exception:
exception_message = (_("Failed to create vol grp %(volGrpName)s"
" from source %(grpSnapshot)s.")
% {'volGrpName': group.id,
'grpSnapshot': source_id})
LOG.exception(exception_message)
raise exception.VolumeBackendAPIException(data=exception_message)
volumes_model_update = self.utils.update_volume_model_updates(
volumes_model_update, volumes, group.id, model_update['status'])
# Update the provider_location & replication status
for volume_model_update in volumes_model_update:
if volume_model_update['id'] in dict_volume_dicts:
volume_model_update.update(
{'provider_location': six.text_type(
dict_volume_dicts[volume_model_update['id']])})
array, src_grp_name, tgt_name, snap_name,
interval_retries_dict, list_volume_pairs,
delete_snapshot=create_snapshot)
# Update the replication status
if group.is_replicated:
volumes_model_update = self._replicate_group(
array, volumes_model_update,
tgt_name, interval_retries_dict)
model_update.update({
'replication_status': fields.ReplicationStatus.ENABLED})
except Exception:
exception_message = (_("Failed to create vol grp %(volGrpName)s"
" from source %(grpSnapshot)s.")
% {'volGrpName': group.id,
'grpSnapshot': source_id})
LOG.error(exception_message)
if array is not None:
LOG.info("Attempting rollback for the create group from src.")
self._rollback_create_group_from_src(array, rollback_dict)
raise exception.VolumeBackendAPIException(data=exception_message)
return model_update, volumes_model_update
def _get_clone_vol_info(self, volume, source_vols, snapshots):
"""Get the clone volume info.
:param volume: the new volume object
:param source_vols: the source volume list
:param snapshots: the source snapshot list
:returns: src_dev_id, extra_specs, vol_size, tgt_vol_name
"""
src_dev_id, vol_size = None, None
extra_specs = self._initial_setup(volume)
if not source_vols:
for snap in snapshots:
if snap.id == volume.snapshot_id:
src_dev_id, __ = self._parse_snap_info(
extra_specs[utils.ARRAY], snap)
vol_size = snap.volume_size
else:
for src_vol in source_vols:
if src_vol.id == volume.source_volid:
src_extra_specs = self._initial_setup(src_vol)
src_dev_id = self._find_device_on_array(
src_vol, src_extra_specs)
vol_size = src_vol.size
tgt_vol_name = self.utils.get_volume_element_name(volume.id)
return src_dev_id, extra_specs, vol_size, tgt_vol_name
def _rollback_create_group_from_src(self, array, rollback_dict):
"""Performs rollback for create group from src in case of failure.
:param array: the array serial number
:param rollback_dict: dict containing rollback details
"""
try:
# Delete the snapshot if required
if rollback_dict.get("snap_name"):
try:
src_dev_ids = [
a for a, b in rollback_dict['list_volume_pairs']]
self.provision.delete_group_replica(
array, rollback_dict["snap_name"],
rollback_dict["source_group_name"],
src_dev_ids, rollback_dict['interval_retries_dict'])
except Exception as e:
LOG.debug("Failed to delete group snapshot. Attempting "
"further rollback. Exception received: %(e)s.",
{'e': e})
if rollback_dict.get('volumes'):
# Remove any devices which were added to the target SG
if rollback_dict['device_ids']:
self.masking.remove_volumes_from_storage_group(
array, rollback_dict['device_ids'],
rollback_dict['target_group_name'],
rollback_dict['interval_retries_dict'])
# Delete all the volumes
for dev_id, extra_specs, volume in rollback_dict['volumes']:
self._remove_vol_and_cleanup_replication(
array, dev_id, "group vol", extra_specs, volume)
self._delete_from_srp(
array, dev_id, "group vol", extra_specs)
# Delete the target SG
if rollback_dict.get("target_group_name"):
self.rest.delete_storage_group(
array, rollback_dict['target_group_name'])
LOG.info("Rollback completed for create group from src.")
except Exception as e:
LOG.error("Rollback failed for the create group from src. "
"Exception received: %(e)s.", {'e': e})
def _replicate_group(self, array, volumes_model_update,
group_name, extra_specs):
"""Replicate a cloned volume group.
@ -3854,10 +3947,11 @@ class VMAXCommon(object):
rdf_group_no, remote_array = self.get_rdf_details(array)
self.rest.replicate_group(
array, group_name, rdf_group_no, remote_array, extra_specs)
# Need to set SRP to None for generic volume group - Not set
# Need to set SRP to None for remote generic volume group - Not set
# automatically, and a volume can only be in one storage group
# managed by FAST
self.rest.set_storagegroup_srp(array, group_name, "None", extra_specs)
self.rest.set_storagegroup_srp(
remote_array, group_name, "None", extra_specs)
for volume_model_update in volumes_model_update:
vol_id = volume_model_update['id']
loc = ast.literal_eval(volume_model_update['provider_location'])

View File

@ -174,7 +174,7 @@ class VMAXProvision(object):
def _unlink_volume(
self, array, source_device_id, target_device_id, snap_name,
extra_specs):
extra_specs, list_volume_pairs=None):
"""Unlink a target volume from its source volume.
:param array: the array serial number
@ -182,6 +182,7 @@ class VMAXProvision(object):
:param target_device_id: the target device id
:param snap_name: the snap name
:param extra_specs: extra specifications
:param list_volume_pairs: list of volume pairs, optional
:return: return code
"""
@ -196,7 +197,8 @@ class VMAXProvision(object):
if not kwargs['modify_vol_success']:
self.rest.modify_volume_snap(
array, source_device_id, target_device_id, snap_name,
extra_specs, unlink=True)
extra_specs, unlink=True,
list_volume_pairs=list_volume_pairs)
kwargs['modify_vol_success'] = True
except exception.VolumeBackendAPIException:
pass
@ -315,26 +317,32 @@ class VMAXProvision(object):
do_delete_temp_snap(snap_name)
def delete_volume_snap_check_for_links(self, array, snap_name,
source_device, extra_specs):
source_devices, extra_specs):
"""Check if a snap has any links before deletion.
If a snapshot has any links, break the replication relationship
before deletion.
:param array: the array serial number
:param snap_name: the snapshot name
:param source_device: the source device id
:param source_devices: the source device ids
:param extra_specs: the extra specifications
"""
LOG.debug("Check for linked devices to SnapVx: %(snap_name)s "
"for volume %(vol)s.",
{'vol': source_device, 'snap_name': snap_name})
linked_list = self.rest.get_snap_linked_device_list(
array, source_device, snap_name)
for link in linked_list:
target_device = link['targetDevice']
self.break_replication_relationship(
array, target_device, source_device, snap_name, extra_specs)
self.delete_volume_snap(array, snap_name, source_device)
list_device_pairs = []
if not isinstance(source_devices, list):
source_devices = [source_devices]
for source_device in source_devices:
LOG.debug("Check for linked devices to SnapVx: %(snap_name)s "
"for volume %(vol)s.",
{'vol': source_device, 'snap_name': snap_name})
linked_list = self.rest.get_snap_linked_device_list(
array, source_device, snap_name)
for link in linked_list:
target_device = link['targetDevice']
list_device_pairs.append((source_device, target_device))
if list_device_pairs:
self._unlink_volume(array, "", "", snap_name, extra_specs,
list_volume_pairs=list_device_pairs)
self.delete_volume_snap(array, snap_name, source_devices)
def extend_volume(self, array, device_id, new_size, extra_specs):
"""Extend a volume.
@ -649,26 +657,26 @@ class VMAXProvision(object):
self.rest.create_storagegroup_snap(
array, source_group, snap_name, extra_specs)
def delete_group_replica(self, array, snap_name,
source_group_name):
def delete_group_replica(self, array, snap_name, source_group_name,
src_dev_ids, extra_specs):
"""Delete the snapshot.
:param array: the array serial number
:param snap_name: the name for the snap shot
:param source_group_name: the source group name
:param src_dev_ids: the list of source device ids
:param extra_specs: extra specifications
"""
# Delete snapvx snapshot
LOG.debug("Deleting Snap Vx snapshot: source group: %(srcGroup)s "
"snapshot: %(snap_name)s.",
{'srcGroup': source_group_name,
'snap_name': snap_name})
# The check for existence of snapshot has already happened
# So we just need to delete the snapshot
self.rest.delete_storagegroup_snap(array, snap_name, source_group_name)
{'srcGroup': source_group_name, 'snap_name': snap_name})
self.delete_volume_snap_check_for_links(
array, snap_name, src_dev_ids, extra_specs)
def link_and_break_replica(self, array, source_group_name,
target_group_name, snap_name, extra_specs,
delete_snapshot=False):
list_volume_pairs, delete_snapshot=False):
"""Links a group snap and breaks the relationship.
:param array: the array serial
@ -676,6 +684,7 @@ class VMAXProvision(object):
:param target_group_name: the target group name
:param snap_name: the snapshot name
:param extra_specs: extra specifications
:param list_volume_pairs: the list of volume pairs
:param delete_snapshot: delete snapshot flag
"""
LOG.debug("Linking Snap Vx snapshot: source group: %(srcGroup)s "
@ -683,66 +692,24 @@ class VMAXProvision(object):
{'srcGroup': source_group_name,
'tgtGroup': target_group_name})
# Link the snapshot
self.rest.modify_storagegroup_snap(
array, source_group_name, target_group_name, snap_name,
extra_specs, link=True)
self.rest.modify_volume_snap(
array, None, None, snap_name, extra_specs, link=True,
list_volume_pairs=list_volume_pairs)
# Unlink the snapshot
LOG.debug("Unlinking Snap Vx snapshot: source group: %(srcGroup)s "
"targetGroup: %(tgtGroup)s.",
{'srcGroup': source_group_name,
'tgtGroup': target_group_name})
self._unlink_group(array, source_group_name,
target_group_name, snap_name, extra_specs)
self._unlink_volume(array, None, None, snap_name, extra_specs,
list_volume_pairs=list_volume_pairs)
# Delete the snapshot if necessary
if delete_snapshot:
LOG.debug("Deleting Snap Vx snapshot: source group: %(srcGroup)s "
"snapshot: %(snap_name)s.",
{'srcGroup': source_group_name,
'snap_name': snap_name})
self.rest.delete_storagegroup_snap(array, snap_name,
source_group_name)
def _unlink_group(
self, array, source_group_name, target_group_name, snap_name,
extra_specs):
"""Unlink a target group from it's source group.
:param array: the array serial number
:param source_group_name: the source group name
:param target_group_name: the target device name
:param snap_name: the snap name
:param extra_specs: extra specifications
:returns: return code
"""
def _unlink_grp():
"""Called at an interval until the synchronization is finished.
:raises: loopingcall.LoopingCallDone
"""
retries = kwargs['retries']
try:
kwargs['retries'] = retries + 1
if not kwargs['modify_grp_snap_success']:
self.rest.modify_storagegroup_snap(
array, source_group_name, target_group_name,
snap_name, extra_specs, unlink=True)
kwargs['modify_grp_snap_success'] = True
except exception.VolumeBackendAPIException:
pass
if kwargs['retries'] > UNLINK_RETRIES:
LOG.error("_unlink_grp failed after %(retries)d "
"tries.", {'retries': retries})
raise loopingcall.LoopingCallDone(retvalue=30)
if kwargs['modify_grp_snap_success']:
raise loopingcall.LoopingCallDone()
kwargs = {'retries': 0,
'modify_grp_snap_success': False}
timer = loopingcall.FixedIntervalLoopingCall(_unlink_grp)
rc = timer.start(interval=UNLINK_INTERVAL).wait()
return rc
source_devices = [a for a, b in list_volume_pairs]
self.delete_volume_snap(array, snap_name, source_devices)
def enable_group_replication(self, array, storagegroup_name,
rdf_group_num, extra_specs, establish=False):
@ -799,15 +766,19 @@ class VMAXProvision(object):
:param rdf_group_num: the rdf group number
:param extra_specs: the extra specifications
"""
action = "Split"
LOG.debug("Splitting remote replication for group %(sg)s",
{'sg': storagegroup_name})
self.rest.modify_storagegroup_rdf(
array, storagegroup_name, rdf_group_num, action, extra_specs)
LOG.debug("Deleting remote replication for group %(sg)s",
{'sg': storagegroup_name})
self.rest.delete_storagegroup_rdf(
array, storagegroup_name, rdf_group_num)
group_details = self.rest.get_storage_group_rep(
array, storagegroup_name)
if (group_details and group_details.get('rdf')
and group_details['rdf'] is True):
action = "Split"
LOG.debug("Splitting remote replication for group %(sg)s",
{'sg': storagegroup_name})
self.rest.modify_storagegroup_rdf(
array, storagegroup_name, rdf_group_num, action, extra_specs)
LOG.debug("Deleting remote replication for group %(sg)s",
{'sg': storagegroup_name})
self.rest.delete_storagegroup_rdf(
array, storagegroup_name, rdf_group_num)
def revert_volume_snapshot(self, array, source_device_id,
snap_name, extra_specs):

View File

@ -1541,7 +1541,8 @@ class VMAXRest(object):
def modify_volume_snap(self, array, source_id, target_id, snap_name,
extra_specs, link=False, unlink=False,
rename=False, new_snap_name=None, restore=False):
rename=False, new_snap_name=None, restore=False,
list_volume_pairs=None):
"""Modify a snapvx snapshot
:param array: the array serial number
@ -1554,9 +1555,9 @@ class VMAXRest(object):
:param rename: Flag to indicate action = Rename
:param new_snap_name: Optional new snapshot name
:param restore: Flag to indicate action = Restore
:param list_volume_pairs: list of volume pairs to link, optional
"""
action = None
operation = ''
action, operation, payload = '', '', {}
if link:
action = "Link"
elif unlink:
@ -1575,8 +1576,16 @@ class VMAXRest(object):
"star": 'false', "force": 'false'}
elif action in ('Link', 'Unlink'):
operation = 'Modify snapVx relationship to target'
payload = {"deviceNameListSource": [{"name": source_id}],
"deviceNameListTarget": [{"name": target_id}],
src_list, tgt_list = [], []
if list_volume_pairs:
for a, b in list_volume_pairs:
src_list.append({'name': a})
tgt_list.append({'name': b})
else:
src_list.append({'name': source_id})
tgt_list.append({'name': target_id})
payload = {"deviceNameListSource": src_list,
"deviceNameListTarget": tgt_list,
"copy": 'true', "action": action,
"star": 'false', "force": 'false',
"exact": 'false', "remote": 'false',
@ -1595,19 +1604,22 @@ class VMAXRest(object):
self.wait_for_job(operation, status_code, job, extra_specs)
def delete_volume_snap(self, array, snap_name,
source_device_id, restored=False):
"""Delete the snapshot of a volume.
source_device_ids, restored=False):
"""Delete the snapshot of a volume or volumes.
:param array: the array serial number
:param snap_name: the name of the snapshot
:param source_device_id: the source device id
:param source_device_ids: the source device ids
:param restored: Flag to indicate terminate restore session
"""
device_list = []
if not isinstance(source_device_ids, list):
source_device_ids = [source_device_ids]
for dev in source_device_ids:
device_list.append({"name": dev})
payload = {"deviceNameListSource": device_list}
if restored:
payload = {"deviceNameListSource": [{"name": source_device_id}],
"restore": True}
else:
payload = {"deviceNameListSource": [{"name": source_device_id}]}
payload.update({"restore": True})
return self.delete_resource(
array, REPLICATION, 'snapshot', snap_name, payload=payload,
private='/private')
@ -2108,7 +2120,6 @@ class VMAXRest(object):
:param storagegroup_name: the storage group name
:returns: volume_list
"""
volume_list = None
params = {"storageGroupId": storagegroup_name}
volume_list = self.get_volume_list(array, params)
@ -2134,50 +2145,6 @@ class VMAXRest(object):
self.wait_for_job('Create storage group snapVx', status_code,
job, extra_specs)
def modify_storagegroup_snap(
self, array, source_sg_id, target_sg_id, snap_name,
extra_specs, link=False, unlink=False):
"""Link or unlink a snapVx to or from a target storagegroup.
:param array: the array serial number
:param source_sg_id: the source device id
:param target_sg_id: the target device id
:param snap_name: the snapshot name
:param extra_specs: extra specifications
:param link: Flag to indicate action = Link
:param unlink: Flag to indicate action = Unlink
"""
payload = ''
if link:
payload = {"link": {"linkStorageGroupName": target_sg_id,
"copy": "true"},
"action": "Link"}
elif unlink:
payload = {"unlink": {"unlinkStorageGroupName": target_sg_id},
"action": "Unlink"}
resource_name = ('%(sg_name)s/snapshot/%(snap_id)s/generation/0'
% {'sg_name': source_sg_id, 'snap_id': snap_name})
status_code, job = self.modify_resource(
array, REPLICATION, 'storagegroup', payload,
resource_name=resource_name)
self.wait_for_job('Modify storagegroup snapVx relationship to target',
status_code, job, extra_specs)
def delete_storagegroup_snap(self, array, snap_name, source_sg_id):
"""Delete the snapshot of a storagegroup.
:param array: the array serial number
:param snap_name: the name of the snapshot
:param source_sg_id: the source device id
"""
resource_name = ('%(sg_name)s/snapshot/%(snap_id)s/generation/0'
% {'sg_name': source_sg_id, 'snap_id': snap_name})
return self.delete_resource(
array, REPLICATION, 'storagegroup', resource_name)
def get_storagegroup_rdf_details(self, array, storagegroup_name,
rdf_group_num):
"""Get the remote replication details of a storage group.

View File

@ -583,18 +583,29 @@ class VMAXUtils(object):
:param status: string value reflects the status of the member volume
:returns: volume_model_updates - updated volumes
"""
LOG.info(
"Updating status for group: %(id)s.",
{'id': group_id})
LOG.info("Updating status for group: %(id)s.", {'id': group_id})
if volumes:
for volume in volumes:
volume_model_updates.append({'id': volume.id,
'status': status})
else:
LOG.info("No volume found for group: %(cg)s.",
{'cg': group_id})
LOG.info("No volume found for group: %(cg)s.", {'cg': group_id})
return volume_model_updates
@staticmethod
def get_grp_volume_model_update(volume, volume_dict, group_id):
"""Create and return the volume model update on creation.
:param volume: volume object
:param volume_dict: the volume dict
:param group_id: consistency group id
:returns: model_update
"""
LOG.info("Updating status for group: %(id)s.", {'id': group_id})
model_update = ({'id': volume.id, 'status': 'available',
'provider_location': six.text_type(volume_dict)})
return model_update
@staticmethod
def update_extra_specs(extraspecs):
"""Update extra specs.
@ -619,39 +630,21 @@ class VMAXUtils(object):
" the provided extra_specs.")
return extraspecs
@staticmethod
def get_intervals_retries_dict(interval, retries):
"""Get the default intervals and retries.
:param interval: Interval in seconds between retries
:param retries: Retry count
:returns: default_dict
"""
default_dict = {}
default_dict[INTERVAL] = interval
default_dict[RETRIES] = retries
return default_dict
def get_volume_group_utils(self, group, interval, retries):
"""Standard utility for generic volume groups.
:param group: the generic volume group object to be created
:param interval: Interval in seconds between retries
:param retries: Retry count
:returns: array, extra specs dict list
:returns: array, intervals_retries_dict
:raises: VolumeBackendAPIException
"""
arrays = set()
extraspecs_dict_list = []
# Check if it is a generic volume group instance
if isinstance(group, Group):
for volume_type in group.volume_types:
extraspecs_dict = (
self._update_extra_specs_list(
volume_type.extra_specs,
volume_type.id, interval, retries))
extraspecs_dict_list.append(extraspecs_dict)
arrays.add(extraspecs_dict[EXTRA_SPECS][ARRAY])
extra_specs = self.update_extra_specs(volume_type.extra_specs)
arrays.add(extra_specs[ARRAY])
else:
msg = (_("Unable to get volume type ids."))
LOG.error(msg)
@ -669,25 +662,8 @@ class VMAXUtils(object):
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
array = arrays.pop()
return array, extraspecs_dict_list
def _update_extra_specs_list(self, extraspecs, volumetype_id,
interval, retries):
"""Update the extra specs list.
:param extraspecs: extraspecs
:param volumetype_Id: volume type identifier
:param interval: Interval in seconds between retries
:param retries: Retry count
:returns: extraspecs_dict_list
"""
extraspecs_dict = {}
extraspecs = self.update_extra_specs(extraspecs)
extraspecs = self._update_intervals_and_retries(
extraspecs, interval, retries)
extraspecs_dict["volumeTypeId"] = volumetype_id
extraspecs_dict[EXTRA_SPECS] = extraspecs
return extraspecs_dict
intervals_retries_dict = {INTERVAL: interval, RETRIES: retries}
return array, intervals_retries_dict
def update_volume_group_name(self, group):
"""Format id and name consistency group.
@ -704,23 +680,6 @@ class VMAXUtils(object):
group_name += group.id
return group_name
@staticmethod
def _update_intervals_and_retries(extra_specs, interval, retries):
"""Updates the extraSpecs with intervals and retries values.
:param extra_specs:
:param interval: Interval in seconds between retries
:param retries: Retry count
:returns: Updated extra_specs
"""
extra_specs[INTERVAL] = interval
LOG.debug("The interval is set at: %(intervalInSecs)s.",
{'intervalInSecs': interval})
extra_specs[RETRIES] = retries
LOG.debug("Retries are set at: %(retries)s.",
{'retries': retries})
return extra_specs
@staticmethod
def add_legacy_pools(pools):
"""Add legacy pools to allow extending a volume after upgrade.
@ -781,6 +740,7 @@ class VMAXUtils(object):
msg = (_('Replication status should be %s for '
'replication-enabled group.')
% fields.ReplicationStatus.ENABLED)
LOG.error(msg)
raise exception.InvalidInput(reason=msg)
else:
LOG.debug('Replication is not enabled on group %s, '