From 9c3cbdd90fbf4e462c23f640e68cd88034c873c2 Mon Sep 17 00:00:00 2001 From: Anthony Lee Date: Fri, 4 Dec 2015 16:27:14 -0800 Subject: [PATCH] 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 --- cinder/tests/unit/test_hpe3par.py | 186 ++++++++++++++++++ cinder/volume/drivers/hpe/hpe_3par_common.py | 122 +++++++++++- cinder/volume/drivers/hpe/hpe_3par_fc.py | 26 ++- cinder/volume/drivers/hpe/hpe_3par_iscsi.py | 26 ++- ...ge-unmanage-snapshot-eb4e504e8782ba43.yaml | 3 + 5 files changed, 358 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/3par-manage-unmanage-snapshot-eb4e504e8782ba43.yaml diff --git a/cinder/tests/unit/test_hpe3par.py b/cinder/tests/unit/test_hpe3par.py index f997c6266..40f10613b 100644 --- a/cinder/tests/unit/test_hpe3par.py +++ b/cinder/tests/unit/test_hpe3par.py @@ -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" diff --git a/cinder/volume/drivers/hpe/hpe_3par_common.py b/cinder/volume/drivers/hpe/hpe_3par_common.py index 14c2845f3..8092b3db5 100644 --- a/cinder/volume/drivers/hpe/hpe_3par_common.py +++ b/cinder/volume/drivers/hpe/hpe_3par_common.py @@ -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': } + """ + 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': } + """ + 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 diff --git a/cinder/volume/drivers/hpe/hpe_3par_fc.py b/cinder/volume/drivers/hpe/hpe_3par_fc.py index 5d813914a..bd817fe89 100644 --- a/cinder/volume/drivers/hpe/hpe_3par_fc.py +++ b/cinder/volume/drivers/hpe/hpe_3par_fc.py @@ -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) diff --git a/cinder/volume/drivers/hpe/hpe_3par_iscsi.py b/cinder/volume/drivers/hpe/hpe_3par_iscsi.py index ae0b9bc0f..d954c72e8 100644 --- a/cinder/volume/drivers/hpe/hpe_3par_iscsi.py +++ b/cinder/volume/drivers/hpe/hpe_3par_iscsi.py @@ -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) diff --git a/releasenotes/notes/3par-manage-unmanage-snapshot-eb4e504e8782ba43.yaml b/releasenotes/notes/3par-manage-unmanage-snapshot-eb4e504e8782ba43.yaml new file mode 100644 index 000000000..f6e860cbb --- /dev/null +++ b/releasenotes/notes/3par-manage-unmanage-snapshot-eb4e504e8782ba43.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added snapshot manage/unmanage support to the HPE 3PAR driver. -- 2.45.2