]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/rbd.py
c20ee8ca282b375220b5b1ae2f56601eae434359
[ceph.git] / ceph / src / pybind / mgr / dashboard / controllers / rbd.py
1 # -*- coding: utf-8 -*-
2 # pylint: disable=unused-argument
3 # pylint: disable=too-many-statements,too-many-branches
4
5 import logging
6 import math
7 from datetime import datetime
8 from functools import partial
9
10 import cherrypy
11 import rbd
12
13 from .. import mgr
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, \
21 rbd_image_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
27
28 logger = logging.getLogger(__name__)
29
30 RBD_SCHEMA = ([{
31 "value": ([str], ''),
32 "pool_name": (str, 'pool name')
33 }])
34
35 RBD_TRASH_SCHEMA = [{
36 "status": (int, ''),
37 "value": ([str], ''),
38 "pool_name": (str, 'pool name')
39 }]
40
41
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
50
51
52 def _sort_features(features, enable=True):
53 """
54 Sorts image features according to feature dependencies:
55
56 object-map depends on exclusive-lock
57 journaling depends on exclusive-lock
58 fast-diff depends on object-map
59 """
60 ORDER = ['exclusive-lock', 'journaling', 'object-map', 'fast-diff'] # noqa: N806
61
62 def key_func(feat):
63 try:
64 return ORDER.index(feat)
65 except ValueError:
66 return id(feat)
67
68 features.sort(key=key_func, reverse=not enable)
69
70
71 @APIRouter('/block/image', Scope.RBD_IMAGE)
72 @APIDoc("RBD Management API", "Rbd")
73 class Rbd(RESTController):
74
75 # set of image features that can be enable on existing images
76 ALLOW_ENABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "journaling"}
77
78 # set of image features that can be disabled on existing images
79 ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten",
80 "journaling"}
81
82 DEFAULT_LIMIT = 5
83
84 def _rbd_list(self, pool_name=None, offset=0, limit=DEFAULT_LIMIT, search='', sort=''):
85 if pool_name:
86 pools = [pool_name]
87 else:
88 pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
89
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
93 pool_result = {}
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)
99
100 images[i]['configuration'] = RbdConfiguration(
101 pool, image['namespace'], image['name']).list()
102 return list(pool_result.values())
103
104 @handle_rbd_error()
105 @handle_rados_error('pool')
106 @EndpointDoc("Display Rbd Images",
107 parameters={
108 'pool_name': (str, 'Pool Name'),
109 'limit': (int, 'limit'),
110 'offset': (int, 'offset'),
111 },
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)
117
118 @handle_rbd_error()
119 @handle_rados_error('pool')
120 def get(self, image_spec):
121 return RbdService.get_image(image_spec)
122
123 @RbdTask('create',
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):
128
129 size = int(size)
130
131 def _create(ioctx):
132 rbd_inst = rbd.RBD()
133
134 # Set order
135 l_order = None
136 if obj_size and obj_size > 0:
137 l_order = int(round(math.log(float(obj_size), 2)))
138
139 # Set features
140 feature_bitmask = format_features(features)
141
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)
147
148 rbd_call(pool_name, namespace, _create)
149 if mirror_mode:
150 RbdMirroringService.enable_image(name, pool_name, namespace,
151 MIRROR_IMAGE_MODE[mirror_mode])
152
153 if schedule_interval:
154 image_spec = get_image_spec(pool_name, namespace, name)
155 RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval)
156
157 @RbdTask('delete', ['{image_spec}'], 2.0)
158 def delete(self, image_spec):
159 pool_name, namespace, image_name = parse_image_spec(image_spec)
160
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'])
165
166 rbd_inst = rbd.RBD()
167 return rbd_call(pool_name, namespace, rbd_inst.remove, image_name)
168
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):
174
175 pool_name, namespace, image_name = parse_image_spec(image_spec)
176
177 def _edit(ioctx, image):
178 rbd_inst = rbd.RBD()
179 # check rename image
180 if name and name != image_name:
181 rbd_inst.rename(ioctx, image_name, name)
182
183 # check resize
184 if size and size != image.size():
185 image.resize(size)
186
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()):
195 continue
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()):
203 continue
204 f_bitmask = format_features([feature])
205 image.update_features(f_bitmask, True)
206
207 RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).set_configuration(
208 configuration)
209
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)
219
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)
226
227 if resync:
228 RbdMirroringService.resync_image(image_name, pool_name, namespace)
229
230 if schedule_interval:
231 RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval)
232
233 if remove_scheduling:
234 RbdMirroringService.snapshot_schedule_remove(image_spec)
235
236 return rbd_image_call(pool_name, namespace, image_name, _edit)
237
238 @RbdTask('copy',
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')
244 @allow_empty_body
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)
249
250 def _src_copy(s_ioctx, s_img):
251 def _copy(d_ioctx):
252 # Set order
253 l_order = None
254 if obj_size and obj_size > 0:
255 l_order = int(round(math.log(float(obj_size), 2)))
256
257 # Set features
258 feature_bitmask = format_features(features)
259
260 if snapshot_name:
261 s_img.set_snap(snapshot_name)
262
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(
266 configuration)
267
268 return rbd_call(dest_pool_name, dest_namespace, _copy)
269
270 return rbd_image_call(pool_name, namespace, image_name, _src_copy)
271
272 @RbdTask('flatten', ['{image_spec}'], 2.0)
273 @RESTController.Resource('POST')
274 @UpdatePermission
275 @allow_empty_body
276 def flatten(self, image_spec):
277
278 def _flatten(ioctx, image):
279 image.flatten()
280
281 pool_name, namespace, image_name = parse_image_spec(image_spec)
282 return rbd_image_call(pool_name, namespace, image_name, _flatten)
283
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))
288
289 @RESTController.Collection('GET')
290 def clone_format_version(self):
291 """Return the RBD clone format version.
292 """
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':
300 return 1
301
302 return 2
303
304 @RbdTask('trash/move', ['{image_spec}'], 2.0)
305 @RESTController.Resource('POST')
306 @allow_empty_body
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.
311 """
312 pool_name, namespace, image_name = parse_image_spec(image_spec)
313 rbd_inst = rbd.RBD()
314 return rbd_call(pool_name, namespace, rbd_inst.trash_move, image_name, delay)
315
316
317 @UIRouter('/block/rbd')
318 class RbdStatus(BaseController):
319 @EndpointDoc("Display RBD Image feature status")
320 @Endpoint()
321 @ReadPermission
322 def status(self):
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
328 return status
329
330
331 @APIRouter('/block/image/{image_spec}/snap', Scope.RBD_IMAGE)
332 @APIDoc("RBD Snapshot Management API", "RbdSnapshot")
333 class RbdSnapshot(RESTController):
334
335 RESOURCE_ID = "snapshot_name"
336
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)
341
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()
348 else:
349 img.create_snap(snapshot_name)
350
351 return rbd_image_call(pool_name, namespace, image_name, _create_snapshot,
352 snapshot_name)
353
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)
358
359 @RbdTask('snap/edit',
360 ['{image_spec}', '{snapshot_name}'], 4.0)
361 def set(self, image_spec, snapshot_name, new_snap_name=None,
362 is_protected=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):
369 if is_protected:
370 img.protect_snap(snapshot_name)
371 else:
372 img.unprotect_snap(snapshot_name)
373
374 pool_name, namespace, image_name = parse_image_spec(image_spec)
375 return rbd_image_call(pool_name, namespace, image_name, _edit, snapshot_name)
376
377 @RbdTask('snap/rollback',
378 ['{image_spec}', '{snapshot_name}'], 5.0)
379 @RESTController.Resource('POST')
380 @UpdatePermission
381 @allow_empty_body
382 def rollback(self, image_spec, snapshot_name):
383 def _rollback(ioctx, img, snapshot_name):
384 img.rollback_to_snap(snapshot_name)
385
386 pool_name, namespace, image_name = parse_image_spec(image_spec)
387 return rbd_image_call(pool_name, namespace, image_name, _rollback, snapshot_name)
388
389 @RbdTask('clone',
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')
395 @allow_empty_body
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):
399 """
400 Clones a snapshot to an image
401 """
402
403 pool_name, namespace, image_name = parse_image_spec(image_spec)
404
405 def _parent_clone(p_ioctx):
406 def _clone(ioctx):
407 # Set order
408 l_order = None
409 if obj_size and obj_size > 0:
410 l_order = int(round(math.log(float(obj_size), 2)))
411
412 # Set features
413 feature_bitmask = format_features(features)
414
415 rbd_inst = rbd.RBD()
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)
419
420 RbdConfiguration(pool_ioctx=ioctx, image_name=child_image_name).set_configuration(
421 configuration)
422
423 return rbd_call(child_pool_name, child_namespace, _clone)
424
425 rbd_call(pool_name, namespace, _parent_clone)
426
427
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"
432
433 def __init__(self):
434 super().__init__()
435 self.rbd_inst = rbd.RBD()
436
437 @ViewCache()
438 def _trash_pool_list(self, pool_name):
439 with mgr.rados.open_ioctx(pool_name) as ioctx:
440 result = []
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)
447 for trash in images:
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())
453 result.append(trash)
454 return result
455
456 def _trash_list(self, pool_name=None):
457 if pool_name:
458 pools = [pool_name]
459 else:
460 pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
461
462 result = []
463 for pool in pools:
464 # pylint: disable=unbalanced-tuple-unpacking
465 status, value = self._trash_pool_list(pool)
466 result.append({'status': status, 'value': value, 'pool_name': pool})
467 return result
468
469 @handle_rbd_error()
470 @handle_rados_error('pool')
471 @EndpointDoc("Get RBD Trash Details by pool name",
472 parameters={
473 'pool_name': (str, 'Name of the pool'),
474 },
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)
479
480 @handle_rbd_error()
481 @handle_rados_error('pool')
482 @RbdTask('trash/purge', ['{pool_name}'], 2.0)
483 @RESTController.Collection('POST', query_params=['pool_name'])
484 @DeletePermission
485 @allow_empty_body
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)
490
491 for pool in pools:
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)
498
499 @RbdTask('trash/restore', ['{image_id_spec}', '{new_image_name}'], 2.0)
500 @RESTController.Resource('POST')
501 @CreatePermission
502 @allow_empty_body
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,
507 new_image_name)
508
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.
514 """
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)))
518
519
520 @APIRouter('/block/pool/{pool_name}/namespace', Scope.RBD_IMAGE)
521 @APIDoc("RBD Namespace Management API", "RbdNamespace")
522 class RbdNamespace(RESTController):
523
524 def __init__(self):
525 super().__init__()
526 self.rbd_inst = rbd.RBD()
527
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',
535 component='rbd')
536 return self.rbd_inst.namespace_create(ioctx, namespace)
537
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)
542 if images:
543 raise DashboardException(
544 msg='Namespace contains images which must be deleted first',
545 code='namespace_contains_images',
546 component='rbd')
547 return self.rbd_inst.namespace_remove(ioctx, namespace)
548
549 def list(self, pool_name):
550 with mgr.rados.open_ioctx(pool_name) as ioctx:
551 result = []
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)
556 result.append({
557 'namespace': namespace,
558 'num_images': len(images) if images else 0
559 })
560 return result