Skip to content

Commit 72cebf0

Browse files
committed
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 <[email protected]>
1 parent 098432f commit 72cebf0

File tree

5 files changed

+101
-9
lines changed

5 files changed

+101
-9
lines changed

src/pybind/mgr/dashboard/controllers/pool.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from ..security import Scope
1111
from ..services.ceph_service import CephService
1212
from ..services.exception import handle_send_command_error
13-
from ..services.rbd import RbdConfiguration
13+
from ..services.rbd import RbdConfiguration, RbdMirroringService
1414
from ..tools import TaskManager, str_to_bool
1515
from . import APIDoc, APIRouter, Endpoint, EndpointDoc, ReadPermission, \
1616
RESTController, Task, UIRouter
@@ -156,6 +156,14 @@ def _get(cls, pool_name: str, attrs: Optional[str] = None, stats: bool = False)
156156
pool = [p for p in pools if p['pool_name'] == pool_name]
157157
if not pool:
158158
raise cherrypy.NotFound('No such pool')
159+
160+
schedule_info = RbdMirroringService.get_snapshot_schedule_info()
161+
if schedule_info:
162+
filtered = [
163+
info for info in schedule_info
164+
if info["name"].split("/", 1)[0] == pool_name
165+
]
166+
pool[0]['schedule_info'] = filtered[0] if filtered else {}
159167
return pool[0]
160168

161169
def get(self, pool_name: str, attrs: Optional[str] = None, stats: bool = False) -> dict:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { RbdFormModel } from './rbd-form.model';
22

33
export class RbdFormCreateRequestModel extends RbdFormModel {
4+
schedule_interval: string;
45
features: Array<string> = [];
56
}

src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,11 @@ export class RbdFormComponent extends CdForm implements OnInit {
659659
this.rbdForm.get('mirroring').setValue(this.mirroring);
660660
this.rbdForm.get('mirroringMode').setValue(response?.mirror_mode);
661661
this.currentImageMirrorMode = response?.mirror_mode;
662-
this.rbdForm.get('schedule').setValue(response?.schedule_interval);
662+
const scheduleInterval = response?.schedule_info?.schedule_interval[0]?.interval;
663+
if (scheduleInterval) {
664+
this.rbdForm.get('schedule').setValue(scheduleInterval);
665+
this.rbdForm.get('schedule').disable();
666+
}
663667
} else {
664668
this.mirroring = false;
665669
this.rbdForm.get('mirroring').setValue(this.mirroring);

src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ export class RbdFormModel {
2121
enable_mirror?: boolean;
2222
mirror_mode?: string;
2323

24-
schedule_interval: string;
24+
schedule_info: ScheduleInfo;
25+
start_time: string;
26+
}
27+
28+
export class ScheduleInfo {
29+
image: string;
30+
schedule_time: string;
31+
schedule_interval: ScheduleInterval[];
32+
}
33+
34+
export class ScheduleInterval {
35+
interval: string;
2536
start_time: string;
2637
}

src/pybind/mgr/dashboard/services/rbd.py

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .ceph_service import CephService
1717

1818
try:
19-
from typing import List, Optional
19+
from typing import Dict, List, Optional
2020
except ImportError:
2121
pass # For typing only
2222

@@ -315,11 +315,11 @@ def _rbd_image(cls, ioctx, pool_name, namespace, image_name, # pylint: disable=
315315
stat['mirror_mode'] = 'journal'
316316
elif mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT:
317317
stat['mirror_mode'] = 'snapshot'
318-
schedule_status = json.loads(_rbd_support_remote(
319-
'mirror_snapshot_schedule_status')[1])
320-
for scheduled_image in schedule_status['scheduled_images']:
321-
if scheduled_image['image'] == get_image_spec(pool_name, namespace, image_name):
322-
stat['schedule_info'] = scheduled_image
318+
schedule_info = RbdMirroringService.get_snapshot_schedule_info(
319+
get_image_spec(pool_name, namespace, image_name)
320+
)
321+
if schedule_info:
322+
stat['schedule_info'] = schedule_info[0]
323323

324324
stat['name'] = image_name
325325

@@ -758,6 +758,74 @@ def snapshot_schedule_add(cls, image_spec: str, interval: str):
758758
def snapshot_schedule_remove(cls, image_spec: str):
759759
_rbd_support_remote('mirror_snapshot_schedule_remove', image_spec)
760760

761+
@classmethod
762+
def snapshot_schedule_list(cls, image_spec: str = ''):
763+
return _rbd_support_remote('mirror_snapshot_schedule_list', image_spec)
764+
765+
@classmethod
766+
def snapshot_schedule_status(cls, image_spec: str = ''):
767+
return _rbd_support_remote('mirror_snapshot_schedule_status', image_spec)
768+
769+
@classmethod
770+
def get_snapshot_schedule_info(cls, image_spec: str = ''):
771+
"""
772+
Retrieve snapshot schedule information by merging schedule list and status.
773+
774+
Args:
775+
image_spec (str, optional): Specification of an RBD image. If empty,
776+
retrieves all schedule information.
777+
Format: "<pool_name>/<namespace_name>/<image_name>".
778+
779+
Returns:
780+
Optional[List[Dict[str, Any]]]: A list of merged schedule information
781+
dictionaries if found, otherwise None.
782+
"""
783+
schedule_info: List[Dict] = []
784+
785+
# schedule list and status provide the schedule interval
786+
# and schedule timestamp respectively.
787+
schedule_list_raw = cls.snapshot_schedule_list(image_spec)
788+
schedule_status_raw = cls.snapshot_schedule_status(image_spec)
789+
790+
try:
791+
schedule_list = json.loads(
792+
schedule_list_raw[1]) if schedule_list_raw and schedule_list_raw[1] else {}
793+
schedule_status = json.loads(
794+
schedule_status_raw[1]) if schedule_status_raw and schedule_status_raw[1] else {}
795+
except (json.JSONDecodeError, TypeError):
796+
return None
797+
798+
if not schedule_list or not schedule_status:
799+
return None
800+
801+
scheduled_images = schedule_status.get("scheduled_images", [])
802+
803+
for _, schedule in schedule_list.items():
804+
name = schedule.get("name")
805+
if not name:
806+
continue
807+
808+
# find status entry for this schedule
809+
# by matching with the image name
810+
image = next((
811+
sched_image for sched_image in scheduled_images
812+
if sched_image.get("image") == name), None)
813+
if not image:
814+
continue
815+
816+
# eventually we are merging both the list and status entries
817+
# all the needed info are fetched above and here we are just mapping
818+
# it to the dictionary so that in one function we get
819+
# the schedule related information.
820+
merged = {
821+
"name": name,
822+
"schedule_interval": schedule.get("schedule", []),
823+
"schedule_time": image.get("schedule_time")
824+
}
825+
schedule_info.append(merged)
826+
827+
return schedule_info if schedule_info else None
828+
761829

762830
class RbdImageMetadataService(object):
763831
def __init__(self, image):

0 commit comments

Comments
 (0)