VMAX driver - Retype replicated volumes

This patch delivers support for retyping (storage-assisted migration)
volumes with a non-replicated volume type to a replicated volume type;
replicated to non-replicated; and between two replicated volume types
with differing service levels.

Change-Id: Ic86826dccc7b830db15fb8a2eeb888dbb895292d
Implements: blueprint vmax-retype-replicated-volumes
This commit is contained in:
Ciara Stacke 2018-01-30 19:09:26 +00:00
parent 3e4cd3812e
commit 992542a9fb
7 changed files with 281 additions and 97 deletions

View File

@ -1484,6 +1484,11 @@ class VMAXUtilsTest(test.TestCase):
self.assertEqual(expected_snap_name, updated_name) self.assertEqual(expected_snap_name, updated_name)
def test_change_replication(self):
new_type = {'extra_specs': self.data.extra_specs_rep_enabled}
self.assertFalse(self.utils.change_replication(True, new_type))
self.assertTrue(self.utils.change_replication(False, new_type))
class VMAXRestTest(test.TestCase): class VMAXRestTest(test.TestCase):
def setUp(self): def setUp(self):
@ -4564,7 +4569,7 @@ class VMAXCommonTest(test.TestCase):
migrate_status = self.common._migrate_volume( migrate_status = self.common._migrate_volume(
self.data.array, volume, device_id, self.data.srp, self.data.array, volume, device_id, self.data.srp,
self.data.slo, self.data.workload, volume_name, self.data.slo, self.data.workload, volume_name,
new_type, extra_specs) new_type, extra_specs)[0]
self.assertTrue(migrate_status) self.assertTrue(migrate_status)
target_extra_specs = { target_extra_specs = {
'array': self.data.array, 'interval': 3, 'array': self.data.array, 'interval': 3,
@ -4580,7 +4585,7 @@ class VMAXCommonTest(test.TestCase):
migrate_status = self.common._migrate_volume( migrate_status = self.common._migrate_volume(
self.data.array, volume, device_id, self.data.srp, self.data.array, volume, device_id, self.data.srp,
self.data.slo, self.data.workload, volume_name, self.data.slo, self.data.workload, volume_name,
new_type, extra_specs) new_type, extra_specs)[0]
self.assertTrue(migrate_status) self.assertTrue(migrate_status)
mock_remove.assert_not_called() mock_remove.assert_not_called()
@ -4610,7 +4615,7 @@ class VMAXCommonTest(test.TestCase):
migrate_status = self.common._migrate_volume( migrate_status = self.common._migrate_volume(
self.data.array, self.data.test_volume, device_id, self.data.array, self.data.test_volume, device_id,
self.data.srp, self.data.slo, self.data.srp, self.data.slo,
self.data.workload, volume_name, new_type, extra_specs) self.data.workload, volume_name, new_type, extra_specs)[0]
self.assertFalse(migrate_status) self.assertFalse(migrate_status)
def test_is_valid_for_storage_assisted_migration_true(self): def test_is_valid_for_storage_assisted_migration_true(self):
@ -4620,20 +4625,20 @@ class VMAXCommonTest(test.TestCase):
ref_return = (True, 'Silver', 'OLTP') ref_return = (True, 'Silver', 'OLTP')
return_val = self.common._is_valid_for_storage_assisted_migration( return_val = self.common._is_valid_for_storage_assisted_migration(
device_id, host, self.data.array, device_id, host, self.data.array,
self.data.srp, volume_name, False) self.data.srp, volume_name, False, False)
self.assertEqual(ref_return, return_val) self.assertEqual(ref_return, return_val)
# No current sgs found # No current sgs found
with mock.patch.object(self.rest, 'get_storage_groups_from_volume', with mock.patch.object(self.rest, 'get_storage_groups_from_volume',
return_value=None): return_value=None):
return_val = self.common._is_valid_for_storage_assisted_migration( return_val = self.common._is_valid_for_storage_assisted_migration(
device_id, host, self.data.array, self.data.srp, device_id, host, self.data.array, self.data.srp,
volume_name, False) volume_name, False, False)
self.assertEqual(ref_return, return_val) self.assertEqual(ref_return, return_val)
host = {'host': 'HostX@Backend#Silver+SRP_1+000197800123'} host = {'host': 'HostX@Backend#Silver+SRP_1+000197800123'}
ref_return = (True, 'Silver', 'NONE') ref_return = (True, 'Silver', 'NONE')
return_val = self.common._is_valid_for_storage_assisted_migration( return_val = self.common._is_valid_for_storage_assisted_migration(
device_id, host, self.data.array, device_id, host, self.data.array,
self.data.srp, volume_name, False) self.data.srp, volume_name, False, False)
self.assertEqual(ref_return, return_val) self.assertEqual(ref_return, return_val)
def test_is_valid_for_storage_assisted_migration_false(self): def test_is_valid_for_storage_assisted_migration_false(self):
@ -4644,36 +4649,25 @@ class VMAXCommonTest(test.TestCase):
host = {'host': 'HostX@Backend#Silver+SRP_1+000197800123+dummy+data'} host = {'host': 'HostX@Backend#Silver+SRP_1+000197800123+dummy+data'}
return_val = self.common._is_valid_for_storage_assisted_migration( return_val = self.common._is_valid_for_storage_assisted_migration(
device_id, host, self.data.array, device_id, host, self.data.array,
self.data.srp, volume_name, False) self.data.srp, volume_name, False, False)
self.assertEqual(ref_return, return_val) self.assertEqual(ref_return, return_val)
# Wrong array # Wrong array
host2 = {'host': 'HostX@Backend#Silver+OLTP+SRP_1+00012345678'} host2 = {'host': 'HostX@Backend#Silver+OLTP+SRP_1+00012345678'}
return_val = self.common._is_valid_for_storage_assisted_migration( return_val = self.common._is_valid_for_storage_assisted_migration(
device_id, host2, self.data.array, device_id, host2, self.data.array,
self.data.srp, volume_name, False) self.data.srp, volume_name, False, False)
self.assertEqual(ref_return, return_val) self.assertEqual(ref_return, return_val)
# Wrong srp # Wrong srp
host3 = {'host': 'HostX@Backend#Silver+OLTP+SRP_2+000197800123'} host3 = {'host': 'HostX@Backend#Silver+OLTP+SRP_2+000197800123'}
return_val = self.common._is_valid_for_storage_assisted_migration( return_val = self.common._is_valid_for_storage_assisted_migration(
device_id, host3, self.data.array, device_id, host3, self.data.array,
self.data.srp, volume_name, False) self.data.srp, volume_name, False, False)
self.assertEqual(ref_return, return_val) self.assertEqual(ref_return, return_val)
# Already in correct sg # Already in correct sg
host4 = {'host': self.data.fake_host} host4 = {'host': self.data.fake_host}
return_val = self.common._is_valid_for_storage_assisted_migration( return_val = self.common._is_valid_for_storage_assisted_migration(
device_id, host4, self.data.array, device_id, host4, self.data.array,
self.data.srp, volume_name, False) self.data.srp, volume_name, False, False)
self.assertEqual(ref_return, return_val)
def test_is_valid_for_storage_assisted_migration_none(self):
device_id = self.data.device_id
host = {'host': self.data.none_host}
volume_name = self.data.test_volume.name
# Testing for 'NONE' Workload
ref_return = (True, 'Diamond', 'NONE')
return_val = self.common._is_valid_for_storage_assisted_migration(
device_id, host, self.data.array,
self.data.srp, volume_name, False)
self.assertEqual(ref_return, return_val) self.assertEqual(ref_return, return_val)
def test_find_volume_group(self): def test_find_volume_group(self):
@ -6333,12 +6327,13 @@ class VMAXMaskingTest(test.TestCase):
self.data.array, self.data.masking_view_name_i) self.data.array, self.data.masking_view_name_i)
mock_delete_mv.assert_called_once() mock_delete_mv.assert_called_once()
@mock.patch.object(masking.VMAXMasking, 'return_volume_to_volume_group')
@mock.patch.object(rest.VMAXRest, 'move_volume_between_storage_groups') @mock.patch.object(rest.VMAXRest, 'move_volume_between_storage_groups')
@mock.patch.object(masking.VMAXMasking, @mock.patch.object(masking.VMAXMasking,
'get_or_create_default_storage_group') 'get_or_create_default_storage_group')
@mock.patch.object(masking.VMAXMasking, 'add_volume_to_storage_group') @mock.patch.object(masking.VMAXMasking, 'add_volume_to_storage_group')
def test_add_volume_to_default_storage_group( def test_add_volume_to_default_storage_group(
self, mock_add_sg, mock_get_sg, mock_move): self, mock_add_sg, mock_get_sg, mock_move, mock_return):
self.mask.add_volume_to_default_storage_group( self.mask.add_volume_to_default_storage_group(
self.data.array, self.device_id, self.volume_name, self.data.array, self.device_id, self.volume_name,
self.extra_specs) self.extra_specs)
@ -6347,14 +6342,12 @@ class VMAXMaskingTest(test.TestCase):
self.data.array, self.device_id, self.volume_name, self.data.array, self.device_id, self.volume_name,
self.extra_specs, src_sg=self.data.storagegroup_name_i) self.extra_specs, src_sg=self.data.storagegroup_name_i)
mock_move.assert_called_once() mock_move.assert_called_once()
mock_add_sg.reset_mock()
vol_grp_member = deepcopy(self.data.test_volume) vol_grp_member = deepcopy(self.data.test_volume)
vol_grp_member.group_id = self.data.test_vol_grp_name_id_only vol_grp_member.group_id = self.data.test_vol_grp_name_id_only
vol_grp_member.group = self.data.test_group
self.mask.add_volume_to_default_storage_group( self.mask.add_volume_to_default_storage_group(
self.data.array, self.device_id, self.volume_name, self.data.array, self.device_id, self.volume_name,
self.extra_specs, volume=vol_grp_member) self.extra_specs, volume=vol_grp_member)
self.assertEqual(2, mock_add_sg.call_count) mock_return.assert_called_once()
@mock.patch.object(provision.VMAXProvision, 'create_storage_group') @mock.patch.object(provision.VMAXProvision, 'create_storage_group')
def test_get_or_create_default_storage_group(self, mock_create_sg): def test_get_or_create_default_storage_group(self, mock_create_sg):
@ -6570,6 +6563,32 @@ class VMAXMaskingTest(test.TestCase):
mock_remove_volume.assert_not_called() mock_remove_volume.assert_not_called()
mock_remove_child_sg.assert_called_once() mock_remove_child_sg.assert_called_once()
@mock.patch.object(masking.VMAXMasking,
'add_volumes_to_storage_group')
def test_add_remote_vols_to_volume_group(self, mock_add):
self.mask.add_remote_vols_to_volume_group(
[self.data.test_volume], self.data.test_rep_group,
self.data.rep_extra_specs)
mock_add.assert_called_once()
@mock.patch.object(masking.VMAXMasking, 'add_remote_vols_to_volume_group')
@mock.patch.object(masking.VMAXMasking,
'_check_adding_volume_to_storage_group')
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True)
@mock.patch.object(volume_utils, 'is_group_a_type',
side_effect=[False, False, True, True])
def test_return_volume_to_volume_group(self, mock_type, mock_cg,
mock_check, mock_add):
vol_grp_member = deepcopy(self.data.test_volume)
vol_grp_member.group_id = self.data.test_vol_grp_name_id_only
vol_grp_member.group = self.data.test_group
for x in range(0, 2):
self.mask.return_volume_to_volume_group(
self.data.array, vol_grp_member, self.data.device_id,
self.data.test_volume.name, self.data.extra_specs)
mock_add.assert_called_once()
class VMAXCommonReplicationTest(test.TestCase): class VMAXCommonReplicationTest(test.TestCase):
def setUp(self): def setUp(self):
@ -7400,7 +7419,7 @@ class VMAXCommonReplicationTest(test.TestCase):
@mock.patch.object(utils.VMAXUtils, 'check_rep_status_enabled') @mock.patch.object(utils.VMAXUtils, 'check_rep_status_enabled')
@mock.patch.object(common.VMAXCommon, @mock.patch.object(common.VMAXCommon,
'_remove_remote_vols_from_volume_group') '_remove_remote_vols_from_volume_group')
@mock.patch.object(common.VMAXCommon, '_add_remote_vols_to_volume_group') @mock.patch.object(masking.VMAXMasking, 'add_remote_vols_to_volume_group')
@mock.patch.object(volume_utils, 'is_group_a_type', return_value=True) @mock.patch.object(volume_utils, 'is_group_a_type', return_value=True)
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type', @mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True) return_value=True)
@ -7414,14 +7433,6 @@ class VMAXCommonReplicationTest(test.TestCase):
mock_add.assert_called_once() mock_add.assert_called_once()
mock_remove.assert_called_once() mock_remove.assert_called_once()
@mock.patch.object(masking.VMAXMasking,
'add_volumes_to_storage_group')
def test_add_remote_vols_to_volume_group(self, mock_add):
self.common._add_remote_vols_to_volume_group(
self.data.remote_array, [self.data.test_volume],
self.data.test_rep_group, self.data.rep_extra_specs)
mock_add.assert_called_once()
@mock.patch.object(masking.VMAXMasking, @mock.patch.object(masking.VMAXMasking,
'remove_volumes_from_storage_group') 'remove_volumes_from_storage_group')
def test_remove_remote_vols_from_volume_group(self, mock_rm): def test_remove_remote_vols_from_volume_group(self, mock_rm):
@ -7485,3 +7496,43 @@ class VMAXCommonReplicationTest(test.TestCase):
self.async_driver.common.failover_host(volumes, None, []) self.async_driver.common.failover_host(volumes, None, [])
mock_fv.assert_not_called() mock_fv.assert_not_called()
mock_fg.assert_called_once() mock_fg.assert_called_once()
@mock.patch.object(common.VMAXCommon, '_retype_volume', return_value=True)
@mock.patch.object(masking.VMAXMasking, 'remove_vol_from_storage_group')
@mock.patch.object(common.VMAXCommon, '_retype_remote_volume',
return_value=True)
@mock.patch.object(common.VMAXCommon, 'setup_volume_replication',
return_value=VMAXCommonData.provider_location2)
@mock.patch.object(common.VMAXCommon,
'_remove_vol_and_cleanup_replication')
@mock.patch.object(utils.VMAXUtils, 'is_replication_enabled',
side_effect=[False, True, True, False, True, True])
def test_migrate_volume_replication(self, mock_re, mock_rm_rep,
mock_setup, mock_retype,
mock_rm, mock_rt):
new_type = {'extra_specs': {}}
for x in range(0, 3):
success, model_update = self.common._migrate_volume(
self.data.array, self.data.test_volume, self.data.device_id,
self.data.srp, 'OLTP', 'Silver', self.data.test_volume.name,
new_type, self.data.extra_specs)
self.assertTrue(success)
mock_rm_rep.assert_called_once()
mock_setup.assert_called_once()
mock_retype.assert_called_once()
@mock.patch.object(
common.VMAXCommon, '_get_replication_extra_specs',
return_value=VMAXCommonData.extra_specs_rep_enabled)
@mock.patch.object(
rest.VMAXRest, 'get_storage_groups_from_volume',
side_effect=[
VMAXCommonData.storagegroup_list, ['OS-SRP_1-Diamond-DSS-RE-SG']])
@mock.patch.object(common.VMAXCommon, '_retype_volume', return_value=True)
def test_retype_volume_replication(self, mock_retype, mock_sg, mock_es):
for x in range(0, 2):
self.common._retype_remote_volume(
self.data.array, self.data.test_volume, self.data.device_id,
self.data.test_volume.name, utils.REP_SYNC,
True, self.data.extra_specs)
mock_retype.assert_called_once()

