]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add CephFS mirror endpoints 66846/head
authorPedro Gonzalez Gomez <pegonzal@ibm.com>
Thu, 8 Jan 2026 19:28:18 +0000 (20:28 +0100)
committerPedro Gonzalez Gomez <pegonzal@ibm.com>
Mon, 26 Jan 2026 08:59:34 +0000 (09:59 +0100)
For: create peers, delete peers, create token, and check module status

Fixes: https://tracker.ceph.com/issues/74362
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@ibm.com>
src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/tests/test_cephfs.py

index 11bbea4d7bcfccd09cb79a9b080f9e4b939a6787..92c3f3911b4972196ad28bde0041beb8dbbe1711 100644 (file)
@@ -16,8 +16,8 @@ from ..services.ceph_service import CephService
 from ..services.cephfs import CephFS as CephFS_
 from ..services.exception import handle_cephfs_error
 from ..tools import ViewCache, str_to_bool
-from . import APIDoc, APIRouter, DeletePermission, Endpoint, EndpointDoc, \
-    ReadPermission, RESTController, UIRouter, UpdatePermission, \
+from . import APIDoc, APIRouter, CreatePermission, DeletePermission, Endpoint, \
+    EndpointDoc, ReadPermission, RESTController, UIRouter, UpdatePermission, \
     allow_empty_body
 
 GET_QUOTAS_SCHEMA = {
@@ -38,6 +38,10 @@ LIST_PEERS_SCHEMA = [{
     }, 'Peer ID'),
 }]
 
