diff --git a/xCAT-test/unit/genesis_incorrectmasterip_check.t b/xCAT-test/unit/genesis_incorrectmasterip_check.t index 2b80dde47..a32b1a23f 100644 --- a/xCAT-test/unit/genesis_incorrectmasterip_check.t +++ b/xCAT-test/unit/genesis_incorrectmasterip_check.t @@ -15,11 +15,14 @@ use XCAT::Test::File qw(repo_path); my $script = repo_path('xCAT-test/autotest/testcase/genesis/test.sh'); plan skip_all => 'genesis test.sh not found' unless -f $script; -plan tests => 5; +plan tests => 9; my $host_arch = `uname -m`; chomp $host_arch; +# grub2.pm names the boot loader grub2., with every ppc64 flavour written as "ppc". +my $loader_name = $host_arch =~ /^ppc64/ ? 'ppc' : $host_arch; + # The case defined its node as ppc64le whatever the management node was, so nodeset could not # find a genesis kernel for it on x86_64 and the case could never pass there. my $run = run_check('xnba', write_boot_file => 1); @@ -39,6 +42,20 @@ my $grub = run_check('grub2', write_boot_file => 1); is($grub->{status}, 0, 'the grub2 check reads the grub2 directory') or diag($grub->{output}); +# grub2.pm writes the boot configuration and only then stops on a missing boot loader. The +# check read the file that failed nodeset had already written, so it passed on the debris. +my $refused = run_check('grub2', write_boot_file => 1, nodeset_status => 1); +isnt($refused->{status}, 0, 'a nodeset that fails makes the check fail'); + +my $refused_xnba = run_check('xnba', write_boot_file => 1, nodeset_status => 1); +isnt($refused_xnba->{status}, 0, 'a nodeset that fails makes the xnba check fail too'); + +# xCAT builds no x86_64 or aarch64 grub2 network boot loader, so grub2.pm stops before it +# configures anything. The check stages one for the node arch and removes it after. +is($grub->{loader_at_nodeset}, "yes\n", + 'the grub2 boot loader for the node arch is in place when nodeset runs'); +ok(!$grub->{loader_left}, 'the staged boot loader is removed again'); + #--- # run_check: run `test.sh --check ` against a scratch tftp root. test.sh resets PATH, # so the xCAT commands are shadowed with shell functions, which bash resolves first. The fake @@ -49,6 +66,7 @@ sub run_check { my $root = tempdir(CLEANUP => 1); my $tftp = "$root/tftpboot"; make_path("$tftp/xcat/xnba/nodes", "$tftp/boot/grub2", "$tftp/petitboot"); + my $boot_loader = "$tftp/boot/grub2/grub2.$loader_name"; my $folder = $loader eq 'xnba' ? "$tftp/xcat/xnba/nodes" : $loader eq 'petitboot' ? "$tftp/petitboot" @@ -71,7 +89,11 @@ makenetworks() { return 0; } tabdump() { return 0; } makehosts() { return 0; } rmdef() { return 0; } -nodeset() { $write; return 0; } +nodeset() { + if [ -e '$boot_loader' ]; then echo yes > '$root/loader.at.nodeset'; else echo no > '$root/loader.at.nodeset'; fi + $write + return @{[ $opt{nodeset_status} || 0 ]}; +} export TFTPDIR='$tftp' . '$script' --check $loader DRIVER @@ -79,6 +101,12 @@ DRIVER my $out = `/bin/bash "$driver" 2>&1`; my $status = $? >> 8; my $chdef = -f "$root/chdef.log" ? read_text("$root/chdef.log") : ''; - return { status => $status, output => $out, chdef => $chdef }; + return { + status => $status, + output => $out, + chdef => $chdef, + loader_at_nodeset => (-f "$root/loader.at.nodeset" ? read_text("$root/loader.at.nodeset") : ''), + loader_left => (-e $boot_loader ? 1 : 0), + }; } diff --git a/xCAT-test/unit/genesis_payload_verification.t b/xCAT-test/unit/genesis_payload_verification.t index 3ecf80e98..3e1d7bffa 100644 --- a/xCAT-test/unit/genesis_payload_verification.t +++ b/xCAT-test/unit/genesis_payload_verification.t @@ -15,39 +15,46 @@ use XCAT::Test::File qw(repo_path); my $verifier = repo_path('xCAT-genesis-builder/verify-genesis-payload'); plan skip_all => 'verify-genesis-payload not found' unless -f $verifier; -plan tests => 9; +plan tests => 11; my $tmpdir = tempdir(CLEANUP => 1); # A complete payload: OpenSSH 9.9 sshd plus its session helper, tmux plus a UTF-8 locale. -my $good = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, dhclient => 1); +my $good = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, dhclient => 1, mktemp => 1); my ($rc, $err) = run($good, 'usr/sbin/dhclient'); is($rc, 0, 'a complete payload passes') or diag($err); # doxcat calls dhclient with ISC flags. The released el9 image carried dhclient.conf and # dhclient-script but no dhclient, so Genesis never acquired an address. -my $nodhcp = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, dhclient => 0); +my $nodhcp = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, dhclient => 0, mktemp => 1); ($rc, $err) = run($nodhcp, 'usr/sbin/dhclient'); isnt($rc, 0, 'a payload without dhclient fails'); like($err, qr{usr/sbin/dhclient}, 'the missing dhclient is named'); # sshd 9.9 execs /usr/libexec/openssh/sshd-session for every connection. -my $nohelper = build_payload(sshd_execs_session => 1, session_helper => 0, tmux => 1, locale => 1, dhclient => 1); +my $nohelper = build_payload(sshd_execs_session => 1, session_helper => 0, tmux => 1, locale => 1, dhclient => 1, mktemp => 1); ($rc, $err) = run($nohelper, 'usr/sbin/dhclient'); isnt($rc, 0, 'a payload whose sshd execs sshd-session but does not ship it fails'); like($err, qr{sshd-session}, 'the missing sshd-session is named'); # OpenSSH 8 does not use the helper, so el8 must still pass without it. -my $openssh8 = build_payload(sshd_execs_session => 0, session_helper => 0, tmux => 1, locale => 1, dhclient => 1); +my $openssh8 = build_payload(sshd_execs_session => 0, session_helper => 0, tmux => 1, locale => 1, dhclient => 1, mktemp => 1); ($rc, $err) = run($openssh8, 'usr/sbin/dhclient'); is($rc, 0, 'an OpenSSH 8 payload passes without sshd-session') or diag($err); # tmux without a UTF-8 locale is what stopped doxcat from ever running. -my $nolocale = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 0, dhclient => 1); +my $nolocale = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 0, dhclient => 1, mktemp => 1); ($rc, $err) = run($nolocale, 'usr/sbin/dhclient'); isnt($rc, 0, 'a payload with tmux and no UTF-8 locale fails'); like($err, qr{C\.utf8}, 'the missing locale is named'); +# getdestiny makes its request file with mktemp. Without it the node never reports its destiny, +# so xcatd never sets nodelist.status and the node stays at powering-on. +my $nomktemp = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, dhclient => 1, mktemp => 0); +($rc, $err) = run($nomktemp, 'usr/sbin/dhclient'); +isnt($rc, 0, 'a payload without mktemp fails'); +like($err, qr{usr/bin/mktemp}, 'the missing mktemp is named'); + ($rc, $err) = run("$tmpdir/does-not-exist"); is($rc >> 0, 2, 'a missing payload directory is a usage error'); @@ -69,6 +76,7 @@ sub build_payload { write_text("$root/usr/lib/locale/C.utf8/LC_CTYPE", "ctype\n"); } write_text("$root/usr/sbin/dhclient", "dhclient\n") if $opt{dhclient}; + write_text("$root/usr/bin/mktemp", "mktemp\n") if $opt{mktemp}; return $root; } diff --git a/xCAT-test/unit/genesis_testcase_helpers.t b/xCAT-test/unit/genesis_testcase_helpers.t index de035f331..b07c51594 100644 --- a/xCAT-test/unit/genesis_testcase_helpers.t +++ b/xCAT-test/unit/genesis_testcase_helpers.t @@ -16,13 +16,20 @@ use XCAT::Test::File qw(repo_path); my $helper = repo_path('xCAT-test/autotest/testcase/genesis/genesistest.pl'); my $shell = repo_path('xCAT-test/autotest/testcase/genesis/test.sh'); plan skip_all => 'genesis testcase helpers not found' unless -f $helper && -f $shell; -plan tests => 11; +plan tests => 16; my $tmpdir = tempdir(CLEANUP => 1); my $source = read_text($helper); eval_subs($source, qw(get_os get_arch check_genesis_file)); +# The destiny status check used to be wait_for_boot(), which waited for "booted" and ignored +# its argument. Take whichever name the script carries, so this test fails on the status the +# check waits for and not on a missing subroutine. +my $waiter_name = waiter_name($source); +eval_subs($source, $waiter_name); +my $waiter = \&{"GenesisTest::$waiter_name"}; + # get_os drives every later branch. AlmaLinux and Rocky release files say neither "Red Hat" # nor "suse" nor "ubuntu", so the management node read as unknown and the check was skipped. is(os_for("AlmaLinux release 9.8 (Olive Jaguar)\n"), 'redhat', 'AlmaLinux is a redhat family node'); @@ -57,6 +64,60 @@ is(os_for("NAME=\"Ubuntu\"\nID=ubuntu\n"), 'ubuntu', 'Ubuntu is still like($run->{makeknownhosts}, qr/-r/, 'makeknownhosts is asked to remove them'); } +# xCAT sets nodelist.status from the destiny the node reports with getdestiny: "shell" for the +# shell destiny, "configuring" for runcmd. A Genesis node never reaches "booted" -- that status +# belongs to an operating system install reporting through updateflag. +{ + no warnings 'once'; + local $GenesisTest::noderange = 'xcat71-cn'; + is(wait_status('shell', 'shell'), 0, + 'a node that reports the shell destiny ends the wait'); + is(wait_status('configuring', 'configuring'), 0, + 'a node that reports the runcmd destiny ends the wait'); + isnt(wait_status('powering-on', 'shell'), 0, + 'a node that never reports its destiny fails the wait'); +} + +# The shell case ignored the result of the wait, so it went on to xdsh whatever the node had +# reported and rested entirely on the xdsh probes. +{ + no warnings 'once'; + eval_subs($source, qw(run_nodeset_shell_test)); + local $GenesisTest::noderange = 'xcat71-cn'; + is(run_shell_test(status => 'shell'), 0, + 'the shell case passes when the node reports the shell destiny'); + isnt(run_shell_test(status => 'powering-on'), 0, + 'the shell case fails when the node never reports the shell destiny'); +} + +#--- +# wait_status: drive the destiny status check with lsdef shadowed to report one status. The +# extracted package neuters sleep, so the failure path does not wait five minutes. +#--- +sub wait_status { + my ($reported, $expected) = @_; + local $ENV{PATH} = stub_bin(lsdef => + "#!/bin/sh\nprintf 'xcat71-cn: status=%s\\n' " . shell_quote($reported)) . ":$ENV{PATH}"; + return $waiter->($expected); +} + +#--- +# run_shell_test: drive the shell case with every command it runs shadowed. xdsh always answers +# as a Genesis node, so the only thing under test is what the case does with the node status. +#--- +sub run_shell_test { + my (%opt) = @_; + my $dir = tempdir(DIR => $tmpdir, CLEANUP => 1); + write_text("$dir/nodeset", "#!/bin/sh\nexit 0\n"); + write_text("$dir/rpower", "#!/bin/sh\nexit 0\n"); + write_text("$dir/makeknownhosts", "#!/bin/sh\nexit 0\n"); + write_text("$dir/lsdef", "#!/bin/sh\nprintf 'xcat71-cn: status=%s\\n' " . shell_quote($opt{status}) . "\n"); + write_text("$dir/xdsh", "#!/bin/sh\nfor a in \"\$@\"; do\n case \"\$a\" in\n */cmdline|/proc/cmdline) printf '%s\\n' 'destiny=shell'; exit 0;;\n esac\ndone\nprintf '%s\\n' '[xCAT Genesis running on node]'\n"); + chmod 0755, map { "$dir/$_" } qw(nodeset rpower makeknownhosts lsdef xdsh); + local $ENV{PATH} = "$dir:$ENV{PATH}"; + return GenesisTest::run_nodeset_shell_test(); +} + #--- # run_testxdsh: drive testxdsh with makeknownhosts and xdsh shadowed. xdsh is asked twice -- # once for the prompt, once for the file -- and the stub answers both from its arguments. @@ -81,6 +142,8 @@ sub run_testxdsh { sub eval_subs { my ($text, @names) = @_; my $code = "package GenesisTest;\nno strict;\nno warnings;\nour \$os;\nour \$check_genesis_file;\nour \$noderange;\n"; + # The waits are minutes long. Neuter sleep so the extracted routines run at test speed. + $code .= "use subs qw(sleep);\nsub sleep { \$GenesisTest::SLEPT += (\$_[0] || 0); return 1; }\n"; $code .= "sub send_msg { push \@GenesisTest::MSG, \$_[1]; return 0; }\n"; foreach my $name (@names) { my ($body) = $text =~ /^(sub \Q$name\E \{.*?^\})$/ms; @@ -91,6 +154,17 @@ sub eval_subs { eval $code or BAIL_OUT("cannot compile the extracted helpers: $@"); } +#--- +# waiter_name: the name the script gives its destiny status check. +#--- +sub waiter_name { + my ($text) = @_; + foreach my $name (qw(wait_for_node_status wait_for_boot)) { + return $name if $text =~ /^sub \Q$name\E \{/m; + } + BAIL_OUT("no destiny status check found in $helper"); +} + #--- # os_for: run get_os with `cat` shadowed so it reads the release text under test. #---