diff --git a/cinder/tests/unit/test_hpelefthand.py b/cinder/tests/unit/test_hpelefthand.py index 5f6c08b7bfb..0fa657aa9bb 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 eeefd49c9e4..3523ccb1e1d 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 00000000000..2c863b933a0 --- /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.