From 28dc05c226887b7e6ce085dab0ddaa07e5a8a417 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:40:16 -0300 Subject: [PATCH] fix(xcat-server): a service node that mounts /install serves no NFS servicenode.nfsserver=1 asks a service node to set up file services for the nodes behind it. With site.installloc set, AAsn.pm mounts the install directory from the management node and exports nothing, and setup_NFS only starts the daemon, so the service node serves no NFS at all. Only the branch where installloc is unset ever wrote an export. An Ubuntu compute node boots the live installer with casper, which mounts the install tree over NFS from its own service node. Measured on xcat22-cn: the boot arguments carried nfsroot=192.168.222.21:/install/ubuntu24.04.4/x86_64, and the node stopped at "(initramfs) Unable to find a live file system on the network". On xcat22-sn, showmount -e returned an empty list and /etc/exports was empty while nfs-kernel-server was active. EL never needed this, because anaconda fetches over http. setup_NFS now exports the install directory when nothing exports it yet, then runs exportfs after the daemon. Re-exporting an NFS mount is not the same as exporting a local directory: the kernel cannot derive a filesystem id for one and refuses the export without an explicit fsid, so nfs_export_line adds fsid and crossmnt when the directory is itself a mount. The fsid comes from the path, so a restart does not hand clients a new filesystem identity. An export the kernel refuses is reported and does not fail the service node. The EL cell is re-run on this tree, because an EL service node now writes an export it did not write before. svrutils_nfs_export_line.t drives the routine. Five mutations were tried and each one fails the assertions that name it. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/SvrUtils.pm | 39 +++++++++++++++++ xCAT-server/lib/xcat/plugins/AAsn.pm | 26 ++++++++++++ xCAT-test/unit/svrutils_nfs_export_line.t | 52 +++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 xCAT-test/unit/svrutils_nfs_export_line.t diff --git a/xCAT-server/lib/perl/xCAT/SvrUtils.pm b/xCAT-server/lib/perl/xCAT/SvrUtils.pm index b15aa3002..a4440e7b3 100644 --- a/xCAT-server/lib/perl/xCAT/SvrUtils.pm +++ b/xCAT-server/lib/perl/xCAT/SvrUtils.pm @@ -1693,6 +1693,45 @@ sub nfs_export_exists { return 0; } +#----------------------------------------------------------------------------- + +=head3 nfs_export_line + + The /etc/exports line for a directory a service node serves to its nodes. + + Arguments: + $dir the directory to export + reexport true when $dir is itself an NFS mount + + Returns: + one export line, with no trailing newline. + + A local directory keeps the options xCAT has always written. Re-exporting an NFS mount needs + two more: the kernel cannot derive a filesystem id for one, so exportfs refuses it without an + explicit fsid, and crossmnt lets a client cross into the mount below. The fsid is derived + from the path so that it survives a restart -- a new one would make every client's mount + stale. + +=cut + +#----------------------------------------------------------------------------- +sub nfs_export_line { + my ( $dir, %opts ) = _nfs_method_args(@_); + my $options = 'rw,no_root_squash,sync,no_subtree_check,insecure'; + if ( $opts{reexport} ) { + $options .= ',crossmnt,fsid=' . _nfs_export_fsid($dir); + } + return "$dir *($options)"; +} + +# A stable, non-zero fsid for a path. 0 is reserved for the export root. +sub _nfs_export_fsid { + my ($dir) = @_; + my $sum = 0; + $sum = ( $sum * 31 + ord($_) ) % 2147483647 for split //, $dir; + return $sum || 1; +} + sub _ensure_nfs_exported { my ($nfsserver, $nfsdirectory, $callback, %opts) = @_; my $export_options = 'rw,no_root_squash,sync,no_subtree_check,insecure'; diff --git a/xCAT-server/lib/xcat/plugins/AAsn.pm b/xCAT-server/lib/xcat/plugins/AAsn.pm index 188d7ebdc..bc5350ccf 100644 --- a/xCAT-server/lib/xcat/plugins/AAsn.pm +++ b/xCAT-server/lib/xcat/plugins/AAsn.pm @@ -919,6 +919,24 @@ sub setup_NFS my $rc = 0; if (xCAT::Utils->isLinux()) { + # servicenode.nfsserver=1 means this node serves files to the nodes behind it. With + # site.installloc set it MOUNTS the install directory from the management node and the + # caller exports nothing, so the node served no NFS at all: an Ubuntu compute node stops + # at "Unable to find a live file system on the network", because casper mounts the + # install tree over NFS from its own service node. Exporting a mount needs an fsid, which + # nfs_export_line adds. + my $installdir = xCAT::TableUtils->getInstallDir() || "/install"; + unless (xCAT::SvrUtils->nfs_export_exists($installdir)) + { + my $line = xCAT::SvrUtils->nfs_export_line($installdir, + reexport => (xCAT::Utils->isMounted($installdir) ? 1 : 0)); + xCAT::Utils->runcmd("/bin/echo '$line' >> /etc/exports", 0); + if ($::RUNCMD_RC != 0) + { + xCAT::MsgUtils->message('S', "Could not add $installdir to /etc/exports."); + } + } + #my $os = xCAT::Utils->osver(); #if ($os =~ /sles.*/) #{ @@ -930,6 +948,14 @@ sub setup_NFS # $rc = xCAT::Utils->startService("nfs"); #} $rc = xCAT::Utils->startservice("nfs"); + + # After the daemon, so a fresh export is picked up. An export the kernel refuses is + # reported and does not fail the service node, which has other services to set up. + xCAT::Utils->runcmd("/usr/sbin/exportfs -a", 0); + if ($::RUNCMD_RC != 0) + { + xCAT::MsgUtils->message('S', "Error with /usr/sbin/exportfs -a."); + } } else { #AIX diff --git a/xCAT-test/unit/svrutils_nfs_export_line.t b/xCAT-test/unit/svrutils_nfs_export_line.t new file mode 100644 index 000000000..3adbfcbfc --- /dev/null +++ b/xCAT-test/unit/svrutils_nfs_export_line.t @@ -0,0 +1,52 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +# A service node with servicenode.nfsserver=1 is asked to "set up file services on this service +# node". When site.installloc is set it MOUNTS /install from the management node, and AAsn.pm +# then exported nothing at all, so the node served no NFS. An Ubuntu compute node boots the live +# installer with casper, which mounts the install tree over NFS from its own service node, and it +# stopped at "Unable to find a live file system on the network". Measured on xcat22-sn: +# showmount -e returned an empty list and /etc/exports was empty while nfs-kernel-server was +# active. +# +# Re-exporting an NFS mount is not the same as exporting a local directory. Linux requires an +# explicit fsid, because it cannot derive one from the underlying filesystem, and refuses the +# export without it. + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use xCAT::SvrUtils; + +can_ok('xCAT::SvrUtils', 'nfs_export_line') or done_testing() && exit; + +my $local = xCAT::SvrUtils->nfs_export_line('/install'); +is($local, '/install *(rw,no_root_squash,sync,no_subtree_check,insecure)', + 'a local directory keeps the options xCAT has always written'); + +my $reexport = xCAT::SvrUtils->nfs_export_line('/install', reexport => 1); +like($reexport, qr{^/install \*\(}, 're-export names the same directory and clients'); +like($reexport, qr{\brw\b}, '... stays read-write'); +like($reexport, qr{\bno_root_squash\b}, '... keeps no_root_squash, which the installer needs'); +like($reexport, qr{\binsecure\b}, '... keeps insecure, for clients on high ports'); +like($reexport, qr{\bfsid=\d+}, '... carries an fsid, without which exportfs refuses an NFS mount'); +like($reexport, qr{\bcrossmnt\b}, '... carries crossmnt, so the mount below it is followed'); + +# The fsid must be stable across runs, or every restart hands clients a new filesystem identity +# and their mounts go stale. +is(xCAT::SvrUtils->nfs_export_line('/install', reexport => 1), $reexport, + 'the same directory yields the same fsid every time'); + +my ($install_fsid) = $reexport =~ /fsid=(\d+)/; +my ($tftp_fsid) = xCAT::SvrUtils->nfs_export_line('/tftpboot', reexport => 1) =~ /fsid=(\d+)/; +isnt($tftp_fsid, $install_fsid, 'two directories do not share one fsid'); +cmp_ok($install_fsid, '>', 0, 'the fsid is not 0, which Linux reserves for the export root'); +# The guard that keeps it non-zero is only reachable when the checksum itself is 0, which an +# empty path produces. Drive it directly rather than claim a path that cannot reach it. +cmp_ok(xCAT::SvrUtils::_nfs_export_fsid(''), '>', 0, + 'a path whose checksum is 0 still yields a usable fsid'); + +done_testing();