]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: fix missing schedule interval in rbd API 65481/head
authorNizamudeen A <nia@redhat.com>
Thu, 11 Sep 2025 04:13:13 +0000 (09:43 +0530)
committerNizamudeen A <nia@redhat.com>
Fri, 12 Sep 2025 03:20:08 +0000 (08:50 +0530)
Fetching the rbd image schedule interval through the rbd_support module
schedule list command

GET /api/rbd will have the following field per image
```
"schedule_info": {
                    "image": "rbd/rbd_1",
                    "schedule_time": "2025-09-11 03:00:00",
                    "schedule_interval": [
                        {
                            "interval": "5d",
                            "start_time": null
                        },
                        {
                            "interval": "3h",
                            "start_time": null
                        }
                    ]
                },
```

Also fixes the UI where schedule interval was missing in the form and
also disable editing the schedule_interval.

Extended the same thing to the `GET /api/pool` endpoint.

Fixes: https://tracker.ceph.com/issues/72977
Signed-off-by: Nizamudeen A <nia@redhat.com>
src/pybind/mgr/dashboard/controllers/pool.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts
src/pybind/mgr/dashboard/services/rbd.py

index 5c25c8b2a5d36a1c97a40e0274c52fc4a4a53d28..ab9319712275f3d854f44ddcbacfc0ee92f359cc 100644 (file)
@@ -10,7 +10,7 @@ from .. import mgr
 from ..security import Scope
 from ..services.ceph_service import CephService
 from ..services.exception import handle_send_command_error
-from ..services.rbd import RbdConfiguration
+from ..services.rbd import RbdConfiguration, RbdMirroringService
 from ..tools import TaskManager, str_to_bool
 from . import APIDoc, APIRouter, Endpoint, EndpointDoc, ReadPermission, \
     RESTController, Task, UIRouter
@@ -156,6 +156,14 @@ class Pool(RESTController):
         pool = [p for p in pools if p['pool_name'] == pool_name]
         if not pool:
             raise cherrypy.NotFound('No such pool')
+
+        schedule_info = RbdMirroringService.get_snapshot_schedule_info()
+        if schedule_info:
+            filtered = [
+                info for info in schedule_info
+                if info["name"].split("/", 1)[0] == pool_name
+            ]
+            pool[0]['schedule_info'] = filtered[0] if filtered else {}
         return pool[0]
 
     def get(self, pool_name: str, attrs: Optional[str] = None, stats: bool = False) -> dict:
index 2a2366f7c02d909ef019d4536d0e38d3c6bb3f8e..a966dcafbe9fb68f57503d84eed0002fe4887f84 100644 (file)
@@ -1,5 +1,6 @@
 import { RbdFormModel } from './rbd-form.model';
 
 export class RbdFormCreateRequestModel extends RbdFormModel {
+  schedule_interval: string;
   features: Array<string> = [];
 }
