From 921230a5ec7be400c74aa33ab8354983ad0bad5e Mon Sep 17 00:00:00 2001 From: Helen Walsh Date: Tue, 16 Feb 2016 22:33:24 +0000 Subject: [PATCH] EMC VMAX - not cleaning up HW Resource WWPN initiators On cleanup, the last volume is unmapped and the masking view and storage group are deleted. Now the initiator group and the associated StorageHardwareIDs are also deleted as long as that initiator group is no longer contained in another masking view. Change-Id: Ib159f719a9c5cbac5c6ea0b7d9474a64a0335b71 Closes-Bug: #1504192 --- cinder/tests/unit/test_emc_vmax.py | 94 ++++++++- cinder/volume/drivers/emc/emc_vmax_masking.py | 186 ++++++++++++++++-- 2 files changed, 260 insertions(+), 20 deletions(-) diff --git a/cinder/tests/unit/test_emc_vmax.py b/cinder/tests/unit/test_emc_vmax.py index 0d059bf31..23e024c34 100644 --- a/cinder/tests/unit/test_emc_vmax.py +++ b/cinder/tests/unit/test_emc_vmax.py @@ -702,7 +702,7 @@ class FakeEcomConnection(object): def Associators(self, objectpath, ResultClass='EMC_StorageHardwareID'): result = None - if ResultClass == 'EMC_StorageHardwareID': + if '_StorageHardwareID' in ResultClass: result = self._assoc_hdwid() elif ResultClass == 'EMC_iSCSIProtocolEndpoint': result = self._assoc_endpoint() @@ -1554,6 +1554,7 @@ class FakeEcomConnection(object): hdwid = SE_StorageHardwareID() hdwid['CreationClassName'] = self.data.hardwareid_creationclass hdwid['StorageID'] = self.data.connector['wwpns'][0] + hdwid['InstanceID'] = "W-+-" + self.data.connector['wwpns'][0] hdwid.path = hdwid storhdwids.append(hdwid) @@ -2478,6 +2479,95 @@ class EMCVMAXISCSIDriverNoFastTestCase(test.TestCase): conn, controllerConfigService, storageGroupInstanceName, storageGroupName, volumeInstanceName, volumeName, extraSpecs) + # Bug 1504192 - if the last volume is being unmapped and the masking view + # goes away, cleanup the initiators and associated initiator group. + def test_delete_initiators_from_initiator_group(self): + conn = self.fake_ecom_connection() + controllerConfigService = ( + self.driver.utils.find_controller_configuration_service( + conn, self.data.storage_system)) + initiatorGroupName = self.data.initiatorgroup_name + initiatorGroupInstanceName = ( + self.driver.common.masking._get_initiator_group_from_masking_view( + conn, self.data.lunmaskctrl_name, self.data.storage_system)) + conn.InvokeMethod = mock.Mock(return_value=1) + # Deletion of initiators failed. + self.driver.common.masking._delete_initiators_from_initiator_group( + conn, controllerConfigService, initiatorGroupInstanceName, + initiatorGroupName) + conn.InvokeMethod = mock.Mock(return_value=0) + # Deletion of initiators successful. + self.driver.common.masking._delete_initiators_from_initiator_group( + conn, controllerConfigService, initiatorGroupInstanceName, + initiatorGroupName) + + # Bug 1504192 - if the last volume is being unmapped and the masking view + # goes away, cleanup the initiators and associated initiator group. + def test_last_volume_delete_initiator_group_exception(self): + extraSpecs = {'volume_backend_name': 'ISCSINoFAST'} + conn = self.fake_ecom_connection() + controllerConfigService = ( + self.driver.utils.find_controller_configuration_service( + conn, self.data.storage_system)) + initiatorGroupInstanceName = ( + self.driver.common.masking._get_initiator_group_from_masking_view( + conn, self.data.lunmaskctrl_name, self.data.storage_system)) + job = { + 'Job': {'InstanceID': '9999', 'status': 'success', 'type': None}} + conn.InvokeMethod = mock.Mock(return_value=(4096, job)) + self.driver.common.masking.get_masking_views_by_initiator_group = ( + mock.Mock(return_value=[])) + self.driver.common.masking._delete_initiators_from_initiator_group = ( + mock.Mock(return_value=True)) + self.driver.common.masking.utils.wait_for_job_complete = ( + mock.Mock(return_value=(2, 'failure'))) + # Exception occurrs while deleting the initiator group. + self.assertRaises( + exception.VolumeBackendAPIException, + self.driver.common.masking._last_volume_delete_initiator_group, + conn, controllerConfigService, initiatorGroupInstanceName, + extraSpecs) + + # Bug 1504192 - if the last volume is being unmapped and the masking view + # goes away, cleanup the initiators and associated initiator group. + def test_last_volume_delete_initiator_group(self): + extraSpecs = {'volume_backend_name': 'ISCSINoFAST'} + conn = self.fake_ecom_connection() + controllerConfigService = ( + self.driver.utils.find_controller_configuration_service( + conn, self.data.storage_system)) + initiatorGroupName = self.data.initiatorgroup_name + initiatorGroupInstanceName = ( + self.driver.common.masking._get_initiator_group_from_masking_view( + conn, self.data.lunmaskctrl_name, self.data.storage_system)) + self.assertEqual(initiatorGroupName, + conn.GetInstance( + initiatorGroupInstanceName)['ElementName']) + # masking view is associated with the initiator group and initiator + # group will not be deleted. + self.driver.common.masking._last_volume_delete_initiator_group( + conn, controllerConfigService, initiatorGroupInstanceName, + extraSpecs) + self.driver.common.masking.get_masking_views_by_initiator_group = ( + mock.Mock(return_value=[])) + self.driver.common.masking._delete_initiators_from_initiator_group = ( + mock.Mock(return_value=True)) + # No Masking view and initiators associated with the Initiator group + # and initiator group will be deleted. + self.driver.common.masking._last_volume_delete_initiator_group( + conn, controllerConfigService, initiatorGroupInstanceName, + extraSpecs) + job = { + 'Job': {'InstanceID': '9999', 'status': 'success', 'type': None}} + conn.InvokeMethod = mock.Mock(return_value=(4096, job)) + self.driver.common.masking.utils.wait_for_job_complete = ( + mock.Mock(return_value=(0, 'success'))) + # Deletion of initiator group is successful after waiting for job + # to complete. + self.driver.common.masking._last_volume_delete_initiator_group( + conn, controllerConfigService, initiatorGroupInstanceName, + extraSpecs) + # Tests removal of last volume in a storage group V2 def test_remove_and_reset_members(self): extraSpecs = {'volume_backend_name': 'GOLD_BE', @@ -5678,7 +5768,7 @@ class EMCV3DriverTestCase(test.TestCase): conn, controllerConfigService, storageGroupName)) vol = self.default_vol() - self.driver.common.masking._delete_mv_and_sg = mock.Mock() + self.driver.common.masking._delete_mv_ig_and_sg = mock.Mock() self.assertTrue(self.driver.common.masking._last_vol_in_SG( conn, controllerConfigService, storageGroupInstanceName, storageGroupName, vol, vol['name'], extraSpecs)) diff --git a/cinder/volume/drivers/emc/emc_vmax_masking.py b/cinder/volume/drivers/emc/emc_vmax_masking.py index d33c5462c..66127d90c 100644 --- a/cinder/volume/drivers/emc/emc_vmax_masking.py +++ b/cinder/volume/drivers/emc/emc_vmax_masking.py @@ -1817,9 +1817,11 @@ class EMCVMAXMasking(object): """Steps if the volume is the last in a storage group. 1. Check if the volume is in a masking view. - 2. If it is in a masking view, delete the masking view, - remove the volume from the storage group, and delete - the storage group. + 2. If it is in a masking view, delete the masking view, remove the + initiators from the initiator group and delete the initiator + group if there are no other masking views associated with the + initiator group, remove the volume from the storage group, and + delete the storage group. 3. If it is not in a masking view, remove the volume from the storage group and delete the storage group. @@ -1853,13 +1855,13 @@ class EMCVMAXMasking(object): @lockutils.synchronized(maskingViewName, "emc-mv-", True) - def do_delete_mv_and_sg(): - return self._delete_mv_and_sg( + def do_delete_mv_ig_and_sg(): + return self._delete_mv_ig_and_sg( conn, controllerConfigService, mvInstanceName, maskingViewName, storageGroupInstanceName, storageGroupName, volumeInstance, volumeName, extraSpecs) - do_delete_mv_and_sg() + do_delete_mv_ig_and_sg() status = True else: # Remove the volume from the storage group and delete the SG. @@ -1925,11 +1927,11 @@ class EMCVMAXMasking(object): "End: number of volumes in masking storage group: %(numVol)d.", {'numVol': len(volumeInstanceNames)}) - def _delete_mv_and_sg(self, conn, controllerConfigService, mvInstanceName, - maskingViewName, storageGroupInstanceName, - storageGroupName, volumeInstance, volumeName, - extraSpecs): - """Delete the Masking view and the Storage Group. + def _delete_mv_ig_and_sg( + self, conn, controllerConfigService, mvInstanceName, + maskingViewName, storageGroupInstanceName, storageGroupName, + volumeInstance, volumeName, extraSpecs): + """Delete the Masking view, the storage Group and the initiator group. Also does necessary cleanup like removing the policy from the storage group for V2 and returning the volume to the default @@ -1950,10 +1952,15 @@ class EMCVMAXMasking(object): storageSystemInstanceName = self.utils.find_storage_system( conn, controllerConfigService) - + initiatorGroupInstanceName = ( + self.get_initiator_group_from_masking_view(conn, mvInstanceName)) self._last_volume_delete_masking_view( conn, controllerConfigService, mvInstanceName, maskingViewName, extraSpecs) + self._last_volume_delete_initiator_group( + conn, controllerConfigService, + initiatorGroupInstanceName, extraSpecs) + if not isV3: isTieringPolicySupported, tierPolicyServiceInstanceName = ( self._get_tiering_info(conn, storageSystemInstanceName, @@ -2261,10 +2268,13 @@ class EMCVMAXMasking(object): self, conn, initiatorGroupInstanceName): """Given initiator group, retrieve the masking view instance name. + Retrieve the list of masking view instances associated with the + initiator group instance name. + :param conn: the ecom connection :param initiatorGroupInstanceName: the instance name of the initiator group - :returns: masking view instance names + :returns: list of masking view instance names """ mvInstanceNames = conn.AssociatorNames( initiatorGroupInstanceName, ResultClass='Symm_LunMaskingView') @@ -2299,13 +2309,13 @@ class EMCVMAXMasking(object): initiatorGroupInstanceNames = conn.AssociatorNames( maskingViewInstanceName, ResultClass='SE_InitiatorMaskingGroup') if len(initiatorGroupInstanceNames) > 0: - LOG.debug("Found initiator group %(pg)s in masking view %(mv)s.", - {'pg': initiatorGroupInstanceNames[0], + LOG.debug("Found initiator group %(ig)s in masking view %(mv)s.", + {'ig': initiatorGroupInstanceNames[0], 'mv': maskingViewInstanceName}) return initiatorGroupInstanceNames[0] else: - LOG.warning(_LW("No port group found in masking view %(mv)s."), - {'mv': maskingViewInstanceName}) + LOG.warning(_LW("No Initiator group found in masking view " + "%(mv)s."), {'mv': maskingViewInstanceName}) def _get_sg_or_mv_associated_with_initiator( self, conn, controllerConfigService, volumeInstanceName, @@ -2436,7 +2446,7 @@ class EMCVMAXMasking(object): if rc != 0: exceptionMessage = (_( "Error Deleting Group: %(storageGroupName)s. " - "Return code: %(rc)lu. Error: %(error)s") + "Return code: %(rc)lu. Error: %(error)s") % {'storageGroupName': storageGroupName, 'rc': rc, 'error': errordesc}) @@ -2444,6 +2454,146 @@ class EMCVMAXMasking(object): raise exception.VolumeBackendAPIException( data=exceptionMessage) + def _delete_initiator_group(self, conn, controllerConfigService, + initiatorGroupInstanceName, initiatorGroupName, + extraSpecs): + """Delete an initiatorGroup. + + :param conn - connection to the ecom server + :param controllerConfigService - controller config service + :param initiatorGroupInstanceName - the initiator group instance name + :param initiatorGroupName - initiator group name + :param extraSpecs: extra specifications + """ + + rc, job = conn.InvokeMethod( + 'DeleteGroup', + controllerConfigService, + MaskingGroup=initiatorGroupInstanceName, + Force=True) + + if rc != 0: + rc, errordesc = self.utils.wait_for_job_complete(conn, job, + extraSpecs) + if rc != 0: + exceptionMessage = (_( + "Error Deleting Initiator Group: %(initiatorGroupName)s. " + "Return code: %(rc)lu. Error: %(error)s") + % {'initiatorGroupName': initiatorGroupName, + 'rc': rc, + 'error': errordesc}) + LOG.error(exceptionMessage) + raise exception.VolumeBackendAPIException( + data=exceptionMessage) + else: + LOG.debug("Initiator group %(initiatorGroupName)s " + "is successfully deleted.", + {'initiatorGroupName': initiatorGroupName}) + else: + LOG.debug("Initiator group %(initiatorGroupName)s " + "is successfully deleted.", + {'initiatorGroupName': initiatorGroupName}) + + def _delete_storage_hardware_id(self, + conn, + hardwareIdManagementService, + hardwareIdPath): + """Delete given initiator path + + Delete the initiator. Do not rise exception or failure if deletion + fails due to any reasons. + + :param conn - connection to the ecom server + :param hardwareIdManagementService - hardware id management service + :param hardwareIdPath - The path of the initiator object + """ + ret = conn.InvokeMethod('DeleteStorageHardwareID', + hardwareIdManagementService, + HardwareID = hardwareIdPath) + if ret == 0: + LOG.debug("Deletion of initiator path %(hardwareIdPath)s " + "is successful.", {'hardwareIdPath': hardwareIdPath}) + else: + LOG.warning(_LW("Deletion of initiator path %(hardwareIdPath)s " + "is failed."), {'hardwareIdPath': hardwareIdPath}) + + def _delete_initiators_from_initiator_group(self, conn, + controllerConfigService, + initiatorGroupInstanceName, + initiatorGroupName): + """Delete initiators + + Delete all initiators associated with the initiator group instance. + Cleanup whatever is possible. It will not return any failure or + rise exception if deletion fails due to any reasons. + + :param conn - connection to the ecom server + :param controllerConfigService - controller config service + :param initiatorGroupInstanceName - the initiator group instance name + """ + storageHardwareIdInstanceNames = ( + conn.AssociatorNames(initiatorGroupInstanceName, + ResultClass='SE_StorageHardwareID')) + if len(storageHardwareIdInstanceNames) == 0: + LOG.debug("No initiators found in Initiator group " + "%(initiatorGroupName)s.", + {'initiatorGroupName': initiatorGroupName}) + return + storageSystemName = controllerConfigService['SystemName'] + hardwareIdManagementService = ( + self.utils.find_storage_hardwareid_service(conn, + storageSystemName)) + for storageHardwareIdInstanceName in storageHardwareIdInstanceNames: + initiatorName = storageHardwareIdInstanceName['InstanceID'] + hardwareIdPath = storageHardwareIdInstanceName + LOG.debug("Initiator %(initiatorName)s " + "will be deleted from the Initiator group " + "%(initiatorGroupName)s. HardwareIdPath is " + "%(hardwareIdPath)s.", + {'initiatorName': initiatorName, + 'initiatorGroupName': initiatorGroupName, + 'hardwareIdPath': hardwareIdPath}) + self._delete_storage_hardware_id(conn, + hardwareIdManagementService, + hardwareIdPath) + + def _last_volume_delete_initiator_group( + self, conn, controllerConfigService, + initiatorGroupInstanceName, extraSpecs): + """Delete the initiator group + + Delete the Initiator group if there are no masking views associated + with the initiator group. + + :param conn: the ecom connection + :param controllerConfigService: controller config service + :param igInstanceNames: initiator group instance name + :param extraSpecs: extra specifications + """ + maskingViewInstanceNames = self.get_masking_views_by_initiator_group( + conn, initiatorGroupInstanceName) + initiatorGroupInstance = conn.GetInstance(initiatorGroupInstanceName) + initiatorGroupName = initiatorGroupInstance['ElementName'] + + if len(maskingViewInstanceNames) == 0: + LOG.debug( + "Last volume is associated with the initiator group, deleting " + "the associated initiator group %(initiatorGroupName)s.", + {'initiatorGroupName': initiatorGroupName}) + self._delete_initiators_from_initiator_group( + conn, controllerConfigService, initiatorGroupInstanceName, + initiatorGroupName) + self._delete_initiator_group(conn, controllerConfigService, + initiatorGroupInstanceName, + initiatorGroupName, extraSpecs) + else: + LOG.warning(_LW("Initiator group %(initiatorGroupName)s is " + "associated with masking views and can't be " + "deleted. Number of associated masking view is: " + "%(nmv)d."), + {'initiatorGroupName': initiatorGroupName, + 'nmv': len(maskingViewInstanceNames)}) + def _create_hardware_ids( self, conn, initiatorNames, storageSystemName): """Create hardwareIds for initiator(s). -- 2.45.2