]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/rbd.py
import quincy beta 17.1.0
[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 rbd
11
12 from .. import mgr
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, \
19 rbd_image_call
20 from ..tools import ViewCache, str_to_bool
21 from . import APIDoc, APIRouter, CreatePermission, DeletePermission, \
22 EndpointDoc, RESTController, Task, UpdatePermission, allow_empty_body
23
24 logger = logging.getLogger(__name__)
25
26 RBD_SCHEMA = ([{
27 "status": (int, 'Status of the image'),
28 "value": ([str], ''),
29 "pool_name": (str, 'pool name')
30 }])
31
32 RBD_TRASH_SCHEMA = [{
33 "status": (int, ''),
34 "value": ([str], ''),
35 "pool_name": (str, 'pool name')
36 }]
37
38
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
47
48
49 def _sort_features(features, enable=True):
50 """
51 Sorts image features according to feature dependencies:
52
53 object-map depends on exclusive-lock
54 journaling depends on exclusive-lock
55 fast-diff depends on object-map
56 """
57 ORDER = ['exclusive-lock', 'journaling', 'object-map', 'fast-diff'] # noqa: N806
58
59 def key_func(feat):
60 try:
61 return ORDER.index(feat)
62 except ValueError:
63 return id(feat)
64
65 features.sort(key=key_func, reverse=not enable)
66
67
68 @APIRouter('/block/image', Scope.RBD_IMAGE)
69 @APIDoc("RBD Management API", "Rbd")
70 class Rbd(RESTController):
71
72 # set of image features that can be enable on existing images
73 ALLOW_ENABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "journaling"}
74
75 # set of image features that can be disabled on existing images
76 ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten",
77 "journaling"}
78
79 def _rbd_list(self, pool_name=None):
80 if pool_name:
81 pools = [pool_name]
82 else:
83 pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
84
85 result = []
86 for pool in pools:
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})
93 return result
94
95 @handle_rbd_error()
96 @handle_rados_error('pool')
97 @EndpointDoc("Display Rbd Images",
98 parameters={
99 'pool_name': (str, 'Pool Name'),
100 },
101 responses={200: RBD_SCHEMA})
102 def list(self, pool_name=None):
103 return self._rbd_list(pool_name)
104
105 @handle_rbd_error()
106 @handle_rados_error('pool')
107 def get(self, image_spec):
108 return RbdService.get_image(image_spec)
109
110 @RbdTask('create',
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):
114
115 size = int(size)
116
117 def _create(ioctx):
118 rbd_inst = rbd.RBD()
119
120 # Set order
121 l_order = None
122 if obj_size and obj_size > 0:
123 l_order = int(round(math.log(float(obj_size), 2)))
124
125 # Set features
126 feature_bitmask = format_features(features)
127
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)
133
134 rbd_call(pool_name, namespace, _create)
135
136 @RbdTask('delete', ['{image_spec}'], 2.0)
137 def delete(self, image_spec):
138 pool_name, namespace, image_name = parse_image_spec(image_spec)
139
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'])
144
145 rbd_inst = rbd.RBD()
146 return rbd_call(pool_name, namespace, rbd_inst.remove, image_name)
147
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)
151
152 def _edit(ioctx, image):
153 rbd_inst = rbd.RBD()
154 # check rename image
155 if name and name != image_name:
156 rbd_inst.rename(ioctx, image_name, name)
157
158 # check resize
159 if size and size != image.size():
160 image.resize(size)
161
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()):
170 continue
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()):
178 continue
179 f_bitmask = format_features([feature])
180 image.update_features(f_bitmask, True)
181
182 RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).set_configuration(
183 configuration)
184
185 return rbd_image_call(pool_name, namespace, image_name, _edit)
186
187 @RbdTask('copy',
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')
193 @allow_empty_body
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)
198
199 def _src_copy(s_ioctx, s_img):
200 def _copy(d_ioctx):
201 # Set order
202 l_order = None
203 if obj_size and obj_size > 0:
204 l_order = int(round(math.log(float(obj_size), 2)))
205
206 # Set features
207 feature_bitmask = format_features(features)
208
209 if snapshot_name:
210 s_img.set_snap(snapshot_name)
211
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(
215 configuration)
216
217 return rbd_call(dest_pool_name, dest_namespace, _copy)
218
219 return rbd_image_call(pool_name, namespace, image_name, _src_copy)
220
221 @RbdTask('flatten', ['{image_spec}'], 2.0)
222 @RESTController.Resource('POST')
223 @UpdatePermission
224 @allow_empty_body
225 def flatten(self, image_spec):
226
227 def _flatten(ioctx, image):
228 image.flatten()
229
230 pool_name, namespace, image_name = parse_image_spec(image_spec)
231 return rbd_image_call(pool_name, namespace, image_name, _flatten)
232
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))
237
238 @RESTController.Collection('GET')
239 def clone_format_version(self):
240 """Return the RBD clone format version.
241 """
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':
249 return 1
250
251 return 2
252
253 @RbdTask('trash/move', ['{image_spec}'], 2.0)
254 @RESTController.Resource('POST')
255 @allow_empty_body
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.
260 """
261 pool_name, namespace, image_name = parse_image_spec(image_spec)
262 rbd_inst = rbd.RBD()
263 return rbd_call(pool_name, namespace, rbd_inst.trash_move, image_name, delay)
264
265
266 @APIRouter('/block/image/{image_spec}/snap', Scope.RBD_IMAGE)
267 @APIDoc("RBD Snapshot Management API", "RbdSnapshot")
268 class RbdSnapshot(RESTController):
269
270 RESOURCE_ID = "snapshot_name"
271
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)
276
277 def _create_snapshot(ioctx, img, snapshot_name):
278 img.create_snap(snapshot_name)
279
280 return rbd_image_call(pool_name, namespace, image_name, _create_snapshot,
281 snapshot_name)
282
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)
287
288 @RbdTask('snap/edit',
289 ['{image_spec}', '{snapshot_name}'], 4.0)
290 def set(self, image_spec, snapshot_name, new_snap_name=None,
291 is_protected=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):
298 if is_protected:
299 img.protect_snap(snapshot_name)
300 else:
301 img.unprotect_snap(snapshot_name)
302
303 pool_name, namespace, image_name = parse_image_spec(image_spec)
304 return rbd_image_call(pool_name, namespace, image_name, _edit, snapshot_name)
305
306 @RbdTask('snap/rollback',
307 ['{image_spec}', '{snapshot_name}'], 5.0)
308 @RESTController.Resource('POST')
309 @UpdatePermission
310 @allow_empty_body
311 def rollback(self, image_spec, snapshot_name):
312 def _rollback(ioctx, img, snapshot_name):
313 img.rollback_to_snap(snapshot_name)
314
315 pool_name, namespace, image_name = parse_image_spec(image_spec)
316 return rbd_image_call(pool_name, namespace, image_name, _rollback, snapshot_name)
317
318 @RbdTask('clone',
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')
324 @allow_empty_body
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):
328 """
329 Clones a snapshot to an image
330 """
331
332 pool_name, namespace, image_name = parse_image_spec(image_spec)
333
334 def _parent_clone(p_ioctx):
335 def _clone(ioctx):
336 # Set order
337 l_order = None
338 if obj_size and obj_size > 0:
339 l_order = int(round(math.log(float(obj_size), 2)))
340
341 # Set features
342 feature_bitmask = format_features(features)
343
344 rbd_inst = rbd.RBD()
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)
348
349 RbdConfiguration(pool_ioctx=ioctx, image_name=child_image_name).set_configuration(
350 configuration)
351
352 return rbd_call(child_pool_name, child_namespace, _clone)
353
354 rbd_call(pool_name, namespace, _parent_clone)
355
356
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"
361
362 def __init__(self):
363 super().__init__()
364 self.rbd_inst = rbd.RBD()
365
366 @ViewCache()
367 def _trash_pool_list(self, pool_name):
368 with mgr.rados.open_ioctx(pool_name) as ioctx:
369 result = []
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)
376 for trash in images:
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())
382 result.append(trash)
383 return result
384
385 def _trash_list(self, pool_name=None):
386 if pool_name:
387 pools = [pool_name]
388 else:
389 pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
390
391 result = []
392 for pool in pools:
393 # pylint: disable=unbalanced-tuple-unpacking
394 status, value = self._trash_pool_list(pool)
395 result.append({'status': status, 'value': value, 'pool_name': pool})
396 return result
397
398 @handle_rbd_error()
399 @handle_rados_error('pool')
400 @EndpointDoc("Get RBD Trash Details by pool name",
401 parameters={
402 'pool_name': (str, 'Name of the pool'),
403 },
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)
408
409 @handle_rbd_error()
410 @handle_rados_error('pool')
411 @RbdTask('trash/purge', ['{pool_name}'], 2.0)
412 @RESTController.Collection('POST', query_params=['pool_name'])
413 @DeletePermission
414 @allow_empty_body
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)
419
420 for pool in pools:
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)
427
428 @RbdTask('trash/restore', ['{image_id_spec}', '{new_image_name}'], 2.0)
429 @RESTController.Resource('POST')
430 @CreatePermission
431 @allow_empty_body
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,
436 new_image_name)
437
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.
443 """
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)))
447
448
449 @APIRouter('/block/pool/{pool_name}/namespace', Scope.RBD_IMAGE)
450 @APIDoc("RBD Namespace Management API", "RbdNamespace")
451 class RbdNamespace(RESTController):
452
453 def __init__(self):
454 super().__init__()
455 self.rbd_inst = rbd.RBD()
456
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',
464 component='rbd')
465 return self.rbd_inst.namespace_create(ioctx, namespace)
466
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)
471 if images:
472 raise DashboardException(
473 msg='Namespace contains images which must be deleted first',
474 code='namespace_contains_images',
475 component='rbd')
476 return self.rbd_inst.namespace_remove(ioctx, namespace)
477
478 def list(self, pool_name):
479 with mgr.rados.open_ioctx(pool_name) as ioctx:
480 result = []
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)
485 result.append({
486 'namespace': namespace,
487 'num_images': len(images) if images else 0
488 })
489 return result