diff --git a/xCAT-server/lib/xcat/plugins/mknb.pm b/xCAT-server/lib/xcat/plugins/mknb.pm index 891310e2d..be6ab48fc 100644 --- a/xCAT-server/lib/xcat/plugins/mknb.pm +++ b/xCAT-server/lib/xcat/plugins/mknb.pm @@ -327,6 +327,41 @@ sub genesis_lzma_command { return; } +#------------------------------------------------------------------------------- + +=head3 stage_genesis_payload + +Descriptions: + Copy the Genesis payload into place for mknb: for a legacy image the unpacked + root tree and then the kernel, for an exported image the nbroot tree. + + Extracted so the outcome can be driven directly. The copies are the only + place mknb learns that an installed Genesis image is unusable, and a caller + cannot tell WHICH copy failed from a single exit status. + +Arguments: + genesis_type, genesis_dir, tftpdir, arch, tempdir, and an optional run + coderef used in place of system() by the tests. +Returns: + (rc, source) -- rc is the exit status of the copy that failed, and source + names it, so the caller reports the file it could not read. + +=cut + +#------------------------------------------------------------------------------- +sub stage_genesis_payload { + my (%a) = @_; + my $run = $a{run} || sub { return system($_[0]); }; + my $rc; + if (($a{genesis_type} // '') eq 'legacy') { + $rc = $run->("shopt -s dotglob; GLOBIGNORE=\".:..\" cp -a $a{genesis_dir}/fs/* $a{tempdir}"); + $rc = $run->("cp -a $a{genesis_dir}/kernel $a{tftpdir}/xcat/genesis.kernel.$a{arch}"); + return ($rc, "$a{genesis_dir}/fs"); + } + $rc = $run->("cp -a $a{genesis_dir}/nbroot/* $a{tempdir}"); + return ($rc, "$a{genesis_dir}/nbroot"); +} + sub process_request { my $request = shift; my $callback = shift; @@ -555,21 +590,13 @@ sub process_request { unless (-e "$tftpdir/xcat") { mkpath("$tftpdir/xcat"); } - my $rc; - if ($genesis_type eq 'legacy') { - $rc = system("shopt -s dotglob; GLOBIGNORE=\".:..\" cp -a $genesis_dir/fs/* $tempdir"); - $rc = system("cp -a $genesis_dir/kernel $tftpdir/xcat/genesis.kernel.$arch"); - $invisibletouch = 1; - } else { - $rc = system("cp -a $genesis_dir/nbroot/* $tempdir"); - } + $invisibletouch = 1 if $genesis_type eq 'legacy'; + my ($rc, $failed_src) = stage_genesis_payload( + genesis_type => $genesis_type, genesis_dir => $genesis_dir, + tftpdir => $tftpdir, arch => $arch, tempdir => $tempdir); if ($rc) { system("rm -rf $tempdir"); - if ($invisibletouch) { - $callback->({ error => ["Failed to copy $genesis_dir/fs contents"], errorcode => [1] }); - } else { - $callback->({ error => ["Failed to copy $genesis_dir/nbroot contents"], errorcode => [1] }); - } + $callback->({ error => ["Failed to copy $failed_src contents"], errorcode => [1] }); return; } my $sshdir; diff --git a/xCAT-test/unit/mknb_genesis_staging.t b/xCAT-test/unit/mknb_genesis_staging.t new file mode 100644 index 000000000..262401f41 --- /dev/null +++ b/xCAT-test/unit/mknb_genesis_staging.t @@ -0,0 +1,64 @@ +#!/usr/bin/env perl +# mknb stages the Genesis payload before it can build a netboot image. Those copies are the +# only point at which mknb learns that an installed Genesis image is unusable, so a copy that +# fails silently produces an initramfs built from nothing and an exit status of 0 -- the node +# then never boots, with no error anywhere naming the cause. +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use Test::More; + +BEGIN { $INC{'xCAT/Utils.pm'} = 1; $INC{'xCAT/MsgUtils.pm'} = 1; + $INC{'xCAT/Table.pm'} = 1; $INC{'xCAT/NetworkUtils.pm'} = 1; + $INC{'xCAT/TableUtils.pm'} = 1; $INC{'xCAT_monitoring/monitorctrl.pm'} = 1; } + +require "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/mknb.pm"; + +can_ok('xCAT_plugin::mknb', 'stage_genesis_payload') + or BAIL_OUT('mknb has no stage_genesis_payload to drive'); + +# Drive the routine with a runner that fails exactly one copy, so each assertion names the +# copy it is about rather than the pair. +sub stage { + my (%opt) = @_; + my @ran; + my ($rc, $src) = xCAT_plugin::mknb::stage_genesis_payload( + genesis_type => $opt{type} // 'legacy', + genesis_dir => '/opt/xcat/share/xcat/netboot/genesis/x86_64', + tftpdir => '/tftpboot', + arch => 'x86_64', + tempdir => '/tmp/scratch', + run => sub { + my ($cmd) = @_; + push @ran, $cmd; + return ($opt{fail} && $cmd =~ /$opt{fail}/) ? 256 : 0; + }, + ); + return { rc => $rc, src => $src, ran => \@ran }; +} + +# --- legacy: both copies must be able to fail the step ----------------------- +my $ok = stage(); +is($ok->{rc}, 0, 'a legacy image whose copies both succeed stages cleanly'); +is(scalar @{ $ok->{ran} }, 2, 'the legacy path copies the root tree and the kernel'); + +my $nofs = stage(fail => qr{/fs/\*}); +isnt($nofs->{rc}, 0, 'an unreadable root tree fails the step'); +like($nofs->{src}, qr{/fs$}, 'and the failure names the root tree'); + +my $nokernel = stage(fail => qr{/kernel }); +isnt($nokernel->{rc}, 0, 'a missing kernel fails the step'); +like($nokernel->{src}, qr{/kernel$}, 'and the failure names the kernel, not the root tree'); + +# --- exported (OpenEmbedded) path ------------------------------------------- +my $nonb = stage(type => 'exported', fail => qr{/nbroot/\*}); +isnt($nonb->{rc}, 0, 'an unreadable nbroot fails the step'); +like($nonb->{src}, qr{/nbroot$}, 'and the failure names nbroot'); + +my $oknb = stage(type => 'exported'); +is($oknb->{rc}, 0, 'an exported image whose copy succeeds stages cleanly'); + +done_testing();