From 542875a100cbfc3cb85499783d5a216e12205bb8 Mon Sep 17 00:00:00 2001 From: Imran Imtiaz Date: Fri, 20 Feb 2026 10:57:15 +0000 Subject: [PATCH] mgr/dashboard: add schedule_level to image API for pool/cluster snapshot schedule Add optional schedule_level param (image|pool|cluster) to PUT /api/block/image/{image_spec}. Removes more-specific schedules before setting at the chosen level. Backward compatible when omitted. Fixes: https://tracker.ceph.com/issues/75043 Assisted-by: Cursor AI Signed-off-by: Imran Imtiaz --- src/pybind/mgr/dashboard/controllers/rbd.py | 4 +- src/pybind/mgr/dashboard/openapi.yaml | 2 + src/pybind/mgr/dashboard/services/rbd.py | 45 ++++++++++++++++++--- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py index 910c4994dcc..54ec2f75de4 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd.py +++ b/src/pybind/mgr/dashboard/controllers/rbd.py @@ -163,11 +163,11 @@ class Rbd(RESTController): def set(self, image_spec, name=None, size=None, features=None, configuration=None, metadata=None, enable_mirror=None, primary=None, force=False, resync=False, mirror_mode=None, image_mirror_mode=None, - schedule_interval='', remove_scheduling=False): + schedule_interval='', remove_scheduling=False, schedule_level=None): return RbdService.set(image_spec, name, size, features, configuration, metadata, enable_mirror, primary, force, resync, mirror_mode, image_mirror_mode, - schedule_interval, remove_scheduling) + schedule_interval, remove_scheduling, schedule_level) @RbdTask('copy', {'src_image_spec': '{image_spec}', diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index c20e63e4a6a..ebe20de5f75 100755 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -820,6 +820,8 @@ paths: schedule_interval: default: '' type: string + schedule_level: + type: string size: type: integer type: object diff --git a/src/pybind/mgr/dashboard/services/rbd.py b/src/pybind/mgr/dashboard/services/rbd.py index 4227093fc9b..678cec79806 100644 --- a/src/pybind/mgr/dashboard/services/rbd.py +++ b/src/pybind/mgr/dashboard/services/rbd.py @@ -115,6 +115,13 @@ def get_image_spec(pool_name, namespace, rbd_name): return '{}/{}{}'.format(pool_name, namespace, rbd_name) +def get_pool_schedule_spec(pool_name, namespace): + """Build the schedule level_spec for pool-level schedule (rbd_support format).""" + if namespace: + return '{}/{}/'.format(pool_name, namespace) + return '{}/'.format(pool_name) + + def parse_image_spec(image_spec): namespace_spec, image_name = image_spec.rsplit('/', 1) if '/' in namespace_spec: @@ -586,10 +593,17 @@ class RbdService(object): def set(cls, image_spec, name=None, size=None, features=None, configuration=None, metadata=None, enable_mirror=None, primary=None, force=False, resync=False, mirror_mode=None, image_mirror_mode=None, - schedule_interval='', remove_scheduling=False): + schedule_interval='', remove_scheduling=False, schedule_level=None): # pylint: disable=too-many-branches pool_name, namespace, image_name = parse_image_spec(image_spec) + if schedule_level is not None and schedule_level not in ('image', 'pool', 'cluster'): + raise DashboardException( + msg='schedule_level must be one of: image, pool, cluster', + code='invalid_schedule_level', + component='rbd') + effective_schedule_level = schedule_level if schedule_level else 'image' + def _edit(ioctx, image): rbd_inst = cls._rbd_inst # check rename image @@ -652,11 +666,30 @@ class RbdService(object): if resync: RbdMirroringService.resync_image(image_name, pool_name, namespace) - if schedule_interval: - RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval) + current_image_name = name if name else image_name + image_schedule_spec = get_image_spec(pool_name, namespace, current_image_name) + pool_schedule_spec = get_pool_schedule_spec(pool_name, namespace) if remove_scheduling: - RbdMirroringService.snapshot_schedule_remove(image_spec) + if effective_schedule_level == 'image': + RbdMirroringService.snapshot_schedule_remove(image_schedule_spec) + elif effective_schedule_level == 'pool': + RbdMirroringService.snapshot_schedule_remove(pool_schedule_spec) + else: + RbdMirroringService.snapshot_schedule_remove('') + + if schedule_interval: + if effective_schedule_level == 'image': + RbdMirroringService.snapshot_schedule_add( + image_schedule_spec, schedule_interval) + elif effective_schedule_level == 'pool': + RbdMirroringService.snapshot_schedule_remove(image_schedule_spec) + RbdMirroringService.snapshot_schedule_add( + pool_schedule_spec, schedule_interval) + else: + RbdMirroringService.snapshot_schedule_remove(image_schedule_spec) + RbdMirroringService.snapshot_schedule_remove(pool_schedule_spec) + RbdMirroringService.snapshot_schedule_add('', schedule_interval) return rbd_image_call(pool_name, namespace, image_name, _edit) @@ -836,7 +869,7 @@ class RbdMirroringService: for _, schedule in schedule_list.items(): name = schedule.get("name") - if not name: + if name is None: continue # find status entry for this schedule @@ -891,7 +924,7 @@ class RbdMirroringService: for _, schedule in schedule_list.items(): name = schedule.get("name") - if name and name == schedule_spec: + if name is not None and name == schedule_spec: return schedule.get("schedule", []) return None -- 2.47.3