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 = {
}, 'Peer ID'),
}]
+BOOTSTRAP_PEERS_SCHEMA = {
+ 'token': (str, 'Bootstrap token'),
+}
+
DAEMON_STATUS_SCHEMA = [{
'daemon_id': (int, 'Daemon ID'),
'filesystems': ([{
)
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')
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
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':
trace.
security:
- jwt: []
- summary: Get peers
+ summary: Create bootstrap peer
tags:
- CephfsMirror
/api/cephfs/mirror/daemon-status:
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:
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
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)
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 = [
{
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', ''))