]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/volumes/nfs: Add command to update cephfs exports
authorVarsha Rao <varao@redhat.com>
Thu, 14 Jan 2021 21:45:13 +0000 (03:15 +0530)
committerVarsha Rao <varao@redhat.com>
Thu, 4 Mar 2021 09:48:07 +0000 (15:18 +0530)
First fetch the export with
$ ceph nfs export get <clusterid> <binding>
then modify this export and pass it to update command
$ ceph nfs export update -i <json_file>

Fixes: https://tracker.ceph.com/issues/45746
Signed-off-by: Varsha Rao <varao@redhat.com>
src/pybind/mgr/volumes/fs/nfs.py
src/pybind/mgr/volumes/module.py

index b9a16c974fd37f40a9795d6ab37de0226a0b41f0..f49e7bf1d023dcede3a9283c5fae2fca91244580 100644 (file)
@@ -6,7 +6,7 @@ import socket
 from os.path import isabs, normpath
 
 from ceph.deployment.service_spec import NFSServiceSpec, PlacementSpec
-from rados import TimedOut
+from rados import TimedOut, ObjectNotFound
 
 import orchestrator
 
@@ -52,6 +52,14 @@ def cluster_setter(func):
     return set_pool_ns_clusterid
 
 
+class FSExportError(Exception):
+    def __init__(self, err_msg, errno=-errno.EINVAL):
+        self.errno = errno
+        self.err_msg = err_msg
+
+    def __str__(self):
+        return self.err_msg
+
 class GaneshaConfParser(object):
     def __init__(self, raw_config):
         self.pos = 0
@@ -322,6 +330,15 @@ class NFSRados:
             FSExport._check_rados_notify(ioctx, config_obj)
             log.debug(f"Added {obj} url to {config_obj}")
 
+    def update_obj(self, conf_block, obj, config_obj):
+        with self.mgr.rados.open_ioctx(self.pool) as ioctx:
+            ioctx.set_namespace(self.namespace)
+            ioctx.write_full(obj, conf_block.encode('utf-8'))
+            log.debug("write configuration into rados object "
+                      f"{self.pool}/{self.namespace}/{obj}:\n{conf_block}")
+            FSExport._check_rados_notify(ioctx, config_obj)
+            log.debug(f"Update export {obj} in {config_obj}")
+
     def remove_obj(self, obj, config_obj):
         with self.mgr.rados.open_ioctx(self.pool) as ioctx:
             ioctx.set_namespace(self.namespace)
@@ -350,19 +367,19 @@ class NFSRados:
 
 class Export(object):
     # pylint: disable=R0902
-    def __init__(self, export_id, path, fsal, cluster_id, pseudo,
-                 access_type='R', clients=None):
+    def __init__(self, export_id, path, cluster_id, pseudo, access_type, squash, security_label,
+            protocols, transports, fsal, clients=None):
         self.export_id = export_id
         self.path = path
         self.fsal = fsal
         self.cluster_id = cluster_id
         self.pseudo = pseudo
         self.access_type = access_type
-        self.squash = 'no_root_squash'
+        self.squash = squash
         self.attr_expiration_time = 0
-        self.security_label = True
-        self.protocols = [4]
-        self.transports = ["TCP"]
+        self.security_label = security_label
+        self.protocols = protocols
+        self.transports = transports
         self.clients = clients
 
     @classmethod
@@ -377,10 +394,14 @@ class Export(object):
 
         return cls(export_block['export_id'],
                    export_block['path'],
-                   CephFSFSal.from_fsal_block(fsal_block[0]),
                    cluster_id,
                    export_block['pseudo'],
                    export_block['access_type'],
+                   export_block['squash'],
+                   export_block['security_label'],
+                   export_block['protocols'],
+                   export_block['transports'],
+                   CephFSFSal.from_fsal_block(fsal_block[0]),
                    [Client.from_client_block(client)
                     for client in client_blocks])
 
@@ -407,10 +428,14 @@ class Export(object):
     def from_dict(cls, export_id, ex_dict):
         return cls(export_id,
                    ex_dict['path'],
-                   CephFSFSal.from_dict(ex_dict['fsal']),
                    ex_dict['cluster_id'],
                    ex_dict['pseudo'],
-                   ex_dict['access_type'],
+                   ex_dict.get('access_type', 'R'),
+                   ex_dict.get('squash', 'no_root_squash'),
+                   ex_dict.get('security_label', True),
+                   ex_dict.get('protocols', [4]),
+                   ex_dict.get('transports', ['TCP']),
+                   CephFSFSal.from_dict(ex_dict['fsal']),
                    [Client.from_dict(client) for client in ex_dict['clients']])
 
     def to_dict(self):
@@ -555,9 +580,7 @@ class FSExport(object):
                 return -errno.ENOENT, "", f"filesystem {fs_name} not found"
 
             pseudo_path = self.format_path(pseudo_path)
