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