From dd065f8e191ffb2762e4cd75a1350e41aed0caae Mon Sep 17 00:00:00 2001 From: Helen Walsh Date: Thu, 8 Jun 2017 14:17:25 +0000 Subject: [PATCH] VMAX - Live Migration, replacing SMI-S with REST In VMAX driver version 3.0, SMI-S has been replaced with unisphere REST. This is porting Live Migration from SMIS to REST. See original https://review.openstack.org/#/c/450430/ for more details. Change-Id: I7e0d9cc382a75148ecd53c48f8b2e4e69a68163c Partially-Implements: blueprint vmax-rest --- .../volume/drivers/dell_emc/vmax/test_vmax.py | 80 ++++++++-- cinder/volume/drivers/dell_emc/vmax/common.py | 149 +++++++++++++++--- cinder/volume/drivers/dell_emc/vmax/fc.py | 1 + cinder/volume/drivers/dell_emc/vmax/iscsi.py | 1 + .../volume/drivers/dell_emc/vmax/masking.py | 115 +++++++++++++- cinder/volume/drivers/dell_emc/vmax/rest.py | 55 ++++++- ...x-rest-livemigration-885dd8731d5a8a88.yaml | 4 + 7 files changed, 362 insertions(+), 43 deletions(-) create mode 100644 releasenotes/notes/vmax-rest-livemigration-885dd8731d5a8a88.yaml diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vmax/test_vmax.py b/cinder/tests/unit/volume/drivers/dell_emc/vmax/test_vmax.py index 5bd8a15b1c5..4911aa3f88e 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/vmax/test_vmax.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/vmax/test_vmax.py @@ -74,6 +74,7 @@ class VMAXCommonData(object): device_id2 = '00002' rdf_group_name = '23_24_007' rdf_group_no = '70' + u4v_version = '84' # connector info wwpn1 = "123456789012345" @@ -1344,11 +1345,12 @@ class VMAXRestTest(test.TestCase): array = self.data.array storagegroup = self.data.defaultstoragegroup_name payload = {'someKey': 'someValue'} + version = self.data.u4v_version with mock.patch.object(self.rest, 'modify_resource'): self.rest.modify_storage_group(array, storagegroup, payload) self.rest.modify_resource.assert_called_once_with( self.data.array, 'sloprovisioning', 'storagegroup', - payload, resource_name=storagegroup) + payload, version, resource_name=storagegroup) def test_create_volume_from_sg_success(self): volume_name = self.data.volume_details[0]['volume_identifier'] @@ -2699,7 +2701,7 @@ class VMAXCommonTest(test.TestCase): volume = self.data.test_volume connector = self.data.connector with mock.patch.object(self.common, 'find_host_lun_id', - return_value={}): + return_value=({}, False, [])): with mock.patch.object(self.common, '_remove_members'): self.common._unmap_lun(volume, connector) self.common._remove_members.assert_not_called() @@ -2711,7 +2713,8 @@ class VMAXCommonTest(test.TestCase): ['host_lun_address']) ref_dict = {'hostlunid': int(host_lun, 16), 'maskingview': self.data.masking_view_name_f, - 'array': self.data.array} + 'array': self.data.array, + 'device_id': self.data.device_id} device_info_dict = self.common.initialize_connection(volume, connector) self.assertEqual(ref_dict, device_info_dict) @@ -2723,7 +2726,7 @@ class VMAXCommonTest(test.TestCase): masking_view_dict = self.common._populate_masking_dict( volume, connector, extra_specs) with mock.patch.object(self.common, 'find_host_lun_id', - return_value={}): + return_value=({}, False, [])): with mock.patch.object( self.common, '_attach_volume', return_value=( {}, self.data.port_group_name_f)): @@ -2731,7 +2734,7 @@ class VMAXCommonTest(test.TestCase): connector) self.assertEqual({}, device_info_dict) self.common._attach_volume.assert_called_once_with( - volume, connector, extra_specs, masking_view_dict) + volume, connector, extra_specs, masking_view_dict, False) def test_attach_volume_success(self): volume = self.data.test_volume @@ -2744,7 +2747,8 @@ class VMAXCommonTest(test.TestCase): ['host_lun_address']) ref_dict = {'hostlunid': int(host_lun, 16), 'maskingview': self.data.masking_view_name_f, - 'array': self.data.array} + 'array': self.data.array, + 'device_id': self.data.device_id} with mock.patch.object(self.masking, 'setup_masking_view', return_value={ 'port_group_name': @@ -2763,7 +2767,7 @@ class VMAXCommonTest(test.TestCase): with mock.patch.object(self.masking, 'setup_masking_view', return_value={}): with mock.patch.object(self.common, 'find_host_lun_id', - return_value={}): + return_value=({}, False, [])): with mock.patch.object( self.masking, 'check_if_rollback_action_for_masking_required'): @@ -2880,8 +2884,9 @@ class VMAXCommonTest(test.TestCase): ['host_lun_address']) ref_masked = {'hostlunid': int(host_lun, 16), 'maskingview': self.data.masking_view_name_f, - 'array': self.data.array} - maskedvols = self.common.find_host_lun_id( + 'array': self.data.array, + 'device_id': self.data.device_id} + maskedvols, __, __ = self.common.find_host_lun_id( volume, host, extra_specs) self.assertEqual(ref_masked, maskedvols) @@ -2891,7 +2896,7 @@ class VMAXCommonTest(test.TestCase): host = 'HostX' with mock.patch.object(self.rest, 'find_mv_connections_for_vol', return_value=None): - maskedvols = self.common.find_host_lun_id( + maskedvols, __, __ = self.common.find_host_lun_id( volume, host, extra_specs) self.assertEqual({}, maskedvols) @@ -3994,6 +3999,7 @@ class VMAXISCSITest(test.TestCase): ref_dict = {'maskingview': self.data.masking_view_name_f, 'array': self.data.array, 'hostlunid': 3, + 'device_id': self.data.device_id, 'ip_and_iqn': [{'ip': self.data.ip, 'iqn': self.data.initiator}], 'is_multipath': False} @@ -4951,6 +4957,60 @@ class VMAXMaskingTest(test.TestCase): self.data.parent_sg_f) self.assertEqual(2, mock_delete.call_count) + @mock.patch.object(masking.VMAXMasking, 'add_child_sg_to_parent_sg') + @mock.patch.object(masking.VMAXMasking, + 'move_volume_between_storage_groups') + @mock.patch.object(provision.VMAXProvision, 'create_storage_group') + def test_pre_live_migration(self, mock_create_sg, mock_move, mock_add): + with mock.patch.object( + rest.VMAXRest, 'get_storage_group', + side_effect=[None, self.data.sg_details[1]["storageGroupId"]] + ): + source_sg = self.data.sg_details[2]["storageGroupId"] + source_parent_sg = self.data.sg_details[4]["storageGroupId"] + source_nf_sg = source_parent_sg[:-2] + 'NONFAST' + self.data.iscsi_device_info['device_id'] = self.data.device_id + self.mask.pre_live_migration( + source_nf_sg, source_sg, source_parent_sg, False, + self.data.iscsi_device_info, None) + mock_create_sg.assert_called_once() + + @mock.patch.object(rest.VMAXRest, 'delete_storage_group') + @mock.patch.object(rest.VMAXRest, 'remove_child_sg_from_parent_sg') + def test_post_live_migration(self, mock_remove_child_sg, mock_delete_sg): + self.data.iscsi_device_info['source_sg'] = self.data.sg_details[2][ + "storageGroupId"] + self.data.iscsi_device_info['source_parent_sg'] = self.data.sg_details[ + 4]["storageGroupId"] + with mock.patch.object( + rest.VMAXRest, 'get_num_vols_in_sg', side_effect=[0, 1]): + self.mask.post_live_migration(self.data.iscsi_device_info, None) + mock_remove_child_sg.assert_called_once() + mock_delete_sg.assert_called_once() + + @mock.patch.object(masking.VMAXMasking, + 'move_volume_between_storage_groups') + @mock.patch.object(rest.VMAXRest, 'delete_storage_group') + @mock.patch.object(rest.VMAXRest, 'remove_child_sg_from_parent_sg') + @mock.patch.object(masking.VMAXMasking, 'remove_volume_from_sg') + def test_failed_live_migration( + self, mock_remove_volume, mock_remove_child_sg, mock_delete_sg, + mock_move): + device_dict = self.data.iscsi_device_info + device_dict['device_id'] = self.data.device_id + device_dict['source_sg'] = self.data.sg_details[2]["storageGroupId"] + device_dict['source_parent_sg'] = self.data.sg_details[4][ + "storageGroupId"] + device_dict['source_nf_sg'] = ( + self.data.sg_details[4]["storageGroupId"][:-2] + 'NONFAST') + sg_list = [device_dict['source_nf_sg']] + with mock.patch.object( + rest.VMAXRest, 'is_child_sg_in_parent_sg', + side_effect=[True, False]): + self.mask.failed_live_migration(device_dict, sg_list, None) + mock_remove_volume.assert_not_called() + mock_remove_child_sg.assert_called_once() + class VMAXCommonReplicationTest(test.TestCase): def setUp(self): diff --git a/cinder/volume/drivers/dell_emc/vmax/common.py b/cinder/volume/drivers/dell_emc/vmax/common.py index 77c4ad3e363..b52a5009f76 100644 --- a/cinder/volume/drivers/dell_emc/vmax/common.py +++ b/cinder/volume/drivers/dell_emc/vmax/common.py @@ -415,16 +415,31 @@ class VMAXCommon(object): LOG.exception(exception_message) raise exception.VolumeBackendAPIException(data=exception_message) - device_info = self.find_host_lun_id( - volume, connector['host'], extra_specs) + device_info, is_live_migration, source_storage_group_list = ( + self.find_host_lun_id(volume, connector['host'], extra_specs)) if 'hostlunid' not in device_info: LOG.info("Volume %s is not mapped. No volume to unmap.", volume_name) return - - device_id = self._find_device_on_array(volume, extra_specs) + if is_live_migration and len(source_storage_group_list) == 1: + LOG.info("Volume %s is mapped. Failed live migration case", + volume_name) + return + source_nf_sg = None array = extra_specs[utils.ARRAY] - self._remove_members(array, volume, device_id, extra_specs) + if len(source_storage_group_list) > 1: + for storage_group in source_storage_group_list: + if 'NONFAST' in storage_group: + source_nf_sg = storage_group + break + if source_nf_sg: + # Remove volume from non fast storage group + self.masking.remove_volume_from_sg( + array, device_info['device_id'], volume_name, storage_group, + extra_specs) + else: + self._remove_members(array, volume, device_info['device_id'], + extra_specs) def initialize_connection(self, volume, connector): """Initializes the connection and returns device and connection info. @@ -463,13 +478,15 @@ class VMAXCommon(object): if self.utils.is_volume_failed_over(volume): extra_specs = self._get_replication_extra_specs( extra_specs, self.rep_config) - device_info_dict = self.find_host_lun_id( - volume, connector['host'], extra_specs) + device_info_dict, is_live_migration, source_storage_group_list = ( + self.find_host_lun_id(volume, connector['host'], extra_specs)) masking_view_dict = self._populate_masking_dict( volume, connector, extra_specs) if ('hostlunid' in device_info_dict and - device_info_dict['hostlunid'] is not None): + device_info_dict['hostlunid'] is not None and + is_live_migration is False) or ( + is_live_migration and len(source_storage_group_list) > 1): hostlunid = device_info_dict['hostlunid'] LOG.info("Volume %(volume)s is already mapped. " "The hostlunid is %(hostlunid)s.", @@ -481,9 +498,39 @@ class VMAXCommon(object): device_info_dict['maskingview'])) else: + if is_live_migration: + source_nf_sg, source_sg, source_parent_sg, is_source_nf_sg = ( + self._setup_for_live_migration( + device_info_dict, source_storage_group_list)) + masking_view_dict['source_nf_sg'] = source_nf_sg + masking_view_dict['source_sg'] = source_sg + masking_view_dict['source_parent_sg'] = source_parent_sg + try: + self.masking.pre_live_migration( + source_nf_sg, source_sg, source_parent_sg, + is_source_nf_sg, device_info_dict, extra_specs) + except Exception: + # Move it back to original storage group + source_storage_group_list = ( + self.rest.get_storage_groups_from_volume( + device_info_dict['array'], + device_info_dict['device_id'])) + self.masking.failed_live_migration( + masking_view_dict, source_storage_group_list, + extra_specs) + exception_message = (_( + "Unable to setup live migration because of the " + "following error: %(errorMessage)s.") + % {'errorMessage': sys.exc_info()[1]}) + raise exception.VolumeBackendAPIException( + data=exception_message) device_info_dict, port_group_name = ( self._attach_volume( - volume, connector, extra_specs, masking_view_dict)) + volume, connector, extra_specs, masking_view_dict, + is_live_migration)) + if is_live_migration: + self.masking.post_live_migration( + masking_view_dict, extra_specs) if self.protocol.lower() == 'iscsi': device_info_dict['ip_and_iqn'] = ( self._find_ip_and_iqns( @@ -492,7 +539,7 @@ class VMAXCommon(object): return device_info_dict def _attach_volume(self, volume, connector, extra_specs, - masking_view_dict): + masking_view_dict, is_live_migration=False): """Attach a volume to a host. :param volume: the volume object @@ -504,14 +551,18 @@ class VMAXCommon(object): :raises: VolumeBackendAPIException """ volume_name = volume.name - + if is_live_migration: + masking_view_dict['isLiveMigration'] = True + else: + masking_view_dict['isLiveMigration'] = False rollback_dict = self.masking.setup_masking_view( masking_view_dict[utils.ARRAY], masking_view_dict, extra_specs) # 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) + + device_info_dict, __, __ = self.find_host_lun_id( + volume, connector['host'], extra_specs) if 'hostlunid' not in device_info_dict: # Did not successfully attach to host, # so a rollback for FAST is required. @@ -830,14 +881,17 @@ class VMAXCommon(object): :returns: dict -- the data dict """ maskedvols = {} + is_live_migration = False volume_name = volume.name device_id = self._find_device_on_array(volume, extra_specs) if device_id: array = extra_specs[utils.ARRAY] host = self.utils.get_host_short_name(host) + source_storage_group_list = ( + self.rest.get_storage_groups_from_volume(array, device_id)) # return only masking views for this host maskingviews = self.get_masking_views_from_volume( - array, device_id, host) + array, device_id, host, source_storage_group_list) for maskingview in maskingviews: host_lun_id = self.rest.find_mv_connections_for_vol( @@ -845,7 +899,8 @@ class VMAXCommon(object): if host_lun_id is not None: devicedict = {'hostlunid': host_lun_id, 'maskingview': maskingview, - 'array': array} + 'array': array, + 'device_id': device_id} maskedvols = devicedict if not maskedvols: LOG.debug( @@ -856,32 +911,55 @@ class VMAXCommon(object): else: LOG.debug("Device info: %(maskedvols)s.", {'maskedvols': maskedvols}) + host = self.utils.get_host_short_name(host) + hoststr = ("-%(host)s-" + % {'host': host}) + + if hoststr.lower() not in maskedvols['maskingview'].lower(): + LOG.debug( + "Volume is masked but not to host %(host)s as is " + "expected. Assuming live migration.", + {'host': host}) + is_live_migration = True + else: + for storage_group in source_storage_group_list: + if 'NONFAST' in storage_group: + is_live_migration = True + break else: exception_message = (_("Cannot retrieve volume %(vol)s " "from the array.") % {'vol': volume_name}) LOG.exception(exception_message) raise exception.VolumeBackendAPIException(exception_message) - return maskedvols + return maskedvols, is_live_migration, source_storage_group_list - def get_masking_views_from_volume(self, array, device_id, host): + def get_masking_views_from_volume(self, array, device_id, host, + storage_group_list=None): """Retrieve masking view list for a volume. :param array: array serial number :param device_id: the volume device id :param host: the host + :param storage_group_list: the storage group list to use :returns: masking view list """ LOG.debug("Getting masking views from volume") maskingview_list = [] short_host = self.utils.get_host_short_name(host) - storagegrouplist = self.rest.get_storage_groups_from_volume( - array, device_id) - for sg in storagegrouplist: + host_compare = False + if not storage_group_list: + storage_group_list = self.rest.get_storage_groups_from_volume( + array, device_id) + host_compare = True + for sg in storage_group_list: mvs = self.rest.get_masking_views_from_storage_group( array, sg) for mv in mvs: - if short_host.lower() in mv.lower(): + if host_compare: + if short_host.lower() in mv.lower(): + maskingview_list.append(mv) + else: maskingview_list.append(mv) return maskingview_list @@ -2523,3 +2601,32 @@ class VMAXCommon(object): secondary_info['SerialNumber'] = six.text_type(rep_config['array']) secondary_info['srpName'] = rep_config['srp'] return secondary_info + + def _setup_for_live_migration(self, device_info_dict, + source_storage_group_list): + """Function to set attributes for live migration. + + :param device_info_dict: the data dict + :param source_storage_group_list: + :returns: source_nf_sg: The non fast storage group + :returns: source_sg: The source storage group + :returns: source_parent_sg: The parent storage group + :returns: is_source_nf_sg:if the non fast storage group already exists + """ + array = device_info_dict['array'] + source_sg = None + is_source_nf_sg = False + # Get parent storage group + source_parent_sg = self.rest.get_element_from_masking_view( + array, device_info_dict['maskingview'], storagegroup=True) + source_nf_sg = source_parent_sg[:-2] + 'NONFAST' + for sg in source_storage_group_list: + is_descendant = self.rest.is_child_sg_in_parent_sg( + array, sg, source_parent_sg) + if is_descendant: + source_sg = sg + is_descendant = self.rest.is_child_sg_in_parent_sg( + array, source_nf_sg, source_parent_sg) + if is_descendant: + is_source_nf_sg = True + return source_nf_sg, source_sg, source_parent_sg, is_source_nf_sg diff --git a/cinder/volume/drivers/dell_emc/vmax/fc.py b/cinder/volume/drivers/dell_emc/vmax/fc.py index d4101b67f19..72fe7fa2758 100644 --- a/cinder/volume/drivers/dell_emc/vmax/fc.py +++ b/cinder/volume/drivers/dell_emc/vmax/fc.py @@ -80,6 +80,7 @@ class VMAXFCDriver(driver.FibreChannelDriver): - QoS support - Support for compression on All Flash - Support for volume replication + - Support for live migration """ VERSION = "3.0.0" diff --git a/cinder/volume/drivers/dell_emc/vmax/iscsi.py b/cinder/volume/drivers/dell_emc/vmax/iscsi.py index eae2399f464..260da112e39 100644 --- a/cinder/volume/drivers/dell_emc/vmax/iscsi.py +++ b/cinder/volume/drivers/dell_emc/vmax/iscsi.py @@ -85,6 +85,7 @@ class VMAXISCSIDriver(driver.ISCSIDriver): - QoS support - Support for compression on All Flash - Support for volume replication + - Support for live migration """ VERSION = "3.0.0" diff --git a/cinder/volume/drivers/dell_emc/vmax/masking.py b/cinder/volume/drivers/dell_emc/vmax/masking.py index 55e652044d4..1ad2e6ba0bd 100644 --- a/cinder/volume/drivers/dell_emc/vmax/masking.py +++ b/cinder/volume/drivers/dell_emc/vmax/masking.py @@ -68,9 +68,12 @@ class VMAXMasking(object): volume_name = masking_view_dict[utils.VOL_NAME] masking_view_dict[utils.EXTRA_SPECS] = extra_specs device_id = masking_view_dict[utils.DEVICE_ID] - default_sg_name = self._get_default_storagegroup_and_remove_vol( - serial_number, device_id, masking_view_dict, volume_name, - extra_specs) + if 'source_nf_sg' in masking_view_dict: + default_sg_name = masking_view_dict['source_nf_sg'] + else: + default_sg_name = self._get_default_storagegroup_and_remove_vol( + serial_number, device_id, masking_view_dict, volume_name, + extra_specs) try: error_message = self._get_or_create_masking_view( @@ -304,9 +307,12 @@ class VMAXMasking(object): return msg def add_child_sg_to_parent_sg( - self, serial_number, child_sg_name, parent_sg_name, extra_specs): + self, serial_number, child_sg_name, parent_sg_name, extra_specs, + default_version=True + ): """Add a child storage group to a parent storage group. + :param default_version: the default uv4 version :param serial_number: the array serial number :param child_sg_name: the name of the child storage group :param parent_sg_name: the name of the aprent storage group @@ -323,8 +329,12 @@ class VMAXMasking(object): serial_number, child_sg_name, parent_sg_name): pass else: - self.rest.add_child_sg_to_parent_sg( - serial_number, child_sg, parent_sg, extra_specs) + if default_version: + self.rest.add_child_sg_to_parent_sg( + serial_number, child_sg, parent_sg, extra_specs) + else: + self.rest.add_empty_child_sg_to_parent_sg( + serial_number, child_sg, parent_sg, extra_specs) do_add_sg_to_sg(child_sg_name, parent_sg_name) @@ -434,6 +444,20 @@ class VMAXMasking(object): return child_sg_name, msg + def move_volume_between_storage_groups( + self, array, device_id, source_storagegroup_name, + target_storagegroup_name, extra_specs): + @coordination.synchronized("emc-sg-{source_storage_group}") + @coordination.synchronized("emc-sg-{target_storage_group}") + def do_move_volume_between_storage_groups(source_storage_group, + target_storage_group): + self.rest.move_volume_between_storage_groups( + array, device_id, source_storage_group, target_storage_group, + extra_specs) + + do_move_volume_between_storage_groups( + source_storagegroup_name, target_storagegroup_name) + def _check_port_group(self, serial_number, portgroup_name): """Check that you can get a port group. @@ -733,6 +757,12 @@ class VMAXMasking(object): if error_message: LOG.error(error_message) message = (_("Rollback")) + elif 'isLiveMigration' in rollback_dict and ( + rollback_dict['isLiveMigration'] is True): + # Live migration case. + # Remove from nonfast storage group to fast sg + self.failed_live_migration(rollback_dict, found_sg_name, + rollback_dict[utils.EXTRA_SPECS]) else: LOG.info("The storage group found is %(found_sg_name)s.", {'found_sg_name': found_sg_name}) @@ -1334,3 +1364,76 @@ class VMAXMasking(object): "not created by the VMAX driver so will " "not be deleted by the VMAX driver.", {'ig_name': initiatorgroup_name}) + + def pre_live_migration(self, source_nf_sg, source_sg, source_parent_sg, + is_source_nf_sg, device_info_dict, extra_specs): + """Run before any live migration operation. + + :param source_nf_sg: The non fast storage group + :param source_sg: The source storage group + :param source_parent_sg: The parent storage group + :param is_source_nf_sg: if the non fast storage group already exists + :param device_info_dict: the data dict + :param extra_specs: extra specifications + """ + if is_source_nf_sg is False: + storage_group = self.rest.get_storage_group( + device_info_dict['array'], source_nf_sg) + if storage_group is None: + self.provision.create_storage_group( + device_info_dict['array'], source_nf_sg, None, None, None, + extra_specs) + self.add_child_sg_to_parent_sg( + device_info_dict['array'], source_nf_sg, source_parent_sg, + extra_specs, default_version=False) + self.move_volume_between_storage_groups( + device_info_dict['array'], device_info_dict['device_id'], + source_sg, source_nf_sg, extra_specs) + + def post_live_migration(self, device_info_dict, extra_specs): + """Run after every live migration operation. + + :param device_info_dict: : the data dict + :param extra_specs: extra specifications + """ + array = device_info_dict['array'] + source_sg = device_info_dict['source_sg'] + # Delete fast storage group + num_vol_in_sg = self.rest.get_num_vols_in_sg( + array, source_sg) + if num_vol_in_sg == 0: + self.rest.remove_child_sg_from_parent_sg( + array, source_sg, device_info_dict['source_parent_sg'], + extra_specs) + self.rest.delete_storage_group(array, source_sg) + + def failed_live_migration(self, device_info_dict, + source_storage_group_list, extra_specs): + """This is run in the event of a failed live migration operation. + + :param device_info_dict: the data dict + :param source_storage_group_list: list of storage groups associated + with the device + :param extra_specs: extra specifications + """ + array = device_info_dict['array'] + source_nf_sg = device_info_dict['source_nf_sg'] + source_sg = device_info_dict['source_sg'] + source_parent_sg = device_info_dict['source_parent_sg'] + device_id = device_info_dict['device_id'] + for sg in source_storage_group_list: + if sg not in [source_sg, source_nf_sg]: + self.remove_volume_from_sg( + array, device_id, device_info_dict['volume_name'], sg, + extra_specs) + if source_nf_sg in source_storage_group_list: + self.move_volume_between_storage_groups( + array, device_id, source_nf_sg, + source_sg, extra_specs) + is_descendant = self.rest.is_child_sg_in_parent_sg( + array, source_nf_sg, source_parent_sg) + if is_descendant: + self.rest.remove_child_sg_from_parent_sg( + array, source_nf_sg, source_parent_sg, extra_specs) + # Delete non fast storage group + self.rest.delete_storage_group(array, source_nf_sg) diff --git a/cinder/volume/drivers/dell_emc/vmax/rest.py b/cinder/volume/drivers/dell_emc/vmax/rest.py index 19a6c732b62..46f888ee2e2 100644 --- a/cinder/volume/drivers/dell_emc/vmax/rest.py +++ b/cinder/volume/drivers/dell_emc/vmax/rest.py @@ -280,7 +280,7 @@ class VMAXRest(object): @staticmethod def _build_uri(array, category, resource_type, - resource_name=None, private=''): + resource_name=None, private='', version=U4V_VERSION): """Build the target url. :param array: the array serial number @@ -292,7 +292,7 @@ class VMAXRest(object): """ target_uri = ('%(private)s/%(version)s/%(category)s/symmetrix/' '%(array)s/%(resource_type)s' - % {'private': private, 'version': U4V_VERSION, + % {'private': private, 'version': version, 'category': category, 'array': array, 'resource_type': resource_type}) if resource_name: @@ -357,9 +357,10 @@ class VMAXRest(object): return status_code, message def modify_resource(self, array, category, resource_type, payload, - resource_name=None, private=''): + version=U4V_VERSION, resource_name=None, private=''): """Modify a resource. + :param version: the uv4 version :param array: the array serial number :param category: the category :param resource_type: the resource type @@ -369,7 +370,7 @@ class VMAXRest(object): :returns: status_code -- int, message -- string (server response) """ target_uri = self._build_uri(array, category, resource_type, - resource_name, private) + resource_name, private, version) status_code, message = self.request(target_uri, PUT, request_object=payload) operation = 'modify %(res)s resource' % {'res': resource_type} @@ -554,6 +555,24 @@ class VMAXRest(object): sc, job = self.modify_storage_group(array, parent_sg, payload) self.wait_for_job('Add child sg to parent sg', sc, job, extra_specs) + def add_empty_child_sg_to_parent_sg( + self, array, child_sg, parent_sg, extra_specs): + """Add an empty storage group to a parent storage group. + + This method adds an existing storage group to another storage + group, i.e. cascaded storage groups. + :param array: the array serial number + :param child_sg: the name of the child sg + :param parent_sg: the name of the parent sg + :param extra_specs: the extra specifications + """ + payload = {"editStorageGroupActionParam": { + "addExistingStorageGroupParam": { + "storageGroupId": [child_sg]}}} + sc, job = self.modify_storage_group(array, parent_sg, payload, + version="83") + self.wait_for_job('Add child sg to parent sg', sc, job, extra_specs) + def remove_child_sg_from_parent_sg( self, array, child_sg, parent_sg, extra_specs): """Remove a storage group from its parent storage group. @@ -621,16 +640,18 @@ class VMAXRest(object): job, extra_specs) return storagegroup_name - def modify_storage_group(self, array, storagegroup, payload): + def modify_storage_group(self, array, storagegroup, payload, + version=U4V_VERSION): """Modify a storage group (PUT operation). + :param version: the uv4 version :param array: the array serial number :param storagegroup: storage group name :param payload: the request payload :returns: status_code -- int, message -- string, server response """ return self.modify_resource( - array, SLOPROVISIONING, 'storagegroup', payload, + array, SLOPROVISIONING, 'storagegroup', payload, version, resource_name=storagegroup) def create_volume_from_sg(self, array, volume_name, storagegroup_name, @@ -836,6 +857,28 @@ class VMAXRest(object): array, SLOPROVISIONING, 'storagegroup', storagegroup_name) LOG.debug("Storage Group successfully deleted.") + def move_volume_between_storage_groups( + self, array, device_id, source_storagegroup_name, + target_storagegroup_name, extra_specs): + """Move a volume to a different storage group. + + :param array: the array serial number + :param source_storagegroup_name: the originating storage group name + :param target_storagegroup_name: the destination storage group name + :param device_id: the device id + :param extra_specs: extra specifications + """ + payload = ({"executionOption": "ASYNCHRONOUS", + "editStorageGroupActionParam": { + "moveVolumeToStorageGroupParam": { + "volumeId": [device_id], + "storageGroupId": target_storagegroup_name, + "useForceFlag": "false"}}}) + status_code, job = self.modify_storage_group( + array, source_storagegroup_name, payload) + self.wait_for_job('move volume between storage groups', status_code, + job, extra_specs) + def get_volume(self, array, device_id): """Get a VMAX volume from array. diff --git a/releasenotes/notes/vmax-rest-livemigration-885dd8731d5a8a88.yaml b/releasenotes/notes/vmax-rest-livemigration-885dd8731d5a8a88.yaml new file mode 100644 index 00000000000..2880e0b2c8c --- /dev/null +++ b/releasenotes/notes/vmax-rest-livemigration-885dd8731d5a8a88.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adding Live Migration functionality to VMAX driver version 3.0.