]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: expose image mirroring commands as endpoints 46249/head
authorPere Diaz Bou <pdiazbou@redhat.com>
Thu, 12 May 2022 18:29:01 +0000 (20:29 +0200)
committerPere Diaz Bou <pdiazbou@redhat.com>
Mon, 23 May 2022 12:20:50 +0000 (14:20 +0200)
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 <pdiazbou@redhat.com>
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rbd.py
src/pybind/mgr/dashboard/tests/test_rbd_service.py

index 066b235214721445f21e35a034c0cfe568a1f8e0..87d40707c00f46926069a1a1fadd2a5b1deb32f4 100644 (file)
@@ -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)
index 00329b78033840b2a81e542d5c5b654caa0e4604..df4e7c78de2ef54ed39e68e86e794375e302029e 100644 (file)
@@ -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':
index 039482ddd87256132999860ba4d15070c2891a14..2c255a4fcb6e515f377ca325eda3983abe67eb8d 100644 (file)
@@ -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)
index 345f360b607feab89fc1682ddd75005a036df874..89b062a7c912fd0c96dcbcbc8ee70b15382015d8 100644 (file)
@@ -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)