summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/controllers/nfs.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/controllers/nfs.py')
-rw-r--r--src/pybind/mgr/dashboard/controllers/nfs.py280
1 files changed, 280 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/controllers/nfs.py b/src/pybind/mgr/dashboard/controllers/nfs.py
new file mode 100644
index 000000000..c5ab05de7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/nfs.py
@@ -0,0 +1,280 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import json
+import logging
+import os
+from functools import partial
+from typing import Any, Dict, List, Optional
+
+import cephfs
+from mgr_module import NFS_GANESHA_SUPPORTED_FSALS
+
+from .. import mgr
+from ..security import Scope
+from ..services.cephfs import CephFS
+from ..services.exception import DashboardException, handle_cephfs_error, \
+ serialize_dashboard_exception
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ ReadPermission, RESTController, Task, UIRouter
+from ._version import APIVersion
+
+logger = logging.getLogger('controllers.nfs')
+
+
+class NFSException(DashboardException):
+ def __init__(self, msg):
+ super(NFSException, self).__init__(component="nfs", msg=msg)
+
+
+# documentation helpers
+EXPORT_SCHEMA = {
+ 'export_id': (int, 'Export ID'),
+ 'path': (str, 'Export path'),
+ 'cluster_id': (str, 'Cluster identifier'),
+ 'pseudo': (str, 'Pseudo FS path'),
+ 'access_type': (str, 'Export access type'),
+ 'squash': (str, 'Export squash policy'),
+ 'security_label': (str, 'Security label'),
+ 'protocols': ([int], 'List of protocol types'),
+ 'transports': ([str], 'List of transport types'),
+ 'fsal': ({
+ 'name': (str, 'name of FSAL'),
+ 'fs_name': (str, 'CephFS filesystem name', True),
+ 'sec_label_xattr': (str, 'Name of xattr for security label', True),
+ 'user_id': (str, 'User id', True)
+ }, 'FSAL configuration'),
+ 'clients': ([{
+ 'addresses': ([str], 'list of IP addresses'),
+ 'access_type': (str, 'Client access type'),
+ 'squash': (str, 'Client squash policy')
+ }], 'List of client configurations'),
+}
+
+
+CREATE_EXPORT_SCHEMA = {
+ 'path': (str, 'Export path'),
+ 'cluster_id': (str, 'Cluster identifier'),
+ 'pseudo': (str, 'Pseudo FS path'),
+ 'access_type': (str, 'Export access type'),
+ 'squash': (str, 'Export squash policy'),
+ 'security_label': (str, 'Security label'),
+ 'protocols': ([int], 'List of protocol types'),
+ 'transports': ([str], 'List of transport types'),
+ 'fsal': ({
+ 'name': (str, 'name of FSAL'),
+ 'fs_name': (str, 'CephFS filesystem name', True),
+ 'sec_label_xattr': (str, 'Name of xattr for security label', True)
+ }, 'FSAL configuration'),
+ 'clients': ([{
+ 'addresses': ([str], 'list of IP addresses'),
+ 'access_type': (str, 'Client access type'),
+ 'squash': (str, 'Client squash policy')
+ }], 'List of client configurations')
+}
+
+
+# pylint: disable=not-callable
+def NfsTask(name, metadata, wait_for): # noqa: N802
+ def composed_decorator(func):
+ return Task("nfs/{}".format(name), metadata, wait_for,
+ partial(serialize_dashboard_exception,
+ include_http_status=True))(func)
+ return composed_decorator
+
+
+@APIRouter('/nfs-ganesha/cluster', Scope.NFS_GANESHA)
+@APIDoc("NFS-Ganesha Cluster Management API", "NFS-Ganesha")
+class NFSGaneshaCluster(RESTController):
+ @ReadPermission
+ @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+ def list(self):
+ return mgr.remote('nfs', 'cluster_ls')
+
+
+@APIRouter('/nfs-ganesha/export', Scope.NFS_GANESHA)
+@APIDoc(group="NFS-Ganesha")
+class NFSGaneshaExports(RESTController):
+ RESOURCE_ID = "cluster_id/export_id"
+
+ @staticmethod
+ def _get_schema_export(export: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ Method that avoids returning export info not exposed in the export schema
+ e.g., rgw user access/secret keys.
+ """
+ schema_fsal_info = {}
+ for key in export['fsal'].keys():
+ if key in EXPORT_SCHEMA['fsal'][0].keys(): # type: ignore
+ schema_fsal_info[key] = export['fsal'][key]
+ export['fsal'] = schema_fsal_info
+ return export
+
+ @EndpointDoc("List all NFS-Ganesha exports",
+ responses={200: [EXPORT_SCHEMA]})
+ def list(self) -> List[Dict[str, Any]]:
+ exports = []
+ for export in mgr.remote('nfs', 'export_ls'):
+ exports.append(self._get_schema_export(export))
+
+ return exports
+
+ @handle_cephfs_error()
+ @NfsTask('create', {'path': '{path}', 'fsal': '{fsal.name}',
+ 'cluster_id': '{cluster_id}'}, 2.0)
+ @EndpointDoc("Creates a new NFS-Ganesha export",
+ parameters=CREATE_EXPORT_SCHEMA,
+ responses={201: EXPORT_SCHEMA})
+ @RESTController.MethodMap(version=APIVersion(2, 0)) # type: ignore
+ def create(self, path, cluster_id, pseudo, access_type,
+ squash, security_label, protocols, transports, fsal, clients) -> Dict[str, Any]:
+ export_mgr = mgr.remote('nfs', 'fetch_nfs_export_obj')
+ if export_mgr.get_export_by_pseudo(cluster_id, pseudo):
+ raise DashboardException(msg=f'Pseudo {pseudo} is already in use.',
+ component='nfs')
+ if hasattr(fsal, 'user_id'):
+ fsal.pop('user_id') # mgr/nfs does not let you customize user_id
+ raw_ex = {
+ 'path': path,
+ 'pseudo': pseudo,
+ 'cluster_id': cluster_id,
+ 'access_type': access_type,
+ 'squash': squash,
+ 'security_label': security_label,
+ 'protocols': protocols,
+ 'transports': transports,
+ 'fsal': fsal,
+ 'clients': clients
+ }
+ ret, _, err = export_mgr.apply_export(cluster_id, json.dumps(raw_ex))
+ if ret == 0:
+ return self._get_schema_export(
+ export_mgr.get_export_by_pseudo(cluster_id, pseudo))
+ raise NFSException(f"Export creation failed {err}")
+
+ @EndpointDoc("Get an NFS-Ganesha export",
+ parameters={
+ 'cluster_id': (str, 'Cluster identifier'),
+ 'export_id': (str, "Export ID")
+ },
+ responses={200: EXPORT_SCHEMA})
+ def get(self, cluster_id, export_id) -> Optional[Dict[str, Any]]:
+ export_id = int(export_id)
+ export = mgr.remote('nfs', 'export_get', cluster_id, export_id)
+ if export:
+ export = self._get_schema_export(export)
+
+ return export
+
+ @NfsTask('edit', {'cluster_id': '{cluster_id}', 'export_id': '{export_id}'},
+ 2.0)
+ @EndpointDoc("Updates an NFS-Ganesha export",
+ parameters=dict(export_id=(int, "Export ID"),
+ **CREATE_EXPORT_SCHEMA),
+ responses={200: EXPORT_SCHEMA})
+ @RESTController.MethodMap(version=APIVersion(2, 0)) # type: ignore
+ def set(self, cluster_id, export_id, path, pseudo, access_type,
+ squash, security_label, protocols, transports, fsal, clients) -> Dict[str, Any]:
+
+ if hasattr(fsal, 'user_id'):
+ fsal.pop('user_id') # mgr/nfs does not let you customize user_id
+ raw_ex = {
+ 'path': path,
+ 'pseudo': pseudo,
+ 'cluster_id': cluster_id,
+ 'export_id': export_id,
+ 'access_type': access_type,
+ 'squash': squash,
+ 'security_label': security_label,
+ 'protocols': protocols,
+ 'transports': transports,
+ 'fsal': fsal,
+ 'clients': clients
+ }
+
+ export_mgr = mgr.remote('nfs', 'fetch_nfs_export_obj')
+ ret, _, err = export_mgr.apply_export(cluster_id, json.dumps(raw_ex))
+ if ret == 0:
+ return self._get_schema_export(
+ export_mgr.get_export_by_pseudo(cluster_id, pseudo))
+ raise NFSException(f"Failed to update export: {err}")
+
+ @NfsTask('delete', {'cluster_id': '{cluster_id}',
+ 'export_id': '{export_id}'}, 2.0)
+ @EndpointDoc("Deletes an NFS-Ganesha export",
+ parameters={
+ 'cluster_id': (str, 'Cluster identifier'),
+ 'export_id': (int, "Export ID")
+ })
+ @RESTController.MethodMap(version=APIVersion(2, 0)) # type: ignore
+ def delete(self, cluster_id, export_id):
+ export_id = int(export_id)
+
+ export = mgr.remote('nfs', 'export_get', cluster_id, export_id)
+ if not export:
+ raise DashboardException(
+ http_status_code=404,
+ msg=f'Export with id {export_id} not found.',
+ component='nfs')
+ mgr.remote('nfs', 'export_rm', cluster_id, export['pseudo'])
+
+
+@UIRouter('/nfs-ganesha', Scope.NFS_GANESHA)
+class NFSGaneshaUi(BaseController):
+ @Endpoint('GET', '/fsals')
+ @ReadPermission
+ def fsals(self):
+ return NFS_GANESHA_SUPPORTED_FSALS
+
+ @Endpoint('GET', '/lsdir')
+ @ReadPermission
+ def lsdir(self, fs_name, root_dir=None, depth=1): # pragma: no cover
+ if root_dir is None:
+ root_dir = "/"
+ if not root_dir.startswith('/'):
+ root_dir = '/{}'.format(root_dir)
+ root_dir = os.path.normpath(root_dir)
+
+ try:
+ depth = int(depth)
+ error_msg = ''
+ if depth < 0:
+ error_msg = '`depth` must be greater or equal to 0.'
+ if depth > 5:
+ logger.warning("Limiting depth to maximum value of 5: "
+ "input depth=%s", depth)
+ depth = 5
+ except ValueError:
+ error_msg = '`depth` must be an integer.'
+ finally:
+ if error_msg:
+ raise DashboardException(code=400,
+ component='nfs',
+ msg=error_msg)
+
+ try:
+ cfs = CephFS(fs_name)
+ paths = [root_dir]
+ paths.extend([p['path'].rstrip('/')
+ for p in cfs.ls_dir(root_dir, depth)])
+ except (cephfs.ObjectNotFound, cephfs.PermissionError):
+ paths = []
+ return {'paths': paths}
+
+ @Endpoint('GET', '/cephfs/filesystems')
+ @ReadPermission
+ def filesystems(self):
+ return CephFS.list_filesystems()
+
+ @Endpoint()
+ @ReadPermission
+ def status(self):
+ status = {'available': True, 'message': None}
+ try:
+ mgr.remote('nfs', 'cluster_ls')
+ except (ImportError, RuntimeError) as error:
+ logger.exception(error)
+ status['available'] = False
+ status['message'] = str(error) # type: ignore
+
+ return status