From 8bd89415fe340512f457acd58225934e9ed8e4e1 Mon Sep 17 00:00:00 2001 From: Pere Diaz Bou Date: Thu, 12 May 2022 20:29:01 +0200 Subject: [PATCH] mgr/dashboard: expose image mirroring commands as endpoints Expose: - enable/disable mirroring in image - promote/demote (primary and non-primary) - resync - snapshot mode: - mirror image snapshot (manual snapshot) - schedule Fixes: https://tracker.ceph.com/issues/55645 Signed-off-by: Pere Diaz Bou --- src/pybind/mgr/dashboard/controllers/rbd.py | 46 ++++++++++++++++-- src/pybind/mgr/dashboard/openapi.yaml | 15 ++++++ src/pybind/mgr/dashboard/services/rbd.py | 48 ++++++++++++++++++- .../mgr/dashboard/tests/test_rbd_service.py | 21 +++++++- 4 files changed, 122 insertions(+), 8 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py index 066b235214721..87d40707c00f4 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd.py +++ b/src/pybind/mgr/dashboard/controllers/rbd.py @@ -5,6 +5,7 @@ import logging import math from datetime import datetime +from enum import Enum from functools import partial import rbd @@ -14,9 +15,9 @@ from ..exceptions import DashboardException from ..security import Scope from ..services.ceph_service import CephService from ..services.exception import handle_rados_error, handle_rbd_error, serialize_dashboard_exception -from ..services.rbd import RbdConfiguration, RbdService, RbdSnapshotService, \ - format_bitmask, format_features, parse_image_spec, rbd_call, \ - rbd_image_call +from ..services.rbd import RbdConfiguration, RbdMirroringService, RbdService, \ + RbdSnapshotService, format_bitmask, format_features, parse_image_spec, \ + rbd_call, rbd_image_call from ..tools import ViewCache, str_to_bool from . import APIDoc, APIRouter, CreatePermission, DeletePermission, \ EndpointDoc, RESTController, Task, UpdatePermission, allow_empty_body @@ -76,6 +77,10 @@ class Rbd(RESTController): ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten", "journaling"} + class MIRROR_IMAGE_MODE(Enum): + journal = rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL + snapshot = rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT + def _rbd_list(self, pool_name=None): if pool_name: pools = [pool_name] @@ -146,7 +151,9 @@ class Rbd(RESTController): return rbd_call(pool_name, namespace, rbd_inst.remove, image_name) @RbdTask('edit', ['{image_spec}', '{name}'], 4.0) - def set(self, image_spec, name=None, size=None, features=None, configuration=None): + def set(self, image_spec, name=None, size=None, features=None, + configuration=None, enable_mirror=None, primary=None, + resync=False, mirror_mode=None, schedule_interval='', start_time=''): pool_name, namespace, image_name = parse_image_spec(image_spec) def _edit(ioctx, image): @@ -182,6 +189,29 @@ class Rbd(RESTController): RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).set_configuration( configuration) + mirror_image_info = image.mirror_image_get_info() + if enable_mirror and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_DISABLED: + RbdMirroringService.enable_image( + image_name, pool_name, namespace, + self.MIRROR_IMAGE_MODE[mirror_mode].value) + elif (enable_mirror is False + and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED): + RbdMirroringService.disable_image( + image_name, pool_name, namespace) + + if primary and not mirror_image_info['primary']: + RbdMirroringService.promote_image( + image_name, pool_name, namespace) + elif primary is False and mirror_image_info['primary']: + RbdMirroringService.demote_image( + image_name, pool_name, namespace) + + if resync: + RbdMirroringService.resync_image(image_name, pool_name, namespace) + + if schedule_interval: + RbdMirroringService.snapshot_schedule(image_spec, schedule_interval, start_time) + return rbd_image_call(pool_name, namespace, image_name, _edit) @RbdTask('copy', @@ -275,7 +305,13 @@ class RbdSnapshot(RESTController): pool_name, namespace, image_name = parse_image_spec(image_spec) def _create_snapshot(ioctx, img, snapshot_name): - img.create_snap(snapshot_name) + mirror_info = img.mirror_image_get_info() + mirror_mode = img.mirror_image_get_mode() + if (mirror_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED + and mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT): + img.mirror_image_create_snapshot() + else: + img.create_snap(snapshot_name) return rbd_image_call(pool_name, namespace, image_name, _create_snapshot, snapshot_name) diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 00329b7803384..df4e7c78de2ef 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -539,12 +539,27 @@ paths: properties: configuration: type: string + enable_mirror: + type: string features: type: string + mirror_mode: + type: string name: type: string + primary: + type: string + resync: + default: false + type: boolean + schedule_interval: + default: '' + type: string size: type: integer + start_time: + default: '' + type: string type: object responses: '200': diff --git a/src/pybind/mgr/dashboard/services/rbd.py b/src/pybind/mgr/dashboard/services/rbd.py index 039482ddd8725..2c255a4fcb6e5 100644 --- a/src/pybind/mgr/dashboard/services/rbd.py +++ b/src/pybind/mgr/dashboard/services/rbd.py @@ -84,13 +84,13 @@ def parse_image_spec(image_spec): def rbd_call(pool_name, namespace, func, *args, **kwargs): with mgr.rados.open_ioctx(pool_name) as ioctx: ioctx.set_namespace(namespace if namespace is not None else '') - func(ioctx, *args, **kwargs) + return func(ioctx, *args, **kwargs) def rbd_image_call(pool_name, namespace, image_name, func, *args, **kwargs): def _ioctx_func(ioctx, image_name, func, *args, **kwargs): with rbd.Image(ioctx, image_name) as img: - func(ioctx, img, *args, **kwargs) + return func(ioctx, img, *args, **kwargs) return rbd_call(pool_name, namespace, _ioctx_func, image_name, func, *args, **kwargs) @@ -415,3 +415,47 @@ class RbdSnapshotService(object): pool_name, namespace, image_name = parse_image_spec(image_spec) return rbd_image_call(pool_name, namespace, image_name, _remove_snapshot, snapshot_name, unprotect) + + +class RBDSchedulerInterval: + def __init__(self, interval: str): + self.amount = int(interval[:-1]) + self.unit = interval[-1] + if self.unit not in 'mhd': + raise ValueError(f'Invalid interval unit {self.unit}') + + def __str__(self): + return f'{self.amount}{self.unit}' + + +class RbdMirroringService: + + @classmethod + def enable_image(cls, image_name: str, pool_name: str, namespace: str, mode: str): + rbd_image_call(pool_name, namespace, image_name, + lambda ioctx, image: image.mirror_image_enable(mode)) + + @classmethod + def disable_image(cls, image_name: str, pool_name: str, namespace: str, force: bool = False): + rbd_image_call(pool_name, namespace, image_name, + lambda ioctx, image: image.mirror_image_disable(force)) + + @classmethod + def promote_image(cls, image_name: str, pool_name: str, namespace: str, force: bool = False): + rbd_image_call(pool_name, namespace, image_name, + lambda ioctx, image: image.mirror_image_promote(force)) + + @classmethod + def demote_image(cls, image_name: str, pool_name: str, namespace: str): + rbd_image_call(pool_name, namespace, image_name, + lambda ioctx, image: image.mirror_image_demote()) + + @classmethod + def resync_image(cls, image_name: str, pool_name: str, namespace: str): + rbd_image_call(pool_name, namespace, image_name, + lambda ioctx, image: image.mirror_image_resync()) + + @classmethod + def snapshot_schedule(cls, image_spec: str, interval: str, start_time: str = ''): + mgr.remote('rbd_support', 'mirror_snapshot_schedule_add', image_spec, + str(RBDSchedulerInterval(interval)), start_time) diff --git a/src/pybind/mgr/dashboard/tests/test_rbd_service.py b/src/pybind/mgr/dashboard/tests/test_rbd_service.py index 345f360b607fe..89b062a7c912f 100644 --- a/src/pybind/mgr/dashboard/tests/test_rbd_service.py +++ b/src/pybind/mgr/dashboard/tests/test_rbd_service.py @@ -11,7 +11,8 @@ except ImportError: import unittest.mock as mock from .. import mgr -from ..services.rbd import RbdConfiguration, RbdService, get_image_spec, parse_image_spec +from ..services.rbd import RbdConfiguration, RBDSchedulerInterval, RbdService, \ + get_image_spec, parse_image_spec class ImageNotFoundStub(Exception): @@ -139,3 +140,21 @@ class RbdServiceTest(unittest.TestCase): 'pool_name': 'test_pool', 'namespace': '' }])) + + def test_valid_interval(self): + test_cases = [ + ('15m', False), + ('1h', False), + ('5d', False), + ('m', True), + ('d', True), + ('1s', True), + ('11', True), + ('1m1', True), + ] + for interval, error in test_cases: + if error: + with self.assertRaises(ValueError): + RBDSchedulerInterval(interval) + else: + self.assertEqual(str(RBDSchedulerInterval(interval)), interval) -- 2.39.5