PowerMax Driver - Metro ODE Support

PowerMax for Cinder now supports extending in-use Metro RDF
enabled volumes using Online Device Expansion.  This
submission implements this new feature.

Change-Id: I5342cbd64d33c38a68c92e4e56cbfce8aaa621c3
Implements: blueprint powermax-metro-ode
This commit is contained in:
Michael McAleer 2019-05-21 18:12:34 +01:00 committed by Simon O'Donovan
parent e2f52da0d3
commit fd8b74b61c
11 changed files with 425 additions and 259 deletions

View File

@ -77,6 +77,7 @@ class PowerMaxData(object):
volume_id = '2b06255d-f5f0-4520-a953-b029196add6a' volume_id = '2b06255d-f5f0-4520-a953-b029196add6a'
no_slo_sg_name = 'OS-HostX-No_SLO-OS-fibre-PG' no_slo_sg_name = 'OS-HostX-No_SLO-OS-fibre-PG'
temp_snapvx = 'temp-00001-snapshot_for_clone' temp_snapvx = 'temp-00001-snapshot_for_clone'
next_gen_ucode = 5978
# connector info # connector info
wwpn1 = '123456789012345' wwpn1 = '123456789012345'
@ -294,6 +295,14 @@ class PowerMaxData(object):
rep_extra_specs5 = deepcopy(rep_extra_specs2) rep_extra_specs5 = deepcopy(rep_extra_specs2)
rep_extra_specs5['target_array_model'] = 'VMAX250F' rep_extra_specs5['target_array_model'] = 'VMAX250F'
rep_extra_specs_ode = deepcopy(rep_extra_specs2)
rep_extra_specs_ode['array'] = array
rep_extra_specs_ode.pop('rep_mode')
rep_extra_specs_ode['mode'] = 'Metro'
rep_extra_specs_legacy = deepcopy(rep_extra_specs_ode)
rep_extra_specs_legacy['mode'] = 'Synchronous'
test_volume_type_1 = volume_type.VolumeType( test_volume_type_1 = volume_type.VolumeType(
id='2b06255d-f5f0-4520-a953-b029196add6a', name='abc', id='2b06255d-f5f0-4520-a953-b029196add6a', name='abc',
extra_specs=extra_specs) extra_specs=extra_specs)

View File

