summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/controllers/nfsganesha.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/controllers/nfsganesha.py')
-rw-r--r--src/pybind/mgr/dashboard/controllers/nfsganesha.py315
1 files changed, 315 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/controllers/nfsganesha.py b/src/pybind/mgr/dashboard/controllers/nfsganesha.py
new file mode 100644
index 00000000..b9599d72
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/nfsganesha.py
@@ -0,0 +1,315 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from functools import partial
+
+import cherrypy
+import cephfs
+
+from . import ApiController, RESTController, UiApiController, BaseController, \
+ Endpoint, Task, ReadPermission, ControllerDoc, EndpointDoc
+from .. import logger
+from ..security import Scope
+from ..services.cephfs import CephFS
+from ..services.cephx import CephX
+from ..services.exception import serialize_dashboard_exception
+from ..services.ganesha import Ganesha, GaneshaConf, NFSException
+from ..services.rgw_client import RgwClient
+
+
+# documentation helpers
+EXPORT_SCHEMA = {
+ 'export_id': (int, 'Export ID'),
+ 'path': (str, 'Export path'),
+ 'cluster_id': (str, 'Cluster identifier'),
+ 'daemons': ([str], 'List of NFS Ganesha daemons identifiers'),
+ 'pseudo': (str, 'Pseudo FS path'),
+ 'tag': (str, 'NFSv3 export tag'),
+ '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'),
+ 'user_id': (str, 'CephX user id', True),
+ 'filesystem': (str, 'CephFS filesystem ID', True),
+ 'sec_label_xattr': (str, 'Name of xattr for security label', True),
+ 'rgw_user_id': (str, 'RGW 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'),
+ 'daemons': ([str], 'List of NFS Ganesha daemons identifiers'),
+ 'pseudo': (str, 'Pseudo FS path'),
+ 'tag': (str, 'NFSv3 export tag'),
+ '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'),
+ 'user_id': (str, 'CephX user id', True),
+ 'filesystem': (str, 'CephFS filesystem ID', True),
+ 'sec_label_xattr': (str, 'Name of xattr for security label', True),
+ 'rgw_user_id': (str, 'RGW 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'),
+ 'reload_daemons': (bool,
+ 'Trigger reload of NFS-Ganesha daemons configuration',
+ True)
+}
+
+
+# pylint: disable=not-callable
+def NfsTask(name, metadata, wait_for):
+ def composed_decorator(func):
+ return Task("nfs/{}".format(name), metadata, wait_for,
+ partial(serialize_dashboard_exception,
+ include_http_status=True))(func)
+ return composed_decorator
+
+
+@ApiController('/nfs-ganesha', Scope.NFS_GANESHA)
+@ControllerDoc("NFS-Ganesha Management API", "NFS-Ganesha")
+class NFSGanesha(RESTController):
+
+ @EndpointDoc("Status of NFS-Ganesha management feature",
+ responses={200: {
+ 'available': (bool, "Is API available?"),
+ 'message': (str, "Error message")
+ }})
+ @Endpoint()
+ @ReadPermission
+ def status(self):
+ status = {'available': True, 'message': None}
+ try:
+ Ganesha.get_ganesha_clusters()
+ except NFSException as e:
+ status['message'] = str(e)
+ status['available'] = False
+
+ return status
+
+
+@ApiController('/nfs-ganesha/export', Scope.NFS_GANESHA)
+@ControllerDoc(group="NFS-Ganesha")
+class NFSGaneshaExports(RESTController):
+ RESOURCE_ID = "cluster_id/export_id"
+
+ @EndpointDoc("List all NFS-Ganesha exports",
+ responses={200: [EXPORT_SCHEMA]})
+ def list(self):
+ result = []
+ for cluster_id in Ganesha.get_ganesha_clusters():
+ result.extend(
+ [export.to_dict()
+ for export in GaneshaConf.instance(cluster_id).list_exports()])
+ return result
+
+ @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})
+ def create(self, path, cluster_id, daemons, pseudo, tag, access_type,
+ squash, security_label, protocols, transports, fsal, clients,
+ reload_daemons=True):
+ if fsal['name'] not in Ganesha.fsals_available():
+ raise NFSException("Cannot create this export. "
+ "FSAL '{}' cannot be managed by the dashboard."
+ .format(fsal['name']))
+
+ ganesha_conf = GaneshaConf.instance(cluster_id)
+ ex_id = ganesha_conf.create_export({
+ 'path': path,
+ 'pseudo': pseudo,
+ 'cluster_id': cluster_id,
+ 'daemons': daemons,
+ 'tag': tag,
+ 'access_type': access_type,
+ 'squash': squash,
+ 'security_label': security_label,
+ 'protocols': protocols,
+ 'transports': transports,
+ 'fsal': fsal,
+ 'clients': clients
+ })
+ if reload_daemons:
+ ganesha_conf.reload_daemons(daemons)
+ return ganesha_conf.get_export(ex_id).to_dict()
+
+ @EndpointDoc("Get an NFS-Ganesha export",
+ parameters={
+ 'cluster_id': (str, 'Cluster identifier'),
+ 'export_id': (int, "Export ID")
+ },
+ responses={200: EXPORT_SCHEMA})
+ def get(self, cluster_id, export_id):
+ export_id = int(export_id)
+ ganesha_conf = GaneshaConf.instance(cluster_id)
+ if not ganesha_conf.has_export(export_id):
+ raise cherrypy.HTTPError(404)
+ return ganesha_conf.get_export(export_id).to_dict()
+
+ @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})
+ def set(self, cluster_id, export_id, path, daemons, pseudo, tag, access_type,
+ squash, security_label, protocols, transports, fsal, clients,
+ reload_daemons=True):
+ export_id = int(export_id)
+ ganesha_conf = GaneshaConf.instance(cluster_id)
+
+ if not ganesha_conf.has_export(export_id):
+ raise cherrypy.HTTPError(404)
+
+ if fsal['name'] not in Ganesha.fsals_available():
+ raise NFSException("Cannot make modifications to this export. "
+ "FSAL '{}' cannot be managed by the dashboard."
+ .format(fsal['name']))
+
+ old_export = ganesha_conf.update_export({
+ 'export_id': export_id,
+ 'path': path,
+ 'cluster_id': cluster_id,
+ 'daemons': daemons,
+ 'pseudo': pseudo,
+ 'tag': tag,
+ 'access_type': access_type,
+ 'squash': squash,
+ 'security_label': security_label,
+ 'protocols': protocols,
+ 'transports': transports,
+ 'fsal': fsal,
+ 'clients': clients
+ })
+ daemons = list(daemons)
+ for d_id in old_export.daemons:
+ if d_id not in daemons:
+ daemons.append(d_id)
+ if reload_daemons:
+ ganesha_conf.reload_daemons(daemons)
+ return ganesha_conf.get_export(export_id).to_dict()
+
+ @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"),
+ 'reload_daemons': (bool,
+ 'Trigger reload of NFS-Ganesha daemons'
+ ' configuration',
+ True)
+ })
+ def delete(self, cluster_id, export_id, reload_daemons=True):
+ export_id = int(export_id)
+ ganesha_conf = GaneshaConf.instance(cluster_id)
+
+ if not ganesha_conf.has_export(export_id):
+ raise cherrypy.HTTPError(404)
+
+ export = ganesha_conf.remove_export(export_id)
+ if reload_daemons:
+ ganesha_conf.reload_daemons(export.daemons)
+
+
+@ApiController('/nfs-ganesha/daemon', Scope.NFS_GANESHA)
+@ControllerDoc(group="NFS-Ganesha")
+class NFSGaneshaService(RESTController):
+
+ @EndpointDoc("List NFS-Ganesha daemons information",
+ responses={200: [{
+ 'daemon_id': (str, 'Daemon identifier'),
+ 'cluster_id': (str, 'Cluster identifier'),
+ 'status': (int,
+ 'Status of daemon (1=RUNNING, 0=STOPPED, -1=ERROR',
+ True),
+ 'desc': (str, 'Error description (if status==-1)', True)
+ }]})
+ def list(self):
+ status_dict = Ganesha.get_daemons_status()
+ if status_dict:
+ return [
+ {
+ 'daemon_id': daemon_id,
+ 'cluster_id': cluster_id,
+ 'status': status_dict[cluster_id][daemon_id]['status'],
+ 'desc': status_dict[cluster_id][daemon_id]['desc']
+ }
+ for cluster_id in status_dict
+ for daemon_id in status_dict[cluster_id]
+ ]
+
+ result = []
+ for cluster_id in Ganesha.get_ganesha_clusters():
+ result.extend(
+ [{'daemon_id': daemon_id, 'cluster_id': cluster_id}
+ for daemon_id in GaneshaConf.instance(cluster_id).list_daemons()])
+ return result
+
+
+@UiApiController('/nfs-ganesha', Scope.NFS_GANESHA)
+class NFSGaneshaUi(BaseController):
+ @Endpoint('GET', '/cephx/clients')
+ @ReadPermission
+ def cephx_clients(self):
+ return [client for client in CephX.list_clients()]
+
+ @Endpoint('GET', '/fsals')
+ @ReadPermission
+ def fsals(self):
+ return Ganesha.fsals_available()
+
+ @Endpoint('GET', '/lsdir')
+ @ReadPermission
+ def lsdir(self, root_dir=None, depth=1): # pragma: no cover
+ if root_dir is None:
+ root_dir = "/"
+ depth = int(depth)
+ if depth > 5:
+ logger.warning("[NFS] Limiting depth to maximum value of 5: "
+ "input depth=%s", depth)
+ depth = 5
+ root_dir = '{}/'.format(root_dir) \
+ if not root_dir.endswith('/') else root_dir
+
+ try:
+ cfs = CephFS()
+ paths = cfs.get_dir_list(root_dir, depth)
+ paths = [p[:-1] for p in paths if p != root_dir]
+ return {'paths': paths}
+ except (cephfs.ObjectNotFound, cephfs.PermissionError):
+ return {'paths': []}
+
+ @Endpoint('GET', '/cephfs/filesystems')
+ @ReadPermission
+ def filesystems(self):
+ return CephFS.list_filesystems()
+
+ @Endpoint('GET', '/rgw/buckets')
+ @ReadPermission
+ def buckets(self, user_id=None):
+ return RgwClient.instance(user_id).get_buckets()
+
+ @Endpoint('GET', '/clusters')
+ @ReadPermission
+ def clusters(self):
+ return Ganesha.get_ganesha_clusters()