2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-24 16:54:03 +00:00

test(xcat-core): capture the el10 Genesis image shipping no openssl

The legacy Genesis image built on el10 carries no openssl command. getcert waits for one
with no bound, so the node reports no destiny and never boots. Nothing in the build or in
the suite sees the hole.

genesis_base_spec_buildrequires.t reads the spec and requires an unconditional
BuildRequires on openssl, and requires that no %{_target_cpu} is read after
BuildArch: noarch, where rpm has already set it to noarch.

genesis_payload_verification.t drives verify-genesis-payload with a dracut module and a
payload, and requires every command the module installs at the top level of install() to be
present. A name installed under a condition is not required, and a module the verifier
reads no names from is a usage error.

genesis_getcert_missing_openssl.t runs getcert with a PATH that holds stubs and no openssl,
under a harness timeout. Ten assertions fail on this commit: getcert never stops, and the
verifier and the spec do not know about openssl.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
Daniel Hilst
2026-09-04 23:13:18 -03:00
parent 4c8162824d
commit 084d776d26
3 changed files with 193 additions and 1 deletions
@@ -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');
@@ -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 </dev/null",
$limit, $bin, $csr_timeout, $getcert, $outfile);
system($cmd);
my $status = $? >> 8;
my $out = -f $outfile ? read_text($outfile) : '';
unlink $outfile;
return ($status, $out);
}
+70 -1
View File
@@ -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);
}