@ -58,29 +58,28 @@ class PowerMaxCommonTest(test.TestCase):
self.utils.get_volumetype_extra_specs = ( self.utils.get_volumetype_extra_specs = (
mock.Mock(return_value=self.data.vol_type_extra_specs)) mock.Mock(return_value=self.data.vol_type_extra_specs))
@mock.patch.object(rest.PowerMaxRest, 'set_rest_credentials') @mock.patch.object(rest.PowerMaxRest, 'get_array_ucode_version',
@mock.patch.object(common.PowerMaxCommon, '_get_slo_workload_combinations', return_value=tpd.PowerMaxData.next_gen_ucode)
return_value=[])
@mock.patch.object(
common.PowerMaxCommon, 'get_attributes_from_cinder_config',
return_value=[])
def test_gather_info_no_opts(self, mock_parse, mock_combo, mock_rest):
configuration = tpfo.FakeConfiguration(
None, 'config_group', None, None)
fc.PowerMaxFCDriver(configuration=configuration)
@mock.patch.object(rest.PowerMaxRest, 'get_array_model_info', @mock.patch.object(rest.PowerMaxRest, 'get_array_model_info',
return_value=('PowerMax 2000', True)) return_value=('PowerMax 2000', True))
@mock.patch.object(rest.PowerMaxRest, 'set_rest_credentials') @mock.patch.object(rest.PowerMaxRest, 'set_rest_credentials')
@mock.patch.object(common.PowerMaxCommon, '_get_slo_workload_combinations', @mock.patch.object(common.PowerMaxCommon, '_get_slo_workload_combinations',
return_value=[]) return_value=[])
@mock.patch.object( @mock.patch.object(common.PowerMaxCommon,
common.PowerMaxCommon, 'get_attributes_from_cinder_config', 'get_attributes_from_cinder_config',
return_value=tpd.PowerMaxData.array_info_wl) side_effect=[[], tpd.PowerMaxData.array_info_wl])
def test_gather_info_next_gen(self, mock_parse, mock_combo, mock_rest, def test_gather_info_tests(self, mck_parse, mck_combo, mck_rest,
mock_nextgen): mck_nextgen, mck_ucode):
# Use-Case 1: Gather info no-opts
configuration = tpfo.FakeConfiguration(
None, 'config_group', None, None)
fc.PowerMaxFCDriver(configuration=configuration)
# Use-Case 2: Gather info next-gen with ucode/version
self.common._gather_info() self.common._gather_info()
self.assertTrue(self.common.next_gen) self.assertTrue(self.common.next_gen)
self.assertEqual(self.common.ucode_level, self.data.next_gen_ucode)
def test_get_slo_workload_combinations_powermax(self): def test_get_slo_workload_combinations_powermax(self):
array_info = self.common.get_attributes_from_cinder_config() array_info = self.common.get_attributes_from_cinder_config()
@ -445,28 +444,42 @@ class PowerMaxCommonTest(test.TestCase):
mock_unmap.assert_called_once_with( mock_unmap.assert_called_once_with(
volume, connector) volume, connector)
@mock.patch.object(rest.PowerMaxRest, 'is_next_gen_array',
return_value=True)
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
@mock.patch.object(provision.PowerMaxProvision, 'extend_volume') @mock.patch.object(provision.PowerMaxProvision, 'extend_volume')
def test_extend_volume_success(self, mock_extend, mock_sync, mock_newgen): @mock.patch.object(common.PowerMaxCommon, '_array_ode_capabilities_check',
return_value=[True] * 4)
@mock.patch.object(common.PowerMaxCommon, '_extend_vol_validation_checks')
def test_extend_vol_no_rep_success(self, mck_val_chk, mck_ode_chk,
mck_extend):
volume = self.data.test_volume volume = self.data.test_volume
array = self.data.array array = self.data.array
device_id = self.data.device_id device_id = self.data.device_id
new_size = self.data.test_volume.size new_size = self.data.test_volume.size
ref_extra_specs = deepcopy(self.data.extra_specs_intervals_set) ref_extra_specs = deepcopy(self.data.extra_specs_intervals_set)
ref_extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f ref_extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f
with mock.patch.object(self.rest, 'is_vol_in_rep_session',
side_effect=[(False, False, None),
(False, True, None)]):
self.common.extend_volume(volume, new_size) self.common.extend_volume(volume, new_size)
mock_extend.assert_called_once_with( mck_extend.assert_called_once_with(
array, device_id, new_size, ref_extra_specs) array, device_id, new_size, ref_extra_specs, None)
# Success, with snapshot, on new VMAX array
mock_extend.reset_mock() @mock.patch.object(provision.PowerMaxProvision, 'extend_volume')
@mock.patch.object(common.PowerMaxCommon, 'get_rdf_details',
return_value=(10, None))
@mock.patch.object(common.PowerMaxCommon, '_array_ode_capabilities_check',
return_value=[True] * 4)
@mock.patch.object(common.PowerMaxCommon, '_extend_vol_validation_checks')
def test_extend_vol_rep_success(self, mck_val_chk, mck_ode_chk,
mck_get_rdf, mck_extend):
volume = self.data.test_volume
array = self.data.array
device_id = self.data.device_id
new_size = self.data.test_volume.size
ref_extra_specs = deepcopy(self.data.rep_extra_specs_ode)
with mock.patch.object(self.common, '_initial_setup',
return_value=self.data.rep_extra_specs_ode):
self.common.next_gen = True
self.common.rep_config = deepcopy(ref_extra_specs)
self.common.extend_volume(volume, new_size) self.common.extend_volume(volume, new_size)
mock_extend.assert_called_once_with( mck_extend.assert_called_with(
array, device_id, new_size, ref_extra_specs) array, device_id, new_size, ref_extra_specs, 10)
def test_extend_volume_failed_snap_src(self): def test_extend_volume_failed_snap_src(self):
volume = self.data.test_volume volume = self.data.test_volume
@ -2647,3 +2660,124 @@ class PowerMaxCommonTest(test.TestCase):
def test_retest_primary_u4p(self, mock_primary_u4p, mock_request): def test_retest_primary_u4p(self, mock_primary_u4p, mock_request):
self.common.retest_primary_u4p() self.common.retest_primary_u4p()
self.assertFalse(self.rest.u4p_in_failover) self.assertFalse(self.rest.u4p_in_failover)
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(None, False, None))
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
def test_extend_vol_validation_checks_success(self, mck_sync, mck_rep):
volume = self.data.test_volume
array = self.data.array
device_id = self.data.device_id
new_size = self.data.test_volume.size + 1
extra_specs = deepcopy(self.data.extra_specs)
self.common._extend_vol_validation_checks(
array, device_id, volume.name, extra_specs, volume.size, new_size)
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(None, False, None))
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
def test_extend_vol_val_check_no_device(self, mck_sync, mck_rep):
volume = self.data.test_volume
array = self.data.array
device_id = None
new_size = self.data.test_volume.size + 1
extra_specs = deepcopy(self.data.extra_specs)
self.assertRaises(
exception.VolumeBackendAPIException,
self.common._extend_vol_validation_checks,
array, device_id, volume.name, extra_specs, volume.size, new_size)
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(None, True, None))
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
def test_extend_vol_val_check_snap_src(self, mck_sync, mck_rep):
volume = self.data.test_volume
array = self.data.array
device_id = self.data.device_id
new_size = self.data.test_volume.size + 1
extra_specs = deepcopy(self.data.extra_specs)
self.common.next_gen = False
self.assertRaises(
exception.VolumeBackendAPIException,
self.common._extend_vol_validation_checks,
array, device_id, volume.name, extra_specs, volume.size, new_size)
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(None, False, None))
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
def test_extend_vol_val_check_wrong_size(self, mck_sync, mck_rep):
volume = self.data.test_volume
array = self.data.array
device_id = self.data.device_id
new_size = volume.size - 1
extra_specs = deepcopy(self.data.extra_specs)
self.assertRaises(
exception.VolumeBackendAPIException,
self.common._extend_vol_validation_checks,
array, device_id, volume.name, extra_specs, volume.size, new_size)
@mock.patch.object(rest.PowerMaxRest, 'is_next_gen_array',
return_value=True)
@mock.patch.object(
rest.PowerMaxRest, 'get_array_ucode_version',
return_value=tpd.PowerMaxData.powermax_model_details['ucode'])
@mock.patch.object(common.PowerMaxCommon, 'get_rdf_details',
return_value=(10, tpd.PowerMaxData.remote_array))
def test_array_ode_capabilities_check(self, mck_rdf, mck_ucode, mck_gen):
array = self.data.powermax_model_details['symmetrixId']
self.common.ucode_level = self.data.powermax_model_details['ucode']
self.common.next_gen = True
r1, r1_ode, r2, r2_ode = self.common._array_ode_capabilities_check(
array, True)
self.assertTrue(r1)
self.assertTrue(r1_ode)
self.assertTrue(r2)
self.assertTrue(r2_ode)
@mock.patch.object(common.PowerMaxCommon,
'_add_new_volume_to_volume_group')
@mock.patch.object(common.PowerMaxCommon, 'setup_volume_replication')
@mock.patch.object(provision.PowerMaxProvision, 'extend_volume')
@mock.patch.object(rest.PowerMaxRest, 'get_size_of_device_on_array',
return_value=tpd.PowerMaxData.test_volume.size)
@mock.patch.object(provision.PowerMaxProvision, 'break_rdf_relationship')
@mock.patch.object(masking.PowerMaxMasking, 'remove_and_reset_members')
@mock.patch.object(
common.PowerMaxCommon, '_get_replication_extra_specs',
return_value=tpd.PowerMaxData.rep_extra_specs)
@mock.patch.object(
common.PowerMaxCommon, 'get_remote_target_device',
return_value=(
tpd.PowerMaxData.device_id2, tpd.PowerMaxData.remote_array,
tpd.PowerMaxData.rdf_group_vol_details['localRdfGroupNumber'],
tpd.PowerMaxData.rdf_group_vol_details['localVolumeState'],
tpd.PowerMaxData.rdf_group_vol_details['rdfpairState']))
def test_extend_legacy_replicated_vol(self, mck_get_tgt, mck_rdf_specs,
mck_reset, mck_break_rdf, mck_size,
mck_extend, mck_set_rep, mck_add):
volume = self.data.test_volume_group_member
array = self.data.array
device_id = self.data.device_id
new_size = volume.size + 1
extra_specs = deepcopy(self.data.extra_specs)
self.common._extend_legacy_replicated_vol(
array, volume, device_id, volume.name, new_size, extra_specs)
@mock.patch.object(
common.PowerMaxCommon, 'get_remote_target_device',
return_value=(None, None, None, None, None))
def test_extend_legacy_replicated_vol_fail(self, mck_get_tgt):
volume = self.data.test_volume_group_member
array = self.data.array
device_id = self.data.device_id
new_size = volume.size + 1
extra_specs = deepcopy(self.data.extra_specs)
self.assertRaises(
exception.VolumeBackendAPIException,
self.common._extend_vol_validation_checks,
array, device_id, volume.name, extra_specs, volume.size, new_size)

