1 # -*- coding: utf-8 -*-
6 from functools
import partial
7 from typing
import NamedTuple
, Optional
, no_type_check
13 from ..controllers
.pool
import RBDPool
14 from ..controllers
.service
import Service
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
.orchestrator
import OrchClient
19 from ..services
.rbd
import rbd_call
20 from ..tools
import ViewCache
21 from . import APIDoc
, APIRouter
, BaseController
, CreatePermission
, Endpoint
, \
22 EndpointDoc
, ReadPermission
, RESTController
, Task
, UIRouter
, \
23 UpdatePermission
, allow_empty_body
25 logger
= logging
.getLogger('controllers.rbd_mirror')
28 # pylint: disable=not-callable
29 def handle_rbd_mirror_error():
30 def composed_decorator(func
):
31 func
= handle_rados_error('rbd-mirroring')(func
)
32 return handle_rbd_error()(func
)
33 return composed_decorator
36 # pylint: disable=not-callable
37 def RbdMirroringTask(name
, metadata
, wait_for
): # noqa: N802
38 def composed_decorator(func
):
39 func
= handle_rbd_mirror_error()(func
)
40 return Task("rbd/mirroring/{}".format(name
), metadata
, wait_for
,
41 partial(serialize_dashboard_exception
, include_http_status
=True))(func
)
42 return composed_decorator
47 for hostname
, server
in CephService
.get_service_map('rbd-mirror').items():
48 for service
in server
['services']:
49 id = service
['id'] # pylint: disable=W0622
50 metadata
= service
['metadata']
51 status
= service
['status'] or {}
54 status
= json
.loads(status
['json'])
55 except (ValueError, KeyError):
58 instance_id
= metadata
['instance_id']
60 # new version that supports per-cluster leader elections
63 # extract per-daemon service data and health
66 'instance_id': instance_id
,
67 'version': metadata
['ceph_version'],
68 'server_hostname': hostname
,
74 daemon
= dict(daemon
, **get_daemon_health(daemon
))
75 daemons
.append(daemon
)
77 return sorted(daemons
, key
=lambda k
: k
['instance_id'])
80 def get_daemon_health(daemon
):
82 'health_color': 'info',
85 for _
, pool_data
in daemon
['status'].items():
86 if (health
['health'] != 'error'
87 and [k
for k
, v
in pool_data
.get('callouts', {}).items()
88 if v
['level'] == 'error']):
90 'health_color': 'error',
93 elif (health
['health'] != 'error'
94 and [k
for k
, v
in pool_data
.get('callouts', {}).items()
95 if v
['level'] == 'warning']):
97 'health_color': 'warning',
100 elif health
['health_color'] == 'info':
102 'health_color': 'success',
108 def get_pools(daemons
): # pylint: disable=R0912, R0915
109 pool_names
= [pool
['pool_name'] for pool
in CephService
.get_pool_list('rbd')
110 if pool
.get('type', 1) == 1]
111 pool_stats
= _get_pool_stats(pool_names
)
112 _update_pool_stats(daemons
, pool_stats
)
116 def _update_pool_stats(daemons
, pool_stats
):
117 _update_pool_stats_with_daemons(daemons
, pool_stats
)
118 for _
, stats
in pool_stats
.items():
119 if stats
['mirror_mode'] == 'disabled':
121 if stats
.get('health', None) is None:
122 # daemon doesn't know about pool
123 stats
['health_color'] = 'error'
124 stats
['health'] = 'Error'
125 elif stats
.get('leader_id', None) is None:
126 # no daemons are managing the pool as leader instance
127 stats
['health_color'] = 'warning'
128 stats
['health'] = 'Warning'
131 def _update_pool_stats_with_daemons(daemons
, pool_stats
):
132 for daemon
in daemons
:
133 for _
, pool_data
in daemon
['status'].items():
134 stats
= pool_stats
.get(pool_data
['name'], None) # type: ignore
138 if pool_data
.get('leader', False):
139 # leader instance stores image counts
140 stats
['leader_id'] = daemon
['metadata']['instance_id']
141 stats
['image_local_count'] = pool_data
.get('image_local_count', 0)
142 stats
['image_remote_count'] = pool_data
.get('image_remote_count', 0)
144 if (stats
.get('health_color', '') != 'error'
145 and pool_data
.get('image_error_count', 0) > 0):
146 stats
['health_color'] = 'error'
147 stats
['health'] = 'Error'
148 elif (stats
.get('health_color', '') != 'error'
149 and pool_data
.get('image_warning_count', 0) > 0):
150 stats
['health_color'] = 'warning'
151 stats
['health'] = 'Warning'
152 elif stats
.get('health', None) is None:
153 stats
['health_color'] = 'success'
154 stats
['health'] = 'OK'
157 def _get_pool_stats(pool_names
):
160 for pool_name
in pool_names
:
161 logger
.debug("Constructing IOCtx %s", pool_name
)
163 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
165 logger
.exception("Failed to open pool %s", pool_name
)
169 mirror_mode
= rbdctx
.mirror_mode_get(ioctx
)
170 peer_uuids
= [x
['uuid'] for x
in rbdctx
.mirror_peer_list(ioctx
)]
171 except: # noqa pylint: disable=W0702
172 logger
.exception("Failed to query mirror settings %s", pool_name
)
177 if mirror_mode
== rbd
.RBD_MIRROR_MODE_DISABLED
:
178 mirror_mode
= "disabled"
179 stats
['health_color'] = "info"
180 stats
['health'] = "Disabled"
181 elif mirror_mode
== rbd
.RBD_MIRROR_MODE_IMAGE
:
182 mirror_mode
= "image"
183 elif mirror_mode
== rbd
.RBD_MIRROR_MODE_POOL
:
186 mirror_mode
= "unknown"
187 stats
['health_color'] = "warning"
188 stats
['health'] = "Warning"
190 pool_stats
[pool_name
] = dict(stats
, **{
191 'mirror_mode': mirror_mode
,
192 'peer_uuids': peer_uuids
198 def get_daemons_and_pools(): # pylint: disable=R0915
199 daemons
= get_daemons()
202 'pools': get_pools(daemons
)
206 class ReplayingData(NamedTuple
):
207 bytes_per_second
: Optional
[int] = None
208 seconds_until_synced
: Optional
[int] = None
209 syncing_percent
: Optional
[float] = None
210 entries_behind_primary
: Optional
[int] = None
215 def _get_pool_datum(pool_name
):
217 logger
.debug("Constructing IOCtx %s", pool_name
)
219 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
221 logger
.exception("Failed to open pool %s", pool_name
)
227 'state_color': 'warning',
231 rbd
.MIRROR_IMAGE_STATUS_STATE_UNKNOWN
: {
233 'state_color': 'warning',
236 rbd
.MIRROR_IMAGE_STATUS_STATE_ERROR
: {
238 'state_color': 'error',
241 rbd
.MIRROR_IMAGE_STATUS_STATE_SYNCING
: {
243 'state_color': 'success',
246 rbd
.MIRROR_IMAGE_STATUS_STATE_STARTING_REPLAY
: {
248 'state_color': 'success',
251 rbd
.MIRROR_IMAGE_STATUS_STATE_REPLAYING
: {
253 'state_color': 'success',
256 rbd
.MIRROR_IMAGE_STATUS_STATE_STOPPING_REPLAY
: {
258 'state_color': 'success',
261 rbd
.MIRROR_IMAGE_STATUS_STATE_STOPPED
: {
263 'state_color': 'info',
271 mirror_image_status
= rbdctx
.mirror_image_status_list(ioctx
)
272 data
['mirror_images'] = sorted([
274 'name': image
['name'],
275 'description': image
['description']
276 }, **mirror_state
['down' if not image
['up'] else image
['state']])
277 for image
in mirror_image_status
278 ], key
=lambda k
: k
['name'])
279 except rbd
.ImageNotFound
:
281 except: # noqa pylint: disable=W0702
282 logger
.exception("Failed to list mirror image status %s", pool_name
)
288 def _update_syncing_image_data(mirror_image
, image
):
289 if mirror_image
['state'] == 'Replaying':
290 p
= re
.compile("replaying, ({.*})")
291 replaying_data
= p
.findall(mirror_image
['description'])
292 assert len(replaying_data
) == 1
293 replaying_data
= json
.loads(replaying_data
[0])
294 if 'replay_state' in replaying_data
and replaying_data
['replay_state'] == 'idle':
296 'state_color': 'info',
299 for field
in ReplayingData
._fields
:
301 image
[field
] = replaying_data
[field
]
305 p
= re
.compile("bootstrapping, IMAGE_COPY/COPY_OBJECT (.*)%")
307 'progress': (p
.findall(mirror_image
['description']) or [0])[0]
312 def _get_content_data(): # pylint: disable=R0914
313 pool_names
= [pool
['pool_name'] for pool
in CephService
.get_pool_list('rbd')
314 if pool
.get('type', 1) == 1]
315 _
, data
= get_daemons_and_pools()
316 daemons
= data
.get('daemons', [])
317 pool_stats
= data
.get('pools', {})
323 for pool_name
in pool_names
:
324 _
, pool
= _get_pool_datum(pool_name
)
328 stats
= pool_stats
.get(pool_name
, {})
329 if stats
.get('mirror_mode', None) is None:
332 mirror_images
= pool
.get('mirror_images', [])
333 for mirror_image
in mirror_images
:
335 'pool_name': pool_name
,
336 'name': mirror_image
['name'],
337 'state_color': mirror_image
['state_color'],
338 'state': mirror_image
['state']
341 if mirror_image
['health'] == 'ok':
343 'description': mirror_image
['description']
345 image_ready
.append(image
)
346 elif mirror_image
['health'] == 'syncing':
347 _update_syncing_image_data(mirror_image
, image
)
348 image_syncing
.append(image
)
351 'description': mirror_image
['description']
353 image_error
.append(image
)
362 'image_error': image_error
,
363 'image_syncing': image_syncing
,
364 'image_ready': image_ready
368 def _reset_view_cache():
369 get_daemons_and_pools
.reset()
370 _get_pool_datum
.reset()
371 _get_content_data
.reset()
374 RBD_MIRROR_SCHEMA
= {
375 "site_name": (str, "Site Name")
379 "mirror_mode": (str, "Mirror Mode")
382 RBDM_SUMMARY_SCHEMA
= {
383 "site_name": (str, "site name"),
386 "daemons": ([str], ""),
388 "name": (str, "Pool name"),
389 "health_color": (str, ""),
390 "health": (str, "pool health"),
391 "mirror_mode": (str, "status"),
392 "peer_uuids": ([str], "")
394 "image_error": ([str], ""),
395 "image_syncing": ([str], ""),
396 "image_ready": ([str], "")
401 @APIRouter('/block/mirroring', Scope
.RBD_MIRRORING
)
402 @APIDoc("RBD Mirroring Management API", "RbdMirroring")
403 class RbdMirroring(BaseController
):
405 @Endpoint(method
='GET', path
='site_name')
406 @handle_rbd_mirror_error()
408 @EndpointDoc("Display Rbd Mirroring sitename",
409 responses
={200: RBD_MIRROR_SCHEMA
})
411 return self
._get
_site
_name
()
413 @Endpoint(method
='PUT', path
='site_name')
414 @handle_rbd_mirror_error()
416 def set(self
, site_name
):
417 rbd
.RBD().mirror_site_name_set(mgr
.rados
, site_name
)
418 return self
._get
_site
_name
()
420 def _get_site_name(self
):
421 return {'site_name': rbd
.RBD().mirror_site_name_get(mgr
.rados
)}
424 @APIRouter('/block/mirroring/summary', Scope
.RBD_MIRRORING
)
425 @APIDoc("RBD Mirroring Summary Management API", "RbdMirroringSummary")
426 class RbdMirroringSummary(BaseController
):
429 @handle_rbd_mirror_error()
431 @EndpointDoc("Display Rbd Mirroring Summary",
432 responses
={200: RBDM_SUMMARY_SCHEMA
})
434 site_name
= rbd
.RBD().mirror_site_name_get(mgr
.rados
)
436 status
, content_data
= _get_content_data()
437 return {'site_name': site_name
,
439 'content_data': content_data
}
442 @APIRouter('/block/mirroring/pool', Scope
.RBD_MIRRORING
)
443 @APIDoc("RBD Mirroring Pool Mode Management API", "RbdMirroringPoolMode")
444 class RbdMirroringPoolMode(RESTController
):
446 RESOURCE_ID
= "pool_name"
448 rbd
.RBD_MIRROR_MODE_DISABLED
: 'disabled',
449 rbd
.RBD_MIRROR_MODE_IMAGE
: 'image',
450 rbd
.RBD_MIRROR_MODE_POOL
: 'pool'
453 @handle_rbd_mirror_error()
454 @EndpointDoc("Display Rbd Mirroring Summary",
456 'pool_name': (str, 'Pool Name'),
458 responses
={200: RBDM_POOL_SCHEMA
})
459 def get(self
, pool_name
):
460 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
461 mode
= rbd
.RBD().mirror_mode_get(ioctx
)
463 'mirror_mode': self
.MIRROR_MODES
.get(mode
, 'unknown')
467 @RbdMirroringTask('pool/edit', {'pool_name': '{pool_name}'}, 5.0)
468 def set(self
, pool_name
, mirror_mode
=None):
469 def _edit(ioctx
, mirror_mode
=None):
471 mode_enum
= {x
[1]: x
[0] for x
in
472 self
.MIRROR_MODES
.items()}.get(mirror_mode
, None)
473 if mode_enum
is None:
474 raise rbd
.Error('invalid mirror mode "{}"'.format(mirror_mode
))
476 current_mode_enum
= rbd
.RBD().mirror_mode_get(ioctx
)
477 if mode_enum
!= current_mode_enum
:
478 rbd
.RBD().mirror_mode_set(ioctx
, mode_enum
)
481 return rbd_call(pool_name
, None, _edit
, mirror_mode
)
484 @APIRouter('/block/mirroring/pool/{pool_name}/bootstrap', Scope
.RBD_MIRRORING
)
485 @APIDoc("RBD Mirroring Pool Bootstrap Management API", "RbdMirroringPoolBootstrap")
486 class RbdMirroringPoolBootstrap(BaseController
):
488 @Endpoint(method
='POST', path
='token')
489 @handle_rbd_mirror_error()
492 def create_token(self
, pool_name
):
493 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
494 token
= rbd
.RBD().mirror_peer_bootstrap_create(ioctx
)
495 return {'token': token
}
497 @Endpoint(method
='POST', path
='peer')
498 @handle_rbd_mirror_error()
501 def import_token(self
, pool_name
, direction
, token
):
502 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
505 'rx': rbd
.RBD_MIRROR_PEER_DIRECTION_RX
,
506 'rx-tx': rbd
.RBD_MIRROR_PEER_DIRECTION_RX_TX
509 direction_enum
= directions
.get(direction
)
510 if direction_enum
is None:
511 raise rbd
.Error('invalid direction "{}"'.format(direction
))
513 rbd
.RBD().mirror_peer_bootstrap_import(ioctx
, direction_enum
, token
)
517 @APIRouter('/block/mirroring/pool/{pool_name}/peer', Scope
.RBD_MIRRORING
)
518 @APIDoc("RBD Mirroring Pool Peer Management API", "RbdMirroringPoolPeer")
519 class RbdMirroringPoolPeer(RESTController
):
521 RESOURCE_ID
= "peer_uuid"
523 @handle_rbd_mirror_error()
524 def list(self
, pool_name
):
525 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
526 peer_list
= rbd
.RBD().mirror_peer_list(ioctx
)
527 return [x
['uuid'] for x
in peer_list
]
529 @handle_rbd_mirror_error()
530 def create(self
, pool_name
, cluster_name
, client_id
, mon_host
=None,
532 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
533 mode
= rbd
.RBD().mirror_mode_get(ioctx
)
534 if mode
== rbd
.RBD_MIRROR_MODE_DISABLED
:
535 raise rbd
.Error('mirroring must be enabled')
537 uuid
= rbd
.RBD().mirror_peer_add(ioctx
, cluster_name
,
538 'client.{}'.format(client_id
))
541 if mon_host
is not None:
542 attributes
[rbd
.RBD_MIRROR_PEER_ATTRIBUTE_NAME_MON_HOST
] = mon_host
544 attributes
[rbd
.RBD_MIRROR_PEER_ATTRIBUTE_NAME_KEY
] = key
546 rbd
.RBD().mirror_peer_set_attributes(ioctx
, uuid
, attributes
)
549 return {'uuid': uuid
}
551 @handle_rbd_mirror_error()
552 def get(self
, pool_name
, peer_uuid
):
553 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
554 peer_list
= rbd
.RBD().mirror_peer_list(ioctx
)
555 peer
= next((x
for x
in peer_list
if x
['uuid'] == peer_uuid
), None)
557 raise cherrypy
.HTTPError(404)
559 # convert full client name to just the client id
560 peer
['client_id'] = peer
['client_name'].split('.', 1)[-1]
561 del peer
['client_name']
563 # convert direction enum to string
565 rbd
.RBD_MIRROR_PEER_DIRECTION_RX
: 'rx',
566 rbd
.RBD_MIRROR_PEER_DIRECTION_TX
: 'tx',
567 rbd
.RBD_MIRROR_PEER_DIRECTION_RX_TX
: 'rx-tx'
569 peer
['direction'] = directions
[peer
.get('direction', rbd
.RBD_MIRROR_PEER_DIRECTION_RX
)]
572 attributes
= rbd
.RBD().mirror_peer_get_attributes(ioctx
, peer_uuid
)
573 except rbd
.ImageNotFound
:
576 peer
['mon_host'] = attributes
.get(rbd
.RBD_MIRROR_PEER_ATTRIBUTE_NAME_MON_HOST
, '')
577 peer
['key'] = attributes
.get(rbd
.RBD_MIRROR_PEER_ATTRIBUTE_NAME_KEY
, '')
580 @handle_rbd_mirror_error()
581 def delete(self
, pool_name
, peer_uuid
):
582 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
583 rbd
.RBD().mirror_peer_remove(ioctx
, peer_uuid
)
586 @handle_rbd_mirror_error()
587 def set(self
, pool_name
, peer_uuid
, cluster_name
=None, client_id
=None,
588 mon_host
=None, key
=None):
589 ioctx
= mgr
.rados
.open_ioctx(pool_name
)
591 rbd
.RBD().mirror_peer_set_cluster(ioctx
, peer_uuid
, cluster_name
)
593 rbd
.RBD().mirror_peer_set_client(ioctx
, peer_uuid
,
594 'client.{}'.format(client_id
))
596 if mon_host
is not None or key
is not None:
598 attributes
= rbd
.RBD().mirror_peer_get_attributes(ioctx
, peer_uuid
)
599 except rbd
.ImageNotFound
:
602 if mon_host
is not None:
603 attributes
[rbd
.RBD_MIRROR_PEER_ATTRIBUTE_NAME_MON_HOST
] = mon_host
605 attributes
[rbd
.RBD_MIRROR_PEER_ATTRIBUTE_NAME_KEY
] = key
606 rbd
.RBD().mirror_peer_set_attributes(ioctx
, peer_uuid
, attributes
)
611 @UIRouter('/block/mirroring', Scope
.RBD_MIRRORING
)
612 class RbdMirroringStatus(BaseController
):
613 @EndpointDoc('Display RBD Mirroring Status')
617 status
= {'available': True, 'message': None}
618 orch_status
= OrchClient
.instance().status()
620 # if the orch is not available we can't create the service
622 if not orch_status
['available']:
624 if not CephService
.get_service_list('rbd-mirror') or not CephService
.get_pool_list('rbd'):
625 status
['available'] = False
626 status
['message'] = 'RBD mirroring is not configured' # type: ignore
630 @EndpointDoc('Configure RBD Mirroring')
637 'service_type': 'rbd-mirror',
642 if not CephService
.get_service_list('rbd-mirror'):
643 service
.create(service_spec
, 'rbd-mirror')
645 if not CephService
.get_pool_list('rbd'):