1 # -*- coding: utf-8 -*-
2 # pylint: disable=unused-argument
5 from enum
import IntEnum
12 from ..exceptions
import DashboardException
13 from ..plugins
.ttl_cache
import ttl_cache
14 from .ceph_service
import CephService
17 from typing
import List
, Optional
19 pass # For typing only
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",
35 class MIRROR_IMAGE_MODE(IntEnum
):
36 journal
= rbd
.RBD_MIRROR_IMAGE_MODE_JOURNAL
37 snapshot
= rbd
.RBD_MIRROR_IMAGE_MODE_SNAPSHOT
40 def _rbd_support_remote(method_name
: str, *args
, **kwargs
):
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}')
49 def format_bitmask(features
):
53 @DISABLEDOCTEST: >>> format_bitmask(45)
54 ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']
56 names
= [val
for key
, val
in RBD_FEATURES_NAME_MAPPING
.items()
57 if key
& features
== key
]
61 def format_features(features
):
63 Converts the features list to bitmask:
65 @DISABLEDOCTEST: >>> format_features(['deep-flatten', 'exclusive-lock',
66 'layering', 'object-map'])
69 @DISABLEDOCTEST: >>> format_features(None) is None
72 @DISABLEDOCTEST: >>> format_features('deep-flatten, exclusive-lock')
75 if isinstance(features
, str):
76 features
= features
.split(',')
78 if not isinstance(features
, list):
82 for key
, value
in RBD_FEATURES_NAME_MAPPING
.items():
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
)
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)
98 pool_name
, namespace
= namespace_spec
, None
99 return pool_name
, namespace
, image_name
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
)
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
)
113 return rbd_call(pool_name
, namespace
, _ioctx_func
, image_name
, func
, *args
, **kwargs
)
116 class RbdConfiguration(object):
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
129 def _ensure_prefix(option
):
131 return option
if option
.startswith('conf_') else 'conf_' + option
134 # type: () -> List[dict]
136 if self
._image
_name
: # image config
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()
143 with rbd
.Image(ioctx
, self
._image
_name
) as image
:
144 result
= image
.config_list()
145 except rbd
.ImageNotFound
:
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.
154 # This is only a workaround for https://tracker.ceph.com/issues/43771 which
155 # already got rejected as not worth the effort.
157 # Are more complete workaround for the dashboard will be implemented with
158 # https://tracker.ceph.com/issues/44224
160 # @TODO: If #44224 is addressed remove this workaround
162 result
= self
._rbd
.config_list(ioctx
)
166 ioctx
= mgr
.rados
.open_ioctx(self
._pool
_name
)
167 ioctx
.set_namespace(self
._namespace
)
169 ioctx
= self
._pool
_ioctx
173 def get(self
, option_name
):
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
)
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
)
183 def set(self
, option_name
, option_value
):
184 # type: (str, str) -> None
186 option_value
= str(option_value
)
187 option_name
= self
._ensure
_prefix
(option_name
)
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
195 image_ioctx
= self
._image
_ioctx
197 image_ioctx
= rbd
.Image(pool_ioctx
, self
._image
_name
)
198 image_ioctx
.__enter
__() # type: ignore
201 image_ioctx
.metadata_set(option_name
, option_value
) # type: ignore
203 self
._rbd
.pool_metadata_set(pool_ioctx
, option_name
, option_value
)
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
208 pool_ioctx
.__exit
__(None, None, None) # type: ignore
210 def remove(self
, option_name
):
212 Removes an option by name. Will not raise an error, if the option hasn't been found.
213 :type option_name str
218 with rbd
.Image(ioctx
, self
._image
_name
) as image
:
219 image
.metadata_remove(option_name
)
221 self
._rbd
.pool_metadata_remove(ioctx
, option_name
)
225 option_name
= self
._ensure
_prefix
(option_name
)
228 with mgr
.rados
.open_ioctx(self
._pool
_name
) as pool_ioctx
:
229 pool_ioctx
.set_namespace(self
._namespace
)
232 _remove(self
._pool
_ioctx
)
234 def set_configuration(self
, configuration
):
236 for option_name
, option_value
in configuration
.items():
237 if option_value
is not None:
238 self
.set(option_name
, option_value
)
240 self
.remove(option_name
)
243 class RbdService(object):
244 _rbd_inst
= rbd
.RBD()
247 def _rbd_disk_usage(cls
, image
, snaps
, whole_object
=True):
248 class DUCallback(object):
252 def __call__(self
, offset
, length
, exists
):
254 self
.used_size
+= length
259 for _
, size
, name
in snaps
:
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
268 return total_used_size
, snap_map
271 def _rbd_image(cls
, ioctx
, pool_name
, namespace
, image_name
): # pylint: disable=R0912
272 with rbd
.Image(ioctx
, image_name
) as img
:
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
285 stat
['mirror_mode'] = 'unknown'
287 stat
['name'] = image_name
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
293 stat
['unique_id'] = get_image_spec(pool_name
, namespace
, img
.id())
294 stat
['id'] = img
.id()
295 stat
['image_format'] = 2
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
)
303 # the following keys are deprecated
304 del stat
['parent_pool']
305 del stat
['parent_name']
307 stat
['timestamp'] = "{}Z".format(img
.create_timestamp()
310 stat
['stripe_count'] = img
.stripe_count()
311 stat
['stripe_unit'] = img
.stripe_unit()
313 data_pool_name
= CephService
.get_pool_name_from_id(
315 if data_pool_name
== pool_name
:
316 data_pool_name
= None
317 stat
['data_pool'] = data_pool_name
320 stat
['parent'] = img
.get_parent_image_spec()
321 except rbd
.ImageNotFound
:
323 stat
['parent'] = None
326 stat
['snapshots'] = []
327 for snap
in img
.list_snaps():
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}')
333 snap
['timestamp'] = "{}Z".format(
334 img
.get_snap_timestamp(snap
['id']).isoformat())
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'] = []
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
349 stat
['snapshots'].append(snap
)
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
(
362 stat
['total_disk_usage'] = total_prov_bytes
363 for snap
, prov_bytes
in snaps_prov_bytes
.items():
365 stat
['disk_usage'] = prov_bytes
367 for ss
in stat
['snapshots']:
368 if ss
['name'] == snap
:
369 ss
['disk_usage'] = prov_bytes
372 stat
['total_disk_usage'] = None
373 stat
['disk_usage'] = None
375 stat
['configuration'] = RbdConfiguration(
376 pool_ioctx
=ioctx
, image_name
=image_name
, image_ioctx
=img
).list()
382 def get_ioctx(cls
, pool_name
, namespace
=''):
383 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
384 ioctx
.set_namespace(namespace
)
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.
392 ioctx
= cls
.get_ioctx(pool_name
, namespace
)
393 images
= cls
._rbd
_inst
.list2(ioctx
)
398 def _pool_namespaces(cls
, pool_name
, namespace
=None):
401 namespaces
= [namespace
]
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('')
410 def _rbd_image_stat(cls
, ioctx
, pool_name
, namespace
, image_name
):
411 return cls
._rbd
_image
(ioctx
, pool_name
, namespace
, image_name
)
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
)
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())
425 raise rbd
.ImageNotFound('No image {} in status `REMOVING` found.'.format(img_spec
),
429 def _rbd_pool_image_refs(cls
, pool_names
: List
[str], namespace
: Optional
[str] = None):
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
)
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 = ''):
445 # let's use -1 to denotate we want ALL images for now. Iscsi currently gathers
446 # all images therefore, we need this.
448 raise DashboardException(msg
=f
'Wrong limit value {limit}', code
=400)
450 refs
= cls
._rbd
_pool
_image
_refs
(pool_names
, namespace
)
452 # transform to list so that we can count
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
)
465 descending
= sort
[0] == '-'
467 if sort_by
not in ['name', 'pool_name', 'namespace']:
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'])
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.
481 stat
= cls
._rbd
_image
_stat
_removing
(
482 ioctx
, image_ref
['pool_name'], image_ref
['namespace'], image_ref
['id'])
483 except rbd
.ImageNotFound
:
486 return result
, len(image_refs
)
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
)
493 ioctx
.set_namespace(namespace
)
495 return cls
._rbd
_image
(ioctx
, pool_name
, namespace
, image_name
)
496 except rbd
.ImageNotFound
:
497 raise cherrypy
.HTTPError(404, 'Image not found')
500 class RbdSnapshotService(object):
503 def remove_snapshot(cls
, image_spec
, snapshot_name
, unprotect
=False):
504 def _remove_snapshot(ioctx
, img
, snapshot_name
, unprotect
):
506 img
.unprotect_snap(snapshot_name
)
507 img
.remove_snap(snapshot_name
)
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
)
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}')
522 return f
'{self.amount}{self.unit}'
525 class RbdMirroringService
:
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
))
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
))
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
))
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())
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())
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
)))
558 def snapshot_schedule_remove(cls
, image_spec
: str):
559 _rbd_support_remote('mirror_snapshot_schedule_remove', image_spec
)