]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/rbd.py
bump version to 15.2.1-pve1
[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 from __future__ import absolute_import
5
6 import math
7 from functools import partial
8 from datetime import datetime
9
10 import rbd
11
12 from . import ApiController, RESTController, Task, UpdatePermission, \
13 DeletePermission, CreatePermission
14 from .. import mgr
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
23
24
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
33
34
35 def _sort_features(features, enable=True):
36 """
37 Sorts image features according to feature dependencies:
38
39 object-map depends on exclusive-lock
40 journaling depends on exclusive-lock
41 fast-diff depends on object-map
42 """
43 ORDER = ['exclusive-lock', 'journaling', 'object-map', 'fast-diff'] # noqa: N806
44
45 def key_func(feat):
46 try:
47 return ORDER.index(feat)
48 except ValueError:
49 return id(feat)
50
51 features.sort(key=key_func, reverse=not enable)
52
53
54 @ApiController('/block/image', Scope.RBD_IMAGE)
55 class Rbd(RESTController):
56
57 # set of image features that can be enable on existing images
58 ALLOW_ENABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "journaling"}
59
60 # set of image features that can be disabled on existing images
61 ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten",
62 "journaling"}
63
64 def _rbd_list(self, pool_name=None):
65 if pool_name:
66 pools = [pool_name]
67 else:
68 pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
69
70 result = []
71 for pool in pools:
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})
78 return result
79
80 @handle_rbd_error()
81 @handle_rados_error('pool')
82 def list(self, pool_name=None):
83 return self._rbd_list(pool_name)
84
85 @handle_rbd_error()
86 @handle_rados_error('pool')
87 def get(self, image_spec):
88 return RbdService.get_image(image_spec)
89
90 @RbdTask('create',
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):
94
95 size = int(size)
96
97 def _create(ioctx):
98 rbd_inst = rbd.RBD()
99
100 # Set order
101 l_order = None
102 if obj_size and obj_size > 0:
103 l_order = int(round(math.log(float(obj_size), 2)))
104
105 # Set features
106 feature_bitmask = format_features(features)
107
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)
113
114 rbd_call(pool_name, namespace, _create)
115
116 @RbdTask('delete', ['{image_spec}'], 2.0)
117 def delete(self, image_spec):
118 pool_name, namespace, image_name = parse_image_spec(image_spec)
119
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'])
124
125 rbd_inst = rbd.RBD()
126 return rbd_call(pool_name, namespace, rbd_inst.remove, image_name)
127
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)
131
132 def _edit(ioctx, image):
133 rbd_inst = rbd.RBD()
134 # check rename image
135 if name and name != image_name:
136 rbd_inst.rename(ioctx, image_name, name)
137
138 # check resize
139 if size and size != image.size():
140 image.resize(size)
141
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()):
150 continue
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()):
158 continue
159 f_bitmask = format_features([feature])
160 image.update_features(f_bitmask, True)
161
162 RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).set_configuration(
163 configuration)
164
165 return rbd_image_call(pool_name, namespace, image_name, _edit)
166
167 @RbdTask('copy',
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)
177
178 def _src_copy(s_ioctx, s_img):
179 def _copy(d_ioctx):
180 # Set order
181 l_order = None
182 if obj_size and obj_size > 0:
183 l_order = int(round(math.log(float(obj_size), 2)))
184
185 # Set features
186 feature_bitmask = format_features(features)
187
188 if snapshot_name:
189 s_img.set_snap(snapshot_name)
190
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(
194 configuration)
195
196 return rbd_call(dest_pool_name, dest_namespace, _copy)
197
198 return rbd_image_call(pool_name, namespace, image_name, _src_copy)
199
200 @RbdTask('flatten', ['{image_spec}'], 2.0)
201 @RESTController.Resource('POST')
202 @UpdatePermission
203 def flatten(self, image_spec):
204
205 def _flatten(ioctx, image):
206 image.flatten()
207
208 pool_name, namespace, image_name = parse_image_spec(image_spec)
209 return rbd_image_call(pool_name, namespace, image_name, _flatten)
210
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))
215
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.
222 """
223 pool_name, namespace, image_name = parse_image_spec(image_spec)
224 rbd_inst = rbd.RBD()
225 return rbd_call(pool_name, namespace, rbd_inst.trash_move, image_name, delay)
226
227
228 @ApiController('/block/image/{image_spec}/snap', Scope.RBD_IMAGE)
229 class RbdSnapshot(RESTController):
230
231 RESOURCE_ID = "snapshot_name"
232
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)
237
238 def _create_snapshot(ioctx, img, snapshot_name):
239 img.create_snap(snapshot_name)
240
241 return rbd_image_call(pool_name, namespace, image_name, _create_snapshot,
242 snapshot_name)
243
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)
248
249 @RbdTask('snap/edit',
250 ['{image_spec}', '{snapshot_name}'], 4.0)
251 def set(self, image_spec, snapshot_name, new_snap_name=None,
252 is_protected=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):
259 if is_protected:
260 img.protect_snap(snapshot_name)
261 else:
262 img.unprotect_snap(snapshot_name)
263
264 pool_name, namespace, image_name = parse_image_spec(image_spec)
265 return rbd_image_call(pool_name, namespace, image_name, _edit, snapshot_name)
266
267 @RbdTask('snap/rollback',
268 ['{image_spec}', '{snapshot_name}'], 5.0)
269 @RESTController.Resource('POST')
270 @UpdatePermission
271 def rollback(self, image_spec, snapshot_name):
272 def _rollback(ioctx, img, snapshot_name):
273 img.rollback_to_snap(snapshot_name)
274
275 pool_name, namespace, image_name = parse_image_spec(image_spec)
276 return rbd_image_call(pool_name, namespace, image_name, _rollback, snapshot_name)
277
278 @RbdTask('clone',
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):
287 """
288 Clones a snapshot to an image
289 """
290
291 pool_name, namespace, image_name = parse_image_spec(image_spec)
292
293 def _parent_clone(p_ioctx):
294 def _clone(ioctx):
295 # Set order
296 l_order = None
297 if obj_size and obj_size > 0:
298 l_order = int(round(math.log(float(obj_size), 2)))
299
300 # Set features
301 feature_bitmask = format_features(features)
302
303 rbd_inst = rbd.RBD()
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)
307
308 RbdConfiguration(pool_ioctx=ioctx, image_name=child_image_name).set_configuration(
309 configuration)
310
311 return rbd_call(child_pool_name, child_namespace, _clone)
312
313 rbd_call(pool_name, namespace, _parent_clone)
314
315
316 @ApiController('/block/image/trash', Scope.RBD_IMAGE)
317 class RbdTrash(RESTController):
318 RESOURCE_ID = "image_id_spec"
319 rbd_inst = rbd.RBD()
320
321 @ViewCache()
322 def _trash_pool_list(self, pool_name):
323 with mgr.rados.open_ioctx(pool_name) as ioctx:
324 result = []
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)
331 for trash in images:
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())
337 result.append(trash)
338 return result
339
340 def _trash_list(self, pool_name=None):
341 if pool_name:
342 pools = [pool_name]
343 else:
344 pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
345
346 result = []
347 for pool in pools:
348 # pylint: disable=unbalanced-tuple-unpacking
349 status, value = self._trash_pool_list(pool)
350 result.append({'status': status, 'value': value, 'pool_name': pool})
351 return result
352
353 @handle_rbd_error()
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)
358
359 @handle_rbd_error()
360 @handle_rados_error('pool')
361 @RbdTask('trash/purge', ['{pool_name}'], 2.0)
362 @RESTController.Collection('POST', query_params=['pool_name'])
363 @DeletePermission
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)
368
369 for pool in pools:
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)
374
375 @RbdTask('trash/restore', ['{image_id_spec}', '{new_image_name}'], 2.0)
376 @RESTController.Resource('POST')
377 @CreatePermission
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,
382 new_image_name)
383
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.
389 """
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)))
393
394
395 @ApiController('/block/pool/{pool_name}/namespace', Scope.RBD_IMAGE)
396 class RbdNamespace(RESTController):
397 rbd_inst = rbd.RBD()
398
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',
406 component='rbd')
407 return self.rbd_inst.namespace_create(ioctx, namespace)
408
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)
413 if images:
414 raise DashboardException(
415 msg='Namespace contains images which must be deleted first',
416 code='namespace_contains_images',
417 component='rbd')
418 return self.rbd_inst.namespace_remove(ioctx, namespace)
419
420 def list(self, pool_name):
421 with mgr.rados.open_ioctx(pool_name) as ioctx:
422 result = []
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)
427 result.append({
428 'namespace': namespace,
429 'num_images': len(images) if images else 0
430 })
431 return result