]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
cephadm: start rpcbind before NFS-Ganesha when NFSv3 is enabled 69192/head
authorKobi Ginon <kginon@redhat.com>
Sun, 31 May 2026 17:22:24 +0000 (20:22 +0300)
committerKobi Ginon <kginon@redhat.com>
Tue, 2 Jun 2026 18:37:07 +0000 (21:37 +0300)
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 <kginon@redhat.com>
src/cephadm/cephadmlib/daemons/nfs.py
src/cephadm/tests/test_nfs.py

index 1783951a71f3ec1b4a9952f2c2f54ce009dbe878..61c3086813544b45973d50260d14c49e93b8fd0a 100644 (file)
@@ -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')
index 1b468516e67b68c711ecef6e986b046089fc5d39..8c0676482cc11b413e51f65a222044a6cb6fe8cf 100644 (file)
@@ -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")