]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: expose image summary API
authorNizamudeen A <nia@redhat.com>
Mon, 18 Aug 2025 07:47:01 +0000 (13:17 +0530)
committerNizamudeen A <nia@redhat.com>
Mon, 1 Sep 2025 15:59:56 +0000 (21:29 +0530)
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 <nia@redhat.com>
(cherry picked from commit f4b09da1521684a4dbae2fe5929484fb74783559)

src/pybind/mgr/dashboard/controllers/_base_controller.py
src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
src/pybind/mgr/dashboard/openapi.yaml

index ac7bc4a6b022cae8a2c6ffdfb0f36b3eec9343db..a1a128da5e6a9a6df41a12a26ab4f420dc7ad562 100644 (file)
@@ -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
 
index 2b081051fe413e975cfbe7bbee6a0f6b0ee62d14..547e460a1989f94bb3e3b6a3ed07fc5e64f95daa 100644 (file)
@@ -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):
index db3a51714c8f33a0bd9f43800fe77e92b186a303..a8830036aaec0acf939bf5b8603566acd25bf596 100755 (executable)
@@ -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: