From 6fa9ac877b7d29596199da1d6d0ad12f01eb134b Mon Sep 17 00:00:00 2001 From: Anthony Lee Date: Tue, 8 Dec 2015 16:15:42 -0800 Subject: [PATCH] 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 --- cinder/tests/unit/test_hpelefthand.py | 203 ++++++++++++++++++ .../volume/drivers/hpe/hpe_lefthand_iscsi.py | 157 +++++++++++++- ...ge-unmanage-snapshot-04de39d268d51169.yaml | 3 + 3 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/lefthand-manage-unmanage-snapshot-04de39d268d51169.yaml diff --git a/cinder/tests/unit/test_hpelefthand.py b/cinder/tests/unit/test_hpelefthand.py index 5f6c08b7b..0fa657aa9 100644 --- a/cinder/tests/unit/test_hpelefthand.py +++ b/cinder/tests/unit/test_hpelefthand.py @@ -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" diff --git a/cinder/volume/drivers/hpe/hpe_lefthand_iscsi.py b/cinder/volume/drivers/hpe/hpe_lefthand_iscsi.py index eeefd49c9..3523ccb1e 100644 --- a/cinder/volume/drivers/hpe/hpe_lefthand_iscsi.py +++ b/cinder/volume/drivers/hpe/hpe_lefthand_iscsi.py @@ -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': } + """ + # 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': } + """ + # 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. diff --git a/releasenotes/notes/lefthand-manage-unmanage-snapshot-04de39d268d51169.yaml b/releasenotes/notes/lefthand-manage-unmanage-snapshot-04de39d268d51169.yaml new file mode 100644 index 000000000..2c863b933 --- /dev/null +++ b/releasenotes/notes/lefthand-manage-unmanage-snapshot-04de39d268d51169.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added snapshot manage/unmanage support to the HPE LeftHand driver. -- 2.45.2