From 504b26a1deaa00fd3e9f6e6984ef6d7ebdd90c21 Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Mon, 18 Aug 2025 13:17:01 +0530 Subject: [PATCH] mgr/dashboard: expose image summary API MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Introduce a new API for getting per image summary ``` ╰─$ curl -kX GET "https://localhost:4200/api/block/mirroring/rbd/test/summary" \ -H "Accept: application/vnd.ceph.api.v1.0+json" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 637 100 637 0 0 14597 0 --:--:-- --:--:-- --:--:-- 14813 { "name": "test2", "id": "10d618ea1a58", "info": { "global_id": "f25678be-64a2-481f-b96c-9bcc566dcbfe", "state": 1, "primary": true }, "remote_statuses": [ { "state": "Replaying", "description": { "bytes_per_second": 0.0, "bytes_per_snapshot": 0.0, "last_snapshot_bytes": 0, "last_snapshot_sync_seconds": 0, "local_snapshot_timestamp": 1755579780, "remote_snapshot_timestamp": 1755579780, "replay_state": "idle" }, "last_update": "2025-08-19T05:03:17Z", "up": true, "mirror_uuid": "4d734616-5a38-4399-b743-86bcd8c1ab8f" } ], "state": 6, "description": "local image is primary", "last_update": "2025-08-19T05:03:10Z", "up": true } ``` Also update the existing API to add the image syncing status. The /summary API's `image_ready` will also have the `remote_status` which is a list of dict to show the status of all the remote clusters (one image can be mirrored to more than one cluster) ``` "image_ready": [ { "pool_name": "rbd", "name": "test2", "state_color": "info", "state": "Stopped", "mirror_mode": "snapshot", "description": "local image is primary", "remote_status": [ { "state": "Replaying", "description": { "bytes_per_second": 0.0, "bytes_per_snapshot": 0.0, "last_snapshot_bytes": 0, "last_snapshot_sync_seconds": 0, "local_snapshot_timestamp": 1755579780, "remote_snapshot_timestamp": 1755579780, "replay_state": "idle" }, "last_update": "2025-08-19T05:03:47Z", "up": true, "mirror_uuid": "4d734616-5a38-4399-b743-86bcd8c1ab8f" } ] } ] ``` Fixes: https://tracker.ceph.com/issues/72520 Signed-off-by: Nizamudeen A (cherry picked from commit f4b09da1521684a4dbae2fe5929484fb74783559) --- .../dashboard/controllers/_base_controller.py | 9 ++- .../dashboard/controllers/rbd_mirroring.py | 60 ++++++++++++++++++- src/pybind/mgr/dashboard/openapi.yaml | 32 ++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/_base_controller.py b/src/pybind/mgr/dashboard/controllers/_base_controller.py index ac7bc4a6b022c..a1a128da5e6a9 100644 --- a/src/pybind/mgr/dashboard/controllers/_base_controller.py +++ b/src/pybind/mgr/dashboard/controllers/_base_controller.py @@ -1,3 +1,4 @@ +import datetime import inspect import json import logging @@ -279,9 +280,15 @@ class BaseController: if version else 'application/xml') return ret.encode('utf8') if json_response: + # convert datetime obj so json can serialize properly + def json_default(obj): + if isinstance(obj, datetime.datetime): + return obj.isoformat().replace("+00:00", "Z") + return str(obj) + cherrypy.response.headers['Content-Type'] = (version.to_mime_type(subtype='json') if version else 'application/json') - ret = json.dumps(ret).encode('utf8') + ret = json.dumps(ret, default=json_default).encode('utf8') return ret return inner diff --git a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py index 2b081051fe413..547e460a1989f 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py +++ b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py @@ -37,6 +37,26 @@ class MirrorHealth(IntEnum): MIRROR_HEALTH_DISABLED = 4 MIRROR_HEALTH_INFO = 5 + +MIRROR_IMAGE_STATUS_MAP = { + rbd.MIRROR_IMAGE_STATUS_STATE_UNKNOWN: "Unknown", + rbd.MIRROR_IMAGE_STATUS_STATE_ERROR: "Error", + rbd.MIRROR_IMAGE_STATUS_STATE_SYNCING: "Syncing", + rbd.MIRROR_IMAGE_STATUS_STATE_STARTING_REPLAY: "Starting Replay", + rbd.MIRROR_IMAGE_STATUS_STATE_REPLAYING: "Replaying", + rbd.MIRROR_IMAGE_STATUS_STATE_STOPPING_REPLAY: "Stopping Replay", + rbd.MIRROR_IMAGE_STATUS_STATE_STOPPED: "Stopped", +} + + +def get_mirror_status_label(code: int) -> str: + default_code = rbd.MIRROR_IMAGE_STATUS_STATE_UNKNOWN + return MIRROR_IMAGE_STATUS_MAP.get( + code, + MIRROR_IMAGE_STATUS_MAP[default_code] + ) + + # pylint: disable=not-callable @@ -220,6 +240,31 @@ def _get_pool_stats(pool_names): return pool_stats +def _get_mirroring_status(pool_name, image_name): + ioctx = mgr.rados.open_ioctx(pool_name) + mode = rbd.RBD().mirror_mode_get(ioctx) + status = rbd.Image(ioctx, image_name).mirror_image_get_status() + for remote in status.get("remote_statuses", []): + remote["state"] = get_mirror_status_label(remote["state"]) + desc = remote.get("description") + + if not desc.startswith("replaying, "): + continue + + try: + metrics = json.loads(desc.split(", ", 1)[1]) + remote["description"] = metrics + except (IndexError, json.JSONDecodeError): + continue + + if mode == rbd.RBD_MIRROR_MODE_POOL: + primary_tid = metrics["primary_position"]["entry_tid"] + non_primary_tid = metrics["non_primary_position"]["entry_tid"] + percent_done = (non_primary_tid / primary_tid) * 100 if primary_tid else 0 + remote["syncing_percent"] = round(percent_done, 2) + return status + + @ViewCache() def get_daemons_and_pools(): # pylint: disable=R0915 daemons = get_daemons() @@ -387,8 +432,10 @@ def _get_content_data(): # pylint: disable=R0914 } if mirror_image['health'] == 'ok': + status = _get_mirroring_status(pool_name, mirror_image['name']) image.update({ - 'description': mirror_image['description'] + 'description': mirror_image['description'], + 'remote_status': status.get("remote_statuses", []) }) image_ready.append(image) elif mirror_image['health'] == 'syncing': @@ -487,6 +534,17 @@ class RbdMirroringSummary(BaseController): 'content_data': content_data} +@APIRouter('/block/mirroring/{pool_name}/{image_name}/summary', Scope.RBD_MIRRORING) +@APIDoc("RBD Mirroring Summary Management API", "RbdMirroringSummary") +class RbdImageMirroringSummary(BaseController): + + @Endpoint() + @handle_rbd_mirror_error() + @ReadPermission + def __call__(self, pool_name, image_name): + return _get_mirroring_status(pool_name, image_name) + + @APIRouter('/block/mirroring/pool', Scope.RBD_MIRRORING) @APIDoc("RBD Mirroring Pool Mode Management API", "RbdMirroringPoolMode") class RbdMirroringPoolMode(RESTController): diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index db3a51714c8f3..a8830036aaec0 100755 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -1558,6 +1558,38 @@ paths: summary: Display Rbd Mirroring Summary tags: - RbdMirroringSummary + /api/block/mirroring/{pool_name}/{image_name}/summary: + get: + parameters: + - in: path + name: pool_name + required: true + schema: + type: string + - in: path + name: image_name + required: true + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RbdMirroringSummary /api/block/pool/{pool_name}/namespace: get: parameters: -- 2.39.5