diff --git a/xCAT-test/unit/genesis_base_spec_buildrequires.t b/xCAT-test/unit/genesis_base_spec_buildrequires.t new file mode 100644 index 000000000..621f3a001 --- /dev/null +++ b/xCAT-test/unit/genesis_base_spec_buildrequires.t @@ -0,0 +1,42 @@ +#!/usr/bin/env perl +# The genesis spec is the build root manifest: what it does not build-require, the buildroot +# only holds by accident, and dracut_install then installs nothing. +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../lib"; +use Test::More; + +use XCAT::Test::File qw(repo_path slurp_repo_file); + +my $relative = 'xCAT-genesis-builder/xCAT-genesis-base.spec'; +plan skip_all => "$relative not found" unless -f repo_path($relative); +plan tests => 4; + +my @lines = split /\n/, slurp_repo_file($relative); + +# getcert, getdestiny, getipmi and getadapter all run the openssl command. el8 and el9 +# held it in the buildroot as a dependency of something else; el10 does not. +my @openssl = grep { /^BuildRequires:\s*openssl\s*$/ } @lines; +is(scalar(@openssl), 1, 'the spec build-requires openssl'); + +my ($buildarch) = grep { $lines[$_] =~ /^BuildArch:\s*noarch/ } 0 .. $#lines; +ok(defined $buildarch, 'the spec sets BuildArch: noarch'); + +# rpm reads the spec a second time with the target set to noarch, so %{_target_cpu} is +# "noarch" from BuildArch onwards. %{tarch} keeps the real architecture. +my @late_target_cpu = grep { $lines[$_] =~ /_target_cpu/ } ($buildarch + 1) .. $#lines; +is(scalar(@late_target_cpu), 0, + '%{_target_cpu} is not read after BuildArch: noarch') + or diag(join "\n", map { ($_ + 1) . ": $lines[$_]" } @late_target_cpu); + +my ($openssl_line) = grep { $lines[$_] =~ /^BuildRequires:\s*openssl\s*$/ } 0 .. $#lines; +my $guarded = 0; +if (defined $openssl_line) { + for my $i (reverse 0 .. $openssl_line - 1) { + last if $lines[$i] =~ /^%endif/; + $guarded = 1, last if $lines[$i] =~ /^%if/; + } +} +is($guarded, 0, 'openssl is build-required on every release'); diff --git a/xCAT-test/unit/genesis_getcert_missing_openssl.t b/xCAT-test/unit/genesis_getcert_missing_openssl.t new file mode 100644 index 000000000..4e7398d9c --- /dev/null +++ b/xCAT-test/unit/genesis_getcert_missing_openssl.t @@ -0,0 +1,81 @@ +#!/usr/bin/env perl +# Drive getcert with openssl absent, and with a certificate key that is not ready yet. +# doxcat runs getcert in the foreground and ignores its status, so a wait with no bound stops +# the boot and prints nothing. +use strict; +use warnings; + +use File::Path qw(make_path); +use File::Slurper qw(read_text write_text); +use File::Temp qw(tempdir); +use FindBin; +use lib "$FindBin::Bin/../lib"; +use Test::More; + +use XCAT::Test::File qw(repo_path); + +my $getcert = repo_path('xCAT-genesis-scripts/usr/bin/getcert'); +plan skip_all => 'getcert not found' unless -f $getcert; +plan tests => 7; + +my $tmpdir = tempdir(CLEANUP => 1); + +# The el10 legacy image ships no openssl. getcert must say so and give up. +my $bin = stub_dir(openssl => undef); +my ($status, $out) = run_getcert($bin, 10, 60); +isnt($status, 124, 'getcert without openssl stops on its own') or diag($out); +isnt($status, 0, 'getcert without openssl reports a failure'); +like($out, qr/openssl/, 'getcert names openssl'); + +# doxcat writes /etc/xcat/certkey.pem in the background, so the first requests can fail. +# getcert must keep asking, then give up and say why. +my $counter = "$tmpdir/req-count"; +$bin = stub_dir(openssl => "always-fails", counter => $counter); +($status, $out) = run_getcert($bin, 30, 5); +isnt($status, 124, 'getcert with an unusable key stops on its own') or diag($out); +isnt($status, 0, 'getcert with an unusable key reports a failure'); +my $tries = -f $counter ? scalar(() = read_text($counter) =~ /req/g) : 0; +cmp_ok($tries, '>', 1, "getcert retries the certificate request ($tries tries)"); +like($out, qr/certkey\.pem/, 'getcert names the key it could not use'); + +#--- +# stub_dir: a PATH directory holding the commands getcert runs. openssl is absent when the +# openssl option is undef. +#--- +sub stub_dir { + my (%opt) = @_; + my $dir = tempdir(DIR => $tmpdir, CLEANUP => 1); + write_stub($dir, 'allowcred.awk', "exec sleep 3\n"); + write_stub($dir, 'hostname', "echo node1\n"); + write_stub($dir, 'logger', "echo \"\$@\" >&2\n"); + write_stub($dir, 'sleep', "exec /bin/sleep \"\$@\"\n"); + if (defined $opt{openssl}) { + my $count = $opt{counter} ? "echo req >> '$opt{counter}'\n" : ''; + write_stub($dir, 'openssl', "[ \"\$1\" = req ] && { $count exit 1; }\nexit 0\n"); + } + return $dir; +} + +sub write_stub { + my ($dir, $name, $body) = @_; + write_text("$dir/$name", "#!/bin/sh\n$body"); + chmod 0755, "$dir/$name"; + return; +} + +#--- +# run_getcert: run getcert with only the stub directory on PATH. The timeout is the harness +# guard: a status of 124 means getcert never stopped. +#--- +sub run_getcert { + my ($bin, $limit, $csr_timeout) = @_; + my $outfile = "$tmpdir/out.$$"; + my $cmd = sprintf( + "timeout -k 2 %d env PATH=%s GETCERT_CSR_TIMEOUT=%d /bin/bash %s 192.0.2.1:3001 >%s 2>&1 > 8; + my $out = -f $outfile ? read_text($outfile) : ''; + unlink $outfile; + return ($status, $out); +} diff --git a/xCAT-test/unit/genesis_payload_verification.t b/xCAT-test/unit/genesis_payload_verification.t index 3e1d7bffa..c845b3cd9 100644 --- a/xCAT-test/unit/genesis_payload_verification.t +++ b/xCAT-test/unit/genesis_payload_verification.t @@ -15,9 +15,10 @@ 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 => 11; +plan tests => 18; my $tmpdir = tempdir(CLEANUP => 1); +my $module_seq = 0; # 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, mktemp => 1); @@ -55,6 +56,39 @@ my $nomktemp = build_payload(sshd_execs_session => 1, session_helper => 1, tmux isnt($rc, 0, 'a payload without mktemp fails'); like($err, qr{usr/bin/mktemp}, 'the missing mktemp is named'); +# dracut_install reports a missing binary and returns, so every name the dracut module +# installs has to be checked against the payload. The el10 image shipped with no openssl and +# getcert waited on it for the life of the node. +my $module = write_module_setup([qw(openssl wget tar)]); +my $full = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, + dhclient => 1, mktemp => 1, commands => [qw(openssl wget tar)]); +($rc, $err) = run_with_commands($module, $full); +is($rc, 0, 'a payload carrying every command the module names passes') or diag($err); + +my $noopenssl = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, + dhclient => 1, mktemp => 1, commands => [qw(wget tar)]); +($rc, $err) = run_with_commands($module, $noopenssl); +isnt($rc, 0, 'a payload without openssl fails'); +like($err, qr/openssl/, 'the missing openssl is named'); + +# The DHCP client is release-dependent, so the module installs it inside a conditional. Those +# names are not the contract; the spec passes the one it wants as a required path. +my $conditional = write_module_setup(['wget'], ['dhclient']); +my $nodhclient = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, + dhclient => 0, mktemp => 1, commands => ['wget']); +($rc, $err) = run_with_commands($conditional, $nodhclient); +is($rc, 0, 'a name installed under a condition is not required') or diag($err); + +# A module the verifier cannot read names for covers nothing, so say so instead of passing. +my $unparsable = "$tmpdir/module-setup-unparsable.sh"; +write_text($unparsable, "#!/bin/bash\nsetup() {\n dracut_install wget\n}\n"); +($rc, $err) = run_with_commands($unparsable, $full); +is($rc, 2, 'a module the verifier finds no command names in is a usage error'); +like($err, qr/command name/, 'the empty command list is named'); + +($rc, $err) = run_with_commands("$tmpdir/no-such-module", $full); +is($rc, 2, 'a module file that cannot be read is a usage error'); + ($rc, $err) = run("$tmpdir/does-not-exist"); is($rc >> 0, 2, 'a missing payload directory is a usage error'); @@ -77,6 +111,7 @@ sub build_payload { } write_text("$root/usr/sbin/dhclient", "dhclient\n") if $opt{dhclient}; write_text("$root/usr/bin/mktemp", "mktemp\n") if $opt{mktemp}; + write_text("$root/usr/bin/$_", "$_\n") for @{ $opt{commands} || [] }; return $root; } @@ -93,3 +128,37 @@ sub run { unlink $errfile; return ($status, $err); } + +#--- +# write_module_setup: a dracut module whose install() names commands at the top level, and +# optionally more inside a conditional. +#--- +sub write_module_setup { + my ($top, $conditional) = @_; + my $path = "$tmpdir/module-setup." . ++$module_seq . ".sh"; + my $text = "#!/bin/bash\n\ninstall() {\n"; + $text .= " dracut_install " . join(' ', @$top) . " # a trailing comment\n"; + $text .= " dracut_install /usr/bin/awk /etc/services\n"; + if ($conditional) { + $text .= " if command -v " . $conditional->[0] . " >/dev/null 2>&1; then\n"; + $text .= " dracut_install " . join(' ', @$conditional) . "\n"; + $text .= " fi\n"; + } + $text .= "}\n"; + write_text($path, $text); + return $path; +} + +#--- +# run_with_commands: run the verifier with the command list read back from a dracut module. +#--- +sub run_with_commands { + my ($module, $root) = @_; + my $errfile = "$tmpdir/err.commands.$$"; + my $cmd = join ' ', map { "'$_'" } ($verifier, '--commands-from', $module, $root); + system("/bin/bash $cmd >/dev/null 2>$errfile"); + my $status = $? >> 8; + my $err = -f $errfile ? read_text($errfile) : ''; + unlink $errfile; + return ($status, $err); +}