3PAR: Implement un/manage snapshot support

Implements support for managing and unmanaging snapshots to
the HPE 3PAR FC and iSCSI drivers.

This patch now allows snapshots to be removed from OpenStack
management but still left on the 3PAR backend. Snapshots on
the 3PAR backend can also be managed by OpenStack.

DocImpact
Implements: blueprint 3par-manage-unmanage-snapshot
Change-Id: I7f5847cefc983726eac8b0eaa57b63ddb9078ebd
This commit is contained in:
Anthony Lee 2015-12-04 16:27:14 -08:00
parent 71f7229b17
commit 9c3cbdd90f
5 changed files with 358 additions and 5 deletions

View File

@ -3007,6 +3007,7 @@ class HPE3PARBaseDriver(object):
common = self.driver._login()
unm_matcher = common._get_3par_unm_name(self.volume['id'])
ums_matcher = common._get_3par_ums_name(self.volume['id'])
existing_ref = {'source-name': unm_matcher}
result = common._get_existing_volume_ref_name(existing_ref)
@ -3016,6 +3017,10 @@ class HPE3PARBaseDriver(object):
result = common._get_existing_volume_ref_name(existing_ref)
self.assertEqual(unm_matcher, result)
existing_ref = {'source-id': self.volume['id']}
result = common._get_existing_volume_ref_name(existing_ref, True)
self.assertEqual(ums_matcher, result)
existing_ref = {'bad-key': 'foo'}
self.assertRaises(
exception.ManageExistingInvalidReference,
@ -3444,6 +3449,87 @@ class HPE3PARBaseDriver(object):
expected +
self.standard_logout)
def test_manage_existing_snapshot(self):
mock_client = self.setup_driver()
new_comment = Comment({
"display_name": "snap",
"volume_name": "volume-007dbfce-7579-40bc-8f90-a20b3902283e",
"volume_id": "007dbfce-7579-40bc-8f90-a20b3902283e",
"description": "",
})
snapshot = {
'display_name': None,
'id': '007dbfce-7579-40bc-8f90-a20b3902283e',
'volume_id': self.VOLUME_ID,
}
mock_client.getVolume.return_value = {
"comment": "{'display_name': 'snap'}",
'copyOf': self.VOLUME_NAME_3PAR,
}
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
oss_matcher = common._get_3par_snap_name(snapshot['id'])
ums_matcher = common._get_3par_ums_name(snapshot['id'])
existing_ref = {'source-name': ums_matcher}
expected_obj = {'display_name': 'snap'}
obj = self.driver.manage_existing_snapshot(snapshot, existing_ref)
expected = [
mock.call.getVolume(existing_ref['source-name']),
mock.call.modifyVolume(existing_ref['source-name'],
{'newName': oss_matcher,
'comment': new_comment}),
]
mock_client.assert_has_calls(
self.standard_login +
expected +
self.standard_logout)
self.assertEqual(expected_obj, obj)
def test_manage_existing_snapshot_invalid_parent(self):
mock_client = self.setup_driver()
snapshot = {
'display_name': None,
'id': '007dbfce-7579-40bc-8f90-a20b3902283e',
'volume_id': self.VOLUME_ID,
}
mock_client.getVolume.return_value = {
"comment": "{'display_name': 'snap'}",
'copyOf': 'fake-invalid',
}
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
ums_matcher = common._get_3par_ums_name(snapshot['id'])
existing_ref = {'source-name': ums_matcher}
self.assertRaises(exception.InvalidInput,
self.driver.manage_existing_snapshot,
snapshot=snapshot,
existing_ref=existing_ref)
expected = [
mock.call.getVolume(existing_ref['source-name']),
]
mock_client.assert_has_calls(
self.standard_login +
expected +
self.standard_logout)
def test_manage_existing_get_size(self):
mock_client = self.setup_driver()
mock_client.getVolume.return_value = {'sizeMiB': 2048}
@ -3519,6 +3605,86 @@ class HPE3PARBaseDriver(object):
expected +
self.standard_logout)
def test_manage_existing_snapshot_get_size(self):
mock_client = self.setup_driver()
mock_client.getVolume.return_value = {'sizeMiB': 2048}
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
ums_matcher = common._get_3par_ums_name(self.snapshot['id'])
snapshot = {}
existing_ref = {'source-name': ums_matcher}
size = self.driver.manage_existing_snapshot_get_size(snapshot,
existing_ref)
expected_size = 2
expected = [mock.call.getVolume(existing_ref['source-name'])]
mock_client.assert_has_calls(
self.standard_login +
expected +
self.standard_logout)
self.assertEqual(expected_size, size)
def test_manage_existing_snapshot_get_size_invalid_reference(self):
mock_client = self.setup_driver()
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
snapshot = {}
existing_ref = {'source-name': self.SNAPSHOT_3PAR_NAME}
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing_snapshot_get_size,
snapshot=snapshot,
existing_ref=existing_ref)
mock_client.assert_has_calls(
self.standard_login +
self.standard_logout)
existing_ref = {}
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing_snapshot_get_size,
snapshot=snapshot,
existing_ref=existing_ref)
mock_client.assert_has_calls(
self.standard_login +
self.standard_logout)
def test_manage_existing_snapshot_get_size_invalid_input(self):
mock_client = self.setup_driver()
mock_client.getVolume.side_effect = hpeexceptions.HTTPNotFound('fake')
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
ums_matcher = common._get_3par_ums_name(self.snapshot['id'])
snapshot = {}
existing_ref = {'source-name': ums_matcher}
self.assertRaises(exception.InvalidInput,
self.driver.manage_existing_snapshot_get_size,
snapshot=snapshot,
existing_ref=existing_ref)
expected = [mock.call.getVolume(existing_ref['source-name'])]
mock_client.assert_has_calls(
self.standard_login +
expected +
self.standard_logout)
def test_unmanage(self):
mock_client = self.setup_driver()
with mock.patch.object(hpecommon.HPE3PARCommon,
@ -3539,6 +3705,26 @@ class HPE3PARBaseDriver(object):
expected +
self.standard_logout)
def test_unmanage_snapshot(self):
mock_client = self.setup_driver()
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
self.driver.unmanage_snapshot(self.snapshot)
oss_matcher = common._get_3par_snap_name(self.snapshot['id'])
ums_matcher = common._get_3par_ums_name(self.snapshot['id'])
expected = [
mock.call.modifyVolume(oss_matcher, {'newName': ums_matcher})
]
mock_client.assert_has_calls(
self.standard_login +
expected +
self.standard_logout)
def test__safe_hostname(self):
long_hostname = "abc123abc123abc123abc123abc123abc123"
fixed_hostname = "abc123abc123abc123abc123abc123a"

