]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: rbd: major refactoring for using the Task decorator
authorRicardo Dias <rdias@suse.com>
Wed, 11 Apr 2018 15:17:14 +0000 (16:17 +0100)
committerRicardo Dias <rdias@suse.com>
Fri, 13 Apr 2018 14:58:49 +0000 (15:58 +0100)
Signed-off-by: Ricardo Dias <rdias@suse.com>
qa/tasks/mgr/dashboard/helper.py
qa/tasks/mgr/dashboard/test_rbd.py
src/pybind/mgr/dashboard/controllers/rbd.py

index 5709c457d40e58de80e05c3bad3508ec310154eb..ecfd697cad5bc195eac39cf7d5d29d6498df1eea 100644 (file)
@@ -150,17 +150,18 @@ class DashboardTestCase(MgrTestCase):
 
     # pylint: disable=too-many-arguments
     @classmethod
-    def _task_request(cls, method, url, task_name, task_metadata, data, timeout):
+    def _task_request(cls, method, url, data, timeout):
         res = cls._request(url, method, data)
-        cls._assertIn(cls._resp.status_code, [200, 201, 204])
-        cls._assertIsInst(res, dict)
-        cls._assertIn('status', res)
-        cls._assertIn(res['status'], ['done', 'executing'])
-        if res['status'] == 'done':
-            cls._assertIn('value', res)
-            log.info("task (%s, %s) finished immediately", task_name,
-                     task_metadata)
-            return res['value']
+        cls._assertIn(cls._resp.status_code, [200, 201, 202, 204, 409])
+
+        if cls._resp.status_code != 202:
+            log.info("task finished immediately")
+            return res
+
+        cls._assertIn('name', res)
+        cls._assertIn('metadata', res)
+        task_name = res['name']
+        task_metadata = res['metadata']
 
         class Waiter(threading.Thread):
             def __init__(self, task_name, task_metadata):
@@ -196,25 +197,26 @@ class DashboardTestCase(MgrTestCase):
                             .format(task_name, task_metadata))
         log.info("task (%s, %s) finished", task_name, task_metadata)
         if thread.res_task['success']:
+            if method == 'POST':
+                cls._resp.status_code = 201
+            elif method == 'PUT':
+                cls._resp.status_code = 200
+            elif method == 'DELETE':
+                cls._resp.status_code = 204
             return thread.res_task['ret_value']
         raise Exception(thread.res_task['exception'])
 
     @classmethod
-    def _task_post(cls, url, task_name, task_metadata, data=None,
-                   timeout=60):
-        return cls._task_request('POST', url, task_name, task_metadata, data,
-                                 timeout)
+    def _task_post(cls, url, data=None, timeout=60):
+        return cls._task_request('POST', url, data, timeout)
 
     @classmethod
-    def _task_delete(cls, url, task_name, task_metadata, data=None,
-                     timeout=60):
-        return cls._task_request('DELETE', url, task_name, task_metadata, data,
-                                 timeout)
+    def _task_delete(cls, url, timeout=60):
+        return cls._task_request('DELETE', url, None, timeout)
 
     @classmethod
-    def _task_put(cls, url, task_name, task_metadata, data=None, timeout=60):
-        return cls._task_request('PUT', url, task_name, task_metadata, data,
-                                 timeout)
+    def _task_put(cls, url, data=None, timeout=60):
+        return cls._task_request('PUT', url, data, timeout)
 
     @classmethod
     def cookies(cls):
index 33342de994933fa571e4b28b178b5171731a2e8c..24095c06d510cf288335a706a0cd37c0d412bfa0 100644 (file)
@@ -29,42 +29,30 @@ class RbdTest(DashboardTestCase):
     def create_image(cls, pool, name, size, **kwargs):
         data = {'name': name, 'pool_name': pool, 'size': size}
         data.update(kwargs)
-        return cls._task_post('/api/rbd', 'rbd/create',
-                              {'pool_name': pool, 'image_name': name}, data)
+        return cls._task_post('/api/rbd', data)
 
     @classmethod
     def remove_image(cls, pool, image):
