From fbbbaa123a720c7abab72a42bc4762c6eef606af Mon Sep 17 00:00:00 2001 From: Pedro Gonzalez Gomez Date: Thu, 20 Nov 2025 15:09:03 +0100 Subject: [PATCH] mgr/dashboard: add GET endpoint for CephFS mirror peers list and daemon status Fixes: https://tracker.ceph.com/issues/74002 Signed-off-by: Pedro Gonzalez Gomez --- .../mgr/dashboard/controllers/cephfs.py | 63 ++++++++ src/pybind/mgr/dashboard/openapi.yaml | 150 ++++++++++++++++++ src/pybind/mgr/dashboard/security.py | 1 + src/pybind/mgr/dashboard/tests/test_cephfs.py | 88 +++++++++- 4 files changed, 301 insertions(+), 1 deletion(-) diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index 29292329aaf..11bbea4d7bc 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -30,6 +30,35 @@ GET_STATFS_SCHEMA = { 'subdirs': (int, '') } +LIST_PEERS_SCHEMA = [{ + 'uuid': ({ + 'client_name': (str, 'Ceph client name'), + 'site_name': (str, 'Remote site name'), + 'fs_name': (str, 'File system name'), + }, 'Peer ID'), +}] + +DAEMON_STATUS_SCHEMA = [{ + 'daemon_id': (int, 'Daemon ID'), + 'filesystems': ([{ + 'filesystem_id': (int, 'Filesystem ID'), + 'name': (str, 'Filesystem name'), + 'directory_count': (int, 'Directory count'), + 'peers': ([{ + 'uuid': (str, 'Peer UUID'), + 'remote': ({ + 'client_name': (str, 'Ceph client name'), + 'cluster_name': (str, 'Remote cluster name'), + 'fs_name': (str, 'Remote filesystem name'), + }, 'Remote peer information'), + 'stats': ({ + 'failure_count': (int, 'Number of sync failures'), + 'recovery_count': (int, 'Number of peer recoveries'), + }, 'Peer statistics'), + }], 'List of peer objects'), + }], 'List of filesystems on daemon'), +}] + # pylint: disable=R0904 @APIRouter('/cephfs', Scope.CEPHFS) @@ -1206,3 +1235,37 @@ class CephFSSnapshotSchedule(RESTController): ) return f'Snapshot schedule for path {path} activated successfully' + + +@APIRouter('/cephfs/mirror', Scope.CEPHFS_MIRROR) +@APIDoc("Cephfs Mirror Management API", "CephfsMirror") +class CephFSMirror(RESTController): + + @EndpointDoc("Get peers", + parameters={ + 'fs_name': (str, 'File system name'), + }, + responses={200: LIST_PEERS_SCHEMA}) + def list(self, fs_name: str): + error_code, out, err = mgr.remote('mirroring', 'snapshot_mirror_peer_list', fs_name) + if error_code != 0: + raise DashboardException( + msg=f'Failed to get Cephfs mirror peers: {err}', + code=error_code, + component='cephfs.mirror' + ) + return json.loads(out) + + @EndpointDoc("Get mirror daemon and peers information", + responses={200: DAEMON_STATUS_SCHEMA}) + @Endpoint('GET', path='/daemon-status') + @ReadPermission + def daemon_status(self): + error_code, out, err = mgr.remote('mirroring', 'snapshot_mirror_daemon_status') + if error_code != 0: + raise DashboardException( + msg=f'Failed to get Cephfs mirror daemon status: {err}', + code=error_code, + component='cephfs.mirror' + ) + return json.loads(out) diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index ca0163d98f6..20e76934bbd 100755 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -1898,6 +1898,154 @@ paths: given path tags: - Cephfs + /api/cephfs/mirror: + get: + parameters: + - description: File system name + in: query + 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/daemon-status: + get: + parameters: [] + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + schema: + items: + properties: + daemon_id: + description: Daemon ID + type: integer + filesystems: + description: List of filesystems on daemon + items: + properties: + directory_count: + description: Directory count + type: integer + filesystem_id: + description: Filesystem ID + type: integer + name: + description: Filesystem name + type: string + peers: + description: List of peer objects + items: + properties: + remote: + description: Remote peer information + properties: + client_name: + description: Ceph client name + type: string + cluster_name: + description: Remote cluster name + type: string + fs_name: + description: Remote filesystem name + type: string + required: + - client_name + - cluster_name + - fs_name + type: object + stats: + description: Peer statistics + properties: + failure_count: + description: Number of sync failures + type: integer + recovery_count: + description: Number of peer recoveries + type: integer + required: + - failure_count + - recovery_count + type: object + uuid: + description: Peer UUID + type: string + required: + - uuid + - remote + - stats + type: object + type: array + required: + - filesystem_id + - name + - directory_count + - peers + type: object + type: array + type: object + required: + - daemon_id + - filesystems + 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 mirror daemon and peers information + tags: + - CephfsMirror /api/cephfs/remove/{name}: delete: parameters: @@ -19313,6 +19461,8 @@ tags: name: CephFSSubvolume - description: Cephfs Management API name: Cephfs +- description: Cephfs Mirror Management API + name: CephfsMirror - description: Cephfs Snapshot Clone Management API name: CephfsSnapshotClone - description: Cephfs Subvolume Group Management API diff --git a/src/pybind/mgr/dashboard/security.py b/src/pybind/mgr/dashboard/security.py index c329d24e1b3..8a7b3dced61 100644 --- a/src/pybind/mgr/dashboard/security.py +++ b/src/pybind/mgr/dashboard/security.py @@ -19,6 +19,7 @@ class Scope(object): RBD_MIRRORING = "rbd-mirroring" RGW = "rgw" CEPHFS = "cephfs" + CEPHFS_MIRROR = "cephfs-mirror" MANAGER = "manager" LOG = "log" GRAFANA = "grafana" diff --git a/src/pybind/mgr/dashboard/tests/test_cephfs.py b/src/pybind/mgr/dashboard/tests/test_cephfs.py index ae425354384..e8c29239fbe 100644 --- a/src/pybind/mgr/dashboard/tests/test_cephfs.py +++ b/src/pybind/mgr/dashboard/tests/test_cephfs.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import json from collections import defaultdict try: @@ -6,7 +7,8 @@ try: except ImportError: from unittest.mock import patch, Mock -from ..controllers.cephfs import CephFS +from .. import mgr +from ..controllers.cephfs import CephFS, CephFSMirror from ..tests import ControllerTestCase @@ -40,3 +42,87 @@ class CephFsTest(ControllerTestCase): self.cephFs._append_mds_metadata(mds_versions, 'foo') self.assertEqual(len(mds_versions), 1) self.assertEqual(mds_versions['bar'], ['foo']) + + +class CephFSMirrorTest(ControllerTestCase): + + @classmethod + def setup_server(cls): + cls.setup_controllers([CephFSMirror]) + + def test_list_success(self): + fs_name = 'test_fs' + expected_peers = [ + { + 'uuid': { + 'client_name': 'client.mirror', + 'site_name': 'remote-site', + 'fs_name': 'test_fs' + } + } + ] + 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.assertStatus(200) + self.assertJsonBody(expected_peers) + mgr.remote.assert_called_once_with('mirroring', 'snapshot_mirror_peer_list', fs_name) + + def test_list_error(self): + fs_name = 'test_fs' + 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.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_daemon_status_success(self): + expected_status = [ + { + 'daemon_id': 1, + 'filesystems': [ + { + 'filesystem_id': 1, + 'name': 'test_fs', + 'directory_count': 5, + 'peers': [ + { + 'uuid': 'peer-uuid-123', + 'remote': { + 'client_name': 'client.mirror', + 'cluster_name': 'remote-cluster', + 'fs_name': 'remote_fs' + }, + 'stats': { + 'failure_count': 0, + 'recovery_count': 1 + } + } + ] + } + ] + } + ] + mock_output = json.dumps(expected_status) + mgr.remote = Mock(return_value=(0, mock_output, '')) + + self._get('/api/cephfs/mirror/daemon-status') + self.assertStatus(200) + self.assertJsonBody(expected_status) + mgr.remote.assert_called_once_with('mirroring', 'snapshot_mirror_daemon_status') + + def test_daemon_status_error(self): + error_message = 'Daemon not available' + mgr.remote = Mock(return_value=(1, '', error_message)) + + self._get('/api/cephfs/mirror/daemon-status') + self.assertStatus(400) + response = self.json_body() + 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') -- 2.47.3