diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/powermax_data.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/powermax_data.py index d9477f962e5..23b9573355a 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/powermax_data.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/powermax_data.py @@ -468,6 +468,10 @@ class PowerMaxData(object): rep_extra_specs_rep_config = deepcopy(rep_extra_specs6) rep_extra_specs_rep_config[utils.REP_CONFIG] = rep_config_sync + rep_extra_specs_rep_config_metro = deepcopy(rep_extra_specs6) + rep_extra_specs_rep_config_metro[utils.REP_CONFIG] = rep_config_metro + rep_extra_specs_rep_config_metro[utils.REP_MODE] = utils.REP_METRO + extra_specs_tags = deepcopy(extra_specs) extra_specs_tags.update({utils.STORAGE_GROUP_TAGS: sg_tags}) diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_common.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_common.py index d9cbc65f313..e44615a8c7a 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_common.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_common.py @@ -577,6 +577,95 @@ class PowerMaxCommonTest(test.TestCase): extra_specs, connector, False, async_grp=None, host_template=None) + @mock.patch.object(utils.PowerMaxUtils, 'is_metro_device', + return_value=True) + @mock.patch.object(provision.PowerMaxProvision, 'verify_slo_workload') + @mock.patch.object(common.PowerMaxCommon, '_remove_members') + @mock.patch.object(common.PowerMaxCommon, 'find_host_lun_id', + return_value=(tpd.PowerMaxData.iscsi_device_info, + False)) + @mock.patch.object( + common.PowerMaxCommon, '_get_replication_extra_specs', + return_value=tpd.PowerMaxData.rep_extra_specs_rep_config_metro) + @mock.patch.object( + common.PowerMaxCommon, '_initial_setup', + return_value=tpd.PowerMaxData.rep_extra_specs_rep_config_metro) + def test_unmap_lun_replication_metro( + self, mck_setup, mck_rep, mck_find, mck_rem, mck_slo, mck_metro): + volume = deepcopy(self.data.test_volume) + connector = deepcopy(self.data.connector) + volume.volume_attachment.objects = [ + deepcopy(self.data.test_volume_attachment)] + extra_specs = deepcopy(self.data.rep_extra_specs_rep_config) + extra_specs[utils.FORCE_VOL_REMOVE] = True + self.common._unmap_lun(volume, connector) + self.assertEqual(2, mck_rem.call_count) + + @mock.patch.object(utils.PowerMaxUtils, 'is_metro_device', + return_value=True) + @mock.patch.object(provision.PowerMaxProvision, 'verify_slo_workload') + @mock.patch.object(common.PowerMaxCommon, '_remove_members') + @mock.patch.object(common.PowerMaxCommon, 'find_host_lun_id', + return_value=(tpd.PowerMaxData.iscsi_device_info, + False)) + @mock.patch.object( + common.PowerMaxCommon, '_get_replication_extra_specs', + return_value=tpd.PowerMaxData.rep_extra_specs_rep_config_metro) + @mock.patch.object( + common.PowerMaxCommon, '_initial_setup', + return_value=tpd.PowerMaxData.rep_extra_specs_rep_config_metro) + def test_unmap_lun_replication_metro_promotion( + self, mck_setup, mck_rep, mck_find, mck_rem, mck_slo, mck_metro): + volume = deepcopy(self.data.test_volume) + connector = deepcopy(self.data.connector) + volume.volume_attachment.objects = [ + deepcopy(self.data.test_volume_attachment)] + extra_specs = deepcopy(self.data.rep_extra_specs_rep_config) + extra_specs[utils.FORCE_VOL_REMOVE] = True + self.common.promotion = True + self.common._unmap_lun(volume, connector) + self.common.promotion = False + self.assertEqual(1, mck_rem.call_count) + + @mock.patch.object(common.PowerMaxCommon, '_unmap_lun') + @mock.patch.object(metadata.PowerMaxVolumeMetadata, 'capture_detach_info') + def test_unmap_lun_promotion_non_replicated_volume( + self, mck_unmap, mck_info): + volume = deepcopy(self.data.test_volume) + connector = deepcopy(self.data.connector) + ret = self.common._unmap_lun_promotion(volume, connector) + self.assertIsNone(ret) + self.assertEqual(0, mck_unmap.call_count) + self.assertEqual(0, mck_info.call_count) + + @mock.patch.object(common.PowerMaxCommon, '_unmap_lun') + @mock.patch.object( + common.PowerMaxCommon, '_initial_setup', + return_value=tpd.PowerMaxData.rep_extra_specs_rep_config_metro) + def test_unmap_lun_promotion_replicated_metro_volume( + self, mck_setup, mck_unmap): + volume = deepcopy(self.data.test_rep_volume) + connector = deepcopy(self.data.connector) + self.common._unmap_lun_promotion(volume, connector) + mck_setup.assert_called_once_with(volume) + mck_unmap.assert_called_once_with(volume, connector) + + @mock.patch.object(metadata.PowerMaxVolumeMetadata, 'capture_detach_info') + @mock.patch.object( + common.PowerMaxCommon, '_initial_setup', + return_value=tpd.PowerMaxData.rep_extra_specs_rep_config) + def test_unmap_lun_promotion_replicated_non_metro_volume( + self, mck_setup, mck_capture): + volume = deepcopy(self.data.test_rep_volume) + connector = deepcopy(self.data.connector) + extra_specs = self.data.rep_extra_specs_rep_config + device_id = self.data.device_id + promotion_key = [utils.PMAX_FAILOVER_START_ARRAY_PROMOTION] + self.common._unmap_lun_promotion(volume, connector) + mck_setup.assert_called_once_with(volume) + mck_capture.assert_called_once_with( + volume, extra_specs, device_id, promotion_key, promotion_key) + def test_initialize_connection_already_mapped(self): volume = self.data.test_volume connector = self.data.connector @@ -712,6 +801,17 @@ class PowerMaxCommonTest(test.TestCase): mock_unmap.assert_called_once_with( volume, connector) + def test_terminate_connection_promotion(self): + volume = self.data.test_volume + connector = self.data.connector + with mock.patch.object( + self.common, '_unmap_lun_promotion') as mock_unmap: + self.common.promotion = True + self.common.terminate_connection(volume, connector) + mock_unmap.assert_called_once_with( + volume, connector) + self.common.promotion = False + @mock.patch.object(provision.PowerMaxProvision, 'extend_volume') @mock.patch.object(common.PowerMaxCommon, '_extend_vol_validation_checks') def test_extend_vol_no_rep_success(self, mck_val_chk, mck_extend): @@ -1975,6 +2075,30 @@ class PowerMaxCommonTest(test.TestCase): host = self.data.new_host self.assertFalse(self.common.retype(volume, new_type, host)) + @mock.patch.object( + common.PowerMaxCommon, '_initial_setup', + return_value=tpd.PowerMaxData.rep_extra_specs_rep_config) + @mock.patch.object(provision.PowerMaxProvision, 'verify_slo_workload', + return_value=(True, True)) + @mock.patch.object(common.PowerMaxCommon, '_slo_workload_migration') + def test_retype_promotion_extra_spec_update( + self, mck_migrate, mck_slo, mck_setup): + device_id = self.data.device_id + volume_name = self.data.test_rep_volume.name + extra_specs = deepcopy(self.data.rep_extra_specs_rep_config) + rep_config = extra_specs[utils.REP_CONFIG] + rep_extra_specs = self.common._get_replication_extra_specs( + extra_specs, rep_config) + extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f + volume = self.data.test_rep_volume + new_type = {'extra_specs': {}} + host = {'host': self.data.new_host} + self.common.promotion = True + self.common.retype(volume, new_type, host) + self.common.promotion = False + mck_migrate.assert_called_once_with( + device_id, volume, host, volume_name, new_type, rep_extra_specs) + def test_slo_workload_migration_valid(self): device_id = self.data.device_id volume_name = self.data.test_volume.name @@ -2222,20 +2346,23 @@ class PowerMaxCommonTest(test.TestCase): ref_return = (True, 'Silver', 'OLTP') return_val = self.common._is_valid_for_storage_assisted_migration( device_id, host, self.data.array, - self.data.srp, volume_name, False, False) + self.data.srp, volume_name, False, False, self.data.slo, + self.data.workload, False) self.assertEqual(ref_return, return_val) # No current sgs found with mock.patch.object(self.rest, 'get_storage_groups_from_volume', return_value=None): return_val = self.common._is_valid_for_storage_assisted_migration( device_id, host, self.data.array, self.data.srp, - volume_name, False, False) + volume_name, False, False, self.data.slo, self.data.workload, + False) self.assertEqual(ref_return, return_val) host = {'host': 'HostX@Backend#Silver+SRP_1+000197800123'} ref_return = (True, 'Silver', 'NONE') return_val = self.common._is_valid_for_storage_assisted_migration( device_id, host, self.data.array, - self.data.srp, volume_name, False, False) + self.data.srp, volume_name, False, False, self.data.slo, + self.data.workload, False) self.assertEqual(ref_return, return_val) def test_is_valid_for_storage_assisted_migration_false(self): @@ -2246,25 +2373,29 @@ class PowerMaxCommonTest(test.TestCase): host = {'host': 'HostX@Backend#Silver+SRP_1+000197800123+dummy+data'} return_val = self.common._is_valid_for_storage_assisted_migration( device_id, host, self.data.array, - self.data.srp, volume_name, False, False) + self.data.srp, volume_name, False, False, self.data.slo, + self.data.workload, False) self.assertEqual(ref_return, return_val) # Wrong array host2 = {'host': 'HostX@Backend#Silver+OLTP+SRP_1+00012345678'} return_val = self.common._is_valid_for_storage_assisted_migration( device_id, host2, self.data.array, - self.data.srp, volume_name, False, False) + self.data.srp, volume_name, False, False, self.data.slo, + self.data.workload, False) self.assertEqual(ref_return, return_val) # Wrong srp host3 = {'host': 'HostX@Backend#Silver+OLTP+SRP_2+000197800123'} return_val = self.common._is_valid_for_storage_assisted_migration( device_id, host3, self.data.array, - self.data.srp, volume_name, False, False) + self.data.srp, volume_name, False, False, self.data.slo, + self.data.workload, False) self.assertEqual(ref_return, return_val) # Already in correct sg host4 = {'host': self.data.fake_host} return_val = self.common._is_valid_for_storage_assisted_migration( device_id, host4, self.data.array, - self.data.srp, volume_name, False, False) + self.data.srp, volume_name, False, False, self.data.slo, + self.data.workload, False) self.assertEqual(ref_return, return_val) def test_is_valid_for_storage_assisted_migration_next_gen(self): @@ -2276,9 +2407,66 @@ class PowerMaxCommonTest(test.TestCase): return_value=True): return_val = self.common._is_valid_for_storage_assisted_migration( device_id, host, self.data.array, - self.data.srp, volume_name, False, False) + self.data.srp, volume_name, False, False, self.data.slo, + self.data.workload, False) self.assertEqual(ref_return, return_val) + def test_is_valid_for_storage_assisted_migration_promotion_change_comp( + self): + device_id = self.data.device_id + host = {'host': self.data.new_host} + volume_name = self.data.test_volume.name + ref_return = (False, None, None) + self.common.promotion = True + return_val = self.common._is_valid_for_storage_assisted_migration( + device_id, host, self.data.array, + self.data.srp, volume_name, True, False, self.data.slo_silver, + self.data.workload, False) + self.common.promotion = False + self.assertEqual(ref_return, return_val) + + def test_is_valid_for_storage_assisted_migration_promotion_change_slo( + self): + device_id = self.data.device_id + host = {'host': self.data.new_host} + volume_name = self.data.test_volume.name + ref_return = (False, None, None) + self.common.promotion = True + return_val = self.common._is_valid_for_storage_assisted_migration( + device_id, host, self.data.array, + self.data.srp, volume_name, False, False, self.data.slo, + self.data.workload, False) + self.common.promotion = False + self.assertEqual(ref_return, return_val) + + def test_is_valid_for_storage_assisted_migration_promotion_change_workload( + self): + device_id = self.data.device_id + host = {'host': self.data.new_host} + volume_name = self.data.test_volume.name + ref_return = (False, None, None) + self.common.promotion = True + return_val = self.common._is_valid_for_storage_assisted_migration( + device_id, host, self.data.array, + self.data.srp, volume_name, False, False, self.data.slo_silver, + 'fail_workload', False) + self.common.promotion = False + self.assertEqual(ref_return, return_val) + + def test_is_valid_for_storage_assisted_migration_promotion_target_not_rep( + self): + device_id = self.data.device_id + host = {'host': self.data.new_host} + volume_name = self.data.test_volume.name + ref_return = (False, None, None) + self.common.promotion = True + return_val = self.common._is_valid_for_storage_assisted_migration( + device_id, host, self.data.array, + self.data.srp, volume_name, False, False, self.data.slo_silver, + 'OLTP', True) + self.common.promotion = False + self.assertEqual(ref_return, return_val) + def test_find_volume_group(self): group = self.data.test_group_1 array = self.data.array @@ -2442,9 +2630,8 @@ class PowerMaxCommonTest(test.TestCase): add_vols = [self.data.test_volume] remove_vols = [] ref_model_update = {'status': fields.GroupStatus.AVAILABLE} - model_update, __, __ = self.common.update_group(group, - add_vols, - remove_vols) + model_update, __, __ = self.common.update_group( + group, add_vols, remove_vols) self.assertEqual(ref_model_update, model_update) @mock.patch.object(common.PowerMaxCommon, '_find_volume_group', @@ -2476,13 +2663,100 @@ class PowerMaxCommonTest(test.TestCase): with mock.patch.object( rest.PowerMaxRest, 'is_volume_in_storagegroup', return_value=False) as mock_exists: - model_update, __, __ = self.common.update_group(group, - add_vols, - remove_vols) + model_update, __, __ = self.common.update_group( + group, add_vols, remove_vols) mock_exists.assert_called_once() - self.assertEqual(ref_model_update, model_update) + @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_update_group_failover_failure( + self, mock_cg_type, mock_type_check): + group = self.data.test_group_1 + add_vols = [] + remove_vols = [self.data.test_volume_group_member] + self.common.failover = True + self.assertRaises( + exception.VolumeBackendAPIException, self.common.update_group, + group, add_vols, remove_vols) + self.common.failover = False + + @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) + @mock.patch.object(common.PowerMaxCommon, '_update_group_promotion') + def test_update_group_during_promotion( + self, mck_update, mock_cg_type, mock_type_check): + group = self.data.test_group_1 + add_vols = [] + remove_vols = [self.data.test_volume_group_member] + ref_model_update = {'status': fields.GroupStatus.AVAILABLE} + self.common.promotion = True + model_update, __, __ = self.common.update_group( + group, add_vols, remove_vols) + self.common.promotion = False + mck_update.assert_called_once_with(group, add_vols, remove_vols) + self.assertEqual(ref_model_update, model_update) + + @mock.patch.object(rest.PowerMaxRest, 'is_volume_in_storagegroup', + return_value=True) + @mock.patch.object( + common.PowerMaxCommon, '_get_replication_extra_specs', + return_value=tpd.PowerMaxData.rep_extra_specs_rep_config) + @mock.patch.object( + common.PowerMaxCommon, '_initial_setup', + return_value=tpd.PowerMaxData.ex_specs_rep_config) + @mock.patch.object(volume_utils, 'is_group_a_type', + return_value=True) + @mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type', + return_value=True) + @mock.patch.object( + masking.PowerMaxMasking, 'remove_volumes_from_storage_group') + def test_update_group_promotion( + self, mck_rem, mock_cg_type, mock_type_check, mck_setup, mck_rep, + mck_in_sg): + group = self.data.test_rep_group + add_vols = [] + remove_vols = [self.data.test_volume_group_member] + remote_array = self.data.remote_array + device_id = [self.data.device_id] + group_name = self.data.storagegroup_name_source + interval_retries_dict = {utils.INTERVAL: 1, + utils.RETRIES: 1, + utils.FORCE_VOL_REMOVE: True} + self.common._update_group_promotion(group, add_vols, remove_vols) + mck_rem.assert_called_once_with( + remote_array, device_id, group_name, interval_retries_dict) + + @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_update_group_promotion_non_replicated( + self, mock_cg_type, mock_type_check): + group = self.data.test_group_failed + add_vols = [] + remove_vols = [self.data.test_volume_group_member] + self.assertRaises(exception.VolumeBackendAPIException, + self.common._update_group_promotion, + group, add_vols, remove_vols) + + @mock.patch.object(volume_utils, 'is_group_a_type', + return_value=True) + @mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type', + return_value=True) + def test_update_group_promotion_add_volumes( + self, mock_cg_type, mock_type_check): + group = self.data.test_rep_group + add_vols = [self.data.test_volume_group_member] + remove_vols = [] + self.assertRaises(exception.VolumeBackendAPIException, + self.common._update_group_promotion, + group, add_vols, remove_vols) + @mock.patch.object(volume_utils, 'is_group_a_type', return_value=False) def test_delete_group(self, mock_check): group = self.data.test_group_1 @@ -3702,7 +3976,7 @@ class PowerMaxCommonTest(test.TestCase): device_id = self.data.device_id volume = self.data.test_attached_volume volume_name = self.data.volume_id - extra_specs = self.data.rep_extra_specs + extra_specs = self.data.rep_extra_specs_rep_config target_slo = self.data.slo_silver target_workload = self.data.workload target_extra_specs = deepcopy(self.data.rep_extra_specs) @@ -3738,7 +4012,7 @@ class PowerMaxCommonTest(test.TestCase): device_id = self.data.device_id volume = self.data.test_attached_volume volume_name = self.data.volume_id - extra_specs = self.data.rep_extra_specs + extra_specs = self.data.rep_extra_specs_rep_config target_slo = self.data.slo_silver target_workload = self.data.workload target_extra_specs = deepcopy(self.data.rep_extra_specs) diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_replication.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_replication.py index fc2ab33ed27..90002890ef1 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_replication.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_replication.py @@ -269,7 +269,7 @@ class PowerMaxReplicationTest(test.TestCase): secondary_id, volume_update_list, group_update_list = ( self.common.failover_host(volumes, backend_id, groups)) mck_validate.assert_called_once_with( - False, backend_id, rep_configs, self.data.array, ['123']) + False, backend_id, rep_configs, self.data.array, ['123'], False) mck_populate.assert_called_once_with(volumes, groups, None) self.assertEqual(backend_id, secondary_id) self.assertEqual('vol_list', volume_update_list) @@ -286,7 +286,56 @@ class PowerMaxReplicationTest(test.TestCase): self.assertRaises(exception.InvalidReplicationTarget, self.common.failover_host, volumes, backend_id) mck_validate.assert_called_once_with( - False, backend_id, rep_configs, self.data.array, ['123']) + False, backend_id, rep_configs, self.data.array, ['123'], False) + + @mock.patch.object( + common.PowerMaxCommon, '_populate_volume_and_group_update_lists') + @mock.patch.object(utils.PowerMaxUtils, 'validate_failover_request', + return_value=(True, 'val')) + @mock.patch.object(rest.PowerMaxRest, 'get_arrays_list', + return_value=['123']) + def test_failover_host_start_promotion( + self, mck_arrays, mck_validate, mck_populate): + volumes = [self.data.test_volume, self.data.test_clone_volume] + groups = [self.data.test_group] + backend_id = utils.PMAX_FAILOVER_START_ARRAY_PROMOTION + rep_configs = self.common.rep_configs + secondary_id, volume_update_list, group_update_list = ( + self.common.failover_host(volumes, backend_id, groups)) + self.assertEqual(0, mck_populate.call_count) + self.assertEqual(backend_id, secondary_id) + self.assertEqual(list(), volume_update_list) + self.assertEqual(list(), group_update_list) + self.assertEqual(self.common.promotion, True) + self.common.promotion = False + mck_validate.assert_called_once_with( + False, backend_id, rep_configs, self.data.array, ['123'], False) + + @mock.patch.object( + common.PowerMaxCommon, '_populate_volume_and_group_update_lists', + return_value=(list(), list())) + @mock.patch.object(utils.PowerMaxUtils, 'validate_failover_request', + return_value=(True, 'val')) + @mock.patch.object(rest.PowerMaxRest, 'get_arrays_list', + return_value=['123']) + def test_failover_host_complete_promotion( + self, mck_arrays, mck_validate, mck_populate): + volume = deepcopy(self.data.test_rep_volume) + volume.replication_status = fields.ReplicationStatus.ERROR + volumes = [volume] + groups = [self.data.test_group] + backend_id = 'default' + rep_configs = self.common.rep_configs + self.common.promotion = True + secondary_id, volume_update_list, group_update_list = ( + self.common.failover_host(volumes, backend_id, groups)) + mck_populate.assert_called_once_with(volumes, groups, None) + mck_validate.assert_called_once_with( + False, backend_id, rep_configs, self.data.array, ['123'], True) + self.assertEqual(backend_id, secondary_id) + self.assertEqual(list(), volume_update_list) + self.assertEqual(list(), group_update_list) + self.assertEqual(self.common.promotion, False) @mock.patch.object(common.PowerMaxCommon, '_update_volume_list_from_sync_vol_list', @@ -316,6 +365,23 @@ class PowerMaxReplicationTest(test.TestCase): 'updates': 'grp_updates'}] self.assertEqual(group_updates_ref, group_updates) + @mock.patch.object(common.PowerMaxCommon, '_initial_setup', + return_value=tpd.PowerMaxData.extra_specs) + def test_populate_volume_and_group_update_lists_promotion_non_rep( + self, mck_setup): + volumes = [self.data.test_volume] + groups = [] + ref_model_update = { + 'volume_id': volumes[0].id, + 'updates': { + 'replication_status': fields.ReplicationStatus.DISABLED}} + self.common.promotion = True + volume_updates, group_updates = ( + self.common._populate_volume_and_group_update_lists( + volumes, groups, None)) + self.common.promotion = False + self.assertEqual(ref_model_update, volume_updates[0]) + def test_failover_replication_empty_group(self): with mock.patch.object(volume_utils, 'is_group_a_type', return_value=True): @@ -370,6 +436,25 @@ class PowerMaxReplicationTest(test.TestCase): self.assertEqual(fields.ReplicationStatus.ERROR, model_update['replication_status']) + @mock.patch.object(common.PowerMaxCommon, '_rdf_vols_partitioned', + return_value=True) + @mock.patch.object(rest.PowerMaxRest, 'srdf_failover_group', + return_value=tpd.PowerMaxData.rdf_group_no_1) + @mock.patch.object(common.PowerMaxCommon, 'get_rdf_details', + return_value=tpd.PowerMaxData.rdf_group_no_1) + @mock.patch.object(common.PowerMaxCommon, '_find_volume_group', + return_value=tpd.PowerMaxData.test_group) + def test_failover_replication_failover_partitioned( + self, mck_find_vol_grp, mck_get_rdf_grp, mck_failover, mck_part): + volumes = [self.data.test_volume_group_member] + vol_group = self.data.test_group + vol_grp_name = self.data.test_group.name + model_update, __ = self.common._failover_replication( + volumes, vol_group, vol_grp_name, host=True) + self.assertEqual(fields.ReplicationStatus.FAILED_OVER, + model_update['replication_status']) + self.assertEqual(0, mck_failover.call_count) + @mock.patch.object(common.PowerMaxCommon, '_failover_replication', return_value=({}, {})) @mock.patch.object(common.PowerMaxCommon, '_sync_check') @@ -438,6 +523,60 @@ class PowerMaxReplicationTest(test.TestCase): extra_specs, rep_config) self.assertEqual(rep_specs, rep_extra_specs) + @mock.patch.object( + rest.PowerMaxRest, 'get_rdf_pair_volume', + return_value={utils.RDF_PAIR_STATE: utils.RDF_PARTITIONED_STATE}) + def test_rdf_vols_partitioned_true_partitioned(self, mck_pair): + array = self.data.array + volumes = [self.data.test_rep_volume] + rdfg = self.data.rdf_group_no_1 + device_id = self.data.device_id2 + is_partitioned = self.common._rdf_vols_partitioned( + array, volumes, rdfg) + self.assertTrue(is_partitioned) + mck_pair.assert_called_once_with(array, rdfg, device_id) + + @mock.patch.object( + rest.PowerMaxRest, 'get_rdf_pair_volume', + return_value={utils.RDF_PAIR_STATE: utils.RDF_TRANSIDLE_STATE}) + def test_rdf_vols_partitioned_true_transidle(self, mck_pair): + array = self.data.array + volumes = [self.data.test_rep_volume] + rdfg = self.data.rdf_group_no_1 + device_id = self.data.device_id2 + is_partitioned = self.common._rdf_vols_partitioned( + array, volumes, rdfg) + self.assertTrue(is_partitioned) + mck_pair.assert_called_once_with(array, rdfg, device_id) + + @mock.patch.object( + rest.PowerMaxRest, 'get_rdf_pair_volume', + return_value={utils.RDF_PAIR_STATE: utils.RDF_SUSPENDED_STATE}) + def test_rdf_vols_partitioned_false(self, mck_pair): + array = self.data.array + volumes = [self.data.test_rep_volume] + rdfg = self.data.rdf_group_no_1 + device_id = self.data.device_id2 + is_partitioned = self.common._rdf_vols_partitioned( + array, volumes, rdfg) + self.assertFalse(is_partitioned) + mck_pair.assert_called_once_with(array, rdfg, device_id) + + @mock.patch.object( + rest.PowerMaxRest, 'get_rdf_pair_volume', + return_value={utils.RDF_PAIR_STATE: utils.RDF_PARTITIONED_STATE}) + def test_rdf_vols_partitioned_true_promotion(self, mck_pair): + self.common.promotion = True + array = self.data.array + volumes = [self.data.test_rep_volume] + rdfg = self.data.rdf_group_no_1 + device_id = self.data.device_id + is_partitioned = self.common._rdf_vols_partitioned( + array, volumes, rdfg) + self.assertTrue(is_partitioned) + self.common.promotion = False + mck_pair.assert_called_once_with(array, rdfg, device_id) + def test_get_secondary_stats(self): rep_config = self.data.rep_config_sync array_map = self.common.get_attributes_from_cinder_config() @@ -1001,6 +1140,86 @@ class PowerMaxReplicationTest(test.TestCase): self.assertTrue(success) self.assertEqual(self.data.replication_model, model_update) + @mock.patch.object( + provision.PowerMaxProvision, 'verify_slo_workload', + return_value=(True, True)) + @mock.patch.object( + common.PowerMaxCommon, 'break_rdf_device_pair_session_promotion') + @mock.patch.object( + common.PowerMaxCommon, 'get_volume_metadata', return_value='') + @mock.patch.object( + common.PowerMaxCommon, '_retype_volume', + return_value=(True, tpd.PowerMaxData.defaultstoragegroup_name)) + def test_migrate_volume_success_rep_promotion( + self, mck_retype, mck_get, mck_break, mck_valid): + array_id = self.data.array + volume = self.data.test_rep_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 + new_type = {'extra_specs': {}} + extra_specs = self.data.rep_extra_specs_rep_config + self.common.promotion = True + target_extra_specs = { + utils.SRP: srp, utils.ARRAY: array_id, utils.SLO: target_slo, + utils.WORKLOAD: target_workload, + utils.INTERVAL: extra_specs[utils.INTERVAL], + utils.RETRIES: extra_specs[utils.RETRIES], + utils.DISABLECOMPRESSION: False} + success, model_update = self.common._migrate_volume( + array_id, volume, device_id, srp, target_slo, target_workload, + volume_name, new_type, extra_specs) + mck_break.assert_called_once_with( + array_id, device_id, volume_name, extra_specs) + mck_retype.assert_called_once_with( + array_id, srp, device_id, volume, volume_name, extra_specs, + target_slo, target_workload, target_extra_specs) + self.assertTrue(success) + self.common.promotion = False + + @mock.patch.object( + common.PowerMaxCommon, '_rdf_vols_partitioned', + return_value=True) + @mock.patch.object( + provision.PowerMaxProvision, 'verify_slo_workload', + return_value=(True, True)) + @mock.patch.object( + common.PowerMaxCommon, 'break_rdf_device_pair_session_promotion') + @mock.patch.object( + common.PowerMaxCommon, 'get_volume_metadata', return_value='') + @mock.patch.object( + common.PowerMaxCommon, '_retype_volume', + return_value=(True, tpd.PowerMaxData.defaultstoragegroup_name)) + def test_migrate_volume_success_rep_partitioned( + self, mck_retype, mck_get, mck_break, mck_valid, mck_partitioned): + array_id = self.data.array + volume = self.data.test_rep_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 + new_type = {'extra_specs': {}} + extra_specs = self.data.rep_extra_specs_rep_config + self.common.promotion = True + target_extra_specs = { + utils.SRP: srp, utils.ARRAY: array_id, utils.SLO: target_slo, + utils.WORKLOAD: target_workload, + utils.INTERVAL: extra_specs[utils.INTERVAL], + utils.RETRIES: extra_specs[utils.RETRIES], + utils.DISABLECOMPRESSION: False} + success, model_update = self.common._migrate_volume( + array_id, volume, device_id, srp, target_slo, target_workload, + volume_name, new_type, extra_specs) + self.assertEqual(0, mck_break.call_count) + mck_retype.assert_called_once_with( + array_id, srp, device_id, volume, volume_name, extra_specs, + target_slo, target_workload, target_extra_specs) + self.assertTrue(success) + self.common.promotion = False + @mock.patch.object(masking.PowerMaxMasking, 'add_volume_to_storage_group') @mock.patch.object(provision.PowerMaxProvision, 'get_or_create_group') @mock.patch.object(utils.PowerMaxUtils, 'get_rdf_management_group_name', @@ -1414,6 +1633,26 @@ class PowerMaxReplicationTest(test.TestCase): self.assertEqual(extra_specs[utils.REP_CONFIG], rep_extra_specs) self.assertTrue(resume_rdf) + @mock.patch.object(masking.PowerMaxMasking, 'remove_volume_from_sg') + @mock.patch.object(rest.PowerMaxRest, 'srdf_delete_device_pair') + @mock.patch.object(utils.PowerMaxUtils, 'get_rdf_management_group_name', + return_value=tpd.PowerMaxData.rdf_managed_async_grp) + def test_break_rdf_device_pair_session_promotion_metro( + self, mck_get, mck_del, mck_rem): + array = self.data.array + device_id = self.data.device_id + volume_name = self.data.test_rep_volume.name + extra_specs = self.data.ex_specs_rep_config + rep_config = extra_specs[utils.REP_CONFIG] + mgmt_group = self.data.rdf_managed_async_grp + rdfg_no = extra_specs['rdf_group_no'] + self.common.break_rdf_device_pair_session_promotion( + array, device_id, volume_name, extra_specs) + mck_get.assert_called_once_with(rep_config) + mck_del.assert_called_once_with(array, rdfg_no, device_id) + mck_rem.assert_called_once_with( + array, device_id, volume_name, mgmt_group, extra_specs) + @mock.patch.object(rest.PowerMaxRest, 'get_rdf_group', return_value=tpd.PowerMaxData.rdf_group_details) @mock.patch.object( diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_utils.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_utils.py index 77087f859db..3a7ffb0e970 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_utils.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_utils.py @@ -354,6 +354,23 @@ class PowerMaxUtilsTest(test.TestCase): {'pool_name': 'Diamond+SRP_1+000197800111'}] self.assertEqual(ref_pools, new_pools) + def test_add_promotion_pools(self): + array = self.data.array + pools = [{'pool_name': 'Diamond+None+SRP_1+000197800111', + 'location_info': '000197800111#SRP_1#None#Diamond'}, + {'pool_name': 'Gold+OLTP+SRP_1+000197800111', + 'location_info': '000197800111#SRP_1#OLTP#Gold'}] + new_pools = self.utils.add_promotion_pools(pools, array) + ref_pools = [{'pool_name': 'Diamond+None+SRP_1+000197800111', + 'location_info': '000197800111#SRP_1#None#Diamond'}, + {'pool_name': 'Gold+OLTP+SRP_1+000197800111', + 'location_info': '000197800111#SRP_1#OLTP#Gold'}, + {'pool_name': 'Diamond+None+SRP_1+000197800123', + 'location_info': '000197800123#SRP_1#None#Diamond'}, + {'pool_name': 'Gold+OLTP+SRP_1+000197800123', + 'location_info': '000197800123#SRP_1#OLTP#Gold'}] + self.assertEqual(ref_pools, new_pools) + def test_update_volume_group_name(self): group = self.data.test_group_1 ref_group_name = self.data.test_vol_grp_name @@ -1277,6 +1294,15 @@ class PowerMaxUtilsTest(test.TestCase): self.utils.validate_multiple_rep_device, rep_devices) + def test_validate_multiple_rep_device_promotion_start_backend_id(self): + backend_id = utils.PMAX_FAILOVER_START_ARRAY_PROMOTION + rep_devices = deepcopy(self.data.multi_rep_device) + rep_devices[0][utils.BACKEND_ID] = backend_id + self.assertRaises( + exception.InvalidConfigurationValue, + self.utils.validate_multiple_rep_device, + rep_devices) + def test_validate_multiple_rep_device_missing_backend_id(self): rep_devices = deepcopy(self.data.multi_rep_device) rep_devices[0].pop(utils.BACKEND_ID) @@ -1357,6 +1383,12 @@ class PowerMaxUtilsTest(test.TestCase): excep_msg = str(e) self.assertIn(expected_str, excep_msg) + def test_get_rep_config_promotion_stats(self): + rep_configs = self.data.multi_rep_config_list + backend_id = 'testing' + rep_device = self.utils.get_rep_config(backend_id, rep_configs, True) + self.assertEqual(rep_configs[0], rep_device) + def test_get_replication_targets(self): rep_targets_expected = [self.data.remote_array] rep_configs = self.data.multi_rep_config_list @@ -1365,25 +1397,27 @@ class PowerMaxUtilsTest(test.TestCase): def test_validate_failover_request_success(self): is_failed_over = False + is_promoted = False failover_backend_id = self.data.rep_backend_id_sync rep_configs = self.data.multi_rep_config_list primary_array = self.data.array array_list = [self.data.array] is_valid, msg = self.utils.validate_failover_request( is_failed_over, failover_backend_id, rep_configs, - primary_array, array_list) + primary_array, array_list, is_promoted) self.assertTrue(is_valid) self.assertEqual("", msg) def test_validate_failover_request_already_failed_over(self): is_failed_over = True + is_promoted = False failover_backend_id = self.data.rep_backend_id_sync rep_configs = self.data.multi_rep_config_list primary_array = self.data.array array_list = [self.data.array] is_valid, msg = self.utils.validate_failover_request( is_failed_over, failover_backend_id, rep_configs, - primary_array, array_list) + primary_array, array_list, is_promoted) self.assertFalse(is_valid) expected_msg = ('Cannot failover, the backend is already in a failed ' 'over state, if you meant to failback, please add ' @@ -1392,13 +1426,14 @@ class PowerMaxUtilsTest(test.TestCase): def test_validate_failover_request_failback_missing_array(self): is_failed_over = True + is_promoted = False failover_backend_id = 'default' rep_configs = self.data.multi_rep_config_list primary_array = self.data.array array_list = [self.data.remote_array] is_valid, msg = self.utils.validate_failover_request( is_failed_over, failover_backend_id, rep_configs, - primary_array, array_list) + primary_array, array_list, is_promoted) self.assertFalse(is_valid) expected_msg = ('Cannot failback, the configured primary array is ' 'not currently available to perform failback to. ' @@ -1406,15 +1441,33 @@ class PowerMaxUtilsTest(test.TestCase): 'Unisphere.') % primary_array self.assertEqual(expected_msg, msg) + def test_validate_failover_request_promotion_finalize(self): + is_failed_over = True + is_promoted = True + failover_backend_id = utils.PMAX_FAILOVER_START_ARRAY_PROMOTION + rep_configs = self.data.multi_rep_config_list + primary_array = self.data.array + array_list = [self.data.array] + is_valid, msg = self.utils.validate_failover_request( + is_failed_over, failover_backend_id, rep_configs, + primary_array, array_list, is_promoted) + self.assertFalse(is_valid) + expected_msg = ('Failover promotion currently in progress, please ' + 'finish the promotion process and issue a failover ' + 'using the "default" backend_id to complete this ' + 'process.') + self.assertEqual(expected_msg, msg) + def test_validate_failover_request_invalid_failback(self): is_failed_over = False + is_promoted = False failover_backend_id = 'default' rep_configs = self.data.multi_rep_config_list primary_array = self.data.array array_list = [self.data.array] is_valid, msg = self.utils.validate_failover_request( is_failed_over, failover_backend_id, rep_configs, - primary_array, array_list) + primary_array, array_list, is_promoted) self.assertFalse(is_valid) expected_msg = ('Cannot failback, backend is not in a failed over ' 'state. If you meant to failover, please either omit ' @@ -1424,13 +1477,14 @@ class PowerMaxUtilsTest(test.TestCase): def test_validate_failover_request_no_backend_id_multi_rep(self): is_failed_over = False + is_promoted = False failover_backend_id = None rep_configs = self.data.multi_rep_config_list primary_array = self.data.array array_list = [self.data.array] is_valid, msg = self.utils.validate_failover_request( is_failed_over, failover_backend_id, rep_configs, - primary_array, array_list) + primary_array, array_list, is_promoted) self.assertFalse(is_valid) expected_msg = ('Cannot failover, no backend_id provided while ' 'multiple replication devices are defined in ' @@ -1441,6 +1495,7 @@ class PowerMaxUtilsTest(test.TestCase): def test_validate_failover_request_incorrect_backend_id_multi_rep(self): is_failed_over = False + is_promoted = False failover_backend_id = 'invalid_id' rep_configs = self.data.multi_rep_config_list primary_array = self.data.array @@ -1448,7 +1503,23 @@ class PowerMaxUtilsTest(test.TestCase): self.assertRaises(exception.InvalidInput, self.utils.validate_failover_request, is_failed_over, failover_backend_id, rep_configs, - primary_array, array_list) + primary_array, array_list, is_promoted) + + def test_validate_failover_request_promotion_before_failover(self): + is_failed_over = False + is_promoted = False + failover_backend_id = utils.PMAX_FAILOVER_START_ARRAY_PROMOTION + rep_configs = self.data.multi_rep_config_list + primary_array = self.data.array + array_list = [self.data.array] + is_valid, msg = self.utils.validate_failover_request( + is_failed_over, failover_backend_id, rep_configs, + primary_array, array_list, is_promoted) + self.assertFalse(is_valid) + expected_msg = ('Cannot start failover promotion. The backend must ' + 'already be in a failover state to perform this' + 'action.') + self.assertEqual(expected_msg, msg) def test_validate_replication_group_config_success(self): rep_configs = deepcopy(self.data.multi_rep_config_list) diff --git a/cinder/volume/drivers/dell_emc/powermax/common.py b/cinder/volume/drivers/dell_emc/powermax/common.py index 05fd56b9d87..83e8207ed2d 100644 --- a/cinder/volume/drivers/dell_emc/powermax/common.py +++ b/cinder/volume/drivers/dell_emc/powermax/common.py @@ -219,9 +219,12 @@ class PowerMaxCommon(object): self.replication_enabled = False self.rep_devices = [] self.failover = True if active_backend_id else False + self.promotion = False self.powermax_array_tag_list = None self.powermax_short_host_name_template = None self.powermax_port_group_name_template = None + if active_backend_id == utils.PMAX_FAILOVER_START_ARRAY_PROMOTION: + self.promotion = True # Gather environment info self._get_replication_info() @@ -850,7 +853,8 @@ class PowerMaxCommon(object): array, volume, device_info['device_id'], extra_specs, connector, is_multiattach, async_grp=mgmt_sg_name, host_template=self.powermax_short_host_name_template) - if self.utils.is_metro_device(rep_config, extra_specs): + if (self.utils.is_metro_device(rep_config, extra_specs) and + not self.promotion): # Need to remove from remote masking view device_info, __ = (self.find_host_lun_id( volume, host_name, extra_specs, rep_extra_specs)) @@ -875,6 +879,31 @@ class PowerMaxCommon(object): volume, extra_specs, device_info['device_id'], mv_list, sg_list) + def _unmap_lun_promotion(self, volume, connector): + """Unmaps a volume from the host during promotion. + + :param volume: the volume Object + :param connector: the connector Object + """ + extra_specs = self._initial_setup(volume) + if not self.utils.is_replication_enabled(extra_specs): + LOG.error('Unable to terminate connections for non-replicated ' + 'volumes during promotion failover. Could not unmap ' + 'volume %s', volume.id) + else: + mode = extra_specs[utils.REP_MODE] + if mode == utils.REP_METRO: + self._unmap_lun(volume, connector) + else: + # During a promotion scenario only Metro volumes will have + # connections present on their remote volumes. + loc = ast.literal_eval(volume.provider_location) + device_id = loc.get('device_id') + promotion_key = [utils.PMAX_FAILOVER_START_ARRAY_PROMOTION] + self.volume_metadata.capture_detach_info( + volume, extra_specs, device_id, promotion_key, + promotion_key) + def initialize_connection(self, volume, connector): """Initializes the connection and returns device and connection info. @@ -1116,7 +1145,10 @@ class PowerMaxCommon(object): volume_name = volume.name LOG.info("Terminate connection: %(volume)s.", {'volume': volume_name}) - self._unmap_lun(volume, connector) + if self.promotion: + self._unmap_lun_promotion(volume, connector) + else: + self._unmap_lun(volume, connector) def extend_volume(self, volume, new_size): """Extends an existing volume. @@ -1333,7 +1365,7 @@ class PowerMaxCommon(object): for array_info in array_info_list: if self.failover: rep_config = self.utils.get_rep_config( - self.active_backend_id, self.rep_configs) + self.active_backend_id, self.rep_configs, True) array_info = self.get_secondary_stats_info( rep_config, array_info) # Add both SLO & Workload name in the pool name @@ -1393,6 +1425,9 @@ class PowerMaxCommon(object): pools.append(pool) pools = self.utils.add_legacy_pools(pools) + if self.promotion: + primary_array = self.configuration.safe_get('powermax_array') + pools = self.utils.add_promotion_pools(pools, primary_array) data = {'vendor_name': "Dell EMC", 'driver_version': self.version, 'storage_protocol': 'unknown', @@ -3798,6 +3833,11 @@ class PowerMaxCommon(object): {'volume': volume_name}) extra_specs = self._initial_setup(volume) + if self.utils.is_replication_enabled(extra_specs) and self.promotion: + rep_config = extra_specs.get('rep_config') + extra_specs = self._get_replication_extra_specs( + extra_specs, rep_config) + if not self.utils.is_retype_supported(volume, extra_specs, new_type['extra_specs'], self.rep_configs): @@ -3839,17 +3879,21 @@ class PowerMaxCommon(object): # Check if old type and new type have different compression types do_change_compression = (self.utils.change_compression_type( is_compression_disabled, new_type)) + is_tgt_rep = self.utils.is_replication_enabled( + new_type[utils.EXTRA_SPECS]) is_valid, target_slo, target_workload = ( self._is_valid_for_storage_assisted_migration( device_id, host, extra_specs[utils.ARRAY], extra_specs[utils.SRP], volume_name, - do_change_compression, do_change_replication)) + do_change_compression, do_change_replication, + extra_specs[utils.SLO], extra_specs[utils.WORKLOAD], + is_tgt_rep)) if not is_valid: # Check if this is multiattach retype case do_change_multiattach = self.utils.change_multiattach( extra_specs, new_type['extra_specs']) - if do_change_multiattach: + if do_change_multiattach and not self.promotion: return True else: LOG.error( @@ -3934,7 +3978,7 @@ class PowerMaxCommon(object): utils.BACKEND_ID_LEGACY_REP) backend_ids_differ = curr_backend_id != tgt_backend_id - if was_rep_enabled: + if was_rep_enabled and not self.promotion: self._validate_rdfg_status(array, extra_specs) orig_mgmt_sg_name = self.utils.get_rdf_management_group_name( extra_specs[utils.REP_CONFIG]) @@ -3953,11 +3997,23 @@ class PowerMaxCommon(object): # 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: - rep_extra_specs, resume_original_sg = ( - self.break_rdf_device_pair_session( - array, device_id, volume_name, extra_specs, volume)) + 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': REPLICATION_DISABLED, + 'replication_status': status, 'replication_driver_data': None} rdf_pair_broken = True if resume_original_sg: @@ -4014,7 +4070,8 @@ class PowerMaxCommon(object): self.rest.srdf_resume_replication( array, rep_extra_specs['mgmt_sg_name'], rep_extra_specs['rdf_group_no'], rep_extra_specs) - if resume_original_sg and resume_original_sg_dict: + if (resume_original_sg and resume_original_sg_dict and + not self.promotion): self.rest.srdf_resume_replication( resume_original_sg_dict[utils.ARRAY], resume_original_sg_dict[utils.SG_NAME], @@ -4026,6 +4083,14 @@ class PowerMaxCommon(object): model_update, volume.metadata, self.get_volume_metadata(array, device_id)) + if self.promotion: + previous_host = volume.get('host') + host_details = previous_host.split('+') + array_index = len(host_details) - 1 + host_details[array_index] = array + updated_host = '+'.join(host_details) + model_update['host'] = updated_host + target_backend_id = None if is_rep_enabled: target_backend_id = target_extra_specs.get( @@ -4054,6 +4119,19 @@ class PowerMaxCommon(object): self, rdf_pair_broken, rdf_pair_created, vol_retyped, remote_retyped, extra_specs, target_extra_specs, volume, volume_name, device_id, source_sg): + """Attempt rollback to previous volume state before migrate exception. + + :param rdf_pair_broken: was the rdf pair broken during migration + :param rdf_pair_created: was a new rdf pair created during migration + :param vol_retyped: was the local volume retyped during migration + :param remote_retyped: was the remote volume retyped during migration + :param extra_specs: extra specs + :param target_extra_specs: target extra specs + :param volume: volume + :param volume_name: volume name + :param device_id: local device id + :param source_sg: local device pre-migrate storage group name + """ array = extra_specs[utils.ARRAY] srp = extra_specs[utils.SRP] slo = extra_specs[utils.SLO] @@ -4143,8 +4221,9 @@ class PowerMaxCommon(object): parent_sg = None if self.utils.is_replication_enabled(target_extra_specs): is_re, rep_mode = True, target_extra_specs['rep_mode'] + if self.utils.is_replication_enabled(extra_specs): mgmt_sg_name = self.utils.get_rdf_management_group_name( - target_extra_specs[utils.REP_CONFIG]) + extra_specs[utils.REP_CONFIG]) device_info = self.rest.get_volume(array, device_id) @@ -4242,6 +4321,21 @@ class PowerMaxCommon(object): self, created_child_sg, add_sg_to_parent, got_default_sg, moved_between_sgs, array, source_sg, parent_sg, target_sg_name, extra_specs, device_id, volume, volume_name): + """Attempt to rollback to previous volume state on retype exception. + + :param created_child_sg: was a child sg created during retype + :param add_sg_to_parent: was a child sg added to parent during retype + :param got_default_sg: was a default sg possibly created during retype + :param moved_between_sgs: was the volume moved between storage groups + :param array: array + :param source_sg: volumes originating storage group name + :param parent_sg: parent storage group name + :param target_sg_name: storage group volume was to be moved to + :param extra_specs: extra specs + :param device_id: device id + :param volume: volume + :param volume_name: volume name + """ if moved_between_sgs: LOG.debug('Volume retype cleanup - Attempt to revert move between ' 'storage groups.') @@ -4416,7 +4510,8 @@ class PowerMaxCommon(object): def _is_valid_for_storage_assisted_migration( self, device_id, host, source_array, source_srp, volume_name, - do_change_compression, do_change_replication): + do_change_compression, do_change_replication, source_slo, + source_workload, is_tgt_rep): """Check if volume is suitable for storage assisted (pool) migration. :param device_id: the volume device id @@ -4426,6 +4521,9 @@ class PowerMaxCommon(object): :param volume_name: the name of the volume to be migrated :param do_change_compression: do change compression :param do_change_replication: flag indicating replication change + :param source_slo: slo setting for source volume type + :param source_workload: workload setting for source volume type + :param is_tgt_rep: is the target volume type replication enabled :returns: boolean -- True/False :returns: string -- targetSlo :returns: string -- targetWorkload @@ -4460,24 +4558,53 @@ class PowerMaxCommon(object): LOG.error("Error parsing array, pool, SLO and workload.") return false_ret - if target_array_serial not in source_array: - LOG.error( - "The source array: %(source_array)s does not " - "match the target array: %(target_array)s - " - "skipping storage-assisted migration.", - {'source_array': source_array, - 'target_array': target_array_serial}) - return false_ret + if self.promotion: + if do_change_compression: + LOG.error( + "When retyping during array promotion, compression " + "changes should not occur during the retype operation. " + "Please ensure the same compression settings are defined " + "in the source and target volume types.") + return false_ret - if target_srp not in source_srp: - LOG.error( - "Only SLO/workload migration within the same SRP Pool is " - "supported in this version. The source pool: " - "%(source_pool_name)s does not match the target array: " - "%(target_pool)s. Skipping storage-assisted migration.", - {'source_pool_name': source_srp, - 'target_pool': target_srp}) - return false_ret + if source_slo != target_slo: + LOG.error( + "When retyping during array promotion, the SLO setting " + "for the source and target volume types should match. " + "Found %s SLO for the source volume type and %s SLO for " + "the target volume type.", source_slo, target_slo) + return false_ret + + if source_workload != target_workload: + LOG.error( + "When retyping during array promotion, the workload " + "setting for the source and target volume types should " + "match. Found %s workload for the source volume type " + "and %s workload for the target volume type.", + source_workload, target_workload) + return false_ret + + if is_tgt_rep: + LOG.error( + "When retyping during array promotion, the target volume " + "type should not have replication enabled. Please ensure " + "replication is disabled on the target volume type.") + return false_ret + + if not self.promotion: + if target_array_serial not in source_array: + LOG.error("The source array: %s does not match the target " + "array: %s - skipping storage-assisted " + "migration.", source_array, target_array_serial) + return false_ret + + if target_srp not in source_srp: + LOG.error( + "Only SLO/workload migration within the same SRP Pool is " + "supported in this version. The source pool: %s does not " + "match the target array: %s. Skipping storage-assisted " + "migration.", source_srp, target_srp) + return false_ret found_storage_group_list = self.rest.get_storage_groups_from_volume( source_array, device_id) @@ -4608,6 +4735,23 @@ class PowerMaxCommon(object): add_to_mgmt_sg, r1_device_id, r2_device_id, mgmt_sg_name, array, remote_array, rdf_group_no, extra_specs, rep_extra_specs, volume, tgt_sg_name): + """Attempt rollback to previous volume state on setup rep exception. + + :param resume_rdf: does the rdfg need to be resumed + :param rdf_pair_created: was an rdf pair created + :param remote_sg_get: was a remote storage group possibly created + :param add_to_mgmt_sg: was the volume added to a management group + :param r1_device_id: local device id + :param r2_device_id: remote device id + :param mgmt_sg_name: rdf management storage group name + :param array: array + :param remote_array: remote array + :param rdf_group_no: rdf group number + :param extra_specs: extra specs + :param rep_extra_specs: rep extra specs + :param volume: volume + :param tgt_sg_name: remote replication storage group name + """ if resume_rdf and not rdf_pair_created: LOG.debug('Configure volume replication cleanup - Attempt to ' 'resume replication.') @@ -4748,7 +4892,7 @@ class PowerMaxCommon(object): sg_name = r1_sg_names[0] rdf_pair = self.rest.get_rdf_pair_volume( array, rdfg_no, device_id) - rdf_pair_state = rdf_pair['rdfpairState'] + rdf_pair_state = rdf_pair[utils.RDF_PAIR_STATE] if rdf_pair_state.lower() not in utils.RDF_SYNCED_STATES: self.rest.wait_for_rdf_pair_sync( array, rdfg_no, device_id, rep_extra_specs) @@ -4804,6 +4948,23 @@ class PowerMaxCommon(object): management_sg, rdf_group_no, extra_specs, r2_sg_names, device_id, remote_array, remote_device_id, volume, volume_name, rep_extra_specs): + """Attempt rollback to previous volume state on remove rep exception. + + :param rdfg_suspended: was the rdf group suspended + :param pair_deleted: was the rdf pair deleted + :param r2_sg_remove: was the remote volume removed from its sg + :param array: array + :param management_sg: rdf management storage group name + :param rdf_group_no: rdf group number + :param extra_specs: extra specs + :param r2_sg_names: remote volume storage group names + :param device_id: device id + :param remote_array: remote array sid + :param remote_device_id: remote device id + :param volume: volume + :param volume_name: volume name + :param rep_extra_specs: rep extra specs + """ if rdfg_suspended and not pair_deleted: LOG.debug('Break RDF pair cleanup - Attempt to resume RDFG.') self.rest.srdf_resume_replication( @@ -4844,6 +5005,54 @@ class PowerMaxCommon(object): LOG.debug('Break RDF pair cleanup - Revert to original rdf ' 'pair successful.') + def break_rdf_device_pair_session_promotion( + self, array, device_id, volume_name, extra_specs): + """Delete RDF device pair deleting R2 volume but leaving R1 in place. + + :param array: the array serial number + :param device_id: the device id + :param volume_name: the volume name + :param extra_specs: the volume extra specifications + """ + LOG.debug('Starting promotion replication cleanup for RDF pair ' + 'source device: %(d_id)s.', {'d_id': device_id}) + + mgmt_sg_name = None + rep_config = extra_specs[utils.REP_CONFIG] + rdfg_no = extra_specs['rdf_group_no'] + extra_specs['force_vol_remove'] = True + if rep_config['mode'] in [utils.REP_ASYNC, utils.REP_METRO]: + mgmt_sg_name = self.utils.get_rdf_management_group_name( + rep_config) + + if rep_config['mode'] == utils.REP_METRO: + group_states = self.rest.get_storage_group_rdf_group_state( + array, mgmt_sg_name, rdfg_no) + group_states = set([x.lower() for x in group_states]) + metro_active_states = { + utils.RDF_ACTIVE, utils.RDF_ACTIVEACTIVE, utils.RDF_ACTIVEBIAS} + active_state_found = ( + bool(group_states.intersection(metro_active_states))) + if active_state_found: + LOG.debug('Found Metro RDF in active state during promotion, ' + 'attempting to suspend.') + try: + self.rest.srdf_suspend_replication( + array, mgmt_sg_name, rdfg_no, extra_specs) + except exception.VolumeBackendAPIException: + LOG.error( + 'Found Metro rdf pair in active state during ' + 'promotion. Attempt to suspend this group using ' + 'storage group %s failed. Please move the rdf pairs ' + 'in this storage group to a non-active state and ' + 'retry the retype operation.', mgmt_sg_name) + raise + self.rest.srdf_delete_device_pair(array, rdfg_no, device_id) + # Remove the volume from the R1 RDFG mgmt SG (R1) + if rep_config['mode'] in [utils.REP_ASYNC, utils.REP_METRO]: + self.masking.remove_volume_from_sg( + array, device_id, volume_name, mgmt_sg_name, extra_specs) + @coordination.synchronized('emc-{rdf_group}-rdf') def _cleanup_remote_target( self, array, volume, remote_array, device_id, target_device, @@ -4963,12 +5172,14 @@ class PowerMaxCommon(object): :returns: secondary_id, volume_update_list, group_update_list :raises: VolumeBackendAPIException """ + volume_update_list = list() + group_update_list = list() primary_array = self._get_configuration_value( utils.VMAX_ARRAY, utils.POWERMAX_ARRAY) array_list = self.rest.get_arrays_list() is_valid, msg = self.utils.validate_failover_request( self.failover, secondary_id, self.rep_configs, primary_array, - array_list) + array_list, self.promotion) if not is_valid: LOG.error(msg) raise exception.InvalidReplicationTarget(msg) @@ -4977,13 +5188,21 @@ class PowerMaxCommon(object): if not self.failover: self.failover = True self.active_backend_id = secondary_id if secondary_id else None - else: + elif secondary_id == 'default': self.failover = False group_fo = 'default' - volume_update_list, group_update_list = ( - self._populate_volume_and_group_update_lists( - volumes, groups, group_fo)) + if secondary_id == utils.PMAX_FAILOVER_START_ARRAY_PROMOTION: + self.promotion = True + LOG.info("Enabled array promotion.") + else: + volume_update_list, group_update_list = ( + self._populate_volume_and_group_update_lists( + volumes, groups, group_fo)) + + if secondary_id == 'default' and self.promotion: + self.promotion = False + LOG.info("Disabled array promotion.") LOG.info("Failover host complete.") return secondary_id, volume_update_list, group_update_list @@ -5061,7 +5280,17 @@ class PowerMaxCommon(object): volume_update_list += vol_updates if len(non_rep_vol_list) > 0: - if self.failover: + if self.promotion: + # Volumes that were promoted will have a replication state + # of error with no other replication metadata. Use this to + # determine which volumes should updated to have a replication + # state of disabled. + for vol in non_rep_vol_list: + volume_update_list.append({ + 'volume_id': vol.id, + 'updates': { + 'replication_status': REPLICATION_DISABLED}}) + elif self.failover: # Since the array has been failed-over, # volumes without replication should be in error. for vol in non_rep_vol_list: @@ -5633,67 +5862,133 @@ class PowerMaxCommon(object): and not group.is_replicated): raise NotImplementedError() - array, interval_retries_dict = self._get_volume_group_info(group) model_update = {'status': fields.GroupStatus.AVAILABLE} - add_vols = [vol for vol in add_volumes] if add_volumes else [] - add_device_ids = self._get_volume_device_ids(add_vols, array) - remove_vols = [vol for vol in remove_volumes] if remove_volumes else [] - remove_device_ids = self._get_volume_device_ids(remove_vols, array) - vol_grp_name = None - try: - volume_group = self._find_volume_group(array, group) - if volume_group: - if 'name' in volume_group: - vol_grp_name = volume_group['name'] - if vol_grp_name is None: - raise exception.GroupNotFound(group_id=group.id) - # Add volume(s) to the group - if add_device_ids: - self.utils.check_rep_status_enabled(group) - for vol in add_vols: - extra_specs = self._initial_setup(vol) - self.utils.check_replication_matched(vol, extra_specs) - self.masking.add_volumes_to_storage_group( - array, add_device_ids, vol_grp_name, interval_retries_dict) - if group.is_replicated: - # Add remote volumes to remote storage group - self.masking.add_remote_vols_to_volume_group( - add_vols, group, interval_retries_dict) - # Remove volume(s) from the group - if remove_device_ids: - if group.is_replicated: - # Need force flag when manipulating RDF enabled SGs - interval_retries_dict[utils.FORCE_VOL_REMOVE] = True - # Check if the volumes exist in the storage group - temp_list = deepcopy(remove_device_ids) - for device_id in temp_list: - if not self.rest.is_volume_in_storagegroup( - array, device_id, vol_grp_name): - remove_device_ids.remove(device_id) + if self.promotion: + self._update_group_promotion( + group, add_volumes, remove_volumes) + elif self.failover: + msg = _('Cannot perform group updates during failover, please ' + 'either failback or perform a promotion operation.') + raise exception.VolumeBackendAPIException(msg) + else: + array, interval_retries_dict = self._get_volume_group_info(group) + add_vols = [vol for vol in add_volumes] if add_volumes else [] + add_device_ids = self._get_volume_device_ids(add_vols, array) + remove_vols = [ + vol for vol in remove_volumes] if remove_volumes else [] + remove_device_ids = self._get_volume_device_ids(remove_vols, array) + vol_grp_name = None + try: + volume_group = self._find_volume_group(array, group) + if volume_group: + if 'name' in volume_group: + vol_grp_name = volume_group['name'] + if vol_grp_name is None: + raise exception.GroupNotFound(group_id=group.id) + # Add volume(s) to the group + if add_device_ids: + self.utils.check_rep_status_enabled(group) + for vol in add_vols: + extra_specs = self._initial_setup(vol) + self.utils.check_replication_matched(vol, extra_specs) + self.masking.add_volumes_to_storage_group( + array, add_device_ids, vol_grp_name, + interval_retries_dict) + if group.is_replicated: + # Add remote volumes to remote storage group + self.masking.add_remote_vols_to_volume_group( + add_vols, group, interval_retries_dict) + # Remove volume(s) from the group if remove_device_ids: - self.masking.remove_volumes_from_storage_group( - array, remove_device_ids, - vol_grp_name, interval_retries_dict) - if group.is_replicated: - # Remove remote volumes from the remote storage group - self._remove_remote_vols_from_volume_group( - array, remove_vols, group, interval_retries_dict) - except exception.GroupNotFound: - raise - except Exception as ex: - exception_message = (_("Failed to update volume group:" - " %(volGrpName)s. Exception: %(ex)s.") - % {'volGrpName': group.id, - 'ex': ex}) - LOG.error(exception_message) - raise exception.VolumeBackendAPIException( - message=exception_message) + if group.is_replicated: + # Need force flag when manipulating RDF enabled SGs + interval_retries_dict[utils.FORCE_VOL_REMOVE] = True + # Check if the volumes exist in the storage group + temp_list = deepcopy(remove_device_ids) + for device_id in temp_list: + if not self.rest.is_volume_in_storagegroup( + array, device_id, vol_grp_name): + remove_device_ids.remove(device_id) + if remove_device_ids: + self.masking.remove_volumes_from_storage_group( + array, remove_device_ids, + vol_grp_name, interval_retries_dict) + if group.is_replicated: + # Remove remote volumes from the remote storage group + self._remove_remote_vols_from_volume_group( + array, remove_vols, group, interval_retries_dict) + except exception.GroupNotFound: + raise + except Exception as ex: + exception_message = (_("Failed to update volume group:" + " %(volGrpName)s. Exception: %(ex)s.") + % {'volGrpName': group.id, + 'ex': ex}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException( + message=exception_message) - self.volume_metadata.capture_modify_group( - vol_grp_name, group.id, add_vols, remove_volumes, array) + self.volume_metadata.capture_modify_group( + vol_grp_name, group.id, add_vols, remove_volumes, array) return model_update, None, None + def _update_group_promotion(self, group, add_volumes, remove_volumes): + """Updates LUNs in generic volume group during array promotion. + + :param group: storage configuration service instance + :param add_volumes: the volumes uuids you want to add to the vol grp + :param remove_volumes: the volumes uuids you want to remove from + the CG + :returns: model_update + :raises: VolumeBackendAPIException + """ + if not group.is_replicated: + msg = _('Group updates are only supported on replicated volume ' + 'groups during failover promotion.') + raise exception.VolumeBackendAPIException(msg) + if add_volumes: + msg = _('Unable to add to volumes to a group, only volume ' + 'removal is supported during promotion.') + raise exception.VolumeBackendAPIException(msg) + + # Either add_volumes or remove_volumes must be provided, if add_volumes + # then excep is raised, other there must be remove_volumes present + volume = remove_volumes[0] + extra_specs = self._initial_setup(volume, volume.volume_type_id) + rep_extra_specs = self._get_replication_extra_specs( + extra_specs, extra_specs[utils.REP_CONFIG]) + remote_array = rep_extra_specs['array'] + + vol_grp_name = None + volume_group = self._find_volume_group(remote_array, group) + if volume_group: + if 'name' in volume_group: + vol_grp_name = volume_group['name'] + if vol_grp_name is None: + raise exception.GroupNotFound(group_id=group.id) + + interval_retries_dict = { + utils.INTERVAL: self.interval, utils.RETRIES: self.retries} + # Volumes have already failed over and had their provider_location + # updated, do not get remote device IDs here + remove_device_ids = self._get_volume_device_ids( + remove_volumes, remote_array) + if remove_device_ids: + interval_retries_dict[utils.FORCE_VOL_REMOVE] = True + # Check if the volumes exist in the storage group + temp_list = deepcopy(remove_device_ids) + for device_id in temp_list: + if not self.rest.is_volume_in_storagegroup( + remote_array, device_id, vol_grp_name): + remove_device_ids.remove(device_id) + if remove_device_ids: + self.masking.remove_volumes_from_storage_group( + remote_array, remove_device_ids, + vol_grp_name, interval_retries_dict) + self.volume_metadata.capture_modify_group( + vol_grp_name, group.id, list(), remove_volumes, remote_array) + def _remove_remote_vols_from_volume_group( self, array, volumes, group, extra_specs): """Remove the remote volumes from their volume group. @@ -6166,7 +6461,10 @@ class PowerMaxCommon(object): if vol_grp_name is None: raise exception.GroupNotFound(group_id=group.id) - if not is_metro: + is_partitioned = self._rdf_vols_partitioned( + remote_array, volumes, rdf_group_no) + + if not is_metro and not is_partitioned: if failover: self.rest.srdf_failover_group( remote_array, vol_grp_name, rdf_group_no, extra_specs) @@ -6217,6 +6515,29 @@ class PowerMaxCommon(object): LOG.debug("Volume model updates: %s", vol_model_updates) return model_update, vol_model_updates + def _rdf_vols_partitioned(self, array, volumes, rdfg): + """Check if rdf volumes have been failed over by powermax array + + :param array: remote array + :param volumes: rdf volumes + :param rdfg: rdf group + :return: devices have partitioned states + """ + is_partitioned = False + for volume in volumes: + if self.promotion: + vol_data = volume.provider_location + else: + vol_data = volume.replication_driver_data + vol_data = ast.literal_eval(vol_data) + device_id = vol_data.get(utils.DEVICE_ID) + vol_details = self.rest.get_rdf_pair_volume(array, rdfg, device_id) + rdf_pair_state = vol_details.get(utils.RDF_PAIR_STATE, '').lower() + if rdf_pair_state in utils.RDF_PARTITIONED_STATES: + is_partitioned = True + break + return is_partitioned + def get_attributes_from_cinder_config(self): """Get all attributes from the configuration file diff --git a/cinder/volume/drivers/dell_emc/powermax/utils.py b/cinder/volume/drivers/dell_emc/powermax/utils.py index 47dca134f63..aac8436de66 100644 --- a/cinder/volume/drivers/dell_emc/powermax/utils.py +++ b/cinder/volume/drivers/dell_emc/powermax/utils.py @@ -80,12 +80,16 @@ RDF_FAILEDOVER_STATE = 'failed over' RDF_ACTIVE = 'active' RDF_ACTIVEACTIVE = 'activeactive' RDF_ACTIVEBIAS = 'activebias' +RDF_PARTITIONED_STATE = 'partitioned' +RDF_TRANSIDLE_STATE = 'transidle' +RDF_PAIR_STATE = 'rdfpairState' RDF_VALID_STATES_SYNC = [RDF_SYNC_STATE, RDF_SUSPENDED_STATE, RDF_SYNCINPROG_STATE] RDF_VALID_STATES_ASYNC = [RDF_CONSISTENT_STATE, RDF_SUSPENDED_STATE, RDF_SYNCINPROG_STATE] RDF_VALID_STATES_METRO = [RDF_ACTIVEBIAS, RDF_ACTIVEACTIVE, RDF_SUSPENDED_STATE, RDF_SYNCINPROG_STATE] +RDF_PARTITIONED_STATES = [RDF_PARTITIONED_STATE, RDF_TRANSIDLE_STATE] RDF_CONS_EXEMPT = 'exempt' RDF_ALLOW_METRO_DELETE = 'allow_delete_metro' RDF_GROUP_NO = 'rdf_group_number' @@ -102,6 +106,7 @@ USED_HOST_NAME = "used_host_name" RDF_SYNCED_STATES = [RDF_SYNC_STATE, RDF_CONSISTENT_STATE, RDF_ACTIVEACTIVE, RDF_ACTIVEBIAS] FORCE_VOL_REMOVE = 'force_vol_remove' +PMAX_FAILOVER_START_ARRAY_PROMOTION = 'pmax_failover_start_array_promotion' # Multiattach constants IS_MULTIATTACH = 'multiattach' @@ -794,6 +799,35 @@ class PowerMaxUtils(object): pools.append(new_pool) return pools + @staticmethod + def add_promotion_pools(pools, primary_array): + """Add duplicate pools with primary SID for operations during promotion + + :param pools: the pool list + :param primary_array: the original primary array. + :returns: pools - the updated pool list + """ + i_pools = deepcopy(pools) + for pool in i_pools: + # pool name + pool_name = pool['pool_name'] + split_name = pool_name.split('+') + array_pos = 3 if len(split_name) == 4 else 2 + array_sid = split_name[array_pos] + updated_pool_name = re.sub(array_sid, primary_array, pool_name) + + # location info + loc = pool['location_info'] + split_loc = loc.split('#') + split_loc[0] = primary_array # Replace the array SID + updated_loc = '#'.join(split_loc) + + new_pool = deepcopy(pool) + new_pool['pool_name'] = updated_pool_name + new_pool['location_info'] = updated_loc + pools.append(new_pool) + return pools + def check_replication_matched(self, volume, extra_specs): """Check volume type and group type. @@ -1204,6 +1238,14 @@ class PowerMaxUtils(object): 'are defined in cinder.conf, backend_id %s is ' 'defined more than once.') % backend_id) raise exception.InvalidConfigurationValue(msg) + elif backend_id == PMAX_FAILOVER_START_ARRAY_PROMOTION: + msg = (_('Invalid Backend ID found. Defining a ' + 'replication device with a Backend ID of %s is ' + 'currently not supported. Please update ' + 'the Backend ID of the related replication ' + 'device in cinder.conf to use valid ' + 'Backend ID value.') % backend_id) + raise exception.InvalidConfigurationValue(msg) else: msg = _('Backend IDs must be assigned for each rep_device ' 'when multiple replication devices are defined in ' @@ -1787,11 +1829,12 @@ class PowerMaxUtils(object): return False @staticmethod - def get_rep_config(backend_id, rep_configs): + def get_rep_config(backend_id, rep_configs, promotion_vol_stats=False): """Get rep_config for given backend_id. :param backend_id: rep config search key -- str :param rep_configs: backend rep_configs -- list + :param promotion_vol_stats: get rep config for vol stats -- bool :returns: rep_config -- dict """ if len(rep_configs) == 1: @@ -1802,20 +1845,25 @@ class PowerMaxUtils(object): if rep_config[BACKEND_ID] == backend_id: rep_device = rep_config if rep_device is None: - msg = (_('Could not find a replication_device with a ' - 'backend_id of "%s" in cinder.conf. Please confirm ' - 'that the replication_device_backend_id extra spec ' - 'for this volume type matches the backend_id of the ' - 'intended replication_device in ' - 'cinder.conf.') % backend_id) - if BACKEND_ID_LEGACY_REP in msg: - msg = (_('Could not find replication_device. Legacy ' - 'replication_device key found, please ensure the ' - 'backend_id for the legacy replication_device in ' - 'cinder.conf has been changed to ' - '"%s".') % BACKEND_ID_LEGACY_REP) - LOG.error(msg) - raise exception.InvalidInput(msg) + if promotion_vol_stats: + # Stat collection only need remote array and srp, any of + # the available replication_devices can provide this. + rep_device = rep_configs[0] + else: + msg = (_('Could not find a replication_device with a ' + 'backend_id of "%s" in cinder.conf. Please ' + 'confirm that the replication_device_backend_id ' + 'extra spec for this volume type matches the ' + 'backend_id of the intended replication_device ' + 'in cinder.conf.') % backend_id) + if BACKEND_ID_LEGACY_REP in msg: + msg = (_('Could not find replication_device. Legacy ' + 'replication_device key found, please ensure ' + 'the backend_id for the legacy ' + 'replication_device in cinder.conf has been ' + 'changed to "%s".') % BACKEND_ID_LEGACY_REP) + LOG.error(msg) + raise exception.InvalidInput(msg) return rep_device @staticmethod @@ -1834,7 +1882,8 @@ class PowerMaxUtils(object): return list(replication_targets) def validate_failover_request(self, is_failed_over, failover_backend_id, - rep_configs, primary_array, arrays_list): + rep_configs, primary_array, arrays_list, + is_promoted): """Validate failover_host request's parameters Validate that a failover_host operation can be performed with @@ -1845,23 +1894,32 @@ class PowerMaxUtils(object): :param rep_configs: backend rep_configs -- list :param primary_array: configured primary array SID -- string :param arrays_list: list of U4P symmetrix IDs -- list + :param is_promoted: current promotion state -- bool :return: (bool, str) is valid, reason on invalid """ is_valid = True msg = "" if is_failed_over: - if failover_backend_id != 'default': + valid_backend_ids = [ + 'default', PMAX_FAILOVER_START_ARRAY_PROMOTION] + if failover_backend_id not in valid_backend_ids: is_valid = False msg = _('Cannot failover, the backend is already in a failed ' 'over state, if you meant to failback, please add ' '--backend_id default to the command.') - elif primary_array not in arrays_list: + elif (failover_backend_id == 'default' and + primary_array not in arrays_list): is_valid = False msg = _('Cannot failback, the configured primary array is ' 'not currently available to perform failback to. ' 'Please ensure array %s is visible in ' 'Unisphere.') % primary_array - + elif is_promoted and failover_backend_id != 'default': + is_valid = False + msg = _('Failover promotion currently in progress, please ' + 'finish the promotion process and issue a failover ' + 'using the "default" backend_id to complete this ' + 'process.') else: if failover_backend_id == 'default': is_valid = False @@ -1869,6 +1927,11 @@ class PowerMaxUtils(object): 'state. If you meant to failover, please either omit ' 'the --backend_id parameter or use the --backend_id ' 'parameter with a valid backend id.') + elif failover_backend_id == PMAX_FAILOVER_START_ARRAY_PROMOTION: + is_valid = False + msg = _('Cannot start failover promotion. The backend must ' + 'already be in a failover state to perform this' + 'action.') elif len(rep_configs) > 1: if failover_backend_id is None: is_valid = False