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:
parent
22eb9b69c1
commit
dd065f8e19
@ -74,6 +74,7 @@ class VMAXCommonData(object):
|
|||||||
device_id2 = '00002'
|
device_id2 = '00002'
|
||||||
rdf_group_name = '23_24_007'
|
rdf_group_name = '23_24_007'
|
||||||
rdf_group_no = '70'
|
rdf_group_no = '70'
|
||||||
|
u4v_version = '84'
|
||||||
|
|
||||||
# connector info
|
# connector info
|
||||||
wwpn1 = "123456789012345"
|
wwpn1 = "123456789012345"
|
||||||
@ -1344,11 +1345,12 @@ class VMAXRestTest(test.TestCase):
|
|||||||
array = self.data.array
|
array = self.data.array
|
||||||
storagegroup = self.data.defaultstoragegroup_name
|
storagegroup = self.data.defaultstoragegroup_name
|
||||||
payload = {'someKey': 'someValue'}
|
payload = {'someKey': 'someValue'}
|
||||||
|
version = self.data.u4v_version
|
||||||
with mock.patch.object(self.rest, 'modify_resource'):
|
with mock.patch.object(self.rest, 'modify_resource'):
|
||||||
self.rest.modify_storage_group(array, storagegroup, payload)
|
self.rest.modify_storage_group(array, storagegroup, payload)
|
||||||
self.rest.modify_resource.assert_called_once_with(
|
self.rest.modify_resource.assert_called_once_with(
|
||||||
self.data.array, 'sloprovisioning', 'storagegroup',
|
self.data.array, 'sloprovisioning', 'storagegroup',
|
||||||
payload, resource_name=storagegroup)
|
payload, version, resource_name=storagegroup)
|
||||||
|
|
||||||
def test_create_volume_from_sg_success(self):
|
def test_create_volume_from_sg_success(self):
|
||||||
volume_name = self.data.volume_details[0]['volume_identifier']
|
volume_name = self.data.volume_details[0]['volume_identifier']
|
||||||
@ -2699,7 +2701,7 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
volume = self.data.test_volume
|
volume = self.data.test_volume
|
||||||
connector = self.data.connector
|
connector = self.data.connector
|
||||||
with mock.patch.object(self.common, 'find_host_lun_id',
|
with mock.patch.object(self.common, 'find_host_lun_id',
|
||||||
return_value={}):
|
return_value=({}, False, [])):
|
||||||
with mock.patch.object(self.common, '_remove_members'):
|
with mock.patch.object(self.common, '_remove_members'):
|
||||||
self.common._unmap_lun(volume, connector)
|
self.common._unmap_lun(volume, connector)
|
||||||
self.common._remove_members.assert_not_called()
|
self.common._remove_members.assert_not_called()
|
||||||
@ -2711,7 +2713,8 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
['host_lun_address'])
|
['host_lun_address'])
|
||||||
ref_dict = {'hostlunid': int(host_lun, 16),
|
ref_dict = {'hostlunid': int(host_lun, 16),
|
||||||
'maskingview': self.data.masking_view_name_f,
|
'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)
|
device_info_dict = self.common.initialize_connection(volume, connector)
|
||||||
self.assertEqual(ref_dict, device_info_dict)
|
self.assertEqual(ref_dict, device_info_dict)
|
||||||
|
|
||||||
@ -2723,7 +2726,7 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
masking_view_dict = self.common._populate_masking_dict(
|
masking_view_dict = self.common._populate_masking_dict(
|
||||||
volume, connector, extra_specs)
|
volume, connector, extra_specs)
|
||||||
with mock.patch.object(self.common, 'find_host_lun_id',
|
with mock.patch.object(self.common, 'find_host_lun_id',
|
||||||
return_value={}):
|
return_value=({}, False, [])):
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
self.common, '_attach_volume', return_value=(
|
self.common, '_attach_volume', return_value=(
|
||||||
{}, self.data.port_group_name_f)):
|
{}, self.data.port_group_name_f)):
|
||||||
@ -2731,7 +2734,7 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
connector)
|
connector)
|
||||||
self.assertEqual({}, device_info_dict)
|
self.assertEqual({}, device_info_dict)
|
||||||
self.common._attach_volume.assert_called_once_with(
|
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):
|
def test_attach_volume_success(self):
|
||||||
volume = self.data.test_volume
|
volume = self.data.test_volume
|
||||||
@ -2744,7 +2747,8 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
['host_lun_address'])
|
['host_lun_address'])
|
||||||
ref_dict = {'hostlunid': int(host_lun, 16),
|
ref_dict = {'hostlunid': int(host_lun, 16),
|
||||||
'maskingview': self.data.masking_view_name_f,
|
'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',
|
with mock.patch.object(self.masking, 'setup_masking_view',
|
||||||
return_value={
|
return_value={
|
||||||
'port_group_name':
|
'port_group_name':
|
||||||
@ -2763,7 +2767,7 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
with mock.patch.object(self.masking, 'setup_masking_view',
|
with mock.patch.object(self.masking, 'setup_masking_view',
|
||||||
return_value={}):
|
return_value={}):
|
||||||
with mock.patch.object(self.common, 'find_host_lun_id',
|
with mock.patch.object(self.common, 'find_host_lun_id',
|
||||||
return_value={}):
|
return_value=({}, False, [])):
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
self.masking,
|
self.masking,
|
||||||
'check_if_rollback_action_for_masking_required'):
|
'check_if_rollback_action_for_masking_required'):
|
||||||
@ -2880,8 +2884,9 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
['host_lun_address'])
|
['host_lun_address'])
|
||||||
ref_masked = {'hostlunid': int(host_lun, 16),
|
ref_masked = {'hostlunid': int(host_lun, 16),
|
||||||
'maskingview': self.data.masking_view_name_f,
|
'maskingview': self.data.masking_view_name_f,
|
||||||
'array': self.data.array}
|
'array': self.data.array,
|
||||||
maskedvols = self.common.find_host_lun_id(
|
'device_id': self.data.device_id}
|
||||||
|
maskedvols, __, __ = self.common.find_host_lun_id(
|
||||||
volume, host, extra_specs)
|
volume, host, extra_specs)
|
||||||
self.assertEqual(ref_masked, maskedvols)
|
self.assertEqual(ref_masked, maskedvols)
|
||||||
|
|
||||||
@ -2891,7 +2896,7 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
host = 'HostX'
|
host = 'HostX'
|
||||||
with mock.patch.object(self.rest, 'find_mv_connections_for_vol',
|
with mock.patch.object(self.rest, 'find_mv_connections_for_vol',
|
||||||
return_value=None):
|
return_value=None):
|
||||||
maskedvols = self.common.find_host_lun_id(
|
maskedvols, __, __ = self.common.find_host_lun_id(
|
||||||
volume, host, extra_specs)
|
volume, host, extra_specs)
|
||||||
self.assertEqual({}, maskedvols)
|
self.assertEqual({}, maskedvols)
|
||||||
|
|
||||||
@ -3994,6 +3999,7 @@ class VMAXISCSITest(test.TestCase):
|
|||||||
ref_dict = {'maskingview': self.data.masking_view_name_f,
|
ref_dict = {'maskingview': self.data.masking_view_name_f,
|
||||||
'array': self.data.array,
|
'array': self.data.array,
|
||||||
'hostlunid': 3,
|
'hostlunid': 3,
|
||||||
|
'device_id': self.data.device_id,
|
||||||
'ip_and_iqn': [{'ip': self.data.ip,
|
'ip_and_iqn': [{'ip': self.data.ip,
|
||||||
'iqn': self.data.initiator}],
|
'iqn': self.data.initiator}],
|
||||||
'is_multipath': False}
|
'is_multipath': False}
|
||||||
@ -4951,6 +4957,60 @@ class VMAXMaskingTest(test.TestCase):
|
|||||||
self.data.parent_sg_f)
|
self.data.parent_sg_f)
|
||||||
self.assertEqual(2, mock_delete.call_count)
|
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):
|
class VMAXCommonReplicationTest(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -415,16 +415,31 @@ class VMAXCommon(object):
|
|||||||
LOG.exception(exception_message)
|
LOG.exception(exception_message)
|
||||||
raise exception.VolumeBackendAPIException(data=exception_message)
|
raise exception.VolumeBackendAPIException(data=exception_message)
|
||||||
|
|
||||||
device_info = self.find_host_lun_id(
|
device_info, is_live_migration, source_storage_group_list = (
|
||||||
volume, connector['host'], extra_specs)
|
self.find_host_lun_id(volume, connector['host'], extra_specs))
|
||||||
if 'hostlunid' not in device_info:
|
if 'hostlunid' not in device_info:
|
||||||
LOG.info("Volume %s is not mapped. No volume to unmap.",
|
LOG.info("Volume %s is not mapped. No volume to unmap.",
|
||||||
volume_name)
|
volume_name)
|
||||||
return
|
return
|
||||||
|
if is_live_migration and len(source_storage_group_list) == 1:
|
||||||
device_id = self._find_device_on_array(volume, extra_specs)
|
LOG.info("Volume %s is mapped. Failed live migration case",
|
||||||
|
volume_name)
|
||||||
|
return
|
||||||
|
source_nf_sg = None
|
||||||
array = extra_specs[utils.ARRAY]
|
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):
|
def initialize_connection(self, volume, connector):
|
||||||
"""Initializes the connection and returns device and connection info.
|
"""Initializes the connection and returns device and connection info.
|
||||||
@ -463,13 +478,15 @@ class VMAXCommon(object):
|
|||||||
if self.utils.is_volume_failed_over(volume):
|
if self.utils.is_volume_failed_over(volume):
|
||||||
extra_specs = self._get_replication_extra_specs(
|
extra_specs = self._get_replication_extra_specs(
|
||||||
extra_specs, self.rep_config)
|
extra_specs, self.rep_config)
|
||||||
device_info_dict = self.find_host_lun_id(
|
device_info_dict, is_live_migration, source_storage_group_list = (
|
||||||
volume, connector['host'], extra_specs)
|
self.find_host_lun_id(volume, connector['host'], extra_specs))
|
||||||
masking_view_dict = self._populate_masking_dict(
|
masking_view_dict = self._populate_masking_dict(
|
||||||
volume, connector, extra_specs)
|
volume, connector, extra_specs)
|
||||||
|
|
||||||
if ('hostlunid' in device_info_dict and
|
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']
|
hostlunid = device_info_dict['hostlunid']
|
||||||
LOG.info("Volume %(volume)s is already mapped. "
|
LOG.info("Volume %(volume)s is already mapped. "
|
||||||
"The hostlunid is %(hostlunid)s.",
|
"The hostlunid is %(hostlunid)s.",
|
||||||
@ -481,9 +498,39 @@ class VMAXCommon(object):
|
|||||||
device_info_dict['maskingview']))
|
device_info_dict['maskingview']))
|
||||||
|
|
||||||
else:
|
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 = (
|
device_info_dict, port_group_name = (
|
||||||
self._attach_volume(
|
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':
|
if self.protocol.lower() == 'iscsi':
|
||||||
device_info_dict['ip_and_iqn'] = (
|
device_info_dict['ip_and_iqn'] = (
|
||||||
self._find_ip_and_iqns(
|
self._find_ip_and_iqns(
|
||||||
@ -492,7 +539,7 @@ class VMAXCommon(object):
|
|||||||
return device_info_dict
|
return device_info_dict
|
||||||
|
|
||||||
def _attach_volume(self, volume, connector, extra_specs,
|
def _attach_volume(self, volume, connector, extra_specs,
|
||||||
masking_view_dict):
|
masking_view_dict, is_live_migration=False):
|
||||||
"""Attach a volume to a host.
|
"""Attach a volume to a host.
|
||||||
|
|
||||||
:param volume: the volume object
|
:param volume: the volume object
|
||||||
@ -504,14 +551,18 @@ class VMAXCommon(object):
|
|||||||
:raises: VolumeBackendAPIException
|
:raises: VolumeBackendAPIException
|
||||||
"""
|
"""
|
||||||
volume_name = volume.name
|
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(
|
rollback_dict = self.masking.setup_masking_view(
|
||||||
masking_view_dict[utils.ARRAY],
|
masking_view_dict[utils.ARRAY],
|
||||||
masking_view_dict, extra_specs)
|
masking_view_dict, extra_specs)
|
||||||
|
|
||||||
# Find host lun id again after the volume is exported to the host.
|
# 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:
|
if 'hostlunid' not in device_info_dict:
|
||||||
# Did not successfully attach to host,
|
# Did not successfully attach to host,
|
||||||
# so a rollback for FAST is required.
|
# so a rollback for FAST is required.
|
||||||
@ -830,14 +881,17 @@ class VMAXCommon(object):
|
|||||||
:returns: dict -- the data dict
|
:returns: dict -- the data dict
|
||||||
"""
|
"""
|
||||||
maskedvols = {}
|
maskedvols = {}
|
||||||
|
is_live_migration = False
|
||||||
volume_name = volume.name
|
volume_name = volume.name
|
||||||
device_id = self._find_device_on_array(volume, extra_specs)
|
device_id = self._find_device_on_array(volume, extra_specs)
|
||||||
if device_id:
|
if device_id:
|
||||||
array = extra_specs[utils.ARRAY]
|
array = extra_specs[utils.ARRAY]
|
||||||
host = self.utils.get_host_short_name(host)
|
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
|
# return only masking views for this host
|
||||||
maskingviews = self.get_masking_views_from_volume(
|
maskingviews = self.get_masking_views_from_volume(
|
||||||
array, device_id, host)
|
array, device_id, host, source_storage_group_list)
|
||||||
|
|
||||||
for maskingview in maskingviews:
|
for maskingview in maskingviews:
|
||||||
host_lun_id = self.rest.find_mv_connections_for_vol(
|
host_lun_id = self.rest.find_mv_connections_for_vol(
|
||||||
@ -845,7 +899,8 @@ class VMAXCommon(object):
|
|||||||
if host_lun_id is not None:
|
if host_lun_id is not None:
|
||||||
devicedict = {'hostlunid': host_lun_id,
|
devicedict = {'hostlunid': host_lun_id,
|
||||||
'maskingview': maskingview,
|
'maskingview': maskingview,
|
||||||
'array': array}
|
'array': array,
|
||||||
|
'device_id': device_id}
|
||||||
maskedvols = devicedict
|
maskedvols = devicedict
|
||||||
if not maskedvols:
|
if not maskedvols:
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
@ -856,32 +911,55 @@ class VMAXCommon(object):
|
|||||||
else:
|
else:
|
||||||
LOG.debug("Device info: %(maskedvols)s.",
|
LOG.debug("Device info: %(maskedvols)s.",
|
||||||
{'maskedvols': maskedvols})
|
{'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:
|
else:
|
||||||
exception_message = (_("Cannot retrieve volume %(vol)s "
|
exception_message = (_("Cannot retrieve volume %(vol)s "
|
||||||
"from the array.") % {'vol': volume_name})
|
"from the array.") % {'vol': volume_name})
|
||||||
LOG.exception(exception_message)
|
LOG.exception(exception_message)
|
||||||
raise exception.VolumeBackendAPIException(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.
|
"""Retrieve masking view list for a volume.
|
||||||
|
|
||||||
:param array: array serial number
|
:param array: array serial number
|
||||||
:param device_id: the volume device id
|
:param device_id: the volume device id
|
||||||
:param host: the host
|
:param host: the host
|
||||||
|
:param storage_group_list: the storage group list to use
|
||||||
:returns: masking view list
|
:returns: masking view list
|
||||||
"""
|
"""
|
||||||
LOG.debug("Getting masking views from volume")
|
LOG.debug("Getting masking views from volume")
|
||||||
maskingview_list = []
|
maskingview_list = []
|
||||||
short_host = self.utils.get_host_short_name(host)
|
short_host = self.utils.get_host_short_name(host)
|
||||||
storagegrouplist = self.rest.get_storage_groups_from_volume(
|
host_compare = False
|
||||||
array, device_id)
|
if not storage_group_list:
|
||||||
for sg in storagegrouplist:
|
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(
|
mvs = self.rest.get_masking_views_from_storage_group(
|
||||||
array, sg)
|
array, sg)
|
||||||
for mv in mvs:
|
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)
|
maskingview_list.append(mv)
|
||||||
return maskingview_list
|
return maskingview_list
|
||||||
|
|
||||||
@ -2523,3 +2601,32 @@ class VMAXCommon(object):
|
|||||||
secondary_info['SerialNumber'] = six.text_type(rep_config['array'])
|
secondary_info['SerialNumber'] = six.text_type(rep_config['array'])
|
||||||
secondary_info['srpName'] = rep_config['srp']
|
secondary_info['srpName'] = rep_config['srp']
|
||||||
return secondary_info
|
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
|
||||||
|
@ -80,6 +80,7 @@ class VMAXFCDriver(driver.FibreChannelDriver):
|
|||||||
- QoS support
|
- QoS support
|
||||||
- Support for compression on All Flash
|
- Support for compression on All Flash
|
||||||
- Support for volume replication
|
- Support for volume replication
|
||||||
|
- Support for live migration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = "3.0.0"
|
VERSION = "3.0.0"
|
||||||
|
@ -85,6 +85,7 @@ class VMAXISCSIDriver(driver.ISCSIDriver):
|
|||||||
- QoS support
|
- QoS support
|
||||||
- Support for compression on All Flash
|
- Support for compression on All Flash
|
||||||
- Support for volume replication
|
- Support for volume replication
|
||||||
|
- Support for live migration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = "3.0.0"
|
VERSION = "3.0.0"
|
||||||
|
@ -68,9 +68,12 @@ class VMAXMasking(object):
|
|||||||
volume_name = masking_view_dict[utils.VOL_NAME]
|
volume_name = masking_view_dict[utils.VOL_NAME]
|
||||||
masking_view_dict[utils.EXTRA_SPECS] = extra_specs
|
masking_view_dict[utils.EXTRA_SPECS] = extra_specs
|
||||||
device_id = masking_view_dict[utils.DEVICE_ID]
|
device_id = masking_view_dict[utils.DEVICE_ID]
|
||||||
default_sg_name = self._get_default_storagegroup_and_remove_vol(
|
if 'source_nf_sg' in masking_view_dict:
|
||||||
serial_number, device_id, masking_view_dict, volume_name,
|
default_sg_name = masking_view_dict['source_nf_sg']
|
||||||
extra_specs)
|
else:
|
||||||
|
default_sg_name = self._get_default_storagegroup_and_remove_vol(
|
||||||
|
serial_number, device_id, masking_view_dict, volume_name,
|
||||||
|
extra_specs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
error_message = self._get_or_create_masking_view(
|
error_message = self._get_or_create_masking_view(
|
||||||
@ -304,9 +307,12 @@ class VMAXMasking(object):
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
def add_child_sg_to_parent_sg(
|
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.
|
"""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 serial_number: the array serial number
|
||||||
:param child_sg_name: the name of the child storage group
|
:param child_sg_name: the name of the child storage group
|
||||||
:param parent_sg_name: the name of the aprent 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):
|
serial_number, child_sg_name, parent_sg_name):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
self.rest.add_child_sg_to_parent_sg(
|
if default_version:
|
||||||
serial_number, child_sg, parent_sg, extra_specs)
|
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)
|
do_add_sg_to_sg(child_sg_name, parent_sg_name)
|
||||||
|
|
||||||
@ -434,6 +444,20 @@ class VMAXMasking(object):
|
|||||||
|
|
||||||
return child_sg_name, msg
|
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):
|
def _check_port_group(self, serial_number, portgroup_name):
|
||||||
"""Check that you can get a port group.
|
"""Check that you can get a port group.
|
||||||
|
|
||||||
@ -733,6 +757,12 @@ class VMAXMasking(object):
|
|||||||
if error_message:
|
if error_message:
|
||||||
LOG.error(error_message)
|
LOG.error(error_message)
|
||||||
message = (_("Rollback"))
|
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:
|
else:
|
||||||
LOG.info("The storage group found is %(found_sg_name)s.",
|
LOG.info("The storage group found is %(found_sg_name)s.",
|
||||||
{'found_sg_name': found_sg_name})
|
{'found_sg_name': found_sg_name})
|
||||||
@ -1334,3 +1364,76 @@ class VMAXMasking(object):
|
|||||||
"not created by the VMAX driver so will "
|
"not created by the VMAX driver so will "
|
||||||
"not be deleted by the VMAX driver.",
|
"not be deleted by the VMAX driver.",
|
||||||
{'ig_name': initiatorgroup_name})
|
{'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)
|
||||||
|
@ -280,7 +280,7 @@ class VMAXRest(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_uri(array, category, resource_type,
|
def _build_uri(array, category, resource_type,
|
||||||
resource_name=None, private=''):
|
resource_name=None, private='', version=U4V_VERSION):
|
||||||
"""Build the target url.
|
"""Build the target url.
|
||||||
|
|
||||||
:param array: the array serial number
|
:param array: the array serial number
|
||||||
@ -292,7 +292,7 @@ class VMAXRest(object):
|
|||||||
"""
|
"""
|
||||||
target_uri = ('%(private)s/%(version)s/%(category)s/symmetrix/'
|
target_uri = ('%(private)s/%(version)s/%(category)s/symmetrix/'
|
||||||
'%(array)s/%(resource_type)s'
|
'%(array)s/%(resource_type)s'
|
||||||
% {'private': private, 'version': U4V_VERSION,
|
% {'private': private, 'version': version,
|
||||||
'category': category, 'array': array,
|
'category': category, 'array': array,
|
||||||
'resource_type': resource_type})
|
'resource_type': resource_type})
|
||||||
if resource_name:
|
if resource_name:
|
||||||
@ -357,9 +357,10 @@ class VMAXRest(object):
|
|||||||
return status_code, message
|
return status_code, message
|
||||||
|
|
||||||
def modify_resource(self, array, category, resource_type, payload,
|
def modify_resource(self, array, category, resource_type, payload,
|
||||||
resource_name=None, private=''):
|
version=U4V_VERSION, resource_name=None, private=''):
|
||||||
"""Modify a resource.
|
"""Modify a resource.
|
||||||
|
|
||||||
|
:param version: the uv4 version
|
||||||
:param array: the array serial number
|
:param array: the array serial number
|
||||||
:param category: the category
|
:param category: the category
|
||||||
:param resource_type: the resource type
|
:param resource_type: the resource type
|
||||||
@ -369,7 +370,7 @@ class VMAXRest(object):
|
|||||||
:returns: status_code -- int, message -- string (server response)
|
:returns: status_code -- int, message -- string (server response)
|
||||||
"""
|
"""
|
||||||
target_uri = self._build_uri(array, category, resource_type,
|
target_uri = self._build_uri(array, category, resource_type,
|
||||||
resource_name, private)
|
resource_name, private, version)
|
||||||
status_code, message = self.request(target_uri, PUT,
|
status_code, message = self.request(target_uri, PUT,
|
||||||
request_object=payload)
|
request_object=payload)
|
||||||
operation = 'modify %(res)s resource' % {'res': resource_type}
|
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)
|
sc, job = self.modify_storage_group(array, parent_sg, payload)
|
||||||
self.wait_for_job('Add child sg to parent sg', sc, job, extra_specs)
|
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(
|
def remove_child_sg_from_parent_sg(
|
||||||
self, array, child_sg, parent_sg, extra_specs):
|
self, array, child_sg, parent_sg, extra_specs):
|
||||||
"""Remove a storage group from its parent storage group.
|
"""Remove a storage group from its parent storage group.
|
||||||
@ -621,16 +640,18 @@ class VMAXRest(object):
|
|||||||
job, extra_specs)
|
job, extra_specs)
|
||||||
return storagegroup_name
|
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).
|
"""Modify a storage group (PUT operation).
|
||||||
|
|
||||||
|
:param version: the uv4 version
|
||||||
:param array: the array serial number
|
:param array: the array serial number
|
||||||
:param storagegroup: storage group name
|
:param storagegroup: storage group name
|
||||||
:param payload: the request payload
|
:param payload: the request payload
|
||||||
:returns: status_code -- int, message -- string, server response
|
:returns: status_code -- int, message -- string, server response
|
||||||
"""
|
"""
|
||||||
return self.modify_resource(
|
return self.modify_resource(
|
||||||
array, SLOPROVISIONING, 'storagegroup', payload,
|
array, SLOPROVISIONING, 'storagegroup', payload, version,
|
||||||
resource_name=storagegroup)
|
resource_name=storagegroup)
|
||||||
|
|
||||||
def create_volume_from_sg(self, array, volume_name, storagegroup_name,
|
def create_volume_from_sg(self, array, volume_name, storagegroup_name,
|
||||||
@ -836,6 +857,28 @@ class VMAXRest(object):
|
|||||||
array, SLOPROVISIONING, 'storagegroup', storagegroup_name)
|
array, SLOPROVISIONING, 'storagegroup', storagegroup_name)
|
||||||
LOG.debug("Storage Group successfully deleted.")
|
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):
|
def get_volume(self, array, device_id):
|
||||||
"""Get a VMAX volume from array.
|
"""Get a VMAX volume from array.
|
||||||
|
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adding Live Migration functionality to VMAX driver version 3.0.
|
Loading…
Reference in New Issue
Block a user