]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/services/rbd.py
import ceph 15.2.14
[ceph.git] / ceph / src / pybind / mgr / dashboard / services / rbd.py
CommitLineData
11fdf7f2 1# -*- coding: utf-8 -*-
9f95a23c 2# pylint: disable=unused-argument
11fdf7f2
TL
3from __future__ import absolute_import
4
6d8e3169 5import errno
11fdf7f2
TL
6import six
7
9f95a23c
TL
8import cherrypy
9
11fdf7f2
TL
10import rbd
11
12from .. import mgr
9f95a23c
TL
13from ..tools import ViewCache
14from .ceph_service import CephService
15
16try:
17 from typing import List
18except ImportError:
19 pass # For typing only
11fdf7f2
TL
20
21
22RBD_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
35def format_bitmask(features):
36 """
37 Formats the bitmask:
38
9f95a23c 39 @DISABLEDOCTEST: >>> format_bitmask(45)
11fdf7f2
TL
40 ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']
41 """
42 names = [val for key, val in RBD_FEATURES_NAME_MAPPING.items()
43 if key & features == key]
44 return sorted(names)
45
46
47def format_features(features):
48 """
49 Converts the features list to bitmask:
50
9f95a23c
TL
51 @DISABLEDOCTEST: >>> format_features(['deep-flatten', 'exclusive-lock',
52 'layering', 'object-map'])
11fdf7f2
TL
53 45
54
9f95a23c 55 @DISABLEDOCTEST: >>> format_features(None) is None
11fdf7f2
TL
56 True
57
9f95a23c 58 @DISABLEDOCTEST: >>> format_features('deep-flatten, exclusive-lock')
11fdf7f2
TL
59 32
60 """
61 if isinstance(features, six.string_types):
62 features = features.split(',')
63
64 if not isinstance(features, list):
65 return None
66
67 res = 0
68 for key, value in RBD_FEATURES_NAME_MAPPING.items():
69 if value in features:
70 res = key | res
71 return res
72
73
9f95a23c
TL
74def get_image_spec(pool_name, namespace, rbd_name):
75 namespace = '{}/'.format(namespace) if namespace else ''
76 return '{}/{}{}'.format(pool_name, namespace, rbd_name)
77
78
79def parse_image_spec(image_spec):
80 namespace_spec, image_name = image_spec.rsplit('/', 1)
81 if '/' in namespace_spec:
82 pool_name, namespace = namespace_spec.rsplit('/', 1)
83 else:
84 pool_name, namespace = namespace_spec, None
85 return pool_name, namespace, image_name
86
87
88def rbd_call(pool_name, namespace, func, *args, **kwargs):
89 with mgr.rados.open_ioctx(pool_name) as ioctx:
90 ioctx.set_namespace(namespace if namespace is not None else '')
91 func(ioctx, *args, **kwargs)
92
93
94def rbd_image_call(pool_name, namespace, image_name, func, *args, **kwargs):
95 def _ioctx_func(ioctx, image_name, func, *args, **kwargs):
96 with rbd.Image(ioctx, image_name) as img:
97 func(ioctx, img, *args, **kwargs)
98
99 return rbd_call(pool_name, namespace, _ioctx_func, image_name, func, *args, **kwargs)
100
101
11fdf7f2
TL
102class RbdConfiguration(object):
103 _rbd = rbd.RBD()
104
9f95a23c
TL
105 def __init__(self, pool_name='', namespace='', image_name='', pool_ioctx=None,
106 image_ioctx=None):
107 # type: (str, str, str, object, object) -> None
11fdf7f2
TL
108 assert bool(pool_name) != bool(pool_ioctx) # xor
109 self._pool_name = pool_name
9f95a23c 110 self._namespace = namespace if namespace is not None else ''
11fdf7f2
TL
111 self._image_name = image_name
112 self._pool_ioctx = pool_ioctx
113 self._image_ioctx = image_ioctx
114
115 @staticmethod
116 def _ensure_prefix(option):
117 # type: (str) -> str
118 return option if option.startswith('conf_') else 'conf_' + option
119
120 def list(self):
9f95a23c 121 # type: () -> List[dict]
11fdf7f2
TL
122 def _list(ioctx):
123 if self._image_name: # image config
9f95a23c
TL
124 try:
125 with rbd.Image(ioctx, self._image_name) as image:
126 result = image.config_list()
127 except rbd.ImageNotFound:
128 result = []
11fdf7f2 129 else: # pool config
e306af50
TL
130 pg_status = list(CephService.get_pool_pg_status(self._pool_name).keys())
131 if len(pg_status) == 1 and 'incomplete' in pg_status[0]:
132 # If config_list would be called with ioctx if it's a bad pool,
133 # the dashboard would stop working, waiting for the response
134 # that would not happen.
135 #
136 # This is only a workaround for https://tracker.ceph.com/issues/43771 which
137 # already got rejected as not worth the effort.
138 #
139 # Are more complete workaround for the dashboard will be implemented with
140 # https://tracker.ceph.com/issues/44224
141 #
142 # @TODO: If #44224 is addressed remove this workaround
143 return []
11fdf7f2
TL
144 result = self._rbd.config_list(ioctx)
145 return list(result)
146
147 if self._pool_name:
148 ioctx = mgr.rados.open_ioctx(self._pool_name)
9f95a23c 149 ioctx.set_namespace(self._namespace)
11fdf7f2
TL
150 else:
151 ioctx = self._pool_ioctx
152
153 return _list(ioctx)
154
155 def get(self, option_name):
156 # type: (str) -> str
157 option_name = self._ensure_prefix(option_name)
158 with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
9f95a23c 159 pool_ioctx.set_namespace(self._namespace)
11fdf7f2
TL
160 if self._image_name:
161 with rbd.Image(pool_ioctx, self._image_name) as image:
162 return image.metadata_get(option_name)
163 return self._rbd.pool_metadata_get(pool_ioctx, option_name)
164
165 def set(self, option_name, option_value):
166 # type: (str, str) -> None
167
168 option_value = str(option_value)
169 option_name = self._ensure_prefix(option_name)
170
171 pool_ioctx = self._pool_ioctx
172 if self._pool_name: # open ioctx
173 pool_ioctx = mgr.rados.open_ioctx(self._pool_name)
9f95a23c
TL
174 pool_ioctx.__enter__() # type: ignore
175 pool_ioctx.set_namespace(self._namespace) # type: ignore
11fdf7f2
TL
176
177 image_ioctx = self._image_ioctx
178 if self._image_name:
179 image_ioctx = rbd.Image(pool_ioctx, self._image_name)
9f95a23c 180 image_ioctx.__enter__() # type: ignore
11fdf7f2
TL
181
182 if image_ioctx:
9f95a23c 183 image_ioctx.metadata_set(option_name, option_value) # type: ignore
11fdf7f2
TL
184 else:
185 self._rbd.pool_metadata_set(pool_ioctx, option_name, option_value)
186
187 if self._image_name: # Name provided, so we opened it and now have to close it
9f95a23c 188 image_ioctx.__exit__(None, None, None) # type: ignore
11fdf7f2 189 if self._pool_name:
9f95a23c 190 pool_ioctx.__exit__(None, None, None) # type: ignore
11fdf7f2
TL
191
192 def remove(self, option_name):
193 """
194 Removes an option by name. Will not raise an error, if the option hasn't been found.
195 :type option_name str
196 """
197 def _remove(ioctx):
198 try:
199 if self._image_name:
200 with rbd.Image(ioctx, self._image_name) as image:
201 image.metadata_remove(option_name)
202 else:
203 self._rbd.pool_metadata_remove(ioctx, option_name)
204 except KeyError:
205 pass
206
207 option_name = self._ensure_prefix(option_name)
208
209 if self._pool_name:
210 with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
9f95a23c 211 pool_ioctx.set_namespace(self._namespace)
11fdf7f2
TL
212 _remove(pool_ioctx)
213 else:
214 _remove(self._pool_ioctx)
215
216 def set_configuration(self, configuration):
217 if configuration:
218 for option_name, option_value in configuration.items():
219 if option_value is not None:
220 self.set(option_name, option_value)
221 else:
222 self.remove(option_name)
9f95a23c
TL
223
224
225class RbdService(object):
226
227 @classmethod
228 def _rbd_disk_usage(cls, image, snaps, whole_object=True):
229 class DUCallback(object):
230 def __init__(self):
231 self.used_size = 0
232
233 def __call__(self, offset, length, exists):
234 if exists:
235 self.used_size += length
236
237 snap_map = {}
238 prev_snap = None
239 total_used_size = 0
240 for _, size, name in snaps:
241 image.set_snap(name)
242 du_callb = DUCallback()
243 image.diff_iterate(0, size, prev_snap, du_callb,
244 whole_object=whole_object)
245 snap_map[name] = du_callb.used_size
246 total_used_size += du_callb.used_size
247 prev_snap = name
248
249 return total_used_size, snap_map
250
251 @classmethod
252 def _rbd_image(cls, ioctx, pool_name, namespace, image_name):
253 with rbd.Image(ioctx, image_name) as img:
254
255 stat = img.stat()
256 stat['name'] = image_name
f6b5b4d7
TL
257 if img.old_format():
258 stat['unique_id'] = get_image_spec(pool_name, namespace, stat['block_name_prefix'])
259 stat['id'] = stat['unique_id']
260 stat['image_format'] = 1
261 else:
262 stat['unique_id'] = get_image_spec(pool_name, namespace, img.id())
263 stat['id'] = img.id()
264 stat['image_format'] = 2
265
9f95a23c
TL
266 stat['pool_name'] = pool_name
267 stat['namespace'] = namespace
268 features = img.features()
269 stat['features'] = features
270 stat['features_name'] = format_bitmask(features)
271
272 # the following keys are deprecated
273 del stat['parent_pool']
274 del stat['parent_name']
275
276 stat['timestamp'] = "{}Z".format(img.create_timestamp()
277 .isoformat())
278
279 stat['stripe_count'] = img.stripe_count()
280 stat['stripe_unit'] = img.stripe_unit()
281
282 data_pool_name = CephService.get_pool_name_from_id(
283 img.data_pool_id())
284 if data_pool_name == pool_name:
285 data_pool_name = None
286 stat['data_pool'] = data_pool_name
287
288 try:
289 stat['parent'] = img.get_parent_image_spec()
290 except rbd.ImageNotFound:
291 # no parent image
292 stat['parent'] = None
293
294 # snapshots
295 stat['snapshots'] = []
296 for snap in img.list_snaps():
297 snap['timestamp'] = "{}Z".format(
298 img.get_snap_timestamp(snap['id']).isoformat())
299 snap['is_protected'] = img.is_protected_snap(snap['name'])
300 snap['used_bytes'] = None
301 snap['children'] = []
302 img.set_snap(snap['name'])
303 for child_pool_name, child_image_name in img.list_children():
304 snap['children'].append({
305 'pool_name': child_pool_name,
306 'image_name': child_image_name
307 })
308 stat['snapshots'].append(snap)
309
310 # disk usage
311 img_flags = img.flags()
312 if 'fast-diff' in stat['features_name'] and \
313 not rbd.RBD_FLAG_FAST_DIFF_INVALID & img_flags:
314 snaps = [(s['id'], s['size'], s['name'])
315 for s in stat['snapshots']]
316 snaps.sort(key=lambda s: s[0])
317 snaps += [(snaps[-1][0] + 1 if snaps else 0, stat['size'], None)]
318 total_prov_bytes, snaps_prov_bytes = cls._rbd_disk_usage(
319 img, snaps, True)
320 stat['total_disk_usage'] = total_prov_bytes
321 for snap, prov_bytes in snaps_prov_bytes.items():
322 if snap is None:
323 stat['disk_usage'] = prov_bytes
324 continue
325 for ss in stat['snapshots']:
326 if ss['name'] == snap:
327 ss['disk_usage'] = prov_bytes
328 break
329 else:
330 stat['total_disk_usage'] = None
331 stat['disk_usage'] = None
332
333 stat['configuration'] = RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).list()
334
335 return stat
336
337 @classmethod
6d8e3169 338 def _rbd_image_refs(cls, ioctx):
9f95a23c 339 rbd_inst = rbd.RBD()
6d8e3169 340 return rbd_inst.list2(ioctx)
9f95a23c
TL
341
342 @classmethod
343 def _rbd_image_stat(cls, ioctx, pool_name, namespace, image_name):
344 return cls._rbd_image(ioctx, pool_name, namespace, image_name)
345
6d8e3169
FG
346 @classmethod
347 def _rbd_image_stat_removing(cls, ioctx, pool_name, namespace, image_id):
348 rbd_inst = rbd.RBD()
349 img = rbd_inst.trash_get(ioctx, image_id)
350 img_spec = get_image_spec(pool_name, namespace, image_id)
351
352 if img['source'] == 'REMOVING':
353 img['unique_id'] = img_spec
354 img['pool_name'] = pool_name
355 img['namespace'] = namespace
356 img['deletion_time'] = "{}Z".format(img['deletion_time'].isoformat())
357 img['deferment_end_time'] = "{}Z".format(img['deferment_end_time'].isoformat())
358 return img
359 raise rbd.ImageNotFound('No image {} in status `REMOVING` found.'.format(img_spec),
360 errno=errno.ENOENT)
361
9f95a23c
TL
362 @classmethod
363 @ViewCache()
364 def rbd_pool_list(cls, pool_name, namespace=None):
365 rbd_inst = rbd.RBD()
366 with mgr.rados.open_ioctx(pool_name) as ioctx:
367 result = []
368 if namespace:
369 namespaces = [namespace]
370 else:
371 namespaces = rbd_inst.namespace_list(ioctx)
372 # images without namespace
373 namespaces.append('')
374 for current_namespace in namespaces:
375 ioctx.set_namespace(current_namespace)
6d8e3169
FG
376 image_refs = cls._rbd_image_refs(ioctx)
377 for image_ref in image_refs:
9f95a23c 378 try:
6d8e3169
FG
379 stat = cls._rbd_image_stat(
380 ioctx, pool_name, current_namespace, image_ref['name'])
9f95a23c 381 except rbd.ImageNotFound:
6d8e3169
FG
382 # Check if the RBD has been deleted partially. This happens for example if
383 # the deletion process of the RBD has been started and was interrupted.
384 try:
385 stat = cls._rbd_image_stat_removing(
386 ioctx, pool_name, current_namespace, image_ref['id'])
387 except rbd.ImageNotFound:
388 continue
9f95a23c
TL
389 result.append(stat)
390 return result
391
392 @classmethod
393 def get_image(cls, image_spec):
394 pool_name, namespace, image_name = parse_image_spec(image_spec)
395 ioctx = mgr.rados.open_ioctx(pool_name)
396 if namespace:
397 ioctx.set_namespace(namespace)
398 try:
399 return cls._rbd_image(ioctx, pool_name, namespace, image_name)
400 except rbd.ImageNotFound:
401 raise cherrypy.HTTPError(404, 'Image not found')
402
403
404class RbdSnapshotService(object):
405
406 @classmethod
407 def remove_snapshot(cls, image_spec, snapshot_name, unprotect=False):
408 def _remove_snapshot(ioctx, img, snapshot_name, unprotect):
409 if unprotect:
410 img.unprotect_snap(snapshot_name)
411 img.remove_snap(snapshot_name)
412
413 pool_name, namespace, image_name = parse_image_spec(image_spec)
414 return rbd_image_call(pool_name, namespace, image_name,
415 _remove_snapshot, snapshot_name, unprotect)