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