View File

@ -260,6 +260,7 @@ class PowerMaxProvisionTest(test.TestCase):
device_id = self.data.device_id device_id = self.data.device_id
new_size = '3' new_size = '3'
extra_specs = self.data.extra_specs extra_specs = self.data.extra_specs
rdfg_num = self.data.rdf_group_no
with mock.patch.object(self.provision.rest, 'extend_volume' with mock.patch.object(self.provision.rest, 'extend_volume'
) as mock_ex: ) as mock_ex:
self.provision.extend_volume(array, device_id, new_size, self.provision.extend_volume(array, device_id, new_size,
@ -269,9 +270,9 @@ class PowerMaxProvisionTest(test.TestCase):
mock_ex.reset_mock() mock_ex.reset_mock()
# Pass in rdf group # Pass in rdf group
self.provision.extend_volume(array, device_id, new_size, self.provision.extend_volume(array, device_id, new_size,
extra_specs, self.data.rdf_group_no) extra_specs, rdfg_num)
mock_ex.assert_called_once_with( mock_ex.assert_called_once_with(
array, device_id, new_size, extra_specs) array, device_id, new_size, extra_specs, rdfg_num)
def test_get_srp_pool_stats(self): def test_get_srp_pool_stats(self):
array = self.data.array array = self.data.array

View File

@ -322,22 +322,6 @@ class PowerMaxReplicationTest(test.TestCase):
self.data.extra_specs, rep_extra_specs) self.data.extra_specs, rep_extra_specs)
mock_pre.assert_called_once() mock_pre.assert_called_once()
@mock.patch.object(rest.PowerMaxRest, 'is_vol_in_rep_session',
return_value=(False, False, None))
@mock.patch.object(common.PowerMaxCommon, 'extend_volume_is_replicated')
@mock.patch.object(common.PowerMaxCommon, '_sync_check')
@mock.patch.object(rest.PowerMaxRest, 'get_array_model_info',
return_value=('VMAX250F', False))
def test_extend_volume_rep_enabled(self, mock_model, mock_sync,
mock_ex_re, mock_is_re):
extra_specs = deepcopy(self.extra_specs)
extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f
volume_name = self.data.test_volume.name
self.common.extend_volume(self.data.test_volume, '5')
mock_ex_re.assert_called_once_with(
self.data.array, self.data.test_volume,
self.data.device_id, volume_name, '5', extra_specs)
def test_set_config_file_get_extra_specs_rep_enabled(self): def test_set_config_file_get_extra_specs_rep_enabled(self):
extra_specs, _ = self.common._set_config_file_and_get_extra_specs( extra_specs, _ = self.common._set_config_file_and_get_extra_specs(
self.data.test_volume) self.data.test_volume)
@ -607,43 +591,6 @@ class PowerMaxReplicationTest(test.TestCase):
self.data.device_id)) self.data.device_id))
self.assertIsNone(target_device4) self.assertIsNone(target_device4)
@mock.patch.object(rest.PowerMaxRest, 'get_array_model_info',
return_value=('PowerMax 2000', True))
@mock.patch.object(common.PowerMaxCommon, 'setup_volume_replication')
@mock.patch.object(provision.PowerMaxProvision, 'extend_volume')
@mock.patch.object(provision.PowerMaxProvision, 'break_rdf_relationship')
@mock.patch.object(masking.PowerMaxMasking, 'remove_and_reset_members')
def test_extend_volume_is_replicated(self, mock_remove, mock_break,
mock_extend, mock_setup, mock_model):
self.common.extend_volume_is_replicated(
self.data.array, self.data.test_volume, self.data.device_id,
'vol1', '5', self.data.extra_specs_rep_enabled)
self.assertEqual(2, mock_remove.call_count)
self.assertEqual(2, mock_extend.call_count)
mock_remove.reset_mock()
mock_extend.reset_mock()
with mock.patch.object(self.rest, 'is_next_gen_array',
return_value=True):
self.common.extend_volume_is_replicated(
self.data.array, self.data.test_volume, self.data.device_id,
'vol1', '5', self.data.extra_specs_rep_enabled)
mock_remove.assert_not_called()
self.assertEqual(2, mock_extend.call_count)
def test_extend_volume_is_replicated_exception(self):
self.assertRaises(exception.VolumeBackendAPIException,
self.common.extend_volume_is_replicated,
self.data.failed_resource, self.data.test_volume,
self.data.device_id, 'vol1', '1',
self.data.extra_specs_rep_enabled)
with mock.patch.object(self.utils, 'is_metro_device',
return_value=True):
self.assertRaises(exception.VolumeBackendAPIException,
self.common.extend_volume_is_replicated,
self.data.array, self.data.test_volume,
self.data.device_id, 'vol1', '1',
self.data.extra_specs_rep_enabled)
@mock.patch.object(rest.PowerMaxRest, 'get_array_model_info', @mock.patch.object(rest.PowerMaxRest, 'get_array_model_info',
return_value=('VMAX250F', False)) return_value=('VMAX250F', False))
@mock.patch.object(common.PowerMaxCommon, @mock.patch.object(common.PowerMaxCommon,

View File

@ -544,22 +544,31 @@ class PowerMaxRestTest(test.TestCase):
self.rest._modify_volume, self.data.array, self.rest._modify_volume, self.data.array,
device_id, payload) device_id, payload)
def test_extend_volume(self): @mock.patch.object(rest.PowerMaxRest, 'wait_for_job')
def test_extend_volume(self, mck_wait):
array = self.data.array
device_id = self.data.device_id device_id = self.data.device_id
new_size = '3' new_size = '3'
extra_specs = self.data.extra_specs,
rdfg_num = self.data.rdf_group_no
extend_vol_payload = {'executionOption': 'ASYNCHRONOUS', extend_vol_payload = {'executionOption': 'ASYNCHRONOUS',
'editVolumeActionParam': { 'editVolumeActionParam': {
'expandVolumeParam': { 'expandVolumeParam': {
'volumeAttribute': { 'volumeAttribute': {
'volume_size': new_size, 'volume_size': new_size,
'capacityUnit': 'GB'}}}} 'capacityUnit': 'GB'},
'rdfGroupNumber': rdfg_num}}}
with mock.patch.object( with mock.patch.object(
self.rest, '_modify_volume', self.rest, '_modify_volume',
return_value=(202, self.data.job_list[0])) as mock_modify: return_value=(202, self.data.job_list[0])) as mck_modify:
self.rest.extend_volume(self.data.array, device_id, new_size,
self.data.extra_specs) self.rest.extend_volume(array, device_id, new_size, extra_specs,
mock_modify.assert_called_once_with( rdfg_num)
self.data.array, device_id, extend_vol_payload)
mck_modify.assert_called_once_with(array, device_id,
extend_vol_payload)
def test_legacy_delete_volume(self): def test_legacy_delete_volume(self):
device_id = self.data.device_id device_id = self.data.device_id
@ -1731,3 +1740,10 @@ class PowerMaxRestTest(test.TestCase):
self.rest.u4p_failover_targets = [] self.rest.u4p_failover_targets = []
self.assertRaises(exception.VolumeBackendAPIException, self.assertRaises(exception.VolumeBackendAPIException,
self.rest._handle_u4p_failover) self.rest._handle_u4p_failover)
@mock.patch.object(rest.PowerMaxRest, 'get_array_detail',
return_value=tpd.PowerMaxData.powermax_model_details)
def test_get_array_ucode(self, mck_ucode):
array = self.data.array
ucode = self.rest.get_array_ucode_version(array)
self.assertEqual(self.data.powermax_model_details['ucode'], ucode)

