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