mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-09-25 01:04:05 +00:00
fix(xcat-core): the Genesis build-root package list and payload check can only be read by evaluating shell
builddeb-genesis-base built its package list inline, between apt-get update and apt-get install. A test could reach the list only by cutting the REQUIRED_PACKAGES assignment out of the script and evaluating it. It could not reach the choice between renamed packages (bind9-dnsutils or dnsutils, util-linux-extra or util-linux) at all. The payload check in verify-genesis-payload was bash, so a test could only run the script and read its stderr. XCAT::GenesisBuildRoot::required_packages() now returns the list for a dpkg architecture. A code ref says which packages the release carries; the default asks apt-cache. builddeb-genesis-base calls it at the same point in the build and installs the same packages. XCAT::GenesisPayload holds the mandatory-command list and the payload check. verify-genesis-payload runs its main() and keeps the same arguments, exit codes and messages. buildrpms.pl stages the module beside the script. Both modules use core Perl only, because the build root has perl-base and nothing more. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
@@ -349,6 +349,9 @@ sub buildsources_genesis_base($) {
|
||||
# %install runs this against the extracted payload before it becomes an rpm.
|
||||
cp "xCAT-genesis-builder/verify-genesis-payload",
|
||||
"$staging_root/verify-genesis-payload";
|
||||
make_path("$staging_root/lib/XCAT");
|
||||
cp "xCAT-genesis-builder/lib/XCAT/GenesisPayload.pm",
|
||||
"$staging_root/lib/XCAT/GenesisPayload.pm";
|
||||
|
||||
unlink $support_tarball if -f $support_tarball;
|
||||
sh_or_die(qq(tar --sort=name --owner=0 --group=0 --mtime="\@$SOURCE_DATE_EPOCH" -cjf "$support_tarball" -C "$staging_parent" xCAT-genesis-base-build-support),
|
||||
|
||||
@@ -72,53 +72,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
|
||||
systemd-sysv hwdata btrfs-progs netcat-openbsd iputils-ping fdisk ncurses-term
|
||||
dpkg-dev debhelper fakeroot devscripts vim-tiny
|
||||
"
|
||||
if [ "$BUILDARCH" = "amd64" ]; then
|
||||
REQUIRED_PACKAGES="$REQUIRED_PACKAGES dmidecode efibootmgr"
|
||||
fi
|
||||
|
||||
echo "Installing build dependencies..."
|
||||
apt-get update -qq
|
||||
|
||||
# Two commands the image needs changed package between releases: nslookup left dnsutils for
|
||||
# bind9-dnsutils in 22.04, and hwclock left util-linux for util-linux-extra in 23.04. Ask apt
|
||||
# which name this release carries rather than branch on the codename.
|
||||
add_first_available() {
|
||||
local p
|
||||
for p in "$@"; do
|
||||
if apt-cache show "$p" >/dev/null 2>&1; then
|
||||
REQUIRED_PACKAGES="$REQUIRED_PACKAGES $p"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo "ERROR: $CODENAME carries none of these packages: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
# A package that exists only on some releases. 26.04 moved the backward-compatibility zone
|
||||
# names (Chile/Continental and the rest of the list the dracut module installs) out of tzdata
|
||||
# into tzdata-legacy.
|
||||
add_if_available() {
|
||||
local p
|
||||
for p in "$@"; do
|
||||
apt-cache show "$p" >/dev/null 2>&1 && REQUIRED_PACKAGES="$REQUIRED_PACKAGES $p"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
add_first_available bind9-dnsutils dnsutils
|
||||
add_first_available util-linux-extra util-linux
|
||||
add_if_available tzdata-legacy
|
||||
|
||||
# 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
|
||||
|
||||
# dpkg-architecture comes from dpkg-dev, which the line above installs.
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
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
|
||||
systemd-sysv hwdata btrfs-progs netcat-openbsd iputils-ping fdisk ncurses-term
|
||||
dpkg-dev debhelper fakeroot devscripts vim-tiny
|
||||
);
|
||||
|
||||
# Two commands the image needs changed package between releases: nslookup left dnsutils for
|
||||
# bind9-dnsutils in 22.04, and hwclock left util-linux for util-linux-extra in 23.04.
|
||||
my @RENAMED_PACKAGES = ([qw(bind9-dnsutils dnsutils)], [qw(util-linux-extra util-linux)]);
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
=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.
|
||||
For a renamed package, apt says which name the release carries, so the list does
|
||||
not branch on the codename. 26.04 moved the backward-compatibility zone names that
|
||||
the dracut module installs out of tzdata into tzdata-legacy.
|
||||
Arguments:
|
||||
$arch: the dpkg architecture (amd64, ppc64el)
|
||||
$codename: the release name, used in the error message
|
||||
$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.
|
||||
Dies with "ERROR: <codename> carries none of these packages: ..." when the release
|
||||
carries neither name of a renamed package.
|
||||
|
||||
=cut
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
sub required_packages {
|
||||
my ($arch, $codename, $carries) = @_;
|
||||
$carries ||= \&apt_carries;
|
||||
|
||||
my @packages = @BASE_PACKAGES;
|
||||
push @packages, qw(dmidecode efibootmgr) if $arch eq 'amd64';
|
||||
|
||||
RENAMED:
|
||||
for my $alternatives (@RENAMED_PACKAGES) {
|
||||
for my $package (@$alternatives) {
|
||||
if ($carries->($package)) {
|
||||
push @packages, $package;
|
||||
next RENAMED;
|
||||
}
|
||||
}
|
||||
die "ERROR: $codename carries none of these packages: @$alternatives\n";
|
||||
}
|
||||
push @packages, 'tzdata-legacy' if $carries->('tzdata-legacy');
|
||||
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)' -- "$@"
|
||||
|
||||
Reference in New Issue
Block a user