From: Kobi Ginon Date: Sun, 31 May 2026 17:22:24 +0000 (+0300) Subject: cephadm: start rpcbind before NFS-Ganesha when NFSv3 is enabled X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=b09f161916e696d2cb50401d4a486b8cdca20124;p=ceph.git cephadm: start rpcbind before NFS-Ganesha when NFSv3 is enabled With Protocols including NFSv3, Ganesha 9.x on Rocky 10 needs a running portmapper to register NFSv3 on TCP. Start rpcbind from the container entrypoint only when ganesha.conf enables v3; skip for NFSv4-only configs. Fixes: https://tracker.ceph.com/issues/76981 Signed-off-by: Kobi Ginon --- diff --git a/src/cephadm/cephadmlib/daemons/nfs.py b/src/cephadm/cephadmlib/daemons/nfs.py index 1783951a71f..61c30868135 100644 --- a/src/cephadm/cephadmlib/daemons/nfs.py +++ b/src/cephadm/cephadmlib/daemons/nfs.py @@ -28,7 +28,9 @@ class NFSGanesha(ContainerDaemonForm): """Defines a NFS-Ganesha container""" daemon_type = 'nfs' - entrypoint = '/usr/bin/ganesha.nfsd' + entrypoint = '/usr/local/scripts/ganesha-entrypoint.sh' + ganesha_binary = '/usr/bin/ganesha.nfsd' + entrypoint_script_name = 'ganesha-entrypoint.sh' daemon_args = ['-F', '-L', 'STDERR'] required_files = ['ganesha.conf', 'idmap.conf'] @@ -88,6 +90,9 @@ class NFSGanesha(ContainerDaemonForm): mounts[os.path.join(data_dir, 'config')] = '/etc/ceph/ceph.conf:z' mounts[os.path.join(data_dir, 'keyring')] = '/etc/ceph/keyring:z' mounts[os.path.join(data_dir, 'etc/ganesha')] = '/etc/ganesha:z' + mounts[ + os.path.join(data_dir, self.entrypoint_script_name) + ] = self.entrypoint if self.rgw: cluster = self.rgw.get('cluster', 'ceph') rgw_user = self.rgw.get('user', 'admin') @@ -118,7 +123,7 @@ class NFSGanesha(ContainerDaemonForm): ctx.container_engine.path, 'exec', container_id, - NFSGanesha.entrypoint, + NFSGanesha.ganesha_binary, '-v', ], verbosity=CallVerbosity.QUIET, @@ -168,6 +173,42 @@ class NFSGanesha(ContainerDaemonForm): # type: () -> List[str] return self.daemon_args + self.extra_args + @staticmethod + def ganesha_conf_text(conf: Union[str, List[str]]) -> str: + if isinstance(conf, list): + return '\n'.join(conf) + return conf + + @staticmethod + def nfsv3_enabled_in_ganesha_conf(conf: Union[str, List[str]]) -> bool: + """Return True when ganesha.conf enables NFSv3 (Protocols includes 3).""" + text = NFSGanesha.ganesha_conf_text(conf) + match = re.search(r'Protocols\s*=\s*([^;]+)', text, re.MULTILINE) + if not match: + # No Protocols line (e.g. minimal test stubs): keep legacy behavior. + return True + protocols = match.group(1) + return bool(re.search(r'(?:^|[,\s])3(?:[,\s]|$)', protocols)) + + @staticmethod + def ganesha_entrypoint_script(nfsv3: bool = True) -> str: + # NFSv3 registration with SunRPC requires a running portmapper. + # The image installs rpcbind but cephadm previously started only + # ganesha.nfsd, which fails on Rocky 10 / Ganesha 9.x with + # "Cannot register NFS V3 on TCP" when rpcbind is not up. + rpcbind_block = '' + if nfsv3: + rpcbind_block = """if command -v rpcbind >/dev/null 2>&1; then + if ! rpcinfo -p >/dev/null 2>&1; then + rpcbind + fi +fi +""" + return f"""#!/bin/bash +set -e +{rpcbind_block}exec /usr/bin/ganesha.nfsd "$@" +""" + def create_daemon_dirs(self, data_dir, uid, gid): # type: (str, int, int) -> None """Create files under the container data dir""" @@ -196,6 +237,13 @@ class NFSGanesha(ContainerDaemonForm): populate_files(config_dir, config_files, uid, gid) populate_files(tls_dir, tls_files, uid, gid) + ganesha_conf = self.files.get('ganesha.conf', '') + nfsv3 = self.nfsv3_enabled_in_ganesha_conf(ganesha_conf) + entrypoint_path = os.path.join(data_dir, self.entrypoint_script_name) + with write_new(entrypoint_path, owner=(uid, gid)) as f: + f.write(self.ganesha_entrypoint_script(nfsv3=nfsv3)) + os.chmod(entrypoint_path, 0o700) + # write the RGW keyring if self.rgw: keyring_path = os.path.join(data_dir, 'keyring.rgw') diff --git a/src/cephadm/tests/test_nfs.py b/src/cephadm/tests/test_nfs.py index 1b468516e67..8c0676482cc 100644 --- a/src/cephadm/tests/test_nfs.py +++ b/src/cephadm/tests/test_nfs.py @@ -119,10 +119,14 @@ def test_nfsganesha_container_mounts(): good_nfs_json(), ) cmounts = nfsg._get_container_mounts("/var/tmp") - assert len(cmounts) == 3 + assert len(cmounts) == 4 assert cmounts["/var/tmp/config"] == "/etc/ceph/ceph.conf:z" assert cmounts["/var/tmp/keyring"] == "/etc/ceph/keyring:z" assert cmounts["/var/tmp/etc/ganesha"] == "/etc/ganesha:z" + assert ( + cmounts["/var/tmp/ganesha-entrypoint.sh"] + == "/usr/local/scripts/ganesha-entrypoint.sh" + ) with with_cephadm_ctx([]) as ctx: nfsg = _cephadm.NFSGanesha( @@ -132,7 +136,7 @@ def test_nfsganesha_container_mounts(): nfs_json(pool=True, files=True, rgw=True), ) cmounts = nfsg._get_container_mounts("/var/tmp") - assert len(cmounts) == 4 + assert len(cmounts) == 5 assert cmounts["/var/tmp/config"] == "/etc/ceph/ceph.conf:z" assert cmounts["/var/tmp/keyring"] == "/etc/ceph/keyring:z" assert cmounts["/var/tmp/etc/ganesha"] == "/etc/ganesha:z" @@ -212,6 +216,52 @@ def test_nfsganesha_get_daemon_args(): assert args == ["-F", "-L", "STDERR"] +@pytest.mark.parametrize( + 'conf,expected', + [ + ('Protocols = 3, 4;', True), + ('Protocols = 4, 3;', True), + ('Protocols = 4;', False), + ('Protocols = 4, nfsrdma, rpcrdma;', False), + ('', True), + ], +) +def test_nfsv3_enabled_in_ganesha_conf(conf, expected): + assert _cephadm.NFSGanesha.nfsv3_enabled_in_ganesha_conf(conf) is expected + + +def test_nfsganesha_entrypoint_script_nfsv3(): + script = _cephadm.NFSGanesha.ganesha_entrypoint_script(nfsv3=True) + assert 'rpcbind' in script + assert 'exec /usr/bin/ganesha.nfsd "$@"' in script + + +def test_nfsganesha_entrypoint_script_v4_only(): + script = _cephadm.NFSGanesha.ganesha_entrypoint_script(nfsv3=False) + assert 'rpcbind' not in script + assert 'exec /usr/bin/ganesha.nfsd "$@"' in script + + +def test_nfsganesha_entrypoint_script_from_conf(): + conf_v3 = '\n'.join( + [ + 'NFS_CORE_PARAM {', + ' Protocols = 3, 4;', + '}', + ] + ) + script = _cephadm.NFSGanesha.ganesha_entrypoint_script( + nfsv3=_cephadm.NFSGanesha.nfsv3_enabled_in_ganesha_conf(conf_v3), + ) + assert 'rpcbind' in script + + conf_v4 = 'NFS_CORE_PARAM { Protocols = 4; }' + script = _cephadm.NFSGanesha.ganesha_entrypoint_script( + nfsv3=_cephadm.NFSGanesha.nfsv3_enabled_in_ganesha_conf(conf_v4), + ) + assert 'rpcbind' not in script + + @mock.patch("cephadm.logger") def test_nfsganesha_create_daemon_dirs(_logger, cephadm_fs): with with_cephadm_ctx([]) as ctx: @@ -225,7 +275,24 @@ def test_nfsganesha_create_daemon_dirs(_logger, cephadm_fs): nfsg.create_daemon_dirs("/var/tmp", 45, 54) cephadm_fs.create_dir("/var/tmp") nfsg.create_daemon_dirs("/var/tmp", 45, 54) - # TODO: make assertions about the dirs created + with open("/var/tmp/ganesha-entrypoint.sh") as f: + assert 'rpcbind' in f.read() + + nfsg_v4 = _cephadm.NFSGanesha( + ctx, + SAMPLE_UUID, + "fred", + { + 'pool': 'party', + 'files': { + 'ganesha.conf': 'NFS_CORE_PARAM { Protocols = 4; }', + 'idmap.conf': '', + }, + }, + ) + nfsg_v4.create_daemon_dirs("/var/tmp", 45, 54) + with open("/var/tmp/ganesha-entrypoint.sh") as f: + assert 'rpcbind' not in f.read() @mock.patch("cephadm.logger")