+BOOTSTRAP_PEERS_SCHEMA = {
+    'token': (str, 'Bootstrap token'),
+}
+
 DAEMON_STATUS_SCHEMA = [{
     'daemon_id': (int, 'Daemon ID'),
     'filesystems': ([{
@@ -1256,6 +1260,60 @@ class CephFSMirror(RESTController):
             )
         return json.loads(out)
 
+    @EndpointDoc("Create bootstrap token",
+                 parameters={
+                     'fs_name': (str, 'File system name'),
+                     'client_name': (str, 'Client entity\'s name'),
+                     'site_name': (str, 'Site name'),
+                 },
+                 responses={200: BOOTSTRAP_PEERS_SCHEMA})
+    @Endpoint('POST')
+    @CreatePermission
+    def token(self, fs_name: str, client_name: str, site_name: str):
+        error_code, out, err = mgr.remote(
+            'mirroring', 'snapshot_mirror_peer_bootstrap_create', fs_name, client_name, site_name)
+        if error_code != 0:
+            raise DashboardException(
+                msg=f'Failed to create bootstrap token: {err}',
+                code=error_code,
+                component='cephfs.mirror'
+            )
+        return json.loads(out)
+
+    @EndpointDoc("Create bootstrap peer",
+                 parameters={
+                     'fs_name': (str, 'File system name'),
+                     'token': (str, 'Bootstrap token'),
+                 },
+                 responses={200: {}})
+    @CreatePermission
+    def create(self, fs_name: str, token: str):
+        error_code, out, err = mgr.remote(
+            'mirroring', 'snapshot_mirror_peer_bootstrap_import', fs_name, token)
+        if error_code != 0:
+            raise DashboardException(
+                msg=f'Failed to import the token to create bootstrap peer: {err}',
+                code=error_code,
+                component='cephfs.mirror'
+            )
+        return json.loads(out)
+
+    @EndpointDoc("Delete peer",
+                 parameters={
+                     'fs_name': (str, 'File system name'),
+                     'peer_uuid': (str, 'Peer UUID'),
+                 })
+    @DeletePermission
+    def delete(self, fs_name: str, peer_uuid: str):
+        error_code, _, err = mgr.remote(
+            'mirroring', 'snapshot_mirror_peer_remove', fs_name, peer_uuid)
+        if error_code != 0:
+            raise DashboardException(
+                msg=f'Failed to delete peer: {err}',
+                code=error_code,
+                component='cephfs.mirror'
+            )
+
     @EndpointDoc("Get mirror daemon and peers information",
                  responses={200: DAEMON_STATUS_SCHEMA})
     @Endpoint('GET', path='/daemon-status')
@@ -1269,3 +1327,18 @@ class CephFSMirror(RESTController):
                 component='cephfs.mirror'
             )
         return json.loads(out)
+
+
+@UIRouter('/cephfs/mirror')
+class CephFSMirrorStatus(RESTController):
+    @ReadPermission
+    @EndpointDoc("Get Cephfs mirror status")
+    @Endpoint()
+    def status(self):
+        status: Dict[str, Any] = {'available': False, 'message': None}
+        try:
+            mgr.remote('mirroring', 'snapshot_mirror_daemon_status')
+            status['available'] = True
+        except (ImportError, RuntimeError):
+            status['message'] = 'Cephfs mirror module is not enabled'
+        return status
index dc17794bb8d43a4dded52f0f552e85572781e22a..221e494e90d8f196832f7dc1ad930e100779e684 100755 (executable)
@@ -2404,43 +2404,34 @@ paths:
       tags:
       - Cephfs
   /api/cephfs/mirror:
-    get:
-      parameters:
-      - description: File system name
-        in: query
-        name: fs_name
-        required: true
-        schema:
-          type: string
+    post:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                fs_name:
+                  description: File system name
+                  type: string
+                token:
+                  description: Bootstrap token
+                  type: string
+              required:
+              - fs_name
+              - token
+              type: object
       responses:
-        '200':
+        '201':
           content:
             application/vnd.ceph.api.v1.0+json:
-              schema:
-                items:
-                  properties:
-                    uuid:
-                      description: Peer ID
-                      properties:
-                        client_name:
-                          description: Ceph client name
-                          type: string
-                        fs_name:
-                          description: File system name
-                          type: string
-                        site_name:
-                          description: Remote site name
-                          type: string
-                      required:
-                      - client_name
-                      - site_name
-                      - fs_name
-                      type: object
-                  type: object
-                required:
-                - uuid
-                type: array
-          description: OK
+              type: object
+          description: Resource created.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
         '400':
           description: Operation exception. Please check the response body for details.
         '401':
@@ -2452,7 +2443,7 @@ paths:
             trace.
       security:
       - jwt: []
-      summary: Get peers
+      summary: Create bootstrap peer
       tags:
       - CephfsMirror
   /api/cephfs/mirror/daemon-status:
@@ -2551,6 +2542,145 @@ paths:
       summary: Get mirror daemon and peers information
       tags:
       - CephfsMirror
+  /api/cephfs/mirror/token:
+    post:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                client_name:
+                  description: Client entity's name
+                  type: string
+                fs_name:
+                  description: File system name
+                  type: string
+                site_name:
+                  description: Site name
+                  type: string
+              required:
+              - fs_name
+              - client_name
+              - site_name
+              type: object
+      responses:
+        '201':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource created.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Create bootstrap token
+      tags:
+      - CephfsMirror
+  /api/cephfs/mirror/{fs_name}:
+    get:
+      parameters:
+      - description: File system name
+        in: path
+        name: fs_name
+        required: true
+        schema:
+          type: string
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                items:
+                  properties:
+                    uuid:
+                      description: Peer ID
+                      properties:
+                        client_name:
+                          description: Ceph client name
+                          type: string
+                        fs_name:
+                          description: File system name
+                          type: string
+                        site_name:
+                          description: Remote site name
+                          type: string
+                      required:
+                      - client_name
+                      - site_name
+                      - fs_name
+                      type: object
+                  type: object
+                required:
+                - uuid
+                type: array
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Get peers
+      tags:
+      - CephfsMirror
+  /api/cephfs/mirror/{fs_name}/{peer_uuid}:
+    delete:
+      parameters:
+      - description: File system name
+        in: path
+        name: fs_name
+        required: true
+        schema:
+          type: string
+      - description: Peer UUID
+        in: path
+        name: peer_uuid
+        required: true
+        schema:
+          type: string
+      responses:
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '204':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource deleted.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Delete peer
+      tags:
+      - CephfsMirror
   /api/cephfs/remove/{name}:
     delete:
       parameters:
index e8c29239fbe8c689c5693d71e5aeec3dc191b62f..a4252f05d32089cdcc99cea4064d7767e5b808ac 100644 (file)
@@ -8,7 +8,7 @@ except ImportError:
     from unittest.mock import patch, Mock
 
 from .. import mgr
-from ..controllers.cephfs import CephFS, CephFSMirror
+from ..controllers.cephfs import CephFS, CephFSMirror, CephFSMirrorStatus
 from ..tests import ControllerTestCase
 
 
@@ -64,7 +64,7 @@ class CephFSMirrorTest(ControllerTestCase):
         mock_output = json.dumps(expected_peers)
         mgr.remote = Mock(return_value=(0, mock_output, ''))
 
-        self._get(f'/api/cephfs/mirror?fs_name={fs_name}')
+        self._get(f'/api/cephfs/mirror/{fs_name}')
         self.assertStatus(200)
         self.assertJsonBody(expected_peers)
         mgr.remote.assert_called_once_with('mirroring', 'snapshot_mirror_peer_list', fs_name)
@@ -74,13 +74,108 @@ class CephFSMirrorTest(ControllerTestCase):
         error_message = 'Failed to connect to remote'
         mgr.remote = Mock(return_value=(1, '', error_message))
 
-        self._get(f'/api/cephfs/mirror?fs_name={fs_name}')
+        self._get(f'/api/cephfs/mirror/{fs_name}')
         self.assertStatus(400)
         response = self.json_body()
         self.assertIn('Failed to get Cephfs mirror peers', response.get('detail', ''))
         self.assertIn(error_message, response.get('detail', ''))
         mgr.remote.assert_called_once_with('mirroring', 'snapshot_mirror_peer_list', fs_name)
 
+    def test_token_success(self):
+        fs_name = 'test_fs'
+        client_name = 'client.mirror'
+        site_name = 'remote-site'
+        expected_token = {'token': 'bootstrap-token-12345'}
+        mock_output = json.dumps(expected_token)
+        mgr.remote = Mock(return_value=(0, mock_output, ''))
+
+        self._post('/api/cephfs/mirror/token', {
+            'fs_name': fs_name,
+            'client_name': client_name,
+            'site_name': site_name
+        })
+        self.assertStatus(200)
+        self.assertJsonBody(expected_token)
+        mgr.remote.assert_called_once_with('mirroring', 'snapshot_mirror_peer_bootstrap_create',
+                                           fs_name, client_name, site_name)
+
+    def test_token_error(self):
+        fs_name = 'test_fs'
+        client_name = 'client.mirror'
+        site_name = 'remote-site'
+        error_message = 'Failed to create bootstrap token'
+        mgr.remote = Mock(return_value=(1, '', error_message))
+
+        self._post('/api/cephfs/mirror/token', {
+            'fs_name': fs_name,
+            'client_name': client_name,
+            'site_name': site_name
+        })
+        self.assertStatus(400)
+        response = self.json_body()
+        self.assertIn('Failed to create bootstrap token', response.get('detail', ''))
+        self.assertIn(error_message, response.get('detail', ''))
+        mgr.remote.assert_called_once_with('mirroring', 'snapshot_mirror_peer_bootstrap_create',
+                                           fs_name, client_name, site_name)
+
+    def test_create_success(self):
+        fs_name = 'test_fs'
+        token = 'bootstrap-token-12345'
+        expected_result = {'peer_uuid': 'peer-uuid-123'}
+        mock_output = json.dumps(expected_result)
+        mgr.remote = Mock(return_value=(0, mock_output, ''))
+
+        self._post('/api/cephfs/mirror', {
+            'fs_name': fs_name,
+            'token': token
+        })
+        self.assertStatus(201)
+        self.assertJsonBody(expected_result)
+        mgr.remote.assert_called_once_with('mirroring', 'snapshot_mirror_peer_bootstrap_import',
+                                           fs_name, token)
+
+    def test_import_token_error(self):
+        fs_name = 'test_fs'
+        token = 'invalid-token'
+        error_message = 'Invalid bootstrap token'
+        mgr.remote = Mock(return_value=(1, '', error_message))
+
+        self._post('/api/cephfs/mirror', {
+            'fs_name': fs_name,
+            'token': token
+        })
+        self.assertStatus(400)
+        response = self.json_body()
+        self.assertIn('Failed to import the token to create bootstrap peer',
+                      response.get('detail', ''))
+        self.assertIn(error_message, response.get('detail', ''))
+        mgr.remote.assert_called_once_with('mirroring', 'snapshot_mirror_peer_bootstrap_import',
+                                           fs_name, token)
+
+    def test_delete_success(self):
+        fs_name = 'test_fs'
+        peer_uuid = 'peer-uuid-123'
+        mgr.remote = Mock(return_value=(0, '', ''))
+
+        self._delete(f'/api/cephfs/mirror/{fs_name}/{peer_uuid}')
+        self.assertStatus(204)
+        mgr.remote.assert_called_once_with('mirroring', 'snapshot_mirror_peer_remove',
+                                           fs_name, peer_uuid)
+
+    def test_delete_error(self):
+        fs_name = 'test_fs'
+        peer_uuid = 'peer-uuid-123'
+        error_message = 'Peer not found'
+        mgr.remote = Mock(return_value=(1, '', error_message))
+
+        self._delete(f'/api/cephfs/mirror/{fs_name}/{peer_uuid}')
+        self.assertStatus(400)
+        response = self.json_body()
+        self.assertIn('Failed to delete peer', response.get('detail', ''))
+        self.assertIn(error_message, response.get('detail', ''))
+        mgr.remote.assert_called_once_with('mirroring', 'snapshot_mirror_peer_remove',
+                                           fs_name, peer_uuid)
+
     def test_daemon_status_success(self):
         expected_status = [
             {
@@ -126,3 +221,36 @@ class CephFSMirrorTest(ControllerTestCase):
         self.assertIn('Failed to get Cephfs mirror daemon status', response.get('detail', ''))
         self.assertIn(error_message, response.get('detail', ''))
         mgr.remote.assert_called_once_with('mirroring', 'snapshot_mirror_daemon_status')
+
+
+class CephFSMirrorStatusTest(ControllerTestCase):
+
+    @classmethod
+    def setup_server(cls):
+        cls.setup_controllers([CephFSMirrorStatus])
+
+    def test_status_available(self):
+        mgr.remote = Mock(return_value=(0, '', ''))
+
+        self._get('/ui-api/cephfs/mirror/status')
+        self.assertStatus(200)
+        response = self.json_body()
+        self.assertTrue(response.get('available'))
+        self.assertIsNone(response.get('message'))
+        mgr.remote.assert_called_once_with('mirroring', 'snapshot_mirror_daemon_status')
+
+    def test_status_unavailable_import_error(self):
+        with patch.object(mgr, 'remote', side_effect=ImportError('Module not found')):
+            self._get('/ui-api/cephfs/mirror/status')
+            self.assertStatus(200)
+            response = self.json_body()
+            self.assertFalse(response.get('available'))
+            self.assertIn('Cephfs mirror module is not enabled', response.get('message', ''))
+
+    def test_status_unavailable_runtime_error(self):
+        with patch.object(mgr, 'remote', side_effect=RuntimeError('Module error')):
+            self._get('/ui-api/cephfs/mirror/status')
+            self.assertStatus(200)
+            response = self.json_body()
+            self.assertFalse(response.get('available'))
+            self.assertIn('Cephfs mirror module is not enabled', response.get('message', ''))