PowerMax Driver - Temporary snapshot enhancements

Improvements to unlinking of volume snapshots and
deletion of temporary snapshots created during
volume cloning operations. Including:

1. Merging of sync and clone check functionality.
2. Removal of snapvx unlink limit use during snapshot unlinking.
3. Adding early exit to volume delete, group delete and retype
in cases where volume snapshots still exist after attempting
unlink and deletion of snapvx sessions.

Change-Id: I60f399eccb32daf5127c64108d5331cf798b2600
This commit is contained in:
odonos12 2020-11-18 09:31:06 +00:00 committed by Helen Walsh
parent 47cdd944b3
commit 1a267bf6ff
4 changed files with 676 additions and 470 deletions

View File

@ -224,10 +224,10 @@ class PowerMaxCommonTest(test.TestCase):
model_update = self.common.create_volume(self.data.test_volume)
self.assertEqual(ref_model_update, model_update)
@mock.patch.object(common.PowerMaxCommon, '_clone_check')
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
@mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata',
return_value='')
def test_create_volume_from_snapshot(self, mck_meta, mck_clone_chk):
def test_create_volume_from_snapshot(self, mck_meta, mck_cleanup_snaps):
ref_model_update = ({'provider_location': six.text_type(
deepcopy(self.data.provider_location_snapshot))})
model_update = self.common.create_volume_from_snapshot(
@ -362,10 +362,14 @@ class PowerMaxCommonTest(test.TestCase):
mck_configure.assert_called_once()
mck_resume.assert_called_once()
@mock.patch.object(common.PowerMaxCommon, '_clone_check')
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
@mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata',
return_value='')
def test_cloned_volume(self, mck_meta, mck_clone_chk):
def test_cloned_volume(self, mck_meta, mck_cleanup_snaps):
array = self.data.array
test_volume = self.data.test_clone_volume
source_device_id = self.data.device_id
extra_specs = self.common._initial_setup(test_volume)
ref_model_update = ({'provider_location': six.text_type(
self.data.provider_location_clone)})
model_update = self.common.create_cloned_volume(
@ -373,18 +377,55 @@ class PowerMaxCommonTest(test.TestCase):
self.assertEqual(
ast.literal_eval(ref_model_update['provider_location']),
ast.literal_eval(model_update['provider_location']))
mck_cleanup_snaps.assert_called_once_with(
array, source_device_id, extra_specs)
def test_delete_volume(self):
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=list())
def test_delete_volume(self, mck_get_snaps):
with mock.patch.object(self.common, '_delete_volume') as mock_delete:
self.common.delete_volume(self.data.test_volume)
mock_delete.assert_called_once_with(self.data.test_volume)
@mock.patch.object(common.PowerMaxCommon, '_clone_check')
@mock.patch.object(common.PowerMaxCommon, '_delete_from_srp')
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snap_info',
return_value=tpd.PowerMaxData.volume_snap_vx)
def test_delete_volume_fail_if_active_snapshots(
self, mck_get_snaps, mck_cleanup, mck_delete):
array = self.data.array
test_volume = self.data.test_volume
device_id = self.data.device_id
extra_specs = self.common._initial_setup(test_volume)
self.assertRaises(exception.VolumeBackendAPIException,
self.common._delete_volume, test_volume)
mck_cleanup.assert_called_once_with(array, device_id, extra_specs)
mck_delete.assert_not_called()
@mock.patch.object(common.PowerMaxCommon, '_delete_from_srp')
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
@mock.patch.object(
rest.PowerMaxRest, 'find_snap_vx_sessions',
return_value=('', tpd.PowerMaxData.snap_tgt_session_cm_enabled))
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=list())
def test_delete_volume_fail_if_snapvx_target(
self, mck_get_snaps, mck_tgt_snap, mck_cleanup, mck_delete):
array = self.data.array
test_volume = self.data.test_volume
device_id = self.data.device_id
extra_specs = self.common._initial_setup(test_volume)
self.assertRaises(exception.VolumeBackendAPIException,
self.common._delete_volume, test_volume)
mck_cleanup.assert_called_once_with(array, device_id, extra_specs)
mck_delete.assert_not_called()
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
@mock.patch.object(
common.PowerMaxCommon, 'get_snapshot_metadata',
return_value={'snap-meta-key-1': 'snap-meta-value-1',
'snap-meta-key-2': 'snap-meta-value-2'})
def test_create_snapshot(self, mck_meta, mck_clone_chk):
def test_create_snapshot(self, mck_meta, mck_cleanup_snaps):
ref_model_update = (
{'provider_location': six.text_type(self.data.snap_location),
'metadata': {'snap-meta-key-1': 'snap-meta-value-1',
@ -1171,8 +1212,8 @@ class PowerMaxCommonTest(test.TestCase):
volume, connector, extra_specs)
self.assertEqual('NONE', masking_view_dict[utils.WORKLOAD])
@mock.patch.object(common.PowerMaxCommon, '_clone_check')
def test_create_cloned_volume(self, mck_clone_chk):
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_create_cloned_volume(self, mck_cleanup_snaps):
volume = self.data.test_clone_volume
source_volume = self.data.test_volume
extra_specs = self.data.extra_specs
@ -1182,8 +1223,8 @@ class PowerMaxCommonTest(test.TestCase):
volume, source_volume, extra_specs))
self.assertEqual(ref_response, (clone_dict, rep_update, rep_info_dict))
@mock.patch.object(common.PowerMaxCommon, '_clone_check')
def test_create_cloned_volume_is_snapshot(self, mck_clone_chk):
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_create_cloned_volume_is_snapshot(self, mck_cleanup_snaps):
volume = self.data.test_snapshot
source_volume = self.data.test_volume
extra_specs = self.data.extra_specs
@ -1193,8 +1234,8 @@ class PowerMaxCommonTest(test.TestCase):
volume, source_volume, extra_specs, True, False))
self.assertEqual(ref_response, (clone_dict, rep_update, rep_info_dict))
@mock.patch.object(common.PowerMaxCommon, '_clone_check')
def test_create_cloned_volume_from_snapshot(self, mck_clone_chk):
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_create_cloned_volume_from_snapshot(self, mck_cleanup_snaps):
volume = self.data.test_clone_volume
source_volume = self.data.test_snapshot
extra_specs = self.data.extra_specs
@ -1232,7 +1273,7 @@ class PowerMaxCommonTest(test.TestCase):
'_find_device_on_array',
return_value=None)
@mock.patch.object(common.PowerMaxCommon,
'_clone_check')
'_cleanup_device_snapvx')
def test_create_cloned_volume_source_not_found(
self, mock_check, mock_device):
volume = self.data.test_clone_volume
@ -1300,16 +1341,18 @@ class PowerMaxCommonTest(test.TestCase):
self.common._create_snapshot,
array, snapshot, source_device_id, extra_specs)
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=list())
@mock.patch.object(masking.PowerMaxMasking,
'remove_vol_from_storage_group')
def test_delete_volume_from_srp(self, mock_rm):
def test_delete_volume_from_srp(self, mock_rm, mock_get_snaps):
array = self.data.array
device_id = self.data.device_id
volume_name = self.data.test_volume.name
ref_extra_specs = deepcopy(self.data.extra_specs_intervals_set)
ref_extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f
volume = self.data.test_volume
with mock.patch.object(self.common, '_sync_check'):
with mock.patch.object(self.common, '_cleanup_device_snapvx'):
with mock.patch.object(
self.common, '_delete_from_srp') as mock_delete:
self.common._delete_volume(volume)
@ -2062,8 +2105,8 @@ class PowerMaxCommonTest(test.TestCase):
return_value=(False, False, False))
@mock.patch.object(common.PowerMaxCommon,
'_remove_vol_and_cleanup_replication')
@mock.patch.object(common.PowerMaxCommon, '_clone_check')
def test_unmanage_success(self, mck_clone, mock_rm, mck_sess):
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_unmanage_success(self, mck_cleanup_snaps, mock_rm, mck_sess):
volume = self.data.test_volume
with mock.patch.object(self.rest, 'rename_volume') as mock_rename:
self.common.unmanage(volume)
@ -2090,8 +2133,8 @@ class PowerMaxCommonTest(test.TestCase):
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(True, True, False))
@mock.patch.object(common.PowerMaxCommon, '_clone_check')
def test_unmanage_temp_snapshot_links(self, mck_clone, mck_sess):
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_unmanage_temp_snapshot_links(self, mck_cleanup_snaps, mck_sess):
volume = self.data.test_volume
self.assertRaises(exception.VolumeIsBusy, self.common.unmanage,
volume)
@ -2251,6 +2294,62 @@ class PowerMaxCommonTest(test.TestCase):
target_slo, target_workload, target_extra_specs)
self.assertTrue(success)
@mock.patch.object(utils.PowerMaxUtils, 'get_rep_config',
return_value=tpd.PowerMaxData.rep_config_metro)
@mock.patch.object(utils.PowerMaxUtils, 'is_replication_enabled',
side_effect=[False, True])
@mock.patch.object(common.PowerMaxCommon, '_validate_rdfg_status')
@mock.patch.object(rest.PowerMaxRest, 'get_slo_list', return_value=[])
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=[{'snapshotName': 'name',
'linkedDevices': 'details'}])
def test_migrate_to_metro_exception_on_linked_snapshot_source(
self, mck_get, mck_slo, mck_validate, mck_rep, mck_config):
array_id = self.data.array
volume = self.data.test_volume
device_id = self.data.device_id
srp = self.data.srp
target_slo = self.data.slo_silver
target_workload = self.data.workload
volume_name = volume.name
target_extra_specs = self.data.rep_extra_specs_rep_config_metro
new_type = {'extra_specs': target_extra_specs}
extra_specs = self.data.extra_specs
self.assertRaises(
exception.VolumeBackendAPIException, self.common._migrate_volume,
array_id, volume, device_id, srp, target_slo, target_workload,
volume_name, new_type, extra_specs)
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
@mock.patch.object(utils.PowerMaxUtils, 'get_rep_config',
return_value=tpd.PowerMaxData.rep_config_metro)
@mock.patch.object(utils.PowerMaxUtils, 'is_replication_enabled',
side_effect=[False, True])
@mock.patch.object(common.PowerMaxCommon, '_validate_rdfg_status')
@mock.patch.object(rest.PowerMaxRest, 'get_slo_list', return_value=[])
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=[{'snapshotName': 'name'}])
@mock.patch.object(rest.PowerMaxRest, 'find_snap_vx_sessions',
return_value=('', {'source_vol_id': 'source_vol_id',
'snap_name': 'snap_name'}))
def test_migrate_to_metro_exception_on_snapshot_target(
self, mck_find, mck_snap, mck_slo, mck_validate, mck_rep,
mck_config, mck_cleanup):
array_id = self.data.array
volume = self.data.test_volume
device_id = self.data.device_id
srp = self.data.srp
target_slo = self.data.slo_silver
target_workload = self.data.workload
volume_name = volume.name
target_extra_specs = self.data.rep_extra_specs_rep_config_metro
new_type = {'extra_specs': target_extra_specs}
extra_specs = self.data.extra_specs
self.assertRaises(
exception.VolumeBackendAPIException, self.common._migrate_volume,
array_id, volume, device_id, srp, target_slo, target_workload,
volume_name, new_type, extra_specs)
@mock.patch.object(common.PowerMaxCommon,
'_post_retype_srdf_protect_storage_group',
return_value=(True, True, True))
@ -2821,8 +2920,10 @@ class PowerMaxCommonTest(test.TestCase):
self.common._update_group_promotion,
group, add_vols, remove_vols)
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=list())
@mock.patch.object(volume_utils, 'is_group_a_type', return_value=False)
def test_delete_group(self, mock_check):
def test_delete_group(self, mock_check, mck_snaps):
group = self.data.test_group_1
volumes = [self.data.test_volume]
context = None
@ -2837,8 +2938,10 @@ class PowerMaxCommonTest(test.TestCase):
context, group, volumes)
self.assertEqual(ref_model_update, model_update)
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=list())
@mock.patch.object(volume_utils, 'is_group_a_type', return_value=False)
def test_delete_group_success(self, mock_check):
def test_delete_group_success(self, mock_check, mck_get_snaps):
group = self.data.test_group_1
volumes = []
ref_model_update = {'status': fields.GroupStatus.DELETED}
@ -2850,7 +2953,9 @@ class PowerMaxCommonTest(test.TestCase):
model_update, __ = self.common._delete_group(group, volumes)
self.assertEqual(ref_model_update, model_update)
def test_delete_group_already_deleted(self):
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=list())
def test_delete_group_already_deleted(self, mck_get_snaps):
group = self.data.test_group_failed
ref_model_update = {'status': fields.GroupStatus.DELETED}
volumes = []
@ -2859,10 +2964,13 @@ class PowerMaxCommonTest(test.TestCase):
model_update, __ = self.common._delete_group(group, volumes)
self.assertEqual(ref_model_update, model_update)
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=list())
@mock.patch.object(volume_utils, 'is_group_a_type', return_value=False)
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True)
def test_delete_group_failed(self, mock_check, mock_type_check):
def test_delete_group_failed(
self, mock_check, mock_type_check, mck_get_snaps):
group = self.data.test_group_1
volumes = []
ref_model_update = {'status': fields.GroupStatus.ERROR_DELETING}
@ -2873,6 +2981,8 @@ class PowerMaxCommonTest(test.TestCase):
group, volumes)
self.assertEqual(ref_model_update, model_update)
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=list())
@mock.patch.object(volume_utils, 'is_group_a_type', return_value=False)
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True)
@ -2885,15 +2995,38 @@ class PowerMaxCommonTest(test.TestCase):
return_value= tpd.PowerMaxData.device_id)
@mock.patch.object(masking.PowerMaxMasking,
'remove_volumes_from_storage_group')
def test_delete_group_clone_check(
def test_delete_group_cleanup_snapvx(
self, mock_rem, mock_find, mock_mems, mock_vols, mock_chk1,
mock_chk2):
mock_chk2, mck_get_snaps):
group = self.data.test_group_1
volumes = [self.data.test_volume_group_member]
with mock.patch.object(
self.common, '_clone_check') as mock_clone_chk:
self.common, '_cleanup_device_snapvx') as mock_cleanup_snapvx:
self.common._delete_group(group, volumes)
mock_clone_chk.assert_called_once()
mock_cleanup_snapvx.assert_called_once()
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=[{'snapshotName': 'name'}])
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_delete_group_with_volumes_exception_on_remaining_snapshots(
self, mck_cleanup, mck_get):
group = self.data.test_group_1
volumes = [self.data.test_volume_group_member]
self.assertRaises(exception.VolumeBackendAPIException,
self.common._delete_group, group, volumes)
@mock.patch.object(rest.PowerMaxRest, 'find_snap_vx_sessions',
return_value=('', {'source_vol_id': 'id',
'snap_name': 'name'}))
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=None)
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_delete_group_with_volumes_exception_on_target_links(
self, mck_cleanup, mck_get, mck_find):
group = self.data.test_group_1
volumes = [self.data.test_volume_group_member]
self.assertRaises(exception.VolumeBackendAPIException,
self.common._delete_group, group, volumes)
@mock.patch.object(rest.PowerMaxRest, 'delete_storage_group')
@mock.patch.object(common.PowerMaxCommon, '_failover_replication',
@ -3104,12 +3237,12 @@ class PowerMaxCommonTest(test.TestCase):
self.common.unmanage_snapshot(self.data.test_snapshot_manage)
mock_mod.assert_called_once()
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
@mock.patch.object(rest.PowerMaxRest, 'modify_volume_snap')
def test_unmanage_snapshot_no_sync_check(self, mock_mod, mock_sync):
def test_unmanage_snapshot_no_snapvx_cleanup(self, mock_mod, mock_cleanup):
self.common.unmanage_snapshot(self.data.test_snapshot_manage)
mock_mod.assert_called_once()
mock_sync.assert_not_called()
mock_cleanup.assert_not_called()
@mock.patch.object(utils.PowerMaxUtils, 'is_volume_failed_over',
return_value=True)
@ -3133,7 +3266,7 @@ class PowerMaxCommonTest(test.TestCase):
@mock.patch.object(provision.PowerMaxProvision, 'delete_volume_snap')
@mock.patch.object(provision.PowerMaxProvision, 'is_restore_complete',
return_value=True)
@mock.patch.object(common.PowerMaxCommon, '_clone_check')
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
@mock.patch.object(provision.PowerMaxProvision, 'revert_volume_snapshot')
def test_revert_to_snapshot(self, mock_revert, mock_clone,
mock_complete, mock_delete, mock_parse):
@ -3388,8 +3521,8 @@ class PowerMaxCommonTest(test.TestCase):
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(None, False, None))
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
def test_extend_vol_validation_checks_success(self, mck_sync, mck_rep):
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_extend_vol_validation_checks_success(self, mck_cleanup, mck_rep):
volume = self.data.test_volume
array = self.data.array
device_id = self.data.device_id
@ -3400,8 +3533,8 @@ class PowerMaxCommonTest(test.TestCase):
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(None, False, None))
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
def test_extend_vol_val_check_no_device(self, mck_sync, mck_rep):
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_extend_vol_val_check_no_device(self, mck_cleanup, mck_rep):
volume = self.data.test_volume
array = self.data.array
device_id = None
@ -3414,8 +3547,8 @@ class PowerMaxCommonTest(test.TestCase):
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(None, True, None))
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
def test_extend_vol_val_check_snap_src(self, mck_sync, mck_rep):
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_extend_vol_val_check_snap_src(self, mck_cleanup, mck_rep):
volume = self.data.test_volume
array = self.data.array
device_id = self.data.device_id
@ -3429,8 +3562,8 @@ class PowerMaxCommonTest(test.TestCase):
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(None, False, None))
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
def test_extend_vol_val_check_wrong_size(self, mck_sync, mck_rep):
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_extend_vol_val_check_wrong_size(self, mck_cleanup, mck_rep):
volume = self.data.test_volume
array = self.data.array
device_id = self.data.device_id
@ -3585,103 +3718,6 @@ class PowerMaxCommonTest(test.TestCase):
port = self.common._get_unisphere_port()
self.assertEqual(ref_port, port)
def test_sync_check(self):
array = self.data.array
device_id = self.data.device_id
extra_specs = self.data.extra_specs
with mock.patch.object(self.common, '_do_sync_check') as mck_sync:
self.common._sync_check(array, device_id, extra_specs, False,
self.data.device_id2)
mck_sync.assert_called_with(array, self.data.device_id,
extra_specs, False)
mck_sync.reset_mock()
with mock.patch.object(self.common, '_get_target_source_device',
return_value=self.data.device_id3):
self.common._sync_check(array, device_id, extra_specs, True)
mck_sync.assert_called_with(array, self.data.device_id,
extra_specs, True)
mck_sync.reset_mock()
self.common._sync_check(array, device_id, extra_specs)
mck_sync.assert_called_with(array, device_id, extra_specs, False)
@mock.patch.object(common.PowerMaxCommon,
'_unlink_targets_and_delete_temp_snapvx')
@mock.patch.object(rest.PowerMaxRest, 'find_snap_vx_sessions',
return_value=(tpd.PowerMaxData.snap_src_sessions,
tpd.PowerMaxData.snap_tgt_session))
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(True, True, False))
def test_do_sync_check(self, mck_rep, mck_find, mck_unlink):
array = self.data.array
device_id = self.data.device_id
extra_specs = self.data.extra_specs
self.common._do_sync_check(array, device_id, extra_specs)
self.assertEqual(3, mck_unlink.call_count)
@mock.patch.object(provision.PowerMaxProvision, 'delete_temp_volume_snap')
@mock.patch.object(provision.PowerMaxProvision, 'unlink_snapvx_tgt_volume')
def test_unlink_targets_and_delete_temp_snapvx_legacy(
self, mck_break, mck_del):
array = self.data.array
extra_specs = self.data.extra_specs
session = deepcopy(self.data.snap_tgt_session_cm_enabled)
legacy_snap_name = 'EMC_SMI'
session['snap_name'] = legacy_snap_name
source = session['source_vol_id']
snap_id = session['snapid']
target = session['target_vol_id']
self.common._unlink_targets_and_delete_temp_snapvx(
session, array, extra_specs)
mck_break.assert_called_with(array, target, source, legacy_snap_name,
extra_specs, snap_id, True)
mck_del.assert_called_once_with(
array, legacy_snap_name, source, snap_id)
@mock.patch.object(provision.PowerMaxProvision, 'delete_temp_volume_snap')
@mock.patch.object(provision.PowerMaxProvision, 'unlink_snapvx_tgt_volume')
def test_unlink_targets_and_delete_temp_snapvx_is_temp(
self, mck_break, mck_del):
array = self.data.array
extra_specs = self.data.extra_specs
session = deepcopy(self.data.snap_tgt_session_cm_enabled)
snap_name = session['snap_name']
source = session['source_vol_id']
snap_id = session['snapid']
target = session['target_vol_id']
self.common._unlink_targets_and_delete_temp_snapvx(
session, array, extra_specs)
mck_break.assert_called_with(array, target, source, snap_name,
extra_specs, snap_id, True)
mck_del.assert_called_once_with(array, snap_name, source, snap_id)
def test_unlink_targets_and_delete_temp_snapvx_no_snap_name_found(self):
array = self.data.array
extra_specs = self.data.extra_specs
session = dict()
self.assertRaises(exception.VolumeBackendAPIException,
self.common._unlink_targets_and_delete_temp_snapvx,
session, array, extra_specs)
@mock.patch.object(provision.PowerMaxProvision, 'delete_temp_volume_snap')
@mock.patch.object(provision.PowerMaxProvision, 'unlink_snapvx_tgt_volume')
def test_unlink_targets_and_delete_temp_snapvx_not_legacy_not_temp(
self, mck_break, mck_del):
array = self.data.array
extra_specs = self.data.extra_specs
session = deepcopy(self.data.snap_tgt_session_cm_enabled)
session['snap_name'] = 'not_leg_or_tmp'
snap_name = session['snap_name']
source = session['source_vol_id']
snap_id = session['snapid']
target = session['target_vol_id']
self.common._unlink_targets_and_delete_temp_snapvx(
session, array, extra_specs)
mck_break.assert_called_with(array, target, source, snap_name,
extra_specs, snap_id, True)
mck_del.assert_not_called()
@mock.patch.object(rest.PowerMaxRest, 'find_snap_vx_sessions',
return_value=(None, tpd.PowerMaxData.snap_tgt_session))
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
@ -3692,83 +3728,6 @@ class PowerMaxCommonTest(test.TestCase):
src_device = self.common._get_target_source_device(array, tgt_device)
self.assertEqual(src_device, self.data.device_id)
@mock.patch.object(common.PowerMaxCommon, '_delete_valid_snapshot')
@mock.patch.object(rest.PowerMaxRest, 'find_snap_vx_sessions',
return_value=(tpd.PowerMaxData.snap_src_sessions,
tpd.PowerMaxData.snap_tgt_session))
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(True, True, False))
def test_clone_check(self, mck_rep, mck_find, mck_del):
array = self.data.array
device_id = self.data.device_id
extra_specs = self.data.extra_specs
self.common.snapvx_unlink_limit = 3
self.common._clone_check(array, device_id, extra_specs)
self.assertEqual(3, mck_del.call_count)
@mock.patch.object(
common.PowerMaxCommon, '_unlink_targets_and_delete_temp_snapvx')
@mock.patch.object(rest.PowerMaxRest, 'find_snap_vx_sessions',
return_value=(tpd.PowerMaxData.snap_src_sessions,
tpd.PowerMaxData.snap_tgt_session))
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(True, True, False))
def test_clone_check_force_unlink(self, mck_rep, mck_find, mck_del):
array = self.data.array
device_id = self.data.device_id
extra_specs = self.data.extra_specs
self.common.snapvx_unlink_limit = 3
self.common._clone_check(
array, device_id, extra_specs, force_unlink=True)
self.assertEqual(3, mck_del.call_count)
@mock.patch.object(common.PowerMaxCommon,
'_unlink_targets_and_delete_temp_snapvx')
def test_delete_valid_snapshot(self, mck_unlink):
array = self.data.array
extra_specs = self.data.extra_specs
session = {'snap_name': 'EMC_SMI_TEST', 'expired': False}
self.common._delete_valid_snapshot(array, session, extra_specs)
mck_unlink.assert_called_with(session, array, extra_specs)
mck_unlink.reset_mock()
session = {'snap_name': 'temp-000AA-snapshot_for_clone',
'expired': True}
self.common._delete_valid_snapshot(array, session, extra_specs)
mck_unlink.assert_called_with(session, array, extra_specs)
mck_unlink.reset_mock()
session = {'snap_name': 'temp-000AA-snapshot_for_clone',
'expired': False}
self.common._delete_valid_snapshot(array, session, extra_specs)
mck_unlink.assert_not_called()
def test_delete_valid_snapshot_exception(self):
array = self.data.array
extra_specs = self.data.extra_specs
session = {'snap_name': 'temp-000AA-snapshot_for_clone',
'expired': True}
with mock.patch.object(
self.common, '_unlink_targets_and_delete_temp_snapvx',
side_effect=exception.VolumeBackendAPIException(
"404 temp-000AA-snapshot_for_clone does not exist")
) as mck_unlink:
self.common._delete_valid_snapshot(array, session, extra_specs)
mck_unlink.assert_called_with(session, array, extra_specs)
with mock.patch.object(
self.common, '_unlink_targets_and_delete_temp_snapvx',
side_effect=exception.VolumeBackendAPIException(
"500 internal server error")):
self.assertRaises(
exception.VolumeBackendAPIException,
self.common._unlink_targets_and_delete_temp_snapvx,
array, session, extra_specs)
@mock.patch.object(rest.PowerMaxRest, '_get_private_volume',
return_value=tpd.PowerMaxData.priv_vol_response_rep)
@mock.patch.object(rest.PowerMaxRest, 'get_array_model_info',
@ -4339,3 +4298,110 @@ class PowerMaxCommonTest(test.TestCase):
rep_driver_data)
self.assertIsNone(group_name)
mock_group.assert_not_called()
@mock.patch.object(
common.PowerMaxCommon, '_unlink_and_delete_temporary_snapshots')
@mock.patch.object(rest.PowerMaxRest, 'find_snap_vx_sessions',
return_value=(None, 'tgt_session'))
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(True, False, False))
def test_cleanup_device_snapvx(self, mck_is_rep, mck_find, mck_unlink):
array = self.data.array
device_id = self.data.device_id
extra_specs = self.data.extra_specs
self.common._cleanup_device_snapvx(array, device_id, extra_specs)
mck_unlink.assert_called_once_with('tgt_session', array, extra_specs)
@mock.patch.object(
common.PowerMaxCommon, '_unlink_and_delete_temporary_snapshots')
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(False, False, False))
def test_cleanup_device_snapvx_no_sessions(self, mck_is_rep, mck_unlink):
array = self.data.array
device_id = self.data.device_id
extra_specs = self.data.extra_specs
self.common._cleanup_device_snapvx(array, device_id, extra_specs)
mck_unlink.assert_not_called()
@mock.patch.object(common.PowerMaxCommon, '_delete_temp_snapshot')
@mock.patch.object(common.PowerMaxCommon, '_unlink_snapshot',
return_value=True)
def test_unlink_and_delete_temporary_snapshots_session_unlinked(
self, mck_unlink, mck_delete):
session = self.data.snap_tgt_session
array = self.data.array
extra_specs = self.data.extra_specs
self.common._unlink_and_delete_temporary_snapshots(
session, array, extra_specs)
mck_unlink.assert_called_once_with(session, array, extra_specs)
mck_delete.assert_called_once_with(session, array)
@mock.patch.object(common.PowerMaxCommon, '_delete_temp_snapshot')
@mock.patch.object(common.PowerMaxCommon, '_unlink_snapshot',
return_value=False)
def test_unlink_and_delete_temporary_snapshots_session_not_unlinked(
self, mck_unlink, mck_delete):
session = self.data.snap_tgt_session
array = self.data.array
extra_specs = self.data.extra_specs
self.common._unlink_and_delete_temporary_snapshots(
session, array, extra_specs)
mck_unlink.assert_called_once_with(session, array, extra_specs)
mck_delete.assert_not_called()
@mock.patch.object(provision.PowerMaxProvision, 'unlink_snapvx_tgt_volume')
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snap',
side_effect=[tpd.PowerMaxData.priv_snap_response.get(
'snapshotSrcs')[0], None])
def test_unlink_temp_snapshot(self, mck_get, mck_unlink):
array = self.data.array
extra_specs = self.data.extra_specs
session = self.data.snap_tgt_session
source = session.get('source_vol_id')
target = session.get('target_vol_id')
snap_name = session.get('snap_name')
snap_id = session.get('snapid')
loop = False
is_unlinked = self.common._unlink_snapshot(session, array, extra_specs)
mck_unlink.assert_called_once_with(
array, target, source, snap_name, extra_specs, snap_id, loop)
self.assertTrue(is_unlinked)
@mock.patch.object(provision.PowerMaxProvision, 'unlink_snapvx_tgt_volume')
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snap',
return_value=tpd.PowerMaxData.priv_snap_response.get(
'snapshotSrcs')[0])
def test_unlink_temp_snapshot_not_unlinked(self, mck_get, mck_unlink):
array = self.data.array
extra_specs = self.data.extra_specs
session = self.data.snap_tgt_session
source = session.get('source_vol_id')
target = session.get('target_vol_id')
snap_name = session.get('snap_name')
snap_id = session.get('snapid')
loop = False
is_unlinked = self.common._unlink_snapshot(session, array, extra_specs)
mck_unlink.assert_called_once_with(
array, target, source, snap_name, extra_specs, snap_id, loop)
self.assertFalse(is_unlinked)
@mock.patch.object(provision.PowerMaxProvision, 'delete_temp_volume_snap')
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snap',
return_value=dict())
def test_delete_temp_snapshot(self, mck_get, mck_delete):
session = self.data.snap_tgt_session
array = self.data.array
snap_name = session.get('snap_name')
source = session.get('source_vol_id')
snap_id = session.get('snapid')
self.common._delete_temp_snapshot(session, array)
mck_delete.assert_called_once_with(array, snap_name, source, snap_id)
@mock.patch.object(provision.PowerMaxProvision, 'delete_temp_volume_snap')
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snap',
return_value={'linkedDevices': 'details'})
def test_delete_temp_snapshot_is_linked(self, mck_get, mck_delete):
session = self.data.snap_tgt_session
array = self.data.array
self.common._delete_temp_snapshot(session, array)
mck_delete.assert_not_called()

