]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/services/rbd.py
e4021735dac3e7996d3f44f321edeae97c684b47
[ceph.git] / ceph / src / pybind / mgr / dashboard / services / rbd.py
1 # -*- coding: utf-8 -*-
2 # pylint: disable=unused-argument
3 import errno
4 import json
5 from enum import IntEnum
6
7 import cherrypy
8 import rados
9 import rbd
10
11 from .. import mgr
12 from ..exceptions import DashboardException
13 from ..plugins.ttl_cache import ttl_cache
14 from .ceph_service import CephService
15
16 try:
17 from typing import List, Optional
18 except ImportError:
19 pass # For typing only
20
21
22 RBD_FEATURES_NAME_MAPPING = {
23 rbd.RBD_FEATURE_LAYERING: "layering",
24 rbd.RBD_FEATURE_STRIPINGV2: "striping",
25 rbd.RBD_FEATURE_EXCLUSIVE_LOCK: "exclusive-lock",
26 rbd.RBD_FEATURE_OBJECT_MAP: "object-map",
27 rbd.RBD_FEATURE_FAST_DIFF: "fast-diff",
28 rbd.RBD_FEATURE_DEEP_FLATTEN: "deep-flatten",
29 rbd.RBD_FEATURE_JOURNALING: "journaling",
30 rbd.RBD_FEATURE_DATA_POOL: "data-pool",
31 rbd.RBD_FEATURE_OPERATIONS: "operations",
32 }
33
34
35 class MIRROR_IMAGE_MODE(IntEnum):
36 journal = rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL
37 snapshot = rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT
38
39
40 def _rbd_support_remote(method_name: str, *args, **kwargs):
41 try:
42 return mgr.remote('rbd_support', method_name, *args, **kwargs)
43 except ImportError as ie:
44 raise DashboardException(f'rbd_support module not found {ie}')
45 except RuntimeError as ie:
46 raise DashboardException(f'rbd_support.{method_name} error: {ie}')
47
48
49 def format_bitmask(features):
50 """
51 Formats the bitmask:
52
53 @DISABLEDOCTEST: >>> format_bitmask(45)
54 ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']
55 """
56 names = [val for key, val in RBD_FEATURES_NAME_MAPPING.items()
57 if key & features == key]
58 return sorted(names)
59
60
61 def format_features(features):
62 """
63 Converts the features list to bitmask:
64
65 @DISABLEDOCTEST: >>> format_features(['deep-flatten', 'exclusive-lock',
66 'layering', 'object-map'])
67 45
68
69 @DISABLEDOCTEST: >>> format_features(None) is None
70 True
71
72 @DISABLEDOCTEST: >>> format_features('deep-flatten, exclusive-lock')
73 32
74 """
75 if isinstance(features, str):
76 features = features.split(',')
77
78 if not isinstance(features, list):
79 return None
80
81 res = 0
82 for key, value in RBD_FEATURES_NAME_MAPPING.items():
83 if value in features:
84 res = key | res
85 return res
86
87
88 def get_image_spec(pool_name, namespace, rbd_name):
89 namespace = '{}/'.format(namespace) if namespace else ''
90 return '{}/{}{}'.format(pool_name, namespace, rbd_name)
91
92
93 def parse_image_spec(image_spec):
94 namespace_spec, image_name = image_spec.rsplit('/', 1)
95 if '/' in namespace_spec:
96 pool_name, namespace = namespace_spec.rsplit('/', 1)
97 else:
98 pool_name, namespace = namespace_spec, None
99 return pool_name, namespace, image_name
100
101
102 def rbd_call(pool_name, namespace, func, *args, **kwargs):
103 with mgr.rados.open_ioctx(pool_name) as ioctx:
104 ioctx.set_namespace(namespace if namespace is not None else '')
105 return func(ioctx, *args, **kwargs)
106
107
108 def rbd_image_call(pool_name, namespace, image_name, func, *args, **kwargs):
109 def _ioctx_func(ioctx, image_name, func, *args, **kwargs):
110 with rbd.Image(ioctx, image_name) as img:
111 return func(ioctx, img, *args, **kwargs)
112
113 return rbd_call(pool_name, namespace, _ioctx_func, image_name, func, *args, **kwargs)
114
115
116 class RbdConfiguration(object):
117 _rbd = rbd.RBD()
118
119 def __init__(self, pool_name: str = '', namespace: str = '', image_name: str = '',
120 pool_ioctx: Optional[rados.Ioctx] = None, image_ioctx: Optional[rbd.Image] = None):
121 assert bool(pool_name) != bool(pool_ioctx) # xor
122 self._pool_name = pool_name
123 self._namespace = namespace if namespace is not None else ''
124 self._image_name = image_name
125 self._pool_ioctx = pool_ioctx
126 self._image_ioctx = image_ioctx
127
128 @staticmethod
129 def _ensure_prefix(option):
130 # type: (str) -> str
131 return option if option.startswith('conf_') else 'conf_' + option
132
133 def list(self):
134 # type: () -> List[dict]
135 def _list(ioctx):
136 if self._image_name: # image config
137 try:
138 # No need to open the context of the image again
139 # if we already did open it.
140 if self._image_ioctx:
141 result = self._image_ioctx.config_list()
142 else:
143 with rbd.Image(ioctx, self._image_name) as image:
144 result = image.config_list()
145 except rbd.ImageNotFound:
146 result = []
147 else: # pool config
148 pg_status = list(CephService.get_pool_pg_status(self._pool_name).keys())
149 if len(pg_status) == 1 and 'incomplete' in pg_status[0]:
150 # If config_list would be called with ioctx if it's a bad pool,
151 # the dashboard would stop working, waiting for the response
152 # that would not happen.
153 #
154 # This is only a workaround for https://tracker.ceph.com/issues/43771 which
155 # already got rejected as not worth the effort.
156 #
157 # Are more complete workaround for the dashboard will be implemented with
158 # https://tracker.ceph.com/issues/44224
159 #
160 # @TODO: If #44224 is addressed remove this workaround
161 return []
162 result = self._rbd.config_list(ioctx)
163 return list(result)
164
165 if self._pool_name:
166 ioctx = mgr.rados.open_ioctx(self._pool_name)
167 ioctx.set_namespace(self._namespace)
168 else:
169 ioctx = self._pool_ioctx
170
171 return _list(ioctx)
172
173 def get(self, option_name):
174 # type: (str) -> str
175 option_name = self._ensure_prefix(option_name)
176 with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
177 pool_ioctx.set_namespace(self._namespace)
178 if self._image_name:
179 with rbd.Image(pool_ioctx, self._image_name) as image:
180 return image.metadata_get(option_name)
181 return self._rbd.pool_metadata_get(pool_ioctx, option_name)
182
183 def set(self, option_name, option_value):
184 # type: (str, str) -> None
185
186 option_value = str(option_value)
187 option_name = self._ensure_prefix(option_name)
188
189 pool_ioctx = self._pool_ioctx
190 if self._pool_name: # open ioctx
191 pool_ioctx = mgr.rados.open_ioctx(self._pool_name)
192 pool_ioctx.__enter__() # type: ignore
193 pool_ioctx.set_namespace(self._namespace) # type: ignore
194
195 image_ioctx = self._image_ioctx
196 if self._image_name:
197 image_ioctx = rbd.Image(pool_ioctx, self._image_name)
198 image_ioctx.__enter__() # type: ignore
199
200 if image_ioctx:
201 image_ioctx.metadata_set(option_name, option_value) # type: ignore
202 else:
203 self._rbd.pool_metadata_set(pool_ioctx, option_name, option_value)
204
205 if self._image_name: # Name provided, so we opened it and now have to close it
206 image_ioctx.__exit__(None, None, None) # type: ignore
207 if self._pool_name:
208 pool_ioctx.__exit__(None, None, None) # type: ignore
209
210 def remove(self, option_name):
211 """
212 Removes an option by name. Will not raise an error, if the option hasn't been found.
213 :type option_name str
214 """
215 def _remove(ioctx):
216 try:
217 if self._image_name:
218 with rbd.Image(ioctx, self._image_name) as image:
219 image.metadata_remove(option_name)
220 else:
221 self._rbd.pool_metadata_remove(ioctx, option_name)
222 except KeyError:
223 pass
224
225 option_name = self._ensure_prefix(option_name)
226
227 if self._pool_name:
228 with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
229 pool_ioctx.set_namespace(self._namespace)
230 _remove(pool_ioctx)
231 else:
232 _remove(self._pool_ioctx)
233
234 def set_configuration(self, configuration):
235 if configuration:
236 for option_name, option_value in configuration.items():
237 if option_value is not None:
238 self.set(option_name, option_value)
239 else:
240 self.remove(option_name)
241
242
243 class RbdService(object):
244 _rbd_inst = rbd.RBD()
245
246 @classmethod
247 def _rbd_disk_usage(cls, image, snaps, whole_object=True):
248 class DUCallback(object):
249 def __init__(self):
250 self.used_size = 0
251
252 def __call__(self, offset, length, exists):
253 if exists:
254 self.used_size += length
255
256 snap_map = {}
257 prev_snap = None
258 total_used_size = 0
259 for _, size, name in snaps:
260 image.set_snap(name)
261 du_callb = DUCallback()
262 image.diff_iterate(0, size, prev_snap, du_callb,
263 whole_object=whole_object)
264 snap_map[name] = du_callb.used_size
265 total_used_size += du_callb.used_size
266 prev_snap = name
267
268 return total_used_size, snap_map
269
270 @classmethod
271 def _rbd_image(cls, ioctx, pool_name, namespace, image_name): # pylint: disable=R0912
272 with rbd.Image(ioctx, image_name) as img:
273 stat = img.stat()
274 mirror_mode = img.mirror_image_get_mode()
275 if mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL:
276 stat['mirror_mode'] = 'journal'
277 elif mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
278 stat['mirror_mode'] = 'snapshot'
279 schedule_status = json.loads(_rbd_support_remote(
280 'mirror_snapshot_schedule_status')[1])
281 for scheduled_image in schedule_status['scheduled_images']:
282 if scheduled_image['image'] == get_image_spec(pool_name, namespace, image_name):
283 stat['schedule_info'] = scheduled_image
284 else:
285 stat['mirror_mode'] = 'unknown'
286
287 stat['name'] = image_name
288 if img.old_format():
289 stat['unique_id'] = get_image_spec(pool_name, namespace, stat['block_name_prefix'])
290 stat['id'] = stat['unique_id']
291 stat['image_format'] = 1
292 else:
293 stat['unique_id'] = get_image_spec(pool_name, namespace, img.id())
294 stat['id'] = img.id()
295 stat['image_format'] = 2
296
297 stat['pool_name'] = pool_name
298 stat['namespace'] = namespace
299 features = img.features()
300 stat['features'] = features
301 stat['features_name'] = format_bitmask(features)
302
303 # the following keys are deprecated
304 del stat['parent_pool']
305 del stat['parent_name']
306
307 stat['timestamp'] = "{}Z".format(img.create_timestamp()
308 .isoformat())
309
310 stat['stripe_count'] = img.stripe_count()
311 stat['stripe_unit'] = img.stripe_unit()
312
313 data_pool_name = CephService.get_pool_name_from_id(
314 img.data_pool_id())
315 if data_pool_name == pool_name:
316 data_pool_name = None
317 stat['data_pool'] = data_pool_name
318
319 try:
320 stat['parent'] = img.get_parent_image_spec()
321 except rbd.ImageNotFound:
322 # no parent image
323 stat['parent'] = None
324
325 # snapshots
326 stat['snapshots'] = []
327 for snap in img.list_snaps():
328 try:
329 snap['mirror_mode'] = MIRROR_IMAGE_MODE(img.mirror_image_get_mode()).name
330 except ValueError as ex:
331 raise DashboardException(f'Unknown RBD Mirror mode: {ex}')
332
333 snap['timestamp'] = "{}Z".format(
334 img.get_snap_timestamp(snap['id']).isoformat())
335
336 snap['is_protected'] = None
337 if mirror_mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
338 snap['is_protected'] = img.is_protected_snap(snap['name'])
339 snap['used_bytes'] = None
340 snap['children'] = []
341
342 if mirror_mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
343 img.set_snap(snap['name'])
344 for child_pool_name, child_image_name in img.list_children():
345 snap['children'].append({
346 'pool_name': child_pool_name,
347 'image_name': child_image_name
348 })
349 stat['snapshots'].append(snap)
350
351 # disk usage
352 img_flags = img.flags()
353 if 'fast-diff' in stat['features_name'] and \
354 not rbd.RBD_FLAG_FAST_DIFF_INVALID & img_flags and \
355 mirror_mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
356 snaps = [(s['id'], s['size'], s['name'])
357 for s in stat['snapshots']]
358 snaps.sort(key=lambda s: s[0])
359 snaps += [(snaps[-1][0] + 1 if snaps else 0, stat['size'], None)]
360 total_prov_bytes, snaps_prov_bytes = cls._rbd_disk_usage(
361 img, snaps, True)
362 stat['total_disk_usage'] = total_prov_bytes
363 for snap, prov_bytes in snaps_prov_bytes.items():
364 if snap is None:
365 stat['disk_usage'] = prov_bytes
366 continue
367 for ss in stat['snapshots']:
368 if ss['name'] == snap:
369 ss['disk_usage'] = prov_bytes
370 break
371 else:
372 stat['total_disk_usage'] = None
373 stat['disk_usage'] = None
374
375 stat['configuration'] = RbdConfiguration(
376 pool_ioctx=ioctx, image_name=image_name, image_ioctx=img).list()
377
378 return stat
379
380 @classmethod
381 @ttl_cache(10)
382 def get_ioctx(cls, pool_name, namespace=''):
383 ioctx = mgr.rados.open_ioctx(pool_name)
384 ioctx.set_namespace(namespace)
385 return ioctx
386
387 @classmethod
388 @ttl_cache(30)
389 def _rbd_image_refs(cls, pool_name, namespace=''):
390 # We add and set the namespace here so that we cache by ioctx and namespace.
391 images = []
392 ioctx = cls.get_ioctx(pool_name, namespace)
393 images = cls._rbd_inst.list2(ioctx)
394 return images
395
396 @classmethod
397 @ttl_cache(30)
398 def _pool_namespaces(cls, pool_name, namespace=None):
399 namespaces = []
400 if namespace:
401 namespaces = [namespace]
402 else:
403 ioctx = cls.get_ioctx(pool_name, namespace=rados.LIBRADOS_ALL_NSPACES)
404 namespaces = cls._rbd_inst.namespace_list(ioctx)
405 # images without namespace
406 namespaces.append('')
407 return namespaces
408
409 @classmethod
410 def _rbd_image_stat(cls, ioctx, pool_name, namespace, image_name):
411 return cls._rbd_image(ioctx, pool_name, namespace, image_name)
412
413 @classmethod
414 def _rbd_image_stat_removing(cls, ioctx, pool_name, namespace, image_id):
415 img = cls._rbd_inst.trash_get(ioctx, image_id)
416 img_spec = get_image_spec(pool_name, namespace, image_id)
417
418 if img['source'] == 'REMOVING':
419 img['unique_id'] = img_spec
420 img['pool_name'] = pool_name
421 img['namespace'] = namespace
422 img['deletion_time'] = "{}Z".format(img['deletion_time'].isoformat())
423 img['deferment_end_time'] = "{}Z".format(img['deferment_end_time'].isoformat())
424 return img
425 raise rbd.ImageNotFound('No image {} in status `REMOVING` found.'.format(img_spec),
426 errno=errno.ENOENT)
427
428 @classmethod
429 def _rbd_pool_image_refs(cls, pool_names: List[str], namespace: Optional[str] = None):
430 joint_refs = []
431 for pool in pool_names:
432 for current_namespace in cls._pool_namespaces(pool, namespace=namespace):
433 image_refs = cls._rbd_image_refs(pool, current_namespace)
434 for image in image_refs:
435 image['namespace'] = current_namespace
436 image['pool_name'] = pool
437 joint_refs.append(image)
438 return joint_refs
439
440 @classmethod
441 def rbd_pool_list(cls, pool_names: List[str], namespace: Optional[str] = None, offset: int = 0,
442 limit: int = 5, search: str = '', sort: str = ''):
443 offset = int(offset)
444 limit = int(limit)
445 # let's use -1 to denotate we want ALL images for now. Iscsi currently gathers
446 # all images therefore, we need this.
447 if limit < -1:
448 raise DashboardException(msg=f'Wrong limit value {limit}', code=400)
449
450 refs = cls._rbd_pool_image_refs(pool_names, namespace)
451 image_refs = []
452 # transform to list so that we can count
453 for ref in refs:
454 if search in ref['name']:
455 image_refs.append(ref)
456 elif search in ref['pool_name']:
457 image_refs.append(ref)
458 elif search in ref['namespace']:
459 image_refs.append(ref)
460
461 result = []
462 end = offset + limit
463 if len(sort) < 2:
464 sort = '+name'
465 descending = sort[0] == '-'
466 sort_by = sort[1:]
467 if sort_by not in ['name', 'pool_name', 'namespace']:
468 sort_by = 'name'
469 if limit == -1:
470 end = len(image_refs)
471 for image_ref in sorted(image_refs, key=lambda v: v[sort_by],
472 reverse=descending)[offset:end]:
473 ioctx = cls.get_ioctx(image_ref['pool_name'], namespace=image_ref['namespace'])
474 try:
475 stat = cls._rbd_image_stat(
476 ioctx, image_ref['pool_name'], image_ref['namespace'], image_ref['name'])
477 except rbd.ImageNotFound:
478 # Check if the RBD has been deleted partially. This happens for example if
479 # the deletion process of the RBD has been started and was interrupted.
480 try:
481 stat = cls._rbd_image_stat_removing(
482 ioctx, image_ref['pool_name'], image_ref['namespace'], image_ref['id'])
483 except rbd.ImageNotFound:
484 continue
485 result.append(stat)
486 return result, len(image_refs)
487
488 @classmethod
489 def get_image(cls, image_spec):
490 pool_name, namespace, image_name = parse_image_spec(image_spec)
491 ioctx = mgr.rados.open_ioctx(pool_name)
492 if namespace:
493 ioctx.set_namespace(namespace)
494 try:
495 return cls._rbd_image(ioctx, pool_name, namespace, image_name)
496 except rbd.ImageNotFound:
497 raise cherrypy.HTTPError(404, 'Image not found')
498
499
500 class RbdSnapshotService(object):
501
502 @classmethod
503 def remove_snapshot(cls, image_spec, snapshot_name, unprotect=False):
504 def _remove_snapshot(ioctx, img, snapshot_name, unprotect):
505 if unprotect:
506 img.unprotect_snap(snapshot_name)
507 img.remove_snap(snapshot_name)
508
509 pool_name, namespace, image_name = parse_image_spec(image_spec)
510 return rbd_image_call(pool_name, namespace, image_name,
511 _remove_snapshot, snapshot_name, unprotect)
512
513
514 class RBDSchedulerInterval:
515 def __init__(self, interval: str):
516 self.amount = int(interval[:-1])
517 self.unit = interval[-1]
518 if self.unit not in 'mhd':
519 raise ValueError(f'Invalid interval unit {self.unit}')
520
521 def __str__(self):
522 return f'{self.amount}{self.unit}'
523
524
525 class RbdMirroringService:
526
527 @classmethod
528 def enable_image(cls, image_name: str, pool_name: str, namespace: str, mode: MIRROR_IMAGE_MODE):
529 rbd_image_call(pool_name, namespace, image_name,
530 lambda ioctx, image: image.mirror_image_enable(mode))
531
532 @classmethod
533 def disable_image(cls, image_name: str, pool_name: str, namespace: str, force: bool = False):
534 rbd_image_call(pool_name, namespace, image_name,
535 lambda ioctx, image: image.mirror_image_disable(force))
536
537 @classmethod
538 def promote_image(cls, image_name: str, pool_name: str, namespace: str, force: bool = False):
539 rbd_image_call(pool_name, namespace, image_name,
540 lambda ioctx, image: image.mirror_image_promote(force))
541
542 @classmethod
543 def demote_image(cls, image_name: str, pool_name: str, namespace: str):
544 rbd_image_call(pool_name, namespace, image_name,
545 lambda ioctx, image: image.mirror_image_demote())
546
547 @classmethod
548 def resync_image(cls, image_name: str, pool_name: str, namespace: str):
549 rbd_image_call(pool_name, namespace, image_name,
550 lambda ioctx, image: image.mirror_image_resync())
551
552 @classmethod
553 def snapshot_schedule_add(cls, image_spec: str, interval: str):
554 _rbd_support_remote('mirror_snapshot_schedule_add', image_spec,
555 str(RBDSchedulerInterval(interval)))
556
557 @classmethod
558 def snapshot_schedule_remove(cls, image_spec: str):
559 _rbd_support_remote('mirror_snapshot_schedule_remove', image_spec)