LeftHand: Implement un/manage snapshot support

Implements support for managing and unmanaging snapshots to
the HPE LeftHand driver.

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

DocImpact
Implements: blueprint lefthand-manage-unmanage-snapshot
Change-Id: Ic0048fd5da437cf8caddc166cf60ec035def39aa
This commit is contained in:
Anthony Lee 2015-12-08 16:15:42 -08:00
parent 9299e7aebe
commit 6fa9ac877b
3 changed files with 362 additions and 1 deletions

View File

@ -108,7 +108,9 @@ class HPELeftHandBaseDriver(object):
snapshot_name = "fakeshapshot"
snapshot_id = 3
snapshot = {
'id': snapshot_id,
'name': snapshot_name,
'display_name': 'fakesnap',
'volume_name': volume_name,
'volume': volume}
@ -1501,6 +1503,70 @@ class TestHPELeftHandISCSIDriver(HPELeftHandBaseDriver, test.TestCase):
mock.call.getVolumeByName(self.volume_name),
mock.call.logout()])
def test_manage_existing_snapshot(self):
mock_client = self.setup_driver()
self.driver.api_version = "1.1"
volume = {
'id': '111',
}
snapshot = {
'display_name': 'Foo Snap',
'id': '12345',
'volume': volume,
'volume_id': '111',
}
with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getSnapshotByName.return_value = {
'id': self.snapshot_id
}
mock_client.getSnapshotParentVolume.return_value = {
'name': 'volume-111'
}
existing_ref = {'source-name': self.snapshot_name}
expected_obj = {'display_name': 'Foo Snap'}
obj = self.driver.manage_existing_snapshot(snapshot, existing_ref)
mock_client.assert_has_calls(
self.driver_startup_call_stack + [
mock.call.getSnapshotByName(self.snapshot_name),
mock.call.getSnapshotParentVolume(self.snapshot_name),
mock.call.modifySnapshot(self.snapshot_id,
{'name': 'snapshot-12345'}),
mock.call.logout()])
self.assertEqual(expected_obj, obj)
def test_manage_existing_snapshot_failed_over_volume(self):
mock_client = self.setup_driver()
self.driver.api_version = "1.1"
volume = {
'id': self.volume_id,
'replication_status': 'failed-over',
}
snapshot = {
'display_name': 'Foo Snap',
'id': '12345',
'volume': volume,
}
existing_ref = {'source-name': self.snapshot_name}
with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
self.assertRaises(exception.InvalidInput,
self.driver.manage_existing_snapshot,
snapshot=snapshot,
existing_ref=existing_ref)
def test_manage_existing_get_size(self):
mock_client = self.setup_driver()
mock_client.getVolumeByName.return_value = {'size': 2147483648}
@ -1597,6 +1663,87 @@ class TestHPELeftHandISCSIDriver(HPELeftHandBaseDriver, test.TestCase):
self.driver_startup_call_stack +
expected)
def test_manage_existing_snapshot_get_size(self):
mock_client = self.setup_driver()
mock_client.getSnapshotByName.return_value = {'size': 2147483648}
self.driver.api_version = "1.1"
with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
snapshot = {}
existing_ref = {'source-name': self.snapshot_name}
size = self.driver.manage_existing_snapshot_get_size(snapshot,
existing_ref)
expected_size = 2
expected = [mock.call.getSnapshotByName(
existing_ref['source-name']),
mock.call.logout()]
mock_client.assert_has_calls(
self.driver_startup_call_stack +
expected)
self.assertEqual(expected_size, size)
def test_manage_existing_snapshot_get_size_invalid_reference(self):
mock_client = self.setup_driver()
mock_client.getSnapshotByName.return_value = {'size': 2147483648}
self.driver.api_version = "1.1"
with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
snapshot = {}
existing_ref = {'source-name': "snapshot-12345"}
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing_snapshot_get_size,
snapshot=snapshot,
existing_ref=existing_ref)
mock_client.assert_has_calls([])
existing_ref = {}
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing_snapshot_get_size,
snapshot=snapshot,
existing_ref=existing_ref)
mock_client.assert_has_calls([])
def test_manage_existing_snapshot_get_size_invalid_input(self):
mock_client = self.setup_driver()
mock_client.getSnapshotByName.side_effect = (
hpeexceptions.HTTPNotFound('fake'))
self.driver.api_version = "1.1"
with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
snapshot = {}
existing_ref = {'source-name': self.snapshot_name}
self.assertRaises(exception.InvalidInput,
self.driver.manage_existing_snapshot_get_size,
snapshot=snapshot,
existing_ref=existing_ref)
expected = [mock.call.getSnapshotByName(
existing_ref['source-name'])]
mock_client.assert_has_calls(
self.driver_startup_call_stack +
expected)
def test_unmanage(self):
mock_client = self.setup_driver()
mock_client.getVolumeByName.return_value = {'id': self.volume_id}
@ -1631,6 +1778,62 @@ class TestHPELeftHandISCSIDriver(HPELeftHandBaseDriver, test.TestCase):
self.driver_startup_call_stack +
expected)
def test_unmanage_snapshot(self):
mock_client = self.setup_driver()
volume = {
'id': self.volume_id,
}
snapshot = {
'name': self.snapshot_name,
'display_name': 'Foo Snap',
'volume': volume,
'id': self.snapshot_id,
}
mock_client.getSnapshotByName.return_value = {'id': self.snapshot_id, }
self.driver.api_version = "1.1"
with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
self.driver.unmanage_snapshot(snapshot)
new_name = 'ums-' + str(self.snapshot_id)
expected = [
mock.call.getSnapshotByName(snapshot['name']),
mock.call.modifySnapshot(self.snapshot_id, {'name': new_name}),
mock.call.logout()
]
mock_client.assert_has_calls(
self.driver_startup_call_stack +
expected)
def test_unmanage_snapshot_failed_over_volume(self):
mock_client = self.setup_driver()
volume = {
'id': self.volume_id,
'replication_status': 'failed-over',
}
snapshot = {
'name': self.snapshot_name,
'display_name': 'Foo Snap',
'volume': volume,
'id': self.snapshot_id,
}
mock_client.getSnapshotByName.return_value = {'id': self.snapshot_id, }
self.driver.api_version = "1.1"
with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
self.assertRaises(exception.SnapshotIsBusy,
self.driver.unmanage_snapshot,
snapshot=snapshot)
def test_api_version(self):
self.setup_driver()
self.driver.api_version = "1.1"