View File

@ -235,9 +235,9 @@ class PowerMaxReplicationTest(test.TestCase):
@mock.patch.object(masking.PowerMaxMasking,
'remove_vol_from_storage_group')
@mock.patch.object(common.PowerMaxCommon, '_delete_from_srp')
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_cleanup_replication_source(
self, mck_sync, mock_del, mock_rm, mock_clean):
self, mck_cleanup, mock_del, mock_rm, mock_clean):
self.common._cleanup_replication_source(
self.data.array, self.data.test_volume, 'vol1',
{'device_id': self.data.device_id}, self.extra_specs)
@ -460,10 +460,10 @@ class PowerMaxReplicationTest(test.TestCase):
@mock.patch.object(common.PowerMaxCommon, '_failover_replication',
return_value=({}, {}))
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
@mock.patch.object(rest.PowerMaxRest, 'get_arrays_list',
return_value=['123'])
def test_failover_host_async(self, mck_arrays, mck_sync, mock_fg):
def test_failover_host_async(self, mck_arrays, mck_cleanup, mock_fg):
volumes = [self.data.test_volume]
extra_specs = deepcopy(self.extra_specs)
extra_specs['rep_mode'] = utils.REP_ASYNC
@ -928,8 +928,8 @@ class PowerMaxReplicationTest(test.TestCase):
return_value=(
tpd.PowerMaxData.provider_location_clone,
tpd.PowerMaxData.replication_update, {}))
@mock.patch.object(common.PowerMaxCommon, '_clone_check')
def test_cloned_rep_volume(self, mck_clone, mck_meta, mck_clone_chk):
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
def test_cloned_rep_volume(self, mck_cleanup, mck_meta, mck_clone_chk):
metadata = deepcopy(self.data.volume_metadata)
metadata['BackendID'] = self.data.rep_backend_id_sync
ref_model_update = {
@ -1069,7 +1069,7 @@ class PowerMaxReplicationTest(test.TestCase):
self.assertTrue(success)
@mock.patch.object(common.PowerMaxCommon, '_validate_rdfg_status')
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
@mock.patch.object(
common.PowerMaxCommon, 'get_volume_metadata', return_value='')
@mock.patch.object(
@ -1086,7 +1086,7 @@ class PowerMaxReplicationTest(test.TestCase):
'remote_array': tpd.PowerMaxData.remote_array},
tpd.PowerMaxData.rep_extra_specs, False))
def test_migrate_volume_success_no_rep_to_rep(
self, mck_configure, mck_retype, mck_protect, mck_get, mck_check,
self, mck_configure, mck_retype, mck_protect, mck_get, mck_cleanup,
mck_valid):
self.common.rep_config = {'mode': utils.REP_SYNC,
'array': self.data.array}
@ -1127,6 +1127,8 @@ class PowerMaxReplicationTest(test.TestCase):
self.data.rep_extra_specs, volume)
self.assertTrue(success)
@mock.patch.object(rest.PowerMaxRest, 'get_volume_snapshot_list',
return_value=list())
@mock.patch.object(utils.PowerMaxUtils, 'get_rep_config',
return_value=tpd.PowerMaxData.rep_config_async)
@mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata',
@ -1143,14 +1145,15 @@ class PowerMaxReplicationTest(test.TestCase):
tpd.PowerMaxData.rep_info_dict,
tpd.PowerMaxData.rep_extra_specs_mgmt,
True))
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
@mock.patch.object(common.PowerMaxCommon, '_cleanup_device_snapvx')
@mock.patch.object(common.PowerMaxCommon, 'break_rdf_device_pair_session',
return_value=(tpd.PowerMaxData.rep_extra_specs_mgmt,
True))
@mock.patch.object(common.PowerMaxCommon, '_validate_rdfg_status')
def test_migrate_volume_success_rep_to_rep(
self, mck_valid, mck_break, mck_sync, mck_rep, mck_retype,
mck_resume, mck_slo, mck_upd_meta, mck_get_meta, mck_rep_conf):
self, mck_valid, mck_break, mck_cleanup, mck_rep, mck_retype,
mck_resume, mck_slo, mck_upd_meta, mck_get_meta, mck_rep_conf,
mck_get_snaps):
array = self.data.array
volume = self.data.test_volume
device_id = self.data.device_id
@ -1179,7 +1182,7 @@ class PowerMaxReplicationTest(test.TestCase):
mck_valid.assert_any_call(array, extra_specs)
mck_break.assert_called_once_with(
array, device_id, volume_name, extra_specs, volume)
mck_sync.assert_called_once_with(array, device_id, extra_specs)
mck_cleanup.assert_called_once_with(array, device_id, extra_specs)
mck_rep.assert_called_once_with(
array, volume, device_id, target_extra_specs)
mck_retype.assert_called_once()

