HPE 3par: Fix issue seen during retype/migrate

1] If migration_status is deleting,
incorrect volume name on 3par (osv_name) is fetched.

2] For retyped/migrated volume, Remote Copy Group name
(rcg_name) is different.

3] For retyped/migrated volume having QOS setting,
vvset_name is different.

This patch adds necessary logic to get appropriate osv_name,
rcg_name and vvset_name (for above scenarios).

Closes-Bug: #2026718
Change-Id: Ifa78eb42b0fa7a4446e5e300fcb13567eac04403
This commit is contained in:
raghavendrat 2023-07-07 19:41:25 +00:00
parent 442c538ec7
commit 74c5ec0ab1
3 changed files with 284 additions and 40 deletions

View File

@ -199,15 +199,18 @@ class HPE3PARBaseDriver(test.TestCase):
'volume_type': None,
'volume_type_id': None}
volume_replicated = {'name': VOLUME_NAME,
'id': VOLUME_ID,
'display_name': 'Foo Volume',
'replication_status': 'disabled',
'provider_location': CLIENT_ID,
'size': 2,
'host': FAKE_CINDER_HOST,
'volume_type': 'replicated',
'volume_type_id': VOLUME_TYPE_ID_REPLICATED}
volume_replicated = fake_volume.fake_volume_obj(
context.get_admin_context(),
name=VOLUME_NAME,
id=VOLUME_ID,
display_name='Foo Volume',
replication_status='disabled',
provider_location = CLIENT_ID,
size=2,
host=FAKE_CINDER_HOST,
volume_type = 'replicated',
volume_type_id = VOLUME_TYPE_ID_REPLICATED,
migration_status = None)
volume_tiramisu = {'name': VOLUME_NAME,
'id': VOLUME_ID,
@ -1257,7 +1260,8 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
volume = self.volume_replicated.copy()
volume = copy.deepcopy(self.volume_replicated)
volume['status'] = 'available'
volume['replication_status'] = 'failed-over'
self.driver.delete_volume(volume)
@ -2092,8 +2096,15 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
mock.call.startRemoteCopy(self.RCG_3PAR_NAME)]
mock_client.assert_has_calls(expected + self.standard_logout)
# volume's status and migration_status
# (i) default scenario
# (ii) deleting temporary volume
@ddt.data({'status': 'available', 'migration_status': 'migrating'},
{'status': 'deleting', 'migration_status': 'deleting'})
@ddt.unpack
@mock.patch.object(volume_types, 'get_volume_type')
def test_retype_rep_type_to_non_rep_type(self, _mock_volume_types):
def test_retype_rep_type_to_non_rep_type(self, _mock_volume_types,
status, migration_status):
conf = self.setup_configuration()
self.replication_targets[0]['replication_mode'] = 'periodic'
@ -2120,7 +2131,9 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
'size': 2,
'host': self.FAKE_CINDER_HOST,
'volume_type': 'replicated',
'volume_type_id': 'gold'}
'volume_type_id': 'gold',
'status': status,
'migration_status': migration_status}
volume_type = {'name': 'replicated',
'deleted': False,
@ -2215,7 +2228,9 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
'size': 2,
'host': self.FAKE_CINDER_HOST,
'volume_type': 'replicated',
'volume_type_id': 'gold'}
'volume_type_id': 'gold',
'status': 'available',
'migration_status': 'migrating'}
volume_type = {'name': 'replicated',
'deleted': False,
@ -2427,6 +2442,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
mock_client.assert_has_calls(expected)
# Default scenario: vvset name is similar to volume name
@mock.patch.object(volume_types, 'get_volume_type')
def test_delete_volume_replicated(self, _mock_volume_types):
# setup_mock_client drive with default configuration
@ -2436,6 +2452,55 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
ex = hpeexceptions.HTTPConflict("In use")
ex._error_code = 34
mock_client.deleteVolume = mock.Mock(side_effect=[ex, 200])
mock_client.getVolumeSet.return_value = 'vvs-0DM4qZEVSKON-DXN-NwVpw'
_mock_volume_types.return_value = {
'name': 'replicated',
'extra_specs': {
'cpg': HPE3PAR_CPG_QOS,
'snap_cpg': HPE3PAR_CPG_SNAP,
'vvs_name': self.VVS_NAME,
'qos': self.QOS,
'replication_enabled': '<is> True',
'replication:mode': 'periodic',
'replication:sync_period': '900',
'volume_type': self.volume_type_replicated}}
VVS_NAME = self.VOLUME_3PAR_NAME.replace('osv-', 'vvs-')
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
volume = copy.deepcopy(self.volume_replicated)
volume['status'] = 'available'
self.driver.delete_volume(volume)
expected = [
mock.call.stopRemoteCopy(self.RCG_3PAR_NAME),
mock.call.removeVolumeFromRemoteCopyGroup(
self.RCG_3PAR_NAME,
self.VOLUME_3PAR_NAME,
removeFromTarget=True),
mock.call.removeRemoteCopyGroup(self.RCG_3PAR_NAME),
mock.call.deleteVolume(self.VOLUME_3PAR_NAME),
mock.call.getVolumeSet(VVS_NAME),
mock.call.deleteVolumeSet(VVS_NAME),
mock.call.deleteVolume(self.VOLUME_3PAR_NAME)]
mock_client.assert_has_calls(expected)
# Second scenario: vvset name is altogether different from volume name
@mock.patch.object(volume_types, 'get_volume_type')
def test_delete_volume_repl_different_vvset(self, _mock_volume_types):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
mock_client = self.setup_driver()
mock_client.getStorageSystemInfo.return_value = {'id': self.CLIENT_ID}
ex = hpeexceptions.HTTPConflict("In use")
ex._error_code = 34
mock_client.deleteVolume = mock.Mock(side_effect=[ex, 200])
ex_not_found = hpeexceptions.HTTPNotFound("Set does not exist")
ex_not_found._error_code = 102
mock_client.getVolumeSet = mock.Mock(side_effect=[ex_not_found, 404])
mock_client.findVolumeSet.return_value = self.VVS_NAME
_mock_volume_types.return_value = {
'name': 'replicated',
@ -2448,10 +2513,13 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
'replication:mode': 'periodic',
'replication:sync_period': '900',
'volume_type': self.volume_type_replicated}}
vvs_name_similar = self.VOLUME_3PAR_NAME.replace('osv-', 'vvs-')
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
self.driver.delete_volume(self.volume_replicated)
volume = copy.deepcopy(self.volume_replicated)
volume['status'] = 'available'
self.driver.delete_volume(volume)
expected = [
mock.call.stopRemoteCopy(self.RCG_3PAR_NAME),
@ -2461,6 +2529,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
removeFromTarget=True),
mock.call.removeRemoteCopyGroup(self.RCG_3PAR_NAME),
mock.call.deleteVolume(self.VOLUME_3PAR_NAME),
mock.call.getVolumeSet(vvs_name_similar),
mock.call.findVolumeSet(self.VOLUME_3PAR_NAME),
mock.call.removeVolumeFromVolumeSet(self.VVS_NAME,
self.VOLUME_3PAR_NAME),
@ -2468,6 +2537,45 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
mock_client.assert_has_calls(expected)
@mock.patch.object(volume_types, 'get_volume_type')
def test_delete_volume_replicated_migrated(self, _mock_volume_types):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
mock_client = self.setup_driver()
mock_client.getStorageSystemInfo.return_value = {'id': self.CLIENT_ID}
mock_client.getVolume.return_value = {'rcopyGroup':
'rcg-CArwlBBhRqq3K-eLUh'}
_mock_volume_types.return_value = {
'name': 'replicated',
'extra_specs': {
'cpg': HPE3PAR_CPG,
'snap_cpg': HPE3PAR_CPG_SNAP,
'replication_enabled': '<is> True',
'replication:mode': 'periodic',
'replication:sync_period': '900',
'volume_type': self.volume_type_replicated}}
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
volume = copy.deepcopy(self.volume_replicated)
volume['status'] = 'available'
volume['migration_status'] = 'success'
self.driver.delete_volume(volume)
rcg_name_updated = 'rcg-CArwlBBhRqq3K-eLUh'
expected = [
mock.call.getVolume(self.VOLUME_3PAR_NAME),
mock.call.stopRemoteCopy(rcg_name_updated),
mock.call.removeVolumeFromRemoteCopyGroup(
rcg_name_updated,
self.VOLUME_3PAR_NAME,
removeFromTarget=True),
mock.call.removeRemoteCopyGroup(rcg_name_updated),
mock.call.deleteVolume(self.VOLUME_3PAR_NAME)]
mock_client.assert_has_calls(expected)
def test_get_cpg_with_volume_return_usercpg(self):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
@ -3378,6 +3486,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
@ddt.unpack
def test_revert_to_snapshot(self, snapshot_attr, vol_name):
snapshot = getattr(self, snapshot_attr)
snapshot['volume']['migration_status'] = None
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
mock_client = self.setup_driver()
@ -5895,6 +6004,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
group.replication_status = fields.ReplicationStatus.ENABLED
group.volume_types = [self.volume_type_tiramisu]
volume['group'] = group
volume['migration_status'] = None
self.driver.revert_to_snapshot(
self.ctxt,
@ -6746,7 +6856,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
mock_replication_client.return_value = mock_replicated_client
# Test a successful fail-back.
volume = self.volume_replicated.copy()
volume = copy.deepcopy(self.volume_replicated)
volume['replication_status'] = 'failed-over'
return_model = self.driver.failover_host(
context.get_admin_context(),
@ -6797,7 +6907,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
mock_replication_client.return_value = mock_replicated_client
# Test an unsuccessful fail-back.
volume = self.volume_replicated.copy()
volume = copy.deepcopy(self.volume_replicated)
volume['replication_status'] = 'failed-over'
self.assertRaises(
@ -6905,6 +7015,51 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
if 'HP:3PAR' in new_key:
self.assertIn(new_key, properties)
@mock.patch.object(volume_types, 'get_volume_type')
def test_rename_migrated_vvset(self, _mock_volume_types):
# Setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
mock_client = self.setup_driver()
mock_client.getStorageSystemInfo.return_value = {'id': self.CLIENT_ID}
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
dest_volume = {'name': self.VOLUME_NAME,
'id': self.VOLUME_ID,
'display_name': 'Foo Volume',
'size': 2,
'host': self.FAKE_CINDER_HOST,
'volume_type': None,
'volume_type_id': None}
src_volume = {'name': self.SRC_CG_VOLUME_NAME,
'id': self.SRC_CG_VOLUME_ID,
'display_name': 'Foo Volume',
'size': 2,
'host': self.FAKE_CINDER_HOST,
'volume_type': None,
'volume_type_id': None}
vvs_name_dest = 'vvs-0DM4qZEVSKON-DXN-NwVpw'
vvs_name_src = 'vvs-vSHRG8dlTGiJbGsH9jz8tg'
vvs_name_temp = 'tos-vSHRG8dlTGiJbGsH9jz8tg'
common = self.driver._login()
return_model = common._rename_migrated_vvset(src_volume,
dest_volume)
expected = [
mock.call.modifyVolumeSet(
vvs_name_dest, newName=vvs_name_temp),
mock.call.modifyVolumeSet(
vvs_name_src, newName=vvs_name_dest),
mock.call.modifyVolumeSet(
vvs_name_temp, newName=vvs_name_src)]
mock_client.assert_has_calls(expected)
self.assertIsNone(return_model)
@ddt.ddt
class TestHPE3PARFCDriver(HPE3PARBaseDriver):

View File

@ -305,11 +305,12 @@ class HPE3PARCommon(object):
error out if it has child snapshot(s). Bug #1994521
4.0.19 - Update code to work with new WSAPI (of 2023). Bug #2015746
4.0.20 - Use small QoS Latency value. Bug #2018994
4.0.21 - Fix issue seen during retype/migrate. Bug #2026718
"""
VERSION = "4.0.20"
VERSION = "4.0.21"
stats = {}
@ -1463,9 +1464,22 @@ class HPE3PARCommon(object):
# v2 replication conversion
def _get_3par_rcg_name(self, volume):
rcg_name = self._encode_name(volume.get('_name_id') or volume['id'])
rcg = "rcg-%s" % rcg_name
return rcg[:22]
# if non-replicated volume is retyped or migrated to replicated vol,
# then rcg_name is different. Try to get that new rcg_name.
if volume['migration_status'] == 'success':
vol_name = self._get_3par_vol_name(volume)
vol_details = self.client.getVolume(vol_name)
rcg_name = vol_details.get('rcopyGroup')
LOG.debug("new rcg_name: %(name)s",
{'name': rcg_name})
return rcg_name
else:
# by default, rcg_name is similar to volume name
rcg_name = self._encode_name(volume.get('_name_id')
or volume['id'])
rcg = "rcg-%s" % rcg_name
return rcg[:22]
def _get_3par_remote_rcg_name(self, volume, provider_location):
return self._get_3par_rcg_name(volume) + ".r" + (
@ -2691,6 +2705,10 @@ class HPE3PARCommon(object):
raise exception.CinderException(ex)
def delete_volume(self, volume):
vol_id = volume.id
name_id = volume.get('_name_id')
LOG.debug("DELETE volume vol_id: %(vol_id)s, name_id: %(name_id)s",
{'vol_id': vol_id, 'name_id': name_id})
@utils.retry(exception.VolumeIsBusy, interval=2, retries=10)
def _try_remove_volume(volume_name):
@ -2706,15 +2724,30 @@ class HPE3PARCommon(object):
# If the volume type is replication enabled, we want to call our own
# method of deconstructing the volume and its dependencies
if self._volume_of_replicated_type(volume, hpe_tiramisu_check=True):
LOG.debug("volume is of replicated_type")
replication_status = volume.get('replication_status', None)
if replication_status and replication_status == "failed-over":
self._delete_replicated_failed_over_volume(volume)
else:
self._do_volume_replication_destroy(volume)
return
LOG.debug("replication_status: %(status)s",
{'status': replication_status})
if replication_status:
if replication_status == "failed-over":
self._delete_replicated_failed_over_volume(volume)
else:
self._do_volume_replication_destroy(volume)
return
volume_name = self._get_3par_vol_name(volume)
# during retype/migrate
if (self._volume_of_replicated_type(volume, hpe_tiramisu_check=True)
and volume['migration_status'] == 'deleting'):
# don't use current osv_name (which was from name_id)
# get new osv_name from id
LOG.debug("get osv_name from volume id")
volume_name = self._encode_name(volume.id)
volume_name = "osv-" + volume_name
LOG.debug("volume_name: %(name)s", {'name': volume_name})
try:
volume_name = self._get_3par_vol_name(volume)
# Try and delete the volume, it might fail here because
# the volume is part of a volume set which will have the
# volume set name in the error.
@ -3073,6 +3106,39 @@ class HPE3PARCommon(object):
log_error('original', e, temp_name, current_name, temp_name)
return True
def _rename_migrated_vvset(self, src_volume, dest_volume):
"""Rename the vvsets after a migration.
"""
vvs_name_src = self._get_3par_vvs_name(src_volume['id'])
vvs_name_dest = self._get_3par_vvs_name(dest_volume['id'])
# There can be parallel execution. Ensure that temp_vvs_name is unique
# eg. if vvs_name_src is: vvs-DK3sEwkPTCqVHdHKHtwZBA
# then temp_vvs_name is : tos-DK3sEwkPTCqVHdHKHtwZBA
temp_vvs_name = 'tos-' + vvs_name_src[4:]
try:
self.client.modifyVolumeSet(vvs_name_dest, newName=temp_vvs_name)
LOG.debug("Renamed vvset %(old)s to %(new)s",
{'old': vvs_name_dest, 'new': temp_vvs_name})
except Exception as ex:
LOG.error("exception: %(details)s", {'details': str(ex)})
try:
self.client.modifyVolumeSet(vvs_name_src, newName=vvs_name_dest)
LOG.debug("Renamed vvset %(old)s to %(new)s",
{'old': vvs_name_src, 'new': vvs_name_dest})
except Exception as ex:
LOG.error("exception: %(details)s", {'details': str(ex)})
try:
self.client.modifyVolumeSet(temp_vvs_name, newName=vvs_name_src)
LOG.debug("Renamed vvset %(old)s to %(new)s",
{'old': temp_vvs_name, 'new': vvs_name_src})
except Exception as ex:
LOG.error("exception: %(details)s", {'details': str(ex)})
def update_migrated_volume(self, context, volume, new_volume,
original_volume_status):
"""Rename the new (temp) volume to it's original name.
@ -3105,6 +3171,13 @@ class HPE3PARCommon(object):
self._update_comment(current_name, volume_id=volume['id'],
_name_id=name_id)
if new_volume_renamed:
type_info = self.get_volume_settings_from_type(volume)
qos = type_info['qos']
if qos:
# rename the vvsets as per volume names
self._rename_migrated_vvset(volume, new_volume)
return {'_name_id': name_id, 'provider_location': provider_location}
@staticmethod
@ -4491,7 +4564,7 @@ class HPE3PARCommon(object):
return True
except Exception as ex:
self._do_volume_replication_destroy(volume)
self._do_volume_replication_destroy(volume, retype=retype)
msg = (_("There was an error setting up a remote copy group "
"on the 3PAR arrays: ('%s'). The volume will not be "
"recognized as replication type.") %
@ -4576,20 +4649,29 @@ class HPE3PARCommon(object):
def _delete_vvset(self, volume):
# volume is part of a volume set.
LOG.debug("_delete_vvset. vol_id: %(id)s", {'id': volume['id']})
volume_name = self._get_3par_vol_name(volume)
vvset_name = self.client.findVolumeSet(volume_name)
LOG.debug("Returned vvset_name = %s", vvset_name)
if vvset_name is not None:
if vvset_name.startswith('vvs-'):
# We have a single volume per volume set, so
# remove the volume set.
self.client.deleteVolumeSet(
self._get_3par_vvs_name(volume['id']))
else:
# We have a pre-defined volume set just remove the
# volume and leave the volume set.
self.client.removeVolumeFromVolumeSet(vvset_name,
volume_name)
vvset_name = self._get_3par_vvs_name(volume['id'])
try:
# find vvset
self.client.getVolumeSet(vvset_name)
# (a) vvset is found:
# We have a single volume per volume set, so
# remove the volume set.
LOG.debug("Deleting vvset: %(name)s", {'name': vvset_name})
self.client.deleteVolumeSet(vvset_name)
except hpeexceptions.HTTPNotFound:
# (b) vvset not found:
# - find the vvset name from volume name
# - remove the volume and leave the vvset
vvset_name = self.client.findVolumeSet(volume_name)
LOG.debug("Removing vol %(volume_name)s from vvset %(vvset_name)s",
{'volume_name': volume_name, 'vvset_name': vvset_name})
self.client.removeVolumeFromVolumeSet(vvset_name, volume_name)
def _get_3par_rcg_name_of_group(self, group_id):
rcg_name = self._encode_name(group_id)

View File

@ -0,0 +1,7 @@
---
fixes:
- |
HPE 3PAR driver `Bug #2026718 <https://bugs.launchpad.net/cinder/+bug/2026718>`_:
Fixed: With this patch, added logic to fetch correct volume name on 3par
(osv_name), rcg_name and vvset_name (for particular scenarios);
so that volumes can be identified and deleted from 3par.