1 # -*- coding: utf-8 -*-
2 # pylint: disable=unused-argument
3 # pylint: disable=too-many-statements,too-many-branches
4 from __future__
import absolute_import
7 from functools
import partial
8 from datetime
import datetime
12 from . import ApiController
, RESTController
, Task
, UpdatePermission
, \
13 DeletePermission
, CreatePermission
15 from ..exceptions
import DashboardException
16 from ..security
import Scope
17 from ..services
.ceph_service
import CephService
18 from ..services
.rbd
import RbdConfiguration
, RbdService
, RbdSnapshotService
, \
19 format_bitmask
, format_features
, parse_image_spec
, rbd_call
, rbd_image_call
20 from ..tools
import ViewCache
, str_to_bool
21 from ..services
.exception
import handle_rados_error
, handle_rbd_error
, \
22 serialize_dashboard_exception
25 # pylint: disable=not-callable
26 def RbdTask(name
, metadata
, wait_for
): # noqa: N802
27 def composed_decorator(func
):
28 func
= handle_rados_error('pool')(func
)
29 func
= handle_rbd_error()(func
)
30 return Task("rbd/{}".format(name
), metadata
, wait_for
,
31 partial(serialize_dashboard_exception
, include_http_status
=True))(func
)
32 return composed_decorator
35 def _sort_features(features
, enable
=True):
37 Sorts image features according to feature dependencies:
39 object-map depends on exclusive-lock
40 journaling depends on exclusive-lock
41 fast-diff depends on object-map
43 ORDER
= ['exclusive-lock', 'journaling', 'object-map', 'fast-diff'] # noqa: N806
47 return ORDER
.index(feat
)
51 features
.sort(key
=key_func
, reverse
=not enable
)
54 @ApiController('/block/image', Scope
.RBD_IMAGE
)
55 class Rbd(RESTController
):
57 # set of image features that can be enable on existing images
58 ALLOW_ENABLE_FEATURES
= {"exclusive-lock", "object-map", "fast-diff", "journaling"}
60 # set of image features that can be disabled on existing images
61 ALLOW_DISABLE_FEATURES
= {"exclusive-lock", "object-map", "fast-diff", "deep-flatten",
64 def _rbd_list(self
, pool_name
=None):
68 pools
= [p
['pool_name'] for p
in CephService
.get_pool_list('rbd')]
72 # pylint: disable=unbalanced-tuple-unpacking
73 status
, value
= RbdService
.rbd_pool_list(pool
)
74 for i
, image
in enumerate(value
):
75 value
[i
]['configuration'] = RbdConfiguration(
76 pool
, image
['namespace'], image
['name']).list()
77 result
.append({'status': status
, 'value': value
, 'pool_name': pool
})
81 @handle_rados_error('pool')
82 def list(self
, pool_name
=None):
83 return self
._rbd
_list
(pool_name
)
86 @handle_rados_error('pool')
87 def get(self
, image_spec
):
88 return RbdService
.get_image(image_spec
)
91 {'pool_name': '{pool_name}', 'namespace': '{namespace}', 'image_name': '{name}'}, 2.0)
92 def create(self
, name
, pool_name
, size
, namespace
=None, obj_size
=None, features
=None,
93 stripe_unit
=None, stripe_count
=None, data_pool
=None, configuration
=None):
102 if obj_size
and obj_size
> 0:
103 l_order
= int(round(math
.log(float(obj_size
), 2)))
106 feature_bitmask
= format_features(features
)
108 rbd_inst
.create(ioctx
, name
, size
, order
=l_order
, old_format
=False,
109 features
=feature_bitmask
, stripe_unit
=stripe_unit
,
110 stripe_count
=stripe_count
, data_pool
=data_pool
)
111 RbdConfiguration(pool_ioctx
=ioctx
, namespace
=namespace
,
112 image_name
=name
).set_configuration(configuration
)
114 rbd_call(pool_name
, namespace
, _create
)
116 @RbdTask('delete', ['{image_spec}'], 2.0)
117 def delete(self
, image_spec
):
118 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
120 image
= RbdService
.get_image(image_spec
)
121 snapshots
= image
['snapshots']
122 for snap
in snapshots
:
123 RbdSnapshotService
.remove_snapshot(image_spec
, snap
['name'], snap
['is_protected'])
126 return rbd_call(pool_name
, namespace
, rbd_inst
.remove
, image_name
)
128 @RbdTask('edit', ['{image_spec}', '{name}'], 4.0)
129 def set(self
, image_spec
, name
=None, size
=None, features
=None, configuration
=None):
130 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
132 def _edit(ioctx
, image
):
135 if name
and name
!= image_name
:
136 rbd_inst
.rename(ioctx
, image_name
, name
)
139 if size
and size
!= image
.size():
142 # check enable/disable features
143 if features
is not None:
144 curr_features
= format_bitmask(image
.features())
145 # check disabled features
146 _sort_features(curr_features
, enable
=False)
147 for feature
in curr_features
:
148 if feature
not in features
and feature
in self
.ALLOW_DISABLE_FEATURES
:
149 if feature
not in format_bitmask(image
.features()):
151 f_bitmask
= format_features([feature
])
152 image
.update_features(f_bitmask
, False)
153 # check enabled features
154 _sort_features(features
)
155 for feature
in features
:
156 if feature
not in curr_features
and feature
in self
.ALLOW_ENABLE_FEATURES
:
157 if feature
in format_bitmask(image
.features()):
159 f_bitmask
= format_features([feature
])
160 image
.update_features(f_bitmask
, True)
162 RbdConfiguration(pool_ioctx
=ioctx
, image_name
=image_name
).set_configuration(
165 return rbd_image_call(pool_name
, namespace
, image_name
, _edit
)
168 {'src_image_spec': '{image_spec}',
169 'dest_pool_name': '{dest_pool_name}',
170 'dest_namespace': '{dest_namespace}',
171 'dest_image_name': '{dest_image_name}'}, 2.0)
172 @RESTController.Resource('POST')
173 def copy(self
, image_spec
, dest_pool_name
, dest_namespace
, dest_image_name
,
174 snapshot_name
=None, obj_size
=None, features
=None,
175 stripe_unit
=None, stripe_count
=None, data_pool
=None, configuration
=None):
176 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
178 def _src_copy(s_ioctx
, s_img
):
182 if obj_size
and obj_size
> 0:
183 l_order
= int(round(math
.log(float(obj_size
), 2)))
186 feature_bitmask
= format_features(features
)
189 s_img
.set_snap(snapshot_name
)
191 s_img
.copy(d_ioctx
, dest_image_name
, feature_bitmask
, l_order
,
192 stripe_unit
, stripe_count
, data_pool
)
193 RbdConfiguration(pool_ioctx
=d_ioctx
, image_name
=dest_image_name
).set_configuration(
196 return rbd_call(dest_pool_name
, dest_namespace
, _copy
)
198 return rbd_image_call(pool_name
, namespace
, image_name
, _src_copy
)
200 @RbdTask('flatten', ['{image_spec}'], 2.0)
201 @RESTController.Resource('POST')
203 def flatten(self
, image_spec
):
205 def _flatten(ioctx
, image
):
208 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
209 return rbd_image_call(pool_name
, namespace
, image_name
, _flatten
)
211 @RESTController.Collection('GET')
212 def default_features(self
):
213 rbd_default_features
= mgr
.get('config')['rbd_default_features']
214 return format_bitmask(int(rbd_default_features
))
216 @RbdTask('trash/move', ['{image_spec}'], 2.0)
217 @RESTController.Resource('POST')
218 def move_trash(self
, image_spec
, delay
=0):
219 """Move an image to the trash.
220 Images, even ones actively in-use by clones,
221 can be moved to the trash and deleted at a later time.
223 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
225 return rbd_call(pool_name
, namespace
, rbd_inst
.trash_move
, image_name
, delay
)
228 @ApiController('/block/image/{image_spec}/snap', Scope
.RBD_IMAGE
)
229 class RbdSnapshot(RESTController
):
231 RESOURCE_ID
= "snapshot_name"
233 @RbdTask('snap/create',
234 ['{image_spec}', '{snapshot_name}'], 2.0)
235 def create(self
, image_spec
, snapshot_name
):
236 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
238 def _create_snapshot(ioctx
, img
, snapshot_name
):
239 img
.create_snap(snapshot_name
)
241 return rbd_image_call(pool_name
, namespace
, image_name
, _create_snapshot
,
244 @RbdTask('snap/delete',
245 ['{image_spec}', '{snapshot_name}'], 2.0)
246 def delete(self
, image_spec
, snapshot_name
):
247 return RbdSnapshotService
.remove_snapshot(image_spec
, snapshot_name
)
249 @RbdTask('snap/edit',
250 ['{image_spec}', '{snapshot_name}'], 4.0)
251 def set(self
, image_spec
, snapshot_name
, new_snap_name
=None,
253 def _edit(ioctx
, img
, snapshot_name
):
254 if new_snap_name
and new_snap_name
!= snapshot_name
:
255 img
.rename_snap(snapshot_name
, new_snap_name
)
256 snapshot_name
= new_snap_name
257 if is_protected
is not None and \
258 is_protected
!= img
.is_protected_snap(snapshot_name
):
260 img
.protect_snap(snapshot_name
)
262 img
.unprotect_snap(snapshot_name
)
264 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
265 return rbd_image_call(pool_name
, namespace
, image_name
, _edit
, snapshot_name
)
267 @RbdTask('snap/rollback',
268 ['{image_spec}', '{snapshot_name}'], 5.0)
269 @RESTController.Resource('POST')
271 def rollback(self
, image_spec
, snapshot_name
):
272 def _rollback(ioctx
, img
, snapshot_name
):
273 img
.rollback_to_snap(snapshot_name
)
275 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
276 return rbd_image_call(pool_name
, namespace
, image_name
, _rollback
, snapshot_name
)
279 {'parent_image_spec': '{image_spec}',
280 'child_pool_name': '{child_pool_name}',
281 'child_namespace': '{child_namespace}',
282 'child_image_name': '{child_image_name}'}, 2.0)
283 @RESTController.Resource('POST')
284 def clone(self
, image_spec
, snapshot_name
, child_pool_name
,
285 child_image_name
, child_namespace
=None, obj_size
=None, features
=None,
286 stripe_unit
=None, stripe_count
=None, data_pool
=None, configuration
=None):
288 Clones a snapshot to an image
291 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
293 def _parent_clone(p_ioctx
):
297 if obj_size
and obj_size
> 0:
298 l_order
= int(round(math
.log(float(obj_size
), 2)))
301 feature_bitmask
= format_features(features
)
304 rbd_inst
.clone(p_ioctx
, image_name
, snapshot_name
, ioctx
,
305 child_image_name
, feature_bitmask
, l_order
,
306 stripe_unit
, stripe_count
, data_pool
)
308 RbdConfiguration(pool_ioctx
=ioctx
, image_name
=child_image_name
).set_configuration(
311 return rbd_call(child_pool_name
, child_namespace
, _clone
)
313 rbd_call(pool_name
, namespace
, _parent_clone
)
316 @ApiController('/block/image/trash', Scope
.RBD_IMAGE
)
317 class RbdTrash(RESTController
):
318 RESOURCE_ID
= "image_id_spec"
322 def _trash_pool_list(self
, pool_name
):
323 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
325 namespaces
= self
.rbd_inst
.namespace_list(ioctx
)
326 # images without namespace
327 namespaces
.append('')
328 for namespace
in namespaces
:
329 ioctx
.set_namespace(namespace
)
330 images
= self
.rbd_inst
.trash_list(ioctx
)
332 trash
['pool_name'] = pool_name
333 trash
['namespace'] = namespace
334 trash
['deletion_time'] = "{}Z".format(trash
['deletion_time'].isoformat())
335 trash
['deferment_end_time'] = "{}Z".format(
336 trash
['deferment_end_time'].isoformat())
340 def _trash_list(self
, pool_name
=None):
344 pools
= [p
['pool_name'] for p
in CephService
.get_pool_list('rbd')]
348 # pylint: disable=unbalanced-tuple-unpacking
349 status
, value
= self
._trash
_pool
_list
(pool
)
350 result
.append({'status': status
, 'value': value
, 'pool_name': pool
})
354 @handle_rados_error('pool')
355 def list(self
, pool_name
=None):
356 """List all entries from trash."""
357 return self
._trash
_list
(pool_name
)
360 @handle_rados_error('pool')
361 @RbdTask('trash/purge', ['{pool_name}'], 2.0)
362 @RESTController.Collection('POST', query_params
=['pool_name'])
364 def purge(self
, pool_name
=None):
365 """Remove all expired images from trash."""
366 now
= "{}Z".format(datetime
.now().isoformat())
367 pools
= self
._trash
_list
(pool_name
)
370 for image
in pool
['value']:
371 if image
['deferment_end_time'] < now
:
372 rbd_call(pool
['pool_name'], image
['namespace'],
373 self
.rbd_inst
.trash_remove
, image
['id'], 0)
375 @RbdTask('trash/restore', ['{image_id_spec}', '{new_image_name}'], 2.0)
376 @RESTController.Resource('POST')
378 def restore(self
, image_id_spec
, new_image_name
):
379 """Restore an image from trash."""
380 pool_name
, namespace
, image_id
= parse_image_spec(image_id_spec
)
381 return rbd_call(pool_name
, namespace
, self
.rbd_inst
.trash_restore
, image_id
,
384 @RbdTask('trash/remove', ['{image_id_spec}'], 2.0)
385 def delete(self
, image_id_spec
, force
=False):
386 """Delete an image from trash.
387 If image deferment time has not expired you can not removed it unless use force.
388 But an actively in-use by clones or has snapshots can not be removed.
390 pool_name
, namespace
, image_id
= parse_image_spec(image_id_spec
)
391 return rbd_call(pool_name
, namespace
, self
.rbd_inst
.trash_remove
, image_id
,
392 int(str_to_bool(force
)))
395 @ApiController('/block/pool/{pool_name}/namespace', Scope
.RBD_IMAGE
)
396 class RbdNamespace(RESTController
):
399 def create(self
, pool_name
, namespace
):
400 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
401 namespaces
= self
.rbd_inst
.namespace_list(ioctx
)
402 if namespace
in namespaces
:
403 raise DashboardException(
404 msg
='Namespace already exists',
405 code
='namespace_already_exists',
407 return self
.rbd_inst
.namespace_create(ioctx
, namespace
)
409 def delete(self
, pool_name
, namespace
):
410 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
411 # pylint: disable=unbalanced-tuple-unpacking
412 _
, images
= RbdService
.rbd_pool_list(pool_name
, namespace
)
414 raise DashboardException(
415 msg
='Namespace contains images which must be deleted first',
416 code
='namespace_contains_images',
418 return self
.rbd_inst
.namespace_remove(ioctx
, namespace
)
420 def list(self
, pool_name
):
421 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
423 namespaces
= self
.rbd_inst
.namespace_list(ioctx
)
424 for namespace
in namespaces
:
425 # pylint: disable=unbalanced-tuple-unpacking
426 _
, images
= RbdService
.rbd_pool_list(pool_name
, namespace
)
428 'namespace': namespace
,
429 'num_images': len(images
) if images
else 0