View File

@ -98,12 +98,6 @@ powermax_opts = [
cfg.MultiOpt(utils.U4P_FAILOVER_TARGETS,
item_type=types.Dict(),
help='Dictionary of Unisphere failover target info.'),
cfg.IntOpt(utils.POWERMAX_SNAPVX_UNLINK_LIMIT,
default=3,
help='Use this value to specify '
'the maximum number of unlinks '
'for the temporary snapshots '
'before a clone operation.'),
cfg.StrOpt(utils.POWERMAX_ARRAY,
help='Serial number of the array to connect to.'),
cfg.StrOpt(utils.POWERMAX_SRP,
@ -242,8 +236,6 @@ class PowerMaxCommon(object):
"""Get relevent details from configuration file."""
self.interval = self.configuration.safe_get('interval')
self.retries = self.configuration.safe_get('retries')
self.snapvx_unlink_limit = self.configuration.safe_get(
utils.POWERMAX_SNAPVX_UNLINK_LIMIT)
self.powermax_array_tag_list = self.configuration.safe_get(
utils.POWERMAX_ARRAY_TAG_LIST)
self.powermax_short_host_name_template = self.configuration.safe_get(
@ -626,11 +618,7 @@ class PowerMaxCommon(object):
source_device_id = self._find_device_on_array(
source_volume, extra_specs)
if not self.next_gen and (
extra_specs.get('rep_mode', None) == utils.REP_METRO):
self._sync_check(array, source_device_id, extra_specs)
else:
self._clone_check(array, source_device_id, extra_specs)
self._cleanup_device_snapvx(array, source_device_id, extra_specs)
clone_dict, rep_update, rep_info_dict = self._create_cloned_volume(
clone_volume, source_volume, extra_specs)
@ -1219,7 +1207,7 @@ class PowerMaxCommon(object):
# 2 - Check if volume is part of an on-going clone operation or if vol
# has source snapshots but not next-gen array
self._sync_check(array, device_id, ex_specs)
self._cleanup_device_snapvx(array, device_id, ex_specs)
__, snapvx_src, __ = self.rest.is_vol_in_rep_session(array, device_id)
if snapvx_src:
if not self.next_gen:
@ -1946,7 +1934,7 @@ class PowerMaxCommon(object):
# Perform any snapvx cleanup if required before creating the clone
if is_snapshot or from_snapvx:
self._clone_check(array, source_device_id, extra_specs)
self._cleanup_device_snapvx(array, source_device_id, extra_specs)
if not is_snapshot:
clone_dict, rep_update, rep_info_dict = self._create_replica(
@ -2047,14 +2035,8 @@ class PowerMaxCommon(object):
:param volume: volume object to be deleted
:returns: volume_name (string vol name)
"""
source_device_id = None
volume_name = volume.name
extra_specs = self._initial_setup(volume)
prov_loc = volume.provider_location
if isinstance(prov_loc, six.string_types):
name = ast.literal_eval(prov_loc)
source_device_id = name.get('source_device_id')
device_id = self._find_device_on_array(volume, extra_specs)
if device_id is None:
@ -2069,8 +2051,27 @@ class PowerMaxCommon(object):
# Check if the volume being deleted is a
# source or target for copy session
self._sync_check(array, device_id, extra_specs,
source_device_id=source_device_id)
self._cleanup_device_snapvx(array, device_id, extra_specs)
# Confirm volume has no more snapshots associated and is not a target
snapshots = self.rest.get_volume_snapshot_list(array, device_id)
__, snapvx_target_details = self.rest.find_snap_vx_sessions(
array, device_id, tgt_only=True)
if snapshots:
snapshot_names = ', '.join(
snap.get('snapshotName') for snap in snapshots)
raise exception.VolumeBackendAPIException(_(
'Cannot delete device %s as it currently has the following '
'active snapshots: %s. Please try again once these snapshots '
'are no longer active.') % (device_id, snapshot_names))
if snapvx_target_details:
source_device = snapvx_target_details.get('source_vol_id')
snapshot_name = snapvx_target_details.get('snap_name')
raise exception.VolumeBackendAPIException(_(
'Cannot delete device %s as it is currently a linked target '
'of snapshot %s. The source device of this link is %s. '
'Please try again once this snapshots is no longer '
'active.') % (device_id, snapshot_name, source_device))
# Remove from any storage groups and cleanup replication
self._remove_vol_and_cleanup_replication(
array, device_id, volume_name, extra_specs, volume)
@ -2910,111 +2911,6 @@ class PowerMaxCommon(object):
self._delete_from_srp(
array, target_device_id, clone_name, extra_specs)
@retry(retry_exc_tuple, interval=1, retries=3)
def _sync_check(self, array, device_id, extra_specs,
tgt_only=False, source_device_id=None):
"""Check if volume is part of a SnapVx sync process.
:param array: the array serial number
:param device_id: volume instance
:param tgt_only: Flag - return only sessions where device is target
:param extra_specs: extra specifications
:param tgt_only: Flag to specify if it is a target
:param source_device_id: source_device_id if it has one
"""
if not source_device_id and tgt_only:
source_device_id = self._get_target_source_device(
array, device_id)
if source_device_id:
@coordination.synchronized("emc-source-{src_device_id}")
def do_unlink_and_delete_snap(src_device_id):
LOG.debug("Locking on source device %(device_id)s.",
{'device_id': src_device_id})
self._do_sync_check(
array, device_id, extra_specs, tgt_only)
do_unlink_and_delete_snap(source_device_id)
else:
self._do_sync_check(
array, device_id, extra_specs, tgt_only)
def _do_sync_check(
self, array, device_id, extra_specs, tgt_only=False):
"""Check if volume is part of a SnapVx sync process.
:param array: the array serial number
:param device_id: volume instance
:param tgt_only: Flag - return only sessions where device is target
:param extra_specs: extra specifications
:param tgt_only: Flag to specify if it is a target
"""
snapvx_tgt, snapvx_src, __ = self.rest.is_vol_in_rep_session(
array, device_id)
get_sessions = True if snapvx_tgt or snapvx_src else False
if get_sessions:
src_sessions, tgt_session = self.rest.find_snap_vx_sessions(
array, device_id, tgt_only)
if tgt_session:
self._unlink_targets_and_delete_temp_snapvx(
tgt_session, array, extra_specs)
if src_sessions and not tgt_only:
if not self.rest.is_snap_id:
src_sessions.sort(key=lambda k: k['snapid'], reverse=True)
for session in src_sessions:
self._unlink_targets_and_delete_temp_snapvx(
session, array, extra_specs)
def _unlink_targets_and_delete_temp_snapvx(
self, session, array, extra_specs):
"""Unlink target and delete the temporary snapvx if valid candidate.
:param session: the snapvx session
:param array: the array serial number
:param extra_specs: extra specifications
"""
snap_name = session.get('snap_name')
if not snap_name:
msg = _("Snap_name not present in volume's snapvx session. "
"Unable to perform unlink and cleanup.")
LOG.error(msg)
raise exception.VolumeBackendAPIException(msg)
source = session.get('source_vol_id')
snap_id = session.get('snapid')
is_legacy = 'EMC_SMI' in snap_name
is_temp = utils.CLONE_SNAPSHOT_NAME in snap_name
target, cm_enabled = None, False
if session.get('target_vol_id'):
target = session.get('target_vol_id')
cm_enabled = session.get('copy_mode')
if target and snap_name:
loop = True if cm_enabled else False
LOG.debug(
"Unlinking source from target. Source: %(vol)s, Target: "
"%(tgt)s, Snap id: %(snapid)s.", {'vol': source, 'tgt': target,
'snapid': snap_id})
self.provision.unlink_snapvx_tgt_volume(
array, target, source, snap_name, extra_specs, snap_id,
loop)
# Candidates for deletion:
# 1. If legacy snapshot with 'EMC_SMI' in snapshot name
# 2. If snapVX snapshot is temporary
# If a target is present in the snap session it will be unlinked by
# the provision.unlink_snapvx_tgt_volume call above. This accounts
# for both CM enabled and disabled, as such by this point there will
# either be no target or it would have been unlinked, therefore
# delete if it is legacy or temp.
if is_legacy or is_temp:
LOG.debug(
"Deleting temporary snapshot. Source: %(vol)s, snap name: "
"%(name)s, snap id: %(snapid)s.", {
'vol': source, 'name': snap_name, 'snapid': snap_id})
self.provision.delete_temp_volume_snap(
array, snap_name, source, snap_id)
def _get_target_source_device(self, array, device_id):
"""Get the source device id of the target.
@ -3036,14 +2932,14 @@ class PowerMaxCommon(object):
return source_device_id
def _clone_check(
self, array, device_id, extra_specs, force_unlink=False):
@retry(retry_exc_tuple, interval=1, retries=3)
def _cleanup_device_snapvx(
self, array, device_id, extra_specs):
"""Perform any snapvx cleanup before creating clones or snapshots
:param array: the array serial
:param device_id: the device ID of the volume
:param extra_specs: extra specifications
:param force_unlink: force unlink even if not expired
"""
snapvx_tgt, snapvx_src, __ = self.rest.is_vol_in_rep_session(
array, device_id)
@ -3053,47 +2949,109 @@ class PowerMaxCommon(object):
def do_unlink_and_delete_snap(src_device_id):
src_sessions, tgt_session = self.rest.find_snap_vx_sessions(
array, src_device_id)
count = 0
if tgt_session and count < self.snapvx_unlink_limit:
self._delete_valid_snapshot(
array, tgt_session, extra_specs, force_unlink)
count += 1
if tgt_session:
self._unlink_and_delete_temporary_snapshots(
tgt_session, array, extra_specs)
if src_sessions:
for session in src_sessions:
if count < self.snapvx_unlink_limit:
self._delete_valid_snapshot(
array, session, extra_specs, force_unlink)
count += 1
else:
break
if not self.rest.is_snap_id:
src_sessions.sort(
key=lambda k: k['snapid'], reverse=True)
for src_session in src_sessions:
self._unlink_and_delete_temporary_snapshots(
src_session, array, extra_specs)
do_unlink_and_delete_snap(device_id)
def _delete_valid_snapshot(
self, array, session, extra_specs, force_unlink=False):
"""Delete a snapshot if valid candidate for deletion.
def _unlink_and_delete_temporary_snapshots(
self, session, array, extra_specs):
"""Helper for unlinking and deleting temporary snapshot sessions
:param array: the array serial
:param session: the snapvx session
:param session: snapvx session
:param array: the array serial number
:param extra_specs: extra specifications
:param force_unlink: force unlink even if not expired
"""
is_legacy = 'EMC_SMI' in session['snap_name']
is_temp = utils.CLONE_SNAPSHOT_NAME in session['snap_name']
is_expired = session['expired']
if force_unlink:
is_expired = True
is_valid = True if is_legacy or (is_temp and is_expired) else False
if is_valid:
try:
self._unlink_targets_and_delete_temp_snapvx(
session, array, extra_specs)
except exception.VolumeBackendAPIException as e:
# Ignore and continue as snapshot has been unlinked
# successfully with incorrect status code returned
if ('404' and session['snap_name'] and
'does not exist' in six.text_type(e)):
pass
try:
session_unlinked = self._unlink_snapshot(
session, array, extra_specs)
if session_unlinked:
self._delete_temp_snapshot(session, array)
except exception.VolumeBackendAPIException as e:
# Ignore and continue as snapshot has been unlinked
# successfully with incorrect status code returned
if ('404' and session['snap_name'] and
'does not exist' in six.text_type(e)):
pass
def _unlink_snapshot(self, session, array, extra_specs):
"""Helper for unlinking temporary snapshot during cleanup.
:param session: session that contains snapshot
:param array: the array serial number
:param extra_specs: extra specifications
:return:
"""
snap_name = session.get('snap_name')
source = session.get('source_vol_id')
snap_id = session.get('snapid')
snap_info = self.rest.get_volume_snap(
array, source, snap_name, snap_id)
is_linked = snap_info.get('linkedDevices')
target, cm_enabled = None, False
if session.get('target_vol_id'):
target = session.get('target_vol_id')
cm_enabled = session.get('copy_mode')
if target and snap_name and is_linked:
loop = True if cm_enabled else False
LOG.debug(
"Unlinking source from target. Source: %(vol)s, Target: "
"%(tgt)s, Snap id: %(snapid)s.",
{'vol': source, 'tgt': target, 'snapid': snap_id})
self.provision.unlink_snapvx_tgt_volume(
array, target, source, snap_name, extra_specs, snap_id,
loop)
is_unlinked = True
snap_info = self.rest.get_volume_snap(
array, source, snap_name, snap_id)
if snap_info and snap_info.get('linkedDevices'):
is_unlinked = False
return is_unlinked
def _delete_temp_snapshot(self, session, array):
"""Helper for deleting temporary snapshot during cleanup.
:param session: Session that contains snapshot
:param array: the array serial number
"""
snap_name = session.get('snap_name')
source = session.get('source_vol_id')
snap_id = session.get('snapid')
LOG.debug(
"Deleting temp snapshot if it exists. Snap name is: "
"%(snap_name)s, Source is: %(source)s, "
"Snap id: %(snap_id)s.",
{'snap_name': snap_name, 'source': source,
'snap_id': snap_id})
is_legacy = 'EMC_SMI' in snap_name if snap_name else False
is_temp = (
utils.CLONE_SNAPSHOT_NAME in snap_name if snap_name else False)
snap_info = self.rest.get_volume_snap(
array, source, snap_name, snap_id)
is_linked = snap_info.get('linkedDevices') if snap_info else False
# Candidates for deletion:
# 1. If legacy snapshot with 'EMC_SMI' in snapshot name
# 2. If snapVX snapshot is temporary
# 3. Snapshot is unlinked. Call _unlink_snapshot before delete.
if (is_legacy or is_temp) and not is_linked:
LOG.debug(
"Deleting temporary snapshot. Source: %(vol)s, snap name: "
"%(name)s, snap id: %(snapid)s.", {
'vol': source, 'name': snap_name, 'snapid': snap_id})
self.provision.delete_temp_volume_snap(
array, snap_name, source, snap_id)
def manage_existing(self, volume, external_ref):
"""Manages an existing PowerMax/VMAX Volume (import to Cinder).
@ -3367,7 +3325,7 @@ class PowerMaxCommon(object):
{'id': volume_id})
else:
# Check if volume is snap source
self._clone_check(array, device_id, extra_specs)
self._cleanup_device_snapvx(array, device_id, extra_specs)
snapvx_tgt, snapvx_src, __ = self.rest.is_vol_in_rep_session(
array, device_id)
if snapvx_src or snapvx_tgt:
@ -3933,13 +3891,7 @@ class PowerMaxCommon(object):
:param extra_specs: the extra specifications
:returns: bool
"""
model_update, success = None, False
rep_info_dict, rep_extra_specs, rep_mode, rep_status = (
None, None, None, False)
resume_target_sg, resume_original_sg = False, False
resume_original_sg_dict = dict()
orig_mgmt_sg_name = ''
is_partitioned = False
orig_mgmt_sg_name = None
target_extra_specs = dict(new_type['extra_specs'])
target_extra_specs.update({
@ -3952,30 +3904,10 @@ class PowerMaxCommon(object):
target_extra_specs)
target_extra_specs.update(
{utils.DISABLECOMPRESSION: compression_disabled})
was_rep_enabled = self.utils.is_replication_enabled(extra_specs)
if self.utils.is_replication_enabled(target_extra_specs):
target_backend_id = target_extra_specs.get(
utils.REPLICATION_DEVICE_BACKEND_ID,
utils.BACKEND_ID_LEGACY_REP)
target_rep_config = self.utils.get_rep_config(
target_backend_id, self.rep_configs)
rep_mode = target_rep_config['mode']
target_extra_specs[utils.REP_MODE] = rep_mode
target_extra_specs[utils.REP_CONFIG] = target_rep_config
is_rep_enabled = True
else:
is_rep_enabled = False
backend_ids_differ = False
if was_rep_enabled and is_rep_enabled:
curr_backend_id = extra_specs.get(
utils.REPLICATION_DEVICE_BACKEND_ID,
utils.BACKEND_ID_LEGACY_REP)
tgt_backend_id = target_extra_specs.get(
utils.REPLICATION_DEVICE_BACKEND_ID,
utils.BACKEND_ID_LEGACY_REP)
backend_ids_differ = curr_backend_id != tgt_backend_id
(was_rep_enabled, is_rep_enabled, backend_ids_differ, rep_mode,
target_extra_specs) = (
self._get_replication_flags(
extra_specs, target_extra_specs))
if was_rep_enabled and not self.promotion:
self._validate_rdfg_status(array, extra_specs)
@ -3992,51 +3924,26 @@ class PowerMaxCommon(object):
rdf_pair_broken, rdf_pair_created, vol_retyped, remote_retyped = (
False, False, False, False)
self._perform_snapshot_cleanup(
array, device_id, was_rep_enabled, is_rep_enabled,
backend_ids_differ, extra_specs, target_extra_specs)
try:
# Scenario 1: Rep -> Non-Rep
# Scenario 2: Cleanup for Rep -> Diff Rep type
if (was_rep_enabled and not is_rep_enabled) or backend_ids_differ:
if self.promotion:
resume_original_sg = False
rdf_group = extra_specs['rdf_group_no']
is_partitioned = self._rdf_vols_partitioned(
array, [volume], rdf_group)
if not is_partitioned:
self.break_rdf_device_pair_session_promotion(
array, device_id, volume_name, extra_specs)
else:
rep_extra_specs, resume_original_sg = (
self.break_rdf_device_pair_session(
array, device_id, volume_name, extra_specs,
volume))
status = (REPLICATION_ERROR if self.promotion else
REPLICATION_DISABLED)
model_update = {
'replication_status': status,
'replication_driver_data': None}
rdf_pair_broken = True
if resume_original_sg:
resume_original_sg_dict = {
utils.ARRAY: array,
utils.SG_NAME: rep_extra_specs['mgmt_sg_name'],
utils.RDF_GROUP_NO: rep_extra_specs['rdf_group_no'],
utils.EXTRA_SPECS: rep_extra_specs}
(model_update, resume_original_sg_dict, rdf_pair_broken,
resume_original_sg, is_partitioned) = (
self._prep_rep_to_non_rep(
array, device_id, volume_name, volume, was_rep_enabled,
is_rep_enabled, backend_ids_differ, extra_specs))
# Scenario 1: Non-Rep -> Rep
# Scenario 2: Rep -> Diff Rep type
if (not was_rep_enabled and is_rep_enabled) or backend_ids_differ:
self._sync_check(array, device_id, extra_specs)
(rep_status, rep_driver_data, rep_info_dict,
rep_extra_specs, resume_target_sg) = (
self.configure_volume_replication(
array, volume, device_id, target_extra_specs))
if rep_status != 'first_vol_in_rdf_group':
rdf_pair_created = True
model_update = {
'replication_status': rep_status,
'replication_driver_data': six.text_type(
{'device_id': rep_info_dict['target_device_id'],
'array': rep_info_dict['remote_array']})}
(model_update, rdf_pair_created, rep_status, rep_driver_data,
rep_info_dict, rep_extra_specs, resume_target_sg) = (
self._prep_non_rep_to_rep(
array, device_id, volume, was_rep_enabled,
is_rep_enabled, backend_ids_differ, target_extra_specs))
success, target_sg_name = self._retype_volume(
array, srp, device_id, volume, volume_name, extra_specs,
@ -4128,6 +4035,185 @@ class PowerMaxCommon(object):
finally:
raise e
def _get_replication_flags(self, extra_specs, target_extra_specs):
"""Get replication flags from extra specifications.
:param extra_specs: extra specification -- dict
:param target_extra_specs: target extra specification -- dict
:returns: was_rep_enabled -- bool, is_rep_enabled -- bool,
backend_ids_differ -- bool, rep_mode -- str,
target_extra_specs -- dict
"""
rep_mode = None
was_rep_enabled = self.utils.is_replication_enabled(extra_specs)
if self.utils.is_replication_enabled(target_extra_specs):
target_backend_id = target_extra_specs.get(
utils.REPLICATION_DEVICE_BACKEND_ID,
utils.BACKEND_ID_LEGACY_REP)
target_rep_config = self.utils.get_rep_config(
target_backend_id, self.rep_configs)
rep_mode = target_rep_config['mode']
target_extra_specs[utils.REP_MODE] = rep_mode
target_extra_specs[utils.REP_CONFIG] = target_rep_config
is_rep_enabled = True
else:
is_rep_enabled = False
backend_ids_differ = False
if was_rep_enabled and is_rep_enabled:
curr_backend_id = extra_specs.get(
utils.REPLICATION_DEVICE_BACKEND_ID,
utils.BACKEND_ID_LEGACY_REP)
tgt_backend_id = target_extra_specs.get(
utils.REPLICATION_DEVICE_BACKEND_ID,
utils.BACKEND_ID_LEGACY_REP)
backend_ids_differ = curr_backend_id != tgt_backend_id
return (was_rep_enabled, is_rep_enabled, backend_ids_differ, rep_mode,
target_extra_specs)
def _prep_non_rep_to_rep(
self, array, device_id, volume, was_rep_enabled,
is_rep_enabled, backend_ids_differ, target_extra_specs):
"""Prepare for non rep to rep retype.
:param array: the array serial number -- str
:param device_id: the device id -- str
:param volume: the volume object -- objects.Volume
:param was_rep_enabled: flag -- bool
:param is_rep_enabled: flag -- bool
:param backend_ids_differ: flag -- bool
:param target_extra_specs: target extra specs -- dict
:returns: model_update -- dict, rdf_pair_created -- bool,
rep_status -- str, rep_driver_data -- dict,
rep_info_dict -- dict, rep_extra_specs -- dict,
resume_target_sg -- bool
"""
model_update, rep_status = None, None
resume_target_sg = False
rdf_pair_created = False
rep_driver_data, rep_info_dict = dict(), dict()
rep_extra_specs = dict()
if (not was_rep_enabled and is_rep_enabled) or backend_ids_differ:
(rep_status, rep_driver_data, rep_info_dict,
rep_extra_specs, resume_target_sg) = (
self.configure_volume_replication(
array, volume, device_id, target_extra_specs))
if rep_status != 'first_vol_in_rdf_group':
rdf_pair_created = True
model_update = {
'replication_status': rep_status,
'replication_driver_data': six.text_type(
{'device_id': rep_info_dict['target_device_id'],
'array': rep_info_dict['remote_array']})}
return (model_update, rdf_pair_created, rep_status, rep_driver_data,
rep_info_dict, rep_extra_specs, resume_target_sg)
def _prep_rep_to_non_rep(
self, array, device_id, volume_name, volume, was_rep_enabled,
is_rep_enabled, backend_ids_differ, extra_specs):
"""Preparation for replication to non-replicated.
:param array: the array serial number -- str
:param device_id: device_id: the device id -- str
:param volume_name: the volume name -- str
:param volume: the volume object -- objects.Volume
:param was_rep_enabled: flag -- bool
:param is_rep_enabled: flag -- bool
:param backend_ids_differ: flag -- bool
:param extra_specs: extra specs -- dict
:returns: model_update --dict , resume_original_sg_dict -- dict,
rdf_pair_broken -- bool, resume_original_sg -- bool,
is_partitioned -- bool
"""
model_update = dict()
resume_original_sg_dict = dict()
rdf_pair_broken = False
resume_original_sg = False
is_partitioned = False
if (was_rep_enabled and not is_rep_enabled) or backend_ids_differ:
if self.promotion:
resume_original_sg = False
rdf_group = extra_specs['rdf_group_no']
is_partitioned = self._rdf_vols_partitioned(
array, [volume], rdf_group)
if not is_partitioned:
self.break_rdf_device_pair_session_promotion(
array, device_id, volume_name, extra_specs)
else:
rep_extra_specs, resume_original_sg = (
self.break_rdf_device_pair_session(
array, device_id, volume_name, extra_specs,
volume))
status = (REPLICATION_ERROR if self.promotion else
REPLICATION_DISABLED)
model_update = {
'replication_status': status,
'replication_driver_data': None}
rdf_pair_broken = True
if resume_original_sg:
resume_original_sg_dict = {
utils.ARRAY: array,
utils.SG_NAME: rep_extra_specs['mgmt_sg_name'],
utils.RDF_GROUP_NO: rep_extra_specs['rdf_group_no'],
utils.EXTRA_SPECS: rep_extra_specs}
return (model_update, resume_original_sg_dict, rdf_pair_broken,
resume_original_sg, is_partitioned)
def _perform_snapshot_cleanup(
self, array, device_id, was_rep_enabled, is_rep_enabled,
backend_ids_differ, extra_specs, target_extra_specs):
"""Perform snapshot cleanup.
Perform snapshot cleanup before any other changes. If retyping
to either async or metro then there should be no linked snapshots
on the volume.
:param array: the array serial number -- str
:param device_id: device_id: the device id -- str
:param was_rep_enabled: flag -- bool
:param is_rep_enabled: flag -- bool
:param backend_ids_differ: flag -- bool
:param extra_specs: extra specs -- dict
:param target_extra_specs: target extra specs -- dict
"""
if (not was_rep_enabled and is_rep_enabled) or backend_ids_differ:
target_rep_mode = target_extra_specs.get(utils.REP_MODE)
target_is_async = target_rep_mode == utils.REP_ASYNC
target_is_metro = target_rep_mode == utils.REP_METRO
if target_is_async or target_is_metro:
self._cleanup_device_snapvx(array, device_id, extra_specs)
snapshots = self.rest.get_volume_snapshot_list(
array, device_id)
__, snapvx_target_details = self.rest.find_snap_vx_sessions(
array, device_id, tgt_only=True)
linked_snapshots = list()
for snapshot in snapshots:
linked_devices = snapshot.get('linkedDevices')
if linked_devices:
snapshot_name = snapshot.get('snapshotName')
linked_snapshots.append(snapshot_name)
if linked_snapshots:
snapshot_names = ', '.join(linked_snapshots)
raise exception.VolumeBackendAPIException(_(
'Unable to complete retype as volume has active'
'snapvx links. Cannot retype to Asynchronous or '
'Metro modes while the volume has active links. '
'Please wait until these snapvx operations have '
'completed and try again. Snapshots: '
'%s') % snapshot_names)
if snapvx_target_details:
source_vol_id = snapvx_target_details.get('source_vol_id')
snap_name = snapvx_target_details.get('snap_name')
raise exception.VolumeBackendAPIException(_(
'Unable to complete retype as volume is a snapvx '
'target. Cannot retype to Asynchronous or Metro '
'modes in this state. Please wait until these snapvx '
'operations complete and try again. Volume %s is '
'currently a target of snapshot %s with source device '
'%s') % (device_id, snap_name, source_vol_id))
def _cleanup_on_migrate_failure(
self, rdf_pair_broken, rdf_pair_created, vol_retyped,
remote_retyped, extra_specs, target_extra_specs, volume,
@ -5168,7 +5254,7 @@ class PowerMaxCommon(object):
{'sourceName': volume_name})
device_id = volume_dict['device_id']
# Check if volume is snap target (e.g. if clone volume)
self._sync_check(array, device_id, extra_specs)
self._cleanup_device_snapvx(array, device_id, extra_specs)
# Remove from any storage groups and cleanup replication
self._remove_vol_and_cleanup_replication(
array, device_id, volume_name, extra_specs, volume)
@ -5571,6 +5657,61 @@ class PowerMaxCommon(object):
array, vol_grp_name)
deleted_volume_device_ids = []
# If volumes are being deleted along with the group, ensure snapshot
# cleanup completes before doing any replication/storage group cleanup.
remaining_device_snapshots = list()
remaining_snapvx_targets = list()
for vol in volumes:
extra_specs = self._initial_setup(vol)
device_id = self._find_device_on_array(vol, extra_specs)
self._cleanup_device_snapvx(array, device_id, extra_specs)
snapshots = self.rest.get_volume_snapshot_list(array, device_id)
__, snapvx_target_details = self.rest.find_snap_vx_sessions(
array, device_id, tgt_only=True)
if snapshots:
snapshot_names = ', '.join(
snap.get('snapshotName') for snap in snapshots)
snap_details = {
'device_id': device_id, 'snapshot_names': snapshot_names}
remaining_device_snapshots.append(snap_details)
if snapvx_target_details:
source_vol_id = snapvx_target_details.get('source_vol_id')
snap_name = snapvx_target_details.get('snap_name')
target_details = {
'device_id': device_id, 'source_vol_id': source_vol_id,
'snapshot_name': snap_name}
remaining_snapvx_targets.append(target_details)
# Fail out if volumes to be deleted still have snapshots.
if remaining_device_snapshots:
for details in remaining_device_snapshots:
device_id = details.get('device_id')
snapshot_names = details.get('snapshot_names')
LOG.error('Cannot delete device %s, it has the '
'following active snapshots, %s.',
device_id, snapshot_names)
raise exception.VolumeBackendAPIException(_(
'Group volumes have active snapshots. Cannot perform group '
'delete. Wait for snapvx sessions to complete their '
'processes or remove volumes from group before attempting '
'to delete again. Please see previously logged error '
'message for device and snapshot details.'))
if remaining_snapvx_targets:
for details in remaining_snapvx_targets:
device_id = details.get('device_id')
snap_name = details.get('snapshot_name')
source_vol_id = details.get('source_vol_id')
LOG.error('Cannot delete device %s, it is current a target '
'of snapshot %s with source device id %s',
device_id, snap_name, source_vol_id)
raise exception.VolumeBackendAPIException(_(
'Some group volumes are targets of a snapvx session. Cannot '
'perform group delete. Wait for snapvx sessions to complete '
'their processes or remove volumes from group before '
'attempting to delete again. Please see previously logged '
'error message for device and snapshot details.'))
# Remove replication for group, if applicable
if group.is_replicated:
vt_extra_specs = self.utils.get_volumetype_extra_specs(
@ -5591,11 +5732,8 @@ class PowerMaxCommon(object):
interval_retries_dict)
for vol in volumes:
extra_specs = self._initial_setup(vol)
device_id = self._find_device_on_array(
vol, extra_specs)
device_id = self._find_device_on_array(vol, extra_specs)
if device_id in volume_device_ids:
self._clone_check(
array, device_id, extra_specs, force_unlink=True)
self.masking.remove_and_reset_members(
array, vol, device_id, vol.name,
extra_specs, False)
@ -6706,7 +6844,7 @@ class PowerMaxCommon(object):
message=exception_message)
else:
snap_id = snap_id_list[0]
self._clone_check(array, sourcedevice_id, extra_specs)
self._cleanup_device_snapvx(array, sourcedevice_id, extra_specs)
try:
LOG.info("Reverting device: %(deviceid)s "
"to snapshot: %(snapname)s.",

View File

@ -132,7 +132,6 @@ POWERMAX_ARRAY = 'powermax_array'
POWERMAX_SRP = 'powermax_srp'
POWERMAX_SERVICE_LEVEL = 'powermax_service_level'
POWERMAX_PORT_GROUPS = 'powermax_port_groups'
POWERMAX_SNAPVX_UNLINK_LIMIT = 'powermax_snapvx_unlink_limit'
POWERMAX_ARRAY_TAG_LIST = 'powermax_array_tag_list'
POWERMAX_SHORT_HOST_NAME_TEMPLATE = 'powermax_short_host_name_template'
POWERMAX_PORT_GROUP_NAME_TEMPLATE = 'powermax_port_group_name_template'