From 888698e38499b6279e2e48147b216a48dd2bab0f Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:22:25 -0300 Subject: [PATCH] fix(xcat-core): the Genesis build-root and payload tests assert on source text and script output, not behaviour genesis_ubuntu_build_root.t cut REQUIRED_PACKAGES out of builddeb-genesis-base with a regex and evaluated it through bash -c. It then matched the apt-cache fallback calls as text. Reformatting the script broke it, and a wrong choice between renamed packages passed it. genesis_payload_verification.t ran the verifier script and parsed its stderr. genesis_ubuntu_build_root.t now calls required_packages() with a chosen set of carried packages and asserts the exact result: the amd64 extras, the name picked for each renamed package, the order apt is asked, tzdata-legacy, and the error for a release that carries neither name. It reads the mandatory commands from XCAT::GenesisPayload, the code the build uses. genesis_payload_verification.t calls the XCAT::GenesisPayload functions with chosen payload trees and asserts the exact missing paths and results. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/unit/genesis_payload_verification.t | 297 +++++++++--------- xCAT-test/unit/genesis_ubuntu_build_root.t | 120 ++++--- 2 files changed, 201 insertions(+), 216 deletions(-) diff --git a/xCAT-test/unit/genesis_payload_verification.t b/xCAT-test/unit/genesis_payload_verification.t index 33e00f6ab..9e4871b4c 100644 --- a/xCAT-test/unit/genesis_payload_verification.t +++ b/xCAT-test/unit/genesis_payload_verification.t @@ -1,192 +1,185 @@ #!/usr/bin/env perl -# Drive verify-genesis-payload against payload trees that each leave out one thing the image -# needs. +# XCAT::GenesisPayload decides whether an extracted Genesis payload is complete. +# verify-genesis-payload calls it in the EL spec and in the Ubuntu builder. Each payload +# below leaves out one thing the image needs. use strict; use warnings; +use File::Basename qw(dirname); use File::Path qw(make_path); -use File::Slurper qw(read_text write_text); +use File::Slurper qw(write_text); use File::Temp qw(tempdir); use FindBin; use lib "$FindBin::Bin/../lib"; +use lib "$FindBin::Bin/../../xCAT-genesis-builder/lib"; use Test::More; -use XCAT::Test::File qw(repo_path); +use XCAT::GenesisPayload qw(module_commands missing_paths check_payload); -my $verifier = repo_path('xCAT-genesis-builder/verify-genesis-payload'); -if (!-f $verifier) { - # Skipping here would cover nothing: the Ubuntu Genesis build has no payload gate at - # all until this file exists. - fail('xCAT-genesis-builder/verify-genesis-payload is missing'); - done_testing(); - exit; -} -plan tests => 22; - -my $tmpdir = tempdir(CLEANUP => 1); -my $module_seq = 0; +my $OPENSSH_99 = "OpenSSH_9.9p1\n/usr/libexec/openssh/sshd-session\n"; +my $OPENSSH_80 = "OpenSSH_8.0p1\n"; # 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); -my ($rc, $err) = run($good, 'usr/sbin/dhclient'); -is($rc, 0, 'a complete payload passes') or diag($err); +my @COMPLETE = qw( + usr/sbin/sshd usr/libexec/openssh/sshd-session usr/bin/tmux + usr/lib/locale/C.utf8/LC_CTYPE usr/sbin/dhclient usr/bin/mktemp + usr/bin/awk etc/services usr/bin/openssl usr/bin/wget usr/bin/tar +); + +# The payload carries exactly the paths it is given. +sub carries { + my %present = map { $_ => 1 } @_; + return sub { $present{ $_[0] } }; +} +sub all_but { my %gone = map { $_ => 1 } @_; return carries(grep { !$gone{$_} } @COMPLETE) } + +is_deeply([ missing_paths(carries(@COMPLETE), sshd => $OPENSSH_99, required => ['usr/sbin/dhclient']) ], + [], 'a complete payload passes'); # doxcat calls dhclient with ISC flags. dhclient.conf and dhclient-script are not enough. -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'); +is_deeply([ missing_paths(all_but('usr/sbin/dhclient'), sshd => $OPENSSH_99, required => ['usr/sbin/dhclient']) ], + ['usr/sbin/dhclient (required by the build)'], + 'a payload without dhclient fails and names it'); # 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, 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, mktemp => 1); -($rc, $err) = run($openssh8, 'usr/sbin/dhclient'); -is($rc, 0, 'an OpenSSH 8 payload passes without sshd-session') or diag($err); +is_deeply([ missing_paths(all_but('usr/libexec/openssh/sshd-session'), sshd => $OPENSSH_99) ], + ['usr/libexec/openssh/sshd-session (this sshd execs it for every connection)'], + 'a payload whose sshd execs sshd-session but does not ship it fails'); # 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, 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'); +is_deeply([ missing_paths(all_but('usr/lib/locale/C.utf8/LC_CTYPE'), sshd => $OPENSSH_99) ], + ['usr/lib/locale/C.utf8/LC_CTYPE (tmux refuses to start without a UTF-8 locale)'], + 'a payload with tmux and no UTF-8 locale fails'); # getdestiny makes its request file with mktemp. -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'); +is_deeply([ missing_paths(all_but('usr/bin/mktemp'), sshd => $OPENSSH_99) ], + ['usr/bin/mktemp (getdestiny makes its request file with it)'], + 'a payload without mktemp fails'); + +# Genesis is reached over ssh. +is_deeply([ missing_paths(all_but('usr/sbin/sshd')) ], + ['usr/sbin/sshd (Genesis is reached over ssh)'], + 'a payload without sshd fails'); + +is_deeply([ missing_paths(carries(grep({ $_ ne 'usr/libexec/openssh/sshd-session' } @COMPLETE), + 'usr/lib/openssh/sshd-session'), sshd => $OPENSSH_99) ], + [], 'the Debian path of sshd-session counts'); + +# OpenSSH 8 does not use the helper, so el8 must still pass without it. +is_deeply([ missing_paths(all_but('usr/libexec/openssh/sshd-session'), sshd => $OPENSSH_80) ], + [], 'an OpenSSH 8 payload passes without sshd-session'); + +is_deeply([ missing_paths(all_but('usr/lib/locale/C.utf8/LC_CTYPE', 'usr/bin/tmux'), sshd => $OPENSSH_99) ], + [], 'a payload without tmux needs no locale'); # dracut_install reports a missing binary and returns, so every name the dracut module -# installs has to be checked against the payload. -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); +# installs has to be checked against the payload. A name starting with "/" is installed at +# that same path; the rest are commands. +my $tmpdir = tempdir(CLEANUP => 1); +my $module = "$tmpdir/module-setup.sh"; +write_text($module, <<'SH'); +#!/bin/bash -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'); +install() { + dracut_install -o openssl wget tar # a trailing comment + dracut_install /usr/bin/awk /etc/services + if command -v dhclient >/dev/null 2>&1; then + dracut_install dhclient + fi +} -# dracut_install installs an absolute path at that same path, so a name starting with "/" is a -# command the payload must carry. doxcat, getdestiny and the firmware wrappers all run awk. -my $noawk = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, - dhclient => 1, mktemp => 1, commands => [qw(openssl wget tar)], absent => ['usr/bin/awk']); -($rc, $err) = run_with_commands($module, $noawk); -isnt($rc, 0, 'a payload without the absolute path /usr/bin/awk fails'); -like($err, qr{/usr/bin/awk}, 'the missing /usr/bin/awk is named'); - -# The module names data files by absolute path too. Genesis resolves service names with -# /etc/services. -my $noservices = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, - dhclient => 1, mktemp => 1, commands => [qw(openssl wget tar)], absent => ['etc/services']); -($rc, $err) = run_with_commands($module, $noservices); -isnt($rc, 0, 'a payload without the absolute path /etc/services fails'); -like($err, qr{/etc/services}, 'the missing /etc/services is named'); +installkernel() { + dracut_install notacommand +} +SH +my @commands = module_commands($module); # 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); +is_deeply(\@commands, [qw(/etc/services /usr/bin/awk openssl tar wget)], + 'the top-level names of install() are read back, without options, comments, conditionals + or other functions'); -# A module the verifier cannot read names for covers nothing, so say so instead of passing. +my %names = (commands => \@commands, source => $module, sshd => $OPENSSH_99); +is_deeply([ missing_paths(carries(@COMPLETE), %names) ], [], + 'a payload carrying every command the module names passes'); +is_deeply([ missing_paths(carries(grep({ $_ ne 'usr/bin/wget' } @COMPLETE), 'sbin/wget'), %names) ], + [], 'a command under sbin counts as present'); + +is_deeply([ missing_paths(all_but('usr/bin/openssl'), %names) ], + ["openssl (installed by $module)"], + 'a payload without openssl fails and names it'); + +# doxcat, getdestiny and the firmware wrappers all run awk. +is_deeply([ missing_paths(all_but('usr/bin/awk'), %names) ], + ["/usr/bin/awk (installed by $module)"], + 'a payload without the absolute path /usr/bin/awk fails'); + +# Genesis resolves service names with /etc/services. +is_deeply([ missing_paths(all_but('etc/services'), %names) ], + ["/etc/services (installed by $module)"], + 'a payload without the absolute path /etc/services fails'); + +# A module the verifier cannot read names from 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'); +ok(!eval { module_commands($unparsable); 1 }, 'a module with no install() names is refused'); +is($@, "verify-genesis-payload: no command name read from $unparsable\n", + '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'); +ok(!eval { module_commands("$tmpdir/no-such-module"); 1 }, 'an unreadable module is refused'); +is($@, "verify-genesis-payload: cannot read $tmpdir/no-such-module\n", + 'the unreadable module is named'); -($rc, $err) = run("$tmpdir/does-not-exist"); -is($rc >> 0, 2, 'a missing payload directory is a usage error'); +# --- the command line reads a real payload tree --------------------------------------------- +my $good = payload_tree(@COMPLETE); +write_text("$good/usr/sbin/sshd", $OPENSSH_99); +my $nodhcp = payload_tree(grep { $_ ne 'usr/sbin/dhclient' } @COMPLETE); +my $nohelper = payload_tree(grep { $_ ne 'usr/libexec/openssh/sshd-session' } @COMPLETE); +write_text("$nohelper/usr/sbin/sshd", $OPENSSH_99); + +is_deeply([ check_payload($good, 'usr/sbin/dhclient') ], + [ 0, "verify-genesis-payload: $good is complete\n" ], + 'a complete payload exits 0 and says so'); +is_deeply([ check_payload($nodhcp, 'usr/sbin/dhclient') ], + [ 1, "verify-genesis-payload: $nodhcp is incomplete:\n" + . " usr/sbin/dhclient (required by the build)\n" ], + 'an incomplete payload exits 1 and lists what is missing'); +is_deeply([ check_payload($nohelper) ], + [ 1, "verify-genesis-payload: $nohelper is incomplete:\n" + . " usr/libexec/openssh/sshd-session (this sshd execs it for every connection)\n" ], + 'the command line reads usr/sbin/sshd to decide on sshd-session'); +is_deeply([ check_payload('--commands-from', $module, $nodhcp) ], + [ 0, "verify-genesis-payload: $nodhcp is complete\n" ], + '--commands-from does not require a name installed under a condition'); +my $noopenssl = payload_tree(grep { $_ ne 'usr/bin/openssl' } @COMPLETE); +is_deeply([ check_payload("--commands-from=$module", $noopenssl) ], + [ 1, "verify-genesis-payload: $noopenssl is incomplete:\n" + . " openssl (installed by $module)\n" ], + '--commands-from= checks the names of the module'); +is_deeply([ check_payload('--commands-from', $unparsable, $good) ], + [ 2, "verify-genesis-payload: no command name read from $unparsable\n" ], + 'a module with no command names is a usage error'); +is_deeply([ check_payload('--commands-from', "$tmpdir/no-such-module", $good) ], + [ 2, "verify-genesis-payload: cannot read $tmpdir/no-such-module\n" ], + 'a module file that cannot be read is a usage error'); +is_deeply([ check_payload("$tmpdir/does-not-exist") ], + [ 2, "verify-genesis-payload: not a payload directory: $tmpdir/does-not-exist\n" ], + 'a missing payload directory is a usage error'); +is_deeply([ check_payload() ], + [ 2, "verify-genesis-payload: not a payload directory: \n" ], + 'no payload directory at all is a usage error'); + +done_testing(); #--- -# build_payload: make a payload tree with the pieces the verifier reasons about. +# payload_tree: a payload directory that carries the given paths as empty files. #--- -sub build_payload { - my (%opt) = @_; +sub payload_tree { my $root = tempdir(DIR => $tmpdir, CLEANUP => 1); - make_path("$root/usr/sbin", "$root/usr/bin", "$root/usr/libexec/openssh"); - write_text("$root/usr/sbin/sshd", - $opt{sshd_execs_session} - ? "OpenSSH_9.9p1\n/usr/libexec/openssh/sshd-session\n" - : "OpenSSH_8.0p1\n"); - write_text("$root/usr/libexec/openssh/sshd-session", "helper\n") if $opt{session_helper}; - write_text("$root/usr/bin/tmux", "tmux\n") if $opt{tmux}; - if ($opt{locale}) { - make_path("$root/usr/lib/locale/C.utf8"); - 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}; - write_text("$root/usr/bin/$_", "$_\n") for @{ $opt{commands} || [] }; - - # The module written by write_module_setup names these two by absolute path. - my %absent = map { $_ => 1 } @{ $opt{absent} || [] }; - for my $path (qw(usr/bin/awk etc/services)) { - next if $absent{$path}; - my ($dir) = $path =~ m{^(.*)/}; - make_path("$root/$dir"); - write_text("$root/$path", "$path\n"); + for my $path (@_) { + make_path(dirname("$root/$path")); + write_text("$root/$path", ''); } return $root; } - -#--- -# run: run the verifier and return its exit status and stderr. -#--- -sub run { - my ($root, @required) = @_; - my $errfile = "$tmpdir/err.$$"; - my $cmd = join ' ', map { "'$_'" } ($verifier, $root, @required); - system("/bin/bash $cmd >/dev/null 2>$errfile"); - my $status = $? >> 8; - my $err = -f $errfile ? read_text($errfile) : ''; - 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); -} diff --git a/xCAT-test/unit/genesis_ubuntu_build_root.t b/xCAT-test/unit/genesis_ubuntu_build_root.t index 4d8623078..ce314c925 100755 --- a/xCAT-test/unit/genesis_ubuntu_build_root.t +++ b/xCAT-test/unit/genesis_ubuntu_build_root.t @@ -3,97 +3,89 @@ # mandatory. dracut_install reports a missing command and returns 0, so a hole in the image # does not fail the build. # -# The mandatory list comes from RUNNING the module: module-setup.sh is sourced with -# dracut_install shadowed, _dracut_install_opt neutralised, and install() called. The -# package list comes from evaluating the REQUIRED_PACKAGES assignment in the build script. +# XCAT::GenesisBuildRoot::required_packages lists the packages of the build root, and +# XCAT::GenesisPayload::module_commands reads the mandatory commands from the module, as +# verify-genesis-payload does. The refusal of a root of another release is in +# genesis_deb_per_codename.t. use strict; use warnings; +use File::Slurper qw(read_text); use File::Temp qw(tempdir); use FindBin; use lib "$FindBin::Bin/../lib"; +use lib "$FindBin::Bin/../../xCAT-genesis-builder/lib"; use Test::More; +use XCAT::GenesisBuildRoot qw(required_packages); +use XCAT::GenesisPayload qw(module_commands); use XCAT::Test::File qw(repo_path); -my $builder = repo_path('xCAT-genesis-builder/builddeb-genesis-base'); -my $module = repo_path('xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh'); -die "builddeb-genesis-base not found\n" unless -f $builder; -die "ubuntu module-setup.sh not found\n" unless -f $module; -plan tests => 9; +my $builder = repo_path('xCAT-genesis-builder/builddeb-genesis-base'); +my $module = repo_path('xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh'); # Mandatory commands a minimal Ubuntu server root does NOT already provide, and the packages # that supply each one. hwclock has two names: it left util-linux for util-linux-extra in -# 23.04, and the build root asks apt which name this release carries. +# 23.04. my %PACKAGES_FOR = ( dhclient => ['isc-dhcp-client'], ifenslave => ['ifenslave'], hwclock => [ 'util-linux-extra', 'util-linux' ], ); -my %mandatory = map { $_ => 1 } mandatory_commands($module); -my @packages = required_packages($builder); +# A release carries exactly the names in its list. +sub release { + my %carried = map { $_ => 1 } @_; + return sub { $carried{ $_[0] } }; +} +my $NOBLE = release(qw(bind9-dnsutils dnsutils util-linux-extra util-linux tzdata-legacy)); +my $FOCAL = release(qw(dnsutils util-linux)); +my @noble = required_packages('amd64', 'noble', $NOBLE); +is_deeply([ @noble[ -5 .. -1 ] ], + [qw(dmidecode efibootmgr bind9-dnsutils util-linux-extra tzdata-legacy)], + 'amd64 adds dmidecode and efibootmgr, then the first name the release carries'); + +my @focal = required_packages('amd64', 'focal', $FOCAL); +is_deeply([ @focal[ -2 .. -1 ] ], [qw(dnsutils util-linux)], + 'a release without the new names gets dnsutils and util-linux, and no tzdata-legacy'); + +my @ppc = required_packages('ppc64el', 'noble', $NOBLE); +is_deeply([ @ppc[ -3 .. -1 ] ], [qw(bind9-dnsutils util-linux-extra tzdata-legacy)], + 'ppc64el gets neither dmidecode nor efibootmgr'); +is_deeply([ @ppc[ 0 .. $#ppc - 3 ] ], [ @noble[ 0 .. $#noble - 5 ] ], + 'ppc64el and amd64 share the base packages'); + +my @asked; +required_packages('amd64', 'noble', sub { push @asked, $_[0]; $NOBLE->($_[0]) }); +is_deeply(\@asked, [qw(bind9-dnsutils util-linux-extra tzdata-legacy)], + 'apt is asked for the older name only when the newer one is absent'); + +ok(!eval { required_packages('amd64', 'oddball', release('bind9-dnsutils')); 1 }, + 'a release that carries no hwclock package fails'); +is($@, "ERROR: oddball carries none of these packages: util-linux-extra util-linux\n", + 'the failure names the release and the missing alternatives'); + +# --- every mandatory command has a package in the build root ----------------------------- +# An absolute path is a data file, not a command. +my %mandatory = map { $_ => 1 } grep { !m{^/} } module_commands($module); +my %packages = map { $_ => 1 } @noble; for my $command (sort keys %PACKAGES_FOR) { my @provider = @{ $PACKAGES_FOR{$command} }; ok($mandatory{$command}, "the Ubuntu dracut module installs '$command' unconditionally"); - my @named = grep { my $p = $_; grep { $_ eq $p } @packages } @provider; - ok(scalar @named, + ok(scalar(grep { $packages{$_} } @provider), "the build root installs @{[ join ' or ', @provider ]}, which provides '$command'"); } +ok(grep({ $_ eq 'util-linux' } @focal), 'a release before 23.04 gets hwclock from util-linux'); # doxcat asks dhclient for the provisioning lease. -ok($mandatory{dhclient} && scalar(grep { $_ eq 'isc-dhcp-client' } @packages), +ok($mandatory{dhclient} && $packages{'isc-dhcp-client'}, 'the Genesis image can obtain a DHCP lease'); -# dracut_install is silent about a missing command, so the payload needs its own gate. -# xCAT-genesis-base.spec runs the same verifier on the EL path. -my $text = do { open my $fh, '<', $builder or die "$builder: $!"; local $/; <$fh> }; -like($text, qr{verify-genesis-payload}, 'builddeb-genesis-base verifies the payload it packages'); +# The build cannot run here, so the call to the payload gate is read from the script. The +# gate itself is exercised by genesis_payload_verification.t. +like(read_text($builder), + qr{^bash "\$DIR/verify-genesis-payload" --commands-from "\$DRACUTMODDIR/module-setup\.sh" "\$GENESIS_FS"}m, + 'builddeb-genesis-base verifies the payload it packages against the module'); -# The image belongs to the release whose kernel it carries, so the builder must refuse a -# root of any other release. -like($text, qr{--expect-codename}, 'builddeb-genesis-base takes the release it is building for'); - -# An absolute path in the install() output is a data file, not a command. -sub mandatory_commands { - my ($path) = @_; - my $dir = tempdir(CLEANUP => 1); - my $driver = "$dir/collect.sh"; - open my $fh, '>', $driver or die "$driver: $!"; - print $fh <<"BASH"; -dracut_install() { printf '%s\\n' "\$\@"; } -instmods() { :; } -inst_multiple() { :; } -inst() { :; } -dpkg-architecture() { echo x86_64-linux-gnu; } -. '$path' -# _dracut_install_opt installs only what the build root already has. Neutralise it after -# sourcing, so its commands stay out of the mandatory set. -_dracut_install_opt() { :; } -install -BASH - close $fh; - my @out = qx{bash '$driver' 2>/dev/null}; - die("running install() from $path produced nothing") unless @out; - my %seen; - my @names = grep { !$seen{$_}++ } grep { length && !m{^/} } map { chomp; $_ } @out; - die("install() from $path named no bare commands") unless @names; - return @names; -} - -# Evaluate the assignment rather than parse it, so the list is the value the script uses. -# add_first_available names the alternatives for a package that was renamed between releases, -# so its candidates count too. -sub required_packages { - my ($path) = @_; - my $text = do { open my $fh, '<', $path or die "$path: $!"; local $/; <$fh> }; - my ($block) = $text =~ /^(REQUIRED_PACKAGES="[^"]*")/ms; - die("no REQUIRED_PACKAGES assignment in $path") unless $block; - my $out = qx{bash -c 'set -u; $block; printf "%s\\n" \$REQUIRED_PACKAGES' 2>/dev/null}; - my @packages = grep { length } split /\s+/, ($out // ''); - die("REQUIRED_PACKAGES in $path evaluated to nothing") unless @packages; - push @packages, grep { length } split /\s+/, $1 - while $text =~ /^add_first_available\s+(.+)$/mg; - return @packages; -} +done_testing();