2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-04 12:07:56 +00:00

Merge pull request #7791 from VersatusHPC/refactor/nfs-export-workflow

refactor(svrutils): centralize NFS export setup
This commit is contained in:
Daniel Hilst
2026-08-31 11:27:41 -03:00
committed by GitHub
3 changed files with 426 additions and 139 deletions
+44 -52
View File
@@ -1674,33 +1674,7 @@ sub setupNFSTree {
my $nfsdirectory = $2;
if ($nfsserver eq $sip) { # on the service node
unless (-d $nfsdirectory) {
if (-e $nfsdirectory) {
unlink $nfsdirectory;
}
mkpath $nfsdirectory;
}
$cmd = "showmount -e $nfsserver";
my @entries = xCAT::Utils->runcmd($cmd, 0);
shift @entries;
if (grep /\Q$nfsdirectory\E/, @entries) {
$callback->({ data => ["$nfsdirectory has been exported already!"] });
} else {
$cmd = "/usr/sbin/exportfs :$nfsdirectory -o rw,no_root_squash,sync,no_subtree_check,insecure";
xCAT::Utils->runcmd($cmd, 0);
$callback->({ data => ["now $nfsdirectory is exported!"] });
}
if (ensure_nfs_export_option($nfsdirectory, 'insecure')) {
xCAT::Utils->runcmd("/usr/sbin/exportfs -r", 0);
$callback->({ data => ["added insecure to existing $nfsdirectory export"] });
} elsif (!nfs_export_exists($nfsdirectory)) {
$cmd = qq{echo "$nfsdirectory *(rw,no_root_squash,sync,no_subtree_check,insecure)" >> /etc/exports};
xCAT::Utils->runcmd($cmd, 0);
$callback->({ data => ["$nfsdirectory is added to /etc/exports with default option"] });
}
_ensure_nfs_exported($nfsserver, $nfsdirectory, $callback);
}
}
}
@@ -1719,6 +1693,44 @@ sub nfs_export_exists {
return 0;
}
sub _ensure_nfs_exported {
my ($nfsserver, $nfsdirectory, $callback, %opts) = @_;
my $export_options = 'rw,no_root_squash,sync,no_subtree_check,insecure';
unless (-d $nfsdirectory) {
if (-e $nfsdirectory) {
unlink $nfsdirectory;
}
mkpath $nfsdirectory;
}
my $cmd = "showmount -e $nfsserver";
my @entries = xCAT::Utils->runcmd($cmd, 0);
shift @entries;
if (grep /\Q$nfsdirectory\E/, @entries) {
$callback->({ data => ["$nfsdirectory has been exported already!"] });
} else {
$cmd = "/usr/sbin/exportfs :$nfsdirectory -o $export_options";
xCAT::Utils->runcmd($cmd, 0);
$callback->({ data => ["now $nfsdirectory is exported!"] });
}
if (ensure_nfs_export_option($nfsdirectory, 'insecure')) {
xCAT::Utils->runcmd("/usr/sbin/exportfs -r", 0);
$callback->({ data => ["added insecure to existing $nfsdirectory export"] });
} elsif (!nfs_export_exists($nfsdirectory)) {
my $append_separator = exists($opts{append_separator})
? $opts{append_separator}
: ' ';
$cmd = qq{echo "$nfsdirectory *($export_options)" >>$append_separator/etc/exports};
xCAT::Utils->runcmd($cmd, 0);
my $message = exists($opts{added_message})
? $opts{added_message}
: "$nfsdirectory is added to /etc/exports with default option";
$callback->({ data => [$message] });
}
}
sub setupStatemnt {
my $sip = shift;
if (($sip) && ($sip =~ /xCAT::SvrUtils/))
@@ -1733,31 +1745,11 @@ sub setupStatemnt {
my $nfsserver = $1;
my $nfsdirectory = $2;
if ($sip eq xCAT::NetworkUtils->getipaddr($nfsserver)) {
unless (-d $nfsdirectory) {
if (-e $nfsdirectory) {
unlink $nfsdirectory;
}
mkpath $nfsdirectory;
}
my $cmd = "showmount -e $nfsserver";
my @entries = xCAT::Utils->runcmd($cmd, 0);
shift @entries;
if (grep /\Q$nfsdirectory\E/, @entries) {
$callback->({ data => ["$nfsdirectory has been exported already!"] });
} else {
$cmd = "/usr/sbin/exportfs :$nfsdirectory -o rw,no_root_squash,sync,no_subtree_check,insecure";
xCAT::Utils->runcmd($cmd, 0);
$callback->({ data => ["now $nfsdirectory is exported!"] });
}
if (ensure_nfs_export_option($nfsdirectory, 'insecure')) {
xCAT::Utils->runcmd("/usr/sbin/exportfs -r", 0);
$callback->({ data => ["added insecure to existing $nfsdirectory export"] });
} elsif (!nfs_export_exists($nfsdirectory)) {
xCAT::Utils->runcmd(qq{echo "$nfsdirectory *(rw,no_root_squash,sync,no_subtree_check,insecure)" >>/etc/exports}, 0);
$callback->({ data => ["$nfsdirectory is added into /etc/exports with default options"] });
}
_ensure_nfs_exported(
$nfsserver, $nfsdirectory, $callback,
append_separator => '',
added_message => "$nfsdirectory is added into /etc/exports with default options",
);
}
}
-87
View File
@@ -1796,91 +1796,4 @@ sub subVars()
return $fdir;
}
sub setupNFSTree {
my $node = shift;
my $sip = shift;
my $callback = shift;
my $cmd = "litetree $node";
my @uris = xCAT::Utils->runcmd($cmd, 0);
foreach my $uri (@uris) {
# parse the result
# the result looks like "nodename: nfsserver:directory";
$uri =~ m/\Q$node\E:\s+(.+):(.+)$/;
my $nfsserver = $1;
my $nfsdirectory = $2;
if ($nfsserver eq $sip) { # on the service node
unless (-d $nfsdirectory) {
if (-e $nfsdirectory) {
unlink $nfsdirectory;
}
mkpath $nfsdirectory;
}
$cmd = "showmount -e $nfsserver";
my @entries = xCAT::Utils->runcmd($cmd, 0);
shift @entries;
if (grep /\Q$nfsdirectory\E/, @entries) {
$callback->({ data => ["$nfsdirectory has been exported already!"] });
} else {
$cmd = "/usr/sbin/exportfs :$nfsdirectory -o rw,no_root_squash,sync,no_subtree_check,insecure";
xCAT::Utils->runcmd($cmd, 0);
$callback->({ data => ["now $nfsdirectory is exported!"] });
}
if (xCAT::SvrUtils->ensure_nfs_export_option($nfsdirectory, 'insecure')) {
xCAT::Utils->runcmd("/usr/sbin/exportfs -r", 0);
$callback->({ data => ["added insecure to existing $nfsdirectory export"] });
} elsif (!xCAT::SvrUtils->nfs_export_exists($nfsdirectory)) {
$cmd = qq{echo "$nfsdirectory *(rw,no_root_squash,sync,no_subtree_check,insecure)" >> /etc/exports};
xCAT::Utils->runcmd($cmd, 0);
$callback->({ data => ["$nfsdirectory is added to /etc/exports with default option"] });
}
}
}
}
sub setupStatemnt {
my $sip = shift;
my $statemnt = shift;
my $callback = shift;
$statemnt =~ m/^(.+):(.+)$/;
my $nfsserver = $1;
my $nfsdirectory = $2;
if ($sip eq inet_ntoa(inet_aton($nfsserver))) {
unless (-d $nfsdirectory) {
if (-e $nfsdirectory) {
unlink $nfsdirectory;
}
mkpath $nfsdirectory;
}
my $cmd = "showmount -e $nfsserver";
my @entries = xCAT::Utils->runcmd($cmd, 0);
shift @entries;
if (grep /\Q$nfsdirectory\E/, @entries) {
$callback->({ data => ["$nfsdirectory has been exported already!"] });
} else {
$cmd = "/usr/sbin/exportfs :$nfsdirectory -o rw,no_root_squash,sync,no_subtree_check,insecure";
xCAT::Utils->runcmd($cmd, 0);
$callback->({ data => ["now $nfsdirectory is exported!"] });
}
if (xCAT::SvrUtils->ensure_nfs_export_option($nfsdirectory, 'insecure')) {
xCAT::Utils->runcmd("/usr/sbin/exportfs -r", 0);
$callback->({ data => ["added insecure to existing $nfsdirectory export"] });
} elsif (!xCAT::SvrUtils->nfs_export_exists($nfsdirectory)) {
xCAT::Utils->runcmd(qq{echo "$nfsdirectory *(rw,no_root_squash,sync,no_subtree_check,insecure)" >> /etc/exports}, 0);
$callback->({ data => ["$nfsdirectory is added into /etc/exports with default options"] });
}
}
}
1;
+382
View File
@@ -0,0 +1,382 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use lib "$FindBin::Bin/../../perl-xCAT";
use Test::More;
use xCAT::SvrUtils;
my $export_options = 'rw,no_root_squash,sync,no_subtree_check,insecure';
for my $caller (qw(setupNFSTree setupStatemnt)) {
subtest "$caller creates active and persistent exports once" => sub {
my $result = exercise_workflow( $caller, 'missing' );
my @prefix = command_prefix( $caller, $result->{directory} );
my $append = append_command( $caller, $result->{directory} );
is_deeply(
$result->{runs}->[0]->{commands},
[
@prefix,
"/usr/sbin/exportfs :$result->{directory} -o $export_options",
$append,
],
'the first run activates and persists the missing export'
);
is_deeply(
$result->{runs}->[0]->{messages},
[
"now $result->{directory} is exported!",
added_message( $caller, $result->{directory} ),
],
'the first run retains its caller-specific messages'
);
is_deeply(
$result->{runs}->[1]->{commands},
[ command_prefix( $caller, $result->{directory} ) ],
'the second run does not repeat export mutations'
);
is_deeply(
$result->{runs}->[1]->{messages},
[ "$result->{directory} has been exported already!" ],
'the second run reports the active export'
);
ok( -d $result->{directory}, 'the missing export directory is created' );
is_deeply(
$result->{ensure_calls},
[
[ $result->{directory}, 'insecure' ],
[ $result->{directory}, 'insecure' ],
],
'persistent options are checked on every run'
);
is( $result->{exists_calls}, 2,
'persistent existence is checked when no option changes' );
is_deeply(
$result->{command_args}, expected_command_args( $result->{runs} ),
'every command retains the suppressed-error mode'
);
is_deeply(
$result->{getipaddr_calls}, expected_getipaddr_calls($caller),
'the state-mount caller resolves the NFS server'
);
};
subtest "$caller reloads an export after adding insecure" => sub {
my $result = exercise_workflow( $caller, 'missing-option' );
my @prefix = command_prefix( $caller, $result->{directory} );
is_deeply(
$result->{runs}->[0]->{commands},
[ @prefix, '/usr/sbin/exportfs -r' ],
'the first run reloads after updating the persistent export'
);
is_deeply(
$result->{runs}->[0]->{messages},
[
"$result->{directory} has been exported already!",
"added insecure to existing $result->{directory} export",
],
'the option update retains its callback messages'
);
is_deeply(
$result->{runs}->[1]->{commands},
[ command_prefix( $caller, $result->{directory} ) ],
'the second run does not reload an unchanged export'
);
is_deeply(
$result->{runs}->[1]->{messages},
[ "$result->{directory} has been exported already!" ],
'the unchanged export has only the active message'
);
ok( -d $result->{directory},
'a file at the export path is replaced with a directory' );
is( $result->{exists_calls}, 1,
'persistent existence is skipped when the option changed' );
is_deeply(
$result->{command_args}, expected_command_args( $result->{runs} ),
'reload commands retain the suppressed-error mode'
);
};
subtest "$caller ignores exports hosted elsewhere" => sub {
my $result = exercise_remote_export($caller);
my @expected = $caller eq 'setupNFSTree'
? ('XCATBYPASS=Y litetree node1')
: ();
is_deeply( $result->{commands}, \@expected,
'no export command runs for a remote server' );
is_deeply( $result->{messages}, [],
'no callback message is emitted for a remote server' );
ok( !-e $result->{directory},
'a remote export directory is not created locally' );
is_deeply(
$result->{command_args},
[ map { [ $_, 0 ] } @expected ],
'remote-server checks retain the suppressed-error mode'
);
my $expected_getipaddr = $caller eq 'setupStatemnt'
? [ ['nfs-server'] ]
: [];
is_deeply( $result->{getipaddr_calls}, $expected_getipaddr,
'only the state-mount caller resolves the remote server' );
};
}
subtest 'setupNFSTree handles multiple litetree entries independently' => sub {
my $result = exercise_multiple_tree_uris();
is_deeply(
$result->{command_args},
[
[ 'XCATBYPASS=Y litetree node1', 0 ],
[ 'showmount -e nfs-server', 0 ],
],
'only the local URI reaches the export workflow'
);
is_deeply(
$result->{messages},
[ "$result->{local_directory} has been exported already!" ],
'the local URI emits one callback message'
);
ok( -d $result->{local_directory}, 'the local URI directory is created' );
ok( !-e $result->{remote_directory},
'the remote URI directory is not created' );
};
done_testing();
sub exercise_workflow {
my ( $caller, $scenario ) = @_;
my $root = tempdir( CLEANUP => 1 );
my $directory = "$root/export";
if ( $scenario eq 'missing-option' ) {
open( my $fh, '>', $directory )
or die "Unable to create $directory: $!";
close($fh) or die "Unable to close $directory: $!";
}
my $active = $scenario eq 'missing' ? 0 : 1;
my $persistent = $scenario eq 'missing' ? 0 : 1;
my $option_missing = $scenario eq 'missing-option' ? 1 : 0;
my @commands;
my @command_args;
my @getipaddr_calls;
my @messages;
my @ensure_calls;
my $exists_calls = 0;
my @runs;
no warnings 'redefine';
local *xCAT::Utils::runcmd = sub {
my @args = @_;
shift @args if @args && $args[0] eq 'xCAT::Utils';
my ($command) = @args;
push @commands, $command;
push @command_args, [@args];
if ( $command eq 'XCATBYPASS=Y litetree node1' ) {
return ("node1: nfs-server:$directory");
}
if ( $command eq 'showmount -e nfs-server' ) {
return $active
? ( 'Export list for nfs-server:', "$directory *" )
: ('Export list for nfs-server:');
}
if ( $command eq "/usr/sbin/exportfs :$directory -o $export_options" ) {
$active = 1;
} elsif ( $command eq append_command( $caller, $directory ) ) {
$persistent = 1;
}
return;
};
local *xCAT::NetworkUtils::getipaddr = sub {
my @args = @_;
shift @args if @args && $args[0] eq 'xCAT::NetworkUtils';
push @getipaddr_calls, [@args];
return '192.0.2.10';
};
local *xCAT::SvrUtils::ensure_nfs_export_option = sub {
push @ensure_calls, [@_];
if ($option_missing) {
$option_missing = 0;
return 1;
}
return 0;
};
local *xCAT::SvrUtils::nfs_export_exists = sub {
$exists_calls++;
return $persistent;
};
my $callback = sub {
my ($response) = @_;
push @messages, $response->{data}->[0];
};
for ( 1 .. 2 ) {
my $command_start = scalar @commands;
my $message_start = scalar @messages;
invoke_caller( $caller, $directory, $callback );
push @runs,
{
commands => [ @commands[ $command_start .. $#commands ] ],
messages => [ @messages[ $message_start .. $#messages ] ],
};
}
return {
command_args => \@command_args,
directory => $directory,
ensure_calls => \@ensure_calls,
exists_calls => $exists_calls,
getipaddr_calls => \@getipaddr_calls,
runs => \@runs,
};
}
sub exercise_remote_export {
my ($caller) = @_;
my $root = tempdir( CLEANUP => 1 );
my $directory = "$root/export";
my @command_args;
my @commands;
my @getipaddr_calls;
my @messages;
no warnings 'redefine';
local *xCAT::Utils::runcmd = sub {
my @args = @_;
shift @args if @args && $args[0] eq 'xCAT::Utils';
my ($command) = @args;
push @commands, $command;
push @command_args, [@args];
return ("node1: nfs-server:$directory")
if $command eq 'XCATBYPASS=Y litetree node1';
return;
};
local *xCAT::NetworkUtils::getipaddr = sub {
my @args = @_;
shift @args if @args && $args[0] eq 'xCAT::NetworkUtils';
push @getipaddr_calls, [@args];
return '192.0.2.10';
};
local *xCAT::SvrUtils::ensure_nfs_export_option = sub {
die 'persistent export options must not be checked for a remote server';
};
local *xCAT::SvrUtils::nfs_export_exists = sub {
die 'persistent exports must not be checked for a remote server';
};
my $callback = sub {
my ($response) = @_;
push @messages, $response->{data}->[0];
};
if ( $caller eq 'setupNFSTree' ) {
xCAT::SvrUtils->setupNFSTree( 'node1', 'other-server', $callback );
} else {
xCAT::SvrUtils->setupStatemnt(
'192.0.2.11', "nfs-server:$directory", $callback
);
}
return {
command_args => \@command_args,
commands => \@commands,
directory => $directory,
getipaddr_calls => \@getipaddr_calls,
messages => \@messages,
};
}
sub exercise_multiple_tree_uris {
my $root = tempdir( CLEANUP => 1 );
my $local_directory = "$root/local";
my $remote_directory = "$root/remote";
my @command_args;
my @messages;
no warnings 'redefine';
local *xCAT::Utils::runcmd = sub {
my @args = @_;
shift @args if @args && $args[0] eq 'xCAT::Utils';
my ($command) = @args;
push @command_args, [@args];
if ( $command eq 'XCATBYPASS=Y litetree node1' ) {
return (
"node1: other-server:$remote_directory",
"node1: nfs-server:$local_directory",
);
}
if ( $command eq 'showmount -e nfs-server' ) {
return ( 'Export list for nfs-server:', "$local_directory *" );
}
return;
};
local *xCAT::SvrUtils::ensure_nfs_export_option = sub { return 0; };
local *xCAT::SvrUtils::nfs_export_exists = sub { return 1; };
my $callback = sub {
my ($response) = @_;
push @messages, $response->{data}->[0];
};
xCAT::SvrUtils->setupNFSTree( 'node1', 'nfs-server', $callback );
return {
command_args => \@command_args,
local_directory => $local_directory,
messages => \@messages,
remote_directory => $remote_directory,
};
}
sub invoke_caller {
my ( $caller, $directory, $callback ) = @_;
if ( $caller eq 'setupNFSTree' ) {
xCAT::SvrUtils->setupNFSTree( 'node1', 'nfs-server', $callback );
} else {
xCAT::SvrUtils->setupStatemnt(
'192.0.2.10', "nfs-server:$directory", $callback
);
}
}
sub command_prefix {
my ( $caller, $directory ) = @_;
my @commands;
push @commands, 'XCATBYPASS=Y litetree node1'
if $caller eq 'setupNFSTree';
push @commands, 'showmount -e nfs-server';
return @commands;
}
sub append_command {
my ( $caller, $directory ) = @_;
my $separator = $caller eq 'setupNFSTree'
? ' >> /etc/exports'
: ' >>/etc/exports';
return qq{echo "$directory *($export_options)"$separator};
}
sub added_message {
my ( $caller, $directory ) = @_;
return $caller eq 'setupNFSTree'
? "$directory is added to /etc/exports with default option"
: "$directory is added into /etc/exports with default options";
}
sub expected_command_args {
my ($runs) = @_;
my @commands = map { @{ $_->{commands} } } @{$runs};
return [ map { [ $_, 0 ] } @commands ];
}
sub expected_getipaddr_calls {
my ($caller) = @_;
return $caller eq 'setupStatemnt'
? [ [ 'nfs-server' ], [ 'nfs-server' ] ]
: [];
}