View File

@ -302,9 +302,8 @@ class VMAXCommon(object):
group_name, volume_name, extra_specs) group_name, volume_name, extra_specs)
# Add remote volume to remote group, if required # Add remote volume to remote group, if required
if volume.group.is_replicated: if volume.group.is_replicated:
self._add_remote_vols_to_volume_group( self.masking.add_remote_vols_to_volume_group(
extra_specs[utils.ARRAY], volume, volume.group, extra_specs, rep_driver_data)
[volume], volume.group, extra_specs, rep_driver_data)
def create_volume_from_snapshot(self, volume, snapshot): def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot. """Creates a volume from a snapshot.
@ -2220,14 +2219,6 @@ class VMAXCommon(object):
{'name': volume_name}) {'name': volume_name})
return False return False
if self.utils.is_replication_enabled(extra_specs):
LOG.error("Volume %(name)s is replicated - "
"Replicated volumes are not eligible for "
"storage assisted retype. Host assisted "
"retype is supported.",
{'name': volume_name})
return False
return self._slo_workload_migration(device_id, volume, host, return self._slo_workload_migration(device_id, volume, host,
volume_name, new_type, extra_specs) volume_name, new_type, extra_specs)
@ -2243,6 +2234,10 @@ class VMAXCommon(object):
:param extra_specs: extra specifications :param extra_specs: extra specifications
:returns: boolean -- True if migration succeeded, False if error. :returns: boolean -- True if migration succeeded, False if error.
""" """
vol_is_replicated = self.utils.is_replication_enabled(extra_specs)
# Check if old type and new type have different replication types
do_change_replication = self.utils.change_replication(
vol_is_replicated, new_type)
is_compression_disabled = self.utils.is_compression_disabled( is_compression_disabled = self.utils.is_compression_disabled(
extra_specs) extra_specs)
# Check if old type and new type have different compression types # Check if old type and new type have different compression types
@ -2252,7 +2247,7 @@ class VMAXCommon(object):
self._is_valid_for_storage_assisted_migration( self._is_valid_for_storage_assisted_migration(
device_id, host, extra_specs[utils.ARRAY], device_id, host, extra_specs[utils.ARRAY],
extra_specs[utils.SRP], volume_name, extra_specs[utils.SRP], volume_name,
do_change_compression)) do_change_compression, do_change_replication))
if not is_valid: if not is_valid:
LOG.error( LOG.error(
@ -2260,14 +2255,15 @@ class VMAXCommon(object):
"assisted migration using retype.", "assisted migration using retype.",
{'name': volume_name}) {'name': volume_name})
return False return False
if volume.host != host['host'] or do_change_compression: if (volume.host != host['host'] or do_change_compression
or do_change_replication):
LOG.debug( LOG.debug(
"Retype Volume %(name)s from source host %(sourceHost)s " "Retype Volume %(name)s from source host %(sourceHost)s "
"to target host %(targetHost)s. Compression change is %(cc)r.", "to target host %(targetHost)s. Compression change is %(cc)r. "
{'name': volume_name, "Replication change is %(rc)s",
'sourceHost': volume.host, {'name': volume_name, 'sourceHost': volume.host,
'targetHost': host['host'], 'targetHost': host['host'],
'cc': do_change_compression}) 'cc': do_change_compression, 'rc': do_change_replication})
return self._migrate_volume( return self._migrate_volume(
extra_specs[utils.ARRAY], volume, device_id, extra_specs[utils.ARRAY], volume, device_id,
extra_specs[utils.SRP], target_slo, extra_specs[utils.SRP], target_slo,
@ -2293,6 +2289,7 @@ class VMAXCommon(object):
:param extra_specs: the extra specifications :param extra_specs: the extra specifications
:returns: bool :returns: bool
""" """
model_update, rep_mode, move_target = None, None, False
target_extra_specs = new_type['extra_specs'] target_extra_specs = new_type['extra_specs']
target_extra_specs[utils.SRP] = srp target_extra_specs[utils.SRP] = srp
target_extra_specs[utils.ARRAY] = array target_extra_specs[utils.ARRAY] = array
@ -2302,28 +2299,82 @@ class VMAXCommon(object):
target_extra_specs[utils.RETRIES] = extra_specs[utils.RETRIES] target_extra_specs[utils.RETRIES] = extra_specs[utils.RETRIES]
is_compression_disabled = self.utils.is_compression_disabled( is_compression_disabled = self.utils.is_compression_disabled(
target_extra_specs) target_extra_specs)
if self.rep_config and self.rep_config.get('mode'):
rep_mode = self.rep_config['mode']
target_extra_specs[utils.REP_MODE] = rep_mode
was_rep_enabled = self.utils.is_replication_enabled(extra_specs)
is_rep_enabled = self.utils.is_replication_enabled(target_extra_specs)
if was_rep_enabled:
if not is_rep_enabled:
# Disable replication is True
self._remove_vol_and_cleanup_replication(
array, device_id, volume_name, extra_specs, volume)
model_update = {'replication_status': REPLICATION_DISABLED,
'replication_driver_data': None}
else:
# Ensure both source and target volumes are retyped
move_target = True
else:
if is_rep_enabled:
# Setup_volume_replication will put volume in correct sg
rep_status, rdf_dict = self.setup_volume_replication(
array, volume, device_id, target_extra_specs)
model_update = {
'replication_status': rep_status,
'replication_driver_data': six.text_type(rdf_dict)}
return True, model_update
try: try:
target_sg_name = self.masking.get_or_create_default_storage_group( target_sg_name = self.masking.get_or_create_default_storage_group(
array, srp, target_slo, target_workload, extra_specs, array, srp, target_slo, target_workload, extra_specs,
is_compression_disabled) is_compression_disabled, is_rep_enabled, rep_mode)
except Exception as e: except Exception as e:
LOG.error("Failed to get or create storage group. " LOG.error("Failed to get or create storage group. "
"Exception received was %(e)s.", {'e': e}) "Exception received was %(e)s.", {'e': e})
return False return False
success = self._retype_volume(
array, device_id, volume_name, target_sg_name,
volume, target_extra_specs)
if success and move_target:
success = self._retype_remote_volume(
array, volume, device_id, volume_name,
rep_mode, is_rep_enabled, target_extra_specs)
return success, model_update
def _retype_volume(self, array, device_id, volume_name, target_sg_name,
volume, extra_specs):
"""Move the volume to the correct storagegroup.
Add the volume to the target storage group, or to the correct default
storage group, and check if it is there.
:param array: the array serial
:param device_id: the device id
:param volume_name: the volume name
:param target_sg_name: the target sg name
:param volume: the volume object
:param extra_specs: the target extra specifications
:returns bool
"""
storagegroups = self.rest.get_storage_groups_from_volume( storagegroups = self.rest.get_storage_groups_from_volume(
array, device_id) array, device_id)
if not storagegroups: if not storagegroups:
LOG.warning("Volume : %(volume_name)s does not currently " LOG.warning("Volume : %(volume_name)s does not currently "
"belong to any storage groups.", "belong to any storage groups.",
{'volume_name': volume_name}) {'volume_name': volume_name})
# Add the volume to the target storage group
self.masking.add_volume_to_storage_group( self.masking.add_volume_to_storage_group(
array, device_id, target_sg_name, volume_name, extra_specs) array, device_id, target_sg_name, volume_name, extra_specs)
# Check if volume should be member of GVG
self.masking.return_volume_to_volume_group(
array, volume, device_id, volume_name, extra_specs)
else: else:
# Move the volume to the correct default storage group for
# its volume type
self.masking.remove_and_reset_members( self.masking.remove_and_reset_members(
array, volume, device_id, volume_name, target_extra_specs, array, volume, device_id, volume_name,
reset=True) extra_specs, reset=True)
# Check that it has been added. # Check that it has been added.
vol_check = self.rest.is_volume_in_storagegroup( vol_check = self.rest.is_volume_in_storagegroup(
@ -2338,9 +2389,48 @@ class VMAXCommon(object):
return True return True
def _retype_remote_volume(self, array, volume, device_id,
volume_name, rep_mode, is_re, extra_specs):
"""Retype the remote volume.
:param array: the array serial number
:param volume: the volume object
:param device_id: the device id
:param volume_name: the volume name
:param rep_mode: the replication mode
:param is_re: replication enabled
:param extra_specs: the target extra specs
:returns: bool
"""
success = True
(target_device, remote_array, _, _, _) = (
self.get_remote_target_device(array, volume, device_id))
rep_extra_specs = self._get_replication_extra_specs(
extra_specs, self.rep_config)
rep_compr_disabled = self.utils.is_compression_disabled(
rep_extra_specs)
remote_sg_name = self.masking.get_or_create_default_storage_group(
remote_array, rep_extra_specs[utils.SRP],
rep_extra_specs[utils.SLO], rep_extra_specs[utils.WORKLOAD],
rep_extra_specs, rep_compr_disabled,
is_re=is_re, rep_mode=rep_mode)
found_storage_group_list = self.rest.get_storage_groups_from_volume(
remote_array, target_device)
move_rqd = True
for found_storage_group_name in found_storage_group_list:
# Check if remote volume is already in the correct sg
if found_storage_group_name == remote_sg_name:
move_rqd = False
break
if move_rqd:
success = self._retype_volume(
remote_array, target_device, volume_name, remote_sg_name,
volume, rep_extra_specs)
return success
def _is_valid_for_storage_assisted_migration( def _is_valid_for_storage_assisted_migration(
self, device_id, host, source_array, self, device_id, host, source_array, source_srp, volume_name,
source_srp, volume_name, do_change_compression): do_change_compression, do_change_replication):
"""Check if volume is suitable for storage assisted (pool) migration. """Check if volume is suitable for storage assisted (pool) migration.
:param device_id: the volume device id :param device_id: the volume device id
@ -2349,6 +2439,7 @@ class VMAXCommon(object):
:param source_srp: the volume's current pool name :param source_srp: the volume's current pool name
:param volume_name: the name of the volume to be migrated :param volume_name: the name of the volume to be migrated
:param do_change_compression: do change compression :param do_change_compression: do change compression
:param do_change_replication: flag indicating replication change
:returns: boolean -- True/False :returns: boolean -- True/False
:returns: string -- targetSlo :returns: string -- targetSlo
:returns: string -- targetWorkload :returns: string -- targetWorkload
@ -2375,6 +2466,8 @@ class VMAXCommon(object):
target_workload = 'NONE' target_workload = 'NONE'
else: else:
raise IndexError raise IndexError
if target_slo.lower() == 'none':
target_slo = None
except IndexError: except IndexError:
LOG.error("Error parsing array, pool, SLO and workload.") LOG.error("Error parsing array, pool, SLO and workload.")
return false_ret return false_ret
@ -2415,9 +2508,11 @@ class VMAXCommon(object):
% {'targetSlo': target_slo, % {'targetSlo': target_slo,
'targetWorkload': target_workload}) 'targetWorkload': target_workload})
if target_combination == emc_fast_setting: if target_combination == emc_fast_setting:
# Check if migration is from compression to non compression # Check if migration is to change compression
# or vice versa # or replication types
if not do_change_compression: action_rqd = (True if do_change_compression
or do_change_replication else False)
if not action_rqd:
LOG.warning( LOG.warning(
"No action required. Volume: %(volume_name)s is " "No action required. Volume: %(volume_name)s is "
"already part of slo/workload combination: " "already part of slo/workload combination: "
@ -3632,8 +3727,8 @@ class VMAXCommon(object):
array, add_device_ids, vol_grp_name, interval_retries_dict) array, add_device_ids, vol_grp_name, interval_retries_dict)
if group.is_replicated: if group.is_replicated:
# Add remote volumes to remote storage group # Add remote volumes to remote storage group
self._add_remote_vols_to_volume_group( self.masking.add_remote_vols_to_volume_group(
array, add_vols, group, interval_retries_dict) add_vols, group, interval_retries_dict)
# Remove volume(s) from the group # Remove volume(s) from the group
if remove_device_ids: if remove_device_ids:
self.masking.remove_volumes_from_storage_group( self.masking.remove_volumes_from_storage_group(
@ -3655,34 +3750,6 @@ class VMAXCommon(object):
return model_update, None, None return model_update, None, None
def _add_remote_vols_to_volume_group(
self, array, volumes, group,
extra_specs, rep_driver_data=None):
"""Add the remote volumes to their volume group.
:param array: the array serial number
:param volumes: list of volumes
:param group: the id of the group
:param extra_specs: the extra specifications
:param rep_driver_data: replication driver data, optional
"""
remote_device_list = []
__, remote_array = self.get_rdf_details(array)
for vol in volumes:
try:
remote_loc = ast.literal_eval(vol.replication_driver_data)
except (ValueError, KeyError):
remote_loc = ast.literal_eval(rep_driver_data)
founddevice_id = self.rest.check_volume_device_id(
remote_array, remote_loc['device_id'], vol.id)
if founddevice_id is not None:
remote_device_list.append(founddevice_id)
group_name = self.provision.get_or_create_volume_group(
remote_array, group, extra_specs)
self.masking.add_volumes_to_storage_group(
remote_array, remote_device_list, group_name, extra_specs)
LOG.info("Added volumes to remote volume group.")
def _remove_remote_vols_from_volume_group( def _remove_remote_vols_from_volume_group(
self, array, volumes, group, extra_specs): self, array, volumes, group, extra_specs):
"""Remove the remote volumes from their volume group. """Remove the remote volumes from their volume group.

View File

@ -90,9 +90,11 @@ class VMAXFCDriver(san.SanDriver, driver.FibreChannelDriver):
- Support for manage/unmanage snapshots - Support for manage/unmanage snapshots
(vmax-manage-unmanage-snapshot) (vmax-manage-unmanage-snapshot)
- Support for revert to volume snapshot - Support for revert to volume snapshot
3.2.0 - Support for retyping replicated volumes (bp
vmax-retype-replicated-volumes)
""" """
VERSION = "3.1.0" VERSION = "3.2.0"
# ThirdPartySystems wiki # ThirdPartySystems wiki
CI_WIKI_NAME = "EMC_VMAX_CI" CI_WIKI_NAME = "EMC_VMAX_CI"

View File

@ -95,9 +95,11 @@ class VMAXISCSIDriver(san.SanISCSIDriver):
- Support for manage/unmanage snapshots - Support for manage/unmanage snapshots
(vmax-manage-unmanage-snapshot) (vmax-manage-unmanage-snapshot)
- Support for revert to volume snapshot - Support for revert to volume snapshot
3.2.0 - Support for retyping replicated volumes (bp
vmax-retype-replicated-volumes)
""" """
VERSION = "3.1.0" VERSION = "3.2.0"
# ThirdPartySystems wiki # ThirdPartySystems wiki
CI_WIKI_NAME = "EMC_VMAX_CI" CI_WIKI_NAME = "EMC_VMAX_CI"

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import ast
import time import time
from oslo_log import log as logging from oslo_log import log as logging
@ -23,6 +24,7 @@ from cinder import exception
from cinder.i18n import _ from cinder.i18n import _
from cinder.volume.drivers.dell_emc.vmax import provision from cinder.volume.drivers.dell_emc.vmax import provision
from cinder.volume.drivers.dell_emc.vmax import utils from cinder.volume.drivers.dell_emc.vmax import utils
from cinder.volume import utils as volume_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -1454,12 +1456,59 @@ class VMAXMasking(object):
# Need to check if the volume needs to be returned to a # Need to check if the volume needs to be returned to a
# generic volume group. This may be necessary in a force-detach # generic volume group. This may be necessary in a force-detach
# situation. # situation.
if volume.group_id is not None: self.return_volume_to_volume_group(
vol_grp_name = self.provision.get_or_create_volume_group( serial_number, volume, device_id, volume_name, extra_specs)
serial_number, volume.group, extra_specs)
self._check_adding_volume_to_storage_group( def return_volume_to_volume_group(self, serial_number, volume,
serial_number, device_id, device_id, volume_name, extra_specs):
vol_grp_name, volume_name, extra_specs) """Return a volume to its volume group, if required.
:param serial_number: the array serial number
:param volume: the volume object
:param device_id: the device id
:param volume_name: the volume name
:param extra_specs: the extra specifications
"""
if (volume.group_id is not None and
(volume_utils.is_group_a_cg_snapshot_type(volume.group)
or volume.group.is_replicated)):
vol_grp_name = self.provision.get_or_create_volume_group(
serial_number, volume.group, extra_specs)
self._check_adding_volume_to_storage_group(
serial_number, device_id,
vol_grp_name, volume_name, extra_specs)
if volume.group.is_replicated:
self.add_remote_vols_to_volume_group(
volume, volume.group, extra_specs)
def add_remote_vols_to_volume_group(
self, volumes, group, extra_specs, rep_driver_data=None):
"""Add the remote volumes to their volume group.
:param volumes: list of volumes
:param group: the id of the group
:param extra_specs: the extra specifications
:param rep_driver_data: replication driver data, optional
"""
remote_device_list = []
remote_array = None
if not isinstance(volumes, list):
volumes = [volumes]
for vol in volumes:
try:
remote_loc = ast.literal_eval(vol.replication_driver_data)
except (ValueError, KeyError):
remote_loc = ast.literal_eval(rep_driver_data)
remote_array = remote_loc['array']
founddevice_id = self.rest.check_volume_device_id(
remote_array, remote_loc['device_id'], vol.id)
if founddevice_id is not None:
remote_device_list.append(founddevice_id)
group_name = self.provision.get_or_create_volume_group(
remote_array, group, extra_specs)
self.add_volumes_to_storage_group(
remote_array, remote_device_list, group_name, extra_specs)
LOG.info("Added volumes to remote volume group.")
def get_or_create_default_storage_group( def get_or_create_default_storage_group(
self, serial_number, srp, slo, workload, extra_specs, self, serial_number, srp, slo, workload, extra_specs,

View File

@ -495,6 +495,16 @@ class VMAXUtils(object):
else: else:
return True return True
def change_replication(self, vol_is_replicated, new_type):
"""Check if volume types have different replication status.
:param vol_is_replicated: from source
:param new_type: from target
:return: bool
"""
is_tgt_rep = self.is_replication_enabled(new_type['extra_specs'])
return vol_is_replicated != is_tgt_rep
@staticmethod @staticmethod
def is_replication_enabled(extra_specs): def is_replication_enabled(extra_specs):
"""Check if replication is to be enabled. """Check if replication is to be enabled.

View File

@ -0,0 +1,3 @@
---
features:
- Support for retype (storage-assisted migration) of replicated volumes on VMAX cinder driver.