From fd8b74b61c79c7d4b7f095c608b90a5b2be31993 Mon Sep 17 00:00:00 2001 From: Michael McAleer Date: Tue, 21 May 2019 18:12:34 +0100 Subject: [PATCH] PowerMax Driver - Metro ODE Support PowerMax for Cinder now supports extending in-use Metro RDF enabled volumes using Online Device Expansion. This submission implements this new feature. Change-Id: I5342cbd64d33c38a68c92e4e56cbfce8aaa621c3 Implements: blueprint powermax-metro-ode --- .../dell_emc/powermax/powermax_data.py | 9 + .../dell_emc/powermax/test_powermax_common.py | 194 ++++++++-- .../powermax/test_powermax_provision.py | 5 +- .../powermax/test_powermax_replication.py | 53 --- .../dell_emc/powermax/test_powermax_rest.py | 30 +- .../drivers/dell_emc/powermax/common.py | 351 ++++++++++-------- cinder/volume/drivers/dell_emc/powermax/fc.py | 1 + .../volume/drivers/dell_emc/powermax/iscsi.py | 1 + .../drivers/dell_emc/powermax/provision.py | 2 +- .../volume/drivers/dell_emc/powermax/rest.py | 33 +- ...ax-ode-metro-support-ed50bb20f932548b.yaml | 5 + 11 files changed, 425 insertions(+), 259 deletions(-) create mode 100644 releasenotes/notes/powermax-ode-metro-support-ed50bb20f932548b.yaml 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 944435af480..8f5c6f02dec 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 @@ -77,6 +77,7 @@ class PowerMaxData(object): volume_id = '2b06255d-f5f0-4520-a953-b029196add6a' no_slo_sg_name = 'OS-HostX-No_SLO-OS-fibre-PG' temp_snapvx = 'temp-00001-snapshot_for_clone' + next_gen_ucode = 5978 # connector info wwpn1 = '123456789012345' @@ -294,6 +295,14 @@ class PowerMaxData(object): rep_extra_specs5 = deepcopy(rep_extra_specs2) rep_extra_specs5['target_array_model'] = 'VMAX250F' + rep_extra_specs_ode = deepcopy(rep_extra_specs2) + rep_extra_specs_ode['array'] = array + rep_extra_specs_ode.pop('rep_mode') + rep_extra_specs_ode['mode'] = 'Metro' + + rep_extra_specs_legacy = deepcopy(rep_extra_specs_ode) + rep_extra_specs_legacy['mode'] = 'Synchronous' + test_volume_type_1 = volume_type.VolumeType( id='2b06255d-f5f0-4520-a953-b029196add6a', name='abc', extra_specs=extra_specs) 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 84b91de3151..5681011bdfb 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 @@ -58,29 +58,28 @@ class PowerMaxCommonTest(test.TestCase): self.utils.get_volumetype_extra_specs = ( mock.Mock(return_value=self.data.vol_type_extra_specs)) - @mock.patch.object(rest.PowerMaxRest, 'set_rest_credentials') - @mock.patch.object(common.PowerMaxCommon, '_get_slo_workload_combinations', - return_value=[]) - @mock.patch.object( - common.PowerMaxCommon, 'get_attributes_from_cinder_config', - return_value=[]) - def test_gather_info_no_opts(self, mock_parse, mock_combo, mock_rest): - configuration = tpfo.FakeConfiguration( - None, 'config_group', None, None) - fc.PowerMaxFCDriver(configuration=configuration) - + @mock.patch.object(rest.PowerMaxRest, 'get_array_ucode_version', + return_value=tpd.PowerMaxData.next_gen_ucode) @mock.patch.object(rest.PowerMaxRest, 'get_array_model_info', return_value=('PowerMax 2000', True)) @mock.patch.object(rest.PowerMaxRest, 'set_rest_credentials') @mock.patch.object(common.PowerMaxCommon, '_get_slo_workload_combinations', return_value=[]) - @mock.patch.object( - common.PowerMaxCommon, 'get_attributes_from_cinder_config', - return_value=tpd.PowerMaxData.array_info_wl) - def test_gather_info_next_gen(self, mock_parse, mock_combo, mock_rest, - mock_nextgen): + @mock.patch.object(common.PowerMaxCommon, + 'get_attributes_from_cinder_config', + side_effect=[[], tpd.PowerMaxData.array_info_wl]) + def test_gather_info_tests(self, mck_parse, mck_combo, mck_rest, + mck_nextgen, mck_ucode): + + # Use-Case 1: Gather info no-opts + configuration = tpfo.FakeConfiguration( + None, 'config_group', None, None) + fc.PowerMaxFCDriver(configuration=configuration) + + # Use-Case 2: Gather info next-gen with ucode/version self.common._gather_info() self.assertTrue(self.common.next_gen) + self.assertEqual(self.common.ucode_level, self.data.next_gen_ucode) def test_get_slo_workload_combinations_powermax(self): array_info = self.common.get_attributes_from_cinder_config() @@ -445,28 +444,42 @@ class PowerMaxCommonTest(test.TestCase): mock_unmap.assert_called_once_with( volume, connector) - @mock.patch.object(rest.PowerMaxRest, 'is_next_gen_array', - return_value=True) - @mock.patch.object(common.PowerMaxCommon, '_sync_check') @mock.patch.object(provision.PowerMaxProvision, 'extend_volume') - def test_extend_volume_success(self, mock_extend, mock_sync, mock_newgen): + @mock.patch.object(common.PowerMaxCommon, '_array_ode_capabilities_check', + return_value=[True] * 4) + @mock.patch.object(common.PowerMaxCommon, '_extend_vol_validation_checks') + def test_extend_vol_no_rep_success(self, mck_val_chk, mck_ode_chk, + mck_extend): volume = self.data.test_volume array = self.data.array device_id = self.data.device_id new_size = self.data.test_volume.size ref_extra_specs = deepcopy(self.data.extra_specs_intervals_set) ref_extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f - with mock.patch.object(self.rest, 'is_vol_in_rep_session', - side_effect=[(False, False, None), - (False, True, None)]): + self.common.extend_volume(volume, new_size) + mck_extend.assert_called_once_with( + array, device_id, new_size, ref_extra_specs, None) + + @mock.patch.object(provision.PowerMaxProvision, 'extend_volume') + @mock.patch.object(common.PowerMaxCommon, 'get_rdf_details', + return_value=(10, None)) + @mock.patch.object(common.PowerMaxCommon, '_array_ode_capabilities_check', + return_value=[True] * 4) + @mock.patch.object(common.PowerMaxCommon, '_extend_vol_validation_checks') + def test_extend_vol_rep_success(self, mck_val_chk, mck_ode_chk, + mck_get_rdf, mck_extend): + volume = self.data.test_volume + array = self.data.array + device_id = self.data.device_id + new_size = self.data.test_volume.size + ref_extra_specs = deepcopy(self.data.rep_extra_specs_ode) + with mock.patch.object(self.common, '_initial_setup', + return_value=self.data.rep_extra_specs_ode): + self.common.next_gen = True + self.common.rep_config = deepcopy(ref_extra_specs) self.common.extend_volume(volume, new_size) - mock_extend.assert_called_once_with( - array, device_id, new_size, ref_extra_specs) - # Success, with snapshot, on new VMAX array - mock_extend.reset_mock() - self.common.extend_volume(volume, new_size) - mock_extend.assert_called_once_with( - array, device_id, new_size, ref_extra_specs) + mck_extend.assert_called_with( + array, device_id, new_size, ref_extra_specs, 10) def test_extend_volume_failed_snap_src(self): volume = self.data.test_volume @@ -2647,3 +2660,124 @@ class PowerMaxCommonTest(test.TestCase): def test_retest_primary_u4p(self, mock_primary_u4p, mock_request): self.common.retest_primary_u4p() self.assertFalse(self.rest.u4p_in_failover) + + @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): + volume = self.data.test_volume + array = self.data.array + device_id = self.data.device_id + new_size = self.data.test_volume.size + 1 + extra_specs = deepcopy(self.data.extra_specs) + self.common._extend_vol_validation_checks( + array, device_id, volume.name, extra_specs, volume.size, new_size) + + @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): + volume = self.data.test_volume + array = self.data.array + device_id = None + new_size = self.data.test_volume.size + 1 + extra_specs = deepcopy(self.data.extra_specs) + self.assertRaises( + exception.VolumeBackendAPIException, + self.common._extend_vol_validation_checks, + array, device_id, volume.name, extra_specs, volume.size, new_size) + + @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): + volume = self.data.test_volume + array = self.data.array + device_id = self.data.device_id + new_size = self.data.test_volume.size + 1 + extra_specs = deepcopy(self.data.extra_specs) + self.common.next_gen = False + self.assertRaises( + exception.VolumeBackendAPIException, + self.common._extend_vol_validation_checks, + array, device_id, volume.name, extra_specs, volume.size, new_size) + + @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): + volume = self.data.test_volume + array = self.data.array + device_id = self.data.device_id + new_size = volume.size - 1 + extra_specs = deepcopy(self.data.extra_specs) + self.assertRaises( + exception.VolumeBackendAPIException, + self.common._extend_vol_validation_checks, + array, device_id, volume.name, extra_specs, volume.size, new_size) + + @mock.patch.object(rest.PowerMaxRest, 'is_next_gen_array', + return_value=True) + @mock.patch.object( + rest.PowerMaxRest, 'get_array_ucode_version', + return_value=tpd.PowerMaxData.powermax_model_details['ucode']) + @mock.patch.object(common.PowerMaxCommon, 'get_rdf_details', + return_value=(10, tpd.PowerMaxData.remote_array)) + def test_array_ode_capabilities_check(self, mck_rdf, mck_ucode, mck_gen): + + array = self.data.powermax_model_details['symmetrixId'] + self.common.ucode_level = self.data.powermax_model_details['ucode'] + self.common.next_gen = True + + r1, r1_ode, r2, r2_ode = self.common._array_ode_capabilities_check( + array, True) + self.assertTrue(r1) + self.assertTrue(r1_ode) + self.assertTrue(r2) + self.assertTrue(r2_ode) + + @mock.patch.object(common.PowerMaxCommon, + '_add_new_volume_to_volume_group') + @mock.patch.object(common.PowerMaxCommon, 'setup_volume_replication') + @mock.patch.object(provision.PowerMaxProvision, 'extend_volume') + @mock.patch.object(rest.PowerMaxRest, 'get_size_of_device_on_array', + return_value=tpd.PowerMaxData.test_volume.size) + @mock.patch.object(provision.PowerMaxProvision, 'break_rdf_relationship') + @mock.patch.object(masking.PowerMaxMasking, 'remove_and_reset_members') + @mock.patch.object( + common.PowerMaxCommon, '_get_replication_extra_specs', + return_value=tpd.PowerMaxData.rep_extra_specs) + @mock.patch.object( + common.PowerMaxCommon, 'get_remote_target_device', + return_value=( + tpd.PowerMaxData.device_id2, tpd.PowerMaxData.remote_array, + tpd.PowerMaxData.rdf_group_vol_details['localRdfGroupNumber'], + tpd.PowerMaxData.rdf_group_vol_details['localVolumeState'], + tpd.PowerMaxData.rdf_group_vol_details['rdfpairState'])) + def test_extend_legacy_replicated_vol(self, mck_get_tgt, mck_rdf_specs, + mck_reset, mck_break_rdf, mck_size, + mck_extend, mck_set_rep, mck_add): + + volume = self.data.test_volume_group_member + array = self.data.array + device_id = self.data.device_id + new_size = volume.size + 1 + extra_specs = deepcopy(self.data.extra_specs) + + self.common._extend_legacy_replicated_vol( + array, volume, device_id, volume.name, new_size, extra_specs) + + @mock.patch.object( + common.PowerMaxCommon, 'get_remote_target_device', + return_value=(None, None, None, None, None)) + def test_extend_legacy_replicated_vol_fail(self, mck_get_tgt): + + volume = self.data.test_volume_group_member + array = self.data.array + device_id = self.data.device_id + new_size = volume.size + 1 + extra_specs = deepcopy(self.data.extra_specs) + self.assertRaises( + exception.VolumeBackendAPIException, + self.common._extend_vol_validation_checks, + array, device_id, volume.name, extra_specs, volume.size, new_size) diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_provision.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_provision.py index edd5feb7523..537a834a564 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_provision.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_provision.py @@ -260,6 +260,7 @@ class PowerMaxProvisionTest(test.TestCase): device_id = self.data.device_id new_size = '3' extra_specs = self.data.extra_specs + rdfg_num = self.data.rdf_group_no with mock.patch.object(self.provision.rest, 'extend_volume' ) as mock_ex: self.provision.extend_volume(array, device_id, new_size, @@ -269,9 +270,9 @@ class PowerMaxProvisionTest(test.TestCase): mock_ex.reset_mock() # Pass in rdf group self.provision.extend_volume(array, device_id, new_size, - extra_specs, self.data.rdf_group_no) + extra_specs, rdfg_num) mock_ex.assert_called_once_with( - array, device_id, new_size, extra_specs) + array, device_id, new_size, extra_specs, rdfg_num) def test_get_srp_pool_stats(self): array = self.data.array 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 5b54cbc0916..f045bad61ea 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 @@ -322,22 +322,6 @@ class PowerMaxReplicationTest(test.TestCase): self.data.extra_specs, rep_extra_specs) mock_pre.assert_called_once() - @mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session', - return_value=(False, False, None)) - @mock.patch.object(common.PowerMaxCommon, 'extend_volume_is_replicated') - @mock.patch.object(common.PowerMaxCommon, '_sync_check') - @mock.patch.object(rest.PowerMaxRest, 'get_array_model_info', - return_value=('VMAX250F', False)) - def test_extend_volume_rep_enabled(self, mock_model, mock_sync, - mock_ex_re, mock_is_re): - extra_specs = deepcopy(self.extra_specs) - extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f - volume_name = self.data.test_volume.name - self.common.extend_volume(self.data.test_volume, '5') - mock_ex_re.assert_called_once_with( - self.data.array, self.data.test_volume, - self.data.device_id, volume_name, '5', extra_specs) - def test_set_config_file_get_extra_specs_rep_enabled(self): extra_specs, _ = self.common._set_config_file_and_get_extra_specs( self.data.test_volume) @@ -607,43 +591,6 @@ class PowerMaxReplicationTest(test.TestCase): self.data.device_id)) self.assertIsNone(target_device4) - @mock.patch.object(rest.PowerMaxRest, 'get_array_model_info', - return_value=('PowerMax 2000', True)) - @mock.patch.object(common.PowerMaxCommon, 'setup_volume_replication') - @mock.patch.object(provision.PowerMaxProvision, 'extend_volume') - @mock.patch.object(provision.PowerMaxProvision, 'break_rdf_relationship') - @mock.patch.object(masking.PowerMaxMasking, 'remove_and_reset_members') - def test_extend_volume_is_replicated(self, mock_remove, mock_break, - mock_extend, mock_setup, mock_model): - self.common.extend_volume_is_replicated( - self.data.array, self.data.test_volume, self.data.device_id, - 'vol1', '5', self.data.extra_specs_rep_enabled) - self.assertEqual(2, mock_remove.call_count) - self.assertEqual(2, mock_extend.call_count) - mock_remove.reset_mock() - mock_extend.reset_mock() - with mock.patch.object(self.rest, 'is_next_gen_array', - return_value=True): - self.common.extend_volume_is_replicated( - self.data.array, self.data.test_volume, self.data.device_id, - 'vol1', '5', self.data.extra_specs_rep_enabled) - mock_remove.assert_not_called() - self.assertEqual(2, mock_extend.call_count) - - def test_extend_volume_is_replicated_exception(self): - self.assertRaises(exception.VolumeBackendAPIException, - self.common.extend_volume_is_replicated, - self.data.failed_resource, self.data.test_volume, - self.data.device_id, 'vol1', '1', - self.data.extra_specs_rep_enabled) - with mock.patch.object(self.utils, 'is_metro_device', - return_value=True): - self.assertRaises(exception.VolumeBackendAPIException, - self.common.extend_volume_is_replicated, - self.data.array, self.data.test_volume, - self.data.device_id, 'vol1', '1', - self.data.extra_specs_rep_enabled) - @mock.patch.object(rest.PowerMaxRest, 'get_array_model_info', return_value=('VMAX250F', False)) @mock.patch.object(common.PowerMaxCommon, diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_rest.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_rest.py index 0da67b28cf1..82ce9a817e1 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_rest.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_rest.py @@ -544,22 +544,31 @@ class PowerMaxRestTest(test.TestCase): self.rest._modify_volume, self.data.array, device_id, payload) - def test_extend_volume(self): + @mock.patch.object(rest.PowerMaxRest, 'wait_for_job') + def test_extend_volume(self, mck_wait): + array = self.data.array device_id = self.data.device_id new_size = '3' + extra_specs = self.data.extra_specs, + rdfg_num = self.data.rdf_group_no + extend_vol_payload = {'executionOption': 'ASYNCHRONOUS', 'editVolumeActionParam': { 'expandVolumeParam': { 'volumeAttribute': { 'volume_size': new_size, - 'capacityUnit': 'GB'}}}} + 'capacityUnit': 'GB'}, + 'rdfGroupNumber': rdfg_num}}} + with mock.patch.object( self.rest, '_modify_volume', - return_value=(202, self.data.job_list[0])) as mock_modify: - self.rest.extend_volume(self.data.array, device_id, new_size, - self.data.extra_specs) - mock_modify.assert_called_once_with( - self.data.array, device_id, extend_vol_payload) + return_value=(202, self.data.job_list[0])) as mck_modify: + + self.rest.extend_volume(array, device_id, new_size, extra_specs, + rdfg_num) + + mck_modify.assert_called_once_with(array, device_id, + extend_vol_payload) def test_legacy_delete_volume(self): device_id = self.data.device_id @@ -1731,3 +1740,10 @@ class PowerMaxRestTest(test.TestCase): self.rest.u4p_failover_targets = [] self.assertRaises(exception.VolumeBackendAPIException, self.rest._handle_u4p_failover) + + @mock.patch.object(rest.PowerMaxRest, 'get_array_detail', + return_value=tpd.PowerMaxData.powermax_model_details) + def test_get_array_ucode(self, mck_ucode): + array = self.data.array + ucode = self.rest.get_array_ucode_version(array) + self.assertEqual(self.data.powermax_model_details['ucode'], ucode) diff --git a/cinder/volume/drivers/dell_emc/powermax/common.py b/cinder/volume/drivers/dell_emc/powermax/common.py index 1ba15c64ce4..baa888c862d 100644 --- a/cinder/volume/drivers/dell_emc/powermax/common.py +++ b/cinder/volume/drivers/dell_emc/powermax/common.py @@ -169,27 +169,31 @@ class PowerMaxCommon(object): def __init__(self, prtcl, version, configuration=None, active_backend_id=None): - self.protocol = prtcl - self.configuration = configuration - self.configuration.append_config_values(powermax_opts) self.rest = rest.PowerMaxRest() self.utils = utils.PowerMaxUtils() self.masking = masking.PowerMaxMasking(prtcl, self.rest) self.provision = provision.PowerMaxProvision(self.rest) - self.version = version self.volume_metadata = volume_metadata.PowerMaxVolumeMetadata( self.rest, version, LOG.isEnabledFor(logging.DEBUG)) - # replication + + # Configuration/Attributes + self.protocol = prtcl + self.configuration = configuration + self.configuration.append_config_values(powermax_opts) + self.active_backend_id = active_backend_id + self.version = version + self.version_dict = {} + self.ucode_level = None + self.next_gen = False self.replication_enabled = False self.extend_replicated_vol = False self.rep_devices = None - self.active_backend_id = active_backend_id self.failover = False + + # Gather environment info self._get_replication_info() self._get_u4p_failover_info() - self.next_gen = False self._gather_info() - self.version_dict = {} def _gather_info(self): """Gather the relevant information for update_volume_stats.""" @@ -202,7 +206,9 @@ class PowerMaxCommon(object): "longer supported.") self.rest.set_rest_credentials(array_info) if array_info: - self.array_model, self.next_gen = self.rest.get_array_model_info( + self.array_model, self.next_gen = ( + self.rest.get_array_model_info(array_info['SerialNumber'])) + self.ucode_level = self.rest.get_array_ucode_version( array_info['SerialNumber']) finalarrayinfolist = self._get_slo_workload_combinations( array_info) @@ -949,60 +955,203 @@ class PowerMaxCommon(object): :param volume: the volume Object :param new_size: the new size to increase the volume to - :returns: dict -- modifiedVolumeDict - the extended volume Object :raises: VolumeBackendAPIException: """ - original_vol_size = volume.size - volume_name = volume.name - extra_specs = self._initial_setup(volume) - array = extra_specs[utils.ARRAY] - device_id = self._find_device_on_array(volume, extra_specs) + # Set specific attributes for extend operation + ex_specs = self._initial_setup(volume) + array = ex_specs[utils.ARRAY] + device_id = self._find_device_on_array(volume, ex_specs) + vol_name = volume.name + orig_vol_size = volume.size + rep_enabled = self.utils.is_replication_enabled(ex_specs) + rdf_grp_no = None + legacy_extend = False + metro_exception = False + + # Run validation and capabilities checks + self._extend_vol_validation_checks( + array, device_id, vol_name, ex_specs, orig_vol_size, new_size) + r1_ode, r1_ode_metro, r2_ode, r2_ode_metro = ( + self._array_ode_capabilities_check(array, rep_enabled)) + + # Get extend workflow dependent on array gen and replication status + if self.next_gen: + if rep_enabled: + (rdf_grp_no, __) = self.get_rdf_details(array) + if self.utils.is_metro_device(self.rep_config, ex_specs): + if not r1_ode_metro or not r2_ode_metro: + metro_exception = True + + elif not self.next_gen and rep_enabled: + if self.utils.is_metro_device(self.rep_config, ex_specs): + metro_exception = True + else: + legacy_extend = True + + # If volume to be extended is SRDF Metro enabled and not FoxTail uCode + if metro_exception: + metro_exception_message = (_( + "Extending a replicated volume with SRDF/Metro enabled is not " + "permitted on this backend. Please contact your storage " + "administrator. Note that you cannot extend SRDF/Metro " + "protected volumes unless running FoxTail PowerMax OS uCode " + "level.")) + LOG.error(metro_exception_message) + raise exception.VolumeBackendAPIException( + message=metro_exception_message) + + # Handle the extend process using workflow info from previous steps + if legacy_extend: + LOG.info("Legacy extend volume %(volume)s to %(new_size)d GBs", + {'volume': vol_name, + 'new_size': int(new_size)}) + self._extend_legacy_replicated_vol( + array, volume, device_id, vol_name, new_size, ex_specs) + else: + LOG.info("ODE extend volume %(volume)s to %(new_size)d GBs", + {'volume': vol_name, + 'new_size': int(new_size)}) + self.provision.extend_volume( + array, device_id, new_size, ex_specs, rdf_grp_no) + + LOG.debug("Leaving extend_volume: %(volume_name)s. ", + {'volume_name': vol_name}) + + def _extend_vol_validation_checks(self, array, device_id, vol_name, + ex_specs, orig_vol_size, new_size): + """Run validation checks on settings for extend volume operation. + + :param array: the array serial number + :param device_id: the device id + :param vol_name: the volume name + :param ex_specs: extra specifications + :param orig_vol_size: the original volume size + :param new_size: the new size the volume should be + :raises: VolumeBackendAPIException: + """ + # 1 - Check device exists if device_id is None: - exception_message = (_("Cannot find Volume: %(volume_name)s. " - "Extend operation. Exiting....") - % {'volume_name': volume_name}) + exception_message = (_( + "Cannot find Volume: %(volume_name)s. Extend operation. " + "Exiting....") % {'volume_name': vol_name}) LOG.error(exception_message) raise exception.VolumeBackendAPIException( message=exception_message) - # Check if volume is part of an on-going clone operation - self._sync_check(array, device_id, extra_specs) + + # 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) __, snapvx_src, __ = self.rest.is_vol_in_rep_session(array, device_id) if snapvx_src: - if not self.rest.is_next_gen_array(array): + if not self.next_gen: exception_message = ( _("The volume: %(volume)s is a snapshot source. " "Extending a volume with snapVx snapshots is only " - "supported on PowerMax/VMAX from HyperMaxOS version " - "5978 onwards. Exiting...") % {'volume': volume_name}) + "supported on PowerMax/VMAX from OS version 5978 " + "onwards. Exiting...") % {'volume': vol_name}) LOG.error(exception_message) raise exception.VolumeBackendAPIException( message=exception_message) - if int(original_vol_size) > int(new_size): + # 3 - Check new size is larger than old size + if int(orig_vol_size) >= int(new_size): exception_message = (_( - "Your original size: %(original_vol_size)s GB is greater " - "than: %(new_size)s GB. Only Extend is supported. Exiting...") - % {'original_vol_size': original_vol_size, - 'new_size': new_size}) + "Your original size: %(orig_vol_size)s GB is greater " + "than or the same as: %(new_size)s GB. Only extend ops are " + "supported. Exiting...") % {'orig_vol_size': orig_vol_size, + 'new_size': new_size}) LOG.error(exception_message) raise exception.VolumeBackendAPIException( message=exception_message) - LOG.info("Extending volume %(volume)s to %(new_size)d GBs", - {'volume': volume_name, - 'new_size': int(new_size)}) - if self.utils.is_replication_enabled(extra_specs): - # Extra logic required if volume is replicated - self.extend_volume_is_replicated( - array, volume, device_id, volume_name, new_size, extra_specs) - else: - self.provision.extend_volume( - array, device_id, new_size, extra_specs) - self.volume_metadata.capture_extend_info( - volume, new_size, device_id, extra_specs, array) + def _array_ode_capabilities_check(self, array, rep_enabled=False): + """Given an array, check Online Device Expansion (ODE) support. - LOG.debug("Leaving extend_volume: %(volume_name)s. ", - {'volume_name': volume_name}) + :param array: the array serial number + :param rep_enabled: if replication is enabled for backend + :returns: r1_ode: (bool) If R1 array supports ODE + :returns: r1_ode_metro: (bool) If R1 array supports ODE with Metro vols + :returns: r2_ode: (bool) If R1 array supports ODE + :returns: r2_ode_metro: (bool) If R1 array supports ODE with Metro vols + """ + r1_ucode = self.ucode_level.split('.') + r1_ode, r1_ode_metro = False, False + r2_ode, r2_ode_metro = False, False + + if self.next_gen: + r1_ode = True + if rep_enabled: + __, r2_array = self.get_rdf_details(array) + r2_ucode = self.rest.get_array_ucode_version(array) + if int(r1_ucode[2]) > utils.UCODE_5978_ELMSR: + r1_ode_metro = True + r2_ucode = r2_ucode.split('.') + if self.rest.is_next_gen_array(r2_array): + r2_ode = True + if int(r2_ucode[2]) > utils.UCODE_5978_ELMSR: + r2_ode_metro = True + + return r1_ode, r1_ode_metro, r2_ode, r2_ode_metro + + def _extend_legacy_replicated_vol(self, array, volume, device_id, + volume_name, new_size, extra_specs): + """Extend a legacy OS volume without Online Device Expansion + + :param array: the array serial number + :param volume: the volume objcet + :param device_id: the volume device id + :param volume_name: the volume name + :param new_size: the new size the volume should be + :param extra_specs: extra specifications + """ + try: + (target_device, remote_array, rdf_group, local_vol_state, + pair_state) = self.get_remote_target_device( + array, volume, device_id) + rep_extra_specs = self._get_replication_extra_specs( + extra_specs, self.rep_config) + + # Volume must be removed from replication (storage) group before + # the replication relationship can be ended (cannot have a mix of + # replicated and non-replicated volumes as the SRDF groups become + # unmanageable) + self.masking.remove_and_reset_members( + array, volume, device_id, volume_name, extra_specs, False) + # Repeat on target side + self.masking.remove_and_reset_members( + remote_array, volume, target_device, volume_name, + rep_extra_specs, False) + LOG.info("Breaking replication relationship...") + self.provision.break_rdf_relationship(array, device_id, + target_device, rdf_group, + rep_extra_specs, pair_state) + # Extend the target volume + LOG.info("Extending target volume...") + # Check to make sure the R2 device requires extending first... + r2_size = self.rest.get_size_of_device_on_array(remote_array, + target_device) + if int(r2_size) < int(new_size): + self.provision.extend_volume(remote_array, target_device, + new_size, rep_extra_specs) + # Extend the source volume + LOG.info("Extending source volume...") + self.provision.extend_volume(array, device_id, new_size, + extra_specs) + # Re-create replication relationship + LOG.info("Recreating replication relationship...") + self.setup_volume_replication(array, volume, device_id, + extra_specs, target_device) + # Check if volume needs to be returned to volume group + if volume.group_id: + self._add_new_volume_to_volume_group( + volume, device_id, volume_name, extra_specs) + + except Exception as e: + exception_message = (_("Error extending volume. Error received " + "was %(e)s") % {'e': e}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException( + message=exception_message) def update_volume_stats(self): """Retrieve stats info.""" @@ -3496,8 +3645,8 @@ class PowerMaxCommon(object): self.provision.get_or_create_group( remote_array, group_name, extra_specs) self.masking.add_volume_to_storage_group( - remote_array, target_device_id, - group_name, volume_name, extra_specs) + remote_array, target_device_id, group_name, volume_name, + extra_specs, force=True) except Exception as e: exception_message = ( _('Exception occurred adding volume %(vol)s to its async ' @@ -3890,120 +4039,6 @@ class PowerMaxCommon(object): return (target_device, remote_array, rdf_group, local_vol_state, pair_state) - def extend_volume_is_replicated( - self, array, volume, device_id, volume_name, - new_size, extra_specs): - """Extend a replication-enabled volume. - - Cannot extend volumes in a synchronization pair where the source - and/or target arrays are running HyperMax versions < 5978. Must first - break the relationship, extend them separately, then recreate the - pair. Extending Metro protected volumes is not supported. - :param array: the array serial number - :param volume: the volume objcet - :param device_id: the volume device id - :param volume_name: the volume name - :param new_size: the new size the volume should be - :param extra_specs: extra specifications - """ - ode_replication, allow_extend = False, self.extend_replicated_vol - if (self.rest.is_next_gen_array(array) - and not self.utils.is_metro_device( - self.rep_config, extra_specs)): - # Check if remote array is next gen - __, remote_array = self.get_rdf_details(array) - if self.rest.is_next_gen_array(remote_array): - ode_replication = True - if self.utils.is_metro_device(self.rep_config, extra_specs): - allow_extend = False - if allow_extend is True or ode_replication is True: - self._extend_with_or_without_ode_replication( - array, volume, device_id, ode_replication, volume_name, - new_size, extra_specs) - else: - exception_message = (_( - "Extending a replicated volume is not permitted on this " - "backend. Please contact your administrator. Note that " - "you cannot extend SRDF/Metro protected volumes.")) - LOG.error(exception_message) - raise exception.VolumeBackendAPIException( - message=exception_message) - - def _extend_with_or_without_ode_replication( - self, array, volume, device_id, ode_replication, volume_name, - new_size, extra_specs): - """Extend a volume with or without Online Device Expansion - - :param array: the array serial number - :param volume: the volume objcet - :param device_id: the volume device id - :param ode_replication: Online device expansion - :param volume_name: the volume name - :param new_size: the new size the volume should be - :param extra_specs: extra specifications - """ - try: - (target_device, remote_array, rdf_group, - local_vol_state, pair_state) = ( - self.get_remote_target_device( - array, volume, device_id)) - rep_extra_specs = self._get_replication_extra_specs( - extra_specs, self.rep_config) - lock_rdf_group = rdf_group - if not ode_replication: - # Volume must be removed from replication (storage) group - # before the replication relationship can be ended (cannot - # have a mix of replicated and non-replicated volumes as - # the SRDF groups become unmanageable) - lock_rdf_group = None - self.masking.remove_and_reset_members( - array, volume, device_id, volume_name, - extra_specs, False) - - # Repeat on target side - self.masking.remove_and_reset_members( - remote_array, volume, target_device, volume_name, - rep_extra_specs, False) - - LOG.info("Breaking replication relationship...") - self.provision.break_rdf_relationship( - array, device_id, target_device, rdf_group, - rep_extra_specs, pair_state) - - # Extend the target volume - LOG.info("Extending target volume...") - # Check to make sure the R2 device requires extending first... - r2_size = self.rest.get_size_of_device_on_array( - remote_array, target_device) - if int(r2_size) < int(new_size): - self.provision.extend_volume( - remote_array, target_device, new_size, - rep_extra_specs, lock_rdf_group) - - # Extend the source volume - LOG.info("Extending source volume...") - self.provision.extend_volume( - array, device_id, new_size, extra_specs, lock_rdf_group) - - if not ode_replication: - # Re-create replication relationship - LOG.info("Recreating replication relationship...") - self.setup_volume_replication( - array, volume, device_id, extra_specs, target_device) - - # Check if volume needs to be returned to volume group - if volume.group_id: - self._add_new_volume_to_volume_group( - volume, device_id, volume_name, extra_specs) - - except Exception as e: - exception_message = (_("Error extending volume. " - "Error received was %(e)s") % - {'e': e}) - LOG.error(exception_message) - raise exception.VolumeBackendAPIException( - message=exception_message) - def enable_rdf(self, array, volume, device_id, rdf_group_no, rep_config, target_name, remote_array, target_device, extra_specs): """Create a replication relationship with a target volume. diff --git a/cinder/volume/drivers/dell_emc/powermax/fc.py b/cinder/volume/drivers/dell_emc/powermax/fc.py index 8b1a83ba559..40b86900ea9 100644 --- a/cinder/volume/drivers/dell_emc/powermax/fc.py +++ b/cinder/volume/drivers/dell_emc/powermax/fc.py @@ -111,6 +111,7 @@ class PowerMaxFCDriver(san.SanDriver, driver.FibreChannelDriver): 4.1.0 - Changing from 90 to 91 rest endpoints - Support for Rapid TDEV Delete (bp powermax-tdev-deallocation) - PowerMax OS Metro formatted volumes fix (bug #1829876) + - Support for Metro ODE (bp/powermax-metro-ode) """ VERSION = "4.1.0" diff --git a/cinder/volume/drivers/dell_emc/powermax/iscsi.py b/cinder/volume/drivers/dell_emc/powermax/iscsi.py index 17d41cdb0ea..df27697cd6c 100644 --- a/cinder/volume/drivers/dell_emc/powermax/iscsi.py +++ b/cinder/volume/drivers/dell_emc/powermax/iscsi.py @@ -116,6 +116,7 @@ class PowerMaxISCSIDriver(san.SanISCSIDriver): 4.1.0 - Changing from 90 to 91 rest endpoints - Support for Rapid TDEV Delete (bp powermax-tdev-deallocation) - PowerMax OS Metro formatted volumes fix (bug #1829876) + - Support for Metro ODE (bp/powermax-metro-ode) """ VERSION = "4.1.0" diff --git a/cinder/volume/drivers/dell_emc/powermax/provision.py b/cinder/volume/drivers/dell_emc/powermax/provision.py index e7a535441e3..f877d296fbd 100644 --- a/cinder/volume/drivers/dell_emc/powermax/provision.py +++ b/cinder/volume/drivers/dell_emc/powermax/provision.py @@ -401,7 +401,7 @@ class PowerMaxProvision(object): @coordination.synchronized('emc-rg-{rdf_group}') def _extend_replicated_volume(rdf_group): self.rest.extend_volume(array, device_id, - new_size, extra_specs) + new_size, extra_specs, rdf_group) _extend_replicated_volume(rdf_group) else: self.rest.extend_volume(array, device_id, new_size, extra_specs) diff --git a/cinder/volume/drivers/dell_emc/powermax/rest.py b/cinder/volume/drivers/dell_emc/powermax/rest.py index 501241eb8e2..7a868a2649f 100644 --- a/cinder/volume/drivers/dell_emc/powermax/rest.py +++ b/cinder/volume/drivers/dell_emc/powermax/rest.py @@ -671,6 +671,18 @@ class PowerMaxRest(object): is_next_gen = True return array_model, is_next_gen + def get_array_ucode_version(self, array): + """Get the PowerMax/VMAX uCode version. + + :param array: the array serial number + :return: the PowerMax/VMAX uCode version + """ + ucode_version = None + system_info = self.get_array_detail(array) + if system_info: + ucode_version = system_info['ucode'] + return ucode_version + def is_compression_capable(self, array): """Check if array is compression capable. @@ -1239,20 +1251,26 @@ class PowerMaxRest(object): return self.modify_resource(array, SLOPROVISIONING, 'volume', payload, resource_name=device_id) - def extend_volume(self, array, device_id, new_size, extra_specs): + def extend_volume(self, array, device_id, new_size, extra_specs, + rdf_grp_no=None): """Extend a PowerMax/VMAX volume. :param array: the array serial number :param device_id: volume device id :param new_size: the new required size for the device :param extra_specs: the extra specifications + :param rdf_grp_no: the RDG group number """ - extend_vol_payload = {"executionOption": "ASYNCHRONOUS", - "editVolumeActionParam": { - "expandVolumeParam": { - "volumeAttribute": { - "volume_size": new_size, - "capacityUnit": "GB"}}}} + extend_vol_payload = {'executionOption': 'ASYNCHRONOUS', + 'editVolumeActionParam': { + 'expandVolumeParam': { + 'volumeAttribute': { + 'volume_size': new_size, + 'capacityUnit': 'GB'}}}} + + if rdf_grp_no: + extend_vol_payload['editVolumeActionParam'][ + 'expandVolumeParam'].update({'rdfGroupNumber': rdf_grp_no}) status_code, job = self._modify_volume( array, device_id, extend_vol_payload) @@ -2320,7 +2338,6 @@ class PowerMaxRest(object): extra_specs[utils.RDF_CONS_EXEMPT] = False payload = self.get_metro_payload_info( array, payload, rdf_group_no, extra_specs) - resource_type = ("rdf_group/%(rdf_num)s/volume" % {'rdf_num': rdf_group_no}) status_code, job = self.create_resource(array, REPLICATION, diff --git a/releasenotes/notes/powermax-ode-metro-support-ed50bb20f932548b.yaml b/releasenotes/notes/powermax-ode-metro-support-ed50bb20f932548b.yaml new file mode 100644 index 00000000000..777a5fec125 --- /dev/null +++ b/releasenotes/notes/powermax-ode-metro-support-ed50bb20f932548b.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + PowerMax for Cinder driver now supports extending in-use Metro RDF enabled + volumes.