diff --git a/buildrpms.pl b/buildrpms.pl index e14bdd436..ef2b750af 100755 --- a/buildrpms.pl +++ b/buildrpms.pl @@ -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), diff --git a/xCAT-genesis-builder/builddeb-genesis-base b/xCAT-genesis-builder/builddeb-genesis-base index 8876c2d7e..41b0696d5 100755 --- a/xCAT-genesis-builder/builddeb-genesis-base +++ b/xCAT-genesis-builder/builddeb-genesis-base @@ -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. diff --git a/xCAT-genesis-builder/lib/XCAT/GenesisBuildRoot.pm b/xCAT-genesis-builder/lib/XCAT/GenesisBuildRoot.pm new file mode 100644 index 000000000..58c981a3f --- /dev/null +++ b/xCAT-genesis-builder/lib/XCAT/GenesisBuildRoot.pm @@ -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: 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; diff --git a/xCAT-genesis-builder/lib/XCAT/GenesisPayload.pm b/xCAT-genesis-builder/lib/XCAT/GenesisPayload.pm new file mode 100644 index 000000000..135bd925e --- /dev/null +++ b/xCAT-genesis-builder/lib/XCAT/GenesisPayload.pm @@ -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 " when the file + cannot be read, and with "verify-genesis-payload: no command name read + from " 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 " ()" 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 ] [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 '' ? '' : $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; diff --git a/xCAT-genesis-builder/verify-genesis-payload b/xCAT-genesis-builder/verify-genesis-payload index 1f44898c1..d5d0369b0 100755 --- a/xCAT-genesis-builder/verify-genesis-payload +++ b/xCAT-genesis-builder/verify-genesis-payload @@ -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 . --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 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:-}" >&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)' -- "$@"