index 7d694e2cab4a250071f130b0ccea53ebefd1f745..84652ebd6c1175d84e3601a975bd7e1e962a13aa 100644 (file)
@@ -659,7 +659,11 @@ export class RbdFormComponent extends CdForm implements OnInit {
         this.rbdForm.get('mirroring').setValue(this.mirroring);
         this.rbdForm.get('mirroringMode').setValue(response?.mirror_mode);
         this.currentImageMirrorMode = response?.mirror_mode;
-        this.rbdForm.get('schedule').setValue(response?.schedule_interval);
+        const scheduleInterval = response?.schedule_info?.schedule_interval[0]?.interval;
+        if (scheduleInterval) {
+          this.rbdForm.get('schedule').setValue(scheduleInterval);
+          this.rbdForm.get('schedule').disable();
+        }
       } else {
         this.mirroring = false;
         this.rbdForm.get('mirroring').setValue(this.mirroring);
index 262d79c95baec30e3398582c40f83700577cadf9..faac9ba28602b92b5a852d54fdd572e0cd4c0059 100644 (file)
@@ -21,6 +21,17 @@ export class RbdFormModel {
   enable_mirror?: boolean;
   mirror_mode?: string;
 
-  schedule_interval: string;
+  schedule_info: ScheduleInfo;
+  start_time: string;
+}
+
+export class ScheduleInfo {
+  image: string;
+  schedule_time: string;
+  schedule_interval: ScheduleInterval[];
+}
+
+export class ScheduleInterval {
+  interval: string;
   start_time: string;
 }
index 812774ba438db5093f07202cb59020ca7c4610e2..72b43be664815a7f78f537152c79f066ad77c0b3 100644 (file)
@@ -16,7 +16,7 @@ from ._paginate import ListPaginator
 from .ceph_service import CephService
 
 try:
-    from typing import List, Optional
+    from typing import Dict, List, Optional
 except ImportError:
     pass  # For typing only
 
@@ -315,11 +315,11 @@ class RbdService(object):
                 stat['mirror_mode'] = 'journal'
             elif mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
                 stat['mirror_mode'] = 'snapshot'
-                schedule_status = json.loads(_rbd_support_remote(
-                    'mirror_snapshot_schedule_status')[1])
-                for scheduled_image in schedule_status['scheduled_images']:
-                    if scheduled_image['image'] == get_image_spec(pool_name, namespace, image_name):
-                        stat['schedule_info'] = scheduled_image
+                schedule_info = RbdMirroringService.get_snapshot_schedule_info(
+                    get_image_spec(pool_name, namespace, image_name)
+                )
+                if schedule_info:
+                    stat['schedule_info'] = schedule_info[0]
 
             stat['name'] = image_name
 
@@ -758,6 +758,74 @@ class RbdMirroringService:
     def snapshot_schedule_remove(cls, image_spec: str):
         _rbd_support_remote('mirror_snapshot_schedule_remove', image_spec)
 
+    @classmethod
+    def snapshot_schedule_list(cls, image_spec: str = ''):
+        return _rbd_support_remote('mirror_snapshot_schedule_list', image_spec)
+
+    @classmethod
+    def snapshot_schedule_status(cls, image_spec: str = ''):
+        return _rbd_support_remote('mirror_snapshot_schedule_status', image_spec)
+
+    @classmethod
+    def get_snapshot_schedule_info(cls, image_spec: str = ''):
+        """
+        Retrieve snapshot schedule information by merging schedule list and status.
+
+        Args:
+            image_spec (str, optional): Specification of an RBD image. If empty,
+                retrieves all schedule information.
+                Format: "<pool_name>/<namespace_name>/<image_name>".
+
+        Returns:
+            Optional[List[Dict[str, Any]]]: A list of merged schedule information
+            dictionaries if found, otherwise None.
+        """
+        schedule_info: List[Dict] = []
+
+        # schedule list and status provide the schedule interval
+        # and schedule timestamp respectively.
+        schedule_list_raw = cls.snapshot_schedule_list(image_spec)
+        schedule_status_raw = cls.snapshot_schedule_status(image_spec)
+
+        try:
+            schedule_list = json.loads(
+                schedule_list_raw[1]) if schedule_list_raw and schedule_list_raw[1] else {}
+            schedule_status = json.loads(
+                schedule_status_raw[1]) if schedule_status_raw and schedule_status_raw[1] else {}
+        except (json.JSONDecodeError, TypeError):
+            return None
+
+        if not schedule_list or not schedule_status:
+            return None
+
+        scheduled_images = schedule_status.get("scheduled_images", [])
+
+        for _, schedule in schedule_list.items():
+            name = schedule.get("name")
+            if not name:
+                continue
+
+            # find status entry for this schedule
+            # by matching with the image name
+            image = next((
+                sched_image for sched_image in scheduled_images
+                if sched_image.get("image") == name), None)
+            if not image:
+                continue
+
+            # eventually we are merging both the list and status entries
+            # all the needed info are fetched above and here we are just mapping
+            # it to the dictionary so that in one function we get
+            # the schedule related information.
+            merged = {
+                "name": name,
+                "schedule_interval": schedule.get("schedule", []),
+                "schedule_time": image.get("schedule_time")
+            }
+            schedule_info.append(merged)
+
+        return schedule_info if schedule_info else None
+
 
 class RbdImageMetadataService(object):
     def __init__(self, image):