-        return cls._task_delete('/api/rbd/{}/{}'.format(pool, image),
-                                'rbd/delete',
-                                {'pool_name': pool, 'image_name': image})
+        return cls._task_delete('/api/rbd/{}/{}'.format(pool, image))
 
     # pylint: disable=too-many-arguments
     @classmethod
     def edit_image(cls, pool, image, name=None, size=None, features=None):
-        return cls._task_put('/api/rbd/{}/{}'.format(pool, image), 'rbd/edit',
-                             {'pool_name': pool, 'image_name': image},
+        return cls._task_put('/api/rbd/{}/{}'.format(pool, image),
                              {'name': name, 'size': size, 'features': features})
 
     @classmethod
     def create_snapshot(cls, pool, image, snapshot):
-        data = {'pool_name': pool, 'image_name': image, 'snapshot_name': snapshot}
         return cls._task_post('/api/rbd/{}/{}/snap'.format(pool, image),
-                              'rbd/snap/create', data,
                               {'snapshot_name': snapshot})
 
     @classmethod
     def remove_snapshot(cls, pool, image, snapshot):
-        return cls._task_delete('/api/rbd//{}/{}/snap/{}'.format(pool, image, snapshot),
-                                'rbd/snap/delete',
-                                {'pool_name': pool, 'image_name': image,
-                                 'snapshot_name': snapshot})
+        return cls._task_delete('/api/rbd//{}/{}/snap/{}'.format(pool, image, snapshot))
 
     @classmethod
     def update_snapshot(cls, pool, image, snapshot, new_name, is_protected):
         return cls._task_put('/api/rbd//{}/{}/snap/{}'.format(pool, image, snapshot),
-                             'rbd/snap/edit',
-                             {'pool_name': pool, 'image_name': image,
-                              'snapshot_name': snapshot},
                              {'new_snap_name': new_name, 'is_protected': is_protected})
 
     @classmethod
@@ -198,8 +186,8 @@ class RbdTest(DashboardTestCase):
 
     def test_create(self):
         rbd_name = 'test_rbd'
-        res = self.create_image('rbd', rbd_name, 10240)
-        self.assertEqual(res, {"success": True})
+        self.create_image('rbd', rbd_name, 10240)
+        self.assertStatus(201)
 
         img = self._get('/api/rbd/rbd/test_rbd')
         self.assertStatus(200)
@@ -220,8 +208,8 @@ class RbdTest(DashboardTestCase):
         self.create_pool('data_pool', 12, 'erasure')
 
         rbd_name = 'test_rbd_in_data_pool'
-        res = self.create_image('rbd', rbd_name, 10240, data_pool='data_pool')
-        self.assertEqual(res, {"success": True})
+        self.create_image('rbd', rbd_name, 10240, data_pool='data_pool')
+        self.assertStatus(201)
 
         img = self._get('/api/rbd/rbd/test_rbd_in_data_pool')
         self.assertStatus(200)
@@ -235,6 +223,7 @@ class RbdTest(DashboardTestCase):
                                             'object-map'])
 
         self.remove_image('rbd', rbd_name)
+        self.assertStatus(204)
         self._ceph_cmd(['osd', 'pool', 'delete', 'data_pool', 'data_pool',
                         '--yes-i-really-really-mean-it'])
 
@@ -242,9 +231,10 @@ class RbdTest(DashboardTestCase):
         res = self.create_image('rbd', 'test_rbd_twice', 10240)
 
         res = self.create_image('rbd', 'test_rbd_twice', 10240)
-        self.assertEqual(res, {"success": False, "errno": 17,
+        self.assertEqual(res, {"errno": 17,
                                "detail": "[errno 17] error creating image"})
         self.remove_image('rbd', 'test_rbd_twice')
+        self.assertStatus(204)
 
     def test_snapshots_and_clone_info(self):
         self.create_snapshot('rbd', 'img1', 'snap1')
@@ -279,6 +269,7 @@ class RbdTest(DashboardTestCase):
                                             'fast-diff', 'layering',
                                             'object-map'])
         self.remove_image('rbd_iscsi', 'img1_clone')
+        self.assertStatus(204)
 
     def test_disk_usage(self):
         self._rbd_cmd(['bench', '--io-type', 'write', '--io-total', '50M', 'rbd/img2'])
@@ -295,105 +286,105 @@ class RbdTest(DashboardTestCase):
 
     def test_delete_non_existent_image(self):
         res = self.remove_image('rbd', 'i_dont_exist')
-        self.assertEqual(res, {"success": False, "errno": 2,
+        self.assertEqual(res, {"errno": 2,
                                "detail": "[errno 2] error removing image"})
 
     def test_image_delete(self):
-        res = self.create_image('rbd', 'delete_me', 2**30)
-        self.assertTrue(res['success'])
-        res = self.create_snapshot('rbd', 'delete_me', 'snap1')
-        self.assertTrue(res['success'])
-        res = self.create_snapshot('rbd', 'delete_me', 'snap2')
-        self.assertTrue(res['success'])
+        self.create_image('rbd', 'delete_me', 2**30)
+        self.assertStatus(201)
+        self.create_snapshot('rbd', 'delete_me', 'snap1')
+        self.assertStatus(201)
+        self.create_snapshot('rbd', 'delete_me', 'snap2')
+        self.assertStatus(201)
 
         img = self._get('/api/rbd/rbd/delete_me')
         self.assertStatus(200)
         self._validate_image(img, name='delete_me', size=2**30)
         self.assertEqual(len(img['snapshots']), 2)
 
-        res = self.remove_snapshot('rbd', 'delete_me', 'snap1')
-        self.assertTrue(res['success'])
-        res = self.remove_snapshot('rbd', 'delete_me', 'snap2')
-        self.assertTrue(res['success'])
+        self.remove_snapshot('rbd', 'delete_me', 'snap1')
+        self.assertStatus(204)
+        self.remove_snapshot('rbd', 'delete_me', 'snap2')
+        self.assertStatus(204)
 
         img = self._get('/api/rbd/rbd/delete_me')
         self.assertStatus(200)
         self._validate_image(img, name='delete_me', size=2**30)
         self.assertEqual(len(img['snapshots']), 0)
 
-        res = self.remove_image('rbd', 'delete_me')
-        self.assertTrue(res['success'])
+        self.remove_image('rbd', 'delete_me')
+        self.assertStatus(204)
 
     def test_image_rename(self):
-        res = self.create_image('rbd', 'edit_img', 2**30)
-        self.assertTrue(res['success'])
+        self.create_image('rbd', 'edit_img', 2**30)
+        self.assertStatus(201)
         self._get('/api/rbd/rbd/edit_img')
         self.assertStatus(200)
-        res = self.edit_image('rbd', 'edit_img', 'new_edit_img')
-        self.assertTrue(res['success'])
+        self.edit_image('rbd', 'edit_img', 'new_edit_img')
+        self.assertStatus(200)
         self._get('/api/rbd/rbd/edit_img')
         self.assertStatus(404)
         self._get('/api/rbd/rbd/new_edit_img')
         self.assertStatus(200)
-        res = self.remove_image('rbd', 'new_edit_img')
-        self.assertTrue(res['success'])
+        self.remove_image('rbd', 'new_edit_img')
+        self.assertStatus(204)
 
     def test_image_resize(self):
-        res = self.create_image('rbd', 'edit_img', 2**30)
-        self.assertTrue(res['success'])
+        self.create_image('rbd', 'edit_img', 2**30)
+        self.assertStatus(201)
         img = self._get('/api/rbd/rbd/edit_img')
         self.assertStatus(200)
         self._validate_image(img, size=2**30)
-        res = self.edit_image('rbd', 'edit_img', size=2*2**30)
-        self.assertTrue(res['success'])
+        self.edit_image('rbd', 'edit_img', size=2*2**30)
+        self.assertStatus(200)
         img = self._get('/api/rbd/rbd/edit_img')
         self.assertStatus(200)
         self._validate_image(img, size=2*2**30)
-        res = self.remove_image('rbd', 'edit_img')
-        self.assertTrue(res['success'])
+        self.remove_image('rbd', 'edit_img')
+        self.assertStatus(204)
 
     def test_image_change_features(self):
-        res = self.create_image('rbd', 'edit_img', 2**30, features=["layering"])
-        self.assertTrue(res['success'])
+        self.create_image('rbd', 'edit_img', 2**30, features=["layering"])
+        self.assertStatus(201)
         img = self._get('/api/rbd/rbd/edit_img')
         self.assertStatus(200)
         self._validate_image(img, features_name=["layering"])
-        res = self.edit_image('rbd', 'edit_img',
-                              features=["fast-diff", "object-map", "exclusive-lock"])
+        self.edit_image('rbd', 'edit_img',
+                        features=["fast-diff", "object-map", "exclusive-lock"])
         img = self._get('/api/rbd/rbd/edit_img')
         self.assertStatus(200)
         self._validate_image(img, features_name=['exclusive-lock',
                                                  'fast-diff', 'layering',
                                                  'object-map'])
-        res = self.edit_image('rbd', 'edit_img',
-                              features=["journaling", "exclusive-lock"])
+        self.edit_image('rbd', 'edit_img',
+                        features=["journaling", "exclusive-lock"])
         img = self._get('/api/rbd/rbd/edit_img')
         self.assertStatus(200)
         self._validate_image(img, features_name=['exclusive-lock',
                                                  'journaling', 'layering'])
-        res = self.remove_image('rbd', 'edit_img')
-        self.assertTrue(res['success'])
+        self.remove_image('rbd', 'edit_img')
+        self.assertStatus(204)
 
     def test_update_snapshot(self):
-        res = self.create_snapshot('rbd', 'img1', 'snap5')
-        self.assertTrue(res['success'])
+        self.create_snapshot('rbd', 'img1', 'snap5')
+        self.assertStatus(201)
         img = self._get('/api/rbd/rbd/img1')
         self._validate_snapshot_list(img['snapshots'], 'snap5', is_protected=False)
 
-        res = self.update_snapshot('rbd', 'img1', 'snap5', 'snap6', None)
-        self.assertTrue(res['success'])
+        self.update_snapshot('rbd', 'img1', 'snap5', 'snap6', None)
+        self.assertStatus(200)
         img = self._get('/api/rbd/rbd/img1')
         self._validate_snapshot_list(img['snapshots'], 'snap6', is_protected=False)
 
-        res = self.update_snapshot('rbd', 'img1', 'snap6', None, True)
-        self.assertTrue(res['success'])
+        self.update_snapshot('rbd', 'img1', 'snap6', None, True)
+        self.assertStatus(200)
         img = self._get('/api/rbd/rbd/img1')
         self._validate_snapshot_list(img['snapshots'], 'snap6', is_protected=True)
 
-        res = self.update_snapshot('rbd', 'img1', 'snap6', 'snap5', False)
-        self.assertTrue(res['success'])
+        self.update_snapshot('rbd', 'img1', 'snap6', 'snap5', False)
+        self.assertStatus(200)
         img = self._get('/api/rbd/rbd/img1')
         self._validate_snapshot_list(img['snapshots'], 'snap5', is_protected=False)
 
-        res = self.remove_snapshot('rbd', 'img1', 'snap5')
-        self.assertTrue(res['success'])
+        self.remove_snapshot('rbd', 'img1', 'snap5')
+        self.assertStatus(204)
index f082b980f05ca21c60d3649279d34e77cf0c76c1..2da342308fc34ace56f99bf445e9243ec8aa54d3 100644 (file)
@@ -1,15 +1,41 @@
 # -*- coding: utf-8 -*-
-# pylint: disable=too-many-arguments,too-many-locals
+# pylint: disable=too-many-arguments,too-many-locals,unused-argument
 from __future__ import absolute_import
 
 import math
 import cherrypy
 import rbd
 
-from . import ApiController, AuthRequired, RESTController
+from . import ApiController, AuthRequired, RESTController, Task
 from .. import mgr
 from ..services.ceph_service import CephService
-from ..tools import ViewCache, TaskManager
+from ..tools import ViewCache
+
+
+# pylint: disable=inconsistent-return-statements
+def _rbd_exception_handler(ex):
+    if isinstance(ex, rbd.OSError):
+        cherrypy.response.status = 409
+        return {'detail': str(ex), 'errno': ex.errno}
+    raise ex
+
+
+def RbdTask(name, metadata, wait_for):
+    return Task("rbd/{}".format(name), metadata, wait_for,
+                _rbd_exception_handler)
+
+
+def _rbd_call(pool_name, func, *args, **kwargs):
+    with mgr.rados.open_ioctx(pool_name) as ioctx:
+        func(ioctx, *args, **kwargs)
+
+
+def _rbd_image_call(pool_name, image_name, func, *args, **kwargs):
+    def _ioctx_func(ioctx, image_name, func, *args, **kwargs):
+        with rbd.Image(ioctx, image_name) as img:
+            func(ioctx, img, *args, **kwargs)
+
+    return _rbd_call(pool_name, _ioctx_func, image_name, func, *args, **kwargs)
 
 
 @ApiController('rbd')
@@ -71,6 +97,25 @@ class Rbd(RESTController):
                 res = key | res
         return res
 
+    @classmethod
+    def _sort_features(cls, features, enable=True):
+        """
+        Sorts image features according to feature dependencies:
+
+        object-map depends on exclusive-lock
+        journaling depends on exclusive-lock
+        fast-diff depends on object-map
+        """
+        ORDER = ['exclusive-lock', 'journaling', 'object-map', 'fast-diff']
+
+        def key_func(feat):
+            try:
+                return ORDER.index(feat)
+            except ValueError:
+                return id(feat)
+
+        features.sort(key=key_func, reverse=not enable)
+
     @classmethod
     def _rbd_disk_usage(cls, image, snaps):
         class DUCallback(object):
@@ -204,176 +249,93 @@ class Rbd(RESTController):
         except rbd.ImageNotFound:
             raise cherrypy.HTTPError(404)
 
-    @classmethod
-    def _create_image(cls, name, pool_name, size, obj_size=None, features=None,
-                      stripe_unit=None, stripe_count=None, data_pool=None):
-        # pylint: disable=too-many-locals
-        rbd_inst = rbd.RBD()
-
-        # Set order
-        order = None
-        if obj_size and obj_size > 0:
-            order = int(round(math.log(float(obj_size), 2)))
-
-        # Set features
-        feature_bitmask = cls._format_features(features)
-
-        ioctx = mgr.rados.open_ioctx(pool_name)
-
-        try:
-            rbd_inst.create(ioctx, name, size, order=order, old_format=False,
-                            features=feature_bitmask, stripe_unit=stripe_unit,
-                            stripe_count=stripe_count, data_pool=data_pool)
-        except rbd.OSError as e:
-            return {'success': False, 'detail': str(e), 'errno': e.errno}
-        return {'success': True}
-
+    @RbdTask('create',
+             {'pool_name': '{pool_name}', 'image_name': '{name}'}, 2.0)
     @RESTController.args_from_json
     def create(self, name, pool_name, size, obj_size=None, features=None,
                stripe_unit=None, stripe_count=None, data_pool=None):
-        task = TaskManager.run('rbd/create',
-                               {'pool_name': pool_name, 'image_name': name},
-                               self._create_image,
-                               [name, pool_name, size, obj_size, features,
-                                stripe_unit, stripe_count, data_pool])
-        status, value = task.wait(1.0)
-        return {'status': status, 'value': value}
-
-    @classmethod
-    def _remove_image(cls, pool_name, image_name):
-        rbd_inst = rbd.RBD()
-        ioctx = mgr.rados.open_ioctx(pool_name)
-        try:
-            rbd_inst.remove(ioctx, image_name)
-        except rbd.OSError as e:
-            return {'success': False, 'detail': str(e), 'errno': e.errno}
-        return {'success': True}
-
-    def delete(self, pool_name, image_name):
-        task = TaskManager.run('rbd/delete',
-                               {'pool_name': pool_name, 'image_name': image_name},
-                               self._remove_image, [pool_name, image_name])
-        status, value = task.wait(2.0)
-        cherrypy.response.status = 200
-        return {'status': status, 'value': value}
+        def _create(ioctx):
+            rbd_inst = rbd.RBD()
 
-    @classmethod
-    def _sort_features(cls, features, enable=True):
-        """
-        Sorts image features according to feature dependencies:
+            # Set order
+            l_order = None
+            if obj_size and obj_size > 0:
+                l_order = int(round(math.log(float(obj_size), 2)))
 
-        object-map depends on exclusive-lock
-        journaling depends on exclusive-lock
-        fast-diff depends on object-map
-        """
-        ORDER = ['exclusive-lock', 'journaling', 'object-map', 'fast-diff']
+            # Set features
+            feature_bitmask = self._format_features(features)
 
-        def key_func(feat):
-            try:
-                return ORDER.index(feat)
-            except ValueError:
-                return id(feat)
+            rbd_inst.create(ioctx, name, size, order=l_order, old_format=False,
+                            features=feature_bitmask, stripe_unit=stripe_unit,
+                            stripe_count=stripe_count, data_pool=data_pool)
 
-        features.sort(key=key_func, reverse=not enable)
+        return _rbd_call(pool_name, _create)
 
-    @classmethod
-    def _edit_image(cls, pool_name, image_name, name, size, features):
+    @RbdTask('delete', ['{pool_name}', '{image_name}'], 2.0)
+    def delete(self, pool_name, image_name):
         rbd_inst = rbd.RBD()
-        ioctx = mgr.rados.open_ioctx(pool_name)
-        image = rbd.Image(ioctx, image_name)
+        return _rbd_call(pool_name, rbd_inst.remove, image_name)
 
-        # check rename image
-        if name and name != image_name:
-            try:
+    @RbdTask('edit', ['{pool_name}', '{image_name}'], 4.0)
+    @RESTController.args_from_json
+    def set(self, pool_name, image_name, name=None, size=None, features=None):
+        def _edit(ioctx, image):
+            rbd_inst = rbd.RBD()
+            # check rename image
+            if name and name != image_name:
                 rbd_inst.rename(ioctx, image_name, name)
-            except rbd.OSError as e:
-                return {'success': False, 'detail': str(e), 'errno': e.errno}
 
-        # check resize
-        if size and size != image.size():
-            try:
+            # check resize
+            if size and size != image.size():
                 image.resize(size)
-            except rbd.OSError as e:
-                return {'success': False, 'detail': str(e), 'errno': e.errno}
-
-        # check enable/disable features
-        if features is not None:
-            curr_features = cls._format_bitmask(image.features())
-            # check disabled features
-            cls._sort_features(curr_features, enable=False)
-            for feature in curr_features:
-                if feature not in features and feature in cls.ALLOW_DISABLE_FEATURES:
-                    f_bitmask = cls._format_features([feature])
-                    image.update_features(f_bitmask, False)
-            # check enabled features
-            cls._sort_features(features)
-            for feature in features:
-                if feature not in curr_features and feature in cls.ALLOW_ENABLE_FEATURES:
-                    f_bitmask = cls._format_features([feature])
-                    image.update_features(f_bitmask, True)
-
-        return {'success': True}
 
-    @RESTController.args_from_json
-    def set(self, pool_name, image_name, name=None, size=None, features=None):
-        task = TaskManager.run('rbd/edit',
-                               {'pool_name': pool_name, 'image_name': image_name},
-                               self._edit_image,
-                               [pool_name, image_name, name, size, features])
-        status, value = task.wait(4.0)
-        return {'status': status, 'value': value}
+            # check enable/disable features
+            if features is not None:
+                curr_features = self._format_bitmask(image.features())
+                # check disabled features
+                self._sort_features(curr_features, enable=False)
+                for feature in curr_features:
+                    if feature not in features and feature in self.ALLOW_DISABLE_FEATURES:
+                        f_bitmask = self._format_features([feature])
+                        image.update_features(f_bitmask, False)
+                # check enabled features
+                self._sort_features(features)
+                for feature in features:
+                    if feature not in curr_features and feature in self.ALLOW_ENABLE_FEATURES:
+                        f_bitmask = self._format_features([feature])
+                        image.update_features(f_bitmask, True)
+
+        return _rbd_image_call(pool_name, image_name, _edit)
 
 
 @ApiController('rbd/:pool_name/:image_name/snap')
 class RbdSnapshot(RESTController):
 
-    @classmethod
-    def _create_snapshot(cls, pool_name, image_name, snapshot_name):
-        ioctx = mgr.rados.open_ioctx(pool_name)
-        img = rbd.Image(ioctx, image_name)
-        try:
-            img.create_snap(snapshot_name)
-        except rbd.OSError as e:
-            return {'success': False, 'detail': str(e), 'errno': e.errno}
-        return {'success': True}
-
+    @RbdTask('snap/create',
+             ['{pool_name}', '{image_name}', '{snapshot_name}'], 2.0)
     @RESTController.args_from_json
     def create(self, pool_name, image_name, snapshot_name):
-        task = TaskManager.run('rbd/snap/create',
-                               {'pool_name': pool_name, 'image_name': image_name,
-                                'snapshot_name': snapshot_name},
-                               self._create_snapshot,
-                               [pool_name, image_name, snapshot_name])
-        status, value = task.wait(1.0)
-        return {'status': status, 'value': value}
+        def _create_snapshot(ioctx, img, snapshot_name):
+            img.create_snap(snapshot_name)
 
-    @classmethod
-    def _remove_snapshot(cls, pool_name, image_name, snapshot_name):
-        ioctx = mgr.rados.open_ioctx(pool_name)
-        img = rbd.Image(ioctx, image_name)
-        try:
-            img.remove_snap(snapshot_name)
-        except rbd.OSError as e:
-            return {'success': False, 'detail': str(e), 'errno': e.errno}
-        return {'success': True}
+        return _rbd_image_call(pool_name, image_name, _create_snapshot,
+                               snapshot_name)
 
+    @RbdTask('snap/delete',
+             ['{pool_name}', '{image_name}', '{snapshot_name}'], 2.0)
     def delete(self, pool_name, image_name, snapshot_name):
-        task = TaskManager.run('rbd/snap/delete',
-                               {'pool_name': pool_name,
-                                'image_name': image_name,
-                                'snapshot_name': snapshot_name},
-                               self._remove_snapshot,
-                               [pool_name, image_name, snapshot_name])
-        status, value = task.wait(1.0)
-        cherrypy.response.status = 200
-        return {'status': status, 'value': value}
+        def _remove_snapshot(ioctx, img, snapshot_name):
+            img.remove_snap(snapshot_name)
 
-    @classmethod
-    def _edit_snapshot(cls, pool_name, image_name, snapshot_name,
-                       new_snap_name, is_protected):
-        ioctx = mgr.rados.open_ioctx(pool_name)
-        img = rbd.Image(ioctx, image_name)
-        try:
+        return _rbd_image_call(pool_name, image_name, _remove_snapshot,
+                               snapshot_name)
+
+    @RbdTask('snap/edit',
+             ['{pool_name}', '{image_name}', '{snapshot_name}'], 4.0)
+    @RESTController.args_from_json
+    def set(self, pool_name, image_name, snapshot_name, new_snap_name=None,
+            is_protected=None):
+        def _edit(ioctx, img, snapshot_name):
             if new_snap_name and new_snap_name != snapshot_name:
                 img.rename_snap(snapshot_name, new_snap_name)
                 snapshot_name = new_snap_name
@@ -383,19 +345,5 @@ class RbdSnapshot(RESTController):
                     img.protect_snap(snapshot_name)
                 else:
                     img.unprotect_snap(snapshot_name)
-        except rbd.OSError as e:
-            return {'success': False, 'detail': str(e), 'errno': e.errno}
-        return {'success': True}
 
-    @RESTController.args_from_json
-    def set(self, pool_name, image_name, snapshot_name, new_snap_name=None,
-            is_protected=None):
-        task = TaskManager.run('rbd/snap/edit',
-                               {'pool_name': pool_name,
-                                'image_name': image_name,
-                                'snapshot_name': snapshot_name},
-                               self._edit_snapshot,
-                               [pool_name, image_name, snapshot_name,
-                                new_snap_name, is_protected])
-        status, value = task.wait(1.0)
-        return {'status': status, 'value': value}
+        return _rbd_image_call(pool_name, image_name, _edit, snapshot_name)