View File

@ -169,27 +169,31 @@ class PowerMaxCommon(object):
def __init__(self, prtcl, version, configuration=None, def __init__(self, prtcl, version, configuration=None,
active_backend_id=None): active_backend_id=None):
self.protocol = prtcl
self.configuration = configuration
self.configuration.append_config_values(powermax_opts)
self.rest = rest.PowerMaxRest() self.rest = rest.PowerMaxRest()
self.utils = utils.PowerMaxUtils() self.utils = utils.PowerMaxUtils()
self.masking = masking.PowerMaxMasking(prtcl, self.rest) self.masking = masking.PowerMaxMasking(prtcl, self.rest)
self.provision = provision.PowerMaxProvision(self.rest) self.provision = provision.PowerMaxProvision(self.rest)
self.version = version
self.volume_metadata = volume_metadata.PowerMaxVolumeMetadata( self.volume_metadata = volume_metadata.PowerMaxVolumeMetadata(
self.rest, version, LOG.isEnabledFor(logging.DEBUG)) self.rest, version, LOG.isEnabledFor(logging.DEBUG))
# replication
# Configuration/Attributes
self.protocol = prtcl
self.configuration = configuration
self.configuration.append_config_values(powermax_opts)
self.active_backend_id = active_backend_id
self.version = version
self.version_dict = {}
self.ucode_level = None
self.next_gen = False
self.replication_enabled = False self.replication_enabled = False
self.extend_replicated_vol = False self.extend_replicated_vol = False
self.rep_devices = None self.rep_devices = None
self.active_backend_id = active_backend_id
self.failover = False self.failover = False
# Gather environment info
self._get_replication_info() self._get_replication_info()
self._get_u4p_failover_info() self._get_u4p_failover_info()
self.next_gen = False
self._gather_info() self._gather_info()
self.version_dict = {}
def _gather_info(self): def _gather_info(self):
"""Gather the relevant information for update_volume_stats.""" """Gather the relevant information for update_volume_stats."""
@ -202,7 +206,9 @@ class PowerMaxCommon(object):
"longer supported.") "longer supported.")
self.rest.set_rest_credentials(array_info) self.rest.set_rest_credentials(array_info)
if array_info: if array_info:
self.array_model, self.next_gen = self.rest.get_array_model_info( self.array_model, self.next_gen = (
self.rest.get_array_model_info(array_info['SerialNumber']))
self.ucode_level = self.rest.get_array_ucode_version(
array_info['SerialNumber']) array_info['SerialNumber'])
finalarrayinfolist = self._get_slo_workload_combinations( finalarrayinfolist = self._get_slo_workload_combinations(
array_info) array_info)
@ -949,60 +955,203 @@ class PowerMaxCommon(object):
:param volume: the volume Object :param volume: the volume Object
:param new_size: the new size to increase the volume to :param new_size: the new size to increase the volume to
:returns: dict -- modifiedVolumeDict - the extended volume Object
:raises: VolumeBackendAPIException: :raises: VolumeBackendAPIException:
""" """
original_vol_size = volume.size # Set specific attributes for extend operation
volume_name = volume.name ex_specs = self._initial_setup(volume)
extra_specs = self._initial_setup(volume) array = ex_specs[utils.ARRAY]
array = extra_specs[utils.ARRAY] device_id = self._find_device_on_array(volume, ex_specs)
device_id = self._find_device_on_array(volume, extra_specs) vol_name = volume.name
if device_id is None: orig_vol_size = volume.size
exception_message = (_("Cannot find Volume: %(volume_name)s. " rep_enabled = self.utils.is_replication_enabled(ex_specs)
"Extend operation. Exiting....") rdf_grp_no = None
% {'volume_name': volume_name}) legacy_extend = False
LOG.error(exception_message) metro_exception = False
# Run validation and capabilities checks
self._extend_vol_validation_checks(
array, device_id, vol_name, ex_specs, orig_vol_size, new_size)
r1_ode, r1_ode_metro, r2_ode, r2_ode_metro = (
self._array_ode_capabilities_check(array, rep_enabled))
# Get extend workflow dependent on array gen and replication status
if self.next_gen:
if rep_enabled:
(rdf_grp_no, __) = self.get_rdf_details(array)
if self.utils.is_metro_device(self.rep_config, ex_specs):
if not r1_ode_metro or not r2_ode_metro:
metro_exception = True
elif not self.next_gen and rep_enabled:
if self.utils.is_metro_device(self.rep_config, ex_specs):
metro_exception = True
else:
legacy_extend = True
# If volume to be extended is SRDF Metro enabled and not FoxTail uCode
if metro_exception:
metro_exception_message = (_(
"Extending a replicated volume with SRDF/Metro enabled is not "
"permitted on this backend. Please contact your storage "
"administrator. Note that you cannot extend SRDF/Metro "
"protected volumes unless running FoxTail PowerMax OS uCode "
"level."))
LOG.error(metro_exception_message)
raise exception.VolumeBackendAPIException( raise exception.VolumeBackendAPIException(
message=exception_message) message=metro_exception_message)
# Check if volume is part of an on-going clone operation
self._sync_check(array, device_id, extra_specs) # Handle the extend process using workflow info from previous steps
__, snapvx_src, __ = self.rest.is_vol_in_rep_session(array, device_id) if legacy_extend:
if snapvx_src: LOG.info("Legacy extend volume %(volume)s to %(new_size)d GBs",
if not self.rest.is_next_gen_array(array): {'volume': vol_name,
exception_message = ( 'new_size': int(new_size)})
_("The volume: %(volume)s is a snapshot source. " self._extend_legacy_replicated_vol(
"Extending a volume with snapVx snapshots is only " array, volume, device_id, vol_name, new_size, ex_specs)
"supported on PowerMax/VMAX from HyperMaxOS version " else:
"5978 onwards. Exiting...") % {'volume': volume_name}) LOG.info("ODE extend volume %(volume)s to %(new_size)d GBs",
{'volume': vol_name,
'new_size': int(new_size)})
self.provision.extend_volume(
array, device_id, new_size, ex_specs, rdf_grp_no)
LOG.debug("Leaving extend_volume: %(volume_name)s. ",
{'volume_name': vol_name})
def _extend_vol_validation_checks(self, array, device_id, vol_name,
ex_specs, orig_vol_size, new_size):
"""Run validation checks on settings for extend volume operation.
:param array: the array serial number
:param device_id: the device id
:param vol_name: the volume name
:param ex_specs: extra specifications
:param orig_vol_size: the original volume size
:param new_size: the new size the volume should be
:raises: VolumeBackendAPIException:
"""
# 1 - Check device exists
if device_id is None:
exception_message = (_(
"Cannot find Volume: %(volume_name)s. Extend operation. "
"Exiting....") % {'volume_name': vol_name})
LOG.error(exception_message) LOG.error(exception_message)
raise exception.VolumeBackendAPIException( raise exception.VolumeBackendAPIException(
message=exception_message) message=exception_message)
if int(original_vol_size) > int(new_size): # 2 - Check if volume is part of an on-going clone operation or if vol
# has source snapshots but not next-gen array
self._sync_check(array, device_id, ex_specs)
__, snapvx_src, __ = self.rest.is_vol_in_rep_session(array, device_id)
if snapvx_src:
if not self.next_gen:
exception_message = (
_("The volume: %(volume)s is a snapshot source. "
"Extending a volume with snapVx snapshots is only "
"supported on PowerMax/VMAX from OS version 5978 "
"onwards. Exiting...") % {'volume': vol_name})
LOG.error(exception_message)
raise exception.VolumeBackendAPIException(
message=exception_message)
# 3 - Check new size is larger than old size
if int(orig_vol_size) >= int(new_size):
exception_message = (_( exception_message = (_(
"Your original size: %(original_vol_size)s GB is greater " "Your original size: %(orig_vol_size)s GB is greater "
"than: %(new_size)s GB. Only Extend is supported. Exiting...") "than or the same as: %(new_size)s GB. Only extend ops are "
% {'original_vol_size': original_vol_size, "supported. Exiting...") % {'orig_vol_size': orig_vol_size,
'new_size': new_size}) 'new_size': new_size})
LOG.error(exception_message) LOG.error(exception_message)
raise exception.VolumeBackendAPIException( raise exception.VolumeBackendAPIException(
message=exception_message) message=exception_message)
LOG.info("Extending volume %(volume)s to %(new_size)d GBs",
{'volume': volume_name,
'new_size': int(new_size)})
if self.utils.is_replication_enabled(extra_specs):
# Extra logic required if volume is replicated
self.extend_volume_is_replicated(
array, volume, device_id, volume_name, new_size, extra_specs)
else:
self.provision.extend_volume(
array, device_id, new_size, extra_specs)
self.volume_metadata.capture_extend_info( def _array_ode_capabilities_check(self, array, rep_enabled=False):
volume, new_size, device_id, extra_specs, array) """Given an array, check Online Device Expansion (ODE) support.
LOG.debug("Leaving extend_volume: %(volume_name)s. ", :param array: the array serial number
{'volume_name': volume_name}) :param rep_enabled: if replication is enabled for backend
:returns: r1_ode: (bool) If R1 array supports ODE
:returns: r1_ode_metro: (bool) If R1 array supports ODE with Metro vols
:returns: r2_ode: (bool) If R1 array supports ODE
:returns: r2_ode_metro: (bool) If R1 array supports ODE with Metro vols
"""
r1_ucode = self.ucode_level.split('.')
r1_ode, r1_ode_metro = False, False
r2_ode, r2_ode_metro = False, False
if self.next_gen:
r1_ode = True
if rep_enabled:
__, r2_array = self.get_rdf_details(array)
r2_ucode = self.rest.get_array_ucode_version(array)
if int(r1_ucode[2]) > utils.UCODE_5978_ELMSR:
r1_ode_metro = True
r2_ucode = r2_ucode.split('.')
if self.rest.is_next_gen_array(r2_array):
r2_ode = True
if int(r2_ucode[2]) > utils.UCODE_5978_ELMSR:
r2_ode_metro = True
return r1_ode, r1_ode_metro, r2_ode, r2_ode_metro
def _extend_legacy_replicated_vol(self, array, volume, device_id,
volume_name, new_size, extra_specs):
"""Extend a legacy OS volume without Online Device Expansion
:param array: the array serial number
:param volume: the volume objcet
:param device_id: the volume device id
:param volume_name: the volume name
:param new_size: the new size the volume should be
:param extra_specs: extra specifications
"""
try:
(target_device, remote_array, rdf_group, local_vol_state,
pair_state) = self.get_remote_target_device(
array, volume, device_id)
rep_extra_specs = self._get_replication_extra_specs(
extra_specs, self.rep_config)
# Volume must be removed from replication (storage) group before
# the replication relationship can be ended (cannot have a mix of
# replicated and non-replicated volumes as the SRDF groups become
# unmanageable)
self.masking.remove_and_reset_members(
array, volume, device_id, volume_name, extra_specs, False)
# Repeat on target side
self.masking.remove_and_reset_members(
remote_array, volume, target_device, volume_name,
rep_extra_specs, False)
LOG.info("Breaking replication relationship...")
self.provision.break_rdf_relationship(array, device_id,
target_device, rdf_group,
rep_extra_specs, pair_state)
# Extend the target volume
LOG.info("Extending target volume...")
# Check to make sure the R2 device requires extending first...
r2_size = self.rest.get_size_of_device_on_array(remote_array,
target_device)
if int(r2_size) < int(new_size):
self.provision.extend_volume(remote_array, target_device,
new_size, rep_extra_specs)
# Extend the source volume
LOG.info("Extending source volume...")
self.provision.extend_volume(array, device_id, new_size,
extra_specs)
# Re-create replication relationship
LOG.info("Recreating replication relationship...")
self.setup_volume_replication(array, volume, device_id,
extra_specs, target_device)
# Check if volume needs to be returned to volume group
if volume.group_id:
self._add_new_volume_to_volume_group(
volume, device_id, volume_name, extra_specs)
except Exception as e:
exception_message = (_("Error extending volume. Error received "
"was %(e)s") % {'e': e})
LOG.error(exception_message)
raise exception.VolumeBackendAPIException(
message=exception_message)
def update_volume_stats(self): def update_volume_stats(self):
"""Retrieve stats info.""" """Retrieve stats info."""
@ -3496,8 +3645,8 @@ class PowerMaxCommon(object):
self.provision.get_or_create_group( self.provision.get_or_create_group(
remote_array, group_name, extra_specs) remote_array, group_name, extra_specs)
self.masking.add_volume_to_storage_group( self.masking.add_volume_to_storage_group(
remote_array, target_device_id, remote_array, target_device_id, group_name, volume_name,
group_name, volume_name, extra_specs) extra_specs, force=True)
except Exception as e: except Exception as e:
exception_message = ( exception_message = (
_('Exception occurred adding volume %(vol)s to its async ' _('Exception occurred adding volume %(vol)s to its async '
@ -3890,120 +4039,6 @@ class PowerMaxCommon(object):
return (target_device, remote_array, rdf_group, return (target_device, remote_array, rdf_group,
local_vol_state, pair_state) local_vol_state, pair_state)
def extend_volume_is_replicated(
self, array, volume, device_id, volume_name,
new_size, extra_specs):
"""Extend a replication-enabled volume.
Cannot extend volumes in a synchronization pair where the source
and/or target arrays are running HyperMax versions < 5978. Must first
break the relationship, extend them separately, then recreate the
pair. Extending Metro protected volumes is not supported.
:param array: the array serial number
:param volume: the volume objcet
:param device_id: the volume device id
:param volume_name: the volume name
:param new_size: the new size the volume should be
:param extra_specs: extra specifications
"""
ode_replication, allow_extend = False, self.extend_replicated_vol
if (self.rest.is_next_gen_array(array)
and not self.utils.is_metro_device(
self.rep_config, extra_specs)):
# Check if remote array is next gen
__, remote_array = self.get_rdf_details(array)
if self.rest.is_next_gen_array(remote_array):
ode_replication = True
if self.utils.is_metro_device(self.rep_config, extra_specs):
allow_extend = False
if allow_extend is True or ode_replication is True:
self._extend_with_or_without_ode_replication(
array, volume, device_id, ode_replication, volume_name,
new_size, extra_specs)
else:
exception_message = (_(
"Extending a replicated volume is not permitted on this "
"backend. Please contact your administrator. Note that "
"you cannot extend SRDF/Metro protected volumes."))
LOG.error(exception_message)
raise exception.VolumeBackendAPIException(
message=exception_message)
def _extend_with_or_without_ode_replication(
self, array, volume, device_id, ode_replication, volume_name,
new_size, extra_specs):
"""Extend a volume with or without Online Device Expansion
:param array: the array serial number
:param volume: the volume objcet
:param device_id: the volume device id
:param ode_replication: Online device expansion
:param volume_name: the volume name
:param new_size: the new size the volume should be
:param extra_specs: extra specifications
"""
try:
(target_device, remote_array, rdf_group,
local_vol_state, pair_state) = (
self.get_remote_target_device(
array, volume, device_id))
rep_extra_specs = self._get_replication_extra_specs(
extra_specs, self.rep_config)
lock_rdf_group = rdf_group
if not ode_replication:
# Volume must be removed from replication (storage) group
# before the replication relationship can be ended (cannot
# have a mix of replicated and non-replicated volumes as
# the SRDF groups become unmanageable)
lock_rdf_group = None
self.masking.remove_and_reset_members(
array, volume, device_id, volume_name,
extra_specs, False)
# Repeat on target side
self.masking.remove_and_reset_members(
remote_array, volume, target_device, volume_name,
rep_extra_specs, False)
LOG.info("Breaking replication relationship...")
self.provision.break_rdf_relationship(
array, device_id, target_device, rdf_group,
rep_extra_specs, pair_state)
# Extend the target volume
LOG.info("Extending target volume...")
# Check to make sure the R2 device requires extending first...
r2_size = self.rest.get_size_of_device_on_array(
remote_array, target_device)
if int(r2_size) < int(new_size):
self.provision.extend_volume(
remote_array, target_device, new_size,
rep_extra_specs, lock_rdf_group)
# Extend the source volume
LOG.info("Extending source volume...")
self.provision.extend_volume(
array, device_id, new_size, extra_specs, lock_rdf_group)
if not ode_replication:
# Re-create replication relationship
LOG.info("Recreating replication relationship...")
self.setup_volume_replication(
array, volume, device_id, extra_specs, target_device)
# Check if volume needs to be returned to volume group
if volume.group_id:
self._add_new_volume_to_volume_group(
volume, device_id, volume_name, extra_specs)
except Exception as e:
exception_message = (_("Error extending volume. "
"Error received was %(e)s") %
{'e': e})
LOG.error(exception_message)
raise exception.VolumeBackendAPIException(
message=exception_message)
def enable_rdf(self, array, volume, device_id, rdf_group_no, rep_config, def enable_rdf(self, array, volume, device_id, rdf_group_no, rep_config,
target_name, remote_array, target_device, extra_specs): target_name, remote_array, target_device, extra_specs):
"""Create a replication relationship with a target volume. """Create a replication relationship with a target volume.

View File

@ -111,6 +111,7 @@ class PowerMaxFCDriver(san.SanDriver, driver.FibreChannelDriver):
4.1.0 - Changing from 90 to 91 rest endpoints 4.1.0 - Changing from 90 to 91 rest endpoints
- Support for Rapid TDEV Delete (bp powermax-tdev-deallocation) - Support for Rapid TDEV Delete (bp powermax-tdev-deallocation)
- PowerMax OS Metro formatted volumes fix (bug #1829876) - PowerMax OS Metro formatted volumes fix (bug #1829876)
- Support for Metro ODE (bp/powermax-metro-ode)
""" """
VERSION = "4.1.0" VERSION = "4.1.0"

View File

@ -116,6 +116,7 @@ class PowerMaxISCSIDriver(san.SanISCSIDriver):
4.1.0 - Changing from 90 to 91 rest endpoints 4.1.0 - Changing from 90 to 91 rest endpoints
- Support for Rapid TDEV Delete (bp powermax-tdev-deallocation) - Support for Rapid TDEV Delete (bp powermax-tdev-deallocation)
- PowerMax OS Metro formatted volumes fix (bug #1829876) - PowerMax OS Metro formatted volumes fix (bug #1829876)
- Support for Metro ODE (bp/powermax-metro-ode)
""" """
VERSION = "4.1.0" VERSION = "4.1.0"

View File

@ -401,7 +401,7 @@ class PowerMaxProvision(object):
@coordination.synchronized('emc-rg-{rdf_group}') @coordination.synchronized('emc-rg-{rdf_group}')
def _extend_replicated_volume(rdf_group): def _extend_replicated_volume(rdf_group):
self.rest.extend_volume(array, device_id, self.rest.extend_volume(array, device_id,
new_size, extra_specs) new_size, extra_specs, rdf_group)
_extend_replicated_volume(rdf_group) _extend_replicated_volume(rdf_group)
else: else:
self.rest.extend_volume(array, device_id, new_size, extra_specs) self.rest.extend_volume(array, device_id, new_size, extra_specs)

View File

@ -671,6 +671,18 @@ class PowerMaxRest(object):
is_next_gen = True is_next_gen = True
return array_model, is_next_gen return array_model, is_next_gen
def get_array_ucode_version(self, array):
"""Get the PowerMax/VMAX uCode version.
:param array: the array serial number
:return: the PowerMax/VMAX uCode version
"""
ucode_version = None
system_info = self.get_array_detail(array)
if system_info:
ucode_version = system_info['ucode']
return ucode_version
def is_compression_capable(self, array): def is_compression_capable(self, array):
"""Check if array is compression capable. """Check if array is compression capable.
@ -1239,20 +1251,26 @@ class PowerMaxRest(object):
return self.modify_resource(array, SLOPROVISIONING, 'volume', return self.modify_resource(array, SLOPROVISIONING, 'volume',
payload, resource_name=device_id) payload, resource_name=device_id)
def extend_volume(self, array, device_id, new_size, extra_specs): def extend_volume(self, array, device_id, new_size, extra_specs,
rdf_grp_no=None):
"""Extend a PowerMax/VMAX volume. """Extend a PowerMax/VMAX volume.
:param array: the array serial number :param array: the array serial number
:param device_id: volume device id :param device_id: volume device id
:param new_size: the new required size for the device :param new_size: the new required size for the device
:param extra_specs: the extra specifications :param extra_specs: the extra specifications
:param rdf_grp_no: the RDG group number
""" """
extend_vol_payload = {"executionOption": "ASYNCHRONOUS", extend_vol_payload = {'executionOption': 'ASYNCHRONOUS',
"editVolumeActionParam": { 'editVolumeActionParam': {
"expandVolumeParam": { 'expandVolumeParam': {
"volumeAttribute": { 'volumeAttribute': {
"volume_size": new_size, 'volume_size': new_size,
"capacityUnit": "GB"}}}} 'capacityUnit': 'GB'}}}}
if rdf_grp_no:
extend_vol_payload['editVolumeActionParam'][
'expandVolumeParam'].update({'rdfGroupNumber': rdf_grp_no})
status_code, job = self._modify_volume( status_code, job = self._modify_volume(
array, device_id, extend_vol_payload) array, device_id, extend_vol_payload)
@ -2320,7 +2338,6 @@ class PowerMaxRest(object):
extra_specs[utils.RDF_CONS_EXEMPT] = False extra_specs[utils.RDF_CONS_EXEMPT] = False
payload = self.get_metro_payload_info( payload = self.get_metro_payload_info(
array, payload, rdf_group_no, extra_specs) array, payload, rdf_group_no, extra_specs)
resource_type = ("rdf_group/%(rdf_num)s/volume" resource_type = ("rdf_group/%(rdf_num)s/volume"
% {'rdf_num': rdf_group_no}) % {'rdf_num': rdf_group_no})
status_code, job = self.create_resource(array, REPLICATION, status_code, job = self.create_resource(array, REPLICATION,

View File

@ -0,0 +1,5 @@
---
features:
- |
PowerMax for Cinder driver now supports extending in-use Metro RDF enabled
volumes.