from __future__ import absolute_import
+import time
+
from .helper import DashboardTestCase, JObj, JLeaf, JList
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:
'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)
import math
from functools import partial
+from datetime import datetime
import cherrypy
import six
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
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):
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)))