From bb9f126e4e590d8e7f1e44a71bed4d02efb1fafe Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Thu, 11 Sep 2025 09:43:13 +0530 Subject: [PATCH] mgr/dashboard: fix missing schedule interval in rbd API 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 (cherry picked from commit 72cebf0126bd07f7d42b0ae7b68646c527044942) --- src/pybind/mgr/dashboard/controllers/pool.py | 10 ++- .../rbd-form/rbd-form-create-request.model.ts | 1 + .../ceph/block/rbd-form/rbd-form.component.ts | 6 +- .../app/ceph/block/rbd-form/rbd-form.model.ts | 13 ++- src/pybind/mgr/dashboard/services/rbd.py | 80 +++++++++++++++++-- 5 files changed, 101 insertions(+), 9 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/pool.py b/src/pybind/mgr/dashboard/controllers/pool.py index 5c25c8b2a5d..ab931971227 100644 --- a/src/pybind/mgr/dashboard/controllers/pool.py +++ b/src/pybind/mgr/dashboard/controllers/pool.py @@ -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: diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts index 2a2366f7c02..a966dcafbe9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts @@ -1,5 +1,6 @@ import { RbdFormModel } from './rbd-form.model'; export class RbdFormCreateRequestModel extends RbdFormModel { + schedule_interval: string; features: Array = []; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts index 7d694e2cab4..84652ebd6c1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts @@ -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); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts index 262d79c95ba..faac9ba2860 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts @@ -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; } diff --git a/src/pybind/mgr/dashboard/services/rbd.py b/src/pybind/mgr/dashboard/services/rbd.py index 812774ba438..72b43be6648 100644 --- a/src/pybind/mgr/dashboard/services/rbd.py +++ b/src/pybind/mgr/dashboard/services/rbd.py @@ -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: "//". + + 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): -- 2.39.5