]>
Commit | Line | Data |
---|---|---|
11fdf7f2 | 1 | # -*- coding: utf-8 -*- |
9f95a23c | 2 | # pylint: disable=unused-argument |
11fdf7f2 TL |
3 | from __future__ import absolute_import |
4 | ||
6d8e3169 | 5 | import errno |
11fdf7f2 TL |
6 | import six |
7 | ||
9f95a23c TL |
8 | import cherrypy |
9 | ||
11fdf7f2 TL |
10 | import rbd |
11 | ||
12 | from .. import mgr | |
9f95a23c TL |
13 | from ..tools import ViewCache |
14 | from .ceph_service import CephService | |
15 | ||
16 | try: | |
17 | from typing import List | |
18 | except ImportError: | |
19 | pass # For typing only | |
11fdf7f2 TL |
20 | |
21 | ||
22 | RBD_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 | ||
35 | def 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 | ||
47 | def 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 |
74 | def 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 | ||
79 | def 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 | ||
88 | def 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 | ||
94 | def 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 |
102 | class 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 | ||
225 | class 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 | ||
404 | class 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) |