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