]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | # -*- coding: utf-8 -*- |
2 | # pylint: disable=unused-argument | |
3 | # pylint: disable=too-many-statements,too-many-branches | |
11fdf7f2 | 4 | |
1911f103 | 5 | import logging |
11fdf7f2 | 6 | import math |
11fdf7f2 | 7 | from datetime import datetime |
f67539c2 | 8 | from functools import partial |
11fdf7f2 | 9 | |
2a845540 | 10 | import cherrypy |
11fdf7f2 TL |
11 | import rbd |
12 | ||
11fdf7f2 | 13 | from .. import mgr |
9f95a23c | 14 | from ..exceptions import DashboardException |
11fdf7f2 TL |
15 | from ..security import Scope |
16 | from ..services.ceph_service import CephService | |
f67539c2 | 17 | from ..services.exception import handle_rados_error, handle_rbd_error, serialize_dashboard_exception |
2a845540 | 18 | from ..services.rbd import MIRROR_IMAGE_MODE, RbdConfiguration, \ |
1e59de90 TL |
19 | RbdImageMetadataService, RbdMirroringService, RbdService, \ |
20 | RbdSnapshotService, format_bitmask, format_features, get_image_spec, \ | |
21 | parse_image_spec, rbd_call, rbd_image_call | |
11fdf7f2 | 22 | from ..tools import ViewCache, str_to_bool |
2a845540 TL |
23 | from . import APIDoc, APIRouter, BaseController, CreatePermission, \ |
24 | DeletePermission, Endpoint, EndpointDoc, ReadPermission, RESTController, \ | |
25 | Task, UIRouter, UpdatePermission, allow_empty_body | |
26 | from ._version import APIVersion | |
11fdf7f2 | 27 | |
1911f103 TL |
28 | logger = logging.getLogger(__name__) |
29 | ||
f67539c2 | 30 | RBD_SCHEMA = ([{ |
f67539c2 TL |
31 | "value": ([str], ''), |
32 | "pool_name": (str, 'pool name') | |
33 | }]) | |
34 | ||
35 | RBD_TRASH_SCHEMA = [{ | |
36 | "status": (int, ''), | |
37 | "value": ([str], ''), | |
38 | "pool_name": (str, 'pool name') | |
39 | }] | |
40 | ||
11fdf7f2 TL |
41 | |
42 | # pylint: disable=not-callable | |
9f95a23c | 43 | def RbdTask(name, metadata, wait_for): # noqa: N802 |
11fdf7f2 TL |
44 | def composed_decorator(func): |
45 | func = handle_rados_error('pool')(func) | |
46 | func = handle_rbd_error()(func) | |
47 | return Task("rbd/{}".format(name), metadata, wait_for, | |
48 | partial(serialize_dashboard_exception, include_http_status=True))(func) | |
49 | return composed_decorator | |
50 | ||
51 | ||
11fdf7f2 TL |
52 | def _sort_features(features, enable=True): |
53 | """ | |
54 | Sorts image features according to feature dependencies: | |
55 | ||
56 | object-map depends on exclusive-lock | |
57 | journaling depends on exclusive-lock | |
58 | fast-diff depends on object-map | |
59 | """ | |
9f95a23c | 60 | ORDER = ['exclusive-lock', 'journaling', 'object-map', 'fast-diff'] # noqa: N806 |
11fdf7f2 TL |
61 | |
62 | def key_func(feat): | |
63 | try: | |
64 | return ORDER.index(feat) | |
65 | except ValueError: | |
66 | return id(feat) | |
67 | ||
68 | features.sort(key=key_func, reverse=not enable) | |
69 | ||
70 | ||
a4b75251 TL |
71 | @APIRouter('/block/image', Scope.RBD_IMAGE) |
72 | @APIDoc("RBD Management API", "Rbd") | |
11fdf7f2 TL |
73 | class Rbd(RESTController): |
74 | ||
11fdf7f2 TL |
75 | # set of image features that can be enable on existing images |
76 | ALLOW_ENABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "journaling"} | |
77 | ||
78 | # set of image features that can be disabled on existing images | |
79 | ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten", | |
80 | "journaling"} | |
81 | ||
2a845540 TL |
82 | DEFAULT_LIMIT = 5 |
83 | ||
84 | def _rbd_list(self, pool_name=None, offset=0, limit=DEFAULT_LIMIT, search='', sort=''): | |
11fdf7f2 TL |
85 | if pool_name: |
86 | pools = [pool_name] | |
87 | else: | |
88 | pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')] | |
89 | ||
2a845540 TL |
90 | images, num_total_images = RbdService.rbd_pool_list( |
91 | pools, offset=offset, limit=limit, search=search, sort=sort) | |
92 | cherrypy.response.headers['X-Total-Count'] = num_total_images | |
93 | pool_result = {} | |
94 | for i, image in enumerate(images): | |
95 | pool = image['pool_name'] | |
96 | if pool not in pool_result: | |
97 | pool_result[pool] = {'value': [], 'pool_name': image['pool_name']} | |
98 | pool_result[pool]['value'].append(image) | |
99 | ||
100 | images[i]['configuration'] = RbdConfiguration( | |
101 | pool, image['namespace'], image['name']).list() | |
1e59de90 TL |
102 | images[i]['metadata'] = rbd_image_call( |
103 | pool, image['namespace'], image['name'], | |
104 | lambda ioctx, image: RbdImageMetadataService(image).list()) | |
105 | ||
2a845540 | 106 | return list(pool_result.values()) |
11fdf7f2 TL |
107 | |
108 | @handle_rbd_error() | |
109 | @handle_rados_error('pool') | |
f67539c2 TL |
110 | @EndpointDoc("Display Rbd Images", |
111 | parameters={ | |
112 | 'pool_name': (str, 'Pool Name'), | |
2a845540 TL |
113 | 'limit': (int, 'limit'), |
114 | 'offset': (int, 'offset'), | |
f67539c2 TL |
115 | }, |
116 | responses={200: RBD_SCHEMA}) | |
2a845540 TL |
117 | @RESTController.MethodMap(version=APIVersion(2, 0)) # type: ignore |
118 | def list(self, pool_name=None, offset: int = 0, limit: int = DEFAULT_LIMIT, | |
119 | search: str = '', sort: str = ''): | |
39ae355f TL |
120 | return self._rbd_list(pool_name, offset=int(offset), limit=int(limit), |
121 | search=search, sort=sort) | |
11fdf7f2 TL |
122 | |
123 | @handle_rbd_error() | |
124 | @handle_rados_error('pool') | |
9f95a23c TL |
125 | def get(self, image_spec): |
126 | return RbdService.get_image(image_spec) | |
11fdf7f2 TL |
127 | |
128 | @RbdTask('create', | |
9f95a23c | 129 | {'pool_name': '{pool_name}', 'namespace': '{namespace}', 'image_name': '{name}'}, 2.0) |
2a845540 TL |
130 | def create(self, name, pool_name, size, namespace=None, schedule_interval='', |
131 | obj_size=None, features=None, stripe_unit=None, stripe_count=None, | |
1e59de90 TL |
132 | data_pool=None, configuration=None, metadata=None, |
133 | mirror_mode=None): | |
11fdf7f2 TL |
134 | |
135 | size = int(size) | |
136 | ||
137 | def _create(ioctx): | |
138 | rbd_inst = rbd.RBD() | |
139 | ||
140 | # Set order | |
141 | l_order = None | |
142 | if obj_size and obj_size > 0: | |
143 | l_order = int(round(math.log(float(obj_size), 2))) | |
144 | ||
145 | # Set features | |
146 | feature_bitmask = format_features(features) | |
147 | ||
148 | rbd_inst.create(ioctx, name, size, order=l_order, old_format=False, | |
149 | features=feature_bitmask, stripe_unit=stripe_unit, | |
150 | stripe_count=stripe_count, data_pool=data_pool) | |
9f95a23c TL |
151 | RbdConfiguration(pool_ioctx=ioctx, namespace=namespace, |
152 | image_name=name).set_configuration(configuration) | |
1e59de90 TL |
153 | if metadata: |
154 | with rbd.Image(ioctx, name) as image: | |
155 | RbdImageMetadataService(image).set_metadata(metadata) | |
9f95a23c TL |
156 | |
157 | rbd_call(pool_name, namespace, _create) | |
1e59de90 | 158 | |
2a845540 TL |
159 | if mirror_mode: |
160 | RbdMirroringService.enable_image(name, pool_name, namespace, | |
161 | MIRROR_IMAGE_MODE[mirror_mode]) | |
162 | ||
163 | if schedule_interval: | |
164 | image_spec = get_image_spec(pool_name, namespace, name) | |
165 | RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval) | |
9f95a23c TL |
166 | |
167 | @RbdTask('delete', ['{image_spec}'], 2.0) | |
168 | def delete(self, image_spec): | |
169 | pool_name, namespace, image_name = parse_image_spec(image_spec) | |
11fdf7f2 | 170 | |
9f95a23c TL |
171 | image = RbdService.get_image(image_spec) |
172 | snapshots = image['snapshots'] | |
173 | for snap in snapshots: | |
174 | RbdSnapshotService.remove_snapshot(image_spec, snap['name'], snap['is_protected']) | |
11fdf7f2 | 175 | |
11fdf7f2 | 176 | rbd_inst = rbd.RBD() |
9f95a23c TL |
177 | return rbd_call(pool_name, namespace, rbd_inst.remove, image_name) |
178 | ||
179 | @RbdTask('edit', ['{image_spec}', '{name}'], 4.0) | |
2a845540 | 180 | def set(self, image_spec, name=None, size=None, features=None, |
1e59de90 TL |
181 | configuration=None, metadata=None, enable_mirror=None, primary=None, |
182 | force=False, resync=False, mirror_mode=None, schedule_interval='', | |
2a845540 TL |
183 | remove_scheduling=False): |
184 | ||
9f95a23c | 185 | pool_name, namespace, image_name = parse_image_spec(image_spec) |
11fdf7f2 | 186 | |
11fdf7f2 TL |
187 | def _edit(ioctx, image): |
188 | rbd_inst = rbd.RBD() | |
189 | # check rename image | |
190 | if name and name != image_name: | |
191 | rbd_inst.rename(ioctx, image_name, name) | |
192 | ||
193 | # check resize | |
194 | if size and size != image.size(): | |
195 | image.resize(size) | |
196 | ||
39ae355f TL |
197 | mirror_image_info = image.mirror_image_get_info() |
198 | if enable_mirror and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_DISABLED: | |
199 | RbdMirroringService.enable_image( | |
200 | image_name, pool_name, namespace, | |
201 | MIRROR_IMAGE_MODE[mirror_mode]) | |
202 | elif (enable_mirror is False | |
203 | and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED): | |
204 | RbdMirroringService.disable_image( | |
205 | image_name, pool_name, namespace) | |
206 | ||
11fdf7f2 TL |
207 | # check enable/disable features |
208 | if features is not None: | |
209 | curr_features = format_bitmask(image.features()) | |
210 | # check disabled features | |
211 | _sort_features(curr_features, enable=False) | |
212 | for feature in curr_features: | |
1e59de90 TL |
213 | if (feature not in features |
214 | and feature in self.ALLOW_DISABLE_FEATURES | |
215 | and feature in format_bitmask(image.features())): | |
11fdf7f2 TL |
216 | f_bitmask = format_features([feature]) |
217 | image.update_features(f_bitmask, False) | |
218 | # check enabled features | |
219 | _sort_features(features) | |
220 | for feature in features: | |
1e59de90 TL |
221 | if (feature not in curr_features |
222 | and feature in self.ALLOW_ENABLE_FEATURES | |
223 | and feature not in format_bitmask(image.features())): | |
11fdf7f2 TL |
224 | f_bitmask = format_features([feature]) |
225 | image.update_features(f_bitmask, True) | |
226 | ||
227 | RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).set_configuration( | |
228 | configuration) | |
1e59de90 TL |
229 | if metadata: |
230 | RbdImageMetadataService(image).set_metadata(metadata) | |
11fdf7f2 | 231 | |
2a845540 TL |
232 | if primary and not mirror_image_info['primary']: |
233 | RbdMirroringService.promote_image( | |
1e59de90 | 234 | image_name, pool_name, namespace, force) |
2a845540 TL |
235 | elif primary is False and mirror_image_info['primary']: |
236 | RbdMirroringService.demote_image( | |
237 | image_name, pool_name, namespace) | |
238 | ||
239 | if resync: | |
240 | RbdMirroringService.resync_image(image_name, pool_name, namespace) | |
241 | ||
242 | if schedule_interval: | |
243 | RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval) | |
244 | ||
245 | if remove_scheduling: | |
246 | RbdMirroringService.snapshot_schedule_remove(image_spec) | |
247 | ||
9f95a23c | 248 | return rbd_image_call(pool_name, namespace, image_name, _edit) |
11fdf7f2 TL |
249 | |
250 | @RbdTask('copy', | |
9f95a23c | 251 | {'src_image_spec': '{image_spec}', |
11fdf7f2 | 252 | 'dest_pool_name': '{dest_pool_name}', |
9f95a23c | 253 | 'dest_namespace': '{dest_namespace}', |
11fdf7f2 TL |
254 | 'dest_image_name': '{dest_image_name}'}, 2.0) |
255 | @RESTController.Resource('POST') | |
f91f0fd5 | 256 | @allow_empty_body |
9f95a23c TL |
257 | def copy(self, image_spec, dest_pool_name, dest_namespace, dest_image_name, |
258 | snapshot_name=None, obj_size=None, features=None, | |
1e59de90 TL |
259 | stripe_unit=None, stripe_count=None, data_pool=None, |
260 | configuration=None, metadata=None): | |
9f95a23c | 261 | pool_name, namespace, image_name = parse_image_spec(image_spec) |
11fdf7f2 TL |
262 | |
263 | def _src_copy(s_ioctx, s_img): | |
264 | def _copy(d_ioctx): | |
265 | # Set order | |
266 | l_order = None | |
267 | if obj_size and obj_size > 0: | |
268 | l_order = int(round(math.log(float(obj_size), 2))) | |
269 | ||
270 | # Set features | |
271 | feature_bitmask = format_features(features) | |
272 | ||
273 | if snapshot_name: | |
274 | s_img.set_snap(snapshot_name) | |
275 | ||
276 | s_img.copy(d_ioctx, dest_image_name, feature_bitmask, l_order, | |
277 | stripe_unit, stripe_count, data_pool) | |
278 | RbdConfiguration(pool_ioctx=d_ioctx, image_name=dest_image_name).set_configuration( | |
279 | configuration) | |
1e59de90 TL |
280 | if metadata: |
281 | with rbd.Image(d_ioctx, dest_image_name) as image: | |
282 | RbdImageMetadataService(image).set_metadata(metadata) | |
11fdf7f2 | 283 | |
9f95a23c | 284 | return rbd_call(dest_pool_name, dest_namespace, _copy) |
11fdf7f2 | 285 | |
9f95a23c | 286 | return rbd_image_call(pool_name, namespace, image_name, _src_copy) |
11fdf7f2 | 287 | |
9f95a23c | 288 | @RbdTask('flatten', ['{image_spec}'], 2.0) |
11fdf7f2 TL |
289 | @RESTController.Resource('POST') |
290 | @UpdatePermission | |
f91f0fd5 | 291 | @allow_empty_body |
9f95a23c | 292 | def flatten(self, image_spec): |
11fdf7f2 TL |
293 | |
294 | def _flatten(ioctx, image): | |
295 | image.flatten() | |
296 | ||
9f95a23c TL |
297 | pool_name, namespace, image_name = parse_image_spec(image_spec) |
298 | return rbd_image_call(pool_name, namespace, image_name, _flatten) | |
11fdf7f2 TL |
299 | |
300 | @RESTController.Collection('GET') | |
301 | def default_features(self): | |
302 | rbd_default_features = mgr.get('config')['rbd_default_features'] | |
303 | return format_bitmask(int(rbd_default_features)) | |
304 | ||
f67539c2 TL |
305 | @RESTController.Collection('GET') |
306 | def clone_format_version(self): | |
307 | """Return the RBD clone format version. | |
308 | """ | |
309 | rbd_default_clone_format = mgr.get('config')['rbd_default_clone_format'] | |
310 | if rbd_default_clone_format != 'auto': | |
311 | return int(rbd_default_clone_format) | |
312 | osd_map = mgr.get_osdmap().dump() | |
313 | min_compat_client = osd_map.get('min_compat_client', '') | |
314 | require_min_compat_client = osd_map.get('require_min_compat_client', '') | |
315 | if max(min_compat_client, require_min_compat_client) < 'mimic': | |
316 | return 1 | |
317 | ||
318 | return 2 | |
319 | ||
9f95a23c | 320 | @RbdTask('trash/move', ['{image_spec}'], 2.0) |
11fdf7f2 | 321 | @RESTController.Resource('POST') |
f91f0fd5 | 322 | @allow_empty_body |
9f95a23c | 323 | def move_trash(self, image_spec, delay=0): |
11fdf7f2 TL |
324 | """Move an image to the trash. |
325 | Images, even ones actively in-use by clones, | |
326 | can be moved to the trash and deleted at a later time. | |
327 | """ | |
9f95a23c | 328 | pool_name, namespace, image_name = parse_image_spec(image_spec) |
11fdf7f2 | 329 | rbd_inst = rbd.RBD() |
9f95a23c | 330 | return rbd_call(pool_name, namespace, rbd_inst.trash_move, image_name, delay) |
11fdf7f2 TL |
331 | |
332 | ||
2a845540 TL |
333 | @UIRouter('/block/rbd') |
334 | class RbdStatus(BaseController): | |
335 | @EndpointDoc("Display RBD Image feature status") | |
336 | @Endpoint() | |
337 | @ReadPermission | |
338 | def status(self): | |
339 | status = {'available': True, 'message': None} | |
340 | if not CephService.get_pool_list('rbd'): | |
341 | status['available'] = False | |
342 | status['message'] = 'No RBD pools in the cluster. Please create a pool '\ | |
343 | 'with the "rbd" application label.' # type: ignore | |
344 | return status | |
345 | ||
346 | ||
a4b75251 TL |
347 | @APIRouter('/block/image/{image_spec}/snap', Scope.RBD_IMAGE) |
348 | @APIDoc("RBD Snapshot Management API", "RbdSnapshot") | |
11fdf7f2 TL |
349 | class RbdSnapshot(RESTController): |
350 | ||
351 | RESOURCE_ID = "snapshot_name" | |
352 | ||
353 | @RbdTask('snap/create', | |
1e59de90 TL |
354 | ['{image_spec}', '{snapshot_name}', '{mirrorImageSnapshot}'], 2.0) |
355 | def create(self, image_spec, snapshot_name, mirrorImageSnapshot): | |
9f95a23c TL |
356 | pool_name, namespace, image_name = parse_image_spec(image_spec) |
357 | ||
11fdf7f2 | 358 | def _create_snapshot(ioctx, img, snapshot_name): |
2a845540 TL |
359 | mirror_info = img.mirror_image_get_info() |
360 | mirror_mode = img.mirror_image_get_mode() | |
1e59de90 | 361 | if (mirror_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED and mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT) and mirrorImageSnapshot: # noqa E501 #pylint: disable=line-too-long |
2a845540 TL |
362 | img.mirror_image_create_snapshot() |
363 | else: | |
364 | img.create_snap(snapshot_name) | |
11fdf7f2 | 365 | |
9f95a23c TL |
366 | return rbd_image_call(pool_name, namespace, image_name, _create_snapshot, |
367 | snapshot_name) | |
11fdf7f2 TL |
368 | |
369 | @RbdTask('snap/delete', | |
9f95a23c TL |
370 | ['{image_spec}', '{snapshot_name}'], 2.0) |
371 | def delete(self, image_spec, snapshot_name): | |
372 | return RbdSnapshotService.remove_snapshot(image_spec, snapshot_name) | |
11fdf7f2 TL |
373 | |
374 | @RbdTask('snap/edit', | |
9f95a23c TL |
375 | ['{image_spec}', '{snapshot_name}'], 4.0) |
376 | def set(self, image_spec, snapshot_name, new_snap_name=None, | |
11fdf7f2 TL |
377 | is_protected=None): |
378 | def _edit(ioctx, img, snapshot_name): | |
379 | if new_snap_name and new_snap_name != snapshot_name: | |
380 | img.rename_snap(snapshot_name, new_snap_name) | |
381 | snapshot_name = new_snap_name | |
382 | if is_protected is not None and \ | |
383 | is_protected != img.is_protected_snap(snapshot_name): | |
384 | if is_protected: | |
385 | img.protect_snap(snapshot_name) | |
386 | else: | |
387 | img.unprotect_snap(snapshot_name) | |
388 | ||
9f95a23c TL |
389 | pool_name, namespace, image_name = parse_image_spec(image_spec) |
390 | return rbd_image_call(pool_name, namespace, image_name, _edit, snapshot_name) | |
11fdf7f2 TL |
391 | |
392 | @RbdTask('snap/rollback', | |
9f95a23c | 393 | ['{image_spec}', '{snapshot_name}'], 5.0) |
11fdf7f2 TL |
394 | @RESTController.Resource('POST') |
395 | @UpdatePermission | |
f91f0fd5 | 396 | @allow_empty_body |
9f95a23c | 397 | def rollback(self, image_spec, snapshot_name): |
11fdf7f2 TL |
398 | def _rollback(ioctx, img, snapshot_name): |
399 | img.rollback_to_snap(snapshot_name) | |
9f95a23c TL |
400 | |
401 | pool_name, namespace, image_name = parse_image_spec(image_spec) | |
402 | return rbd_image_call(pool_name, namespace, image_name, _rollback, snapshot_name) | |
11fdf7f2 TL |
403 | |
404 | @RbdTask('clone', | |
9f95a23c | 405 | {'parent_image_spec': '{image_spec}', |
11fdf7f2 | 406 | 'child_pool_name': '{child_pool_name}', |
9f95a23c | 407 | 'child_namespace': '{child_namespace}', |
11fdf7f2 TL |
408 | 'child_image_name': '{child_image_name}'}, 2.0) |
409 | @RESTController.Resource('POST') | |
f91f0fd5 | 410 | @allow_empty_body |
9f95a23c TL |
411 | def clone(self, image_spec, snapshot_name, child_pool_name, |
412 | child_image_name, child_namespace=None, obj_size=None, features=None, | |
1e59de90 TL |
413 | stripe_unit=None, stripe_count=None, data_pool=None, |
414 | configuration=None, metadata=None): | |
11fdf7f2 TL |
415 | """ |
416 | Clones a snapshot to an image | |
417 | """ | |
418 | ||
9f95a23c TL |
419 | pool_name, namespace, image_name = parse_image_spec(image_spec) |
420 | ||
11fdf7f2 TL |
421 | def _parent_clone(p_ioctx): |
422 | def _clone(ioctx): | |
423 | # Set order | |
424 | l_order = None | |
425 | if obj_size and obj_size > 0: | |
426 | l_order = int(round(math.log(float(obj_size), 2))) | |
427 | ||
428 | # Set features | |
429 | feature_bitmask = format_features(features) | |
430 | ||
431 | rbd_inst = rbd.RBD() | |
432 | rbd_inst.clone(p_ioctx, image_name, snapshot_name, ioctx, | |
433 | child_image_name, feature_bitmask, l_order, | |
434 | stripe_unit, stripe_count, data_pool) | |
435 | ||
436 | RbdConfiguration(pool_ioctx=ioctx, image_name=child_image_name).set_configuration( | |
437 | configuration) | |
1e59de90 TL |
438 | if metadata: |
439 | with rbd.Image(ioctx, child_image_name) as image: | |
440 | RbdImageMetadataService(image).set_metadata(metadata) | |
11fdf7f2 | 441 | |
9f95a23c | 442 | return rbd_call(child_pool_name, child_namespace, _clone) |
11fdf7f2 | 443 | |
9f95a23c | 444 | rbd_call(pool_name, namespace, _parent_clone) |
11fdf7f2 TL |
445 | |
446 | ||
a4b75251 TL |
447 | @APIRouter('/block/image/trash', Scope.RBD_IMAGE) |
448 | @APIDoc("RBD Trash Management API", "RbdTrash") | |
11fdf7f2 | 449 | class RbdTrash(RESTController): |
9f95a23c | 450 | RESOURCE_ID = "image_id_spec" |
f67539c2 TL |
451 | |
452 | def __init__(self): | |
453 | super().__init__() | |
454 | self.rbd_inst = rbd.RBD() | |
11fdf7f2 TL |
455 | |
456 | @ViewCache() | |
457 | def _trash_pool_list(self, pool_name): | |
458 | with mgr.rados.open_ioctx(pool_name) as ioctx: | |
11fdf7f2 | 459 | result = [] |
9f95a23c TL |
460 | namespaces = self.rbd_inst.namespace_list(ioctx) |
461 | # images without namespace | |
462 | namespaces.append('') | |
463 | for namespace in namespaces: | |
464 | ioctx.set_namespace(namespace) | |
465 | images = self.rbd_inst.trash_list(ioctx) | |
466 | for trash in images: | |
467 | trash['pool_name'] = pool_name | |
468 | trash['namespace'] = namespace | |
469 | trash['deletion_time'] = "{}Z".format(trash['deletion_time'].isoformat()) | |
470 | trash['deferment_end_time'] = "{}Z".format( | |
471 | trash['deferment_end_time'].isoformat()) | |
472 | result.append(trash) | |
11fdf7f2 TL |
473 | return result |
474 | ||
475 | def _trash_list(self, pool_name=None): | |
476 | if pool_name: | |
477 | pools = [pool_name] | |
478 | else: | |
479 | pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')] | |
480 | ||
481 | result = [] | |
482 | for pool in pools: | |
483 | # pylint: disable=unbalanced-tuple-unpacking | |
484 | status, value = self._trash_pool_list(pool) | |
485 | result.append({'status': status, 'value': value, 'pool_name': pool}) | |
486 | return result | |
487 | ||
488 | @handle_rbd_error() | |
489 | @handle_rados_error('pool') | |
f67539c2 TL |
490 | @EndpointDoc("Get RBD Trash Details by pool name", |
491 | parameters={ | |
492 | 'pool_name': (str, 'Name of the pool'), | |
493 | }, | |
494 | responses={200: RBD_TRASH_SCHEMA}) | |
11fdf7f2 TL |
495 | def list(self, pool_name=None): |
496 | """List all entries from trash.""" | |
497 | return self._trash_list(pool_name) | |
498 | ||
499 | @handle_rbd_error() | |
500 | @handle_rados_error('pool') | |
501 | @RbdTask('trash/purge', ['{pool_name}'], 2.0) | |
502 | @RESTController.Collection('POST', query_params=['pool_name']) | |
503 | @DeletePermission | |
f91f0fd5 | 504 | @allow_empty_body |
11fdf7f2 TL |
505 | def purge(self, pool_name=None): |
506 | """Remove all expired images from trash.""" | |
1911f103 | 507 | now = "{}Z".format(datetime.utcnow().isoformat()) |
11fdf7f2 TL |
508 | pools = self._trash_list(pool_name) |
509 | ||
510 | for pool in pools: | |
511 | for image in pool['value']: | |
512 | if image['deferment_end_time'] < now: | |
1911f103 TL |
513 | logger.info('Removing trash image %s (pool=%s, namespace=%s, name=%s)', |
514 | image['id'], pool['pool_name'], image['namespace'], image['name']) | |
9f95a23c TL |
515 | rbd_call(pool['pool_name'], image['namespace'], |
516 | self.rbd_inst.trash_remove, image['id'], 0) | |
11fdf7f2 | 517 | |
9f95a23c | 518 | @RbdTask('trash/restore', ['{image_id_spec}', '{new_image_name}'], 2.0) |
11fdf7f2 TL |
519 | @RESTController.Resource('POST') |
520 | @CreatePermission | |
f91f0fd5 | 521 | @allow_empty_body |
9f95a23c | 522 | def restore(self, image_id_spec, new_image_name): |
11fdf7f2 | 523 | """Restore an image from trash.""" |
9f95a23c TL |
524 | pool_name, namespace, image_id = parse_image_spec(image_id_spec) |
525 | return rbd_call(pool_name, namespace, self.rbd_inst.trash_restore, image_id, | |
526 | new_image_name) | |
11fdf7f2 | 527 | |
9f95a23c TL |
528 | @RbdTask('trash/remove', ['{image_id_spec}'], 2.0) |
529 | def delete(self, image_id_spec, force=False): | |
11fdf7f2 TL |
530 | """Delete an image from trash. |
531 | If image deferment time has not expired you can not removed it unless use force. | |
532 | But an actively in-use by clones or has snapshots can not be removed. | |
533 | """ | |
9f95a23c TL |
534 | pool_name, namespace, image_id = parse_image_spec(image_id_spec) |
535 | return rbd_call(pool_name, namespace, self.rbd_inst.trash_remove, image_id, | |
536 | int(str_to_bool(force))) | |
537 | ||
538 | ||
a4b75251 TL |
539 | @APIRouter('/block/pool/{pool_name}/namespace', Scope.RBD_IMAGE) |
540 | @APIDoc("RBD Namespace Management API", "RbdNamespace") | |
9f95a23c | 541 | class RbdNamespace(RESTController): |
f67539c2 TL |
542 | |
543 | def __init__(self): | |
544 | super().__init__() | |
545 | self.rbd_inst = rbd.RBD() | |
9f95a23c TL |
546 | |
547 | def create(self, pool_name, namespace): | |
548 | with mgr.rados.open_ioctx(pool_name) as ioctx: | |
549 | namespaces = self.rbd_inst.namespace_list(ioctx) | |
550 | if namespace in namespaces: | |
551 | raise DashboardException( | |
552 | msg='Namespace already exists', | |
553 | code='namespace_already_exists', | |
554 | component='rbd') | |
555 | return self.rbd_inst.namespace_create(ioctx, namespace) | |
556 | ||
557 | def delete(self, pool_name, namespace): | |
558 | with mgr.rados.open_ioctx(pool_name) as ioctx: | |
559 | # pylint: disable=unbalanced-tuple-unpacking | |
2a845540 | 560 | images, _ = RbdService.rbd_pool_list([pool_name], namespace=namespace) |
9f95a23c TL |
561 | if images: |
562 | raise DashboardException( | |
563 | msg='Namespace contains images which must be deleted first', | |
564 | code='namespace_contains_images', | |
565 | component='rbd') | |
566 | return self.rbd_inst.namespace_remove(ioctx, namespace) | |
567 | ||
568 | def list(self, pool_name): | |
569 | with mgr.rados.open_ioctx(pool_name) as ioctx: | |
570 | result = [] | |
571 | namespaces = self.rbd_inst.namespace_list(ioctx) | |
572 | for namespace in namespaces: | |
573 | # pylint: disable=unbalanced-tuple-unpacking | |
2a845540 | 574 | images, _ = RbdService.rbd_pool_list([pool_name], namespace=namespace) |
9f95a23c TL |
575 | result.append({ |
576 | 'namespace': namespace, | |
577 | 'num_images': len(images) if images else 0 | |
578 | }) | |
579 | return result |