View File

@ -217,10 +217,11 @@ class HPE3PARCommon(object):
3.0.3 - Remove db access for consistency groups
3.0.4 - Adds v2 managed replication support
3.0.5 - Adds v2 unmanaged replication support
3.0.6 - Adding manage/unmanage snapshot support
"""
VERSION = "3.0.5"
VERSION = "3.0.6"
stats = {}
@ -759,6 +760,73 @@ class HPE3PARCommon(object):
# any model updates from retype.
return updates
def manage_existing_snapshot(self, snapshot, existing_ref):
"""Manage an existing 3PAR snapshot.
existing_ref is a dictionary of the form:
{'source-name': <name of the snapshot>}
"""
target_snap_name = self._get_existing_volume_ref_name(existing_ref,
is_snapshot=True)
# Check for the existence of the snapshot.
try:
snap = self.client.getVolume(target_snap_name)
except hpeexceptions.HTTPNotFound:
err = (_("Snapshot '%s' doesn't exist on array.") %
target_snap_name)
LOG.error(err)
raise exception.InvalidInput(reason=err)
# Make sure the snapshot is being associated with the correct volume.
parent_vol_name = self._get_3par_vol_name(snapshot['volume_id'])
if parent_vol_name != snap['copyOf']:
err = (_("The provided snapshot '%s' is not a snapshot of "
"the provided volume.") % target_snap_name)
LOG.error(err)
raise exception.InvalidInput(reason=err)
new_comment = {}
# Use the display name from the existing snapshot if no new name
# was chosen by the user.
if snapshot['display_name']:
display_name = snapshot['display_name']
new_comment['display_name'] = snapshot['display_name']
elif 'comment' in snap:
display_name = self._get_3par_vol_comment_value(snap['comment'],
'display_name')
if display_name:
new_comment['display_name'] = display_name
else:
display_name = None
# Generate the new snapshot information based on the new ID.
new_snap_name = self._get_3par_snap_name(snapshot['id'])
new_comment['volume_id'] = snapshot['id']
new_comment['volume_name'] = 'volume-' + snapshot['id']
if snapshot.get('display_description', None):
new_comment['description'] = snapshot['display_description']
else:
new_comment['description'] = ""
new_vals = {'newName': new_snap_name,
'comment': json.dumps(new_comment)}
# Update the existing snapshot with the new name and comments.
self.client.modifyVolume(target_snap_name, new_vals)
LOG.info(_LI("Snapshot '%(ref)s' renamed to '%(new)s'."),
{'ref': existing_ref['source-name'], 'new': new_snap_name})
updates = {'display_name': display_name}
LOG.info(_LI("Snapshot %(disp)s '%(new)s' is now being managed."),
{'disp': display_name, 'new': new_snap_name})
# Return display name to update the name displayed in the GUI.
return updates
def manage_existing_get_size(self, volume, existing_ref):
"""Return size of volume to be managed by manage_existing.
@ -785,6 +853,33 @@ class HPE3PARCommon(object):
return int(math.ceil(float(vol['sizeMiB']) / units.Ki))
def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
"""Return size of snapshot to be managed by manage_existing_snapshot.
existing_ref is a dictionary of the form:
{'source-name': <name of the snapshot>}
"""
target_snap_name = self._get_existing_volume_ref_name(existing_ref,
is_snapshot=True)
# Make sure the reference is not in use.
if re.match('osv-*|oss-*|vvs-*|unm-*', target_snap_name):
reason = _("Reference must be for an unmanaged snapshot.")
raise exception.ManageExistingInvalidReference(
existing_ref=target_snap_name,
reason=reason)
# Check for the existence of the snapshot.
try:
snap = self.client.getVolume(target_snap_name)
except hpeexceptions.HTTPNotFound:
err = (_("Snapshot '%s' doesn't exist on array.") %
target_snap_name)
LOG.error(err)
raise exception.InvalidInput(reason=err)
return int(math.ceil(float(snap['sizeMiB']) / units.Ki))
def unmanage(self, volume):
"""Removes the specified volume from Cinder management."""
# Rename the volume's name to unm-* format so that it can be
@ -799,7 +894,21 @@ class HPE3PARCommon(object):
'vol': vol_name,
'new': new_vol_name})
def _get_existing_volume_ref_name(self, existing_ref):
def unmanage_snapshot(self, snapshot):
"""Removes the specified snapshot from Cinder management."""
# Rename the snapshots's name to ums-* format so that it can be
# easily found later.
snap_name = self._get_3par_snap_name(snapshot['id'])
new_snap_name = self._get_3par_ums_name(snapshot['id'])
self.client.modifyVolume(snap_name, {'newName': new_snap_name})
LOG.info(_LI("Snapshot %(disp)s '%(vol)s' is no longer managed. "
"Snapshot renamed to '%(new)s'."),
{'disp': snapshot['display_name'],
'vol': snap_name,
'new': new_snap_name})
def _get_existing_volume_ref_name(self, existing_ref, is_snapshot=False):
"""Returns the volume name of an existing reference.
Checks if an existing volume reference has a source-name or
@ -810,7 +919,10 @@ class HPE3PARCommon(object):
if 'source-name' in existing_ref:
vol_name = existing_ref['source-name']
elif 'source-id' in existing_ref:
vol_name = self._get_3par_unm_name(existing_ref['source-id'])
if is_snapshot:
vol_name = self._get_3par_ums_name(existing_ref['source-id'])
else:
vol_name = self._get_3par_unm_name(existing_ref['source-id'])
else:
reason = _("Reference must contain source-name or source-id.")
raise exception.ManageExistingInvalidReference(
@ -884,6 +996,10 @@ class HPE3PARCommon(object):
snapshot_name = self._encode_name(snapshot_id)
return "oss-%s" % snapshot_name
def _get_3par_ums_name(self, snapshot_id):
ums_name = self._encode_name(snapshot_id)
return "ums-%s" % ums_name
def _get_3par_vvs_name(self, volume_id):
vvs_name = self._encode_name(volume_id)
return "vvs-%s" % vvs_name

View File

@ -50,6 +50,7 @@ class HPE3PARFCDriver(driver.TransferVD,
driver.ManageableVD,
driver.ExtendVD,
driver.SnapshotVD,
driver.ManageableSnapshotsVD,
driver.MigrateVD,
driver.ConsistencyGroupVD,
driver.BaseVD):
@ -93,10 +94,11 @@ class HPE3PARFCDriver(driver.TransferVD,
3.0.1 - Remove db access for consistency groups
3.0.2 - Adds v2 managed replication support
3.0.3 - Adds v2 unmanaged replication support
3.0.4 - Adding manage/unmanage snapshot support
"""
VERSION = "3.0.3"
VERSION = "3.0.4"
def __init__(self, *args, **kwargs):
super(HPE3PARFCDriver, self).__init__(*args, **kwargs)
@ -511,6 +513,13 @@ class HPE3PARFCDriver(driver.TransferVD,
finally:
self._logout(common)
def manage_existing_snapshot(self, snapshot, existing_ref):
common = self._login()
try:
return common.manage_existing_snapshot(snapshot, existing_ref)
finally:
self._logout(common)
def manage_existing_get_size(self, volume, existing_ref):
common = self._login(volume)
try:
@ -518,6 +527,14 @@ class HPE3PARFCDriver(driver.TransferVD,
finally:
self._logout(common)
def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
common = self._login()
try:
return common.manage_existing_snapshot_get_size(snapshot,
existing_ref)
finally:
self._logout(common)
def unmanage(self, volume):
common = self._login(volume)
try:
@ -525,6 +542,13 @@ class HPE3PARFCDriver(driver.TransferVD,
finally:
self._logout(common)
def unmanage_snapshot(self, snapshot):
common = self._login()
try:
common.unmanage_snapshot(snapshot)
finally:
self._logout(common)
def attach_volume(self, context, volume, instance_uuid, host_name,
mountpoint):
common = self._login(volume)

View File

@ -55,6 +55,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
driver.ManageableVD,
driver.ExtendVD,
driver.SnapshotVD,
driver.ManageableSnapshotsVD,
driver.MigrateVD,
driver.ConsistencyGroupVD,
driver.BaseVD):
@ -105,10 +106,11 @@ class HPE3PARISCSIDriver(driver.TransferVD,
3.0.3 - Fix multipath dictionary key error. bug #1522062
3.0.4 - Adds v2 managed replication support
3.0.5 - Adds v2 unmanaged replication support
3.0.6 - Adding manage/unmanage snapshot support
"""
VERSION = "3.0.5"
VERSION = "3.0.6"
def __init__(self, *args, **kwargs):
super(HPE3PARISCSIDriver, self).__init__(*args, **kwargs)
@ -822,6 +824,13 @@ class HPE3PARISCSIDriver(driver.TransferVD,
finally:
self._logout(common)
def manage_existing_snapshot(self, snapshot, existing_ref):
common = self._login()
try:
return common.manage_existing_snapshot(snapshot, existing_ref)
finally:
self._logout(common)
def manage_existing_get_size(self, volume, existing_ref):
common = self._login(volume)
try:
@ -829,6 +838,14 @@ class HPE3PARISCSIDriver(driver.TransferVD,
finally:
self._logout(common)
def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
common = self._login()
try:
return common.manage_existing_snapshot_get_size(snapshot,
existing_ref)
finally:
self._logout(common)
def unmanage(self, volume):
common = self._login(volume)
try:
@ -836,6 +853,13 @@ class HPE3PARISCSIDriver(driver.TransferVD,
finally:
self._logout(common)
def unmanage_snapshot(self, snapshot):
common = self._login()
try:
common.unmanage_snapshot(snapshot)
finally:
self._logout(common)
def attach_volume(self, context, volume, instance_uuid, host_name,
mountpoint):
common = self._login(volume)

View File

@ -0,0 +1,3 @@
---
features:
- Added snapshot manage/unmanage support to the HPE 3PAR driver.