1 # -*- coding: utf-8 -*-
2 # pylint: disable=unused-argument
3 # pylint: disable=too-many-statements,too-many-branches
7 from datetime
import datetime
8 from functools
import partial
14 from ..exceptions
import DashboardException
15 from ..security
import Scope
16 from ..services
.ceph_service
import CephService
17 from ..services
.exception
import handle_rados_error
, handle_rbd_error
, serialize_dashboard_exception
18 from ..services
.rbd
import MIRROR_IMAGE_MODE
, RbdConfiguration
, \
19 RbdImageMetadataService
, RbdMirroringService
, RbdService
, \
20 RbdSnapshotService
, format_bitmask
, format_features
, get_image_spec
, \
21 parse_image_spec
, rbd_call
, rbd_image_call
22 from ..tools
import ViewCache
, str_to_bool
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
28 logger
= logging
.getLogger(__name__
)
32 "pool_name": (str, 'pool name')
38 "pool_name": (str, 'pool name')
42 # pylint: disable=not-callable
43 def RbdTask(name
, metadata
, wait_for
): # noqa: N802
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
52 @APIRouter('/block/image', Scope
.RBD_IMAGE
)
53 @APIDoc("RBD Management API", "Rbd")
54 class Rbd(RESTController
):
58 def _rbd_list(self
, pool_name
=None, offset
=0, limit
=DEFAULT_LIMIT
, search
='', sort
=''):
62 pools
= [p
['pool_name'] for p
in CephService
.get_pool_list('rbd')]
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
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
)
74 images
[i
]['configuration'] = RbdConfiguration(
75 pool
, image
['namespace'], image
['name']).list()
76 images
[i
]['metadata'] = rbd_image_call(
77 pool
, image
['namespace'], image
['name'],
78 lambda ioctx
, image
: RbdImageMetadataService(image
).list())
80 return list(pool_result
.values())
83 @handle_rados_error('pool')
84 @EndpointDoc("Display Rbd Images",
86 'pool_name': (str, 'Pool Name'),
87 'limit': (int, 'limit'),
88 'offset': (int, 'offset'),
90 responses
={200: RBD_SCHEMA
})
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 = ''):
94 return self
._rbd
_list
(pool_name
, offset
=int(offset
), limit
=int(limit
),
95 search
=search
, sort
=sort
)
98 @handle_rados_error('pool')
99 def get(self
, image_spec
):
100 return RbdService
.get_image(image_spec
)
103 {'pool_name': '{pool_name}', 'namespace': '{namespace}', 'image_name': '{name}'}, 2.0)
104 def create(self
, name
, pool_name
, size
, namespace
=None, schedule_interval
='',
105 obj_size
=None, features
=None, stripe_unit
=None, stripe_count
=None,
106 data_pool
=None, configuration
=None, metadata
=None,
109 RbdService
.create(name
, pool_name
, size
, namespace
,
110 obj_size
, features
, stripe_unit
, stripe_count
,
111 data_pool
, configuration
, metadata
)
114 RbdMirroringService
.enable_image(name
, pool_name
, namespace
,
115 MIRROR_IMAGE_MODE
[mirror_mode
])
117 if schedule_interval
:
118 image_spec
= get_image_spec(pool_name
, namespace
, name
)
119 RbdMirroringService
.snapshot_schedule_add(image_spec
, schedule_interval
)
121 @RbdTask('delete', ['{image_spec}'], 2.0)
122 def delete(self
, image_spec
):
123 return RbdService
.delete(image_spec
)
125 @RbdTask('edit', ['{image_spec}', '{name}'], 4.0)
126 def set(self
, image_spec
, name
=None, size
=None, features
=None,
127 configuration
=None, metadata
=None, enable_mirror
=None, primary
=None,
128 force
=False, resync
=False, mirror_mode
=None, schedule_interval
='',
129 remove_scheduling
=False):
130 return RbdService
.set(image_spec
, name
, size
, features
,
131 configuration
, metadata
, enable_mirror
, primary
,
132 force
, resync
, mirror_mode
, schedule_interval
,
136 {'src_image_spec': '{image_spec}',
137 'dest_pool_name': '{dest_pool_name}',
138 'dest_namespace': '{dest_namespace}',
139 'dest_image_name': '{dest_image_name}'}, 2.0)
140 @RESTController.Resource('POST')
142 def copy(self
, image_spec
, dest_pool_name
, dest_namespace
, dest_image_name
,
143 snapshot_name
=None, obj_size
=None, features
=None,
144 stripe_unit
=None, stripe_count
=None, data_pool
=None,
145 configuration
=None, metadata
=None):
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
)
151 @RbdTask('flatten', ['{image_spec}'], 2.0)
152 @RESTController.Resource('POST')
155 def flatten(self
, image_spec
):
156 return RbdService
.flatten(image_spec
)
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
))
163 @RESTController.Collection('GET')
164 def clone_format_version(self
):
165 """Return the RBD clone format version.
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':
178 @RbdTask('trash/move', ['{image_spec}'], 2.0)
179 @RESTController.Resource('POST')
181 def move_trash(self
, image_spec
, delay
=0):
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.
186 return RbdService
.move_image_to_trash(image_spec
, delay
)
189 @UIRouter('/block/rbd')
190 class RbdStatus(BaseController
):
191 @EndpointDoc("Display RBD Image feature status")
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
203 @APIRouter('/block/image/{image_spec}/snap', Scope
.RBD_IMAGE
)
204 @APIDoc("RBD Snapshot Management API", "RbdSnapshot")
205 class RbdSnapshot(RESTController
):
207 RESOURCE_ID
= "snapshot_name"
209 @RbdTask('snap/create',
210 ['{image_spec}', '{snapshot_name}', '{mirrorImageSnapshot}'], 2.0)
211 def create(self
, image_spec
, snapshot_name
, mirrorImageSnapshot
):
212 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
214 def _create_snapshot(ioctx
, img
, snapshot_name
):
215 mirror_info
= img
.mirror_image_get_info()
216 mirror_mode
= img
.mirror_image_get_mode()
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
218 img
.mirror_image_create_snapshot()
220 img
.create_snap(snapshot_name
)
222 return rbd_image_call(pool_name
, namespace
, image_name
, _create_snapshot
,
225 @RbdTask('snap/delete',
226 ['{image_spec}', '{snapshot_name}'], 2.0)
227 def delete(self
, image_spec
, snapshot_name
):
228 return RbdSnapshotService
.remove_snapshot(image_spec
, snapshot_name
)
230 @RbdTask('snap/edit',
231 ['{image_spec}', '{snapshot_name}'], 4.0)
232 def set(self
, image_spec
, snapshot_name
, new_snap_name
=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
):
241 img
.protect_snap(snapshot_name
)
243 img
.unprotect_snap(snapshot_name
)
245 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
246 return rbd_image_call(pool_name
, namespace
, image_name
, _edit
, snapshot_name
)
248 @RbdTask('snap/rollback',
249 ['{image_spec}', '{snapshot_name}'], 5.0)
250 @RESTController.Resource('POST')
253 def rollback(self
, image_spec
, snapshot_name
):
254 def _rollback(ioctx
, img
, snapshot_name
):
255 img
.rollback_to_snap(snapshot_name
)
257 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
258 return rbd_image_call(pool_name
, namespace
, image_name
, _rollback
, snapshot_name
)
261 {'parent_image_spec': '{image_spec}',
262 'child_pool_name': '{child_pool_name}',
263 'child_namespace': '{child_namespace}',
264 'child_image_name': '{child_image_name}'}, 2.0)
265 @RESTController.Resource('POST')
267 def clone(self
, image_spec
, snapshot_name
, child_pool_name
,
268 child_image_name
, child_namespace
=None, obj_size
=None, features
=None,
269 stripe_unit
=None, stripe_count
=None, data_pool
=None,
270 configuration
=None, metadata
=None):
272 Clones a snapshot to an image
275 pool_name
, namespace
, image_name
= parse_image_spec(image_spec
)
277 def _parent_clone(p_ioctx
):
281 if obj_size
and obj_size
> 0:
282 l_order
= int(round(math
.log(float(obj_size
), 2)))
285 feature_bitmask
= format_features(features
)
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
)
292 RbdConfiguration(pool_ioctx
=ioctx
, image_name
=child_image_name
).set_configuration(
295 with rbd
.Image(ioctx
, child_image_name
) as image
:
296 RbdImageMetadataService(image
).set_metadata(metadata
)
298 return rbd_call(child_pool_name
, child_namespace
, _clone
)
300 rbd_call(pool_name
, namespace
, _parent_clone
)
303 @APIRouter('/block/image/trash', Scope
.RBD_IMAGE
)
304 @APIDoc("RBD Trash Management API", "RbdTrash")
305 class RbdTrash(RESTController
):
306 RESOURCE_ID
= "image_id_spec"
310 self
.rbd_inst
= rbd
.RBD()
313 def _trash_pool_list(self
, pool_name
):
314 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
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
)
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())
331 def _trash_list(self
, pool_name
=None):
335 pools
= [p
['pool_name'] for p
in CephService
.get_pool_list('rbd')]
339 # pylint: disable=unbalanced-tuple-unpacking
340 status
, value
= self
._trash
_pool
_list
(pool
)
341 result
.append({'status': status
, 'value': value
, 'pool_name': pool
})
345 @handle_rados_error('pool')
346 @EndpointDoc("Get RBD Trash Details by pool name",
348 'pool_name': (str, 'Name of the pool'),
350 responses
={200: RBD_TRASH_SCHEMA
})
351 def list(self
, pool_name
=None):
352 """List all entries from trash."""
353 return self
._trash
_list
(pool_name
)
356 @handle_rados_error('pool')
357 @RbdTask('trash/purge', ['{pool_name}'], 2.0)
358 @RESTController.Collection('POST', query_params
=['pool_name'])
361 def purge(self
, pool_name
=None):
362 """Remove all expired images from trash."""
363 now
= "{}Z".format(datetime
.utcnow().isoformat())
364 pools
= self
._trash
_list
(pool_name
)
367 for image
in pool
['value']:
368 if image
['deferment_end_time'] < now
:
369 logger
.info('Removing trash image %s (pool=%s, namespace=%s, name=%s)',
370 image
['id'], pool
['pool_name'], image
['namespace'], image
['name'])
371 rbd_call(pool
['pool_name'], image
['namespace'],
372 self
.rbd_inst
.trash_remove
, image
['id'], 0)
374 @RbdTask('trash/restore', ['{image_id_spec}', '{new_image_name}'], 2.0)
375 @RESTController.Resource('POST')
378 def restore(self
, image_id_spec
, new_image_name
):
379 """Restore an image from trash."""
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
,
384 @RbdTask('trash/remove', ['{image_id_spec}'], 2.0)
385 def delete(self
, image_id_spec
, force
=False):
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.
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
)))
395 @APIRouter('/block/pool/{pool_name}/namespace', Scope
.RBD_IMAGE
)
396 @APIDoc("RBD Namespace Management API", "RbdNamespace")
397 class RbdNamespace(RESTController
):
401 self
.rbd_inst
= rbd
.RBD()
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',
411 return self
.rbd_inst
.namespace_create(ioctx
, namespace
)
413 def delete(self
, pool_name
, namespace
):
414 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
415 # pylint: disable=unbalanced-tuple-unpacking
416 images
, _
= RbdService
.rbd_pool_list([pool_name
], namespace
=namespace
)
418 raise DashboardException(
419 msg
='Namespace contains images which must be deleted first',
420 code
='namespace_contains_images',
422 return self
.rbd_inst
.namespace_remove(ioctx
, namespace
)
424 def list(self
, pool_name
):
425 with mgr
.rados
.open_ioctx(pool_name
) as ioctx
:
427 namespaces
= self
.rbd_inst
.namespace_list(ioctx
)
428 for namespace
in namespaces
:
429 # pylint: disable=unbalanced-tuple-unpacking
430 images
, _
= RbdService
.rbd_pool_list([pool_name
], namespace
=namespace
)
432 'namespace': namespace
,
433 'num_images': len(images
) if images
else 0