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 cec369cd08b..5385265ccbc 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 @@ -44,6 +44,7 @@ class PowerMaxData(object): port_group_name_f = 'OS-fibre-PG' port_group_name_i = 'OS-iscsi-PG' masking_view_name_f = 'OS-HostX-F-OS-fibre-PG-MV' + masking_view_name_Y_f = 'OS-HostY-F-OS-fibre-PG-MV' masking_view_name_i = 'OS-HostX-SRP_1-I-OS-iscsi-PG-MV' initiatorgroup_name_f = 'OS-HostX-F-IG' initiatorgroup_name_i = 'OS-HostX-I-IG' @@ -277,7 +278,8 @@ class PowerMaxData(object): 'storagetype:storagegrouptags': u'good, comma, separated,list'} vol_type_extra_specs_tags_bad = { 'storagetype:storagegrouptags': u'B&d, [list]'} - + extra_specs_port_group_template = deepcopy(extra_specs) + extra_specs_port_group_template['port_group_template'] = 'portGroupName' extra_specs_migrate = deepcopy(extra_specs) extra_specs_migrate[utils.PORTGROUPNAME] = port_group_name_f @@ -401,7 +403,9 @@ class PowerMaxData(object): 'storagegroup_name': storagegroup_name_f, 'volume_name': test_volume.name, 'workload': workload, - 'replication_enabled': False} + 'replication_enabled': False, + 'used_host_name': 'HostX', + 'port_group_label': port_group_name_f} masking_view_dict_no_slo = deepcopy(masking_view_dict) masking_view_dict_no_slo.update( diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/powermax_fake_objects.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/powermax_fake_objects.py index 21e57ff588f..710f2400c2c 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/powermax_fake_objects.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/powermax_fake_objects.py @@ -323,6 +323,10 @@ class FakeConfiguration(object): self.u4p_failover_timeout = value elif key == 'u4p_primary': self.u4p_primary = value + elif key == 'powermax_short_host_name_template': + self.powermax_short_host_name_template = value + elif key == 'powermax_port_group_name_template': + self.powermax_port_group_name_template = value def safe_get(self, key): try: 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 ce09872f07e..563bdc0a6f2 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 @@ -45,9 +45,11 @@ class PowerMaxCommonTest(test.TestCase): self.mock_object(volume_utils, 'get_max_over_subscription_ratio', return_value=1.0) configuration = tpfo.FakeConfiguration( - None, 'CommonTests', 1, 1, san_ip='1.1.1.1', san_login='smc', + emc_file=None, volume_backend_name='CommonTests', interval=1, + retries=1, san_ip='1.1.1.1', san_login='smc', vmax_array=self.data.array, vmax_srp='SRP_1', san_password='smc', - san_api_port=8443, vmax_port_groups=[self.data.port_group_name_f]) + san_api_port=8443, vmax_port_groups=[self.data.port_group_name_f], + powermax_port_group_name_template='portGroupName') rest.PowerMaxRest._establish_rest_session = mock.Mock( return_value=tpfo.FakeRequestsSession()) driver = fc.PowerMaxFCDriver(configuration=configuration) @@ -72,7 +74,6 @@ class PowerMaxCommonTest(test.TestCase): 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) @@ -83,6 +84,54 @@ class PowerMaxCommonTest(test.TestCase): self.assertTrue(self.common.next_gen) self.assertEqual(self.common.ucode_level, self.data.next_gen_ucode) + @mock.patch.object(common.PowerMaxCommon, + '_gather_info') + def test_get_attributes_from_config_short_host_template( + self, mock_gather): + configuration = tpfo.FakeConfiguration( + emc_file=None, volume_backend_name='config_group', interval='10', + retries='10', replication_device=None, + powermax_short_host_name_template='shortHostName') + driver = fc.PowerMaxFCDriver(configuration=configuration) + driver.common._get_attributes_from_config() + self.assertEqual( + 'shortHostName', driver.common.powermax_short_host_name_template) + + @mock.patch.object(common.PowerMaxCommon, + '_gather_info') + def test_get_attributes_from_config_no_short_host_template( + self, mock_gather): + configuration = tpfo.FakeConfiguration( + emc_file=None, volume_backend_name='config_group', interval='10', + retries='10', replication_device=None) + driver = fc.PowerMaxFCDriver(configuration=configuration) + driver.common._get_attributes_from_config() + self.assertIsNone(driver.common.powermax_short_host_name_template) + + @mock.patch.object(common.PowerMaxCommon, + '_gather_info') + def test_get_attributes_from_config_port_group_template( + self, mock_gather): + configuration = tpfo.FakeConfiguration( + emc_file=None, volume_backend_name='config_group', interval='10', + retries='10', replication_device=None, + powermax_port_group_name_template='portGroupName') + driver = fc.PowerMaxFCDriver(configuration=configuration) + driver.common._get_attributes_from_config() + self.assertEqual( + 'portGroupName', driver.common.powermax_port_group_name_template) + + @mock.patch.object(common.PowerMaxCommon, + '_gather_info') + def test_get_attributes_from_config_no_port_group_template( + self, mock_gather): + configuration = tpfo.FakeConfiguration( + emc_file=None, volume_backend_name='config_group', interval='10', + retries='10', replication_device=None) + driver = fc.PowerMaxFCDriver(configuration=configuration) + driver.common._get_attributes_from_config() + self.assertIsNone(driver.common.powermax_port_group_name_template) + def test_get_slo_workload_combinations_powermax(self): array_info = self.common.get_attributes_from_cinder_config() finalarrayinfolist = self.common._get_slo_workload_combinations( @@ -267,7 +316,8 @@ class PowerMaxCommonTest(test.TestCase): array, volume, device_id, extra_specs, self.data.connector, False) mock_rm.assert_called_once_with( array, volume, device_id, volume_name, - extra_specs, True, self.data.connector, async_grp=None) + extra_specs, True, self.data.connector, async_grp=None, + host_template=None) @mock.patch.object(masking.PowerMaxMasking, 'return_volume_to_fast_managed_group') @@ -282,7 +332,8 @@ class PowerMaxCommonTest(test.TestCase): array, volume, device_id, extra_specs, self.data.connector, True) mock_rm.assert_called_once_with( array, volume, device_id, volume_name, - extra_specs, False, self.data.connector, async_grp=None) + extra_specs, False, self.data.connector, async_grp=None, + host_template=None) mock_return.assert_called_once() def test_unmap_lun(self): @@ -296,7 +347,18 @@ class PowerMaxCommonTest(test.TestCase): self.common._unmap_lun(volume, connector) mock_remove.assert_called_once_with( array, volume, device_id, extra_specs, - connector, False, async_grp=None) + connector, False, async_grp=None, host_template=None) + + def test_unmap_lun_force(self): + volume = self.data.test_volume + extra_specs = deepcopy(self.data.extra_specs_intervals_set) + extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f + connector = deepcopy(self.data.connector) + del connector['host'] + with mock.patch.object( + self.common.utils, 'get_host_short_name') as mock_host: + self.common._unmap_lun(volume, connector) + mock_host.assert_not_called() @mock.patch.object(common.PowerMaxCommon, '_remove_members') def test_unmap_lun_attachments(self, mock_rm): @@ -327,7 +389,7 @@ class PowerMaxCommonTest(test.TestCase): self.common._unmap_lun(volume, connector) mock_remove.assert_called_once_with( array, volume, device_id, extra_specs, - connector, False, async_grp=None) + connector, False, async_grp=None, host_template=None) def test_unmap_lun_not_mapped(self): volume = self.data.test_volume @@ -350,7 +412,7 @@ class PowerMaxCommonTest(test.TestCase): self.common._unmap_lun(volume, None) mock_remove.assert_called_once_with( array, volume, device_id, extra_specs, None, - False, async_grp=None) + False, async_grp=None, host_template=None) def test_initialize_connection_already_mapped(self): volume = self.data.test_volume @@ -613,7 +675,9 @@ class PowerMaxCommonTest(test.TestCase): @mock.patch.object( common.PowerMaxCommon, '_get_masking_views_from_volume', - return_value=([], [tpd.PowerMaxData.masking_view_name_f])) + return_value=([tpd.PowerMaxData.masking_view_name_f], + [tpd.PowerMaxData.masking_view_name_f, + tpd.PowerMaxData.masking_view_name_Y_f])) def test_find_host_lun_id_multiattach(self, mock_mask): volume = self.data.test_volume extra_specs = self.data.extra_specs @@ -631,6 +695,27 @@ class PowerMaxCommonTest(test.TestCase): self.data.extra_specs, self.data.rep_extra_specs) mock_tgt.assert_called_once() + @mock.patch.object(rest.PowerMaxRest, 'find_mv_connections_for_vol', + return_value='1') + @mock.patch.object(common.PowerMaxCommon, '_get_masking_views_from_volume', + side_effect=[([], ['OS-HostX-I-PG-MV']), + (['OS-HostX-I-PG-MV'], + ['OS-HostX-I-PG-MV'])]) + @mock.patch.object(rest.PowerMaxRest, 'get_volume', + return_value=tpd.PowerMaxData.volume_details[0]) + def test_find_host_lun_id_backward_compatible( + self, mock_vol, mock_mvs, mock_mv_conns): + expected_dict = {'hostlunid': '1', 'maskingview': 'OS-HostX-I-PG-MV', + 'array': '000197800123', 'device_id': '00001'} + self.common.powermax_short_host_name_template = ( + 'shortHostName[:7]finance') + masked_vols, is_multiattach = self.common.find_host_lun_id( + self.data.test_volume, 'HostX', + self.data.extra_specs) + self.assertEqual(expected_dict, masked_vols) + self.assertFalse(is_multiattach) + mock_mv_conns.assert_called_once() + def test_get_masking_views_from_volume(self): array = self.data.array device_id = self.data.device_id @@ -692,6 +777,7 @@ class PowerMaxCommonTest(test.TestCase): extra_specs[utils.WORKLOAD] = self.data.workload ref_mv_dict = self.data.masking_view_dict self.common.next_gen = False + self.common.powermax_port_group_name_template = 'portGroupName' masking_view_dict = self.common._populate_masking_dict( volume, connector, extra_specs) self.assertEqual(ref_mv_dict, masking_view_dict) @@ -1142,6 +1228,31 @@ class PowerMaxCommonTest(test.TestCase): self.data.test_volume, self.data.connector) self.assertEqual([self.data.wwnn1], metro_wwns) + @mock.patch.object(common.PowerMaxCommon, + '_get_target_wwns_from_masking_view') + @mock.patch.object(utils.PowerMaxUtils, 'get_host_name_label', + return_value = 'my_short_h94485') + @mock.patch.object(utils.PowerMaxUtils, 'is_replication_enabled', + return_value=False) + def test_get_target_wwns_host_override( + self, mock_rep_check, mock_label, mock_mv): + host_record = {'host': 'my_short_host_name'} + connector = deepcopy(self.data.connector) + connector.update(host_record) + extra_specs = {'pool_name': 'Diamond+DSS+SRP_1+000197800123', + 'srp': 'SRP_1', 'array': '000197800123', + 'storagetype:portgroupname': 'OS-fibre-PG', + 'interval': 1, 'retries': 1, 'slo': 'Diamond', + 'workload': 'DSS'} + host_template = 'shortHostName[:10]uuid[:5]' + self.common.powermax_short_host_name_template = host_template + self.common.get_target_wwns_from_masking_view( + self.data.test_volume, connector) + mock_label.assert_called_once_with( + connector['host'], host_template) + mock_mv.assert_called_once_with( + self.data.device_id, 'my_short_h94485', extra_specs) + def test_get_port_group_from_masking_view(self): array = self.data.array maskingview_name = self.data.masking_view_name_f @@ -1464,7 +1575,7 @@ class PowerMaxCommonTest(test.TestCase): @mock.patch.object(rest.PowerMaxRest, 'get_storage_group', return_value=tpd.PowerMaxData.sg_details[1]) @mock.patch.object(utils.PowerMaxUtils, 'get_child_sg_name', - return_value=('OS-Test-SG', '', '', '')) + return_value=('OS-Test-SG', '', '')) @mock.patch.object(rest.PowerMaxRest, 'is_child_sg_in_parent_sg', return_value=True) @mock.patch.object(masking.PowerMaxMasking, @@ -1500,7 +1611,7 @@ class PowerMaxCommonTest(test.TestCase): rest.PowerMaxRest, 'get_volume', return_value=tpd.PowerMaxData.volume_details_attached) @mock.patch.object(utils.PowerMaxUtils, 'get_child_sg_name', - return_value=('OS-Test-SG', '', '', '')) + return_value=('OS-Test-SG', '', '')) @mock.patch.object(provision.PowerMaxProvision, 'create_storage_group') @mock.patch.object(masking.PowerMaxMasking, 'add_child_sg_to_parent_sg') @mock.patch.object(rest.PowerMaxRest, 'is_child_sg_in_parent_sg', @@ -1544,7 +1655,7 @@ class PowerMaxCommonTest(test.TestCase): @mock.patch.object(rest.PowerMaxRest, 'get_storage_group', side_effect=[tpd.PowerMaxData.sg_details[1], None]) @mock.patch.object(utils.PowerMaxUtils, 'get_child_sg_name', - return_value=('OS-Test-SG', '', '', '')) + return_value=('OS-Test-SG', '', '')) @mock.patch.object(rest.PowerMaxRest, 'is_child_sg_in_parent_sg', return_value=False) @mock.patch.object(masking.PowerMaxMasking, diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_fc.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_fc.py index 0469b4d0a18..c0e7babba9b 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_fc.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_fc.py @@ -148,6 +148,19 @@ class PowerMaxFCTest(test.TestCase): self.data.test_volume, self.data.connector) self.assertEqual({}, zoning_mappings) + @mock.patch.object( + common.PowerMaxCommon, 'get_masking_views_from_volume', + side_effect = ([(None, False), + ([tpd.PowerMaxData.masking_view_name_f], False)])) + def test_get_zoning_mappings_retry_backward_compatibility( + self, mock_views): + with mock.patch.object(self.common.utils, 'get_host_name_label', + return_value=None) as mock_label: + self.driver._get_zoning_mappings( + self.data.test_volume, self.data.connector) + self.assertEqual(2, mock_label.call_count) + self.assertEqual(2, mock_views.call_count) + @mock.patch.object( common.PowerMaxCommon, 'get_masking_views_from_volume', return_value=([tpd.PowerMaxData.masking_view_name_f], True)) diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_masking.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_masking.py index 29f9dc658a3..101b0dc8f18 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_masking.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_masking.py @@ -833,7 +833,7 @@ class PowerMaxMaskingTest(test.TestCase): self.data.masking_view_name_f] with mock.patch.object( rest.PowerMaxRest, 'get_masking_views_by_initiator_group', - side_effect=[mv_list, []]): + side_effect=[mv_list, mv_list, [], []]): self.mask._last_volume_delete_initiator_group( self.data.array, self.data.initiatorgroup_name_i, self.data.connector['host']) @@ -974,7 +974,8 @@ class PowerMaxMaskingTest(test.TestCase): def test_pre_multiattach(self, mock_return): mv_dict = self.mask.pre_multiattach( self.data.array, self.data.device_id, - self.data.masking_view_dict_multiattach, self.data.extra_specs) + self.data.masking_view_dict_multiattach, + self.data.extra_specs) mock_return.assert_not_called() self.assertEqual(self.data.storagegroup_name_f, mv_dict[utils.FAST_SG]) @@ -993,7 +994,8 @@ class PowerMaxMaskingTest(test.TestCase): return_value='DiamondDSS'): self.mask.pre_multiattach( self.data.array, self.data.device_id, - self.data.masking_view_dict_multiattach, self.data.extra_specs) + self.data.masking_view_dict_multiattach, + self.data.extra_specs) utils.PowerMaxUtils.truncate_string.assert_called_once_with( 'DiamondDSS', 10) @@ -1007,7 +1009,8 @@ class PowerMaxMaskingTest(test.TestCase): self, mock_return, mock_sg): for x in range(0, 2): self.mask.return_volume_to_fast_managed_group( - self.data.array, self.data.device_id, self.data.extra_specs) + self.data.array, self.data.device_id, + self.data.extra_specs) no_slo_specs = deepcopy(self.data.extra_specs) no_slo_specs[utils.SLO] = None self.mask.return_volume_to_fast_managed_group( @@ -1093,3 +1096,44 @@ class PowerMaxMaskingTest(test.TestCase): self.data.array, self.data.add_volume_sg_info_dict, self.data.extra_specs_tags) mock_except.assert_called() + + @mock.patch.object(rest.PowerMaxRest, + 'get_masking_views_from_storage_group', + return_value=[tpd.PowerMaxData.masking_view_name_f]) + def test_get_host_and_port_group_labels(self, mock_mv): + host_label, port_group_label = ( + self.mask._get_host_and_port_group_labels( + self.data.array, self.data.parent_sg_f)) + self.assertEqual('HostX', host_label) + self.assertEqual('OS-fibre-PG', port_group_label) + + @mock.patch.object(rest.PowerMaxRest, + 'get_masking_views_from_storage_group', + return_value=['OS-HostX699ea-I-p-name3b02c-MV']) + def test_get_host_and_port_group_labels_complex(self, mock_mv): + host_label, port_group_label = ( + self.mask._get_host_and_port_group_labels( + self.data.array, self.data.parent_sg_f)) + self.assertEqual('HostX699ea', host_label) + self.assertEqual('p-name3b02c', port_group_label) + + @mock.patch.object(rest.PowerMaxRest, + 'get_masking_views_from_storage_group', + return_value=['OS-myhost-I-myportgroup-MV']) + def test_get_host_and_port_group_labels_plain(self, mock_mv): + host_label, port_group_label = ( + self.mask._get_host_and_port_group_labels( + self.data.array, self.data.parent_sg_f)) + self.assertEqual('myhost', host_label) + self.assertEqual('myportgroup', port_group_label) + + @mock.patch.object(rest.PowerMaxRest, + 'get_masking_views_from_storage_group', + return_value=[ + 'OS-host-with-dash-I-portgroup-with-dashes-MV']) + def test_get_host_and_port_group_labels_dashes(self, mock_mv): + host_label, port_group_label = ( + self.mask._get_host_and_port_group_labels( + self.data.array, self.data.parent_sg_f)) + self.assertEqual('host-with-dash', host_label) + self.assertEqual('portgroup-with-dashes', port_group_label) 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 d50a0334182..859fbfad142 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 @@ -51,18 +51,18 @@ class PowerMaxReplicationTest(test.TestCase): 'allow_extend': 'True'} volume_utils.get_max_over_subscription_ratio = mock.Mock() configuration = tpfo.FakeConfiguration( - None, 'CommonReplicationTests', 1, 1, san_ip='1.1.1.1', - san_login='smc', vmax_array=self.data.array, vmax_srp='SRP_1', - san_password='smc', san_api_port=8443, + None, 'CommonReplicationTests', interval=1, retries=1, + san_ip='1.1.1.1', san_login='smc', vmax_array=self.data.array, + vmax_srp='SRP_1', san_password='smc', san_api_port=8443, vmax_port_groups=[self.data.port_group_name_f], replication_device=self.replication_device) rest.PowerMaxRest._establish_rest_session = mock.Mock( return_value=tpfo.FakeRequestsSession()) driver = fc.PowerMaxFCDriver(configuration=configuration) iscsi_config = tpfo.FakeConfiguration( - None, 'CommonReplicationTests', 1, 1, san_ip='1.1.1.1', - san_login='smc', vmax_array=self.data.array, vmax_srp='SRP_1', - san_password='smc', san_api_port=8443, + None, 'CommonReplicationTests', interval=1, retries=1, + san_ip='1.1.1.1', san_login='smc', vmax_array=self.data.array, + vmax_srp='SRP_1', san_password='smc', san_api_port=8443, vmax_port_groups=[self.data.port_group_name_i], replication_device=self.replication_device) iscsi_driver = iscsi.PowerMaxISCSIDriver(configuration=iscsi_config) @@ -87,9 +87,9 @@ class PowerMaxReplicationTest(test.TestCase): 'rdf_group_label': self.data.rdf_group_name, 'allow_extend': 'True', 'mode': 'async'} async_configuration = tpfo.FakeConfiguration( - None, 'CommonReplicationTests', 1, 1, san_ip='1.1.1.1', - san_login='smc', vmax_array=self.data.array, vmax_srp='SRP_1', - san_password='smc', san_api_port=8443, + None, 'CommonReplicationTests', interval=1, retries=1, + san_ip='1.1.1.1', san_login='smc', vmax_array=self.data.array, + vmax_srp='SRP_1', san_password='smc', san_api_port=8443, vmax_port_groups=[self.data.port_group_name_f], replication_device=self.async_rep_device) self.async_driver = fc.PowerMaxFCDriver( @@ -101,9 +101,9 @@ class PowerMaxReplicationTest(test.TestCase): 'rdf_group_label': self.data.rdf_group_name, 'allow_extend': 'True', 'mode': 'metro'} metro_configuration = tpfo.FakeConfiguration( - None, 'CommonReplicationTests', 1, 1, san_ip='1.1.1.1', - san_login='smc', vmax_array=self.data.array, vmax_srp='SRP_1', - san_password='smc', san_api_port=8443, + None, 'CommonReplicationTests', interval=1, retries=1, + san_ip='1.1.1.1', san_login='smc', vmax_array=self.data.array, + vmax_srp='SRP_1', san_password='smc', san_api_port=8443, vmax_port_groups=[self.data.port_group_name_f], replication_device=self.metro_rep_device) self.metro_driver = fc.PowerMaxFCDriver( 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 20993c479b7..88dc69c825d 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 @@ -450,6 +450,7 @@ class PowerMaxUtilsTest(test.TestCase): def test_get_child_sg_name(self): host_name = 'HostX' + port_group_label = self.data.port_group_name_f # Slo and rep enabled extra_specs1 = { 'pool_name': u'Diamond+DSS+SRP_1+000197800123', @@ -463,23 +464,24 @@ class PowerMaxUtilsTest(test.TestCase): 'rep_mode': 'Synchronous', utils.PORTGROUPNAME: self.data.port_group_name_f} - child_sg_name, do_disable_compression, rep_enabled, pg_name = ( - self.utils.get_child_sg_name(host_name, extra_specs1)) + child_sg_name, do_disable_compression, rep_enabled = ( + self.utils.get_child_sg_name( + host_name, extra_specs1, port_group_label)) re_name = self.data.storagegroup_name_f + '-RE' self.assertEqual(re_name, child_sg_name) # Disable compression extra_specs2 = deepcopy(self.data.extra_specs_disable_compression) - extra_specs2[utils.PORTGROUPNAME] = self.data.port_group_name_f - child_sg_name, do_disable_compression, rep_enabled, pg_name = ( - self.utils.get_child_sg_name(host_name, extra_specs2)) + child_sg_name, do_disable_compression, rep_enabled = ( + self.utils.get_child_sg_name( + host_name, extra_specs2, port_group_label)) cd_name = self.data.storagegroup_name_f + '-CD' self.assertEqual(cd_name, child_sg_name) # No slo extra_specs3 = deepcopy(self.data.extra_specs) extra_specs3[utils.SLO] = None - extra_specs3[utils.PORTGROUPNAME] = self.data.port_group_name_f - child_sg_name, do_disable_compression, rep_enabled, pg_name = ( - self.utils.get_child_sg_name(host_name, extra_specs3)) + child_sg_name, do_disable_compression, rep_enabled = ( + self.utils.get_child_sg_name( + host_name, extra_specs3, port_group_label)) self.assertEqual(self.data.no_slo_sg_name, child_sg_name) def test_change_multiattach(self): @@ -732,3 +734,285 @@ class PowerMaxUtilsTest(test.TestCase): input_list = 'one,two,three' output_string = self.utils.convert_list_to_string(input_list) self.assertEqual('one,two,three', output_string) + + def test_regex_check_case_2(self): + test_template = 'shortHostName[:10]uuid[:5]' + is_ok, case = self.utils.regex_check(test_template, True) + self.assertTrue(is_ok) + self.assertEqual('2', case) + + def test_regex_check_case_3(self): + test_template = 'shortHostName[-10:]uuid[:5]' + is_ok, case = self.utils.regex_check(test_template, True) + self.assertTrue(is_ok) + self.assertEqual('3', case) + + def test_regex_check_case_4(self): + test_template = 'shortHostName[:7]finance' + is_ok, case = self.utils.regex_check(test_template, True) + self.assertTrue(is_ok) + self.assertEqual('4', case) + + def test_regex_check_case_5(self): + test_template = 'shortHostName[-6:]production' + is_ok, case = self.utils.regex_check(test_template, True) + self.assertTrue(is_ok) + self.assertEqual('5', case) + + def test_regex_check_case_2_misspelt(self): + test_template = 'shortHstName[:10]uuid[:5]' + is_ok, case = self.utils.regex_check(test_template, True) + self.assertFalse(is_ok) + self.assertEqual('0', case) + + def test_regex_check_case_3_misspelt(self): + test_template = 'shortHostName[-10:]uud[:5]' + is_ok, case = self.utils.regex_check(test_template, True) + self.assertFalse(is_ok) + self.assertEqual('0', case) + + def test_regex_check_case_4_misspelt(self): + test_template = 'shortHotName[:7]finance' + is_ok, case = self.utils.regex_check(test_template, True) + self.assertFalse(is_ok) + self.assertEqual('0', case) + + def test_regex_check_case_5_misspelt(self): + test_template = 'shortHstName[-6:]production' + is_ok, case = self.utils.regex_check(test_template, True) + self.assertFalse(is_ok) + self.assertEqual('0', case) + + def test_regex_check_case_4_invalid_chars(self): + test_template = 'shortHostName[:7]f*n&nce' + is_ok, case = self.utils.regex_check(test_template, True) + self.assertFalse(is_ok) + self.assertEqual('0', case) + + def test_regex_check_case_5_invalid_chars(self): + test_template = 'shortHostName[-6:]pr*ducti*n' + is_ok, case = self.utils.regex_check(test_template, True) + self.assertFalse(is_ok) + self.assertEqual('0', case) + + def test_regex_check_case_2_missing_square_bracket(self): + test_template = 'shortHostName[:10uuid[:5]' + is_ok, case = self.utils.regex_check(test_template, True) + self.assertFalse(is_ok) + self.assertEqual('0', case) + + def test_regex_check_case_4_missing_square_bracket(self): + test_template = 'shortHostName[:10finance' + is_ok, case = self.utils.regex_check(test_template, True) + self.assertFalse(is_ok) + self.assertEqual('0', case) + + def test_prepare_string_entity_case_2(self): + test_template = 'shortHostName[:10]uuid[:5]' + altered_string = self.utils.prepare_string_entity( + test_template, 'my_short_host_name', True) + self.assertEqual( + 'my_short_host_name[:10]uuid[:5]', + altered_string) + + def test_prepare_string_entity_case_3(self): + test_template = 'shortHostName[-10:]uuid[:5]' + altered_string = self.utils.prepare_string_entity( + test_template, 'my_short_host_name', True) + self.assertEqual( + 'my_short_host_name[-10:]uuid[:5]', + altered_string) + + def test_prepare_string_entity_case_4(self): + test_template = 'shortHostName[:7]finance' + altered_string = self.utils.prepare_string_entity( + test_template, 'my_short_host_name', True) + self.assertEqual( + 'my_short_host_name[:7]finance', + altered_string) + + def test_prepare_string_entity_case_5(self): + test_template = 'shortHostName[-6:]production' + altered_string = self.utils.prepare_string_entity( + test_template, 'my_short_host_name', True) + self.assertEqual( + 'my_short_host_name[-6:]production', + altered_string) + + def test_prepare_string_with_uuid_case_2(self): + test_template = 'shortHostName[:10]uuid[:5]' + pass_two, uuid = self.utils.prepare_string_with_uuid( + test_template, 'my_short_host_name', True) + self.assertEqual( + 'my_short_host_name[:10]944854dce45898b544a1cb9071d3cc35[:5]', + pass_two) + self.assertEqual('944854dce45898b544a1cb9071d3cc35', uuid) + + def test_prepare_string_with_uuid_case_3(self): + test_template = 'shortHostName[-10:]uuid[:5]' + pass_two, uuid = self.utils.prepare_string_with_uuid( + test_template, 'my_short_host_name', True) + self.assertEqual( + 'my_short_host_name[-10:]944854dce45898b544a1cb9071d3cc35[:5]', + pass_two) + self.assertEqual('944854dce45898b544a1cb9071d3cc35', uuid) + + def test_check_upper_limit_short_host(self): + self.assertRaises(exception.VolumeBackendAPIException, + self.utils.check_upper_limit, + 12, 12, True) + + def test_check_upper_limit_short_host_case_4(self): + user_define_name = 'Little_too_long' + self.assertRaises(exception.VolumeBackendAPIException, + self.utils.check_upper_limit, + 12, len(user_define_name), True) + + def test_validate_short_host_name_from_template_case_1(self): + test_template = 'shortHostName' + short_host_name = 'my_short_host' + result_string = self.utils.validate_short_host_name_from_template( + test_template, short_host_name) + self.assertEqual('my_short_host', result_string) + + def test_validate_short_host_name_from_template_case_1_exceeds_16char( + self): + test_template = 'shortHostName' + short_host_name = 'my_short_host_greater_than_16chars' + result_string = self.utils.validate_short_host_name_from_template( + test_template, short_host_name) + self.assertEqual('6chars0bc43f914e', result_string) + + def test_validate_short_host_name_from_template_case_1_template_misspelt( + self): + test_template = 'shortHstName' + short_host_name = 'my_short_host' + self.assertRaises(exception.VolumeBackendAPIException, + self.utils.validate_short_host_name_from_template, + test_template, short_host_name) + + def test_validate_short_host_name_from_template_case_2(self): + test_template = 'shortHostName[:10]uuid[:5]' + short_host_name = 'my_short_host_name' + result_string = self.utils.validate_short_host_name_from_template( + test_template, short_host_name) + self.assertEqual('my_short_h94485', result_string) + + def test_validate_short_host_name_from_template_case_2_shorter_than(self): + test_template = 'shortHostName[:10]uuid[:5]' + short_host_name = 'HostX' + result_string = self.utils.validate_short_host_name_from_template( + test_template, short_host_name) + self.assertEqual('HostX699ea', result_string) + + def test_validate_short_host_name_from_template_case_3(self): + test_template = 'shortHostName[-10:]uuid[:5]' + short_host_name = 'my_short_host_name' + result_string = self.utils.validate_short_host_name_from_template( + test_template, short_host_name) + self.assertEqual('_host_name94485', result_string) + + def test_validate_short_host_name_from_template_case_3_shorter_than(self): + test_template = 'shortHostName[-10:]uuid[:5]' + short_host_name = 'HostX' + result_string = self.utils.validate_short_host_name_from_template( + test_template, short_host_name) + self.assertEqual('HostX699ea', result_string) + + def test_validate_short_host_name_from_template_case_4(self): + test_template = 'shortHostName[:7]finance' + short_host_name = 'my_short_host_name' + result_string = self.utils.validate_short_host_name_from_template( + test_template, short_host_name) + self.assertEqual('my_shorfinance', result_string) + + def test_validate_short_host_name_from_template_case_5(self): + test_template = 'shortHostName[-6:]production' + short_host_name = 'my_short_host_name' + result_string = self.utils.validate_short_host_name_from_template( + test_template, short_host_name) + self.assertEqual('t_nameproduction', result_string) + + def test_validate_short_host_name_exception_missing_minus(self): + test_template = 'shortHostName[6:]production' + short_host_name = 'my_short_host_name' + self.assertRaises(exception.VolumeBackendAPIException, + self.utils.validate_short_host_name_from_template, + test_template, short_host_name) + + def test_validate_port_group_from_template_case_1(self): + test_template = 'portGroupName' + port_group_name = 'my_pg' + result_string = self.utils.validate_port_group_name_from_template( + test_template, port_group_name) + self.assertEqual('my_pg', result_string) + + def test_validate_port_group_from_template_case_1_long(self): + test_template = 'portGroupName' + port_group_name = 'my_port_group_name' + result_string = self.utils.validate_port_group_name_from_template( + test_template, port_group_name) + self.assertEqual('p_name5ba163', result_string) + + def test_validate_port_group_from_template_case_1_misspelt(self): + test_template = 'portGr*upName' + port_group_name = 'my_port_group_name' + self.assertRaises(exception.VolumeBackendAPIException, + self.utils.validate_port_group_name_from_template, + test_template, port_group_name) + + def test_validate_port_group_from_template_case_2(self): + test_template = 'portGroupName[:6]uuid[:5]' + port_group_name = 'my_port_group_name' + result_string = self.utils.validate_port_group_name_from_template( + test_template, port_group_name) + self.assertEqual('my_por3b02c', result_string) + + def test_validate_port_group_from_template_case_3(self): + test_template = 'portGroupName[-6:]uuid[:5]' + port_group_name = 'my_port_group_name' + result_string = self.utils.validate_port_group_name_from_template( + test_template, port_group_name) + self.assertEqual('p_name3b02c', result_string) + + def test_validate_port_group_from_template_case_4(self): + test_template = 'portGroupName[:6]test' + port_group_name = 'my_port_group_name' + result_string = self.utils.validate_port_group_name_from_template( + test_template, port_group_name) + self.assertEqual('my_portest', result_string) + + def test_validate_port_group_from_template_case_5(self): + test_template = 'portGroupName[-7:]test' + port_group_name = 'my_port_group_name' + result_string = self.utils.validate_port_group_name_from_template( + test_template, port_group_name) + self.assertEqual('up_nametest', result_string) + + def test_validate_port_group_name_exception_missing_minus(self): + test_template = 'portGroupName[6:]test' + port_group_name = 'my_port_group_name' + self.assertRaises(exception.VolumeBackendAPIException, + self.utils.validate_port_group_name_from_template, + test_template, port_group_name) + + def test_validate_port_group_name_exception_chars_exceeded(self): + test_template = 'portGroupName[:10]test' + port_group_name = 'my_port_group_name' + self.assertRaises(exception.VolumeBackendAPIException, + self.utils.validate_port_group_name_from_template, + test_template, port_group_name) + + def test_get_port_name_label_default(self): + port_name_in = 'my_port_group_name' + port_group_template = 'portGroupName' + port_name_out = self.utils.get_port_name_label( + port_name_in, port_group_template) + self.assertEqual('p_name5ba163', port_name_out) + + def test_get_port_name_label_template(self): + port_name_in = 'my_port_group_name' + port_group_template = 'portGroupName[-6:]uuid[:5]' + port_name_out = self.utils.get_port_name_label( + port_name_in, port_group_template) + self.assertEqual('p_name3b02c', port_name_out) diff --git a/cinder/volume/drivers/dell_emc/powermax/common.py b/cinder/volume/drivers/dell_emc/powermax/common.py index c7ddda8f4b4..4326c18c348 100644 --- a/cinder/volume/drivers/dell_emc/powermax/common.py +++ b/cinder/volume/drivers/dell_emc/powermax/common.py @@ -140,7 +140,13 @@ powermax_opts = [ 'configured prior for server connection.'), cfg.ListOpt(utils.POWERMAX_ARRAY_TAG_LIST, bounds=True, - help='List of user assigned name for storage array.')] + help='List of user assigned name for storage array.'), + cfg.StrOpt(utils.POWERMAX_SHORT_HOST_NAME_TEMPLATE, + default='shortHostName', + help='User defined override for short host name.'), + cfg.StrOpt(utils.POWERMAX_PORT_GROUP_NAME_TEMPLATE, + default='portGroupName', + help='User defined override for port group name.')] CONF.register_opts(powermax_opts, group=configuration.SHARED_CONF_GROUP) @@ -185,6 +191,8 @@ class PowerMaxCommon(object): self.rep_devices = [] self.failover = False self.powermax_array_tag_list = None + self.powermax_short_host_name_template = None + self.powermax_port_group_name_template = None # Gather environment info self._get_replication_info() @@ -220,6 +228,10 @@ class PowerMaxCommon(object): utils.POWERMAX_SNAPVX_UNLINK_LIMIT) self.powermax_array_tag_list = self.configuration.safe_get( utils.POWERMAX_ARRAY_TAG_LIST) + self.powermax_short_host_name_template = self.configuration.safe_get( + utils.POWERMAX_SHORT_HOST_NAME_TEMPLATE) + self.powermax_port_group_name_template = self.configuration.safe_get( + utils.POWERMAX_PORT_GROUP_NAME_TEMPLATE) self.pool_info['backend_name'] = ( self.configuration.safe_get('volume_backend_name')) mosr = volume_utils.get_max_over_subscription_ratio( @@ -673,7 +685,7 @@ class PowerMaxCommon(object): def _remove_members(self, array, volume, device_id, extra_specs, connector, is_multiattach, - async_grp=None): + async_grp=None, host_template=None): """This method unmaps a volume from a host. Removes volume from the storage group that belongs to a masking view. @@ -690,7 +702,8 @@ class PowerMaxCommon(object): reset = False if is_multiattach else True self.masking.remove_and_reset_members( array, volume, device_id, volume_name, - extra_specs, reset, connector, async_grp=async_grp) + extra_specs, reset, connector, async_grp=async_grp, + host_template=host_template) if is_multiattach: self.masking.return_volume_to_fast_managed_group( array, device_id, extra_specs) @@ -713,7 +726,7 @@ class PowerMaxCommon(object): async_grp = None LOG.info("Unmap volume: %(volume)s.", {'volume': volume}) if connector is not None: - host = self.utils.get_host_short_name(connector['host']) + host_name = connector.get('host') attachment_list = volume.volume_attachment LOG.debug("Volume attachment list: %(atl)s. " "Attachment type: %(at)s", @@ -725,7 +738,7 @@ class PowerMaxCommon(object): if att_list is not None: host_list = [att.connector['host'] for att in att_list if att is not None and att.connector is not None] - current_host_occurances = host_list.count(host) + current_host_occurances = host_list.count(host_name) if current_host_occurances > 1: LOG.info("Volume is attached to multiple instances on " "this host. Not removing the volume from the " @@ -734,10 +747,10 @@ class PowerMaxCommon(object): else: LOG.warning("Cannot get host name from connector object - " "assuming force-detach.") - host = None + host_name = None device_info, is_multiattach = ( - self.find_host_lun_id(volume, host, extra_specs)) + self.find_host_lun_id(volume, host_name, extra_specs)) if 'hostlunid' not in device_info: LOG.info("Volume %s is not mapped. No volume to unmap.", volume_name) @@ -746,23 +759,25 @@ class PowerMaxCommon(object): if self.utils.does_vol_need_rdf_management_group(extra_specs): async_grp = self.utils.get_async_rdf_managed_grp_name( self.rep_config) - self._remove_members(array, volume, device_info['device_id'], - extra_specs, connector, is_multiattach, - async_grp=async_grp) + self._remove_members( + array, volume, device_info['device_id'], extra_specs, connector, + is_multiattach, async_grp=async_grp, + host_template=self.powermax_short_host_name_template) if self.utils.is_metro_device(self.rep_config, extra_specs): # Need to remove from remote masking view device_info, __ = (self.find_host_lun_id( - volume, host, extra_specs, rep_extra_specs)) + volume, host_name, extra_specs, rep_extra_specs)) if 'hostlunid' in device_info: self._remove_members( rep_extra_specs[utils.ARRAY], volume, device_info['device_id'], rep_extra_specs, connector, - is_multiattach, async_grp=async_grp) + is_multiattach, async_grp=async_grp, + host_template=self.powermax_short_host_name_template) else: # Make an attempt to clean up initiator group self.masking.attempt_ig_cleanup( connector, self.protocol, rep_extra_specs[utils.ARRAY], - True) + True, host_template=self.powermax_short_host_name_template) if is_multiattach and LOG.isEnabledFor(logging.DEBUG): mv_list, sg_list = ( self._get_mvs_and_sgs_from_volume( @@ -817,7 +832,7 @@ class PowerMaxCommon(object): if self.utils.is_volume_failed_over(volume): extra_specs = rep_extra_specs device_info_dict, is_multiattach = ( - self.find_host_lun_id(volume, connector['host'], extra_specs)) + self.find_host_lun_id(volume, connector.get('host'), extra_specs)) masking_view_dict = self._populate_masking_dict( volume, connector, extra_specs) masking_view_dict[utils.IS_MULTIATTACH] = is_multiattach @@ -843,7 +858,7 @@ class PowerMaxCommon(object): device_info_dict['maskingview'])) if self.utils.is_metro_device(self.rep_config, extra_specs): remote_info_dict, is_multiattach = ( - self.find_host_lun_id(volume, connector['host'], + self.find_host_lun_id(volume, connector.get('host'), extra_specs, rep_extra_specs)) if remote_info_dict.get('hostlunid') is None: # Need to attach on remote side @@ -987,7 +1002,7 @@ class PowerMaxCommon(object): # Find host lun id again after the volume is exported to the host. device_info_dict, __ = self.find_host_lun_id( - volume, connector['host'], extra_specs, rep_extra_specs) + volume, connector.get('host'), extra_specs, rep_extra_specs) if 'hostlunid' not in device_info_dict: # Did not successfully attach to host, so a rollback is required. error_message = (_("Error Attaching volume %(vol)s. Cannot " @@ -1499,13 +1514,23 @@ class PowerMaxCommon(object): device_id = self.get_remote_target_device( extra_specs[utils.ARRAY], volume, device_id)[0] extra_specs = rep_extra_specs - host_name = self.utils.get_host_short_name(host) if host else None + + host_name = self.utils.get_host_name_label( + host, self.powermax_short_host_name_template) if host else None if device_id: array = extra_specs[utils.ARRAY] # Return only masking views for this host host_maskingviews, all_masking_view_list = ( self._get_masking_views_from_volume( array, device_id, host_name)) + if not host_maskingviews: + # Backward compatibility if a new template was added to + # an existing backend. + host_name = self.utils.get_host_short_name( + host) if host else None + host_maskingviews, all_masking_view_list = ( + self._get_masking_views_from_volume_for_host( + all_masking_view_list, host_name)) for maskingview in host_maskingviews: host_lun_id = self.rest.find_mv_connections_for_vol( @@ -1516,6 +1541,7 @@ class PowerMaxCommon(object): 'array': array, 'device_id': device_id} maskedvols = devicedict + if not maskedvols: LOG.debug( "Host lun id not found for volume: %(volume_name)s " @@ -1573,17 +1599,28 @@ class PowerMaxCommon(object): :returns: masking view list, all masking view list """ LOG.debug("Getting masking views from volume") - host_maskingview_list, all_masking_view_list = [], [] - host_compare = True if host else False mvs, __ = self._get_mvs_and_sgs_from_volume(array, device_id) - for mv in mvs: - all_masking_view_list.append(mv) - if host_compare: - if host.lower() in mv.lower(): - host_maskingview_list.append(mv) - maskingview_list = (host_maskingview_list if host_compare else - all_masking_view_list) - return maskingview_list, all_masking_view_list + return self._get_masking_views_from_volume_for_host(mvs, host) + + def _get_masking_views_from_volume_for_host( + self, masking_views, host_name): + """Check all masking views for host_name + + :param masking_views: list of masking view + :param host_name: the host name for comparision + :returns: masking view list, all masking view list + """ + LOG.debug("Getting masking views from volume for host %(host)s ", + {'host': host_name}) + host_masking_view_list, all_masking_view_list = [], [] + for masking_view in masking_views: + all_masking_view_list.append(masking_view) + if host_name: + if host_name.lower() in masking_view.lower(): + host_masking_view_list.append(masking_view) + host_masking_view_list = (host_masking_view_list if host_name else + all_masking_view_list) + return host_masking_view_list, all_masking_view_list def _get_mvs_and_sgs_from_volume(self, array, device_id): """Helper function to retrieve masking views and storage groups. @@ -1664,7 +1701,10 @@ class PowerMaxCommon(object): raise exception.VolumeBackendAPIException(exception_message) protocol = self.utils.get_short_protocol_type(self.protocol) - short_host_name = self.utils.get_host_short_name(connector['host']) + short_host_name = self.utils.get_host_name_label( + connector['host'], self.powermax_short_host_name_template) + masking_view_dict[utils.USED_HOST_NAME] = short_host_name + masking_view_dict[utils.SLO] = extra_specs[utils.SLO] masking_view_dict[utils.WORKLOAD] = 'NONE' if self.next_gen else ( extra_specs[utils.WORKLOAD]) @@ -1675,19 +1715,27 @@ class PowerMaxCommon(object): "in cinder.conf or as an extra spec. Port group " "cannot be left empty as creating a new masking " "view will fail.") + masking_view_dict[utils.PORT_GROUP_LABEL] = ( + self.utils.get_port_name_label( + extra_specs[utils.PORTGROUPNAME], + self.powermax_port_group_name_template)) + masking_view_dict[utils.PORTGROUPNAME] = ( extra_specs[utils.PORTGROUPNAME]) masking_view_dict[utils.INITIATOR_CHECK] = ( self._get_initiator_check_flag()) - child_sg_name, do_disable_compression, rep_enabled, short_pg_name = ( - self.utils.get_child_sg_name(short_host_name, extra_specs)) + child_sg_name, do_disable_compression, rep_enabled = ( + self.utils.get_child_sg_name( + short_host_name, extra_specs, + masking_view_dict[utils.PORT_GROUP_LABEL])) masking_view_dict[utils.DISABLECOMPRESSION] = do_disable_compression masking_view_dict[utils.IS_RE] = rep_enabled mv_prefix = ( "OS-%(shortHostName)s-%(protocol)s-%(pg)s" % {'shortHostName': short_host_name, - 'protocol': protocol, 'pg': short_pg_name}) + 'protocol': protocol, + 'pg': masking_view_dict[utils.PORT_GROUP_LABEL]}) masking_view_dict[utils.SG_NAME] = child_sg_name @@ -2159,7 +2207,8 @@ class PowerMaxCommon(object): """ metro_wwns = [] host = connector['host'] - short_host_name = self.utils.get_host_short_name(host) + short_host_name = self.utils.get_host_name_label( + host, self.powermax_short_host_name_template) if host else None extra_specs = self._initial_setup(volume) rep_extra_specs = self._get_replication_extra_specs( extra_specs, self.rep_config) @@ -3443,8 +3492,12 @@ class PowerMaxCommon(object): {'volume_name': device_id}) return False, None - target_sg_name, __, __, __ = self.utils.get_child_sg_name( - attached_host, target_extra_specs) + port_group_label = self.utils.get_port_name_label( + target_extra_specs[utils.PORTGROUPNAME], + self.powermax_port_group_name_template) + + target_sg_name, __, __ = self.utils.get_child_sg_name( + attached_host, target_extra_specs, port_group_label) target_sg = self.rest.get_storage_group(array, target_sg_name) if not target_sg: diff --git a/cinder/volume/drivers/dell_emc/powermax/fc.py b/cinder/volume/drivers/dell_emc/powermax/fc.py index 76fe7762402..59555c3c138 100644 --- a/cinder/volume/drivers/dell_emc/powermax/fc.py +++ b/cinder/volume/drivers/dell_emc/powermax/fc.py @@ -117,6 +117,8 @@ class PowerMaxFCDriver(san.SanDriver, driver.FibreChannelDriver): - Volume/Snapshot backed metadata inclusion - Debug metadata compression and service level info fix 4.2.0 - Support of Unisphere storage group and array tags + - User defined override for short host name and port group name + (bp powermax-user-defined-hostname-portgroup) """ VERSION = "4.2.0" @@ -332,7 +334,8 @@ class PowerMaxFCDriver(san.SanDriver, driver.FibreChannelDriver): """ loc = volume.provider_location name = ast.literal_eval(loc) - host = self.common.utils.get_host_short_name(connector['host']) + host_label = self.common.utils.get_host_name_label( + connector['host'], self.common.powermax_short_host_name_template) zoning_mappings = {} try: array = name['array'] @@ -345,7 +348,14 @@ class PowerMaxFCDriver(san.SanDriver, driver.FibreChannelDriver): masking_views, is_metro = ( self.common.get_masking_views_from_volume( - array, volume, device_id, host)) + array, volume, device_id, host_label)) + if not masking_views: + # Backward compatibility with pre Ussuri short host name. + host_label = self.common.utils.get_host_short_name( + connector['host']) + masking_views, is_metro = ( + self.common.get_masking_views_from_volume( + array, volume, device_id, host_label)) if masking_views: portgroup = ( self.common.get_port_group_from_masking_view( @@ -379,7 +389,7 @@ class PowerMaxFCDriver(san.SanDriver, driver.FibreChannelDriver): else: masking_views, __ = ( self.common.get_masking_views_from_volume( - metro_array, volume, metro_device_id, host)) + metro_array, volume, metro_device_id, host_label)) if masking_views: metro_portgroup = ( self.common.get_port_group_from_masking_view( diff --git a/cinder/volume/drivers/dell_emc/powermax/iscsi.py b/cinder/volume/drivers/dell_emc/powermax/iscsi.py index abd64c4ea2f..4921bb6417b 100644 --- a/cinder/volume/drivers/dell_emc/powermax/iscsi.py +++ b/cinder/volume/drivers/dell_emc/powermax/iscsi.py @@ -122,6 +122,8 @@ class PowerMaxISCSIDriver(san.SanISCSIDriver): - Volume/Snapshot backed metadata inclusion - Debug metadata compression and service level info fix 4.2.0 - Support of Unisphere storage group and array tags + - User defined override for short host name and port group name + (bp powermax-user-defined-hostname-portgroup) """ VERSION = "4.2.0" diff --git a/cinder/volume/drivers/dell_emc/powermax/masking.py b/cinder/volume/drivers/dell_emc/powermax/masking.py index c538f077b3a..49cc0e3fbf7 100644 --- a/cinder/volume/drivers/dell_emc/powermax/masking.py +++ b/cinder/volume/drivers/dell_emc/powermax/masking.py @@ -240,8 +240,10 @@ class PowerMaxMasking(object): storagegroup_name = masking_view_dict[utils.SG_NAME] connector = masking_view_dict[utils.CONNECTOR] port_group_name = masking_view_dict[utils.PORTGROUPNAME] - LOG.info("Port Group in masking view operation: %(port_group_name)s.", - {'port_group_name': port_group_name}) + LOG.info("Port Group in masking view operation: %(pg_name)s. " + "The port group labels is %(pg_label)s.", + {'pg_name': masking_view_dict[utils.PORTGROUPNAME], + 'pg_label': masking_view_dict[utils.PORT_GROUP_LABEL]}) init_group_name, error_message = (self._get_or_create_initiator_group( serial_number, init_group_name, connector, extra_specs)) @@ -1082,7 +1084,8 @@ class PowerMaxMasking(object): @coordination.synchronized("emc-vol-{device_id}") def remove_and_reset_members( self, serial_number, volume, device_id, volume_name, - extra_specs, reset=True, connector=None, async_grp=None): + extra_specs, reset=True, connector=None, async_grp=None, + host_template=None): """This is called on a delete, unmap device or rollback. :param serial_number: the array serial number @@ -1093,14 +1096,16 @@ class PowerMaxMasking(object): :param reset: reset, return to original SG (optional) :param connector: the connector object (optional) :param async_grp: the async rep group (optional) + :param host_template: the host template (optional) """ self._cleanup_deletion( serial_number, volume, device_id, volume_name, - extra_specs, connector, reset, async_grp) + extra_specs, connector, reset, async_grp, + host_template=host_template) def _cleanup_deletion( self, serial_number, volume, device_id, volume_name, - extra_specs, connector, reset, async_grp): + extra_specs, connector, reset, async_grp, host_template=None): """Prepare a volume for a delete operation. :param serial_number: the array serial number @@ -1111,6 +1116,7 @@ class PowerMaxMasking(object): :param connector: the connector object :param reset: flag to indicate if reset is required -- bool :param async_grp: the async rep group + :param host_template: the host template (if it exists) """ move = False short_host_name = None @@ -1124,21 +1130,24 @@ class PowerMaxMasking(object): if len(storagegroup_names) == 1 and reset is True: move = True elif connector is not None: - short_host_name = self.utils.get_host_short_name( - connector['host']) + short_host_name = self.utils.get_host_name_label( + connector.get('host'), + host_template) if connector.get('host') else None move = reset if short_host_name: for sg_name in storagegroup_names: if short_host_name in sg_name: self.remove_volume_from_sg( serial_number, device_id, volume_name, sg_name, - extra_specs, connector, move) + extra_specs, connector, move, + host_template=host_template) break else: for sg_name in storagegroup_names: self.remove_volume_from_sg( serial_number, device_id, volume_name, sg_name, - extra_specs, connector, move) + extra_specs, connector, move, + host_template=host_template) if reset is True and move is False: self.add_volume_to_default_storage_group( serial_number, device_id, volume_name, @@ -1146,7 +1155,7 @@ class PowerMaxMasking(object): def remove_volume_from_sg( self, serial_number, device_id, vol_name, storagegroup_name, - extra_specs, connector=None, move=False): + extra_specs, connector=None, move=False, host_template=None): """Remove a volume from a storage group. :param serial_number: the array serial number @@ -1156,6 +1165,7 @@ class PowerMaxMasking(object): :param extra_specs: the extra specifications :param connector: the connector object :param move: flag to indicate if move should be used instead of remove + :param host_template: the host template (if it exists) """ masking_list = self.rest.get_masking_views_from_storage_group( serial_number, storagegroup_name) @@ -1179,7 +1189,7 @@ class PowerMaxMasking(object): # Last volume in the storage group - delete sg. self._last_vol_in_sg( serial_number, device_id, vol_name, sg_name, - extra_specs, move) + extra_specs, move, host_template=host_template) else: # Not the last volume so remove it from storage group self._multiple_vols_in_sg( @@ -1221,7 +1231,8 @@ class PowerMaxMasking(object): # Last volume in the storage group - delete sg. self._last_vol_in_sg( serial_number, device_id, vol_name, sg_name, - extra_specs, move, connector) + extra_specs, move, connector, + host_template=host_template) else: # Not the last volume so remove it from storage group self._multiple_vols_in_sg( @@ -1235,8 +1246,9 @@ class PowerMaxMasking(object): return do_remove_volume_from_sg(masking_name, storagegroup_name, parent_sg_name, serial_number) - def _last_vol_in_sg(self, serial_number, device_id, volume_name, - storagegroup_name, extra_specs, move, connector=None): + def _last_vol_in_sg( + self, serial_number, device_id, volume_name, storagegroup_name, + extra_specs, move, connector=None, host_template=None): """Steps if the volume is the last in a storage group. 1. Check if the volume is in a masking view. @@ -1255,6 +1267,7 @@ class PowerMaxMasking(object): :param extra_specs: extra specifications :param move: flag to indicate a move instead of remove :param connector: the connector object + :param host_template: the host template (if it exists) :returns: status -- bool """ LOG.debug("Only one volume remains in storage group " @@ -1269,7 +1282,8 @@ class PowerMaxMasking(object): else: status = self._last_vol_masking_views( serial_number, storagegroup_name, maskingview_list, - device_id, volume_name, extra_specs, connector, move) + device_id, volume_name, extra_specs, connector, move, + host_template) return status def _last_vol_no_masking_views(self, serial_number, storagegroup_name, @@ -1314,7 +1328,8 @@ class PowerMaxMasking(object): def _last_vol_masking_views( self, serial_number, storagegroup_name, maskingview_list, - device_id, volume_name, extra_specs, connector, move): + device_id, volume_name, extra_specs, connector, move, + host_template=None): """Remove the last vol from an sg associated with masking views. Helper function for removing the last vol from a storage group @@ -1326,6 +1341,7 @@ class PowerMaxMasking(object): :param volume_name: the volume name :param extra_specs: the extra specifications :param move: flag to indicate a move instead of remove + :param host_template: the host template (if it exists) :returns: status -- bool """ status = False @@ -1336,7 +1352,8 @@ class PowerMaxMasking(object): if num_vols_in_mv == 1: self._delete_mv_ig_and_sg( serial_number, device_id, mv, storagegroup_name, - parent_sg_name, connector, move, extra_specs) + parent_sg_name, connector, move, extra_specs, + host_template=host_template) else: self._remove_last_vol_and_delete_sg( serial_number, device_id, volume_name, @@ -1435,7 +1452,7 @@ class PowerMaxMasking(object): def _delete_mv_ig_and_sg( self, serial_number, device_id, masking_view, storagegroup_name, - parent_sg_name, connector, move, extra_specs): + parent_sg_name, connector, move, extra_specs, host_template=None): """Delete the masking view, storage groups and initiator group. :param serial_number: array serial number @@ -1446,15 +1463,15 @@ class PowerMaxMasking(object): :param connector: the connector object :param move: flag to indicate if the volume should be moved :param extra_specs: the extra specifications + :param host_template: the host template (if it exists) """ - host = (self.utils.get_host_short_name(connector['host']) - if connector else None) - initiatorgroup = self.rest.get_element_from_masking_view( serial_number, masking_view, host=True) self._last_volume_delete_masking_view(serial_number, masking_view) self._last_volume_delete_initiator_group( - serial_number, initiatorgroup, host) + serial_number, initiatorgroup, + connector.get('host') if connector else None, + host_template) self._delete_cascaded_storage_groups( serial_number, storagegroup_name, parent_sg_name, extra_specs, device_id, move) @@ -1643,7 +1660,8 @@ class PowerMaxMasking(object): self.rest.delete_storage_group(serial_number, storagegroup_name) def _last_volume_delete_initiator_group( - self, serial_number, initiatorgroup_name, host): + self, serial_number, initiatorgroup_name, host, + host_template=None): """Delete the initiator group. Delete the Initiator group if it has been created by the PowerMax @@ -1651,40 +1669,58 @@ class PowerMaxMasking(object): :param serial_number: the array serial number :param initiatorgroup_name: initiator group name :param host: the short name of the host + :param host_template: the host template (if it exists) """ + def _do_delete_initiator_group(array, init_group_name): + is_deleted = False + maskingview_names = ( + self.rest.get_masking_views_by_initiator_group( + array, init_group_name)) + if not maskingview_names: + @coordination.synchronized( + "emc-ig-{ig_name}-{array}") + def _delete_ig(ig_name, array): + # Check initiator group hasn't been recently deleted + ig_details = self.rest.get_initiator_group( + serial_number, ig_name) + if ig_details: + LOG.debug( + "Last volume associated with the initiator " + "group - deleting the associated initiator " + "group %(initiatorgroup_name)s.", + {'initiatorgroup_name': ig_name}) + self.rest.delete_initiator_group( + array, ig_name) + return True + else: + return False + is_deleted = _delete_ig(init_group_name, array) + else: + LOG.warning("Initiator group %(ig_name)s is associated " + "with masking views and can't be deleted. " + "Number of associated masking view is: " + "%(nmv)d.", + {'ig_name': init_group_name, + 'nmv': len(maskingview_names)}) + return is_deleted + if host is not None: - protocol = self.utils.get_short_protocol_type(self.protocol) - default_ig_name = ("OS-%(shortHostName)s-%(protocol)s-IG" - % {'shortHostName': host, - 'protocol': protocol}) + host_label = (self.utils.get_host_name_label( + host, host_template) if host else None) + default_ig_name = self.utils.get_possible_initiator_name( + host_label, self.protocol) if initiatorgroup_name == default_ig_name: - maskingview_names = ( - self.rest.get_masking_views_by_initiator_group( - serial_number, initiatorgroup_name)) - if not maskingview_names: - @coordination.synchronized( - "emc-ig-{ig_name}-{serial_number}") - def _delete_ig(ig_name, serial_number): - # Check initiator group hasn't been recently deleted - ig_details = self.rest.get_initiator_group( - serial_number, ig_name) - if ig_details: - LOG.debug( - "Last volume associated with the initiator " - "group - deleting the associated initiator " - "group %(initiatorgroup_name)s.", - {'initiatorgroup_name': initiatorgroup_name}) - self.rest.delete_initiator_group( - serial_number, initiatorgroup_name) - _delete_ig(initiatorgroup_name, serial_number) - else: - LOG.warning("Initiator group %(ig_name)s is associated " - "with masking views and can't be deleted. " - "Number of associated masking view is: " - "%(nmv)d.", - {'ig_name': initiatorgroup_name, - 'nmv': len(maskingview_names)}) + is_deleted = _do_delete_initiator_group( + serial_number, initiatorgroup_name) + if not is_deleted: + host_label = (self.utils.get_host_short_name( + host) if host else None) + default_ig_name = self.utils.get_possible_initiator_name( + host_label, self.protocol) + if initiatorgroup_name == default_ig_name: + _do_delete_initiator_group( + serial_number, initiatorgroup_name) else: LOG.warning("Initiator group %(ig_name)s was " "not created by the PowerMax driver so will " @@ -1715,19 +1751,13 @@ class PowerMaxMasking(object): for sg in sg_list.get('storageGroupId', []): if slo_wl_combo in sg: fast_source_sg_name = sg - masking_view_name = ( - self.rest.get_masking_views_from_storage_group( - serial_number, fast_source_sg_name))[0] - port_group_name = self.rest.get_element_from_masking_view( - serial_number, masking_view_name, portgroup=True) - short_pg_name = self.utils.get_pg_short_name(port_group_name) - short_host_name = masking_view_name.lstrip('OS-').rstrip( - '-%s-MV' % short_pg_name)[:-2] - extra_specs[utils.PORTGROUPNAME] = short_pg_name + short_host_name, port_group_label = ( + self._get_host_and_port_group_labels( + serial_number, fast_source_sg_name)) no_slo_extra_specs = deepcopy(extra_specs) no_slo_extra_specs[utils.SLO] = None - no_slo_sg_name, __, __, __ = self.utils.get_child_sg_name( - short_host_name, no_slo_extra_specs) + no_slo_sg_name, __, __ = self.utils.get_child_sg_name( + short_host_name, no_slo_extra_specs, port_group_label) source_sg_details = self.rest.get_storage_group( serial_number, fast_source_sg_name) parent_sg_name = source_sg_details[ @@ -1763,6 +1793,34 @@ class PowerMaxMasking(object): message=exception_message) return mv_dict + def _get_host_and_port_group_labels( + self, serial_number, storage_group): + """Get the host and port group labels + + :param serial_number: the array serial number + :param storage_group: the storage group + :returns: short_host_name, port_group_label + """ + masking_view_name = ( + self.rest.get_masking_views_from_storage_group( + serial_number, storage_group))[0] + object_dict = self.get_components_from_masking_view_name( + masking_view_name) + return object_dict['host'], object_dict['portgroup'] + + def get_components_from_masking_view_name(self, masking_view_name): + """Get the host and port group labels + + :param masking_view_name: the masking view name + :returns: object dict + """ + regex_str = (r'^(?POS)-(?P.+?)(?PI|F)-' + r'(?P(?!CD|RE|CD-RE).+)-(?PMV)$') + + object_dict = self.utils.get_object_components_and_correct_host( + regex_str, masking_view_name) + return object_dict + def return_volume_to_fast_managed_group( self, serial_number, device_id, extra_specs): """Return a volume to a fast managed group if slo is set. @@ -1782,18 +1840,11 @@ class PowerMaxMasking(object): for sg in sg_list.get('storageGroupId', []): if slo_wl_combo in sg: no_slo_sg_name = sg - masking_view_name = ( - self.rest.get_masking_views_from_storage_group( - serial_number, no_slo_sg_name))[0] - port_group_name = self.rest.get_element_from_masking_view( - serial_number, masking_view_name, portgroup=True) - short_pg_name = self.utils.get_pg_short_name( - port_group_name) - short_host_name = masking_view_name.lstrip('OS-').rstrip( - '-%s-MV' % short_pg_name)[:-2] - extra_specs[utils.PORTGROUPNAME] = short_pg_name - fast_sg_name, _, _, _ = self.utils.get_child_sg_name( - short_host_name, extra_specs) + short_host_name, port_group_label = ( + self._get_host_and_port_group_labels( + serial_number, no_slo_sg_name)) + fast_sg_name, _, _ = self.utils.get_child_sg_name( + short_host_name, extra_specs, port_group_label) source_sg_details = self.rest.get_storage_group( serial_number, no_slo_sg_name) parent_sg_name = source_sg_details[ @@ -1863,20 +1914,24 @@ class PowerMaxMasking(object): self.rest.delete_storage_group( serial_number, child_sg_name) - def attempt_ig_cleanup(self, connector, protocol, serial_number, force): + def attempt_ig_cleanup( + self, connector, protocol, serial_number, force, + host_template=None): """Attempt to cleanup an orphan initiator group :param connector: connector object :param protocol: iscsi or fc :param serial_number: extra the array serial number :param force: flag to indicate if operation should be forced + :param host_template: the host template (if it exists) """ protocol = self.utils.get_short_protocol_type(protocol) - host_name = connector['host'] - short_host_name = self.utils.get_host_short_name(host_name) - init_group = ( - ("OS-%(shortHostName)s-%(protocol)s-IG" - % {'shortHostName': short_host_name, - 'protocol': protocol})) + host_name = connector.get('host') + + host_label = self.utils.get_host_name_label( + host_name, host_template=host_template) + initiator_group_name = self.utils.get_possible_initiator_name( + host_label, protocol) + self._check_ig_rollback( - serial_number, init_group, connector, force) + serial_number, initiator_group_name, connector, force) diff --git a/cinder/volume/drivers/dell_emc/powermax/metadata.py b/cinder/volume/drivers/dell_emc/powermax/metadata.py index acb4f54c60c..98d3a5211b0 100644 --- a/cinder/volume/drivers/dell_emc/powermax/metadata.py +++ b/cinder/volume/drivers/dell_emc/powermax/metadata.py @@ -319,7 +319,8 @@ class PowerMaxVolumeMetadata(object): parent_storage_group=parent_storage_group, initiator_group=initiator_group, port_group=port_group, - host=host, is_multipath=is_multipath, + host=host, used_host_name=masking_view_dict[utils.USED_HOST_NAME], + is_multipath=is_multipath, identifier_name=self.utils.get_volume_element_name(volume.id), openstack_name=volume.display_name, mv_list=mv_list, sg_list=sg_list, diff --git a/cinder/volume/drivers/dell_emc/powermax/utils.py b/cinder/volume/drivers/dell_emc/powermax/utils.py index def14b8bb9b..1da071985da 100644 --- a/cinder/volume/drivers/dell_emc/powermax/utils.py +++ b/cinder/volume/drivers/dell_emc/powermax/utils.py @@ -44,6 +44,8 @@ TRUNCATE_5 = 5 TRUNCATE_27 = 27 UCODE_5978_ELMSR = 221 UCODE_5978 = 5978 +UPPER_HOST_CHARS = 16 +UPPER_PORT_GROUP_CHARS = 12 ARRAY = 'array' SLO = 'slo' @@ -80,6 +82,7 @@ DEFAULT_PORT = 8443 CLONE_SNAPSHOT_NAME = "snapshot_for_clone" STORAGE_GROUP_TAGS = 'storagetype:storagegrouptags' TAG_LIST = 'tag_list' +USED_HOST_NAME = "used_host_name" # Multiattach constants IS_MULTIATTACH = 'multiattach' @@ -112,6 +115,9 @@ POWERMAX_SERVICE_LEVEL = 'powermax_service_level' POWERMAX_PORT_GROUPS = 'powermax_port_groups' POWERMAX_SNAPVX_UNLINK_LIMIT = 'powermax_snapvx_unlink_limit' POWERMAX_ARRAY_TAG_LIST = 'powermax_array_tag_list' +POWERMAX_SHORT_HOST_NAME_TEMPLATE = 'powermax_short_host_name_template' +POWERMAX_PORT_GROUP_NAME_TEMPLATE = 'powermax_port_group_name_template' +PORT_GROUP_LABEL = 'port_group_label' class PowerMaxUtils(object): @@ -127,6 +133,20 @@ class PowerMaxUtils(object): def get_host_short_name(self, host_name): """Returns the short name for a given qualified host name. + Checks the host name to see if it is the fully qualified host name + and returns part before the dot. If there is no dot in the host name + the full host name is returned. + :param host_name: the fully qualified host name + :returns: string -- the short host_name + """ + short_host_name = self.get_host_short_name_from_fqn(host_name) + + return self.generate_unique_trunc_host(short_host_name) + + @staticmethod + def get_host_short_name_from_fqn(host_name): + """Returns the short name for a given qualified host name. + Checks the host name to see if it is the fully qualified host name and returns part before the dot. If there is no dot in the host name the full host name is returned. @@ -139,7 +159,7 @@ class PowerMaxUtils(object): else: short_host_name = host_name - return self.generate_unique_trunc_host(short_host_name) + return short_host_name @staticmethod def get_volumetype_extra_specs(volume, volume_type_id=None): @@ -286,15 +306,12 @@ class PowerMaxUtils(object): :param host_name: long host name :returns: truncated host name """ - if host_name and len(host_name) > 16: - host_name = host_name.lower() - m = hashlib.md5() - m.update(host_name.encode('utf-8')) - uuid = m.hexdigest() + if host_name and len(host_name) > UPPER_HOST_CHARS: + uuid = self.get_uuid_of_input(host_name) new_name = ("%(host)s%(uuid)s" % {'host': host_name[-6:], 'uuid': uuid}) - host_name = self.truncate_string(new_name, 16) + host_name = self.truncate_string(new_name, UPPER_HOST_CHARS) return host_name def get_pg_short_name(self, portgroup_name): @@ -303,17 +320,27 @@ class PowerMaxUtils(object): :param portgroup_name: long portgroup_name :returns: truncated portgroup_name """ - if portgroup_name and len(portgroup_name) > 12: - portgroup_name = portgroup_name.lower() - m = hashlib.md5() - m.update(portgroup_name.encode('utf-8')) - uuid = m.hexdigest() + if portgroup_name and len(portgroup_name) > UPPER_PORT_GROUP_CHARS: + uuid = self.get_uuid_of_input(portgroup_name) new_name = ("%(pg)s%(uuid)s" % {'pg': portgroup_name[-6:], 'uuid': uuid}) - portgroup_name = self.truncate_string(new_name, 12) + portgroup_name = self.truncate_string( + new_name, UPPER_PORT_GROUP_CHARS) return portgroup_name + @staticmethod + def get_uuid_of_input(input_str): + """Get the uuid of the input string + + :param input_str: input string + :returns: uuid + """ + input_str = input_str.lower() + m = hashlib.md5() + m.update(input_str.encode('utf-8')) + return m.hexdigest() + @staticmethod def get_default_oversubscription_ratio(max_over_sub_ratio): """Override ratio if necessary. @@ -732,6 +759,7 @@ class PowerMaxUtils(object): """Get the name of the default sg from the extra specs. :param extra_specs: extra specs + :param rep_mode: replication mode :returns: default sg - string """ do_disable_compression = self.is_compression_disabled( @@ -763,7 +791,7 @@ class PowerMaxUtils(object): """Get the temporary group name used for failover. :param rep_config: the replication config - :return: temp_grp_name + :returns: temp_grp_name """ temp_grp_name = ("OS-%(rdf)s-temp-rdf-sg" % {'rdf': rep_config['rdf_group_label']}) @@ -771,15 +799,15 @@ class PowerMaxUtils(object): {'name': temp_grp_name}) return temp_grp_name - def get_child_sg_name(self, host_name, extra_specs): + def get_child_sg_name(self, host_name, extra_specs, port_group_label): """Get the child storage group name for a masking view. :param host_name: the short host name :param extra_specs: the extra specifications - :return: child sg name, compression flag, rep flag, short pg name + :param port_group_label: the port group label + :returns: child sg name, compression flag, rep flag, short pg name """ do_disable_compression = False - pg_name = self.get_pg_short_name(extra_specs[PORTGROUPNAME]) rep_enabled = self.is_replication_enabled(extra_specs) if extra_specs[SLO]: slo_wl_combo = self.truncate_string( @@ -790,7 +818,7 @@ class PowerMaxUtils(object): % {'shortHostName': host_name, 'srpName': unique_name, 'combo': slo_wl_combo, - 'pg': pg_name}) + 'pg': port_group_label}) do_disable_compression = self.is_compression_disabled( extra_specs) if do_disable_compression: @@ -799,11 +827,11 @@ class PowerMaxUtils(object): else: child_sg_name = ( "OS-%(shortHostName)s-No_SLO-%(pg)s" - % {'shortHostName': host_name, 'pg': pg_name}) + % {'shortHostName': host_name, 'pg': port_group_label}) if rep_enabled: rep_mode = extra_specs.get(REP_MODE, None) child_sg_name += self.get_replication_prefix(rep_mode) - return child_sg_name, do_disable_compression, rep_enabled, pg_name + return child_sg_name, do_disable_compression, rep_enabled @staticmethod def change_multiattach(extra_specs, new_type_extra_specs): @@ -811,7 +839,7 @@ class PowerMaxUtils(object): :param extra_specs: the source type extra specs :param new_type_extra_specs: the target type extra specs - :return: bool + :returns: bool """ is_src_multiattach = volume_utils.is_boolean_str( extra_specs.get('multiattach')) @@ -824,7 +852,7 @@ class PowerMaxUtils(object): """Check if a volume with verbose description is valid for management. :param source_vol: the verbose volume dict - :return: bool True/False + :returns: bool True/False """ vol_head = source_vol['volumeHeader'] @@ -865,7 +893,7 @@ class PowerMaxUtils(object): """Check if a volume with snapshot description is valid for management. :param source_vol: the verbose volume dict - :return: bool True/False + :returns: bool True/False """ vol_head = source_vol['volumeHeader'] @@ -902,7 +930,7 @@ class PowerMaxUtils(object): """Parse a hostname from a storage group ID. :param device_info: the device info dict - :return: str -- the attached hostname + :returns: str -- the attached hostname """ try: sg_id = device_info.get("storageGroupId")[0] @@ -962,7 +990,7 @@ class PowerMaxUtils(object): """Compare number of cylinders of source and target. :param cylinders_source: number of cylinders on source - :param cylinders_target: number of cylinders on target + :param cylinder_target: number of cylinders on target """ if float(cylinders_source) > float(cylinder_target): exception_message = ( @@ -1054,3 +1082,330 @@ class PowerMaxUtils(object): """ return ','.join(map(str, list_input)) if isinstance( list_input, list) else list_input + + def validate_short_host_name_from_template( + self, short_host_template, short_host_name): + """Validate that the short host name is in a format we can use. + + Can be one of + shortHostName - where shortHostName is what the driver specifies + it to be, default + shortHostName[:x]uuid[:x] - where first x characters of the short + host name and x uuid characters created from md5 hash of + short host name + shortHostName[:x]userdef - where first x characters of the short + host name and a user defined name + shortHostName[-x:]uuid[:x] - where last x characters of short host + name and x uuid characters created from md5 hash of short host + name + shortHostName[-x:]suserdef - where last x characters of the short + host name and a user defined name + + :param short_host_template: short host name template + :param short_host_name: short host name + :raises: VolumeBackendAPIException + :returns: new short host name -- string + """ + new_short_host_name = None + is_ok, case = self.regex_check(short_host_template, True) + if is_ok: + new_short_host_name = ( + self.generate_entity_string( + case, short_host_template, short_host_name, True)) + if not new_short_host_name: + error_message = (_('Unable to generate string from short ' + 'host template %(template)s. Please refer to ' + 'the online documentation for correct ' + 'template format(s) for short host name.') % + {'template': short_host_template}) + LOG.error(error_message) + raise exception.VolumeBackendAPIException( + message=error_message) + + return new_short_host_name + + def validate_port_group_name_from_template( + self, port_group_template, port_group_name): + """Validate that the port group name is in a format we can use. + + Can be one of + portGroupName - where portGroupName is what the driver specifies + it to be, default + portGroupName[:x]uuid[:x] - where first x characters of the short + host name and x uuid characters created from md5 hash of + short host name + portGroupName[:x]userdef - where first x characters of the short + host name and a user defined name + portGroupName[-x:]uuid[:x] - where last x characters of short host + name and x uuid characters created from md5 hash of short host + name + portGroupName[-x:]userdef - where last x characters of the short + host name and a user defined name + + :param port_group_template: port group name template + :param port_group_name: port group name + :raises: VolumeBackendAPIException + :returns: new port group name -- string + """ + new_port_group_name = None + is_ok, case = self.regex_check(port_group_template, False) + if is_ok: + new_port_group_name = ( + self.generate_entity_string( + case, port_group_template, port_group_name, False)) + + if not new_port_group_name: + error_message = (_('Unable to generate string from port group ' + 'template %(template)s. Please refer to ' + 'the online documentation for correct ' + 'template format(s) for port groups.') % + {'template': port_group_template}) + LOG.error(error_message) + raise exception.VolumeBackendAPIException( + message=error_message) + + return new_port_group_name + + def generate_entity_string( + self, case, entity_template, entity_name, entity_flag): + """Generate the entity string if the template checks out + + :param case: one of five cases + :param entity_template: entity template + :param entity_name: entity name + :param entity_flag: storage group or port group flag + :returns: new entity name -- string + """ + new_entity_name = None + override_rule_warning = False + try: + if case == '1': + new_entity_name = self.get_name_if_default_template( + entity_name, entity_flag) + elif case == '2': + pass_two, uuid = self.prepare_string_with_uuid( + entity_template, entity_name, entity_flag) + m = re.match(r'^' + entity_name + + r'\[:(\d+)\]' + uuid + r'\[:(\d+)\]$', pass_two) + if m: + num_1 = m.group(1) + num_2 = m.group(2) + self.check_upper_limit( + int(num_1), int(num_2), entity_flag) + new_entity_name = ( + entity_name[:int(num_1)] + uuid[:int(num_2)]) + override_rule_warning = True + elif case == '3': + pass_two, uuid = self.prepare_string_with_uuid( + entity_template, entity_name, entity_flag) + m = re.match(r'^' + entity_name + + r'\[-(\d+):\]' + uuid + r'\[:(\d+)\]$', pass_two) + if m: + num_1 = m.group(1) + num_2 = m.group(2) + self.check_upper_limit( + int(num_1), int(num_2), entity_flag) + new_entity_name = ( + entity_name[-int(num_1):] + uuid[:int(num_2)]) + override_rule_warning = True + elif case == '4': + pass_two = self.prepare_string_entity( + entity_template, entity_name, entity_flag) + m = re.match(r'^' + entity_name + + r'\[:(\d+)\]' + r'([a-zA-Z0-9_\\-]+)$', pass_two) + if m: + num_1 = m.group(1) + user_defined = m.group(2) + self.check_upper_limit( + int(num_1), len(user_defined), entity_flag) + new_entity_name = entity_name[:int(num_1)] + user_defined + override_rule_warning = True + elif case == '5': + pass_two = self.prepare_string_entity( + entity_template, entity_name, entity_flag) + m = re.match(r'^' + entity_name + + r'\[-(\d+):\]' + r'([a-zA-Z0-9_\\-]+)$', pass_two) + if m: + num_1 = m.group(1) + user_defined = m.group(2) + self.check_upper_limit( + int(num_1), len(user_defined), entity_flag) + new_entity_name = entity_name[-int(num_1):] + user_defined + override_rule_warning = True + if override_rule_warning: + LOG.warning( + "You have opted to override the %(entity)s naming format. " + "Once changed and you have attached volumes or created " + "new instances, you cannot revert to default or change to " + "another format.", + {'entity': 'storage group' + if entity_flag else 'port group'}) + + except Exception: + new_entity_name = None + return new_entity_name + + def get_name_if_default_template(self, entity_name, is_short_host_flag): + """Get the entity name if it is the default template + + :param entity_name: the first number + :param is_short_host_flag: the second number + :returns: entity name -- string + """ + if is_short_host_flag: + return self.get_host_short_name(entity_name) + else: + return self.get_pg_short_name(entity_name) + + @staticmethod + def check_upper_limit(num_1, num_2, is_host_flag): + """Check that the sum of number is less than upper limit. + + :param num_1: the first number + :param num_2: the second number + :param is_host_flag: is short host boolean + :raises: VolumeBackendAPIException + """ + if is_host_flag: + if (num_1 + num_2) > UPPER_HOST_CHARS: + error_message = (_("Host name exceeds the character upper " + "limit of %(upper)d. Please check your " + "short host template.") % + {'upper': UPPER_HOST_CHARS}) + LOG.error(error_message) + raise exception.VolumeBackendAPIException( + message=error_message) + else: + if (num_1 + num_2) > UPPER_PORT_GROUP_CHARS: + error_message = (_("Port group name exceeds the character " + "upper limit of %(upper)d. Please check " + "your port group template") % + {'upper': UPPER_PORT_GROUP_CHARS}) + LOG.error(error_message) + raise exception.VolumeBackendAPIException( + message=error_message) + + def prepare_string_with_uuid( + self, template, entity_str, is_short_host_flag): + """Prepare string for pass three + + :param template: the template + :param entity_str: the entity string + :param is_short_host_flag: is short host + :returns: pass_two -- string + uuid -- string + """ + pass_one = self.prepare_string_entity( + template, entity_str, is_short_host_flag) + uuid = self.get_uuid_of_input(entity_str) + pass_two = pass_one.replace('uuid', uuid) + return pass_two, uuid + + @staticmethod + def prepare_string_entity(template, entity_str, is_host_flag): + """Prepare string for pass two + + :param template: the template + :param entity_str: the entity string + :param is_host_flag: is host boolean + :returns: pass_one -- string + """ + entity_type = 'shortHostName' if is_host_flag else 'portGroupName' + # Replace entity type with variable + return template.replace( + entity_type, entity_str) + + @staticmethod + def regex_check(template, is_short_host_flag): + """Check the template is in a validate format. + + :param template: short host name template + :param is_short_host_flag: short host boolean + :returns: boolean, + case -- string + """ + if is_short_host_flag: + entity = 'shortHostName' + else: + entity = 'portGroupName' + if re.match(r'^' + entity + r'$', template): + return True, '1' + elif re.match(r'^' + entity + r'\[:\d+\]uuid\[:\d+\]$', template): + return True, '2' + elif re.match(r'^' + entity + r'\[-\d+:\]uuid\[:\d+\]$', template): + return True, '3' + elif re.match(r'^' + entity + r'\[:\d+\][a-zA-Z0-9_\\-]+$', template): + return True, '4' + elif re.match(r'^' + entity + r'\[-\d+:\][a-zA-Z0-9_\\-]+$', + template): + return True, '5' + return False, '0' + + def get_host_name_label(self, host_name_in, host_template): + """Get the host name label that will be used in PowerMax Objects + + :param host_name_in: host name as portrayed in connector object + :param host_template: + :returns: host_name_out + """ + host_name_out = self.get_host_short_name( + host_name_in) + if host_template: + short_host_name = self.get_host_short_name_from_fqn( + host_name_in) + host_name_out = ( + self.validate_short_host_name_from_template( + host_template, short_host_name)) + return host_name_out + + def get_port_name_label(self, port_name_in, port_group_template): + """Get the port name label that will be used in PowerMax Objects + + :rtype: object + :param host_name_in: host name as portrayed in connector object + :param port_group_template: port group template + :returns: port_name_out + """ + port_name_out = self.get_pg_short_name(port_name_in) + if port_group_template: + port_name_out = ( + self.validate_port_group_name_from_template( + port_group_template, port_name_in)) + return port_name_out + + @staticmethod + def get_object_components(regex_str, input_str): + """Get components from input string. + + :param regex_str: the regex -- str + :param input_str: the input string -- str + :returns: dict + """ + full_str = re.compile(regex_str) + match = full_str.match(input_str) + return match.groupdict() if match else None + + def get_object_components_and_correct_host(self, regex_str, input_str): + """Get components from input string. + + :param regex_str: the regex -- str + :param input_str: the input string -- str + :returns: object components -- dict + """ + object_dict = self.get_object_components(regex_str, input_str) + if object_dict and 'host' in object_dict: + if object_dict['host'].endswith('-'): + object_dict['host'] = object_dict['host'][:-1] + return object_dict + + def get_possible_initiator_name(self, host_label, protocol): + """Get possible initiator name based on the host + + :param host_label: the host label -- str + :param protocol: the protocol -- str + :returns: initiator_group_name -- str + """ + protocol = self.get_short_protocol_type(protocol) + return ("OS-%(shortHostName)s-%(protocol)s-IG" + % {'shortHostName': host_label, + 'protocol': protocol}) diff --git a/releasenotes/notes/powermax-user-defined-hostname-portgroup-0b01aaaa730dfaaf.yaml b/releasenotes/notes/powermax-user-defined-hostname-portgroup-0b01aaaa730dfaaf.yaml new file mode 100644 index 00000000000..cafd97756bc --- /dev/null +++ b/releasenotes/notes/powermax-user-defined-hostname-portgroup-0b01aaaa730dfaaf.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Dell EMC PowerMax driver now faciliates the user to override the + short host name and port group name seen in PowerMax masking view + and storage view terminology. This means the user can give more + meaningful names, especially when the short host name exceeds 16 + characters and the port group name exceeds 12 characters, which + is the condition where the driver truncates these values.