-            if not isabs(pseudo_path) or pseudo_path == "/":
-                return -errno.EINVAL, "", f"pseudo path {pseudo_path} is invalid. "\
-                        "It should be an absolute path and it cannot be just '/'."
+            self._validate_pseudo_path(pseudo_path)
 
             if cluster_id not in self.exports:
                 self.exports[cluster_id] = []
@@ -592,7 +615,7 @@ class FSExport(object):
             return 0, "", "Export already exists"
         except Exception as e:
             log.exception(f"Failed to create {pseudo_path} export for {cluster_id}")
-            return -errno.EINVAL, "", str(e)
+            return getattr(e, 'errno', -1), "", str(e)
 
     @export_cluster_checker
     def delete_export(self, cluster_id, pseudo_path):
@@ -639,6 +662,164 @@ class FSExport(object):
             log.exception(f"Failed to get {pseudo_path} export for {cluster_id}")
             return getattr(e, 'errno', -1), "", str(e)
 
+    def _validate_pseudo_path(self, path):
+        if not isabs(path) or path == "/":
+            raise FSExportError(f"pseudo path {path} is invalid. "\
+                    "It should be an absolute path and it cannot be just '/'.")
+
+    def _validate_squash(self, squash):
+        valid_squash_ls = ["root", "root_squash", "rootsquash", "rootid", "root_id_squash",
+                "rootidsquash", "all", "all_squash", "allsquash", "all_anomnymous", "allanonymous",
+                "no_root_squash", "none", "noidsquash"]
+        if squash not in valid_squash_ls:
+            raise FSExportError(f"squash {squash} not in valid list {valid_squash_ls}")
+
+    def _validate_security_label(self, label):
+        if not isinstance(label, bool):
+            raise FSExportError('Only boolean values allowed')
+
+    def _validate_protocols(self, proto):
+        for p in proto:
+            if p not in [3, 4]:
+                raise FSExportError(f"Invalid protocol {p}")
+        if 3 in proto:
+            log.warning("NFS V3 is an old version, it might not work")
+
+    def _validate_transport(self, transport):
+        valid_transport = ["UDP", "TCP"]
+        for trans in transport:
+            if trans.upper() not in valid_transport:
+                raise FSExportError(f'{trans} is not a valid transport protocol')
+
+    def _validate_access_type(self, access_type):
+        valid_ones = ['RW', 'RO']
+        if access_type not in valid_ones:
+            raise FSExportError(f'{access_type} is invalid, valid access type are {valid_ones}')
+
+    def _validate_fsal(self, old, new):
+        if old.name != new['name']:
+            raise FSExportError('FSAL name change not allowed')
+        if old.user_id != new['user_id']:
+            raise FSExportError('User ID modification is not allowed')
+        if new['sec_label_xattr']:
+            raise FSExportError('Security label xattr cannot be changed')
+        if old.fs_name != new['fs_name']:
+            if not self.check_fs(new['fs_name']):
+                raise FSExportError(f"filesystem {new['fs_name']} not found")
+            return 1
+
+    def _validate_client(self, client):
+        self._validate_access_type(client['access_type'])
+        self._validate_squash(client['squash'])
+
+    def _validate_clients(self, clients_ls):
+        for client in clients_ls:
+            self._validate_client(client)
+
+    def _fetch_export_obj(self, ex_id):
+        try:
+            with self.mgr.rados.open_ioctx(self.rados_pool) as ioctx:
+                ioctx.set_namespace(self.rados_namespace)
+                export =  Export.from_export_block(GaneshaConfParser(ioctx.read(f"export-{ex_id}"
+                    ).decode("utf-8")).parse()[0], self.rados_namespace)
+                return export
+        except ObjectNotFound:
+            log.exception(f"Export ID: {ex_id} not found")
+
+    def _validate_export(self, new_export_dict):
+        if new_export_dict['cluster_id'] not in available_clusters(self.mgr):
+            raise FSExportError(f"Cluster {new_export_dict['cluster_id']} does not exists",
+                    -errno.ENOENT)
+        export = self._fetch_export(new_export_dict['pseudo'])
+        out_msg = ''
+        if export:
+            # Check if export id matches
+            if export.export_id != new_export_dict['export_id']:
+                raise FSExportError('Export ID changed, Cannot update export')
+        else:
+            # Fetch export based on export id object
+            export = self._fetch_export_obj(new_export_dict['export_id'])
+            if not export:
+                raise FSExportError('Export does not exist')
+            else:
+                new_export_dict['pseudo'] = self.format_path(new_export_dict['pseudo'])
+                self._validate_pseudo_path(new_export_dict['pseudo'])
+                log.debug(f"Pseudo path has changed from {export.pseudo} to "\
+                          f"{new_export_dict['pseudo']}")
+        # Check if squash changed
+        if export.squash != new_export_dict['squash']:
+            if new_export_dict['squash']:
+                new_export_dict['squash'] = new_export_dict['squash'].lower()
+                self._validate_squash(new_export_dict['squash'])
+            log.debug(f"squash has changed from {export.squash} to {new_export_dict['squash']}")
+        # Security label check
+        if export.security_label != new_export_dict['security_label']:
+            self._validate_security_label(new_export_dict['security_label'])
+        # Protocol Checking
+        if export.protocols != new_export_dict['protocols']:
+            self._validate_protocols(new_export_dict['protocols'])
+        # Transport checking
+        if export.transports != new_export_dict['transports']:
+            self._validate_transport(new_export_dict['transports'])
+        # Path check
+        if export.path != new_export_dict['path']:
+            new_export_dict['path'] = self.format_path(new_export_dict['path'])
+            out_msg = 'update caps'
+        # Check Access Type
+        if export.access_type != new_export_dict['access_type']:
+            self._validate_access_type(new_export_dict['access_type'])
+        # Fsal block check
+        if export.fsal != new_export_dict['fsal']:
+            ret = self._validate_fsal(export.fsal, new_export_dict['fsal'])
+            if ret == 1 and not out_msg:
+                out_msg = 'update caps'
+        # Check client block
+        if export.clients != new_export_dict['clients']:
+            self._validate_clients(new_export_dict['clients'])
+        log.debug(f'Validation succeeded for Export {export.pseudo}')
+        return export, out_msg
+
+    def _update_user_id(self, path, access_type, fs_name, user_id):
+        osd_cap = 'allow rw pool={} namespace={}, allow rw tag cephfs data={}'.format(
+                self.rados_pool, self.rados_namespace, fs_name)
+        access_type = 'r' if access_type == 'RO' else 'rw'
+
+        self.mgr.check_mon_command({
+            'prefix': 'auth caps',
+            'entity': f'client.{user_id}',
+            'caps': ['mon', 'allow r', 'osd', osd_cap, 'mds', 'allow {} path={}'.format(
+                access_type, path)],
+            })
+
+        log.info(f"Export user updated {user_id}")
+
+    def _update_export(self, export):
+        self.exports[self.rados_namespace].append(export)
+        NFSRados(self.mgr, self.rados_namespace).update_obj(
+                GaneshaConfParser.write_block(export.to_export_block()),
+                f'export-{export.export_id}', f'conf-nfs.{export.cluster_id}')
+
+    def update_export(self, export_config):
+        try:
+            if not export_config:
+                return -errno.EINVAL, "", "Empty Config!!"
+            update_export = json.loads(export_config)
+            old_export, update_user_caps = self._validate_export(update_export)
+            if update_user_caps:
+                self._update_user_id(update_export['path'], update_export['access_type'],
+                        update_export['fsal']['fs_name'], update_export['fsal']['user_id'])
+            update_export = Export.from_dict(update_export['export_id'], update_export)
+            update_export.fsal.cephx_key = old_export.fsal.cephx_key
+            self._update_export(update_export)
+            export_ls = self.exports[self.rados_namespace]
+            if old_export not in export_ls:
+                # This happens when export is fetched by ID
+                old_export = self._fetch_export(old_export.pseudo)
+            export_ls.remove(old_export)
+            return 0, "Successfully updated export", ""
+        except Exception as e:
+            return getattr(e, 'errno', -1), '', f'Failed to update export: {e}'
+
 
 class NFSCluster:
     def __init__(self, mgr):
