From 8c981876c244b2d73ec1ca23d46376ff65def150 Mon Sep 17 00:00:00 2001 From: Shweta Bhosale Date: Wed, 21 Jan 2026 21:13:39 +0530 Subject: [PATCH 1/3] mgr/cephadm: Updated NFS default protocol to v4 and v3 is enabled only when enable_nfsv3 is set in the spec Fixes: https://tracker.ceph.com/issues/74492 Signed-off-by: Shweta Bhosale (cherry picked from commit 9b0becfdf248576565481369780f5599766c3810) Conflicts: doc/cephadm/services/nfs.rst - due to documentation related to RDMA doc/mgr/nfs.rst - due to params related to RDMA src/pybind/mgr/cephadm/services/nfs.py - due to other extra params src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2 - due to RDMA src/pybind/mgr/cephadm/tests/services/test_ingress.py - not exists on tentacle src/pybind/mgr/cephadm/tests/services/test_nfs.py - not exists on tentacle src/pybind/mgr/nfs/cluster.py - due to other nfs parameters which are not supported in tentacle src/pybind/mgr/nfs/module.py - due to other nfs parameters which are not supported in tentacle src/python-common/ceph/deployment/service_spec.py - due to extra variable not supported in tentacle --- doc/cephadm/services/nfs.rst | 5 ++++ doc/mgr/nfs.rst | 5 +++- src/pybind/mgr/cephadm/services/nfs.py | 1 + .../templates/services/nfs/ganesha.conf.j2 | 2 +- src/pybind/mgr/cephadm/tests/test_services.py | 28 ++++++++++++++++++- src/pybind/mgr/nfs/cluster.py | 17 +++++++++-- src/pybind/mgr/nfs/module.py | 6 ++-- .../ceph/deployment/service_spec.py | 2 ++ 8 files changed, 58 insertions(+), 8 deletions(-) diff --git a/doc/cephadm/services/nfs.rst b/doc/cephadm/services/nfs.rst index ab616ddcb13..0b5872d3d06 100644 --- a/doc/cephadm/services/nfs.rst +++ b/doc/cephadm/services/nfs.rst @@ -49,9 +49,14 @@ Alternatively, an NFS service can be applied using a YAML specification. - host2 spec: port: 12345 + monitoring_port: 567 + enable_nfsv3: true + In this example, we run the server on the non-default ``port`` of 12345 (instead of the default 2049) on ``host1`` and ``host2``. +By default, only the NFSv4 protocol is enabled. NFSv3 can be enabled by setting +``enable_nfsv3`` to ``true`` in the service specification. The specification can then be applied by running the following command: diff --git a/doc/mgr/nfs.rst b/doc/mgr/nfs.rst index 3a130f4f0a9..cf9abe38ff0 100644 --- a/doc/mgr/nfs.rst +++ b/doc/mgr/nfs.rst @@ -31,7 +31,7 @@ Create NFS Ganesha Cluster .. prompt:: bash # - ceph nfs cluster create [] [--ingress] [--virtual_ip ] [--ingress-mode {default|keepalive-only|haproxy-standard|haproxy-protocol}] [--port ] + ceph nfs cluster create [] [--ingress] [--virtual_ip ] [--ingress-mode {default|keepalive-only|haproxy-standard|haproxy-protocol}] [--port ] [--enable-nfsv3] This creates a common recovery pool for all NFS Ganesha daemons, new user based on ``cluster_id``, and a common NFS Ganesha config RADOS object. @@ -62,6 +62,9 @@ cluster):: NFS can be deployed on a port other than 2049 (the default) with ``--port ``. +By default, only NFS v4 protocol is enabled. To enable both NFS v3 and v4 protocols, +add the ``--enable-nfsv3`` flag. + To deploy NFS with a high-availability front-end (virtual IP and load balancer), add the ``--ingress`` flag and specify a virtual IP address. This will deploy a combination of keepalived and haproxy to provide an high-availability NFS frontend for the NFS diff --git a/src/pybind/mgr/cephadm/services/nfs.py b/src/pybind/mgr/cephadm/services/nfs.py index b695fa0b44c..8ef87ae81cd 100644 --- a/src/pybind/mgr/cephadm/services/nfs.py +++ b/src/pybind/mgr/cephadm/services/nfs.py @@ -129,6 +129,7 @@ class NFSService(CephService): "nfs_idmap_conf": nfs_idmap_conf, "enable_nlm": str(spec.enable_nlm).lower(), "cluster_id": self.mgr._cluster_fsid, + "protocols": "3, 4" if spec.enable_nfsv3 else "4" } if spec.enable_haproxy_protocol: context["haproxy_hosts"] = self._haproxy_hosts() diff --git a/src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2 b/src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2 index 8642c0ae162..7cf50f954b8 100644 --- a/src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2 +++ b/src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2 @@ -2,7 +2,7 @@ NFS_CORE_PARAM { Enable_NLM = {{ enable_nlm }}; Enable_RQUOTA = false; - Protocols = 3, 4; + Protocols = {{ protocols }}; mount_path_pseudo = true; Enable_UDP = false; NFS_Port = {{ port }}; diff --git a/src/pybind/mgr/cephadm/tests/test_services.py b/src/pybind/mgr/cephadm/tests/test_services.py index a43d83896e3..06c7a0af2c8 100644 --- a/src/pybind/mgr/cephadm/tests/test_services.py +++ b/src/pybind/mgr/cephadm/tests/test_services.py @@ -3391,7 +3391,7 @@ class TestIngressService: 'NFS_CORE_PARAM {\n' ' Enable_NLM = true;\n' ' Enable_RQUOTA = false;\n' - ' Protocols = 3, 4;\n' + ' Protocols = 4;\n' ' mount_path_pseudo = true;\n' ' Enable_UDP = false;\n' ' NFS_Port = 2049;\n' @@ -3518,6 +3518,32 @@ class TestIngressService: ) assert nfs_generated_conf == nfs_expected_conf + @patch("cephadm.serve.CephadmServe._run_cephadm") + @patch("cephadm.services.nfs.NFSService.fence_old_ranks", MagicMock()) + @patch("cephadm.services.nfs.NFSService.run_grace_tool", MagicMock()) + @patch("cephadm.services.nfs.NFSService.purge", MagicMock()) + @patch("cephadm.services.nfs.NFSService.create_rados_config_obj", MagicMock()) + def test_nfs_enable_nfsv3(self, _run_cephadm, cephadm_module: CephadmOrchestrator): + _run_cephadm.side_effect = async_side_effect(('{}', '', 0)) + + with with_host(cephadm_module, 'test'): + # Test with enable_nfsv3=False (default) + nfs_spec = NFSServiceSpec(service_id="foo", placement=PlacementSpec(hosts=['test'])) + with with_service(cephadm_module, nfs_spec) as _: + nfs_generated_conf, _ = service_registry.get_service('nfs').generate_config( + CephadmDaemonDeploySpec(host='test', daemon_id='foo.test.0.0', service_name=nfs_spec.service_name())) + ganesha_conf = nfs_generated_conf['files']['ganesha.conf'] + assert "Protocols = 4;" in ganesha_conf + + # Test with enable_nfsv3=True + nfs_spec = NFSServiceSpec(service_id="foo", placement=PlacementSpec(hosts=['test']), + enable_nfsv3=True) + with with_service(cephadm_module, nfs_spec) as _: + nfs_generated_conf, _ = service_registry.get_service('nfs').generate_config( + CephadmDaemonDeploySpec(host='test', daemon_id='foo.test.0.0', service_name=nfs_spec.service_name())) + ganesha_conf = nfs_generated_conf['files']['ganesha.conf'] + assert "Protocols = 3, 4;" in ganesha_conf + class TestCephFsMirror: @patch("cephadm.serve.CephadmServe._run_cephadm") diff --git a/src/pybind/mgr/nfs/cluster.py b/src/pybind/mgr/nfs/cluster.py index 85b7ed42f96..ae3005205dd 100644 --- a/src/pybind/mgr/nfs/cluster.py +++ b/src/pybind/mgr/nfs/cluster.py @@ -65,6 +65,7 @@ class NFSCluster: virtual_ip: Optional[str] = None, ingress_mode: Optional[IngressType] = None, port: Optional[int] = None, + enable_nfsv3: bool = False, ) -> None: if not port: port = 2049 # default nfs port @@ -98,7 +99,8 @@ class NFSCluster: # use non-default port so we don't conflict with ingress port=ganesha_port, virtual_ip=virtual_ip_for_ganesha, - enable_haproxy_protocol=enable_haproxy_protocol) + enable_haproxy_protocol=enable_haproxy_protocol, + enable_nfsv3=enable_nfsv3) completion = self.mgr.apply_nfs(spec) orchestrator.raise_if_exception(completion) ispec = IngressSpec(service_type='ingress', @@ -116,7 +118,8 @@ class NFSCluster: # standalone nfs spec = NFSServiceSpec(service_type='nfs', service_id=cluster_id, placement=PlacementSpec.from_string(placement), - port=port) + port=port, + enable_nfsv3=enable_nfsv3) completion = self.mgr.apply_nfs(spec) orchestrator.raise_if_exception(completion) log.debug("Successfully deployed nfs daemons with cluster id %s and placement %s", @@ -140,6 +143,7 @@ class NFSCluster: ingress: Optional[bool] = None, ingress_mode: Optional[IngressType] = None, port: Optional[int] = None, + enable_nfsv3: bool = False, ) -> None: try: if virtual_ip: @@ -163,7 +167,14 @@ class NFSCluster: self.create_empty_rados_obj(cluster_id) if cluster_id not in available_clusters(self.mgr): - self._call_orch_apply_nfs(cluster_id, placement, virtual_ip, ingress_mode, port) + self._call_orch_apply_nfs( + cluster_id, + placement, + virtual_ip, + ingress_mode, + port, + enable_nfsv3=enable_nfsv3, + ) return raise NonFatalError(f"{cluster_id} cluster already exists") except Exception as e: diff --git a/src/pybind/mgr/nfs/module.py b/src/pybind/mgr/nfs/module.py index 751f2f7c999..c16b1a2507c 100644 --- a/src/pybind/mgr/nfs/module.py +++ b/src/pybind/mgr/nfs/module.py @@ -133,11 +133,13 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): ingress: Optional[bool] = None, virtual_ip: Optional[str] = None, ingress_mode: Optional[IngressType] = None, - port: Optional[int] = None) -> None: + port: Optional[int] = None, + enable_nfsv3: bool = False) -> None: """Create an NFS Cluster""" return self.nfs.create_nfs_cluster(cluster_id=cluster_id, placement=placement, virtual_ip=virtual_ip, ingress=ingress, - ingress_mode=ingress_mode, port=port) + ingress_mode=ingress_mode, port=port, + enable_nfsv3=enable_nfsv3) @NFSCLICommand('nfs cluster rm', perm='rw') @object_format.EmptyResponder() diff --git a/src/python-common/ceph/deployment/service_spec.py b/src/python-common/ceph/deployment/service_spec.py index 71c22eb5b06..e0fe7b9acd8 100644 --- a/src/python-common/ceph/deployment/service_spec.py +++ b/src/python-common/ceph/deployment/service_spec.py @@ -1185,6 +1185,7 @@ class NFSServiceSpec(ServiceSpec): extra_entrypoint_args: Optional[GeneralArgList] = None, idmap_conf: Optional[Dict[str, Dict[str, str]]] = None, custom_configs: Optional[List[CustomConfig]] = None, + enable_nfsv3: bool = False, ): assert service_type == 'nfs' super(NFSServiceSpec, self).__init__( @@ -1199,6 +1200,7 @@ class NFSServiceSpec(ServiceSpec): self.enable_haproxy_protocol = enable_haproxy_protocol self.idmap_conf = idmap_conf self.enable_nlm = enable_nlm + self.enable_nfsv3 = enable_nfsv3 def get_port_start(self) -> List[int]: if self.port: From 41e187ebaaf361167a4cf9aff1715dac76d28672 Mon Sep 17 00:00:00 2001 From: Shweta Bhosale Date: Fri, 6 Feb 2026 18:27:58 +0530 Subject: [PATCH 2/3] mgr/nfs: set nfs export protocols based on cluster's protocol settings Fixes: https://tracker.ceph.com/issues/74492 Signed-off-by: Shweta Bhosale (cherry picked from commit 44c13620288c434aef6fc00aa4e7a8c64c7e4134) Conflicts: src/pybind/mgr/nfs/export.py - due to code refactoring in main --- src/pybind/mgr/nfs/export.py | 30 ++++++++++++++++++++++++++++ src/pybind/mgr/nfs/ganesha_conf.py | 2 +- src/pybind/mgr/nfs/tests/test_nfs.py | 14 ++++++------- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/pybind/mgr/nfs/export.py b/src/pybind/mgr/nfs/export.py index aff6779bb16..c8ad9ac73cb 100644 --- a/src/pybind/mgr/nfs/export.py +++ b/src/pybind/mgr/nfs/export.py @@ -227,6 +227,25 @@ class ExportMgr: self.rados_pool = POOL_NAME self._exports: Optional[Dict[str, List[Export]]] = export_ls + def _get_cluster_protocols(self, cluster_id: str) -> List[int]: + """Get the list of supported NFS protocols for a cluster. + """ + try: + import orchestrator + from ceph.deployment.service_spec import NFSServiceSpec + + completion = self.mgr.describe_service(service_type='nfs', service_name=f'nfs.{cluster_id}') + services = orchestrator.raise_if_exception(completion) + for service in services: + if service.spec and isinstance(service.spec, NFSServiceSpec): + spec = cast(NFSServiceSpec, service.spec) + if getattr(spec, 'enable_nfsv3', False): + return [3, 4] + return [4] + except Exception as e: + log.debug(f"Failed to get cluster protocols for {cluster_id}: {e}, defaulting to v4 only") + return [4] + @property def exports(self) -> Dict[str, List[Export]]: if self._exports is None: @@ -749,6 +768,8 @@ class ExportMgr: _validate_cmount_path(cmount_path, path) # type: ignore pseudo_path = normalize_path(pseudo_path) + # Get the protocols based on cluster's enable_nfsv3 setting + protocols = self._get_cluster_protocols(cluster_id) if not self._fetch_export(cluster_id, pseudo_path): export = self.create_export_from_dict( @@ -766,6 +787,7 @@ class ExportMgr: }, "clients": clients, "sectype": sectype, + "protocols": protocols, }, earmark_resolver ) @@ -797,6 +819,9 @@ class ExportMgr: if not bucket and not user_id: raise ErrorResponse("Must specify either bucket or user_id") + # Get the protocols based on cluster's enable_nfsv3 setting + protocols = self._get_cluster_protocols(cluster_id) + if not self._fetch_export(cluster_id, pseudo_path): export = self.create_export_from_dict( cluster_id, @@ -812,6 +837,7 @@ class ExportMgr: }, "clients": clients, "sectype": sectype, + "protocols": protocols, } ) log.debug("creating rgw export %s", export) @@ -868,6 +894,10 @@ class ExportMgr: else: new_export_dict['fsal']['cmount_path'] = '/' + # Set protocols based on cluster's enable_nfsv3 setting if not explicitly specified + if 'protocols' not in new_export_dict: + new_export_dict['protocols'] = self._get_cluster_protocols(cluster_id) + new_export = self.create_export_from_dict( cluster_id, new_export_dict.get('export_id', self._gen_export_id(cluster_id)), diff --git a/src/pybind/mgr/nfs/ganesha_conf.py b/src/pybind/mgr/nfs/ganesha_conf.py index e4cff653d25..cb8a6287b9f 100644 --- a/src/pybind/mgr/nfs/ganesha_conf.py +++ b/src/pybind/mgr/nfs/ganesha_conf.py @@ -464,7 +464,7 @@ class Export: ex_dict.get('access_type', 'RO'), ex_dict.get('squash', 'no_root_squash'), ex_dict.get('security_label', True), - ex_dict.get('protocols', [3, 4]), + ex_dict.get('protocols', [4]), ex_dict.get('transports', ['TCP']), FSAL.from_dict(ex_dict.get('fsal', {})), [Client.from_dict(client) for client in ex_dict.get('clients', [])], diff --git a/src/pybind/mgr/nfs/tests/test_nfs.py b/src/pybind/mgr/nfs/tests/test_nfs.py index c0c1a73e269..d62727b1307 100644 --- a/src/pybind/mgr/nfs/tests/test_nfs.py +++ b/src/pybind/mgr/nfs/tests/test_nfs.py @@ -116,7 +116,7 @@ EXPORT { Path = /; Pseudo = /cephfs_b/; Access_Type = RW; - Protocols = 4; + Protocols = 3, 4; Attr_Expiration_Time = 0; FSAL { @@ -406,7 +406,7 @@ NFS_CORE_PARAM { assert export.pseudo == "/cephfs_b/" assert export.access_type == "RW" assert export.squash == "no_root_squash" - assert export.protocols == [4] + assert export.protocols == [3, 4] assert export.fsal.name == "CEPH" assert export.fsal.user_id == "nfs.foo.b.lgudhr" assert export.fsal.fs_name == "b" @@ -1017,7 +1017,7 @@ NFS_CORE_PARAM { assert export.pseudo == "/mybucket" assert export.access_type == "none" assert export.squash == "none" - assert export.protocols == [3, 4] + assert export.protocols == [4] assert export.transports == ["TCP"] assert export.fsal.name == "RGW" assert export.fsal.user_id == "bucket_owner_user" @@ -1060,7 +1060,7 @@ NFS_CORE_PARAM { assert export.pseudo == "/mybucket" assert export.access_type == "none" assert export.squash == "none" - assert export.protocols == [3, 4] + assert export.protocols == [4] assert export.transports == ["TCP"] assert export.fsal.name == "RGW" assert export.fsal.access_key_id == "the_access_key" @@ -1102,7 +1102,7 @@ NFS_CORE_PARAM { assert export.pseudo == "/mybucket" assert export.access_type == "none" assert export.squash == "none" - assert export.protocols == [3, 4] + assert export.protocols == [4] assert export.transports == ["TCP"] assert export.fsal.name == "RGW" assert export.fsal.access_key_id == "the_access_key" @@ -1151,7 +1151,7 @@ NFS_CORE_PARAM { assert export.pseudo == "/cephfs2" assert export.access_type == "none" assert export.squash == "none" - assert export.protocols == [3, 4] + assert export.protocols == [4] assert export.transports == ["TCP"] assert export.fsal.name == "CEPH" assert export.fsal.user_id == "nfs.foo.myfs.86ca58ef" @@ -1190,7 +1190,7 @@ NFS_CORE_PARAM { assert export.pseudo == "/cephfs3" assert export.access_type == "RW" assert export.squash == "root" - assert export.protocols == [3, 4] + assert export.protocols == [4] assert export.fsal.name == "CEPH" assert export.fsal.user_id == "nfs.foo.myfs.86ca58ef" assert export.fsal.cephx_key == "thekeyforclientabc" From 58cac6f23aa50856baf39c495a1d9d1786eca9b8 Mon Sep 17 00:00:00 2001 From: Shweta Bhosale Date: Mon, 13 Apr 2026 17:16:22 +0530 Subject: [PATCH 3/3] mgr/nfs: Updated enable_nfsv3 check while export creation Fixes: https://tracker.ceph.com/issues/74492 Signed-off-by: Shweta Bhosale (cherry picked from commit 64732c127dba1974cb60bc448ed01eceb0831d0a) --- src/pybind/mgr/nfs/export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pybind/mgr/nfs/export.py b/src/pybind/mgr/nfs/export.py index c8ad9ac73cb..6882b3747bc 100644 --- a/src/pybind/mgr/nfs/export.py +++ b/src/pybind/mgr/nfs/export.py @@ -237,7 +237,7 @@ class ExportMgr: completion = self.mgr.describe_service(service_type='nfs', service_name=f'nfs.{cluster_id}') services = orchestrator.raise_if_exception(completion) for service in services: - if service.spec and isinstance(service.spec, NFSServiceSpec): + if service.spec: spec = cast(NFSServiceSpec, service.spec) if getattr(spec, 'enable_nfsv3', False): return [3, 4]