Merge PR #70675 into tentacle

* refs/pull/70675/head:
	mgr/nfs: Updated enable_nfsv3 check while export creation
	mgr/nfs: set nfs export protocols based on cluster's protocol settings
	mgr/cephadm: Updated NFS default protocol to v4 and v3 is enabled only when enable_nfsv3 is set in the spec

Reviewed-by: Adam King <adking@redhat.com>
This commit is contained in:
Patrick Donnelly 2026-07-31 13:05:09 -04:00
commit 36ad54815f
No known key found for this signature in database
GPG Key ID: 053758C0A8A3CE2F
11 changed files with 96 additions and 16 deletions

View File

@ -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:

View File

@ -31,7 +31,7 @@ Create NFS Ganesha Cluster
.. prompt:: bash #
ceph nfs cluster create <cluster_id> [<placement>] [--ingress] [--virtual_ip <value>] [--ingress-mode {default|keepalive-only|haproxy-standard|haproxy-protocol}] [--port <int>]
ceph nfs cluster create <cluster_id> [<placement>] [--ingress] [--virtual_ip <value>] [--ingress-mode {default|keepalive-only|haproxy-standard|haproxy-protocol}] [--port <int>] [--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 <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

View File

@ -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()

View File

@ -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 }};

View File

@ -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")

View File

@ -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:

View File

@ -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:
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)),

View File

@ -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', [])],

View File

@ -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()

View File

@ -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"

View File

@ -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: