]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add RBD Trash endpoints
authorTiago Melo <tmelo@suse.com>
Tue, 19 Jun 2018 13:33:41 +0000 (14:33 +0100)
committerTiago Melo <tspmelo@gmail.com>
Tue, 25 Sep 2018 13:02:58 +0000 (14:02 +0100)
Fixes: http://tracker.ceph.com/issues/24272
Signed-off-by: Tiago Melo <tmelo@suse.com>
qa/tasks/mgr/dashboard/test_rbd.py
src/pybind/mgr/dashboard/controllers/rbd.py

index c240d0609eaa5ddad2ff3514c9461816489ed8a0..db637ca6254790ccfe8790c42006420195f927f6 100644 (file)
@@ -3,6 +3,8 @@
 
 from __future__ import absolute_import
 
+import time
+
 from .helper import DashboardTestCase, JObj, JLeaf, JList
 
 
@@ -136,6 +138,31 @@ class RbdTest(DashboardTestCase):
         cls._ceph_cmd(['osd', 'pool', 'delete', 'rbd_data', 'rbd_data',
                        '--yes-i-really-really-mean-it'])
 
+    @classmethod
+    def create_image_in_trash(cls, pool, name, delay=0, **kwargs):
+        cls.create_image(pool, name, 10240)
+        img = cls._get('/api/block/image/{}/{}'.format(pool, name))
+
+        cls._task_post("/api/block/image/{}/{}/move_trash".format(pool, name),
+                        {'delay': delay})
+
+        return img['id']
+
+    @classmethod
+    def remove_trash(cls, pool, image_id, image_name, force=False):
+        return cls._task_delete('/api/block/image/trash/{}/{}/?image_name={}&force={}'.format('rbd', image_id, image_name, force))
+
+    @classmethod
+    def get_trash(cls, pool, image_id):
+        trash = cls._get('/api/block/image/trash/?pool_name={}'.format(pool))
+        if isinstance(trash, list):
+            for pool in trash:
+                for image in pool['value']:
+                    if image['id'] == image_id:
+                        return image
+
+        return None
+
     def _validate_image(self, img, **kwargs):
         """
         Example of an RBD image json:
@@ -599,3 +626,83 @@ class RbdTest(DashboardTestCase):
                                             'object-map'])
 
         self.remove_image('rbd', rbd_name_encoded)
+
+    def test_move_image_to_trash(self):
+        id = self.create_image_in_trash('rbd', 'test_rbd')
+        self.assertStatus(200)
+
+        self._get('/api/block/image/rbd/test_rbd')
+        self.assertStatus(404)
+
+        time.sleep(1)
+
+        image = self.get_trash('rbd', id)
+        self.assertIsNotNone(image)
+
+        self.remove_trash('rbd', id, 'test_rbd')
+
+    def test_list_trash(self):
+        id = self.create_image_in_trash('rbd', 'test_rbd', 0)
+        data = self._get('/api/block/image/trash/?pool_name={}'.format('rbd'))
+        self.assertStatus(200)
+        self.assertIsInstance(data, list)
+        self.assertIsNotNone(data)
+
+        self.remove_trash('rbd', id, 'test_rbd')
+        self.assertStatus(204)
+
+    def test_restore_trash(self):
+        id = self.create_image_in_trash('rbd', 'test_rbd')
+
+        self._task_post('/api/block/image/trash/{}/{}/restore'.format('rbd', id), {'new_image_name': 'test_rbd'})
+
+        self._get('/api/block/image/rbd/test_rbd')
+        self.assertStatus(200)
+
+        image = self.get_trash('rbd', id)
+        self.assertIsNone(image)
+
+        self.remove_image('rbd', 'test_rbd')
+
+    def test_remove_expired_trash(self):
+        id = self.create_image_in_trash('rbd', 'test_rbd', 0)
+        self.remove_trash('rbd', id, 'test_rbd', False)
+        self.assertStatus(204)
+
+        image = self.get_trash('rbd', id)
+        self.assertIsNone(image)
+
+    def test_remove_not_expired_trash(self):
+        id = self.create_image_in_trash('rbd', 'test_rbd', 9999)
+        self.remove_trash('rbd', id, 'test_rbd', False)
+        self.assertStatus(400)
+
+        image = self.get_trash('rbd', id)
+        self.assertIsNotNone(image)
+
+        self.remove_trash('rbd', id, 'test_rbd', True)
+
+    def test_remove_not_expired_trash_with_force(self):
+        id = self.create_image_in_trash('rbd', 'test_rbd', 9999)
+        self.remove_trash('rbd', id, 'test_rbd', True)
+        self.assertStatus(204)
+
+        image = self.get_trash('rbd', id)
+        self.assertIsNone(image)
+
+    def test_purge_trash(self):
+        id_expired = self.create_image_in_trash('rbd', 'test_rbd_expired', 0)
+        id_not_expired = self.create_image_in_trash('rbd', 'test_rbd', 9999)
+
+        time.sleep(1)
+
+        self._task_post('/api/block/image/trash/purge?pool_name={}'.format('rbd'))
+        self.assertStatus(200)
+
+        time.sleep(1)
+
+        trash_not_expired = self.get_trash('rbd', id_not_expired)
+        self.assertIsNotNone(trash_not_expired)
+
+        trash_expired = self.get_trash('rbd', id_expired)
+        self.assertIsNone(trash_expired)
index d9b217fd6f75bf2acab2ba41d42e24610dcdb749..a8f4d452a45ecacb7f07c1ceef2e4e7b796798d1 100644 (file)
@@ -5,6 +5,7 @@ from __future__ import absolute_import
 
 import math
 from functools import partial
+from datetime import datetime
 
 import cherrypy
 import six
@@ -15,7 +16,7 @@ from . import ApiController, RESTController, Task, UpdatePermission
 from .. import mgr
 from ..security import Scope
 from ..services.ceph_service import CephService
-from ..tools import ViewCache
+from ..tools import ViewCache, str_to_bool
 from ..services.exception import handle_rados_error, handle_rbd_error, \
     serialize_dashboard_exception
 
@@ -374,6 +375,16 @@ class Rbd(RESTController):
         rbd_default_features = mgr.get('config')['rbd_default_features']
         return _format_bitmask(int(rbd_default_features))
 
+    @RbdTask('trash/move', ['{pool_name}', '{image_name}'], 2.0)
+    @RESTController.Resource('POST')
+    def move_trash(self, pool_name, image_name, delay=0):
+        """Move an image to the trash.
+        Images, even ones actively in-use by clones,
+        can be moved to the trash and deleted at a later time.
+        """
+        rbd_inst = rbd.RBD()
+        return _rbd_call(pool_name, rbd_inst.trash_move, image_name, delay)
+
 
 @ApiController('/block/image/{pool_name}/{image_name}/snap', Scope.RBD_IMAGE)
 class RbdSnapshot(RESTController):
@@ -453,3 +464,68 @@ class RbdSnapshot(RESTController):
             return _rbd_call(child_pool_name, _clone)
 
         return _rbd_call(pool_name, _parent_clone)
+
+
+@ApiController('/block/image/trash')
+class RbdTrash(RESTController):
+    RESOURCE_ID = "pool_name/image_id"
+    rbd_inst = rbd.RBD()
+
+    @ViewCache()
+    def _trash_pool_list(self, pool_name):
+        with mgr.rados.open_ioctx(pool_name) as ioctx:
+            images = self.rbd_inst.trash_list(ioctx)
+            result = []
+            for trash in images:
+                trash['pool_name'] = pool_name
+                trash['deletion_time'] = "{}Z".format(trash['deletion_time'].isoformat())
+                trash['deferment_end_time'] = "{}Z".format(trash['deferment_end_time'].isoformat())
+                result.append(trash)
+            return result
+
+    def _trash_list(self, pool_name=None):
+        if pool_name:
+            pools = [pool_name]
+        else:
+            pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
+
+        result = []
+        for pool in pools:
+            # pylint: disable=unbalanced-tuple-unpacking
+            status, value = self._trash_pool_list(pool)
+            result.append({'status': status, 'value': value, 'pool_name': pool})
+        return result
+
+    @handle_rbd_error()
+    @handle_rados_error('pool')
+    def list(self, pool_name=None):
+        """List all entries from trash."""
+        return self._trash_list(pool_name)
+
+    @handle_rbd_error()
+    @handle_rados_error('pool')
+    @RbdTask('trash/purge', ['{pool_name}'], 2.0)
+    @RESTController.Collection('POST', query_params=['pool_name'])
+    def purge(self, pool_name=None):
+        """Remove all expired images from trash."""
+        now = "{}Z".format(datetime.now().isoformat())
+        pools = self._trash_list(pool_name)
+
+        for pool in pools:
+            for image in pool['value']:
+                if image['deferment_end_time'] < now:
+                    _rbd_call(pool['pool_name'], self.rbd_inst.trash_remove, image['id'], 0)
+
+    @RbdTask('trash/restore', ['{pool_name}', '{image_id}', '{new_image_name}'], 2.0)
+    @RESTController.Resource('POST')
+    def restore(self, pool_name, image_id, new_image_name):
+        """Restore an image from trash."""
+        return _rbd_call(pool_name, self.rbd_inst.trash_restore, image_id, new_image_name)
+
+    @RbdTask('trash/remove', ['{pool_name}', '{image_id}', '{image_name}'], 2.0)
+    def delete(self, pool_name, image_id, image_name, force=False):
+        """Delete an image from trash.
+        If image deferment time has not expired you can not removed it unless use force.
+        But an actively in-use by clones or has snapshots can not be removed.
+        """
+        return _rbd_call(pool_name, self.rbd_inst.trash_remove, image_id, int(str_to_bool(force)))