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
This commit is contained in:
Helen Walsh 2017-06-08 14:17:25 +00:00
parent 22eb9b69c1
commit dd065f8e19
7 changed files with 362 additions and 43 deletions

View File

@ -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):

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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)

View File

@ -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.

View File

@ -0,0 +1,4 @@
---
features:
- |
Adding Live Migration functionality to VMAX driver version 3.0.