View File

@ -148,9 +148,10 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
2.0.1 - Remove db access for consistency groups
2.0.2 - Adds v2 managed replication support
2.0.3 - Adds v2 unmanaged replication support
2.0.4 - Add manage/unmanage snapshot support
"""
VERSION = "2.0.3"
VERSION = "2.0.4"
device_stats = {}
@ -1159,6 +1160,95 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
# any model updates from retype.
return updates
def manage_existing_snapshot(self, snapshot, existing_ref):
"""Manage an existing LeftHand snapshot.
existing_ref is a dictionary of the form:
{'source-name': <name of the snapshot>}
"""
# Check API Version
self._check_api_version()
# Potential parent volume for the snapshot
volume = snapshot['volume']
if volume.get('replication_status') == 'failed-over':
err = (_("Managing of snapshots to failed-over volumes is "
"not allowed."))
raise exception.InvalidInput(reason=err)
target_snap_name = self._get_existing_volume_ref_name(existing_ref)
# Check for the existence of the virtual volume.
client = self._login()
try:
updates = self._manage_snapshot(client,
volume,
snapshot,
target_snap_name,
existing_ref)
finally:
self._logout(client)
# Return display name to update the name displayed in the GUI and
# any model updates from retype.
return updates
def _manage_snapshot(self, client, volume, snapshot, target_snap_name,
existing_ref):
# Check for the existence of the virtual volume.
try:
snapshot_info = client.getSnapshotByName(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.
try:
parent_vol = client.getSnapshotParentVolume(target_snap_name)
except hpeexceptions.HTTPNotFound:
err = (_("Could not find the parent volume for Snapshot '%s' on "
"array.") % target_snap_name)
LOG.error(err)
raise exception.InvalidInput(reason=err)
parent_vol_name = 'volume-' + snapshot['volume_id']
if parent_vol_name != parent_vol['name']:
err = (_("The provided snapshot '%s' is not a snapshot of "
"the provided volume.") % target_snap_name)
LOG.error(err)
raise exception.InvalidInput(reason=err)
# Generate the new snapshot information based on the new ID.
new_snap_name = 'snapshot-' + snapshot['id']
new_vals = {"name": new_snap_name}
try:
# Update the existing snapshot with the new name.
client.modifySnapshot(snapshot_info['id'], new_vals)
except hpeexceptions.HTTPServerError:
err = (_("An error occured while attempting to modify"
"Snapshot '%s'.") % snapshot_info['id'])
LOG.error(err)
LOG.info(_LI("Snapshot '%(ref)s' renamed to '%(new)s'."),
{'ref': existing_ref['source-name'], 'new': new_snap_name})
display_name = None
if snapshot['display_name']:
display_name = snapshot['display_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 updates
def manage_existing_get_size(self, volume, existing_ref):
"""Return size of volume to be managed by manage_existing.
@ -1192,6 +1282,39 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
return int(math.ceil(float(volume_info['size']) / units.Gi))
def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
"""Return size of volume to be managed by manage_existing.
existing_ref is a dictionary of the form:
{'source-name': <name of the virtual volume>}
"""
# Check API version.
self._check_api_version()
target_snap_name = self._get_existing_volume_ref_name(existing_ref)
# Make sure the reference is not in use.
if re.match('volume-*|snapshot-*|unm-*', target_snap_name):
reason = _("Reference must be the name of an unmanaged "
"snapshot.")
raise exception.ManageExistingInvalidReference(
existing_ref=target_snap_name,
reason=reason)
# Check for the existence of the virtual volume.
client = self._login()
try:
snapshot_info = client.getSnapshotByName(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)
finally:
self._logout(client)
return int(math.ceil(float(snapshot_info['size']) / units.Gi))
def unmanage(self, volume):
"""Removes the specified volume from Cinder management."""
# Check API version.
@ -1214,6 +1337,38 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
'vol': volume['name'],
'new': new_vol_name})
def unmanage_snapshot(self, snapshot):
"""Removes the specified snapshot from Cinder management."""
# Check API version.
self._check_api_version()
# Potential parent volume for the snapshot
volume = snapshot['volume']
if volume.get('replication_status') == 'failed-over':
err = (_("Unmanaging of snapshots from 'failed-over' volumes is "
"not allowed."))
LOG.error(err)
# TODO(leeantho) Change this exception to Invalid when the volume
# manager supports handling that.
raise exception.SnapshotIsBusy(snapshot_name=snapshot['id'])
# Rename the snapshots's name to ums-* format so that it can be
# easily found later.
client = self._login()
try:
snapshot_info = client.getSnapshotByName(snapshot['name'])
new_snap_name = 'ums-' + six.text_type(snapshot['id'])
options = {'name': new_snap_name}
client.modifySnapshot(snapshot_info['id'], options)
LOG.info(_LI("Snapshot %(disp)s '%(vol)s' is no longer managed. "
"Snapshot renamed to '%(new)s'."),
{'disp': snapshot['display_name'],
'vol': snapshot['name'],
'new': new_snap_name})
finally:
self._logout(client)
def _get_existing_volume_ref_name(self, existing_ref):
"""Returns the volume name of an existing reference.

View File

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