1 # -*- coding: utf-8 -*-
2 # pylint: disable=unused-argument
3 # pylint: disable=too-many-statements,too-many-branches
7 from datetime
import datetime
8 from functools
import partial
13 from ..exceptions
import DashboardException
14 from ..security
import Scope
15 from ..services
.ceph_service
import CephService
16 from ..services
.exception
import handle_rados_error
, handle_rbd_error
, serialize_dashboard_exception
17 from ..services
.rbd
import RbdConfiguration
, RbdService
, RbdSnapshotService
, \
18 format_bitmask
, format_features
, parse_image_spec
, rbd_call
, \
20 from ..tools
import ViewCache
, str_to_bool
21 from . import APIDoc
, APIRouter
, CreatePermission
, DeletePermission
, \
22 EndpointDoc
, RESTController
, Task
, UpdatePermission
, allow_empty_body
24 logger
= logging
.getLogger(__name__
)
27 "status": (int, 'Status of the image'),
29 "pool_name": (str, 'pool name')
35 "pool_name": (str, 'pool name')
39 # pylint: disable=not-callable
40 def RbdTask(name
, metadata
, wait_for
): # noqa: N802
41 def composed_decorator(func
):
42 func
= handle_rados_error('pool')(func
)
43 func
= handle_rbd_error()(func
)
44 return Task("rbd/{}".format(name
), metadata
, wait_for
,
45 partial(serialize_dashboard_exception
, include_http_status
=True))(func
)
46 return composed_decorator
49 def _sort_features(features
, enable
=True):
51 Sorts image features according to feature dependencies:
53 object-map depends on exclusive-lock
54 journaling depends on exclusive-lock
55 fast-diff depends on object-map
57 ORDER
= ['exclusive-lock', 'journaling', 'object-map', 'fast-diff'] # noqa: N806
61 return ORDER
.index(feat
)
65 features
.sort(key
=key_func
, reverse
=not enable
)
68 @APIRouter('/block/image', Scope
.RBD_IMAGE
)
69 @APIDoc("RBD Management API", "Rbd")
70 class Rbd(RESTController
):
72 # set of image features that can be enable on existing images
73 ALLOW_ENABLE_FEATURES
= {"exclusive-lock", "object-map", "fast-diff", "journaling"}
75 # set of image features that can be disabled on existing images
76 ALLOW_DISABLE_FEATURES
= {"exclusive-lock", "object-map", "fast-diff", "deep-flatten",
79 def _rbd_list(self
, pool_name
=None):
83 pools
= [p
['pool_name'] for p
in CephService
.get_pool_list('rbd')]
87 # pylint: disable=unbalanced-tuple-unpacking
88 status
, value
= RbdService
.rbd_pool_list(pool
)
89 for i
, image
in enumerate(value
):
90 value
[i
]['configuration'] = RbdConfiguration(
91 pool
, image
['namespace'], image
['name']).list()
92 result
.append({'status': status
, 'value': value
, 'pool_name': pool
})
96 @handle_rados_error('pool')
97 @EndpointDoc("Display Rbd Images",
99 'pool_name': (str, 'Pool Name'),
101 responses
={200: RBD_SCHEMA
})
102 def list(self
, pool_name
=None):
103 return self
._rbd
_list
(pool_name
)
106 @handle_rados_error('pool')
107 def get(self
, image_spec
):
108 return RbdService
.get_image(image_spec
)
111 {'pool_name': '{pool_name}', 'namespace': '{namespace}', 'image_name': '{name}'}, 2.0)
112 def create(self
, name
, pool_name
, size
, namespace
=None, obj_size
=None, features
=None,
113 stripe_unit
=None, stripe_count
=None, data_pool
=None, configuration
=None):
122 if obj_size
and obj_size
> 0:
123 l_order
= int(round(math
.log(float(obj_size
), 2)))
126 feature_bitmask
= format_features(features
)
128 rbd_inst
.create(ioctx
, name
, size
, order
=l_order
, old_format
=False,
129 features
=feature_bitmask
, stripe_unit
=stripe_unit
,
130 stripe_count
=stripe_count
, data_pool
=data_pool
)
131 RbdConfiguration(pool_ioctx
=ioctx
, namespace
=namespace
,
132 image_name
=name
).set_configuration(configuration
)
134 rbd_call(pool_name
, namespace
, _create
)
136 @RbdTask('delete', ['{image_spec}'], 2.0)
137 def delete(self
, image_spec
):
138 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
140 image
= RbdService
.get_image(image_spec
)
141 snapshots
= image
['snapshots']
142 for snap
in snapshots
:
143 RbdSnapshotService
.remove_snapshot(image_spec
, snap
['name'], snap
['is_protected'])
146 return rbd_call(pool_name
, namespace
, rbd_inst
.remove
, image_name
)
148 @RbdTask('edit', ['{image_spec}', '{name}'], 4.0)
149 def set(self
, image_spec
, name
=None, size
=None, features
=None, configuration
=None):
150 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
152 def _edit(ioctx
, image
):
155 if name
and name
!= image_name
:
156 rbd_inst
.rename(ioctx
, image_name
, name
)
159 if size
and size
!= image
.size():
162 # check enable/disable features
163 if features
is not None:
164 curr_features
= format_bitmask(image
.features())
165 # check disabled features
166 _sort_features(curr_features
, enable
=False)
167 for feature
in curr_features
:
168 if feature
not in features
and feature
in self
.ALLOW_DISABLE_FEATURES
:
169 if feature
not in format_bitmask(image
.features()):
171 f_bitmask
= format_features([feature
])
172 image
.update_features(f_bitmask
, False)
173 # check enabled features
174 _sort_features(features
)
175 for feature
in features
:
176 if feature
not in curr_features
and feature
in self
.ALLOW_ENABLE_FEATURES
:
177 if feature
in format_bitmask(image
.features()):
179 f_bitmask
= format_features([feature
])
180 image
.update_features(f_bitmask
, True)
182 RbdConfiguration(pool_ioctx
=ioctx
, image_name
=image_name
).set_configuration(
185 return rbd_image_call(pool_name
, namespace
, image_name
, _edit
)
188 {'src_image_spec': '{image_spec}',
189 'dest_pool_name': '{dest_pool_name}',
190 'dest_namespace': '{dest_namespace}',
191 'dest_image_name': '{dest_image_name}'}, 2.0)
192 @RESTController.Resource('POST')
194 def copy(self
, image_spec
, dest_pool_name
, dest_namespace
, dest_image_name
,
195 snapshot_name
=None, obj_size
=None, features
=None,
196 stripe_unit
=None, stripe_count
=None, data_pool
=None, configuration
=None):
197 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
199 def _src_copy(s_ioctx
, s_img
):
203 if obj_size
and obj_size
> 0:
204 l_order
= int(round(math
.log(float(obj_size
), 2)))
207 feature_bitmask
= format_features(features
)
210 s_img
.set_snap(snapshot_name
)
212 s_img
.copy(d_ioctx
, dest_image_name
, feature_bitmask
, l_order
,
213 stripe_unit
, stripe_count
, data_pool
)
214 RbdConfiguration(pool_ioctx
=d_ioctx
, image_name
=dest_image_name
).set_configuration(
217 return rbd_call(dest_pool_name
, dest_namespace
, _copy
)
219 return rbd_image_call(pool_name
, namespace
, image_name
, _src_copy
)
221 @RbdTask('flatten', ['{image_spec}'], 2.0)
222 @RESTController.Resource('POST')
225 def flatten(self
, image_spec
):
227 def _flatten(ioctx
, image
):
230 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
231 return rbd_image_call(pool_name
, namespace
, image_name
, _flatten
)
233 @RESTController.Collection('GET')
234 def default_features(self
):
235 rbd_default_features
= mgr
.get('config')['rbd_default_features']
236 return format_bitmask(int(rbd_default_features
))
238 @RESTController.Collection('GET')
239 def clone_format_version(self
):
240 """Return the RBD clone format version.
242 rbd_default_clone_format
= mgr
.get('config')['rbd_default_clone_format']
243 if rbd_default_clone_format
!= 'auto':
244 return int(rbd_default_clone_format
)
245 osd_map
= mgr
.get_osdmap().dump()
246 min_compat_client
= osd_map
.get('min_compat_client', '')
247 require_min_compat_client
= osd_map
.get('require_min_compat_client', '')
248 if max(min_compat_client
, require_min_compat_client
) < 'mimic':
253 @RbdTask('trash/move', ['{image_spec}'], 2.0)
254 @RESTController.Resource('POST')
256 def move_trash(self
, image_spec
, delay
=0):
257 """Move an image to the trash.
258 Images, even ones actively in-use by clones,
259 can be moved to the trash and deleted at a later time.
261 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
263 return rbd_call(pool_name
, namespace
, rbd_inst
.trash_move
, image_name
, delay
)
266 @APIRouter('/block/image/{image_spec}/snap', Scope
.RBD_IMAGE
)
267 @APIDoc("RBD Snapshot Management API", "RbdSnapshot")
268 class RbdSnapshot(RESTController
):
270 RESOURCE_ID
= "snapshot_name"
272 @RbdTask('snap/create',
273 ['{image_spec}', '{snapshot_name}'], 2.0)
274 def create(self
, image_spec
, snapshot_name
):
275 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
277 def _create_snapshot(ioctx
, img
, snapshot_name
):
278 img
.create_snap(snapshot_name
)
280 return rbd_image_call(pool_name
, namespace
, image_name
, _create_snapshot
,
283 @RbdTask('snap/delete',
284 ['{image_spec}', '{snapshot_name}'], 2.0)
285 def delete(self
, image_spec
, snapshot_name
):
286 return RbdSnapshotService
.remove_snapshot(image_spec
, snapshot_name
)
288 @RbdTask('snap/edit',
289 ['{image_spec}', '{snapshot_name}'], 4.0)
290 def set(self
, image_spec
, snapshot_name
, new_snap_name
=None,
292 def _edit(ioctx
, img
, snapshot_name
):
293 if new_snap_name
and new_snap_name
!= snapshot_name
:
294 img
.rename_snap(snapshot_name
, new_snap_name
)
295 snapshot_name
= new_snap_name
296 if is_protected
is not None and \
297 is_protected
!= img
.is_protected_snap(snapshot_name
):
299 img
.protect_snap(snapshot_name
)
301 img
.unprotect_snap(snapshot_name
)
303 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
304 return rbd_image_call(pool_name
, namespace
, image_name
, _edit
, snapshot_name
)
306 @RbdTask('snap/rollback',
307 ['{image_spec}', '{snapshot_name}'], 5.0)
308 @RESTController.Resource('POST')
311 def rollback(self
, image_spec
, snapshot_name
):
312 def _rollback(ioctx
, img
, snapshot_name
):
313 img
.rollback_to_snap(snapshot_name
)
315 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
316 return rbd_image_call(pool_name
, namespace
, image_name
, _rollback
, snapshot_name
)
319 {'parent_image_spec': '{image_spec}',
320 'child_pool_name': '{child_pool_name}',
321 'child_namespace': '{child_namespace}',
322 'child_image_name': '{child_image_name}'}, 2.0)
323 @RESTController.Resource('POST')
325 def clone(self
, image_spec
, snapshot_name
, child_pool_name
,
326 child_image_name
, child_namespace
=None, obj_size
=None, features
=None,
327 stripe_unit
=None, stripe_count
=None, data_pool
=None, configuration
=None):
329 Clones a snapshot to an image
332 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
334 def _parent_clone(p_ioctx
):
338 if obj_size
and obj_size
> 0:
339 l_order
= int(round(math
.log(float(obj_size
), 2)))
342 feature_bitmask
= format_features(features
)
345 rbd_inst
.clone(p_ioctx
, image_name
, snapshot_name
, ioctx
,
346 child_image_name
, feature_bitmask
, l_order
,
347 stripe_unit
, stripe_count
, data_pool
)
349 RbdConfiguration(pool_ioctx
=ioctx
, image_name
=child_image_name
).set_configuration(
352 return rbd_call(child_pool_name
, child_namespace
, _clone
)
354 rbd_call(pool_name
, namespace
, _parent_clone
)
357 @APIRouter('/block/image/trash', Scope
.RBD_IMAGE
)
358 @APIDoc("RBD Trash Management API", "RbdTrash")
359 class RbdTrash(RESTController
):
360 RESOURCE_ID
= "image_id_spec"
364 self
.rbd_inst
= rbd
.RBD()
367 def _trash_pool_list(self
, pool_name
):
368 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
370 namespaces
= self
.rbd_inst
.namespace_list(ioctx
)
371 # images without namespace
372 namespaces
.append('')
373 for namespace
in namespaces
:
374 ioctx
.set_namespace(namespace
)
375 images
= self
.rbd_inst
.trash_list(ioctx
)
377 trash
['pool_name'] = pool_name
378 trash
['namespace'] = namespace
379 trash
['deletion_time'] = "{}Z".format(trash
['deletion_time'].isoformat())
380 trash
['deferment_end_time'] = "{}Z".format(
381 trash
['deferment_end_time'].isoformat())
385 def _trash_list(self
, pool_name
=None):
389 pools
= [p
['pool_name'] for p
in CephService
.get_pool_list('rbd')]
393 # pylint: disable=unbalanced-tuple-unpacking
394 status
, value
= self
._trash
_pool
_list
(pool
)
395 result
.append({'status': status
, 'value': value
, 'pool_name': pool
})
399 @handle_rados_error('pool')
400 @EndpointDoc("Get RBD Trash Details by pool name",
402 'pool_name': (str, 'Name of the pool'),
404 responses
={200: RBD_TRASH_SCHEMA
})
405 def list(self
, pool_name
=None):
406 """List all entries from trash."""
407 return self
._trash
_list
(pool_name
)
410 @handle_rados_error('pool')
411 @RbdTask('trash/purge', ['{pool_name}'], 2.0)
412 @RESTController.Collection('POST', query_params
=['pool_name'])
415 def purge(self
, pool_name
=None):
416 """Remove all expired images from trash."""
417 now
= "{}Z".format(datetime
.utcnow().isoformat())
418 pools
= self
._trash
_list
(pool_name
)
421 for image
in pool
['value']:
422 if image
['deferment_end_time'] < now
:
423 logger
.info('Removing trash image %s (pool=%s, namespace=%s, name=%s)',
424 image
['id'], pool
['pool_name'], image
['namespace'], image
['name'])
425 rbd_call(pool
['pool_name'], image
['namespace'],
426 self
.rbd_inst
.trash_remove
, image
['id'], 0)
428 @RbdTask('trash/restore', ['{image_id_spec}', '{new_image_name}'], 2.0)
429 @RESTController.Resource('POST')
432 def restore(self
, image_id_spec
, new_image_name
):
433 """Restore an image from trash."""
434 pool_name
, namespace
, image_id
= parse_image_spec(image_id_spec
)
435 return rbd_call(pool_name
, namespace
, self
.rbd_inst
.trash_restore
, image_id
,
438 @RbdTask('trash/remove', ['{image_id_spec}'], 2.0)
439 def delete(self
, image_id_spec
, force
=False):
440 """Delete an image from trash.
441 If image deferment time has not expired you can not removed it unless use force.
442 But an actively in-use by clones or has snapshots can not be removed.
444 pool_name
, namespace
, image_id
= parse_image_spec(image_id_spec
)
445 return rbd_call(pool_name
, namespace
, self
.rbd_inst
.trash_remove
, image_id
,
446 int(str_to_bool(force
)))
449 @APIRouter('/block/pool/{pool_name}/namespace', Scope
.RBD_IMAGE
)
450 @APIDoc("RBD Namespace Management API", "RbdNamespace")
451 class RbdNamespace(RESTController
):
455 self
.rbd_inst
= rbd
.RBD()
457 def create(self
, pool_name
, namespace
):
458 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
459 namespaces
= self
.rbd_inst
.namespace_list(ioctx
)
460 if namespace
in namespaces
:
461 raise DashboardException(
462 msg
='Namespace already exists',
463 code
='namespace_already_exists',
465 return self
.rbd_inst
.namespace_create(ioctx
, namespace
)
467 def delete(self
, pool_name
, namespace
):
468 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
469 # pylint: disable=unbalanced-tuple-unpacking
470 _
, images
= RbdService
.rbd_pool_list(pool_name
, namespace
)
472 raise DashboardException(
473 msg
='Namespace contains images which must be deleted first',
474 code
='namespace_contains_images',
476 return self
.rbd_inst
.namespace_remove(ioctx
, namespace
)
478 def list(self
, pool_name
):
479 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
481 namespaces
= self
.rbd_inst
.namespace_list(ioctx
)
482 for namespace
in namespaces
:
483 # pylint: disable=unbalanced-tuple-unpacking
484 _
, images
= RbdService
.rbd_pool_list(pool_name
, namespace
)
486 'namespace': namespace
,
487 'num_images': len(images
) if images
else 0