]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Implement manage/unmanage snapshot in Pure drivers
authorPatrick East <patrick.east@purestorage.com>
Wed, 12 Aug 2015 22:37:50 +0000 (15:37 -0700)
committerPatrick East <patrick.east@purestorage.com>
Wed, 26 Aug 2015 19:31:44 +0000 (12:31 -0700)
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

cinder/tests/unit/test_pure.py
cinder/volume/drivers/pure.py

index 50162c155c38a1edaca415738b1a4c6055253870..39b2c677b08bd7120a96364ac301e48a18db9ddc 100644 (file)
@@ -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)
index b73a053a72e2ec067f36e3b4dfeddee2c012da73..902c3a8e8e1d96fe9e7f3c7179db2df4307ed76d 100644 (file)
@@ -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):