mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-10-02 17:02:17 +00:00
fix(xcat-core): Genesis base tests from master read the old directory and evaluate shell
Master added a workflow and several tests after this branch renamed xCAT-genesis-builder to xCAT-genesis-base. They still name files under xCAT-genesis-builder/, which no longer exists, so they fail or read nothing. The OpenEmbedded workflow, six bats files and four unit tests now use the new paths. Two of those unit tests could reach the code only as shell. genesis_ubuntu_build_root.t cut the package list out of builddeb-genesis-base and evaluated it through bash. genesis_payload_verification.t ran the bash verifier and parsed its stderr. XCAT::GenesisBuildRoot::required_packages() now returns the build-root package list, and builddeb-genesis-base calls it. XCAT::GenesisPayload holds the mandatory-command list and the payload check, and verify-genesis-payload runs its main() with the same arguments, exit codes and messages. stage_genesis_base_sources() stages the module beside the script. Both modules use core Perl only. The two tests now call these functions with chosen inputs and assert the exact results. genesis_base_sources_staged.t expects the module in the staged tarball. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,7 @@ name: genesis_openembedded
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'xCAT-genesis-builder/oe/**'
|
||||
- 'xCAT-genesis-base/oe/**'
|
||||
- 'xCAT-genesis-scripts/**'
|
||||
- '.github/workflows/genesis_openembedded.yml'
|
||||
workflow_dispatch:
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: pip
|
||||
cache-dependency-path: xCAT-genesis-builder/oe/requirements.txt
|
||||
cache-dependency-path: xCAT-genesis-base/oe/requirements.txt
|
||||
- name: Install OpenEmbedded host dependencies
|
||||
run: >-
|
||||
sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
build-essential chrpath cpio diffstat file gawk git locales
|
||||
lz4 openssl socat texinfo unzip wget xz-utils zstd
|
||||
- name: Install KAS
|
||||
run: python -m pip install -r xCAT-genesis-builder/oe/requirements.txt
|
||||
run: python -m pip install -r xCAT-genesis-base/oe/requirements.txt
|
||||
- name: Enable BitBake user namespaces
|
||||
run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
||||
- name: Validate every architecture configuration
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
for architecture in x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64 s390x; do
|
||||
export KAS_BUILD_DIR="${RUNNER_TEMP}/kas-build-${architecture}"
|
||||
configuration="xCAT-genesis-builder/oe/kas/${architecture}.yml"
|
||||
configuration="xCAT-genesis-base/oe/kas/${architecture}.yml"
|
||||
kas dump "${configuration}" >/dev/null
|
||||
kas shell "${configuration}" -c 'bitbake -p'
|
||||
done
|
||||
@@ -47,8 +47,8 @@ jobs:
|
||||
KAS_BUILD_DIR: ${{ runner.temp }}/kas-build-x86_64
|
||||
run: |
|
||||
set -euo pipefail
|
||||
kas shell xCAT-genesis-builder/oe/kas/x86_64.yml -c \
|
||||
kas shell xCAT-genesis-base/oe/kas/x86_64.yml -c \
|
||||
'bitbake -n xcat-genesis-image xcat-genesis-extension-smoke'
|
||||
export KAS_BUILD_DIR="${RUNNER_TEMP}/kas-build-s390x"
|
||||
kas shell xCAT-genesis-builder/oe/kas/s390x.yml -c \
|
||||
kas shell xCAT-genesis-base/oe/kas/s390x.yml -c \
|
||||
'bitbake -n xcat-genesis-image xcat-genesis-extension-smoke'
|
||||
|
||||
@@ -382,9 +382,9 @@ sub stage_probe_helpers {
|
||||
|
||||
Descriptions:
|
||||
Write the build support tarball that xCAT-genesis-base.spec unpacks:
|
||||
the dracut_105 modules, 80-net-name-slot.rules and
|
||||
verify-genesis-payload, taken from the xCAT-genesis-base directory of
|
||||
a checkout.
|
||||
the dracut_105 modules, 80-net-name-slot.rules, and
|
||||
verify-genesis-payload with its XCAT::GenesisPayload module, taken from
|
||||
the xCAT-genesis-base directory of a checkout.
|
||||
Arguments:
|
||||
$checkout: the top of the xcat-core checkout
|
||||
$tarball: the path of the tarball to write
|
||||
@@ -414,6 +414,9 @@ sub stage_genesis_base_sources {
|
||||
# The spec runs the verifier against the extracted payload in %install.
|
||||
copy("$source/verify-genesis-payload", "$staging_root/verify-genesis-payload")
|
||||
or die "Unable to stage $source/verify-genesis-payload: $!\n";
|
||||
make_path("$staging_root/lib/XCAT");
|
||||
copy("$source/lib/XCAT/GenesisPayload.pm", "$staging_root/lib/XCAT/GenesisPayload.pm")
|
||||
or die "Unable to stage $source/lib/XCAT/GenesisPayload.pm: $!\n";
|
||||
|
||||
unlink $tarball if -f $tarball;
|
||||
sh_or_die(qq(tar --sort=name --owner=0 --group=0 --mtime="\@$epoch" -cjf "$tarball" -C "$staging_parent" xCAT-genesis-base-build-support),
|
||||
|
||||
@@ -37,36 +37,12 @@ echo "Building xcat-genesis-base for $BUILDARCH ($TARCH) on Ubuntu $CODENAME"
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
REQUIRED_PACKAGES="
|
||||
dracut linux-image-generic
|
||||
ipmitool lldpad ethtool iproute2 kexec-tools screen
|
||||
openssh-server openssh-client rsyslog chrony
|
||||
nfs-common rpcbind pciutils usbutils parted
|
||||
dosfstools e2fsprogs lvm2 mdadm net-tools
|
||||
bc psmisc rsync wget cpio
|
||||
isc-dhcp-client ifenslave
|
||||
dpkg-dev debhelper fakeroot devscripts vim-tiny
|
||||
"
|
||||
if [ "$BUILDARCH" = "amd64" ]; then
|
||||
REQUIRED_PACKAGES="$REQUIRED_PACKAGES dmidecode efibootmgr"
|
||||
fi
|
||||
|
||||
# Keep only the packages this release actually has. hwclock moved out of util-linux into
|
||||
# util-linux-extra, so focal and jammy have no such package and naming it there fails the
|
||||
# whole install. util-linux only Suggests it, and --no-install-recommends is passed, so the
|
||||
# releases that did split it must name it.
|
||||
optional_packages() {
|
||||
local package
|
||||
for package in "$@"; do
|
||||
if apt-cache show "$package" >/dev/null 2>&1; then
|
||||
echo "$package"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
echo "Installing build dependencies..."
|
||||
apt-get update -qq
|
||||
REQUIRED_PACKAGES="$REQUIRED_PACKAGES $(optional_packages util-linux-extra)"
|
||||
|
||||
# XCAT::GenesisBuildRoot holds the package list, so the unit tests can call it.
|
||||
REQUIRED_PACKAGES=$(perl -I"$DIR/lib" -MXCAT::GenesisBuildRoot=required_packages \
|
||||
-e 'print "$_\n" for required_packages(@ARGV)' "$BUILDARCH" "$CODENAME")
|
||||
apt-get install -y --no-install-recommends $REQUIRED_PACKAGES
|
||||
|
||||
# Set up dracut module
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package XCAT::GenesisBuildRoot;
|
||||
|
||||
# builddeb-genesis-base runs this module inside an Ubuntu build root, where only perl-base is
|
||||
# installed. Use core modules only.
|
||||
use strict;
|
||||
use warnings;
|
||||
use Exporter 'import';
|
||||
|
||||
our @EXPORT_OK = qw(required_packages apt_carries);
|
||||
|
||||
my @BASE_PACKAGES = qw(
|
||||
dracut linux-image-generic
|
||||
ipmitool lldpad ethtool iproute2 kexec-tools screen
|
||||
openssh-server openssh-client rsyslog chrony
|
||||
nfs-common rpcbind pciutils usbutils parted
|
||||
dosfstools e2fsprogs lvm2 mdadm net-tools
|
||||
bc psmisc rsync wget cpio
|
||||
isc-dhcp-client ifenslave
|
||||
dpkg-dev debhelper fakeroot devscripts vim-tiny
|
||||
);
|
||||
|
||||
# hwclock moved out of util-linux into util-linux-extra. focal and jammy have no such package,
|
||||
# and naming it there fails the whole install. util-linux only Suggests it, and the build
|
||||
# passes --no-install-recommends, so the releases that split it must name it.
|
||||
my @OPTIONAL_PACKAGES = qw(util-linux-extra);
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
=head3 apt_carries
|
||||
|
||||
Descriptions: ask apt-cache whether the release carries a package.
|
||||
Arguments:
|
||||
$package: the package name
|
||||
Returns:
|
||||
1 when apt-cache knows the package, 0 when it does not
|
||||
|
||||
=cut
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
sub apt_carries {
|
||||
my ($package) = @_;
|
||||
return system('sh', '-c', 'apt-cache show "$1" >/dev/null 2>&1', 'sh', $package) == 0
|
||||
? 1 : 0;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
=head3 required_packages
|
||||
|
||||
Descriptions: list the packages the Genesis build root needs.
|
||||
An optional package is listed only when the release carries it.
|
||||
Arguments:
|
||||
$arch: the dpkg architecture (amd64, ppc64el)
|
||||
$codename: the release name
|
||||
$carries: optional code ref. It takes a package name and returns true when the
|
||||
release carries that package. The default is apt_carries.
|
||||
Returns:
|
||||
the package names, in install order.
|
||||
|
||||
=cut
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
sub required_packages {
|
||||
my ($arch, $codename, $carries) = @_;
|
||||
$carries ||= \&apt_carries;
|
||||
|
||||
my @packages = @BASE_PACKAGES;
|
||||
push @packages, qw(dmidecode efibootmgr) if $arch eq 'amd64';
|
||||
push @packages, grep { $carries->($_) } @OPTIONAL_PACKAGES;
|
||||
return @packages;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -0,0 +1,196 @@
|
||||
package XCAT::GenesisPayload;
|
||||
|
||||
# verify-genesis-payload runs this module in the rpm %install of xCAT-genesis-base and inside
|
||||
# the Ubuntu build root, where only perl-base is installed. Use core modules only.
|
||||
use strict;
|
||||
use warnings;
|
||||
use Exporter 'import';
|
||||
|
||||
our @EXPORT_OK = qw(module_commands missing_paths check_payload main);
|
||||
|
||||
my $ME = 'verify-genesis-payload';
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
=head3 module_commands
|
||||
|
||||
Descriptions: read back the names a dracut module installs.
|
||||
Only the top level of install() counts. A name under a condition is
|
||||
release-dependent, so the caller names it as a required path instead.
|
||||
An option to dracut_install (a word that starts with "-") is not a name.
|
||||
Arguments:
|
||||
$module_setup: the path of the module-setup.sh
|
||||
Returns:
|
||||
the names, sorted, each one once. A name that starts with "/" is an
|
||||
absolute path; any other name is a command.
|
||||
Dies with "verify-genesis-payload: cannot read <file>" when the file
|
||||
cannot be read, and with "verify-genesis-payload: no command name read
|
||||
from <file>" when install() names nothing.
|
||||
|
||||
=cut
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
sub module_commands {
|
||||
my ($module_setup) = @_;
|
||||
open(my $fh, '<', $module_setup)
|
||||
or die "$ME: cannot read $module_setup\n";
|
||||
|
||||
my ($in_install, %names);
|
||||
while (my $line = <$fh>) {
|
||||
if ($line =~ /^install\(\)/) { $in_install = 1; next }
|
||||
$in_install = 0 if $in_install && $line =~ /^}/;
|
||||
next unless $in_install && $line =~ s/^ dracut_install //;
|
||||
$line =~ s/#.*//s;
|
||||
$names{$_} = 1 for grep { length && !/^-/ } split /\s+/, $line;
|
||||
}
|
||||
close($fh);
|
||||
|
||||
die "$ME: no command name read from $module_setup\n" unless %names;
|
||||
return sort keys %names;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
=head3 missing_paths
|
||||
|
||||
Descriptions: list what a Genesis payload lacks.
|
||||
dracut_install reports a missing binary and returns, so the image can
|
||||
ship without it. This check runs on the extracted payload before it is
|
||||
packaged.
|
||||
Arguments:
|
||||
$have: code ref. It takes a path relative to the payload root and
|
||||
returns true when the payload carries it.
|
||||
%opt:
|
||||
required: paths relative to the payload root that the caller needs
|
||||
commands: names from module_commands. A bare command is looked
|
||||
for in bin, sbin, usr/bin and usr/sbin; an absolute
|
||||
path is looked for under the payload root.
|
||||
source: the module the commands came from, for the message
|
||||
sshd: the content of usr/sbin/sshd, or undef without one
|
||||
Returns:
|
||||
one "<path> (<reason>)" string per missing item, in the order checked.
|
||||
An empty list means the payload is complete.
|
||||
|
||||
=cut
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
sub missing_paths {
|
||||
my ($have, %opt) = @_;
|
||||
my @missing;
|
||||
|
||||
for my $path (@{ $opt{required} || [] }) {
|
||||
push @missing, "$path (required by the build)" unless $have->($path);
|
||||
}
|
||||
|
||||
for my $want (@{ $opt{commands} || [] }) {
|
||||
my $found =
|
||||
$want =~ m{^/(.*)}
|
||||
? $have->($1)
|
||||
: scalar(grep { $have->("$_/$want") } qw(bin sbin usr/bin usr/sbin));
|
||||
push @missing, "$want (installed by $opt{source})" unless $found;
|
||||
}
|
||||
|
||||
push @missing, "usr/sbin/sshd (Genesis is reached over ssh)"
|
||||
unless $have->('usr/sbin/sshd');
|
||||
push @missing, "usr/bin/mktemp (getdestiny makes its request file with it)"
|
||||
unless $have->('usr/bin/mktemp');
|
||||
|
||||
# OpenSSH 9.8 split the per-connection work into sshd-session, which sshd execs by
|
||||
# absolute path. EL9 carries OpenSSH 9.9, so an image with sshd alone refuses every
|
||||
# connection.
|
||||
if (index($opt{sshd} // '', 'sshd-session') >= 0
|
||||
&& !$have->('usr/libexec/openssh/sshd-session')
|
||||
&& !$have->('usr/lib/openssh/sshd-session'))
|
||||
{
|
||||
push @missing,
|
||||
"usr/libexec/openssh/sshd-session (this sshd execs it for every connection)";
|
||||
}
|
||||
|
||||
# tmux exits under the C locale. The hook then runs doxcat directly, but a Genesis
|
||||
# shell without tmux loses the console attach.
|
||||
if ($have->('usr/bin/tmux') && !$have->('usr/lib/locale/C.utf8/LC_CTYPE')) {
|
||||
push @missing,
|
||||
"usr/lib/locale/C.utf8/LC_CTYPE (tmux refuses to start without a UTF-8 locale)";
|
||||
}
|
||||
return @missing;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
=head3 check_payload
|
||||
|
||||
Descriptions: the command line of verify-genesis-payload, without the output:
|
||||
[--commands-from <module-setup.sh>] <payload-root> [required-path ...]
|
||||
Arguments:
|
||||
@args: the command line
|
||||
Returns:
|
||||
($status, $message). $status is the exit status: 0 when the payload is
|
||||
complete, 1 when it lacks something, 2 on a usage error. $message is
|
||||
the text to print, one line or a list of missing items.
|
||||
|
||||
=cut
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
sub check_payload {
|
||||
my @args = @_;
|
||||
my $commands_from = '';
|
||||
while (@args) {
|
||||
if ($args[0] eq '--commands-from') {
|
||||
shift @args;
|
||||
$commands_from = shift(@args) // '';
|
||||
} elsif ($args[0] =~ /^--commands-from=(.*)/s) {
|
||||
$commands_from = $1;
|
||||
shift @args;
|
||||
} else {
|
||||
last;
|
||||
}
|
||||
}
|
||||
|
||||
my $payload = shift(@args) // '';
|
||||
if ($payload eq '' || !-d $payload) {
|
||||
return (2, "$ME: not a payload directory: " . ($payload eq '' ? '<empty>' : $payload) . "\n");
|
||||
}
|
||||
|
||||
my @commands;
|
||||
if ($commands_from ne '') {
|
||||
@commands = eval { module_commands($commands_from) };
|
||||
return (2, $@) if $@;
|
||||
}
|
||||
|
||||
my $sshd;
|
||||
if (open(my $fh, '<:raw', "$payload/usr/sbin/sshd")) {
|
||||
local $/;
|
||||
$sshd = <$fh> // '';
|
||||
close($fh);
|
||||
}
|
||||
my @missing = missing_paths(sub { -e "$payload/$_[0]" },
|
||||
required => \@args,
|
||||
commands => \@commands,
|
||||
source => $commands_from,
|
||||
sshd => $sshd);
|
||||
return (1, "$ME: $payload is incomplete:" . join('', map { "\n $_" } @missing) . "\n")
|
||||
if @missing;
|
||||
return (0, "$ME: $payload is complete\n");
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
=head3 main
|
||||
|
||||
Descriptions: run check_payload and print its message: on STDOUT when the
|
||||
payload is complete, on STDERR otherwise.
|
||||
Arguments:
|
||||
@args: the command line
|
||||
Returns:
|
||||
the exit status from check_payload
|
||||
|
||||
=cut
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
sub main {
|
||||
my ($status, $message) = check_payload(@_);
|
||||
print { $status ? *STDERR : *STDOUT } $message;
|
||||
return $status;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -6,115 +6,9 @@
|
||||
# going and the image ships without it. Check the extracted payload before it is packaged.
|
||||
#
|
||||
# Paths given on the command line are relative to <payload-root>. --commands-from reads back
|
||||
# what the dracut module installs: a bare command name is looked for in the four binary
|
||||
# directories, an absolute path under <payload-root> itself. The caller adds what only it
|
||||
# knows (the DHCP client is not the same package on every release); the rules below come from
|
||||
# the payload itself.
|
||||
# what the dracut module installs. XCAT::GenesisPayload, in lib/ beside this script, holds
|
||||
# the rules, so the unit tests call the same code.
|
||||
#
|
||||
# Exit status: 0 complete, 1 something missing, 2 usage error.
|
||||
|
||||
set -u
|
||||
|
||||
commands_from=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--commands-from)
|
||||
commands_from=${2:-}
|
||||
shift 2 || true
|
||||
;;
|
||||
--commands-from=*)
|
||||
commands_from=${1#*=}
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
payload=${1:-}
|
||||
if [ -z "$payload" ] || [ ! -d "$payload" ]; then
|
||||
echo "verify-genesis-payload: not a payload directory: ${payload:-<empty>}" >&2
|
||||
exit 2
|
||||
fi
|
||||
shift
|
||||
|
||||
missing=""
|
||||
|
||||
# have PATH: true when the payload carries PATH as a file, following the usr-merge symlinks
|
||||
# the image ships (/sbin -> usr/sbin).
|
||||
have() {
|
||||
[ -e "$payload/$1" ]
|
||||
}
|
||||
|
||||
require() {
|
||||
local path=$1 why=$2
|
||||
have "$path" || missing="$missing
|
||||
$path ($why)"
|
||||
}
|
||||
|
||||
for path in "$@"; do
|
||||
require "$path" "required by the build"
|
||||
done
|
||||
|
||||
# The dracut module names every command and every data file Genesis needs. A name the build
|
||||
# root does not supply installs nothing and says nothing, so read the names back and check
|
||||
# each one. Names under a condition are release-dependent, so only the top level of install()
|
||||
# counts.
|
||||
if [ -n "$commands_from" ]; then
|
||||
if [ ! -r "$commands_from" ]; then
|
||||
echo "verify-genesis-payload: cannot read $commands_from" >&2
|
||||
exit 2
|
||||
fi
|
||||
commands=$(awk '
|
||||
/^install\(\)/ { in_install = 1; next }
|
||||
in_install && /^}/ { in_install = 0 }
|
||||
in_install && /^ dracut_install / {
|
||||
sub(/#.*/, "")
|
||||
sub(/^ dracut_install /, "")
|
||||
print
|
||||
}' "$commands_from" | tr ' \t' '\n\n' | grep -v '^$' | grep -v '^-' | sort -u)
|
||||
if [ -z "$commands" ]; then
|
||||
echo "verify-genesis-payload: no command name read from $commands_from" >&2
|
||||
exit 2
|
||||
fi
|
||||
for want in $commands; do
|
||||
case "$want" in
|
||||
# dracut_install installs an absolute path at that same path, so read it back
|
||||
# under the payload root. Dropping these let an image with no /usr/bin/awk pass.
|
||||
/*) have "${want#/}" || missing="$missing
|
||||
$want (installed by $commands_from)"
|
||||
;;
|
||||
*) have "bin/$want" || have "sbin/$want" \
|
||||
|| have "usr/bin/$want" || have "usr/sbin/$want" \
|
||||
|| missing="$missing
|
||||
$want (installed by $commands_from)"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
require usr/sbin/sshd "Genesis is reached over ssh"
|
||||
require usr/bin/mktemp "getdestiny makes its request file with it"
|
||||
|
||||
# OpenSSH 9.8 split the per-connection work into sshd-session, which sshd execs by absolute
|
||||
# path. EL9 carries OpenSSH 9.9, so an image with sshd alone refuses every connection.
|
||||
if have usr/sbin/sshd && grep -qa 'sshd-session' "$payload/usr/sbin/sshd" 2>/dev/null; then
|
||||
if ! have usr/libexec/openssh/sshd-session && ! have usr/lib/openssh/sshd-session; then
|
||||
missing="$missing
|
||||
usr/libexec/openssh/sshd-session (this sshd execs it for every connection)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# tmux exits under the C locale. The hook falls back to running doxcat directly, so this is
|
||||
# not fatal to booting, but a Genesis shell without tmux loses the console attach.
|
||||
if have usr/bin/tmux && ! have usr/lib/locale/C.utf8/LC_CTYPE; then
|
||||
missing="$missing
|
||||
usr/lib/locale/C.utf8/LC_CTYPE (tmux refuses to start without a UTF-8 locale)"
|
||||
fi
|
||||
|
||||
if [ -n "$missing" ]; then
|
||||
echo "verify-genesis-payload: $payload is incomplete:$missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "verify-genesis-payload: $payload is complete"
|
||||
exit 0
|
||||
exec perl -I"$(dirname "$0")/lib" -MXCAT::GenesisPayload=main -e 'exit main(@ARGV)' -- "$@"
|
||||
|
||||
@@ -12,7 +12,7 @@ load 'helpers/shell_source'
|
||||
|
||||
setup()
|
||||
{
|
||||
SCRIPT="$(repo_path 'xCAT-genesis-builder/debuild-xcat-genesis-base')"
|
||||
SCRIPT="$(repo_path 'xCAT-genesis-base/debuild-xcat-genesis-base')"
|
||||
# Fail rather than skip: a checkout without the converter has no deb rename to measure,
|
||||
# and a skip there covers nothing while reading green.
|
||||
[ -r "$SCRIPT" ]
|
||||
|
||||
@@ -13,8 +13,8 @@ load 'helpers/shell_source'
|
||||
|
||||
setup()
|
||||
{
|
||||
SCRIPT="$(repo_path 'xCAT-genesis-builder/builddeb-genesis-base')"
|
||||
CONTROL="$(repo_path 'xCAT-genesis-builder/debian/control')"
|
||||
SCRIPT="$(repo_path 'xCAT-genesis-base/builddeb-genesis-base')"
|
||||
CONTROL="$(repo_path 'xCAT-genesis-base/debian/control')"
|
||||
[ -r "$SCRIPT" ] || skip "$SCRIPT is required"
|
||||
[ -r "$CONTROL" ] || skip "$CONTROL is required"
|
||||
export SCRIPT CONTROL
|
||||
|
||||
@@ -9,8 +9,8 @@ load 'helpers/shell_source'
|
||||
|
||||
setup()
|
||||
{
|
||||
EL_HOOK="$(repo_path 'xCAT-genesis-builder/dracut_105/el/xcat-cmdline.sh')"
|
||||
UBUNTU_HOOK="$(repo_path 'xCAT-genesis-builder/dracut_105/ubuntu/xcat-cmdline.sh')"
|
||||
EL_HOOK="$(repo_path 'xCAT-genesis-base/dracut_105/el/xcat-cmdline.sh')"
|
||||
UBUNTU_HOOK="$(repo_path 'xCAT-genesis-base/dracut_105/ubuntu/xcat-cmdline.sh')"
|
||||
[ -r "$EL_HOOK" ] || skip "$EL_HOOK is required"
|
||||
[ -r "$UBUNTU_HOOK" ] || skip "$UBUNTU_HOOK is required"
|
||||
export EL_HOOK UBUNTU_HOOK
|
||||
|
||||
@@ -14,8 +14,8 @@ ISC6='dhclient -6 -pf /var/run/dhclient6.eth0.pid eth0 -lf /var/lib/dhclient/dhc
|
||||
setup()
|
||||
{
|
||||
DOXCAT="$(repo_path 'xCAT-genesis-scripts/usr/bin/doxcat')"
|
||||
SPEC="$(repo_path 'xCAT-genesis-builder/xCAT-genesis-base.spec')"
|
||||
MODULE="$(repo_path 'xCAT-genesis-builder/dracut_105/el/module-setup.sh')"
|
||||
SPEC="$(repo_path 'xCAT-genesis-base/xCAT-genesis-base.spec')"
|
||||
MODULE="$(repo_path 'xCAT-genesis-base/dracut_105/el/module-setup.sh')"
|
||||
[ -r "$DOXCAT" ] || skip "$DOXCAT is required"
|
||||
[ -r "$SPEC" ] || skip "$SPEC is required"
|
||||
[ -r "$MODULE" ] || skip "$MODULE is required"
|
||||
|
||||
@@ -77,13 +77,13 @@ assert_hook()
|
||||
}
|
||||
|
||||
@test "the legacy hook gives root the home directory /" {
|
||||
assert_hook 'xCAT-genesis-builder/xcat-cmdline.sh'
|
||||
assert_hook 'xCAT-genesis-base/xcat-cmdline.sh'
|
||||
}
|
||||
|
||||
@test "the el dracut 105 hook gives root the home directory /" {
|
||||
assert_hook 'xCAT-genesis-builder/dracut_105/el/xcat-cmdline.sh'
|
||||
assert_hook 'xCAT-genesis-base/dracut_105/el/xcat-cmdline.sh'
|
||||
}
|
||||
|
||||
@test "the ubuntu dracut 105 hook gives root the home directory /" {
|
||||
assert_hook 'xCAT-genesis-builder/dracut_105/ubuntu/xcat-cmdline.sh'
|
||||
assert_hook 'xCAT-genesis-base/dracut_105/ubuntu/xcat-cmdline.sh'
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ setup()
|
||||
{
|
||||
go_xcat_require_source
|
||||
SCRIPTS_DEBIAN="$(repo_path 'xCAT-genesis-scripts/debian')"
|
||||
SPEC="$(repo_path 'xCAT-genesis-builder/xCAT-genesis-base.spec')"
|
||||
SPEC="$(repo_path 'xCAT-genesis-base/xCAT-genesis-base.spec')"
|
||||
[ -d "$SCRIPTS_DEBIAN" ] || skip "$SCRIPTS_DEBIAN is required"
|
||||
[ -r "$SPEC" ] || skip "$SPEC is required"
|
||||
export SCRIPTS_DEBIAN SPEC
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env perl
|
||||
# xCAT-genesis-base.spec builds the Genesis image from a tarball that buildrpms.pl stages:
|
||||
# the dracut_105 modules, 80-net-name-slot.rules and verify-genesis-payload. Renaming the
|
||||
# directory they live in breaks that staging silently.
|
||||
# the dracut_105 modules, 80-net-name-slot.rules, and verify-genesis-payload with its module.
|
||||
# Renaming the directory they live in breaks that staging silently.
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
@@ -37,6 +37,8 @@ make_path("$checkout/xCAT-genesis-base/dracut_105/el",
|
||||
write_text("$checkout/xCAT-genesis-base/dracut_105/$_/$MODULE", "$_ module\n") for qw(el ubuntu);
|
||||
write_text("$checkout/xCAT-genesis-base/80-net-name-slot.rules", "rules\n");
|
||||
write_text("$checkout/xCAT-genesis-base/verify-genesis-payload", "verifier\n");
|
||||
make_path("$checkout/xCAT-genesis-base/lib/XCAT");
|
||||
write_text("$checkout/xCAT-genesis-base/lib/XCAT/GenesisPayload.pm", "1;\n");
|
||||
|
||||
my $tarball = "$scratch/scratch.tar.bz2";
|
||||
is(stage_genesis_base_sources($checkout, $tarball, $EPOCH), $tarball,
|
||||
@@ -51,9 +53,12 @@ is_deeply([ names(@listing) ], [
|
||||
"xCAT-genesis-base-build-support/dracut_105/el/$MODULE",
|
||||
'xCAT-genesis-base-build-support/dracut_105/ubuntu/',
|
||||
"xCAT-genesis-base-build-support/dracut_105/ubuntu/$MODULE",
|
||||
'xCAT-genesis-base-build-support/lib/',
|
||||
'xCAT-genesis-base-build-support/lib/XCAT/',
|
||||
'xCAT-genesis-base-build-support/lib/XCAT/GenesisPayload.pm',
|
||||
'xCAT-genesis-base-build-support/verify-genesis-payload',
|
||||
],
|
||||
'the tarball holds the dracut modules, the rules file and the payload verifier');
|
||||
'the tarball holds the dracut modules, the rules file and the payload verifier with its module');
|
||||
is_deeply([ grep { !m{ root/root .* 2023-11-14 22:13 } } @listing ], [],
|
||||
'every member is owned by root and dated SOURCE_DATE_EPOCH');
|
||||
|
||||
@@ -62,7 +67,7 @@ my %shipped = map { $_ => 1 }
|
||||
names(members(stage_genesis_base_sources(repo_path('.'), "$scratch/shipped.tar.bz2", $EPOCH)));
|
||||
is_deeply([ grep { !$shipped{"xCAT-genesis-base-build-support/$_"} }
|
||||
"dracut_105/el/$MODULE", "dracut_105/ubuntu/$MODULE", '80-net-name-slot.rules',
|
||||
'verify-genesis-payload' ], [],
|
||||
'verify-genesis-payload', 'lib/XCAT/GenesisPayload.pm' ], [],
|
||||
'xCAT-genesis-base in the checkout carries the dracut modules, the rules file and the verifier');
|
||||
|
||||
# A checkout that still uses the old name must stop the build, not produce an empty tarball.
|
||||
|
||||
@@ -10,7 +10,7 @@ use Test::More;
|
||||
|
||||
use XCAT::Test::File qw(repo_path slurp_repo_file);
|
||||
|
||||
my $relative = 'xCAT-genesis-builder/xCAT-genesis-base.spec';
|
||||
my $relative = 'xCAT-genesis-base/xCAT-genesis-base.spec';
|
||||
plan skip_all => "$relative not found" unless -f repo_path($relative);
|
||||
plan tests => 4;
|
||||
|
||||
|
||||
@@ -1,186 +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-base/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');
|
||||
plan skip_all => 'verify-genesis-payload not found' unless -f $verifier;
|
||||
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=<file> 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: <empty>\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);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ my %tarch = (
|
||||
|
||||
my %spec = (
|
||||
'xCAT-genesis-scripts' => "$root/xCAT-genesis-scripts/xCAT-genesis-scripts.spec",
|
||||
'xCAT-genesis-base' => "$root/xCAT-genesis-builder/xCAT-genesis-base.spec",
|
||||
'xCAT-genesis-base' => "$root/xCAT-genesis-base/xCAT-genesis-base.spec",
|
||||
);
|
||||
|
||||
for my $pkg (sort keys %spec) {
|
||||
|
||||
@@ -3,24 +3,22 @@
|
||||
# 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.
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../xCAT-genesis-base/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');
|
||||
plan skip_all => 'builddeb-genesis-base not found' unless -f $builder;
|
||||
plan skip_all => 'ubuntu module-setup.sh not found' unless -f $module;
|
||||
plan tests => 9;
|
||||
my $module = repo_path('xCAT-genesis-base/dracut_105/ubuntu/module-setup.sh');
|
||||
|
||||
# Mandatory commands a minimal Ubuntu server root does NOT already provide, and the package
|
||||
# that supplies each one on every release xCAT builds for.
|
||||
@@ -29,87 +27,53 @@ my %PACKAGE_FOR = (
|
||||
ifenslave => 'ifenslave',
|
||||
);
|
||||
|
||||
# A release carries exactly the names in its list.
|
||||
sub release {
|
||||
my %carried = map { $_ => 1 } @_;
|
||||
return sub { $carried{ $_[0] } };
|
||||
}
|
||||
|
||||
# hwclock is not in that list because the package that carries it moved. Measured on the
|
||||
# four Ubuntu management nodes: focal and jammy have it in util-linux, which is essential
|
||||
# and always in the build root, and no util-linux-extra exists to install; noble and
|
||||
# resolute have it in util-linux-extra. util-linux only Suggests that package, and this
|
||||
# build passes --no-install-recommends, so the releases that split it must name it and the
|
||||
# releases that did not must not.
|
||||
my @noble = required_packages('amd64', 'noble', release('util-linux-extra'));
|
||||
my @jammy = required_packages('amd64', 'jammy', release());
|
||||
|
||||
my %mandatory = map { $_ => 1 } mandatory_commands($module);
|
||||
my @packages = required_packages($builder);
|
||||
# An absolute path is a data file, not a command.
|
||||
my %mandatory = map { $_ => 1 } grep { !m{^/} } module_commands($module);
|
||||
my %packages = map { $_ => 1 } @jammy;
|
||||
|
||||
for my $command (sort keys %PACKAGE_FOR) {
|
||||
ok($mandatory{$command}, "the Ubuntu dracut module installs '$command' unconditionally");
|
||||
ok(scalar(grep { $_ eq $PACKAGE_FOR{$command} } @packages),
|
||||
ok($packages{ $PACKAGE_FOR{$command} },
|
||||
"the build root installs $PACKAGE_FOR{$command}, which provides '$command'");
|
||||
}
|
||||
|
||||
# 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');
|
||||
|
||||
ok($mandatory{hwclock}, "the Ubuntu dracut module installs 'hwclock' unconditionally");
|
||||
|
||||
# Naming a package apt cannot locate fails the whole install, and the script runs under
|
||||
# set -e, so an unconditional util-linux-extra stops the build on focal and jammy.
|
||||
ok(!scalar(grep { $_ eq 'util-linux-extra' } @packages),
|
||||
'the unconditional list does not name util-linux-extra');
|
||||
is_deeply([ grep { $_ eq 'util-linux-extra' } @jammy ], [],
|
||||
'a release without util-linux-extra does not get it');
|
||||
is_deeply([ @noble[ 0 .. $#noble - 1 ] ], \@jammy,
|
||||
'a release that carries util-linux-extra gets the same list ...');
|
||||
is($noble[-1], 'util-linux-extra', '... with util-linux-extra last');
|
||||
|
||||
# What the script does instead: keep a package only where apt has a candidate for it.
|
||||
{
|
||||
is_deeply(optional_packages($builder, 'util-linux-extra', 0), ['util-linux-extra'],
|
||||
'a release that carries util-linux-extra installs it');
|
||||
is_deeply(optional_packages($builder, 'util-linux-extra', 1), [],
|
||||
'a release without it installs nothing in its place');
|
||||
}
|
||||
my @ppc = required_packages('ppc64el', 'noble', release('util-linux-extra'));
|
||||
is_deeply([ @noble[ -3 .. -1 ] ], [qw(dmidecode efibootmgr util-linux-extra)],
|
||||
'amd64 adds dmidecode and efibootmgr');
|
||||
is_deeply([ @ppc[ 0 .. $#ppc - 1 ] ], [ @noble[ 0 .. $#noble - 3 ] ],
|
||||
'ppc64el gets neither dmidecode nor efibootmgr');
|
||||
|
||||
# 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;
|
||||
}
|
||||
my @asked;
|
||||
required_packages('amd64', 'noble', sub { push @asked, $_[0]; 1 });
|
||||
is_deeply(\@asked, ['util-linux-extra'], 'apt is asked only about the optional package');
|
||||
|
||||
# Run the script's own selector with apt-cache shadowed, so the decision is exercised
|
||||
# rather than read. $rc is what the shadow returns: 0 for a release that has the package.
|
||||
sub optional_packages {
|
||||
my ($path, $package, $rc) = @_;
|
||||
my $text = do { open my $fh, '<', $path or die "$path: $!"; local $/; <$fh> };
|
||||
my ($block) = $text =~ /^(optional_packages\(\)\s*\{.*?^\})/ms;
|
||||
BAIL_OUT("no optional_packages() in $path") unless $block;
|
||||
my $out = qx{bash -c 'set -u; apt-cache() { return $rc; }; $block; optional_packages $package' 2>/dev/null};
|
||||
return [ grep { length } split /\s+/, ($out // '') ];
|
||||
}
|
||||
|
||||
# Evaluate the assignment rather than parse it, so the list is the value the script uses.
|
||||
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;
|
||||
return @packages;
|
||||
}
|
||||
done_testing();
|
||||
|
||||
Reference in New Issue
Block a user