]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/controllers/rbd.py
bump version to 18.2.4-pve3
[ceph.git] / ceph / src / pybind / mgr / dashboard / controllers / rbd.py
CommitLineData
11fdf7f2
TL
1# -*- coding: utf-8 -*-
2# pylint: disable=unused-argument
3# pylint: disable=too-many-statements,too-many-branches
11fdf7f2 4
1911f103 5import logging
11fdf7f2 6import math
11fdf7f2 7from datetime import datetime
f67539c2 8from functools import partial
11fdf7f2 9
2a845540 10import cherrypy
11fdf7f2
TL
11import rbd
12
11fdf7f2 13from .. import mgr
9f95a23c 14from ..exceptions import DashboardException
11fdf7f2
TL
15from ..security import Scope
16from ..services.ceph_service import CephService
f67539c2 17from ..services.exception import handle_rados_error, handle_rbd_error, serialize_dashboard_exception
2a845540 18from ..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 22from ..tools import ViewCache, str_to_bool
2a845540
TL
23from . import APIDoc, APIRouter, BaseController, CreatePermission, \
24 DeletePermission, Endpoint, EndpointDoc, ReadPermission, RESTController, \
25 Task, UIRouter, UpdatePermission, allow_empty_body
26from ._version import APIVersion
11fdf7f2 27
1911f103
TL
28logger = logging.getLogger(__name__)
29
f67539c2 30RBD_SCHEMA = ([{
f67539c2
TL
31 "value": ([str], ''),
32 "pool_name": (str, 'pool name')
33}])
34
35RBD_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 43def 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
a4b75251
TL
52@APIRouter('/block/image', Scope.RBD_IMAGE)
53@APIDoc("RBD Management API", "Rbd")
11fdf7f2
TL
54class Rbd(RESTController):
55
2a845540
TL
56 DEFAULT_LIMIT = 5
57
58 def _rbd_list(self, pool_name=None, offset=0, limit=DEFAULT_LIMIT, search='', sort=''):
11fdf7f2
TL
59 if pool_name:
60 pools = [pool_name]
61 else:
62 pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
63
2a845540
TL
64 images, num_total_images = RbdService.rbd_pool_list(
65 pools, offset=offset, limit=limit, search=search, sort=sort)
66 cherrypy.response.headers['X-Total-Count'] = num_total_images
67 pool_result = {}
68 for i, image in enumerate(images):
69 pool = image['pool_name']
70 if pool not in pool_result:
71 pool_result[pool] = {'value': [], 'pool_name': image['pool_name']}
72 pool_result[pool]['value'].append(image)
73
74 images[i]['configuration'] = RbdConfiguration(
75 pool, image['namespace'], image['name']).list()
1e59de90
TL
76 images[i]['metadata'] = rbd_image_call(
77 pool, image['namespace'], image['name'],
78 lambda ioctx, image: RbdImageMetadataService(image).list())
79
2a845540 80 return list(pool_result.values())
11fdf7f2
TL
81
82 @handle_rbd_error()
83 @handle_rados_error('pool')
f67539c2
TL
84 @EndpointDoc("Display Rbd Images",
85 parameters={
86 'pool_name': (str, 'Pool Name'),
2a845540
TL
87 'limit': (int, 'limit'),
88 'offset': (int, 'offset'),
f67539c2
TL
89 },
90 responses={200: RBD_SCHEMA})
2a845540
TL
91 @RESTController.MethodMap(version=APIVersion(2, 0)) # type: ignore
92 def list(self, pool_name=None, offset: int = 0, limit: int = DEFAULT_LIMIT,
93 search: str = '', sort: str = ''):
39ae355f
TL
94 return self._rbd_list(pool_name, offset=int(offset), limit=int(limit),
95 search=search, sort=sort)
11fdf7f2
TL
96
97 @handle_rbd_error()
98 @handle_rados_error('pool')
9f95a23c
TL
99 def get(self, image_spec):
100 return RbdService.get_image(image_spec)
11fdf7f2
TL
101
102 @RbdTask('create',
9f95a23c 103 {'pool_name': '{pool_name}', 'namespace': '{namespace}', 'image_name': '{name}'}, 2.0)
2a845540
TL
104 def create(self, name, pool_name, size, namespace=None, schedule_interval='',
105 obj_size=None, features=None, stripe_unit=None, stripe_count=None,
1e59de90
TL
106 data_pool=None, configuration=None, metadata=None,
107 mirror_mode=None):
11fdf7f2 108
aee94f69
TL
109 RbdService.create(name, pool_name, size, namespace,
110 obj_size, features, stripe_unit, stripe_count,
111 data_pool, configuration, metadata)
1e59de90 112
2a845540
TL
113 if mirror_mode:
114 RbdMirroringService.enable_image(name, pool_name, namespace,
115 MIRROR_IMAGE_MODE[mirror_mode])
116
117 if schedule_interval:
118 image_spec = get_image_spec(pool_name, namespace, name)
119 RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval)
9f95a23c
TL
120
121 @RbdTask('delete', ['{image_spec}'], 2.0)
122 def delete(self, image_spec):
aee94f69 123 return RbdService.delete(image_spec)
9f95a23c
TL
124
125 @RbdTask('edit', ['{image_spec}', '{name}'], 4.0)
2a845540 126 def set(self, image_spec, name=None, size=None, features=None,
1e59de90
TL
127 configuration=None, metadata=None, enable_mirror=None, primary=None,
128 force=False, resync=False, mirror_mode=None, schedule_interval='',
2a845540 129 remove_scheduling=False):
aee94f69
TL
130 return RbdService.set(image_spec, name, size, features,
131 configuration, metadata, enable_mirror, primary,
132 force, resync, mirror_mode, schedule_interval,
133 remove_scheduling)
11fdf7f2
TL
134
135 @RbdTask('copy',
9f95a23c 136 {'src_image_spec': '{image_spec}',
11fdf7f2 137 'dest_pool_name': '{dest_pool_name}',
9f95a23c 138 'dest_namespace': '{dest_namespace}',
11fdf7f2
TL
139 'dest_image_name': '{dest_image_name}'}, 2.0)
140 @RESTController.Resource('POST')
f91f0fd5 141 @allow_empty_body
9f95a23c
TL
142 def copy(self, image_spec, dest_pool_name, dest_namespace, dest_image_name,
143 snapshot_name=None, obj_size=None, features=None,
1e59de90
TL
144 stripe_unit=None, stripe_count=None, data_pool=None,
145 configuration=None, metadata=None):
aee94f69
TL
146 return RbdService.copy(image_spec, dest_pool_name, dest_namespace, dest_image_name,
147 snapshot_name, obj_size, features,
148 stripe_unit, stripe_count, data_pool,
149 configuration, metadata)
11fdf7f2 150
9f95a23c 151 @RbdTask('flatten', ['{image_spec}'], 2.0)
11fdf7f2
TL
152 @RESTController.Resource('POST')
153 @UpdatePermission
f91f0fd5 154 @allow_empty_body
9f95a23c 155 def flatten(self, image_spec):
aee94f69 156 return RbdService.flatten(image_spec)
11fdf7f2
TL
157
158 @RESTController.Collection('GET')
159 def default_features(self):
160 rbd_default_features = mgr.get('config')['rbd_default_features']
161 return format_bitmask(int(rbd_default_features))
162
f67539c2
TL
163 @RESTController.Collection('GET')
164 def clone_format_version(self):
165 """Return the RBD clone format version.
166 """
167 rbd_default_clone_format = mgr.get('config')['rbd_default_clone_format']
168 if rbd_default_clone_format != 'auto':
169 return int(rbd_default_clone_format)
170 osd_map = mgr.get_osdmap().dump()
171 min_compat_client = osd_map.get('min_compat_client', '')
172 require_min_compat_client = osd_map.get('require_min_compat_client', '')
173 if max(min_compat_client, require_min_compat_client) < 'mimic':
174 return 1
175
176 return 2
177
9f95a23c 178 @RbdTask('trash/move', ['{image_spec}'], 2.0)
11fdf7f2 179 @RESTController.Resource('POST')
f91f0fd5 180 @allow_empty_body
9f95a23c 181 def move_trash(self, image_spec, delay=0):
11fdf7f2
TL
182 """Move an image to the trash.
183 Images, even ones actively in-use by clones,
184 can be moved to the trash and deleted at a later time.
185 """
aee94f69 186 return RbdService.move_image_to_trash(image_spec, delay)
11fdf7f2
TL
187
188
2a845540
TL
189@UIRouter('/block/rbd')
190class RbdStatus(BaseController):
191 @EndpointDoc("Display RBD Image feature status")
192 @Endpoint()
193 @ReadPermission
194 def status(self):
195 status = {'available': True, 'message': None}
196 if not CephService.get_pool_list('rbd'):
197 status['available'] = False
198 status['message'] = 'No RBD pools in the cluster. Please create a pool '\
199 'with the "rbd" application label.' # type: ignore
200 return status
201
202
a4b75251
TL
203@APIRouter('/block/image/{image_spec}/snap', Scope.RBD_IMAGE)
204@APIDoc("RBD Snapshot Management API", "RbdSnapshot")
11fdf7f2
TL
205class RbdSnapshot(RESTController):
206
207 RESOURCE_ID = "snapshot_name"
208
209 @RbdTask('snap/create',
1e59de90
TL
210 ['{image_spec}', '{snapshot_name}', '{mirrorImageSnapshot}'], 2.0)
211 def create(self, image_spec, snapshot_name, mirrorImageSnapshot):
9f95a23c
TL
212 pool_name, namespace, image_name = parse_image_spec(image_spec)
213
11fdf7f2 214 def _create_snapshot(ioctx, img, snapshot_name):
2a845540
TL
215 mirror_info = img.mirror_image_get_info()
216 mirror_mode = img.mirror_image_get_mode()
1e59de90 217 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
218 img.mirror_image_create_snapshot()
219 else:
220 img.create_snap(snapshot_name)
11fdf7f2 221
9f95a23c
TL
222 return rbd_image_call(pool_name, namespace, image_name, _create_snapshot,
223 snapshot_name)
11fdf7f2
TL
224
225 @RbdTask('snap/delete',
9f95a23c
TL
226 ['{image_spec}', '{snapshot_name}'], 2.0)
227 def delete(self, image_spec, snapshot_name):
228 return RbdSnapshotService.remove_snapshot(image_spec, snapshot_name)
11fdf7f2
TL
229
230 @RbdTask('snap/edit',
9f95a23c
TL
231 ['{image_spec}', '{snapshot_name}'], 4.0)
232 def set(self, image_spec, snapshot_name, new_snap_name=None,
11fdf7f2
TL
233 is_protected=None):
234 def _edit(ioctx, img, snapshot_name):
235 if new_snap_name and new_snap_name != snapshot_name:
236 img.rename_snap(snapshot_name, new_snap_name)
237 snapshot_name = new_snap_name
238 if is_protected is not None and \
239 is_protected != img.is_protected_snap(snapshot_name):
240 if is_protected:
241 img.protect_snap(snapshot_name)
242 else:
243 img.unprotect_snap(snapshot_name)
244
9f95a23c
TL
245 pool_name, namespace, image_name = parse_image_spec(image_spec)
246 return rbd_image_call(pool_name, namespace, image_name, _edit, snapshot_name)
11fdf7f2
TL
247
248 @RbdTask('snap/rollback',
9f95a23c 249 ['{image_spec}', '{snapshot_name}'], 5.0)
11fdf7f2
TL
250 @RESTController.Resource('POST')
251 @UpdatePermission
f91f0fd5 252 @allow_empty_body
9f95a23c 253 def rollback(self, image_spec, snapshot_name):
11fdf7f2
TL
254 def _rollback(ioctx, img, snapshot_name):
255 img.rollback_to_snap(snapshot_name)
9f95a23c
TL
256
257 pool_name, namespace, image_name = parse_image_spec(image_spec)
258 return rbd_image_call(pool_name, namespace, image_name, _rollback, snapshot_name)
11fdf7f2
TL
259
260 @RbdTask('clone',
9f95a23c 261 {'parent_image_spec': '{image_spec}',
11fdf7f2 262 'child_pool_name': '{child_pool_name}',
9f95a23c 263 'child_namespace': '{child_namespace}',
11fdf7f2
TL
264 'child_image_name': '{child_image_name}'}, 2.0)
265 @RESTController.Resource('POST')
f91f0fd5 266 @allow_empty_body
9f95a23c
TL
267 def clone(self, image_spec, snapshot_name, child_pool_name,
268 child_image_name, child_namespace=None, obj_size=None, features=None,
1e59de90
TL
269 stripe_unit=None, stripe_count=None, data_pool=None,
270 configuration=None, metadata=None):
11fdf7f2
TL
271 """
272 Clones a snapshot to an image
273 """
274
9f95a23c
TL
275 pool_name, namespace, image_name = parse_image_spec(image_spec)
276
11fdf7f2
TL
277 def _parent_clone(p_ioctx):
278 def _clone(ioctx):
279 # Set order
280 l_order = None
281 if obj_size and obj_size > 0:
282 l_order = int(round(math.log(float(obj_size), 2)))
283
284 # Set features
285 feature_bitmask = format_features(features)
286
287 rbd_inst = rbd.RBD()
288 rbd_inst.clone(p_ioctx, image_name, snapshot_name, ioctx,
289 child_image_name, feature_bitmask, l_order,
290 stripe_unit, stripe_count, data_pool)
291
292 RbdConfiguration(pool_ioctx=ioctx, image_name=child_image_name).set_configuration(
293 configuration)
1e59de90
TL
294 if metadata:
295 with rbd.Image(ioctx, child_image_name) as image:
296 RbdImageMetadataService(image).set_metadata(metadata)
11fdf7f2 297
9f95a23c 298 return rbd_call(child_pool_name, child_namespace, _clone)
11fdf7f2 299
9f95a23c 300 rbd_call(pool_name, namespace, _parent_clone)
11fdf7f2
TL
301
302
a4b75251
TL
303@APIRouter('/block/image/trash', Scope.RBD_IMAGE)
304@APIDoc("RBD Trash Management API", "RbdTrash")
11fdf7f2 305class RbdTrash(RESTController):
9f95a23c 306 RESOURCE_ID = "image_id_spec"
f67539c2
TL
307
308 def __init__(self):
309 super().__init__()
310 self.rbd_inst = rbd.RBD()
11fdf7f2
TL
311
312 @ViewCache()
313 def _trash_pool_list(self, pool_name):
314 with mgr.rados.open_ioctx(pool_name) as ioctx:
11fdf7f2 315 result = []
9f95a23c
TL
316 namespaces = self.rbd_inst.namespace_list(ioctx)
317 # images without namespace
318 namespaces.append('')
319 for namespace in namespaces:
320 ioctx.set_namespace(namespace)
321 images = self.rbd_inst.trash_list(ioctx)
322 for trash in images:
323 trash['pool_name'] = pool_name
324 trash['namespace'] = namespace
325 trash['deletion_time'] = "{}Z".format(trash['deletion_time'].isoformat())
326 trash['deferment_end_time'] = "{}Z".format(
327 trash['deferment_end_time'].isoformat())
328 result.append(trash)
11fdf7f2
TL
329 return result
330
331 def _trash_list(self, pool_name=None):
332 if pool_name:
333 pools = [pool_name]
334 else:
335 pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
336
337 result = []
338 for pool in pools:
339 # pylint: disable=unbalanced-tuple-unpacking
340 status, value = self._trash_pool_list(pool)
341 result.append({'status': status, 'value': value, 'pool_name': pool})
342 return result
343
344 @handle_rbd_error()
345 @handle_rados_error('pool')
f67539c2
TL
346 @EndpointDoc("Get RBD Trash Details by pool name",
347 parameters={
348 'pool_name': (str, 'Name of the pool'),
349 },
350 responses={200: RBD_TRASH_SCHEMA})
11fdf7f2
TL
351 def list(self, pool_name=None):
352 """List all entries from trash."""
353 return self._trash_list(pool_name)
354
355 @handle_rbd_error()
356 @handle_rados_error('pool')
357 @RbdTask('trash/purge', ['{pool_name}'], 2.0)
358 @RESTController.Collection('POST', query_params=['pool_name'])
359 @DeletePermission
f91f0fd5 360 @allow_empty_body
11fdf7f2
TL
361 def purge(self, pool_name=None):
362 """Remove all expired images from trash."""
1911f103 363 now = "{}Z".format(datetime.utcnow().isoformat())
11fdf7f2
TL
364 pools = self._trash_list(pool_name)
365
366 for pool in pools:
367 for image in pool['value']:
368 if image['deferment_end_time'] < now:
1911f103
TL
369 logger.info('Removing trash image %s (pool=%s, namespace=%s, name=%s)',
370 image['id'], pool['pool_name'], image['namespace'], image['name'])
9f95a23c
TL
371 rbd_call(pool['pool_name'], image['namespace'],
372 self.rbd_inst.trash_remove, image['id'], 0)
11fdf7f2 373
9f95a23c 374 @RbdTask('trash/restore', ['{image_id_spec}', '{new_image_name}'], 2.0)
11fdf7f2
TL
375 @RESTController.Resource('POST')
376 @CreatePermission
f91f0fd5 377 @allow_empty_body
9f95a23c 378 def restore(self, image_id_spec, new_image_name):
11fdf7f2 379 """Restore an image from trash."""
9f95a23c
TL
380 pool_name, namespace, image_id = parse_image_spec(image_id_spec)
381 return rbd_call(pool_name, namespace, self.rbd_inst.trash_restore, image_id,
382 new_image_name)
11fdf7f2 383
9f95a23c
TL
384 @RbdTask('trash/remove', ['{image_id_spec}'], 2.0)
385 def delete(self, image_id_spec, force=False):
11fdf7f2
TL
386 """Delete an image from trash.
387 If image deferment time has not expired you can not removed it unless use force.
388 But an actively in-use by clones or has snapshots can not be removed.
389 """
9f95a23c
TL
390 pool_name, namespace, image_id = parse_image_spec(image_id_spec)
391 return rbd_call(pool_name, namespace, self.rbd_inst.trash_remove, image_id,
392 int(str_to_bool(force)))
393
394
a4b75251
TL
395@APIRouter('/block/pool/{pool_name}/namespace', Scope.RBD_IMAGE)
396@APIDoc("RBD Namespace Management API", "RbdNamespace")
9f95a23c 397class RbdNamespace(RESTController):
f67539c2
TL
398
399 def __init__(self):
400 super().__init__()
401 self.rbd_inst = rbd.RBD()
9f95a23c
TL
402
403 def create(self, pool_name, namespace):
404 with mgr.rados.open_ioctx(pool_name) as ioctx:
405 namespaces = self.rbd_inst.namespace_list(ioctx)
406 if namespace in namespaces:
407 raise DashboardException(
408 msg='Namespace already exists',
409 code='namespace_already_exists',
410 component='rbd')
411 return self.rbd_inst.namespace_create(ioctx, namespace)
412
413 def delete(self, pool_name, namespace):
414 with mgr.rados.open_ioctx(pool_name) as ioctx:
415 # pylint: disable=unbalanced-tuple-unpacking
2a845540 416 images, _ = RbdService.rbd_pool_list([pool_name], namespace=namespace)
9f95a23c
TL
417 if images:
418 raise DashboardException(
419 msg='Namespace contains images which must be deleted first',
420 code='namespace_contains_images',
421 component='rbd')
422 return self.rbd_inst.namespace_remove(ioctx, namespace)
423
424 def list(self, pool_name):
425 with mgr.rados.open_ioctx(pool_name) as ioctx:
426 result = []
427 namespaces = self.rbd_inst.namespace_list(ioctx)
428 for namespace in namespaces:
429 # pylint: disable=unbalanced-tuple-unpacking
2a845540 430 images, _ = RbdService.rbd_pool_list([pool_name], namespace=namespace)
9f95a23c
TL
431 result.append({
432 'namespace': namespace,
433 'num_images': len(images) if images else 0
434 })
435 return result