]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
17ef0b88b2a362e6fa9b94dd4cedb100fcfcbc31
[ceph.git] / ceph / src / pybind / mgr / dashboard / controllers / rbd_mirroring.py
1 # -*- coding: utf-8 -*-
2
3 import json
4 import logging
5 import re
6 from functools import partial
7 from typing import NamedTuple, Optional, no_type_check
8
9 import cherrypy
10 import rbd
11
12 from .. import mgr
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
24
25 logger = logging.getLogger('controllers.rbd_mirror')
26
27
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
34
35
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
43
44
45 def get_daemons():
46 daemons = []
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 {}
52
53 try:
54 status = json.loads(status['json'])
55 except (ValueError, KeyError):
56 status = {}
57
58 instance_id = metadata['instance_id']
59 if id == instance_id:
60 # new version that supports per-cluster leader elections
61 id = metadata['id']
62
63 # extract per-daemon service data and health
64 daemon = {
65 'id': id,
66 'instance_id': instance_id,
67 'version': metadata['ceph_version'],
68 'server_hostname': hostname,
69 'service': service,
70 'server': server,
71 'metadata': metadata,
72 'status': status
73 }
74 daemon = dict(daemon, **get_daemon_health(daemon))
75 daemons.append(daemon)
76
77 return sorted(daemons, key=lambda k: k['instance_id'])
78
79
80 def get_daemon_health(daemon):
81 health = {
82 'health_color': 'info',
83 'health': 'Unknown'
84 }
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']):
89 health = {
90 'health_color': 'error',
91 'health': 'Error'
92 }
93 elif (health['health'] != 'error'
94 and [k for k, v in pool_data.get('callouts', {}).items()
95 if v['level'] == 'warning']):
96 health = {
97 'health_color': 'warning',
98 'health': 'Warning'
99 }
100 elif health['health_color'] == 'info':
101 health = {
102 'health_color': 'success',
103 'health': 'OK'
104 }
105 return health
106
107
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)
113 return pool_stats
114
115
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':
120 continue
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'
129
130
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
135 if stats is None:
136 continue
137
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)
143
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'
155
156
157 def _get_pool_stats(pool_names):
158 pool_stats = {}
159 rbdctx = rbd.RBD()
160 for pool_name in pool_names:
161 logger.debug("Constructing IOCtx %s", pool_name)
162 try:
163 ioctx = mgr.rados.open_ioctx(pool_name)
164 except TypeError:
165 logger.exception("Failed to open pool %s", pool_name)
166 continue
167
168 try:
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)
173 mirror_mode = None
174 peer_uuids = []
175
176 stats = {}
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:
184 mirror_mode = "pool"
185 else:
186 mirror_mode = "unknown"
187 stats['health_color'] = "warning"
188 stats['health'] = "Warning"
189
190 pool_stats[pool_name] = dict(stats, **{
191 'mirror_mode': mirror_mode,
192 'peer_uuids': peer_uuids
193 })
194 return pool_stats
195
196
197 @ViewCache()
198 def get_daemons_and_pools(): # pylint: disable=R0915
199 daemons = get_daemons()
200 return {
201 'daemons': daemons,
202 'pools': get_pools(daemons)
203 }
204
205
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
211
212
213 @ViewCache()
214 @no_type_check
215 def _get_pool_datum(pool_name):
216 data = {}
217 logger.debug("Constructing IOCtx %s", pool_name)
218 try:
219 ioctx = mgr.rados.open_ioctx(pool_name)
220 except TypeError:
221 logger.exception("Failed to open pool %s", pool_name)
222 return None
223
224 mirror_state = {
225 'down': {
226 'health': 'issue',
227 'state_color': 'warning',
228 'state': 'Unknown',
229 'description': None
230 },
231 rbd.MIRROR_IMAGE_STATUS_STATE_UNKNOWN: {
232 'health': 'issue',
233 'state_color': 'warning',
234 'state': 'Unknown'
235 },
236 rbd.MIRROR_IMAGE_STATUS_STATE_ERROR: {
237 'health': 'issue',
238 'state_color': 'error',
239 'state': 'Error'
240 },
241 rbd.MIRROR_IMAGE_STATUS_STATE_SYNCING: {
242 'health': 'syncing',
243 'state_color': 'success',
244 'state': 'Syncing'
245 },
246 rbd.MIRROR_IMAGE_STATUS_STATE_STARTING_REPLAY: {
247 'health': 'syncing',
248 'state_color': 'success',
249 'state': 'Starting'
250 },
251 rbd.MIRROR_IMAGE_STATUS_STATE_REPLAYING: {
252 'health': 'syncing',
253 'state_color': 'success',
254 'state': 'Replaying'
255 },
256 rbd.MIRROR_IMAGE_STATUS_STATE_STOPPING_REPLAY: {
257 'health': 'ok',
258 'state_color': 'success',
259 'state': 'Stopping'
260 },
261 rbd.MIRROR_IMAGE_STATUS_STATE_STOPPED: {
262 'health': 'ok',
263 'state_color': 'info',
264 'state': 'Stopped'
265 }
266
267 }
268
269 rbdctx = rbd.RBD()
270 try:
271 mirror_image_status = rbdctx.mirror_image_status_list(ioctx)
272 data['mirror_images'] = sorted([
273 dict({
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:
280 pass
281 except: # noqa pylint: disable=W0702
282 logger.exception("Failed to list mirror image status %s", pool_name)
283 raise
284
285 return data
286
287
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':
295 image.update({
296 'state_color': 'info',
297 'state': 'Idle'
298 })
299 for field in ReplayingData._fields:
300 try:
301 image[field] = replaying_data[field]
302 except KeyError:
303 pass
304 else:
305 p = re.compile("bootstrapping, IMAGE_COPY/COPY_OBJECT (.*)%")
306 image.update({
307 'progress': (p.findall(mirror_image['description']) or [0])[0]
308 })
309
310
311 @ViewCache()
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', {})
318
319 pools = []
320 image_error = []
321 image_syncing = []
322 image_ready = []
323 for pool_name in pool_names:
324 _, pool = _get_pool_datum(pool_name)
325 if not pool:
326 pool = {}
327
328 stats = pool_stats.get(pool_name, {})
329 if stats.get('mirror_mode', None) is None:
330 continue
331
332 mirror_images = pool.get('mirror_images', [])
333 for mirror_image in mirror_images:
334 image = {
335 'pool_name': pool_name,
336 'name': mirror_image['name'],
337 'state_color': mirror_image['state_color'],
338 'state': mirror_image['state']
339 }
340
341 if mirror_image['health'] == 'ok':
342 image.update({
343 'description': mirror_image['description']
344 })
345 image_ready.append(image)
346 elif mirror_image['health'] == 'syncing':
347 _update_syncing_image_data(mirror_image, image)
348 image_syncing.append(image)
349 else:
350 image.update({
351 'description': mirror_image['description']
352 })
353 image_error.append(image)
354
355 pools.append(dict({
356 'name': pool_name
357 }, **stats))
358
359 return {
360 'daemons': daemons,
361 'pools': pools,
362 'image_error': image_error,
363 'image_syncing': image_syncing,
364 'image_ready': image_ready
365 }
366
367
368 def _reset_view_cache():
369 get_daemons_and_pools.reset()
370 _get_pool_datum.reset()
371 _get_content_data.reset()
372
373
374 RBD_MIRROR_SCHEMA = {
375 "site_name": (str, "Site Name")
376 }
377
378 RBDM_POOL_SCHEMA = {
379 "mirror_mode": (str, "Mirror Mode")
380 }
381
382 RBDM_SUMMARY_SCHEMA = {
383 "site_name": (str, "site name"),
384 "status": (int, ""),
385 "content_data": ({
386 "daemons": ([str], ""),
387 "pools": ([{
388 "name": (str, "Pool name"),
389 "health_color": (str, ""),
390 "health": (str, "pool health"),
391 "mirror_mode": (str, "status"),
392 "peer_uuids": ([str], "")
393 }], "Pools"),
394 "image_error": ([str], ""),
395 "image_syncing": ([str], ""),
396 "image_ready": ([str], "")
397 }, "")
398 }
399
400
401 @APIRouter('/block/mirroring', Scope.RBD_MIRRORING)
402 @APIDoc("RBD Mirroring Management API", "RbdMirroring")
403 class RbdMirroring(BaseController):
404
405 @Endpoint(method='GET', path='site_name')
406 @handle_rbd_mirror_error()
407 @ReadPermission
408 @EndpointDoc("Display Rbd Mirroring sitename",
409 responses={200: RBD_MIRROR_SCHEMA})
410 def get(self):
411 return self._get_site_name()
412
413 @Endpoint(method='PUT', path='site_name')
414 @handle_rbd_mirror_error()
415 @UpdatePermission
416 def set(self, site_name):
417 rbd.RBD().mirror_site_name_set(mgr.rados, site_name)
418 return self._get_site_name()
419
420 def _get_site_name(self):
421 return {'site_name': rbd.RBD().mirror_site_name_get(mgr.rados)}
422
423
424 @APIRouter('/block/mirroring/summary', Scope.RBD_MIRRORING)
425 @APIDoc("RBD Mirroring Summary Management API", "RbdMirroringSummary")
426 class RbdMirroringSummary(BaseController):
427
428 @Endpoint()
429 @handle_rbd_mirror_error()
430 @ReadPermission
431 @EndpointDoc("Display Rbd Mirroring Summary",
432 responses={200: RBDM_SUMMARY_SCHEMA})
433 def __call__(self):
434 site_name = rbd.RBD().mirror_site_name_get(mgr.rados)
435
436 status, content_data = _get_content_data()
437 return {'site_name': site_name,
438 'status': status,
439 'content_data': content_data}
440
441
442 @APIRouter('/block/mirroring/pool', Scope.RBD_MIRRORING)
443 @APIDoc("RBD Mirroring Pool Mode Management API", "RbdMirroringPoolMode")
444 class RbdMirroringPoolMode(RESTController):
445
446 RESOURCE_ID = "pool_name"
447 MIRROR_MODES = {
448 rbd.RBD_MIRROR_MODE_DISABLED: 'disabled',
449 rbd.RBD_MIRROR_MODE_IMAGE: 'image',
450 rbd.RBD_MIRROR_MODE_POOL: 'pool'
451 }
452
453 @handle_rbd_mirror_error()
454 @EndpointDoc("Display Rbd Mirroring Summary",
455 parameters={
456 'pool_name': (str, 'Pool Name'),
457 },
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)
462 data = {
463 'mirror_mode': self.MIRROR_MODES.get(mode, 'unknown')
464 }
465 return data
466
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):
470 if mirror_mode:
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))
475
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)
479 _reset_view_cache()
480
481 return rbd_call(pool_name, None, _edit, mirror_mode)
482
483
484 @APIRouter('/block/mirroring/pool/{pool_name}/bootstrap', Scope.RBD_MIRRORING)
485 @APIDoc("RBD Mirroring Pool Bootstrap Management API", "RbdMirroringPoolBootstrap")
486 class RbdMirroringPoolBootstrap(BaseController):
487
488 @Endpoint(method='POST', path='token')
489 @handle_rbd_mirror_error()
490 @UpdatePermission
491 @allow_empty_body
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}
496
497 @Endpoint(method='POST', path='peer')
498 @handle_rbd_mirror_error()
499 @UpdatePermission
500 @allow_empty_body
501 def import_token(self, pool_name, direction, token):
502 ioctx = mgr.rados.open_ioctx(pool_name)
503
504 directions = {
505 'rx': rbd.RBD_MIRROR_PEER_DIRECTION_RX,
506 'rx-tx': rbd.RBD_MIRROR_PEER_DIRECTION_RX_TX
507 }
508
509 direction_enum = directions.get(direction)
510 if direction_enum is None:
511 raise rbd.Error('invalid direction "{}"'.format(direction))
512
513 rbd.RBD().mirror_peer_bootstrap_import(ioctx, direction_enum, token)
514 return {}
515
516
517 @APIRouter('/block/mirroring/pool/{pool_name}/peer', Scope.RBD_MIRRORING)
518 @APIDoc("RBD Mirroring Pool Peer Management API", "RbdMirroringPoolPeer")
519 class RbdMirroringPoolPeer(RESTController):
520
521 RESOURCE_ID = "peer_uuid"
522
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]
528
529 @handle_rbd_mirror_error()
530 def create(self, pool_name, cluster_name, client_id, mon_host=None,
531 key=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')
536
537 uuid = rbd.RBD().mirror_peer_add(ioctx, cluster_name,
538 'client.{}'.format(client_id))
539
540 attributes = {}
541 if mon_host is not None:
542 attributes[rbd.RBD_MIRROR_PEER_ATTRIBUTE_NAME_MON_HOST] = mon_host
543 if key is not None:
544 attributes[rbd.RBD_MIRROR_PEER_ATTRIBUTE_NAME_KEY] = key
545 if attributes:
546 rbd.RBD().mirror_peer_set_attributes(ioctx, uuid, attributes)
547
548 _reset_view_cache()
549 return {'uuid': uuid}
550
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)
556 if not peer:
557 raise cherrypy.HTTPError(404)
558
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']
562
563 # convert direction enum to string
564 directions = {
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'
568 }
569 peer['direction'] = directions[peer.get('direction', rbd.RBD_MIRROR_PEER_DIRECTION_RX)]
570
571 try:
572 attributes = rbd.RBD().mirror_peer_get_attributes(ioctx, peer_uuid)
573 except rbd.ImageNotFound:
574 attributes = {}
575
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, '')
578 return peer
579
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)
584 _reset_view_cache()
585
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)
590 if cluster_name:
591 rbd.RBD().mirror_peer_set_cluster(ioctx, peer_uuid, cluster_name)
592 if client_id:
593 rbd.RBD().mirror_peer_set_client(ioctx, peer_uuid,
594 'client.{}'.format(client_id))
595
596 if mon_host is not None or key is not None:
597 try:
598 attributes = rbd.RBD().mirror_peer_get_attributes(ioctx, peer_uuid)
599 except rbd.ImageNotFound:
600 attributes = {}
601
602 if mon_host is not None:
603 attributes[rbd.RBD_MIRROR_PEER_ATTRIBUTE_NAME_MON_HOST] = mon_host
604 if key is not None:
605 attributes[rbd.RBD_MIRROR_PEER_ATTRIBUTE_NAME_KEY] = key
606 rbd.RBD().mirror_peer_set_attributes(ioctx, peer_uuid, attributes)
607
608 _reset_view_cache()
609
610
611 @UIRouter('/block/mirroring', Scope.RBD_MIRRORING)
612 class RbdMirroringStatus(BaseController):
613 @EndpointDoc('Display RBD Mirroring Status')
614 @Endpoint()
615 @ReadPermission
616 def status(self):
617 status = {'available': True, 'message': None}
618 orch_status = OrchClient.instance().status()
619
620 # if the orch is not available we can't create the service
621 # using dashboard.
622 if not orch_status['available']:
623 return status
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
627 return status
628
629 @Endpoint('POST')
630 @EndpointDoc('Configure RBD Mirroring')
631 @CreatePermission
632 def configure(self):
633 rbd_pool = RBDPool()
634 service = Service()
635
636 service_spec = {
637 'service_type': 'rbd-mirror',
638 'placement': {},
639 'unmanaged': False
640 }
641
642 if not CephService.get_service_list('rbd-mirror'):
643 service.create(service_spec, 'rbd-mirror')
644
645 if not CephService.get_pool_list('rbd'):
646 rbd_pool.create()