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
14 from ..exceptions
import DashboardException
15 from ..security
import Scope
16 from ..services
.ceph_service
import CephService
17 from ..services
.exception
import handle_rados_error
, handle_rbd_error
, serialize_dashboard_exception
18 from ..services
.rbd
import MIRROR_IMAGE_MODE
, RbdConfiguration
, \
19 RbdMirroringService
, RbdService
, RbdSnapshotService
, format_bitmask
, \
20 format_features
, get_image_spec
, parse_image_spec
, rbd_call
, \
22 from ..tools
import ViewCache
, str_to_bool
23 from . import APIDoc
, APIRouter
, BaseController
, CreatePermission
, \
24 DeletePermission
, Endpoint
, EndpointDoc
, ReadPermission
, RESTController
, \
25 Task
, UIRouter
, UpdatePermission
, allow_empty_body
26 from ._version
import APIVersion
28 logger
= logging
.getLogger(__name__
)
32 "pool_name": (str, 'pool name')
38 "pool_name": (str, 'pool name')
42 # pylint: disable=not-callable
43 def RbdTask(name
, metadata
, wait_for
): # noqa: N802
44 def composed_decorator(func
):
45 func
= handle_rados_error('pool')(func
)
46 func
= handle_rbd_error()(func
)
47 return Task("rbd/{}".format(name
), metadata
, wait_for
,
48 partial(serialize_dashboard_exception
, include_http_status
=True))(func
)
49 return composed_decorator
52 def _sort_features(features
, enable
=True):
54 Sorts image features according to feature dependencies:
56 object-map depends on exclusive-lock
57 journaling depends on exclusive-lock
58 fast-diff depends on object-map
60 ORDER
= ['exclusive-lock', 'journaling', 'object-map', 'fast-diff'] # noqa: N806
64 return ORDER
.index(feat
)
68 features
.sort(key
=key_func
, reverse
=not enable
)
71 @APIRouter('/block/image', Scope
.RBD_IMAGE
)
72 @APIDoc("RBD Management API", "Rbd")
73 class Rbd(RESTController
):
75 # set of image features that can be enable on existing images
76 ALLOW_ENABLE_FEATURES
= {"exclusive-lock", "object-map", "fast-diff", "journaling"}
78 # set of image features that can be disabled on existing images
79 ALLOW_DISABLE_FEATURES
= {"exclusive-lock", "object-map", "fast-diff", "deep-flatten",
84 def _rbd_list(self
, pool_name
=None, offset
=0, limit
=DEFAULT_LIMIT
, search
='', sort
=''):
88 pools
= [p
['pool_name'] for p
in CephService
.get_pool_list('rbd')]
90 images
, num_total_images
= RbdService
.rbd_pool_list(
91 pools
, offset
=offset
, limit
=limit
, search
=search
, sort
=sort
)
92 cherrypy
.response
.headers
['X-Total-Count'] = num_total_images
94 for i
, image
in enumerate(images
):
95 pool
= image
['pool_name']
96 if pool
not in pool_result
:
97 pool_result
[pool
] = {'value': [], 'pool_name': image
['pool_name']}
98 pool_result
[pool
]['value'].append(image
)
100 images
[i
]['configuration'] = RbdConfiguration(
101 pool
, image
['namespace'], image
['name']).list()
102 return list(pool_result
.values())
105 @handle_rados_error('pool')
106 @EndpointDoc("Display Rbd Images",
108 'pool_name': (str, 'Pool Name'),
109 'limit': (int, 'limit'),
110 'offset': (int, 'offset'),
112 responses
={200: RBD_SCHEMA
})
113 @RESTController.MethodMap(version
=APIVersion(2, 0)) # type: ignore
114 def list(self
, pool_name
=None, offset
: int = 0, limit
: int = DEFAULT_LIMIT
,
115 search
: str = '', sort
: str = ''):
116 return self
._rbd
_list
(pool_name
, offset
=offset
, limit
=limit
, search
=search
, sort
=sort
)
119 @handle_rados_error('pool')
120 def get(self
, image_spec
):
121 return RbdService
.get_image(image_spec
)
124 {'pool_name': '{pool_name}', 'namespace': '{namespace}', 'image_name': '{name}'}, 2.0)
125 def create(self
, name
, pool_name
, size
, namespace
=None, schedule_interval
='',
126 obj_size
=None, features
=None, stripe_unit
=None, stripe_count
=None,
127 data_pool
=None, configuration
=None, mirror_mode
=None):
136 if obj_size
and obj_size
> 0:
137 l_order
= int(round(math
.log(float(obj_size
), 2)))
140 feature_bitmask
= format_features(features
)
142 rbd_inst
.create(ioctx
, name
, size
, order
=l_order
, old_format
=False,
143 features
=feature_bitmask
, stripe_unit
=stripe_unit
,
144 stripe_count
=stripe_count
, data_pool
=data_pool
)
145 RbdConfiguration(pool_ioctx
=ioctx
, namespace
=namespace
,
146 image_name
=name
).set_configuration(configuration
)
148 rbd_call(pool_name
, namespace
, _create
)
150 RbdMirroringService
.enable_image(name
, pool_name
, namespace
,
151 MIRROR_IMAGE_MODE
[mirror_mode
])
153 if schedule_interval
:
154 image_spec
= get_image_spec(pool_name
, namespace
, name
)
155 RbdMirroringService
.snapshot_schedule_add(image_spec
, schedule_interval
)
157 @RbdTask('delete', ['{image_spec}'], 2.0)
158 def delete(self
, image_spec
):
159 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
161 image
= RbdService
.get_image(image_spec
)
162 snapshots
= image
['snapshots']
163 for snap
in snapshots
:
164 RbdSnapshotService
.remove_snapshot(image_spec
, snap
['name'], snap
['is_protected'])
167 return rbd_call(pool_name
, namespace
, rbd_inst
.remove
, image_name
)
169 @RbdTask('edit', ['{image_spec}', '{name}'], 4.0)
170 def set(self
, image_spec
, name
=None, size
=None, features
=None,
171 configuration
=None, enable_mirror
=None, primary
=None,
172 resync
=False, mirror_mode
=None, schedule_interval
='',
173 remove_scheduling
=False):
175 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
177 def _edit(ioctx
, image
):
180 if name
and name
!= image_name
:
181 rbd_inst
.rename(ioctx
, image_name
, name
)
184 if size
and size
!= image
.size():
187 # check enable/disable features
188 if features
is not None:
189 curr_features
= format_bitmask(image
.features())
190 # check disabled features
191 _sort_features(curr_features
, enable
=False)
192 for feature
in curr_features
:
193 if feature
not in features
and feature
in self
.ALLOW_DISABLE_FEATURES
:
194 if feature
not in format_bitmask(image
.features()):
196 f_bitmask
= format_features([feature
])
197 image
.update_features(f_bitmask
, False)
198 # check enabled features
199 _sort_features(features
)
200 for feature
in features
:
201 if feature
not in curr_features
and feature
in self
.ALLOW_ENABLE_FEATURES
:
202 if feature
in format_bitmask(image
.features()):
204 f_bitmask
= format_features([feature
])
205 image
.update_features(f_bitmask
, True)
207 RbdConfiguration(pool_ioctx
=ioctx
, image_name
=image_name
).set_configuration(
210 mirror_image_info
= image
.mirror_image_get_info()
211 if enable_mirror
and mirror_image_info
['state'] == rbd
.RBD_MIRROR_IMAGE_DISABLED
:
212 RbdMirroringService
.enable_image(
213 image_name
, pool_name
, namespace
,
214 MIRROR_IMAGE_MODE
[mirror_mode
])
215 elif (enable_mirror
is False
216 and mirror_image_info
['state'] == rbd
.RBD_MIRROR_IMAGE_ENABLED
):
217 RbdMirroringService
.disable_image(
218 image_name
, pool_name
, namespace
)
220 if primary
and not mirror_image_info
['primary']:
221 RbdMirroringService
.promote_image(
222 image_name
, pool_name
, namespace
)
223 elif primary
is False and mirror_image_info
['primary']:
224 RbdMirroringService
.demote_image(
225 image_name
, pool_name
, namespace
)
228 RbdMirroringService
.resync_image(image_name
, pool_name
, namespace
)
230 if schedule_interval
:
231 RbdMirroringService
.snapshot_schedule_add(image_spec
, schedule_interval
)
233 if remove_scheduling
:
234 RbdMirroringService
.snapshot_schedule_remove(image_spec
)
236 return rbd_image_call(pool_name
, namespace
, image_name
, _edit
)
239 {'src_image_spec': '{image_spec}',
240 'dest_pool_name': '{dest_pool_name}',
241 'dest_namespace': '{dest_namespace}',
242 'dest_image_name': '{dest_image_name}'}, 2.0)
243 @RESTController.Resource('POST')
245 def copy(self
, image_spec
, dest_pool_name
, dest_namespace
, dest_image_name
,
246 snapshot_name
=None, obj_size
=None, features
=None,
247 stripe_unit
=None, stripe_count
=None, data_pool
=None, configuration
=None):
248 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
250 def _src_copy(s_ioctx
, s_img
):
254 if obj_size
and obj_size
> 0:
255 l_order
= int(round(math
.log(float(obj_size
), 2)))
258 feature_bitmask
= format_features(features
)
261 s_img
.set_snap(snapshot_name
)
263 s_img
.copy(d_ioctx
, dest_image_name
, feature_bitmask
, l_order
,
264 stripe_unit
, stripe_count
, data_pool
)
265 RbdConfiguration(pool_ioctx
=d_ioctx
, image_name
=dest_image_name
).set_configuration(
268 return rbd_call(dest_pool_name
, dest_namespace
, _copy
)
270 return rbd_image_call(pool_name
, namespace
, image_name
, _src_copy
)
272 @RbdTask('flatten', ['{image_spec}'], 2.0)
273 @RESTController.Resource('POST')
276 def flatten(self
, image_spec
):
278 def _flatten(ioctx
, image
):
281 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
282 return rbd_image_call(pool_name
, namespace
, image_name
, _flatten
)
284 @RESTController.Collection('GET')
285 def default_features(self
):
286 rbd_default_features
= mgr
.get('config')['rbd_default_features']
287 return format_bitmask(int(rbd_default_features
))
289 @RESTController.Collection('GET')
290 def clone_format_version(self
):
291 """Return the RBD clone format version.
293 rbd_default_clone_format
= mgr
.get('config')['rbd_default_clone_format']
294 if rbd_default_clone_format
!= 'auto':
295 return int(rbd_default_clone_format
)
296 osd_map
= mgr
.get_osdmap().dump()
297 min_compat_client
= osd_map
.get('min_compat_client', '')
298 require_min_compat_client
= osd_map
.get('require_min_compat_client', '')
299 if max(min_compat_client
, require_min_compat_client
) < 'mimic':
304 @RbdTask('trash/move', ['{image_spec}'], 2.0)
305 @RESTController.Resource('POST')
307 def move_trash(self
, image_spec
, delay
=0):
308 """Move an image to the trash.
309 Images, even ones actively in-use by clones,
310 can be moved to the trash and deleted at a later time.
312 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
314 return rbd_call(pool_name
, namespace
, rbd_inst
.trash_move
, image_name
, delay
)
317 @UIRouter('/block/rbd')
318 class RbdStatus(BaseController
):
319 @EndpointDoc("Display RBD Image feature status")
323 status
= {'available': True, 'message': None}
324 if not CephService
.get_pool_list('rbd'):
325 status
['available'] = False
326 status
['message'] = 'No RBD pools in the cluster. Please create a pool '\
327 'with the "rbd" application label.' # type: ignore
331 @APIRouter('/block/image/{image_spec}/snap', Scope
.RBD_IMAGE
)
332 @APIDoc("RBD Snapshot Management API", "RbdSnapshot")
333 class RbdSnapshot(RESTController
):
335 RESOURCE_ID
= "snapshot_name"
337 @RbdTask('snap/create',
338 ['{image_spec}', '{snapshot_name}'], 2.0)
339 def create(self
, image_spec
, snapshot_name
):
340 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
342 def _create_snapshot(ioctx
, img
, snapshot_name
):
343 mirror_info
= img
.mirror_image_get_info()
344 mirror_mode
= img
.mirror_image_get_mode()
345 if (mirror_info
['state'] == rbd
.RBD_MIRROR_IMAGE_ENABLED
346 and mirror_mode
== rbd
.RBD_MIRROR_IMAGE_MODE_SNAPSHOT
):
347 img
.mirror_image_create_snapshot()
349 img
.create_snap(snapshot_name
)
351 return rbd_image_call(pool_name
, namespace
, image_name
, _create_snapshot
,
354 @RbdTask('snap/delete',
355 ['{image_spec}', '{snapshot_name}'], 2.0)
356 def delete(self
, image_spec
, snapshot_name
):
357 return RbdSnapshotService
.remove_snapshot(image_spec
, snapshot_name
)
359 @RbdTask('snap/edit',
360 ['{image_spec}', '{snapshot_name}'], 4.0)
361 def set(self
, image_spec
, snapshot_name
, new_snap_name
=None,
363 def _edit(ioctx
, img
, snapshot_name
):
364 if new_snap_name
and new_snap_name
!= snapshot_name
:
365 img
.rename_snap(snapshot_name
, new_snap_name
)
366 snapshot_name
= new_snap_name
367 if is_protected
is not None and \
368 is_protected
!= img
.is_protected_snap(snapshot_name
):
370 img
.protect_snap(snapshot_name
)
372 img
.unprotect_snap(snapshot_name
)
374 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
375 return rbd_image_call(pool_name
, namespace
, image_name
, _edit
, snapshot_name
)
377 @RbdTask('snap/rollback',
378 ['{image_spec}', '{snapshot_name}'], 5.0)
379 @RESTController.Resource('POST')
382 def rollback(self
, image_spec
, snapshot_name
):
383 def _rollback(ioctx
, img
, snapshot_name
):
384 img
.rollback_to_snap(snapshot_name
)
386 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
387 return rbd_image_call(pool_name
, namespace
, image_name
, _rollback
, snapshot_name
)
390 {'parent_image_spec': '{image_spec}',
391 'child_pool_name': '{child_pool_name}',
392 'child_namespace': '{child_namespace}',
393 'child_image_name': '{child_image_name}'}, 2.0)
394 @RESTController.Resource('POST')
396 def clone(self
, image_spec
, snapshot_name
, child_pool_name
,
397 child_image_name
, child_namespace
=None, obj_size
=None, features
=None,
398 stripe_unit
=None, stripe_count
=None, data_pool
=None, configuration
=None):
400 Clones a snapshot to an image
403 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
405 def _parent_clone(p_ioctx
):
409 if obj_size
and obj_size
> 0:
410 l_order
= int(round(math
.log(float(obj_size
), 2)))
413 feature_bitmask
= format_features(features
)
416 rbd_inst
.clone(p_ioctx
, image_name
, snapshot_name
, ioctx
,
417 child_image_name
, feature_bitmask
, l_order
,
418 stripe_unit
, stripe_count
, data_pool
)
420 RbdConfiguration(pool_ioctx
=ioctx
, image_name
=child_image_name
).set_configuration(
423 return rbd_call(child_pool_name
, child_namespace
, _clone
)
425 rbd_call(pool_name
, namespace
, _parent_clone
)
428 @APIRouter('/block/image/trash', Scope
.RBD_IMAGE
)
429 @APIDoc("RBD Trash Management API", "RbdTrash")
430 class RbdTrash(RESTController
):
431 RESOURCE_ID
= "image_id_spec"
435 self
.rbd_inst
= rbd
.RBD()
438 def _trash_pool_list(self
, pool_name
):
439 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
441 namespaces
= self
.rbd_inst
.namespace_list(ioctx
)
442 # images without namespace
443 namespaces
.append('')
444 for namespace
in namespaces
:
445 ioctx
.set_namespace(namespace
)
446 images
= self
.rbd_inst
.trash_list(ioctx
)
448 trash
['pool_name'] = pool_name
449 trash
['namespace'] = namespace
450 trash
['deletion_time'] = "{}Z".format(trash
['deletion_time'].isoformat())
451 trash
['deferment_end_time'] = "{}Z".format(
452 trash
['deferment_end_time'].isoformat())
456 def _trash_list(self
, pool_name
=None):
460 pools
= [p
['pool_name'] for p
in CephService
.get_pool_list('rbd')]
464 # pylint: disable=unbalanced-tuple-unpacking
465 status
, value
= self
._trash
_pool
_list
(pool
)
466 result
.append({'status': status
, 'value': value
, 'pool_name': pool
})
470 @handle_rados_error('pool')
471 @EndpointDoc("Get RBD Trash Details by pool name",
473 'pool_name': (str, 'Name of the pool'),
475 responses
={200: RBD_TRASH_SCHEMA
})
476 def list(self
, pool_name
=None):
477 """List all entries from trash."""
478 return self
._trash
_list
(pool_name
)
481 @handle_rados_error('pool')
482 @RbdTask('trash/purge', ['{pool_name}'], 2.0)
483 @RESTController.Collection('POST', query_params
=['pool_name'])
486 def purge(self
, pool_name
=None):
487 """Remove all expired images from trash."""
488 now
= "{}Z".format(datetime
.utcnow().isoformat())
489 pools
= self
._trash
_list
(pool_name
)
492 for image
in pool
['value']:
493 if image
['deferment_end_time'] < now
:
494 logger
.info('Removing trash image %s (pool=%s, namespace=%s, name=%s)',
495 image
['id'], pool
['pool_name'], image
['namespace'], image
['name'])
496 rbd_call(pool
['pool_name'], image
['namespace'],
497 self
.rbd_inst
.trash_remove
, image
['id'], 0)
499 @RbdTask('trash/restore', ['{image_id_spec}', '{new_image_name}'], 2.0)
500 @RESTController.Resource('POST')
503 def restore(self
, image_id_spec
, new_image_name
):
504 """Restore an image from trash."""
505 pool_name
, namespace
, image_id
= parse_image_spec(image_id_spec
)
506 return rbd_call(pool_name
, namespace
, self
.rbd_inst
.trash_restore
, image_id
,
509 @RbdTask('trash/remove', ['{image_id_spec}'], 2.0)
510 def delete(self
, image_id_spec
, force
=False):
511 """Delete an image from trash.
512 If image deferment time has not expired you can not removed it unless use force.
513 But an actively in-use by clones or has snapshots can not be removed.
515 pool_name
, namespace
, image_id
= parse_image_spec(image_id_spec
)
516 return rbd_call(pool_name
, namespace
, self
.rbd_inst
.trash_remove
, image_id
,
517 int(str_to_bool(force
)))
520 @APIRouter('/block/pool/{pool_name}/namespace', Scope
.RBD_IMAGE
)
521 @APIDoc("RBD Namespace Management API", "RbdNamespace")
522 class RbdNamespace(RESTController
):
526 self
.rbd_inst
= rbd
.RBD()
528 def create(self
, pool_name
, namespace
):
529 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
530 namespaces
= self
.rbd_inst
.namespace_list(ioctx
)
531 if namespace
in namespaces
:
532 raise DashboardException(
533 msg
='Namespace already exists',
534 code
='namespace_already_exists',
536 return self
.rbd_inst
.namespace_create(ioctx
, namespace
)
538 def delete(self
, pool_name
, namespace
):
539 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
540 # pylint: disable=unbalanced-tuple-unpacking
541 images
, _
= RbdService
.rbd_pool_list([pool_name
], namespace
=namespace
)
543 raise DashboardException(
544 msg
='Namespace contains images which must be deleted first',
545 code
='namespace_contains_images',
547 return self
.rbd_inst
.namespace_remove(ioctx
, namespace
)
549 def list(self
, pool_name
):
550 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
552 namespaces
= self
.rbd_inst
.namespace_list(ioctx
)
553 for namespace
in namespaces
:
554 # pylint: disable=unbalanced-tuple-unpacking
555 images
, _
= RbdService
.rbd_pool_list([pool_name
], namespace
=namespace
)
557 'namespace': namespace
,
558 'num_images': len(images
) if images
else 0