From: Patrick East Date: Wed, 12 Aug 2015 22:37:50 +0000 (-0700) Subject: Implement manage/unmanage snapshot in Pure drivers X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=9f3267165ec93f2d40cbd4dfca6d75f4309ddfb4;p=openstack-build%2Fcinder-build.git Implement manage/unmanage snapshot in Pure drivers This adds implementations for manage_existing_snapshot, manage_existing_snapshot_get_size, and unmanage_snapshot for PureFCDriver and PureISCSIDriver. Similar to the implementation of the manage/unmanage volume these will need a volume_ref containing a ‘name’ key and value that matches an object in Purity. In this case it will need to be the full name of a snapshot object. Due to this requiring a rename of the snapshot it will only be available if using Purity REST API v1.4 or greater. If using a lower version an exception will be raised. Change-Id: Icb788e6cc0ee0e13393a12bc6aaccc57e1d491fd Implements: blueprint pure-manage-snapshot --- diff --git a/cinder/tests/unit/test_pure.py b/cinder/tests/unit/test_pure.py index 50162c155..39b2c677b 100644 --- a/cinder/tests/unit/test_pure.py +++ b/cinder/tests/unit/test_pure.py @@ -93,6 +93,7 @@ SNAPSHOT = { "display_name": "fake_snapshot", "cgsnapshot_id": None, } +SNAPSHOT_PURITY_NAME = SRC_VOL["name"] + '-cinder.' + SNAPSHOT["name"] SNAPSHOT_WITH_CGROUP = SNAPSHOT.copy() SNAPSHOT_WITH_CGROUP['cgsnapshot_id'] = \ "4a2f7e3a-312a-40c5-96a8-536b8a0fe075" @@ -177,6 +178,13 @@ FC_CONNECTION_INFO = { "discard": True, }, } +PURE_SNAPSHOT = { + "created": "2015-05-27T17:34:33Z", + "name": "vol1.snap1", + "serial": "8343DFDE2DAFBE40000115E4", + "size": 3221225472, + "source": "vol1" +} class FakePureStorageHTTPError(Exception): @@ -229,6 +237,7 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase): self.driver = self.fake_pure_base_volume_driver( configuration=self.mock_config) self.driver._array = self.array + self.array.get_rest_version.return_value = '1.4' def test_generate_purity_host_name(self): result = self.driver._generate_purity_host_name( @@ -1050,7 +1059,7 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase): size = self.driver.manage_existing_get_size(VOLUME, volume_ref) self.assertEqual(expected_size, size) - self.array.get_volume.assert_called_with(ref_name) + self.array.get_volume.assert_called_with(ref_name, snap=False) def test_manage_existing_get_size_error_propagates(self): self.array.get_volume.return_value = mock.MagicMock() @@ -1099,6 +1108,157 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase): self.array.rename_volume.assert_called_with(vol_name, unmanaged_vol_name) + def test_manage_existing_snapshot(self): + ref_name = PURE_SNAPSHOT['name'] + snap_ref = {'name': ref_name} + self.array.get_volume.return_value = [PURE_SNAPSHOT] + self.driver.manage_existing_snapshot(SNAPSHOT, snap_ref) + self.array.rename_volume.assert_called_once_with(ref_name, + SNAPSHOT_PURITY_NAME) + self.array.get_volume.assert_called_with(PURE_SNAPSHOT['source'], + snap=True) + + def test_manage_existing_snapshot_multiple_snaps_on_volume(self): + ref_name = PURE_SNAPSHOT['name'] + snap_ref = {'name': ref_name} + pure_snaps = [PURE_SNAPSHOT] + for i in range(5): + snap = PURE_SNAPSHOT.copy() + snap['name'] += str(i) + pure_snaps.append(snap) + self.array.get_volume.return_value = pure_snaps + self.driver.manage_existing_snapshot(SNAPSHOT, snap_ref) + self.array.rename_volume.assert_called_once_with(ref_name, + SNAPSHOT_PURITY_NAME) + + def test_manage_existing_snapshot_error_propagates(self): + self.array.get_volume.return_value = [PURE_SNAPSHOT] + self.assert_error_propagates( + [self.array.rename_volume], + self.driver.manage_existing_snapshot, + SNAPSHOT, {'name': PURE_SNAPSHOT['name']} + ) + + def test_manage_existing_snapshot_bad_ref(self): + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot, + SNAPSHOT, {'bad_key': 'bad_value'}) + + def test_manage_existing_snapshot_empty_ref(self): + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot, + SNAPSHOT, {'name': ''}) + + def test_manage_existing_snapshot_none_ref(self): + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot, + SNAPSHOT, {'name': None}) + + def test_manage_existing_snapshot_volume_ref_not_exist(self): + self.array.get_volume.side_effect = \ + self.purestorage_module.PureHTTPError( + text="Volume does not exist.", + code=400 + ) + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot, + SNAPSHOT, {'name': 'non-existing-volume.snap1'}) + + def test_manage_existing_snapshot_ref_not_exist(self): + ref_name = PURE_SNAPSHOT['name'] + '-fake' + snap_ref = {'name': ref_name} + self.array.get_volume.return_value = [PURE_SNAPSHOT] + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot, + SNAPSHOT, snap_ref) + + def test_manage_existing_snapshot_bad_api_version(self): + self.array.get_rest_version.return_value = '1.3' + self.assertRaises(exception.PureDriverException, + self.driver.manage_existing_snapshot, + SNAPSHOT, {'name': PURE_SNAPSHOT['name']}) + + def test_manage_existing_snapshot_get_size(self): + ref_name = PURE_SNAPSHOT['name'] + snap_ref = {'name': ref_name} + self.array.get_volume.return_value = [PURE_SNAPSHOT] + + size = self.driver.manage_existing_snapshot_get_size(SNAPSHOT, + snap_ref) + expected_size = 3.0 + self.assertEqual(expected_size, size) + self.array.get_volume.assert_called_with(PURE_SNAPSHOT['source'], + snap=True) + + def test_manage_existing_snapshot_get_size_error_propagates(self): + self.array.get_volume.return_value = [PURE_SNAPSHOT] + self.assert_error_propagates( + [self.array.get_volume], + self.driver.manage_existing_snapshot_get_size, + SNAPSHOT, {'name': PURE_SNAPSHOT['name']} + ) + + def test_manage_existing_snapshot_get_size_bad_ref(self): + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot_get_size, + SNAPSHOT, {'bad_key': 'bad_value'}) + + def test_manage_existing_snapshot_get_size_empty_ref(self): + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot_get_size, + SNAPSHOT, {'name': ''}) + + def test_manage_existing_snapshot_get_size_none_ref(self): + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot_get_size, + SNAPSHOT, {'name': None}) + + def test_manage_existing_snapshot_get_size_volume_ref_not_exist(self): + self.array.get_volume.side_effect = \ + self.purestorage_module.PureHTTPError( + text="Volume does not exist.", + code=400 + ) + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot_get_size, + SNAPSHOT, {'name': 'non-existing-volume.snap1'}) + + def test_manage_existing_snapshot_get_size_bad_api_version(self): + self.array.get_rest_version.return_value = '1.3' + self.assertRaises(exception.PureDriverException, + self.driver.manage_existing_snapshot_get_size, + SNAPSHOT, {'name': PURE_SNAPSHOT['name']}) + + def test_unmanage_snapshot(self): + unmanaged_snap_name = SNAPSHOT_PURITY_NAME + "-unmanaged" + self.driver.unmanage_snapshot(SNAPSHOT) + self.array.rename_volume.assert_called_with(SNAPSHOT_PURITY_NAME, + unmanaged_snap_name) + + def test_unmanage_snapshot_error_propagates(self): + self.assert_error_propagates([self.array.rename_volume], + self.driver.unmanage_snapshot, + SNAPSHOT) + + def test_unmanage_snapshot_with_deleted_snapshot(self): + unmanaged_snap_name = SNAPSHOT_PURITY_NAME + "-unmanaged" + self.array.rename_volume.side_effect = \ + self.purestorage_module.PureHTTPError( + text="Snapshot does not exist.", + code=400 + ) + + self.driver.unmanage_snapshot(SNAPSHOT) + + self.array.rename_volume.assert_called_with(SNAPSHOT_PURITY_NAME, + unmanaged_snap_name) + + def test_unmanage_snapshot_bad_api_version(self): + self.array.get_rest_version.return_value = '1.3' + self.assertRaises(exception.PureDriverException, + self.driver.unmanage_snapshot, + SNAPSHOT) + def test_retype(self): # Ensure that we return true no matter what the inputs are retyped, update = self.driver.retype(None, None, None, None, None) diff --git a/cinder/volume/drivers/pure.py b/cinder/volume/drivers/pure.py index b73a053a7..902c3a8e8 100644 --- a/cinder/volume/drivers/pure.py +++ b/cinder/volume/drivers/pure.py @@ -62,6 +62,9 @@ ERR_MSG_PENDING_ERADICATION = "has been destroyed" CONNECT_LOCK_NAME = 'PureVolumeDriver_connect' +UNMANAGED_SUFFIX = '-unmanaged' +MANAGE_SNAP_REQUIRED_API_VERSIONS = ['1.4'] + def log_debug_trace(f): def wrapper(*args, **kwargs): @@ -493,11 +496,14 @@ class PureBaseVolumeDriver(san.SanDriver): return model_update, snapshots - def _validate_manage_existing_ref(self, existing_ref): + def _validate_manage_existing_ref(self, existing_ref, is_snap=False): """Ensure that an existing_ref is valid and return volume info If the ref is not valid throw a ManageExistingInvalidReference exception with an appropriate error. + + Will return volume or snapshot information from the array for + the object specified by existing_ref. """ if "name" not in existing_ref or not existing_ref["name"]: raise exception.ManageExistingInvalidReference( @@ -505,12 +511,21 @@ class PureBaseVolumeDriver(san.SanDriver): reason=_("manage_existing requires a 'name'" " key to identify an existing volume.")) - ref_vol_name = existing_ref['name'] + if is_snap: + # Purity snapshot names are prefixed with the source volume name + ref_vol_name, ref_snap_suffix = existing_ref['name'].split('.') + else: + ref_vol_name = existing_ref['name'] try: - volume_info = self._array.get_volume(ref_vol_name) + volume_info = self._array.get_volume(ref_vol_name, snap=is_snap) if volume_info: - return volume_info + if is_snap: + for snap in volume_info: + if snap['name'] == existing_ref['name']: + return snap + else: + return volume_info except purestorage.PureHTTPError as err: with excutils.save_and_reraise_exception() as ctxt: if (err.code == 400 and @@ -521,7 +536,7 @@ class PureBaseVolumeDriver(san.SanDriver): # to throw a Invalid Reference exception raise exception.ManageExistingInvalidReference( existing_ref=existing_ref, - reason=_("Unable to find volume with name=%s") % ref_vol_name) + reason=_("Unable to find Purity ref with name=%s") % ref_vol_name) @log_debug_trace def manage_existing(self, volume, existing_ref): @@ -546,7 +561,9 @@ class PureBaseVolumeDriver(san.SanDriver): new_vol_name = self._get_vol_name(volume) LOG.info(_LI("Renaming existing volume %(ref_name)s to %(new_name)s"), {"ref_name": ref_vol_name, "new_name": new_vol_name}) - self._array.rename_volume(ref_vol_name, new_vol_name) + self._rename_volume_object(ref_vol_name, + new_vol_name, + raise_not_exist=True) return None @log_debug_trace @@ -557,10 +574,27 @@ class PureBaseVolumeDriver(san.SanDriver): """ volume_info = self._validate_manage_existing_ref(existing_ref) - size = math.ceil(float(volume_info["size"]) / units.Gi) + size = int(math.ceil(float(volume_info["size"]) / units.Gi)) return size + def _rename_volume_object(self, old_name, new_name, raise_not_exist=False): + """Rename a volume object (could be snapshot) in Purity. + + This will not raise an exception if the object does not exist + """ + try: + self._array.rename_volume(old_name, new_name) + except purestorage.PureHTTPError as err: + with excutils.save_and_reraise_exception() as ctxt: + if (err.code == 400 and + ERR_MSG_NOT_EXIST in err.text): + ctxt.reraise = raise_not_exist + LOG.warning(_LW("Unable to rename %(old_name)s, error " + "message: %(error)s"), + {"old_name": old_name, "error": err.text}) + return new_name + @log_debug_trace def unmanage(self, volume): """Removes the specified volume from Cinder management. @@ -570,18 +604,67 @@ class PureBaseVolumeDriver(san.SanDriver): The volume will be renamed with "-unmanaged" as a suffix """ vol_name = self._get_vol_name(volume) - unmanaged_vol_name = vol_name + "-unmanaged" + unmanaged_vol_name = vol_name + UNMANAGED_SUFFIX LOG.info(_LI("Renaming existing volume %(ref_name)s to %(new_name)s"), {"ref_name": vol_name, "new_name": unmanaged_vol_name}) - try: - self._array.rename_volume(vol_name, unmanaged_vol_name) - except purestorage.PureHTTPError as err: - with excutils.save_and_reraise_exception() as ctxt: - if (err.code == 400 and - ERR_MSG_NOT_EXIST in err.text): - ctxt.reraise = False - LOG.warning(_LW("Volume unmanage was unable to rename " - "the volume, error message: %s"), err.text) + self._rename_volume_object(vol_name, unmanaged_vol_name) + + def _verify_manage_snap_api_requirements(self): + api_version = self._array.get_rest_version() + if api_version not in MANAGE_SNAP_REQUIRED_API_VERSIONS: + msg = _('Unable to do manage snapshot operations with Purity REST ' + 'API version %(api_version)s, requires ' + '%(required_versions)s.') % { + 'api_version': api_version, + 'required_versions': MANAGE_SNAP_REQUIRED_API_VERSIONS + } + raise exception.PureDriverException(reason=msg) + + def manage_existing_snapshot(self, snapshot, existing_ref): + """Brings an existing backend storage object under Cinder management. + + We expect a snapshot name in the existing_ref that matches one in + Purity. + """ + self._verify_manage_snap_api_requirements() + self._validate_manage_existing_ref(existing_ref, is_snap=True) + ref_snap_name = existing_ref['name'] + new_snap_name = self._get_snap_name(snapshot) + LOG.info(_LI("Renaming existing snapshot %(ref_name)s to " + "%(new_name)s"), {"ref_name": ref_snap_name, + "new_name": new_snap_name}) + self._rename_volume_object(ref_snap_name, + new_snap_name, + raise_not_exist=True) + return None + + def manage_existing_snapshot_get_size(self, snapshot, existing_ref): + """Return size of snapshot to be managed by manage_existing. + + We expect a snapshot name in the existing_ref that matches one in + Purity. + """ + self._verify_manage_snap_api_requirements() + snap_info = self._validate_manage_existing_ref(existing_ref, + is_snap=True) + size = int(math.ceil(float(snap_info["size"]) / units.Gi)) + return size + + def unmanage_snapshot(self, snapshot): + """Removes the specified snapshot from Cinder management. + + Does not delete the underlying backend storage object. + + We expect a snapshot name in the existing_ref that matches one in + Purity. + """ + self._verify_manage_snap_api_requirements() + snap_name = self._get_snap_name(snapshot) + unmanaged_snap_name = snap_name + UNMANAGED_SUFFIX + LOG.info(_LI("Renaming existing snapshot %(ref_name)s to " + "%(new_name)s"), {"ref_name": snap_name, + "new_name": unmanaged_snap_name}) + self._rename_volume_object(snap_name, unmanaged_snap_name) @staticmethod def _get_vol_name(volume):