# 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):
.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):
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
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)
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)
'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'])
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')
'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'])
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)
# -*- 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')
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):
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
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)