1 # -*- coding: utf-8 -*-
2 from __future__
import absolute_import
8 from functools
import partial
14 from . import ApiController
, Endpoint
, Task
, BaseController
, ReadPermission
, \
15 UpdatePermission
, RESTController
18 from ..security
import Scope
19 from ..services
.ceph_service
import CephService
20 from ..services
.rbd
import rbd_call
21 from ..tools
import ViewCache
22 from ..services
.exception
import handle_rados_error
, handle_rbd_error
, \
23 serialize_dashboard_exception
26 from typing
import no_type_check
28 no_type_check
= object() # Just for type checking
31 logger
= logging
.getLogger('controllers.rbd_mirror')
34 # pylint: disable=not-callable
35 def handle_rbd_mirror_error():
36 def composed_decorator(func
):
37 func
= handle_rados_error('rbd-mirroring')(func
)
38 return handle_rbd_error()(func
)
39 return composed_decorator
42 # pylint: disable=not-callable
43 def RbdMirroringTask(name
, metadata
, wait_for
): # noqa: N802
44 def composed_decorator(func
):
45 func
= handle_rbd_mirror_error()(func
)
46 return Task("rbd/mirroring/{}".format(name
), metadata
, wait_for
,
47 partial(serialize_dashboard_exception
, include_http_status
=True))(func
)
48 return composed_decorator
52 def get_daemons_and_pools(): # pylint: disable=R0915
55 for hostname
, server
in CephService
.get_service_map('rbd-mirror').items():
56 for service
in server
['services']:
57 id = service
['id'] # pylint: disable=W0622
58 metadata
= service
['metadata']
59 status
= service
['status'] or {}
62 status
= json
.loads(status
['json'])
63 except (ValueError, KeyError):
66 instance_id
= metadata
['instance_id']
68 # new version that supports per-cluster leader elections
71 # extract per-daemon service data and health
74 'instance_id': instance_id
,
75 'version': metadata
['ceph_version'],
76 'server_hostname': hostname
,
82 daemon
= dict(daemon
, **get_daemon_health(daemon
))
83 daemons
.append(daemon
)
85 return sorted(daemons
, key
=lambda k
: k
['instance_id'])
87 def get_daemon_health(daemon
):
89 'health_color': 'info',
92 for _
, pool_data
in daemon
['status'].items():
93 if (health
['health'] != 'error'
94 and [k
for k
, v
in pool_data
.get('callouts', {}).items()
95 if v
['level'] == 'error']):
97 'health_color': 'error',
100 elif (health
['health'] != 'error'
101 and [k
for k
, v
in pool_data
.get('callouts', {}).items()
102 if v
['level'] == 'warning']):
104 'health_color': 'warning',
107 elif health
['health_color'] == 'info':
109 'health_color': 'success',
114 def get_pools(daemons
): # pylint: disable=R0912, R0915
115 pool_names
= [pool
['pool_name'] for pool
in CephService
.get_pool_list('rbd')
116 if pool
.get('type', 1) == 1]
119 for pool_name
in pool_names
:
120 logger
.debug("Constructing IOCtx %s", pool_name
)
122 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
124 logger
.exception("Failed to open pool %s", pool_name
)
128 mirror_mode
= rbdctx
.mirror_mode_get(ioctx
)
129 peer_uuids
= [x
['uuid'] for x
in rbdctx
.mirror_peer_list(ioctx
)]
130 except: # noqa pylint: disable=W0702
131 logger
.exception("Failed to query mirror settings %s", pool_name
)
136 if mirror_mode
== rbd
.RBD_MIRROR_MODE_DISABLED
:
137 mirror_mode
= "disabled"
138 stats
['health_color'] = "info"
139 stats
['health'] = "Disabled"
140 elif mirror_mode
== rbd
.RBD_MIRROR_MODE_IMAGE
:
141 mirror_mode
= "image"
142 elif mirror_mode
== rbd
.RBD_MIRROR_MODE_POOL
:
145 mirror_mode
= "unknown"
146 stats
['health_color'] = "warning"
147 stats
['health'] = "Warning"
149 pool_stats
[pool_name
] = dict(stats
, **{
150 'mirror_mode': mirror_mode
,
151 'peer_uuids': peer_uuids
154 for daemon
in daemons
:
155 for _
, pool_data
in daemon
['status'].items():
156 stats
= pool_stats
.get(pool_data
['name'], None) # type: ignore
160 if pool_data
.get('leader', False):
161 # leader instance stores image counts
162 stats
['leader_id'] = daemon
['metadata']['instance_id']
163 stats
['image_local_count'] = pool_data
.get('image_local_count', 0)
164 stats
['image_remote_count'] = pool_data
.get('image_remote_count', 0)
166 if (stats
.get('health_color', '') != 'error'
167 and pool_data
.get('image_error_count', 0) > 0):
168 stats
['health_color'] = 'error'
169 stats
['health'] = 'Error'
170 elif (stats
.get('health_color', '') != 'error'
171 and pool_data
.get('image_warning_count', 0) > 0):
172 stats
['health_color'] = 'warning'
173 stats
['health'] = 'Warning'
174 elif stats
.get('health', None) is None:
175 stats
['health_color'] = 'success'
176 stats
['health'] = 'OK'
178 for _
, stats
in pool_stats
.items():
179 if stats
['mirror_mode'] == 'disabled':
181 if stats
.get('health', None) is None:
182 # daemon doesn't know about pool
183 stats
['health_color'] = 'error'
184 stats
['health'] = 'Error'
185 elif stats
.get('leader_id', None) is None:
186 # no daemons are managing the pool as leader instance
187 stats
['health_color'] = 'warning'
188 stats
['health'] = 'Warning'
191 daemons
= get_daemons()
194 'pools': get_pools(daemons
)
200 def _get_pool_datum(pool_name
):
202 logger
.debug("Constructing IOCtx %s", pool_name
)
204 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
206 logger
.exception("Failed to open pool %s", pool_name
)
212 'state_color': 'warning',
216 rbd
.MIRROR_IMAGE_STATUS_STATE_UNKNOWN
: {
218 'state_color': 'warning',
221 rbd
.MIRROR_IMAGE_STATUS_STATE_ERROR
: {
223 'state_color': 'error',
226 rbd
.MIRROR_IMAGE_STATUS_STATE_SYNCING
: {
229 rbd
.MIRROR_IMAGE_STATUS_STATE_STARTING_REPLAY
: {
231 'state_color': 'success',
234 rbd
.MIRROR_IMAGE_STATUS_STATE_REPLAYING
: {
236 'state_color': 'success',
239 rbd
.MIRROR_IMAGE_STATUS_STATE_STOPPING_REPLAY
: {
241 'state_color': 'success',
244 rbd
.MIRROR_IMAGE_STATUS_STATE_STOPPED
: {
246 'state_color': 'info',
253 mirror_image_status
= rbdctx
.mirror_image_status_list(ioctx
)
254 data
['mirror_images'] = sorted([
256 'name': image
['name'],
257 'description': image
['description']
258 }, **mirror_state
['down' if not image
['up'] else image
['state']])
259 for image
in mirror_image_status
260 ], key
=lambda k
: k
['name'])
261 except rbd
.ImageNotFound
:
263 except: # noqa pylint: disable=W0702
264 logger
.exception("Failed to list mirror image status %s", pool_name
)
271 def _get_content_data(): # pylint: disable=R0914
272 pool_names
= [pool
['pool_name'] for pool
in CephService
.get_pool_list('rbd')
273 if pool
.get('type', 1) == 1]
274 _
, data
= get_daemons_and_pools()
275 daemons
= data
.get('daemons', [])
276 pool_stats
= data
.get('pools', {})
282 for pool_name
in pool_names
:
283 _
, pool
= _get_pool_datum(pool_name
)
287 stats
= pool_stats
.get(pool_name
, {})
288 if stats
.get('mirror_mode', None) is None:
291 mirror_images
= pool
.get('mirror_images', [])
292 for mirror_image
in mirror_images
:
294 'pool_name': pool_name
,
295 'name': mirror_image
['name']
298 if mirror_image
['health'] == 'ok':
300 'state_color': mirror_image
['state_color'],
301 'state': mirror_image
['state'],
302 'description': mirror_image
['description']
304 image_ready
.append(image
)
305 elif mirror_image
['health'] == 'syncing':
306 p
= re
.compile("bootstrapping, IMAGE_COPY/COPY_OBJECT (.*)%")
308 'progress': (p
.findall(mirror_image
['description']) or [0])[0]
310 image_syncing
.append(image
)
313 'state_color': mirror_image
['state_color'],
314 'state': mirror_image
['state'],
315 'description': mirror_image
['description']
317 image_error
.append(image
)
326 'image_error': image_error
,
327 'image_syncing': image_syncing
,
328 'image_ready': image_ready
332 def _reset_view_cache():
333 get_daemons_and_pools
.reset()
334 _get_pool_datum
.reset()
335 _get_content_data
.reset()
338 @ApiController('/block/mirroring', Scope
.RBD_MIRRORING
)
339 class RbdMirroring(BaseController
):
341 @Endpoint(method
='GET', path
='site_name')
342 @handle_rbd_mirror_error()
345 return self
._get
_site
_name
()
347 @Endpoint(method
='PUT', path
='site_name')
348 @handle_rbd_mirror_error()
350 def set(self
, site_name
):
351 rbd
.RBD().mirror_site_name_set(mgr
.rados
, site_name
)
352 return self
._get
_site
_name
()
354 def _get_site_name(self
):
355 return {'site_name': rbd
.RBD().mirror_site_name_get(mgr
.rados
)}
358 @ApiController('/block/mirroring/summary', Scope
.RBD_MIRRORING
)
359 class RbdMirroringSummary(BaseController
):
362 @handle_rbd_mirror_error()
365 site_name
= rbd
.RBD().mirror_site_name_get(mgr
.rados
)
367 status
, content_data
= _get_content_data()
368 return {'site_name': site_name
,
370 'content_data': content_data
}
373 @ApiController('/block/mirroring/pool', Scope
.RBD_MIRRORING
)
374 class RbdMirroringPoolMode(RESTController
):
376 RESOURCE_ID
= "pool_name"
378 rbd
.RBD_MIRROR_MODE_DISABLED
: 'disabled',
379 rbd
.RBD_MIRROR_MODE_IMAGE
: 'image',
380 rbd
.RBD_MIRROR_MODE_POOL
: 'pool'
383 @handle_rbd_mirror_error()
384 def get(self
, pool_name
):
385 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
386 mode
= rbd
.RBD().mirror_mode_get(ioctx
)
388 'mirror_mode': self
.MIRROR_MODES
.get(mode
, 'unknown')
392 @RbdMirroringTask('pool/edit', {'pool_name': '{pool_name}'}, 5.0)
393 def set(self
, pool_name
, mirror_mode
=None):
394 def _edit(ioctx
, mirror_mode
=None):
396 mode_enum
= {x
[1]: x
[0] for x
in
397 self
.MIRROR_MODES
.items()}.get(mirror_mode
, None)
398 if mode_enum
is None:
399 raise rbd
.Error('invalid mirror mode "{}"'.format(mirror_mode
))
401 current_mode_enum
= rbd
.RBD().mirror_mode_get(ioctx
)
402 if mode_enum
!= current_mode_enum
:
403 rbd
.RBD().mirror_mode_set(ioctx
, mode_enum
)
406 return rbd_call(pool_name
, None, _edit
, mirror_mode
)
409 @ApiController('/block/mirroring/pool/{pool_name}/bootstrap',
411 class RbdMirroringPoolBootstrap(BaseController
):
413 @Endpoint(method
='POST', path
='token')
414 @handle_rbd_mirror_error()
416 def create_token(self
, pool_name
):
417 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
418 token
= rbd
.RBD().mirror_peer_bootstrap_create(ioctx
)
419 return {'token': token
}
421 @Endpoint(method
='POST', path
='peer')
422 @handle_rbd_mirror_error()
424 def import_token(self
, pool_name
, direction
, token
):
425 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
428 'rx': rbd
.RBD_MIRROR_PEER_DIRECTION_RX
,
429 'rx-tx': rbd
.RBD_MIRROR_PEER_DIRECTION_RX_TX
432 direction_enum
= directions
.get(direction
)
433 if direction_enum
is None:
434 raise rbd
.Error('invalid direction "{}"'.format(direction
))
436 rbd
.RBD().mirror_peer_bootstrap_import(ioctx
, direction_enum
, token
)
440 @ApiController('/block/mirroring/pool/{pool_name}/peer', Scope
.RBD_MIRRORING
)
441 class RbdMirroringPoolPeer(RESTController
):
443 RESOURCE_ID
= "peer_uuid"
445 @handle_rbd_mirror_error()
446 def list(self
, pool_name
):
447 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
448 peer_list
= rbd
.RBD().mirror_peer_list(ioctx
)
449 return [x
['uuid'] for x
in peer_list
]
451 @handle_rbd_mirror_error()
452 def create(self
, pool_name
, cluster_name
, client_id
, mon_host
=None,
454 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
455 mode
= rbd
.RBD().mirror_mode_get(ioctx
)
456 if mode
== rbd
.RBD_MIRROR_MODE_DISABLED
:
457 raise rbd
.Error('mirroring must be enabled')
459 uuid
= rbd
.RBD().mirror_peer_add(ioctx
, cluster_name
,
460 'client.{}'.format(client_id
))
463 if mon_host
is not None:
464 attributes
[rbd
.RBD_MIRROR_PEER_ATTRIBUTE_NAME_MON_HOST
] = mon_host
466 attributes
[rbd
.RBD_MIRROR_PEER_ATTRIBUTE_NAME_KEY
] = key
468 rbd
.RBD().mirror_peer_set_attributes(ioctx
, uuid
, attributes
)
471 return {'uuid': uuid
}
473 @handle_rbd_mirror_error()
474 def get(self
, pool_name
, peer_uuid
):
475 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
476 peer_list
= rbd
.RBD().mirror_peer_list(ioctx
)
477 peer
= next((x
for x
in peer_list
if x
['uuid'] == peer_uuid
), None)
479 raise cherrypy
.HTTPError(404)
481 # convert full client name to just the client id
482 peer
['client_id'] = peer
['client_name'].split('.', 1)[-1]
483 del peer
['client_name']
485 # convert direction enum to string
487 rbd
.RBD_MIRROR_PEER_DIRECTION_RX
: 'rx',
488 rbd
.RBD_MIRROR_PEER_DIRECTION_TX
: 'tx',
489 rbd
.RBD_MIRROR_PEER_DIRECTION_RX_TX
: 'rx-tx'
491 peer
['direction'] = directions
[peer
.get('direction', rbd
.RBD_MIRROR_PEER_DIRECTION_RX
)]
494 attributes
= rbd
.RBD().mirror_peer_get_attributes(ioctx
, peer_uuid
)
495 except rbd
.ImageNotFound
:
498 peer
['mon_host'] = attributes
.get(rbd
.RBD_MIRROR_PEER_ATTRIBUTE_NAME_MON_HOST
, '')
499 peer
['key'] = attributes
.get(rbd
.RBD_MIRROR_PEER_ATTRIBUTE_NAME_KEY
, '')
502 @handle_rbd_mirror_error()
503 def delete(self
, pool_name
, peer_uuid
):
504 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
505 rbd
.RBD().mirror_peer_remove(ioctx
, peer_uuid
)
508 @handle_rbd_mirror_error()
509 def set(self
, pool_name
, peer_uuid
, cluster_name
=None, client_id
=None,
510 mon_host
=None, key
=None):
511 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
513 rbd
.RBD().mirror_peer_set_cluster(ioctx
, peer_uuid
, cluster_name
)
515 rbd
.RBD().mirror_peer_set_client(ioctx
, peer_uuid
,
516 'client.{}'.format(client_id
))
518 if mon_host
is not None or key
is not None:
520 attributes
= rbd
.RBD().mirror_peer_get_attributes(ioctx
, peer_uuid
)
521 except rbd
.ImageNotFound
:
524 if mon_host
is not None:
525 attributes
[rbd
.RBD_MIRROR_PEER_ATTRIBUTE_NAME_MON_HOST
] = mon_host
527 attributes
[rbd
.RBD_MIRROR_PEER_ATTRIBUTE_NAME_KEY
] = key
528 rbd
.RBD().mirror_peer_set_attributes(ioctx
, peer_uuid
, attributes
)