]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/services/rbd.py
bump version to 18.2.2-pve1
[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 import math
6 from enum import IntEnum
7
8 import cherrypy
9 import rados
10 import rbd
11
12 from .. import mgr
13 from ..exceptions import DashboardException
14 from ..plugins.ttl_cache import ttl_cache, ttl_cache_invalidator
15 from ._paginate import ListPaginator
16 from .ceph_service import CephService
17
18 try:
19 from typing import List, Optional
20 except ImportError:
21 pass # For typing only
22
23
24 RBD_FEATURES_NAME_MAPPING = {
25 rbd.RBD_FEATURE_LAYERING: "layering",
26 rbd.RBD_FEATURE_STRIPINGV2: "striping",
27 rbd.RBD_FEATURE_EXCLUSIVE_LOCK: "exclusive-lock",
28 rbd.RBD_FEATURE_OBJECT_MAP: "object-map",
29 rbd.RBD_FEATURE_FAST_DIFF: "fast-diff",
30 rbd.RBD_FEATURE_DEEP_FLATTEN: "deep-flatten",
31 rbd.RBD_FEATURE_JOURNALING: "journaling",
32 rbd.RBD_FEATURE_DATA_POOL: "data-pool",
33 rbd.RBD_FEATURE_OPERATIONS: "operations",
34 }
35
36 RBD_IMAGE_REFS_CACHE_REFERENCE = 'rbd_image_refs'
37 GET_IOCTX_CACHE = 'get_ioctx'
38 POOL_NAMESPACES_CACHE = 'pool_namespaces'
39
40
41 class MIRROR_IMAGE_MODE(IntEnum):
42 journal = rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL
43 snapshot = rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT
44
45
46 def _rbd_support_remote(method_name: str, *args, **kwargs):
47 try:
48 return mgr.remote('rbd_support', method_name, *args, **kwargs)
49 except ImportError as ie:
50 raise DashboardException(f'rbd_support module not found {ie}')
51 except RuntimeError as ie:
52 raise DashboardException(f'rbd_support.{method_name} error: {ie}')
53
54
55 def format_bitmask(features):
56 """
57 Formats the bitmask:
58
59 @DISABLEDOCTEST: >>> format_bitmask(45)
60 ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']
61 """
62 names = [val for key, val in RBD_FEATURES_NAME_MAPPING.items()
63 if key & features == key]
64 return sorted(names)
65
66
67 def format_features(features):
68 """
69 Converts the features list to bitmask:
70
71 @DISABLEDOCTEST: >>> format_features(['deep-flatten', 'exclusive-lock',
72 'layering', 'object-map'])
73 45
74
75 @DISABLEDOCTEST: >>> format_features(None) is None
76 True
77
78 @DISABLEDOCTEST: >>> format_features('deep-flatten, exclusive-lock')
79 32
80 """
81 if isinstance(features, str):
82 features = features.split(',')
83
84 if not isinstance(features, list):
85 return None
86
87 res = 0
88 for key, value in RBD_FEATURES_NAME_MAPPING.items():
89 if value in features:
90 res = key | res
91 return res
92
93
94 def _sort_features(features, enable=True):
95 """
96 Sorts image features according to feature dependencies:
97
98 object-map depends on exclusive-lock
99 journaling depends on exclusive-lock
100 fast-diff depends on object-map
101 """
102 ORDER = ['exclusive-lock', 'journaling', 'object-map', 'fast-diff'] # noqa: N806
103
104 def key_func(feat):
105 try:
106 return ORDER.index(feat)
107 except ValueError:
108 return id(feat)
109
110 features.sort(key=key_func, reverse=not enable)
111
112
113 def get_image_spec(pool_name, namespace, rbd_name):
114 namespace = '{}/'.format(namespace) if namespace else ''
115 return '{}/{}{}'.format(pool_name, namespace, rbd_name)
116
117
118 def parse_image_spec(image_spec):
119 namespace_spec, image_name = image_spec.rsplit('/', 1)
120 if '/' in namespace_spec:
121 pool_name, namespace = namespace_spec.rsplit('/', 1)
122 else:
123 pool_name, namespace = namespace_spec, None
124 return pool_name, namespace, image_name
125
126
127 def rbd_call(pool_name, namespace, func, *args, **kwargs):
128 with mgr.rados.open_ioctx(pool_name) as ioctx:
129 ioctx.set_namespace(namespace if namespace is not None else '')
130 return func(ioctx, *args, **kwargs)
131
132
133 def rbd_image_call(pool_name, namespace, image_name, func, *args, **kwargs):
134 def _ioctx_func(ioctx, image_name, func, *args, **kwargs):
135 with rbd.Image(ioctx, image_name) as img:
136 return func(ioctx, img, *args, **kwargs)
137
138 return rbd_call(pool_name, namespace, _ioctx_func, image_name, func, *args, **kwargs)
139
140
141 class RbdConfiguration(object):
142 _rbd = rbd.RBD()
143
144 def __init__(self, pool_name: str = '', namespace: str = '', image_name: str = '',
145 pool_ioctx: Optional[rados.Ioctx] = None, image_ioctx: Optional[rbd.Image] = None):
146 assert bool(pool_name) != bool(pool_ioctx) # xor
147 self._pool_name = pool_name
148 self._namespace = namespace if namespace is not None else ''
149 self._image_name = image_name
150 self._pool_ioctx = pool_ioctx
151 self._image_ioctx = image_ioctx
152
153 @staticmethod
154 def _ensure_prefix(option):
155 # type: (str) -> str
156 return option if option.startswith('conf_') else 'conf_' + option
157
158 def list(self):
159 # type: () -> List[dict]
160 def _list(ioctx):
161 if self._image_name: # image config
162 try:
163 # No need to open the context of the image again
164 # if we already did open it.
165 if self._image_ioctx:
166 result = self._image_ioctx.config_list()
167 else:
168 with rbd.Image(ioctx, self._image_name) as image:
169 result = image.config_list()
170 except rbd.ImageNotFound:
171 result = []
172 else: # pool config
173 pg_status = list(CephService.get_pool_pg_status(self._pool_name).keys())
174 if len(pg_status) == 1 and 'incomplete' in pg_status[0]:
175 # If config_list would be called with ioctx if it's a bad pool,
176 # the dashboard would stop working, waiting for the response
177 # that would not happen.
178 #
179 # This is only a workaround for https://tracker.ceph.com/issues/43771 which
180 # already got rejected as not worth the effort.
181 #
182 # Are more complete workaround for the dashboard will be implemented with
183 # https://tracker.ceph.com/issues/44224
184 #
185 # @TODO: If #44224 is addressed remove this workaround
186 return []
187 result = self._rbd.config_list(ioctx)
188 return list(result)
189
190 if self._pool_name:
191 ioctx = mgr.rados.open_ioctx(self._pool_name)
192 ioctx.set_namespace(self._namespace)
193 else:
194 ioctx = self._pool_ioctx
195
196 return _list(ioctx)
197
198 def get(self, option_name):
199 # type: (str) -> str
200 option_name = self._ensure_prefix(option_name)
201 with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
202 pool_ioctx.set_namespace(self._namespace)
203 if self._image_name:
204 with rbd.Image(pool_ioctx, self._image_name) as image:
205 return image.metadata_get(option_name)
206 return self._rbd.pool_metadata_get(pool_ioctx, option_name)
207
208 def set(self, option_name, option_value):
209 # type: (str, str) -> None
210
211 option_value = str(option_value)
212 option_name = self._ensure_prefix(option_name)
213
214 pool_ioctx = self._pool_ioctx
215 if self._pool_name: # open ioctx
216 pool_ioctx = mgr.rados.open_ioctx(self._pool_name)
217 pool_ioctx.__enter__() # type: ignore
218 pool_ioctx.set_namespace(self._namespace) # type: ignore
219
220 image_ioctx = self._image_ioctx
221 if self._image_name:
222 image_ioctx = rbd.Image(pool_ioctx, self._image_name)
223 image_ioctx.__enter__() # type: ignore
224
225 if image_ioctx:
226 image_ioctx.metadata_set(option_name, option_value) # type: ignore
227 else:
228 self._rbd.pool_metadata_set(pool_ioctx, option_name, option_value)
229
230 if self._image_name: # Name provided, so we opened it and now have to close it
231 image_ioctx.__exit__(None, None, None) # type: ignore
232 if self._pool_name:
233 pool_ioctx.__exit__(None, None, None) # type: ignore
234
235 def remove(self, option_name):
236 """
237 Removes an option by name. Will not raise an error, if the option hasn't been found.
238 :type option_name str
239 """
240 def _remove(ioctx):
241 try:
242 if self._image_name:
243 with rbd.Image(ioctx, self._image_name) as image:
244 image.metadata_remove(option_name)
245 else:
246 self._rbd.pool_metadata_remove(ioctx, option_name)
247 except KeyError:
248 pass
249
250 option_name = self._ensure_prefix(option_name)
251
252 if self._pool_name:
253 with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
254 pool_ioctx.set_namespace(self._namespace)
255 _remove(pool_ioctx)
256 else:
257 _remove(self._pool_ioctx)
258
259 def set_configuration(self, configuration):
260 if configuration:
261 for option_name, option_value in configuration.items():
262 if option_value is not None:
263 self.set(option_name, option_value)
264 else:
265 self.remove(option_name)
266
267
268 class RbdService(object):
269 _rbd_inst = rbd.RBD()
270
271 # set of image features that can be enable on existing images
272 ALLOW_ENABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "journaling"}
273
274 # set of image features that can be disabled on existing images
275 ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten",
276 "journaling"}
277
278 @classmethod
279 def _rbd_disk_usage(cls, image, snaps, whole_object=True):
280 class DUCallback(object):
281 def __init__(self):
282 self.used_size = 0
283
284 def __call__(self, offset, length, exists):
285 if exists:
286 self.used_size += length
287
288 snap_map = {}
289 prev_snap = None
290 total_used_size = 0
291 for _, size, name in snaps:
292 image.set_snap(name)
293 du_callb = DUCallback()
294 image.diff_iterate(0, size, prev_snap, du_callb,
295 whole_object=whole_object)
296 snap_map[name] = du_callb.used_size
297 total_used_size += du_callb.used_size
298 prev_snap = name
299
300 return total_used_size, snap_map
301
302 @classmethod
303 def _rbd_image(cls, ioctx, pool_name, namespace, image_name): # pylint: disable=R0912
304 with rbd.Image(ioctx, image_name) as img:
305 stat = img.stat()
306 mirror_info = img.mirror_image_get_info()
307 mirror_mode = img.mirror_image_get_mode()
308 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
309 stat['mirror_mode'] = 'journal'
310 elif mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
311 stat['mirror_mode'] = 'snapshot'
312 schedule_status = json.loads(_rbd_support_remote(
313 'mirror_snapshot_schedule_status')[1])
314 for scheduled_image in schedule_status['scheduled_images']:
315 if scheduled_image['image'] == get_image_spec(pool_name, namespace, image_name):
316 stat['schedule_info'] = scheduled_image
317 else:
318 stat['mirror_mode'] = 'Disabled'
319
320 stat['name'] = image_name
321
322 stat['primary'] = None
323 if mirror_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED:
324 stat['primary'] = mirror_info['primary']
325
326 if img.old_format():
327 stat['unique_id'] = get_image_spec(pool_name, namespace, stat['block_name_prefix'])
328 stat['id'] = stat['unique_id']
329 stat['image_format'] = 1
330 else:
331 stat['unique_id'] = get_image_spec(pool_name, namespace, img.id())
332 stat['id'] = img.id()
333 stat['image_format'] = 2
334
335 stat['pool_name'] = pool_name
336 stat['namespace'] = namespace
337 features = img.features()
338 stat['features'] = features
339 stat['features_name'] = format_bitmask(features)
340
341 # the following keys are deprecated
342 del stat['parent_pool']
343 del stat['parent_name']
344
345 stat['timestamp'] = "{}Z".format(img.create_timestamp()
346 .isoformat())
347
348 stat['stripe_count'] = img.stripe_count()
349 stat['stripe_unit'] = img.stripe_unit()
350
351 data_pool_name = CephService.get_pool_name_from_id(
352 img.data_pool_id())
353 if data_pool_name == pool_name:
354 data_pool_name = None
355 stat['data_pool'] = data_pool_name
356
357 stat['parent'] = cls._rbd_image_stat_parent(img)
358
359 # snapshots
360 stat['snapshots'] = []
361 for snap in img.list_snaps():
362 try:
363 snap['mirror_mode'] = MIRROR_IMAGE_MODE(img.mirror_image_get_mode()).name
364 except ValueError as ex:
365 raise DashboardException(f'Unknown RBD Mirror mode: {ex}')
366
367 snap['timestamp'] = "{}Z".format(
368 img.get_snap_timestamp(snap['id']).isoformat())
369
370 snap['is_protected'] = None
371 if mirror_mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
372 snap['is_protected'] = img.is_protected_snap(snap['name'])
373 snap['used_bytes'] = None
374 snap['children'] = []
375
376 if mirror_mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
377 img.set_snap(snap['name'])
378 for child_pool_name, child_image_name in img.list_children():
379 snap['children'].append({
380 'pool_name': child_pool_name,
381 'image_name': child_image_name
382 })
383 stat['snapshots'].append(snap)
384
385 # disk usage
386 img_flags = img.flags()
387 if 'fast-diff' in stat['features_name'] and \
388 not rbd.RBD_FLAG_FAST_DIFF_INVALID & img_flags and \
389 mirror_mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
390 snaps = [(s['id'], s['size'], s['name'])
391 for s in stat['snapshots']]
392 snaps.sort(key=lambda s: s[0])
393 snaps += [(snaps[-1][0] + 1 if snaps else 0, stat['size'], None)]
394 total_prov_bytes, snaps_prov_bytes = cls._rbd_disk_usage(
395 img, snaps, True)
396 stat['total_disk_usage'] = total_prov_bytes
397 for snap, prov_bytes in snaps_prov_bytes.items():
398 if snap is None:
399 stat['disk_usage'] = prov_bytes
400 continue
401 for ss in stat['snapshots']:
402 if ss['name'] == snap:
403 ss['disk_usage'] = prov_bytes
404 break
405 else:
406 stat['total_disk_usage'] = None
407 stat['disk_usage'] = None
408
409 stat['configuration'] = RbdConfiguration(
410 pool_ioctx=ioctx, image_name=image_name, image_ioctx=img).list()
411
412 stat['metadata'] = RbdImageMetadataService(img).list()
413
414 return stat
415
416 @classmethod
417 def _rbd_image_stat_parent(cls, img):
418 stat_parent = None
419 try:
420 stat_parent = img.get_parent_image_spec()
421 except rbd.ImageNotFound:
422 # no parent image
423 stat_parent = None
424 return stat_parent
425
426 @classmethod
427 @ttl_cache(10, label=GET_IOCTX_CACHE)
428 def get_ioctx(cls, pool_name, namespace=''):
429 ioctx = mgr.rados.open_ioctx(pool_name)
430 ioctx.set_namespace(namespace)
431 return ioctx
432
433 @classmethod
434 @ttl_cache(30, label=RBD_IMAGE_REFS_CACHE_REFERENCE)
435 def _rbd_image_refs(cls, pool_name, namespace=''):
436 # We add and set the namespace here so that we cache by ioctx and namespace.
437 images = []
438 ioctx = cls.get_ioctx(pool_name, namespace)
439 images = cls._rbd_inst.list2(ioctx)
440 return images
441
442 @classmethod
443 @ttl_cache(30, label=POOL_NAMESPACES_CACHE)
444 def _pool_namespaces(cls, pool_name, namespace=None):
445 namespaces = []
446 if namespace:
447 namespaces = [namespace]
448 else:
449 ioctx = cls.get_ioctx(pool_name, namespace=rados.LIBRADOS_ALL_NSPACES)
450 namespaces = cls._rbd_inst.namespace_list(ioctx)
451 # images without namespace
452 namespaces.append('')
453 return namespaces
454
455 @classmethod
456 def _rbd_image_stat(cls, ioctx, pool_name, namespace, image_name):
457 return cls._rbd_image(ioctx, pool_name, namespace, image_name)
458
459 @classmethod
460 def _rbd_image_stat_removing(cls, ioctx, pool_name, namespace, image_id):
461 img = cls._rbd_inst.trash_get(ioctx, image_id)
462 img_spec = get_image_spec(pool_name, namespace, image_id)
463
464 if img['source'] == 'REMOVING':
465 img['unique_id'] = img_spec
466 img['pool_name'] = pool_name
467 img['namespace'] = namespace
468 img['deletion_time'] = "{}Z".format(img['deletion_time'].isoformat())
469 img['deferment_end_time'] = "{}Z".format(img['deferment_end_time'].isoformat())
470 return img
471 raise rbd.ImageNotFound('No image {} in status `REMOVING` found.'.format(img_spec),
472 errno=errno.ENOENT)
473
474 @classmethod
475 def _rbd_pool_image_refs(cls, pool_names: List[str], namespace: Optional[str] = None):
476 joint_refs = []
477 for pool in pool_names:
478 for current_namespace in cls._pool_namespaces(pool, namespace=namespace):
479 image_refs = cls._rbd_image_refs(pool, current_namespace)
480 for image in image_refs:
481 image['namespace'] = current_namespace
482 image['pool_name'] = pool
483 joint_refs.append(image)
484 return joint_refs
485
486 @classmethod
487 def rbd_pool_list(cls, pool_names: List[str], namespace: Optional[str] = None, offset: int = 0,
488 limit: int = 5, search: str = '', sort: str = ''):
489 image_refs = cls._rbd_pool_image_refs(pool_names, namespace)
490 params = ['name', 'pool_name', 'namespace']
491 paginator = ListPaginator(offset, limit, sort, search, image_refs,
492 searchable_params=params, sortable_params=params,
493 default_sort='+name')
494
495 result = []
496 for image_ref in paginator.list():
497 with mgr.rados.open_ioctx(image_ref['pool_name']) as ioctx:
498 ioctx.set_namespace(image_ref['namespace'])
499 # Check if the RBD has been deleted partially. This happens for example if
500 # the deletion process of the RBD has been started and was interrupted.
501
502 try:
503 stat = cls._rbd_image_stat(
504 ioctx, image_ref['pool_name'], image_ref['namespace'], image_ref['name'])
505 except rbd.ImageNotFound:
506 try:
507 stat = cls._rbd_image_stat_removing(
508 ioctx, image_ref['pool_name'], image_ref['namespace'], image_ref['id'])
509 except rbd.ImageNotFound:
510 continue
511 result.append(stat)
512 return result, paginator.get_count()
513
514 @classmethod
515 def get_image(cls, image_spec):
516 pool_name, namespace, image_name = parse_image_spec(image_spec)
517 ioctx = mgr.rados.open_ioctx(pool_name)
518 if namespace:
519 ioctx.set_namespace(namespace)
520 try:
521 return cls._rbd_image(ioctx, pool_name, namespace, image_name)
522 except rbd.ImageNotFound:
523 raise cherrypy.HTTPError(404, 'Image not found')
524
525 @classmethod
526 @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
527 def create(cls, name, pool_name, size, namespace=None,
528 obj_size=None, features=None, stripe_unit=None, stripe_count=None,
529 data_pool=None, configuration=None, metadata=None):
530 size = int(size)
531
532 def _create(ioctx):
533 rbd_inst = cls._rbd_inst
534
535 # Set order
536 l_order = None
537 if obj_size and obj_size > 0:
538 l_order = int(round(math.log(float(obj_size), 2)))
539
540 # Set features
541 feature_bitmask = format_features(features)
542
543 rbd_inst.create(ioctx, name, size, order=l_order, old_format=False,
544 features=feature_bitmask, stripe_unit=stripe_unit,
545 stripe_count=stripe_count, data_pool=data_pool)
546 RbdConfiguration(pool_ioctx=ioctx, namespace=namespace,
547 image_name=name).set_configuration(configuration)
548 if metadata:
549 with rbd.Image(ioctx, name) as image:
550 RbdImageMetadataService(image).set_metadata(metadata)
551 rbd_call(pool_name, namespace, _create)
552
553 @classmethod
554 @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
555 def set(cls, image_spec, name=None, size=None, features=None,
556 configuration=None, metadata=None, enable_mirror=None, primary=None,
557 force=False, resync=False, mirror_mode=None, schedule_interval='',
558 remove_scheduling=False):
559 # pylint: disable=too-many-branches
560 pool_name, namespace, image_name = parse_image_spec(image_spec)
561
562 def _edit(ioctx, image):
563 rbd_inst = cls._rbd_inst
564 # check rename image
565 if name and name != image_name:
566 rbd_inst.rename(ioctx, image_name, name)
567
568 # check resize
569 if size and size != image.size():
570 image.resize(size)
571
572 mirror_image_info = image.mirror_image_get_info()
573 if enable_mirror and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_DISABLED:
574 RbdMirroringService.enable_image(
575 image_name, pool_name, namespace,
576 MIRROR_IMAGE_MODE[mirror_mode])
577 elif (enable_mirror is False
578 and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED):
579 RbdMirroringService.disable_image(
580 image_name, pool_name, namespace)
581
582 # check enable/disable features
583 if features is not None:
584 curr_features = format_bitmask(image.features())
585 # check disabled features
586 _sort_features(curr_features, enable=False)
587 for feature in curr_features:
588 if (feature not in features
589 and feature in cls.ALLOW_DISABLE_FEATURES
590 and feature in format_bitmask(image.features())):
591 f_bitmask = format_features([feature])
592 image.update_features(f_bitmask, False)
593 # check enabled features
594 _sort_features(features)
595 for feature in features:
596 if (feature not in curr_features
597 and feature in cls.ALLOW_ENABLE_FEATURES
598 and feature not in format_bitmask(image.features())):
599 f_bitmask = format_features([feature])
600 image.update_features(f_bitmask, True)
601
602 RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).set_configuration(
603 configuration)
604 if metadata:
605 RbdImageMetadataService(image).set_metadata(metadata)
606
607 if primary and not mirror_image_info['primary']:
608 RbdMirroringService.promote_image(
609 image_name, pool_name, namespace, force)
610 elif primary is False and mirror_image_info['primary']:
611 RbdMirroringService.demote_image(
612 image_name, pool_name, namespace)
613
614 if resync:
615 RbdMirroringService.resync_image(image_name, pool_name, namespace)
616
617 if schedule_interval:
618 RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval)
619
620 if remove_scheduling:
621 RbdMirroringService.snapshot_schedule_remove(image_spec)
622
623 return rbd_image_call(pool_name, namespace, image_name, _edit)
624
625 @classmethod
626 @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
627 def delete(cls, image_spec):
628 pool_name, namespace, image_name = parse_image_spec(image_spec)
629
630 image = RbdService.get_image(image_spec)
631 snapshots = image['snapshots']
632 for snap in snapshots:
633 RbdSnapshotService.remove_snapshot(image_spec, snap['name'], snap['is_protected'])
634
635 rbd_inst = rbd.RBD()
636 return rbd_call(pool_name, namespace, rbd_inst.remove, image_name)
637
638 @classmethod
639 @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
640 def copy(cls, image_spec, dest_pool_name, dest_namespace, dest_image_name,
641 snapshot_name=None, obj_size=None, features=None,
642 stripe_unit=None, stripe_count=None, data_pool=None,
643 configuration=None, metadata=None):
644 pool_name, namespace, image_name = parse_image_spec(image_spec)
645
646 def _src_copy(s_ioctx, s_img):
647 def _copy(d_ioctx):
648 # Set order
649 l_order = None
650 if obj_size and obj_size > 0:
651 l_order = int(round(math.log(float(obj_size), 2)))
652
653 # Set features
654 feature_bitmask = format_features(features)
655
656 if snapshot_name:
657 s_img.set_snap(snapshot_name)
658
659 s_img.copy(d_ioctx, dest_image_name, feature_bitmask, l_order,
660 stripe_unit, stripe_count, data_pool)
661 RbdConfiguration(pool_ioctx=d_ioctx, image_name=dest_image_name).set_configuration(
662 configuration)
663 if metadata:
664 with rbd.Image(d_ioctx, dest_image_name) as image:
665 RbdImageMetadataService(image).set_metadata(metadata)
666
667 return rbd_call(dest_pool_name, dest_namespace, _copy)
668
669 return rbd_image_call(pool_name, namespace, image_name, _src_copy)
670
671 @classmethod
672 @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
673 def flatten(cls, image_spec):
674 def _flatten(ioctx, image):
675 image.flatten()
676
677 pool_name, namespace, image_name = parse_image_spec(image_spec)
678 return rbd_image_call(pool_name, namespace, image_name, _flatten)
679
680 @classmethod
681 def move_image_to_trash(cls, image_spec, delay):
682 pool_name, namespace, image_name = parse_image_spec(image_spec)
683 rbd_inst = cls._rbd_inst
684 return rbd_call(pool_name, namespace, rbd_inst.trash_move, image_name, delay)
685
686
687 class RbdSnapshotService(object):
688
689 @classmethod
690 def remove_snapshot(cls, image_spec, snapshot_name, unprotect=False):
691 def _remove_snapshot(ioctx, img, snapshot_name, unprotect):
692 if unprotect:
693 img.unprotect_snap(snapshot_name)
694 img.remove_snap(snapshot_name)
695
696 pool_name, namespace, image_name = parse_image_spec(image_spec)
697 return rbd_image_call(pool_name, namespace, image_name,
698 _remove_snapshot, snapshot_name, unprotect)
699
700
701 class RBDSchedulerInterval:
702 def __init__(self, interval: str):
703 self.amount = int(interval[:-1])
704 self.unit = interval[-1]
705 if self.unit not in 'mhd':
706 raise ValueError(f'Invalid interval unit {self.unit}')
707
708 def __str__(self):
709 return f'{self.amount}{self.unit}'
710
711
712 class RbdMirroringService:
713
714 @classmethod
715 def enable_image(cls, image_name: str, pool_name: str, namespace: str, mode: MIRROR_IMAGE_MODE):
716 rbd_image_call(pool_name, namespace, image_name,
717 lambda ioctx, image: image.mirror_image_enable(mode))
718
719 @classmethod
720 def disable_image(cls, image_name: str, pool_name: str, namespace: str, force: bool = False):
721 rbd_image_call(pool_name, namespace, image_name,
722 lambda ioctx, image: image.mirror_image_disable(force))
723
724 @classmethod
725 def promote_image(cls, image_name: str, pool_name: str, namespace: str, force: bool = False):
726 rbd_image_call(pool_name, namespace, image_name,
727 lambda ioctx, image: image.mirror_image_promote(force))
728
729 @classmethod
730 def demote_image(cls, image_name: str, pool_name: str, namespace: str):
731 rbd_image_call(pool_name, namespace, image_name,
732 lambda ioctx, image: image.mirror_image_demote())
733
734 @classmethod
735 def resync_image(cls, image_name: str, pool_name: str, namespace: str):
736 rbd_image_call(pool_name, namespace, image_name,
737 lambda ioctx, image: image.mirror_image_resync())
738
739 @classmethod
740 def snapshot_schedule_add(cls, image_spec: str, interval: str):
741 _rbd_support_remote('mirror_snapshot_schedule_add', image_spec,
742 str(RBDSchedulerInterval(interval)))
743
744 @classmethod
745 def snapshot_schedule_remove(cls, image_spec: str):
746 _rbd_support_remote('mirror_snapshot_schedule_remove', image_spec)
747
748
749 class RbdImageMetadataService(object):
750 def __init__(self, image):
751 self._image = image
752
753 def list(self):
754 result = self._image.metadata_list()
755 # filter out configuration metadata
756 return {v[0]: v[1] for v in result if not v[0].startswith('conf_')}
757
758 def get(self, name):
759 return self._image.metadata_get(name)
760
761 def set(self, name, value):
762 self._image.metadata_set(name, value)
763
764 def remove(self, name):
765 try:
766 self._image.metadata_remove(name)
767 except KeyError:
768 pass
769
770 def set_metadata(self, metadata):
771 for name, value in metadata.items():
772 if value is not None:
773 self.set(name, value)
774 else:
775 self.remove(name)