index 262441b7f00038f6c7b6bed01f177ea9ae927b53..b2acbbdeb468903a33983e26bbf363b3601fe55f 100644 (file)
@@ -352,6 +352,11 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule):
             'desc': "Fetch a export of a NFS cluster given the pseudo path/binding",
             'perm': 'r'
         },
+        {
+            'cmd': 'nfs export update ',
+            'desc': "Update an export of a NFS cluster by `-i <json_file>`",
+            'perm': 'rw'
+        },
         {
             'cmd': 'nfs cluster create '
                    'name=type,type=CephString '
@@ -705,6 +710,11 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule):
     def _cmd_nfs_export_get(self, inbuf, cmd):
         return self.fs_export.get_export(cluster_id=cmd['clusterid'], pseudo_path=cmd['binding'])
 
+    @mgr_cmd_wrap
+    def _cmd_nfs_export_update(self, inbuf, cmd):
+        # The export <json_file> is passed to -i and it's processing is handled by the Ceph CLI.
+        return self.fs_export.update_export(export_config=inbuf)
+
     @mgr_cmd_wrap
     def _cmd_nfs_cluster_create(self, inbuf, cmd):
         return self.nfs.create_nfs_cluster(cluster_id=cmd['clusterid'], export_type=cmd['type'],