diff --git a/xCAT-server/lib/perl/xCAT/SvrUtils.pm b/xCAT-server/lib/perl/xCAT/SvrUtils.pm index 2b286c95e..03379afd9 100644 --- a/xCAT-server/lib/perl/xCAT/SvrUtils.pm +++ b/xCAT-server/lib/perl/xCAT/SvrUtils.pm @@ -1708,6 +1708,43 @@ sub nfs_export_exists { return 0; } +#----------------------------------------------------------------------------- + +=head3 nfs_export_line + + The /etc/exports line for a directory a service node serves to its nodes. + + Arguments: + $dir the directory to export + reexport true when $dir is itself an NFS mount + + Returns: + one export line, with no trailing newline. + + exportfs refuses an NFS mount without an explicit fsid, and crossmnt lets a client cross into + the mount below. The fsid is derived from the path, so a restart does not make client mounts + stale. + +=cut + +#----------------------------------------------------------------------------- +sub nfs_export_line { + my ( $dir, %opts ) = _nfs_method_args(@_); + my $options = 'rw,no_root_squash,sync,no_subtree_check,insecure'; + if ( $opts{reexport} ) { + $options .= ',crossmnt,fsid=' . _nfs_export_fsid($dir); + } + return "$dir *($options)"; +} + +# A stable, non-zero fsid for a path. 0 is reserved for the export root. +sub _nfs_export_fsid { + my ($dir) = @_; + my $sum = 0; + $sum = ( $sum * 31 + ord($_) ) % 2147483647 for split //, $dir; + return $sum || 1; +} + sub _ensure_nfs_exported { my ($nfsserver, $nfsdirectory, $callback, %opts) = @_; my $export_options = 'rw,no_root_squash,sync,no_subtree_check,insecure'; @@ -2512,4 +2549,29 @@ sub searchcompressedrootimg{ } + +#----------------------------------------------------------------------------- + +=head3 named_service_action + + Which service action brings a freshly written named configuration into effect. + + Linux gets a restart. On Debian the package already runs named, so a start is a no-op and + the daemon keeps serving the configuration it read at install time. + + Arguments: the platform, 'aix' or 'linux' + Returns: 'start', 'restart', or '' for a platform with no action + +=cut + +#----------------------------------------------------------------------------- +sub named_service_action { + my ($platform) = @_; + $platform = '' unless defined $platform; + return 'start' if $platform eq 'aix'; + return 'restart' if $platform eq 'linux'; + return ''; +} + + 1; diff --git a/xCAT-server/lib/xcat/plugins/AAsn.pm b/xCAT-server/lib/xcat/plugins/AAsn.pm index 188d7ebdc..543967e3a 100644 --- a/xCAT-server/lib/xcat/plugins/AAsn.pm +++ b/xCAT-server/lib/xcat/plugins/AAsn.pm @@ -788,6 +788,8 @@ sub setup_FTP =cut #----------------------------------------------------------------------------- + + sub setup_DNS { my $srvclist = shift; @@ -827,11 +829,15 @@ sub setup_DNS #} #my $rc = xCAT::Utils->startService($serv); + # Restart, not start. makenamed.conf has just rewritten named.conf, and a start is a no-op + # where the package already runs the daemon -- Debian does -- so the service keeps serving the + # configuration it read at install time. my $rc = 0; - if (xCAT::Utils->isAIX()) { + my $action = xCAT::SvrUtils::named_service_action(xCAT::Utils->isAIX() ? 'aix' : 'linux'); + if ($action eq 'start') { $rc = xCAT::Utils->startService("named"); - } elsif (xCAT::Utils->isLinux()) { - $rc = xCAT::Utils->startservice("named"); + } elsif ($action eq 'restart') { + $rc = xCAT::Utils->restartservice("named"); } if ($rc != 0) @@ -919,6 +925,20 @@ sub setup_NFS my $rc = 0; if (xCAT::Utils->isLinux()) { + # nfsserver=1 means this node serves files. With site.installloc set the install directory + # is itself a mount here, and re-exporting one needs an fsid. + my $installdir = xCAT::TableUtils->getInstallDir() || "/install"; + unless (xCAT::SvrUtils->nfs_export_exists($installdir)) + { + my $line = xCAT::SvrUtils->nfs_export_line($installdir, + reexport => (xCAT::Utils->isMounted($installdir) ? 1 : 0)); + xCAT::Utils->runcmd("/bin/echo '$line' >> /etc/exports", 0); + if ($::RUNCMD_RC != 0) + { + xCAT::MsgUtils->message('S', "Could not add $installdir to /etc/exports."); + } + } + #my $os = xCAT::Utils->osver(); #if ($os =~ /sles.*/) #{ @@ -930,6 +950,13 @@ sub setup_NFS # $rc = xCAT::Utils->startService("nfs"); #} $rc = xCAT::Utils->startservice("nfs"); + + # After the daemon, so a fresh export is picked up. + xCAT::Utils->runcmd("/usr/sbin/exportfs -a", 0); + if ($::RUNCMD_RC != 0) + { + xCAT::MsgUtils->message('S', "Error with /usr/sbin/exportfs -a."); + } } else { #AIX diff --git a/xCAT-server/sbin/makenamed.conf b/xCAT-server/sbin/makenamed.conf index f63c0f480..9de0e704d 100755 --- a/xCAT-server/sbin/makenamed.conf +++ b/xCAT-server/sbin/makenamed.conf @@ -19,7 +19,7 @@ is_lsb_ubuntu () exit 1 # Not Ubuntu } - ' /etc/lsb-release >/dev/null 2>&1 + ' "${1:-/etc/lsb-release}" >/dev/null 2>&1 # Routine exit status is exit status of the last command -- the awk script. # @@ -28,14 +28,56 @@ is_lsb_ubuntu () } -DIRECTORY=/var/named +# forwarder_addresses [ []] +# +# The addresses named must forward to, one per line. +# +# A loopback address is not a forwarder: under systemd-resolved /etc/resolv.conf holds the +# 127.0.0.53 stub, and that stub points back at this named. The real servers are in the +# systemd-resolved uplink file. +forwarder_addresses() +{ + _fa_resolv=${1:-/etc/resolv.conf} + _fa_uplink=${2:-/run/systemd/resolve/resolv.conf} + _fa_addrs=$(_fa_usable "$_fa_resolv") + if [ -z "$_fa_addrs" ]; then + _fa_addrs=$(_fa_usable "$_fa_uplink") + fi + [ -n "$_fa_addrs" ] && printf '%s\n' "$_fa_addrs" + return 0 +} -# check for SLES -grep -s -q sles /etc/os-release -IS_SLES=$? -if [ -f /etc/SuSE-release ] || [ $IS_SLES -eq 0 ]; then - DIRECTORY=/var/lib/named -fi +_fa_usable() +{ + [ -r "$1" ] || return 0 + awk '$1 == "nameserver" && $2 !~ /^127\./ && $2 != "::1" { print $2 }' "$1" 2>/dev/null +} + +# named_directory [ [ []]] +# +# The working directory for named. It must be writable by the user named drops to: Debian runs +# it as "bind" and ships /var/cache/bind, while /var/named is created root-owned. +named_directory() +{ + _nd_lsb=${1:-/etc/lsb-release} + _nd_os=${2:-/etc/os-release} + _nd_suse=${3:-/etc/SuSE-release} + + if is_lsb_ubuntu "$_nd_lsb"; then + echo /var/cache/bind + return 0 + fi + if [ -f "$_nd_suse" ] || grep -s -q sles "$_nd_os"; then + echo /var/lib/named + return 0 + fi + echo /var/named +} + +# A test loads this file for the routines above, so the rest must not run on a require. +[ "${MAKENAMED_LIB:-}" = 1 ] && return 0 + +DIRECTORY=$(named_directory) FILE=/etc/named.conf if ( is_lsb_ubuntu ); then @@ -58,7 +100,7 @@ echo "options { forward only; forwarders {" >$FILE -for i in $(grep "^nameserver" /etc/resolv.conf | awk '{print $2}') +forwarder_addresses | while read -r i do echo " $i;" done >>$FILE diff --git a/xCAT-server/share/xcat/install/ubuntu/service.subiquity.tmpl b/xCAT-server/share/xcat/install/ubuntu/service.subiquity.tmpl new file mode 100644 index 000000000..ffd5b545d --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/service.subiquity.tmpl @@ -0,0 +1,137 @@ +#cloud-config +autoinstall: + version: 1 + keyboard: {layout: us, toggle: null, variant: ''} + locale: en_US + network: + version: 2 + ethernets: + xcat-install: + match: + name: "e*" + dhcp4: true + ssh: + allow-pw: true + authorized-keys: [] + install-server: true + identity: + realname: 'xCAT Admin' + username: xcatadm + hostname: #HOSTNAME# + password: "#CRYPTORLOCKED:passwd:key=system,username=root:password#" +#UBUNTU_SUBIQUITY_APT_CONFIG# + kernel: + package: linux-generic + user-data: + hostname: #HOSTNAME# + disable_root: false + package_update: false + package_upgrade: false + timezone: #TABLE:site:key=timezone:value# + chpasswd: + list: + - "root:#CRYPTORLOCKED:passwd:key=system,username=root:password#" + expire: false + packages: + - openssh-server + - openssh-client + - wget + - vim + - rsync + - busybox-static + - gawk + - bind9-dnsutils + - chrony + - gpg + - #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL# + early-commands: + - | + exec >/tmp/pre-install.log 2>&1 + set -eux + echo "=== xCAT early-commands ===" + echo "=== network ===" + ip addr show + ip route show + echo "=== disk detection ===" + wget -T 30 -O /tmp/getinstdisk http://#XCATVAR:XCATMASTER##COLONHTTPPORT#/install/autoinst/getinstdisk + test -s /tmp/getinstdisk + chmod u+x /tmp/getinstdisk + /tmp/getinstdisk + echo "=== pre-install script ===" + wget -T 30 -O /tmp/pre.sh http://#XCATVAR:XCATMASTER##COLONHTTPPORT#/install/autoinst/#HOSTNAME#.pre + test -s /tmp/pre.sh + chmod u+x /tmp/pre.sh + /tmp/pre.sh + echo "=== storage injection ===" + test -s /tmp/partitionfile + sed -i '/^\.\.\.$/d' /autoinstall.yaml + cat /tmp/partitionfile >> /autoinstall.yaml + echo "=== DNS setup ===" + # glibc's resolver discards a nameserver line naming a host, so writing the xcatmaster + # *name* leaves the installer -- and the in-target apt that inherits this file -- with no + # DNS. Resolve it here, while the live installer's DHCP resolv.conf still works, and keep + # that file when the name does not resolve. A name written back is the case this step + # exists to prevent. + xcatmaster_host="#TABLE:noderes:$NODE:xcatmaster#" + xcatmaster_ip="$(getent ahostsv4 "$xcatmaster_host" | awk '{print $1; exit}')" + if [ -n "$xcatmaster_ip" ]; then + rm -f /etc/resolv.conf + echo "nameserver $xcatmaster_ip" >/etc/resolv.conf + else + echo "xcat: $xcatmaster_host has no IPv4 address; keeping the resolver DHCP supplied" + fi + echo "domain #TABLE:site:key=domain:value#" >>/etc/resolv.conf + echo "=== early-commands complete ===" + late-commands: + - mkdir -p /target/var/log/xcat/ + - '{ + cat /tmp/pre-install.log >> /target/var/log/xcat/xcat.log; + installnic="#SUBIQUITYINSTALLNIC#"; + installmac="#SUBIQUITYINSTALLMAC#"; + mkdir -p /target/etc/netplan; + if [ -z "${installnic}" ]; then + printf ''%s\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \"${installmac}\"" " dhcp4: true" " dhcp4-overrides:" " use-domains: true" >/target/etc/netplan/00-xcat-install.yaml; + else + printf ''%s\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \"${installmac}\"" " set-name: ${installnic}" " dhcp4: true" " dhcp4-overrides:" " use-domains: true" >/target/etc/netplan/00-xcat-install.yaml; + fi; + chmod 600 /target/etc/netplan/00-xcat-install.yaml; + printf ''%s\n'' ''#HOSTNAME#'' >/target/etc/hostname; + if grep -q ''^127\.0\.1\.1'' /target/etc/hosts; then + sed -i ''s/^127\.0\.1\.1.*/127.0.1.1 #HOSTNAME#/'' /target/etc/hosts; + else + printf ''%s\n'' ''127.0.1.1 #HOSTNAME#'' >>/target/etc/hosts; + fi; + mkdir -p /target/etc/cloud; + touch /target/etc/cloud/cloud-init.disabled; + echo "Updating kernel command line..."; + printf ''%s\n'' ''GRUB_CMDLINE_LINUX="#TABLEBLANKOKAY:bootparams:$NODE:kcmdline#"'' >>/target/etc/default/grub; + curtin in-target --target /target update-grub2; + echo "Running late_command Installation script..."; + wget -T 30 http://#XCATVAR:XCATMASTER##COLONHTTPPORT#/install/autoinst/#HOSTNAME#.post; + chmod u+x #HOSTNAME#.post; + cp ./#HOSTNAME#.post /target/root/post.script; + curtin in-target --target /target /root/post.script; + } >>/target/var/log/xcat/xcat.log 2>&1' + # The installer's sources for the otherpkgs repository and the pkgdir mirrors, and the apt + # configuration that kept recommended packages out, served the install; ospkgs and otherpkgs + # write their own after the first boot. A separate item, so the status of the post script above + # still decides whether the install goes on. + - rm -f /target/etc/apt/sources.list.d/xcat-otherpkgs-*.list /target/etc/apt/sources.list.d/xcat-otherpkgs-*.sources /target/etc/apt/sources.list.d/xcat-pkgdir-*.list /target/etc/apt/sources.list.d/xcat-pkgdir-*.sources /target/etc/apt/apt.conf.d/94curtin-config + # Flip the node to local-disk boot, or it PXE-loops back into the installer on reboot. + # xcatd's install monitor greets with "ready", then answers "next" with "done" and runs + # "nodeset next". Require both tokens: another service on that port is not a flipped + # node. The monitor listens on site.xcatiport; TABLEBLANKOKAY because an absent key fails a + # plain TABLE lookup, and with it the whole template. + # Retry, and log a failure rather than reinstall silently. Both reads are bounded: a monitor + # that accepts and never answers would block a bare read and hang the install here. A + # monitor that died and never came back is #7759. + - ['bash', '-c', 'xm=#XCATVAR:XCATMASTER#; port="#TABLEBLANKOKAY:site:key=xcatiport:value#"; [ -n "$port" ] || port=3002; ok=0; for i in 1 2 3 4 5; do if exec 3<>/dev/tcp/$xm/$port; then if read -r -t 10 hello <&3 && [ "$hello" = "ready" ]; then printf "next\n" >&3; if read -r -t 10 ack <&3 && [ "$ack" = "done" ]; then ok=1; fi; fi; exec 3>&- 3<&-; [ "$ok" = 1 ] && break; fi; sleep 5; done; if [ "$ok" != 1 ]; then echo "xcat: FAILED to flip $(hostname) to local-disk boot via $xm:$port; the node will PXE back into the installer" >>/target/var/log/xcat/xcat.log; fi; exit 0'] + error-commands: + # Subiquity waits for every error command to return. "nc -l 8080" waits for a collector, which + # an unattended install does not have. Keep the archive on the installer, and print the end of + # the curtin log to the console, which the management node records. + # + # Both destinations are read from the environment so a test can point them at its own + # directory. The installer sets neither, so an install writes where it always did. + - ['sh', '-c', 'tar -c --ignore-failed-read --transform="s/^/#HOSTNAME#-logs\//" /var/crash /var/log/installer /tmp/pre-install.log /autoinstall.yaml >"${XCAT_ERROR_ARCHIVE:-/run/#HOSTNAME#-logs.tar}" 2>/dev/null; exit 0'] + - ['sh', '-c', 'tail -n 80 /var/log/installer/curtin-install.log >"${XCAT_ERROR_CONSOLE:-/dev/console}" 2>/dev/null; exit 0'] diff --git a/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh new file mode 100755 index 000000000..0fb2c7ac8 --- /dev/null +++ b/xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh @@ -0,0 +1,240 @@ +#!/bin/sh +# +# check_provisioning_source.sh --baseline +# check_provisioning_source.sh +# check_provisioning_source.sh --count [] +# +# Answer which server sent the compute node its boot payload during THIS run. +# +# The hierarchy cases set noderes.servicenode and read SERVICEGROUP back out of the compute +# node's xcatinfo. That records what xCAT wrote, not where the node fetched from. The management +# node, the service node and the compute node share one subnet, both dhcpd instances hold a +# reservation for the compute node, and xCAT does not arbitrate between them. The compute node +# takes whichever server answers first, so a flat provision satisfies every other assertion the +# cases make. +# +# The httpd access logs settle it. The xNBA exchange hands out an http:// filename, so the +# kernel, the initrd, the root image and the install tree are HTTP requests logged against the +# compute node's address on the server that answered them. +# +# Two things bound what counts, and without either one the answer is wrong in both directions. +# Run --baseline before provisioning: an earlier flat run leaves management-node requests for the +# same address, and counting the whole log fails a later hierarchical run for them. And only a +# request under /install or /tftpboot is a boot payload: a 404 for /favicon.ico is a request from +# the compute node that carries no payload, and it must not stand for one. wget asks for the root +# image as //install/..., so the leading slash repeats. +# +# Scope: the PXE ROM exchange hands out xcat/xnba.kpxe over TFTP and httpd never sees it. +# xnba.kpxe is the same binary on both servers, so it decides nothing about the fetch source. +# +# Run this on the management node. It reads the service node's log with "xdsh -e", which copies +# this script to the service node and runs it there with --count. + +set -u + +TOKEN=XCAT_HTTPD_REQUESTS +STATE="${XCAT_PROV_SOURCE_STATE:-/var/tmp/xcat-provisioning-source.base}" + +# Every readable candidate log. The combined format puts the client address in field 1, and the +# Debian per-vhost format puts the vhost there and the client in field 2. +local_logs() +{ + # An explicit log REPLACES the search. Without this a test that points here still reads the + # host's own apache log, so it cannot control what the check counts. + if [ -n "${XCAT_HTTPD_ACCESS_LOG:-}" ]; then + [ -r "$XCAT_HTTPD_ACCESS_LOG" ] && echo "$XCAT_HTTPD_ACCESS_LOG" + return 0 + fi + + for f in /var/log/httpd/access_log \ + /var/log/apache2/access.log \ + /var/log/apache2/access_log \ + /var/log/apache2/other_vhosts_access.log + do + [ -r "$f" ] || continue + echo "$f" + done +} + +# Record how many lines each log holds now, as file:lines joined by commas. +baseline_local() +{ + spec="" + for f in $(local_logs); do + n=$(wc -l <"$f" 2>/dev/null | tr -d ' ') + [ -n "$n" ] || n=0 + spec="$spec,$f:$n" + done + echo "$TOKEN base $(echo "$spec" | sed 's/^,//')" +} + +# Count the boot-payload requests this address made AFTER the baseline. A log shorter than its +# baseline was rotated, so its baseline no longer locates anything and the whole file is new. +count_local_requests() +{ + ip="$1" + spec="${2:-}" + logs=$(local_logs | tr '\n' ' ') + + if [ -z "$logs" ]; then + echo "$TOKEN nolog 0 0 0" + return 0 + fi + + checked="" + for f in $logs; do + base=$(echo "$spec" | tr ',' '\n' | sed -n "s|^$f:||p" | head -1) + [ -n "$base" ] || base=0 + now=$(wc -l <"$f" 2>/dev/null | tr -d ' ') + [ -n "$now" ] || now=0 + [ "$now" -lt "$base" ] && base=0 + checked="$checked,$f:$base" + done + + # shellcheck disable=SC2086 + awk -v ip="$ip" -v token="$TOKEN" -v bases="$(echo "$checked" | sed 's/^,//')" ' + BEGIN { + n = split(bases, a, ",") + for (i = 1; i <= n; i++) { + if (a[i] == "") continue + p = index(a[i], ":") + if (p) base[substr(a[i], 1, p - 1)] = substr(a[i], p + 1) + 0 + } + } + { + total++ + b = (FILENAME in base) ? base[FILENAME] : 0 + if (FNR <= b) next + fresh++ + if ($1 != ip && $2 != ip) next + path = "" + for (i = 1; i <= NF; i++) if ($i ~ /^"(GET|HEAD|POST)$/) { path = $(i + 1); break } + if (path ~ /^\/+(install|tftpboot)\//) payload++ + } + END { print token, "ok", payload + 0, fresh + 0, total + 0 } + ' $logs +} + +# xdsh prefixes each line with the node name, so read the fields after the token. +read_counts() +{ + awk -v token="$TOKEN" ' + { for (i = 1; i <= NF; i++) if ($i == token) { print $(i+1), $(i+2), $(i+3), $(i+4); exit } } + ' +} + +read_baseline() +{ + awk -v token="$TOKEN" ' + { for (i = 1; i <= NF; i++) if ($i == token && $(i+1) == "base") { print $(i+2); exit } } + ' +} + +node_address() +{ + node="$1" + addr=$(lsdef -t node -o "$node" -i ip 2>/dev/null | sed -n 's/^[[:space:]]*ip=//p' | head -1) + [ -n "$addr" ] || addr=$(getent ahostsv4 "$node" 2>/dev/null | awk '{ print $1; exit }') + echo "$addr" +} + +if [ "${1:-}" = "--count" ]; then + count_local_requests "${2:-}" "${3:-}" + exit 0 +fi + +if [ "${1:-}" = "--baseline-local" ]; then + baseline_local + exit 0 +fi + +MODE=check +if [ "${1:-}" = "--baseline" ]; then + MODE=baseline + shift +fi + +CN="${1:-}" +SN="${2:-}" +if [ -z "$CN" ] || [ -z "$SN" ]; then + echo "provisioning source error: usage: $0 [--baseline] " >&2 + exit 2 +fi + +SELF=$(readlink -f "$0") +MN=$(hostname) + +if [ "$MODE" = baseline ]; then + MN_BASE=$(baseline_local | read_baseline) + SN_BASE=$(xdsh "$SN" -e "$SELF" --baseline-local 2>&1 | read_baseline) + if [ -z "$MN_BASE" ]; then + echo "provisioning source error: no access log to baseline on $MN" >&2 + exit 1 + fi + if [ -z "$SN_BASE" ]; then + echo "provisioning source error: no access log to baseline on $SN" >&2 + exit 1 + fi + printf 'MN %s\nSN %s\n' "$MN_BASE" "$SN_BASE" >"$STATE" || exit 1 + echo "provisioning source baseline recorded in $STATE" + exit 0 +fi + +# Without a baseline this cannot tell a request from this run from one an earlier run left +# behind, so it refuses rather than answering from the whole log. +if [ ! -r "$STATE" ]; then + echo "provisioning source error: no baseline in $STATE. Run --baseline before provisioning" >&2 + exit 1 +fi +MN_BASE=$(sed -n 's/^MN //p' "$STATE" | head -1) +SN_BASE=$(sed -n 's/^SN //p' "$STATE" | head -1) + +CN_IP=$(node_address "$CN") +if [ -z "$CN_IP" ]; then + echo "provisioning source error: $CN has no address, so no log can be read for it" >&2 + exit 1 +fi + +MN_COUNTS=$(count_local_requests "$CN_IP" "$MN_BASE" | read_counts) +SN_COUNTS=$(xdsh "$SN" -e "$SELF" --count "$CN_IP" "$SN_BASE" 2>&1 | read_counts) + +set -- $MN_COUNTS +MN_STATE="${1:-none}" MN_REQ="${2:-0}" MN_NEW="${3:-0}" MN_LINES="${4:-0}" +set -- $SN_COUNTS +SN_STATE="${1:-none}" SN_REQ="${2:-0}" SN_NEW="${3:-0}" SN_LINES="${4:-0}" + +echo "$SN served $CN $SN_REQ boot-payload request(s) since the baseline (log $SN_STATE, $SN_NEW new of $SN_LINES lines)" +echo "$MN served $CN $MN_REQ boot-payload request(s) since the baseline (log $MN_STATE, $MN_NEW new of $MN_LINES lines)" + +RC=0 + +if [ "$SN_STATE" != ok ]; then + echo "provisioning source error: no httpd access log could be read on $SN" >&2 + RC=1 +fi + +# An empty log makes "the management node served nothing" a property of the file, not a +# measurement. New lines are NOT required: the service node is provisioned before the baseline, so +# after it a correct hierarchical run leaves the management node's log unchanged. +if [ "$MN_STATE" != ok ] || [ "$MN_LINES" -eq 0 ]; then + echo "provisioning source error: no httpd access log with entries could be read on $MN" >&2 + RC=1 +fi + +if [ "$RC" -eq 0 ] && [ "$SN_REQ" -eq 0 ]; then + echo "provisioning source error: $SN served $CN no boot payload, so it did not provision it" >&2 + RC=1 +fi + +# Count requests, not bytes. A 304 or a HEAD carries no body, so the management node can answer +# for the compute node and still log 0 bytes. +if [ "$RC" -eq 0 ] && [ "$MN_REQ" -gt 0 ]; then + echo "provisioning source error: $MN answered $MN_REQ boot-payload request(s) for $CN, so this provision was flat" >&2 + RC=1 +fi + +if [ "$RC" -eq 0 ]; then + echo "provisioning source ok: $SN served $CN and $MN served it nothing" +fi + +exit "$RC" diff --git a/xCAT-test/autotest/testcase/installation/SN_setup_case b/xCAT-test/autotest/testcase/installation/SN_setup_case index 97447b6df..a5f5ace43 100644 --- a/xCAT-test/autotest/testcase/installation/SN_setup_case +++ b/xCAT-test/autotest/testcase/installation/SN_setup_case @@ -1,7 +1,10 @@ start:SN_setup_case -os:Linux +os:rhels,sles label:sn_diskful,provision #stop:yes +# createrepo, the xcat/ prefix in the otherpkgs pkglist paths, and yum or zypper for +# perl-Sys-Virt are all rpm. A Debian management node has none of them, so os: above +# names the rpm families only and SN_setup_case_ub covers Ubuntu. cmd:fdisk -l cmd:df -T cmd:XCAT_DATABASE=$$XCAT_DATABASE /opt/xcat/share/xcat/tools/autotest/testcase/installation/pre_deploy_sn __GETNODEATTR($$SN,os)__ __GETNODEATTR($$SN,arch)__ diff --git a/xCAT-test/autotest/testcase/installation/SN_setup_case_ub b/xCAT-test/autotest/testcase/installation/SN_setup_case_ub new file mode 100644 index 000000000..1aedfc146 --- /dev/null +++ b/xCAT-test/autotest/testcase/installation/SN_setup_case_ub @@ -0,0 +1,118 @@ +start:SN_setup_case_ub +os:ubuntu +label:sn_diskful,provision +#stop:yes +# The Debian counterpart of SN_setup_case. Same order, same database setup and the same node +# state at the end, so reg_linux_diskless_installation_hierarchy and +# reg_linux_diskfull_installation_hierarchy run after either one without a change. +# +# Four things differ, all of them package format. A Debian management node has no createrepo, +# so the staged repositories are indexed with dpkg-scanpackages. service.ubuntu.otherpkgs.pkglist +# names its directories as xcat-core and xcat-dep, without the xcat/ prefix the EL pkglists use, +# so otherpkgdir goes one level deeper. perl-Sys-Virt is libsys-virt-perl. SN_install_openbmc_py.sh +# is not called: it acts on rhels7 ppc64 only. +cmd:fdisk -l +cmd:df -T +cmd:XCAT_DATABASE=$$XCAT_DATABASE /opt/xcat/share/xcat/tools/autotest/testcase/installation/pre_deploy_sn __GETNODEATTR($$SN,os)__ __GETNODEATTR($$SN,arch)__ +check:rc==0 + +cmd:chtab key=nameservers site.value="" +check:rc==0 + +cmd:makedns -n +check:rc==0 +cmd:if [ -x /usr/bin/goconserver ]; then makegocons $$SN; else makeconservercf $$SN;fi +check:rc==0 +cmd:sleep 20 +cmd:if [[ "__GETNODEATTR($$SN,arch)__" =~ "ppc64" ]]; then getmacs -D $$SN -V; fi +check:rc==0 +cmd:makedhcp -n +check:rc==0 +cmd:makedhcp -a +check:rc==0 +cmd:sleep 2 +cmd:if [ -f /etc/kea/kea-dhcp4.conf ]; then grep $$SN /etc/kea/kea-dhcp4.conf; elif [ -f /var/lib/dhcp/dhcpd.leases ]; then grep $$SN /var/lib/dhcp/dhcpd.leases; fi +check:output=~$$SN +cmd:chdef -t node $$SN groups=service,all +check:rc==0 +cmd:chdef -t group -o service profile=service primarynic=mac installnic=mac +check:rc==0 +cmd:chdef -t group -o service setupnfs=1 setupdhcp=1 setuptftp=1 setupnameserver=1 setupconserver=2 setupntp=1 +check:rc==0 +cmd:chdef -t group -o service nfsserver=$$MN tftpserver=$$MN xcatmaster=$$MN monserver=$$MN +check:rc==0 +cmd:chtab node=service postscripts.postscripts="servicenode" +check:rc==0 + +cmd:copycds $$ISO +check:rc==0 + +cmd:chdef -t site clustersite installloc="/install" +check:rc==0 + +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/installation/stage_sn_apt_repo.sh /install/post/otherpkgs/__GETNODEATTR($$SN,os)__/__GETNODEATTR($$SN,arch)__/xcat/xcat-core +check:rc==0 +check:output=~apt index ok + +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/installation/stage_sn_apt_repo.sh /install/post/otherpkgs/__GETNODEATTR($$SN,os)__/__GETNODEATTR($$SN,arch)__/xcat/xcat-dep __GETNODEATTR($$SN,os)__ +check:rc==0 +check:output=~apt index ok + +# service.ubuntu.otherpkgs.pkglist names mariadb-client with no directory, so the otherpkgs +# postscript writes a source line for otherpkgdir itself. apt-get update then fails on it, and +# the postscript deletes the source line it had just written for xcat-core, because the +# apt-cache show that follows returns non-zero. Index this level too. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/installation/stage_sn_apt_repo.sh /install/post/otherpkgs/__GETNODEATTR($$SN,os)__/__GETNODEATTR($$SN,arch)__/xcat +check:rc==0 +check:output=~apt index ok + +cmd:chdef -t osimage __GETNODEATTR($$SN,os)__-__GETNODEATTR($$SN,arch)__-install-service otherpkgdir=/install/post/otherpkgs/__GETNODEATTR($$SN,os)__/__GETNODEATTR($$SN,arch)__/xcat +check:rc==0 + +cmd:chdef -t osimage __GETNODEATTR($$SN,os)__-__GETNODEATTR($$SN,arch)__-install-service otherpkglist=/opt/xcat/share/xcat/install/ubuntu/service.ubuntu.otherpkgs.pkglist +check:rc==0 + +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/retry_install.sh $$SN __GETNODEATTR($$SN,os)__-__GETNODEATTR($$SN,arch)__-install-service +check:rc==0 +check:output=~Provision node\(s\)\: $$SN + +cmd:if [[ -f /var/lib/kea/kea-leases4.csv ]]; then cat /var/lib/kea/kea-leases4.csv; elif [[ -f /var/lib/dhcp/dhcpd.leases ]]; then cat /var/lib/dhcp/dhcpd.leases; fi + +cmd:ping $$SN -c 3 +check:rc==0 +check:output=~64 bytes from $$SN +cmd:lsdef -l $$SN | grep status +check:rc==0 +check:output=~booted +cmd:xdsh $$SN date +check:rc==0 +check:output=~\d\d:\d\d:\d\d +cmd:xdsh $$SN "ps -ef |grep xcatd" +check:rc==0 +check:output=~xcatd: +# Ubuntu's /etc/bash.bashrc returns before /etc/profile.d on a non-interactive shell, so the +# xCAT commands are not on the PATH xdsh gets. The EL shell reads profile.d there and the EL +# case does not need this. +cmd:xdsh $$SN ". /etc/profile.d/xcat.sh; lsdef" +check:rc==0 +check:output=~$$SN: $$SN +cmd:xdsh $$SN ". /etc/profile.d/xcat.sh; tabdump site" +check:rc==0 +check:output=~tftpdir +cmd:xdsh $$SN "mount" +check:rc==0 +check:output=~/install on /install +cmd:xdsh $$SN "mount" +check:rc==0 +check:output=~/tftpboot on /tftpboot +cmd:xdsh $$SN cat /opt/xcat/xcatinfo +check:rc==0 +check:output=~NODE=$$SN +check:output=~IMAGENAME=__GETNODEATTR($$SN,os)__-__GETNODEATTR($$SN,arch)__-install-service +cmd:xdsh $$SN "cat /var/log/xcat/xcat.log" +cmd:xdsh $$SN "DEBIAN_FRONTEND=noninteractive apt-get install -y libsys-virt-perl" +check:rc==0 +cmd:makentp -a +check:rc==0 +cmd:if [ -x /usr/bin/goconserver ]; then makegocons -d $$SN; else makeconservercf -d $$SN;fi +end diff --git a/xCAT-test/autotest/testcase/installation/reg_linux_diskfull_installation_hierarchy b/xCAT-test/autotest/testcase/installation/reg_linux_diskfull_installation_hierarchy index 7b3680355..47b329f07 100644 --- a/xCAT-test/autotest/testcase/installation/reg_linux_diskfull_installation_hierarchy +++ b/xCAT-test/autotest/testcase/installation/reg_linux_diskfull_installation_hierarchy @@ -47,6 +47,10 @@ check:rc==0 cmd:if [[ -f /test.synclist ]] ;then mv -f /test.synclist /test.synclist.bak;fi; cmd:echo "/test.synclist -> /test.synclist" > /test.synclist;chdef -t osimage -o __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-install-compute synclists=/test.synclist check:rc==0 +# The logs carry requests from earlier runs, so record where both of them end +# before this provision. The check below reads only what comes after. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/check_provisioning_source.sh --baseline $$CN $$SN +check:rc==0 cmd:nodeset $$CN osimage=__GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-install-compute check:rc==0 cmd:updatenode $$CN -f @@ -74,6 +78,13 @@ check:rc==0 check:output=~NODE=$$CN check:output=~IMAGENAME=__GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-install-compute check:output=~SERVICEGROUP=$$SN +# SERVICEGROUP above is a copy of the noderes.servicenode value this case wrote, so it says +# what xCAT set and not which server sent the boot payload. Both dhcpd instances answer for +# $$CN, so the management node can win the xNBA exchange and serve it. The httpd access logs +# are what separates the two topologies. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/check_provisioning_source.sh $$CN $$SN +check:rc==0 +check:output=~provisioning source ok cmd:xdsh $$CN "cat /var/log/xcat/xcat.log" cmd:xdsh $$CN "cat /test.synclist" check:rc==0 diff --git a/xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_hierarchy b/xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_hierarchy index 5b6d56c03..fbc24cc23 100644 --- a/xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_hierarchy +++ b/xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_hierarchy @@ -53,6 +53,10 @@ check:rc==0 cmd:packimage __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute check:rc==0 +# The logs carry requests from earlier runs, so record where both of them end +# before this provision. The check below reads only what comes after. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/check_provisioning_source.sh --baseline $$CN $$SN +check:rc==0 cmd:nodeset $$CN osimage=__GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute check:rc==0 @@ -82,6 +86,13 @@ check:output=~NODE=$$CN check:output=~IMAGENAME='__GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute' check:output=~IMAGEUUID='\w+-\w+-\w+-\w+-\w+' check:output=~SERVICEGROUP=$$SN +# SERVICEGROUP above is a copy of the noderes.servicenode value this case wrote, so it says +# what xCAT set and not which server sent the boot payload. Both dhcpd instances answer for +# $$CN, so the management node can win the xNBA exchange and serve it. The httpd access logs +# are what separates the two topologies. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/check_provisioning_source.sh $$CN $$SN +check:rc==0 +check:output=~provisioning source ok cmd:xdsh $$CN "cat /var/log/xcat/xcat.log" cmd:rootimgdir=`lsdef -t osimage __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute|grep rootimgdir|awk -F'=' '{print $2}'`; if [ -d $rootimgdir.regbak ]; then rm -rf $rootimgdir; mv $rootimgdir.regbak $rootimgdir; fi check:rc==0 @@ -171,6 +182,10 @@ cmd:packimage -m squashfs __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-ne check:rc==0 check:output=~archive method:squashfs +# The logs carry requests from earlier runs, so record where both of them end +# before this provision. The check below reads only what comes after. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/check_provisioning_source.sh --baseline $$CN $$SN +check:rc==0 cmd:nodeset $$CN osimage=__GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute check:rc==0 @@ -200,6 +215,13 @@ check:output=~NODE=$$CN check:output=~IMAGENAME='__GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute' check:output=~IMAGEUUID='\w+-\w+-\w+-\w+-\w+' check:output=~SERVICEGROUP=$$SN +# SERVICEGROUP above is a copy of the noderes.servicenode value this case wrote, so it says +# what xCAT set and not which server sent the boot payload. Both dhcpd instances answer for +# $$CN, so the management node can win the xNBA exchange and serve it. The httpd access logs +# are what separates the two topologies. +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/check_provisioning_source.sh $$CN $$SN +check:rc==0 +check:output=~provisioning source ok cmd:xdsh $$CN "cat /var/log/xcat/xcat.log" cmd:rootimgdir=`lsdef -t osimage __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute|grep rootimgdir|awk -F'=' '{print $2}'`; if [ -d $rootimgdir.regbak ]; then rm -rf $rootimgdir; mv $rootimgdir.regbak $rootimgdir; fi check:rc==0 diff --git a/xCAT-test/autotest/testcase/installation/stage_sn_apt_repo.sh b/xCAT-test/autotest/testcase/installation/stage_sn_apt_repo.sh new file mode 100755 index 000000000..55f49efe4 --- /dev/null +++ b/xCAT-test/autotest/testcase/installation/stage_sn_apt_repo.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# +# stage_sn_apt_repo.sh [] +# +# Write a flat apt index at , so the otherpkgs postscript on a Debian +# service node can install from it. +# +# The postscript builds one source line per directory named in the osimage otherpkglist, in the +# form "deb :/// ./". That trailing "./" is apt's flat layout: it +# reads /Packages and takes each Filename relative to it. A reprepro tree with +# dists/ and pool/ has no such file, and neither has a directory of loose debs. +# +# selects one subdirectory when the tree holds one per release, which is how +# xcat-dep stages its debs. Without it, or with no subdirectory whose name starts the os +# version, the whole tree is indexed. +# +# Run this on the management node. The EL cases call createrepo here, which no Debian host has. + +set -u + +DIR="${1:-}" +OSVER="${2:-}" + +if [ -z "$DIR" ]; then + echo "usage: $0 []" >&2 + exit 2 +fi +if [ ! -d "$DIR" ]; then + echo "apt index error: $DIR is not a directory" >&2 + exit 1 +fi + +scan=. +for sub in "$DIR"/*/; do + [ -d "$sub" ] || continue + name=$(basename "$sub") + case "$OSVER" in + "$name"*) scan=$name ;; + esac +done + +cd "$DIR" || exit 1 +err=$(mktemp) || exit 1 +if ! dpkg-scanpackages -m "$scan" > Packages.new 2>"$err"; then + echo "apt index error: dpkg-scanpackages failed under $DIR/$scan" >&2 + cat "$err" >&2 + rm -f Packages.new "$err" + exit 1 +fi +rm -f "$err" + +# An empty index is the failure this guard exists for: apt reports no candidate, and the +# service node install then fails somewhere else entirely. +count=$(grep -c '^Package: ' Packages.new || true) +if [ "$count" -eq 0 ]; then + echo "apt index error: no deb package under $DIR/$scan" >&2 + rm -f Packages.new + exit 1 +fi + +mv -f Packages.new Packages +gzip -9cf Packages > Packages.gz +echo "apt index ok: $count package(s) indexed at $DIR from $scan" diff --git a/xCAT-test/bats/check_provisioning_source.bats b/xCAT-test/bats/check_provisioning_source.bats new file mode 100644 index 000000000..c3d4ab94e --- /dev/null +++ b/xCAT-test/bats/check_provisioning_source.bats @@ -0,0 +1,291 @@ +#!/usr/bin/env bats +# +# Drive check_provisioning_source.sh, which is what tells a hierarchical provision from a flat +# one. The management node, the service node and the compute node share one subnet and both +# dhcpd instances answer for the compute node, so the management node can win the xNBA exchange +# and serve the boot payload itself. The httpd access logs are the only record of that. +# +# The check takes a baseline of both logs before provisioning and reads only what came after, so +# these tests write the old lines, take the baseline, then append the lines of the run. +# +# lsdef, xdsh and hostname are stubbed. XCAT_HTTPD_ACCESS_LOG is the management node's log. + +load 'helpers/shell_source' + +CN=cn01 +SN=sn01 +CN_IP=192.0.2.10 + +setup() +{ + SCRIPT="$(require_repo_file 'xCAT-test/autotest/testcase/commoncmd/check_provisioning_source.sh')" + BIN="${BATS_TEST_TMPDIR}/bin" + MN_LOG="${BATS_TEST_TMPDIR}/mn-access_log" + SN_LOG="${BATS_TEST_TMPDIR}/sn-access_log" + STATE="${BATS_TEST_TMPDIR}/baseline" + mkdir -p "$BIN" + : >"$MN_LOG" + : >"$SN_LOG" + + printf '#!/bin/sh\nprintf "Object name: %s\\n ip=%s\\n" "$3" "%s"\n' "%s" "%s" "$CN_IP" >"$BIN/lsdef" + printf '#!/bin/sh\necho mn01\n' >"$BIN/hostname" + # A test that reaches the network measures the lab, not this script. + printf '#!/bin/sh\necho "unexpected getent $*" >&2\nexit 1\n' >"$BIN/getent" + # xdsh -e copies the script to the service node and runs it there. Run it here instead, with + # the service node log in place of the management node one, and prefix the node name as xdsh + # does. + printf '#!/bin/sh\nnode=$1\nshift\n[ "$1" = "-e" ] && shift\nscript=$1\nshift\nXCAT_HTTPD_ACCESS_LOG=%s "$script" "$@" | sed "s/^/$node: /"\n' \ + "$SN_LOG" >"$BIN/xdsh" + chmod 0755 "$BIN"/* + + export PATH="$BIN:$PATH" +} + +# One access-log line in the combined format: $1 client, $2 bytes, $3 path. +access_line() +{ + printf '%s - - [01/Jan/2026:00:00:00 +0000] "GET %s HTTP/1.1" 200 %s "-" "iPXE"\n' "$1" "$3" "$2" +} + +take_baseline() +{ + env XCAT_HTTPD_ACCESS_LOG="$MN_LOG" XCAT_PROV_SOURCE_STATE="$STATE" \ + "$SCRIPT" --baseline "$CN" "$SN" >/dev/null +} + +run_check() +{ + run env XCAT_HTTPD_ACCESS_LOG="$MN_LOG" XCAT_PROV_SOURCE_STATE="$STATE" "$SCRIPT" "$CN" "$SN" +} + +# A log that has to hold something at baseline time, so the baseline is not trivially zero. +seed_logs() +{ + access_line 192.0.2.30 512 /install/rh/x86_64/ >"$SN_LOG" + access_line 192.0.2.31 512 /install/rh/x86_64/ >"$MN_LOG" +} + +@test "a service node that served the compute node and a silent management node pass" { + seed_logs + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "a management node request from BEFORE the baseline does not fail this run" { + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$MN_LOG" + access_line 192.0.2.30 512 /install/rh/x86_64/ >"$SN_LOG" + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "a service node request from BEFORE the baseline does not satisfy the check" { + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" + access_line 192.0.2.31 512 /install/rh/x86_64/ >"$MN_LOG" + take_baseline + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"no boot payload"* ]] +} + +@test "a request that carries no boot payload does not satisfy the check" { + seed_logs + take_baseline + printf '%s - - [01/Jan/2026:00:00:00 +0000] "GET /favicon.ico HTTP/1.1" 404 209 "-" "iPXE"\n' \ + "$CN_IP" >>"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"no boot payload"* ]] +} + +@test "a management node request that carries no boot payload does not read as a flat provision" { + seed_logs + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + printf '%s - - [01/Jan/2026:00:00:00 +0000] "GET /favicon.ico HTTP/1.1" 404 209 "-" "iPXE"\n' \ + "$CN_IP" >>"$MN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "the check refuses to answer with no baseline" { + seed_logs + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"no baseline"* ]] + [[ "$output" != *"provisioning source ok"* ]] +} + +@test "a log rotated below its baseline is read from its first line" { + for i in 1 2 3 4 5; do access_line 192.0.2.30 512 /install/rh/x86_64/; done >"$SN_LOG" + access_line 192.0.2.31 512 /install/rh/x86_64/ >"$MN_LOG" + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "the management node answering for the compute node fails the check" { + seed_logs + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + { + access_line 192.0.2.21 4096 /install/rh/x86_64/ + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel + } >>"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"this provision was flat"* ]] + [[ "$output" != *"provisioning source ok"* ]] +} + +@test "a management node answering only a bodyless request still fails the check" { + seed_logs + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + { + access_line 192.0.2.21 4096 /install/rh/x86_64/ + printf '%s - - [01/Jan/2026:00:00:00 +0000] "HEAD %s HTTP/1.1" 304 - "-" "iPXE"\n' \ + "$CN_IP" /tftpboot/xcat/genesis.kernel + } >>"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"this provision was flat"* ]] +} + +@test "a service node that served the compute node nothing fails the check" { + seed_logs + take_baseline + access_line 192.0.2.22 4096 /install/rh/x86_64/ >>"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"no boot payload"* ]] +} + +@test "an unreadable service node log fails the check instead of passing it" { + seed_logs + take_baseline + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + printf '#!/bin/sh\nexit 1\n' >"$BIN/xdsh" + chmod 0755 "$BIN/xdsh" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"no httpd access log could be read on $SN"* ]] +} + +# The management node provisions the service node BEFORE this baseline, so it can serve nothing +# after it. That is the hierarchical result, and it was read as a broken log. +@test "a management node that serves nothing after the baseline passes" { + seed_logs + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "a management node log with no entry at all fails the check instead of reading as silence" { + : >"$MN_LOG" + access_line 192.0.2.30 512 /install/rh/x86_64/ >"$SN_LOG" + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"no httpd access log with entries could be read on mn01"* ]] +} + +# The compute node fetches the root image with wget from a URL that carries a double slash, so +# the path in the log reads //install/... and the payload filter did not match it. +@test "a boot payload requested under a doubled slash counts" { + seed_logs + take_baseline + access_line "$CN_IP" 978729607 //install/netboot/ubuntu/x86_64/compute/rootimg.cpio.gz >>"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "a doubled-slash boot payload from the management node reads as a flat provision" { + seed_logs + take_baseline + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + { + access_line 192.0.2.21 4096 /install/rh/x86_64/ + access_line "$CN_IP" 978729607 //install/netboot/ubuntu/x86_64/compute/rootimg.cpio.gz + } >>"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"this provision was flat"* ]] +} + +@test "the Debian per-vhost log format is read as the client address" { + seed_logs + take_baseline + printf 'xcat:80 %s - - [01/Jan/2026:00:00:00 +0000] "GET %s HTTP/1.1" 200 12345678\n' \ + "$CN_IP" /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + printf 'xcat:80 %s - - [01/Jan/2026:00:00:00 +0000] "GET %s HTTP/1.1" 200 4096\n' \ + 192.0.2.21 /install/ubuntu/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -eq 0 ] + [[ "$output" == *"provisioning source ok"* ]] +} + +@test "a compute node with no address fails the check" { + seed_logs + take_baseline + printf '#!/bin/sh\nexit 1\n' >"$BIN/lsdef" + printf '#!/bin/sh\nexit 2\n' >"$BIN/getent" + chmod 0755 "$BIN/lsdef" "$BIN/getent" + access_line "$CN_IP" 12345678 /tftpboot/xcat/genesis.kernel >>"$SN_LOG" + access_line 192.0.2.21 4096 /install/rh/x86_64/ >>"$MN_LOG" + + run_check + [ "$status" -ne 0 ] + [[ "$output" == *"has no address"* ]] +} + +@test "a baseline that cannot reach the service node names the service node" { + seed_logs + printf '#!/bin/sh\nexit 1\n' >"$BIN/xdsh" + chmod 0755 "$BIN/xdsh" + + run env XCAT_HTTPD_ACCESS_LOG="$MN_LOG" XCAT_PROV_SOURCE_STATE="$STATE" \ + "$SCRIPT" --baseline "$CN" "$SN" + [ "$status" -ne 0 ] + [[ "$output" == *"no access log to baseline on $SN"* ]] + # The message used to append the management node's baseline spec after the node name, so a + # substring match on the name alone passes either way. + [[ "$output" != *"$MN_LOG"* ]] + [ ! -e "$STATE" ] +} diff --git a/xCAT-test/bats/makenamed_forwarders.bats b/xCAT-test/bats/makenamed_forwarders.bats new file mode 100644 index 000000000..0bda68e04 --- /dev/null +++ b/xCAT-test/bats/makenamed_forwarders.bats @@ -0,0 +1,107 @@ +#!/usr/bin/env bats +# +# makenamed.conf builds the forwarder list of a service node's named from /etc/resolv.conf. +# +# On a host managed by systemd-resolved that file holds the 127.0.0.53 stub, and the stub points +# back at this named for the link. forward-only to it is a loop: the service node answers +# nothing, and every compute node behind it fails to resolve. Measured on xcat22-sn, where +# "dig @ xcat22-sn.xcat22.lab" returned nothing while the same query to the management node +# answered. The EL service node has the real upstream in /etc/resolv.conf, which is why it works. +# +# systemd-resolved writes the real servers to /run/systemd/resolve/resolv.conf. + +load 'helpers/shell_source' + +setup() +{ + SCRIPT="$(require_repo_file 'xCAT-server/sbin/makenamed.conf')" + RESOLV="${BATS_TEST_TMPDIR}/resolv.conf" + UPLINK="${BATS_TEST_TMPDIR}/uplink.conf" + # MAKENAMED_LIB stops the script before it writes anything, so only the routine is loaded. + MAKENAMED_LIB=1 . "$SCRIPT" + export -f forwarder_addresses _fa_usable +} + +@test "a real nameserver is a forwarder" { + printf 'nameserver 192.168.222.1\nsearch xcat22.lab\n' >"$RESOLV" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$status" -eq 0 ] + [ "$output" = "192.168.222.1" ] +} + +@test "the systemd-resolved stub is not a forwarder, and the uplink answers instead" { + printf 'nameserver 127.0.0.53\noptions edns0 trust-ad\n' >"$RESOLV" + printf 'nameserver 192.168.222.1\nsearch xcat22.lab\n' >"$UPLINK" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$status" -eq 0 ] + [ "$output" = "192.168.222.1" ] +} + +@test "any loopback address is rejected, not only the stub" { + printf 'nameserver 127.0.0.1\nnameserver ::1\n' >"$RESOLV" + printf 'nameserver 10.0.0.1\n' >"$UPLINK" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$output" = "10.0.0.1" ] +} + +@test "several real nameservers are all forwarders, in order" { + printf 'nameserver 192.168.222.1\nnameserver 10.0.0.2\n' >"$RESOLV" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$output" = "192.168.222.1 +10.0.0.2" ] +} + +@test "a real nameserver wins over the uplink, which is only the fallback" { + printf 'nameserver 192.168.222.1\n' >"$RESOLV" + printf 'nameserver 10.9.9.9\n' >"$UPLINK" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$output" = "192.168.222.1" ] +} + +@test "no usable address anywhere prints nothing rather than a blank forwarder" { + printf 'nameserver 127.0.0.53\n' >"$RESOLV" + : >"$UPLINK" + # Count the lines. A blank line reaches named.conf as an empty forwarder entry, which bind + # rejects as a syntax error, and $output cannot tell one from no output at all. + run bash -c "forwarder_addresses '$RESOLV' '$UPLINK' | wc -l" + [ "$status" -eq 0 ] + [ "$output" = "0" ] +} + +@test "an absent uplink file is not an error" { + printf 'nameserver 127.0.0.53\n' >"$RESOLV" + run forwarder_addresses "$RESOLV" "${BATS_TEST_TMPDIR}/absent" + [ "$status" -eq 0 ] + [ "$output" = "" ] +} + +# named runs as "bind" on Debian and writes its managed-keys database into the configured +# directory. /var/named is created root-owned, so the write fails, DNSSEC initialisation fails +# with it, and the server answers NXDOMAIN to every query -- including forwarded ones, which is +# how a correct forwarder list still resolved nothing on xcat22-sn. + +@test "Ubuntu names the directory its named can write" { + printf 'DISTRIB_ID=Ubuntu\nDISTRIB_RELEASE=24.04\n' >"${BATS_TEST_TMPDIR}/lsb" + run named_directory "${BATS_TEST_TMPDIR}/lsb" "${BATS_TEST_TMPDIR}/absent-os" "${BATS_TEST_TMPDIR}/absent-suse" + [ "$output" = "/var/cache/bind" ] +} + +@test "SLES keeps its own directory" { + : >"${BATS_TEST_TMPDIR}/lsb" + printf 'ID="sles"\n' >"${BATS_TEST_TMPDIR}/os" + run named_directory "${BATS_TEST_TMPDIR}/lsb" "${BATS_TEST_TMPDIR}/os" "${BATS_TEST_TMPDIR}/absent-suse" + [ "$output" = "/var/lib/named" ] +} + +@test "EL keeps /var/named" { + : >"${BATS_TEST_TMPDIR}/lsb" + printf 'ID="almalinux"\n' >"${BATS_TEST_TMPDIR}/os" + run named_directory "${BATS_TEST_TMPDIR}/lsb" "${BATS_TEST_TMPDIR}/os" "${BATS_TEST_TMPDIR}/absent-suse" + [ "$output" = "/var/named" ] +} + +@test "an absent lsb-release is not Ubuntu" { + printf 'ID="almalinux"\n' >"${BATS_TEST_TMPDIR}/os" + run named_directory "${BATS_TEST_TMPDIR}/absent-lsb" "${BATS_TEST_TMPDIR}/os" "${BATS_TEST_TMPDIR}/absent-suse" + [ "$output" = "/var/named" ] +} diff --git a/xCAT-test/bats/otherpkgs_apt_source_trusted.bats b/xCAT-test/bats/otherpkgs_apt_source_trusted.bats new file mode 100644 index 000000000..7217d122c --- /dev/null +++ b/xCAT-test/bats/otherpkgs_apt_source_trusted.bats @@ -0,0 +1,59 @@ +#!/usr/bin/env bats +# +# The otherpkgs postscript stages an apt repository that carries a Packages file and no Release. +# apt refuses an unsigned repository outright -- "does not have a Release file" -- so the +# postscript's apt-cache show finds nothing, it deletes the source line it has just written, and +# the package is never installed. On a service node that package is xcatsn. +# +# This drives apt against a real flat repository in a scratch tree, with both spellings of the +# source line, and then holds the postscript to the one apt accepts. + +load 'helpers/shell_source' + +setup() +{ + command -v apt-get >/dev/null 2>&1 || skip 'apt-get is required' + command -v dpkg-scanpackages >/dev/null 2>&1 || skip 'dpkg-scanpackages is required' + + REPO="${BATS_TEST_TMPDIR}/repo" + ROOT="${BATS_TEST_TMPDIR}/aptroot" + mkdir -p "$REPO" "$ROOT/etc/apt/sources.list.d" "$ROOT/var/lib/apt/lists/partial" \ + "$ROOT/var/lib/dpkg" "$ROOT/var/cache/apt/archives/partial" + : >"$ROOT/var/lib/dpkg/status" + + # A minimal binary package, so the index describes something real. + local build="${BATS_TEST_TMPDIR}/pkg/xcatsn" + mkdir -p "$build/DEBIAN" + printf 'Package: xcatsn\nVersion: 2.20.0\nArchitecture: all\nMaintainer: t \nDescription: probe\n' \ + >"$build/DEBIAN/control" + dpkg-deb --build -Znone "$build" "$REPO/xcatsn_2.20.0_all.deb" >/dev/null + ( cd "$REPO" && dpkg-scanpackages -m . >Packages 2>/dev/null ) +} + +# Resolve xcatsn through apt with the given source line. Sets OUT and STATUS. +resolve() +{ + printf '%s\n' "$1" >"$ROOT/etc/apt/sources.list.d/probe.list" + apt-get -o "Dir=$ROOT" -o "Dir::State::status=$ROOT/var/lib/dpkg/status" \ + -o "Dir::Etc::sourcelist=$ROOT/etc/apt/sources.list.d/probe.list" \ + -o Dir::Etc::sourceparts=/dev/null -o APT::Get::List-Cleanup=0 \ + update >/dev/null 2>&1 || true # an untrusted source makes update itself exit 100, + # and that refusal is what this test measures. Under + # bats' set -e an unguarded failure here aborts the + # function before apt-cache runs, so the test could + # only ever pass where apt-get is absent and it skips. + OUT="$(apt-cache -o "Dir=$ROOT" -o "Dir::State::status=$ROOT/var/lib/dpkg/status" \ + show xcatsn 2>&1)" && STATUS=0 || STATUS=$? +} + +@test "apt refuses the source line without trusted=yes, so the package cannot be found" { + resolve "deb file://$REPO ./" + [ "$STATUS" -ne 0 ] + [[ "$OUT" != *"Package: xcatsn"* ]] +} + +@test "apt resolves the package when the source line is trusted" { + resolve "deb [trusted=yes] file://$REPO ./" + [ "$STATUS" -eq 0 ] + [[ "$OUT" == *"Package: xcatsn"* ]] +} diff --git a/xCAT-test/bats/stage_sn_apt_repo.bats b/xCAT-test/bats/stage_sn_apt_repo.bats new file mode 100644 index 000000000..2ba4d3992 --- /dev/null +++ b/xCAT-test/bats/stage_sn_apt_repo.bats @@ -0,0 +1,107 @@ +#!/usr/bin/env bats +# +# Drive stage_sn_apt_repo.sh, which writes the flat apt index a Debian service node installs +# from. The otherpkgs postscript writes "deb :/// ./", and that +# trailing "./" makes apt read /Packages and resolve each Filename against it. +# +# dpkg-scanpackages is stubbed, so the test measures the directory selection and the guards and +# not dpkg-dev. + +load 'helpers/shell_source' + +setup() +{ + SCRIPT="$(require_repo_file 'xCAT-test/autotest/testcase/installation/stage_sn_apt_repo.sh')" + REPO="${BATS_TEST_TMPDIR}/repo" + BIN="${BATS_TEST_TMPDIR}/bin" + RECORD="${BATS_TEST_TMPDIR}/scanned" + mkdir -p "$REPO" "$BIN" + + # Record the directory it was asked to scan, and emit one stanza per deb found under it. + cat >"$BIN/dpkg-scanpackages" <>"$RECORD" +find "\$1" -name '*.deb' | while read -r d; do + printf 'Package: %s\nFilename: %s\n\n' "\$(basename "\$d" .deb)" "\$d" +done +exit 0 +STUB + chmod 0755 "$BIN/dpkg-scanpackages" + export PATH="$BIN:$PATH" +} + +deb() +{ + mkdir -p "$(dirname "$REPO/$1")" + : >"$REPO/$1" +} + +scanned() +{ + read_file_or_empty "$RECORD" +} + +@test "a reprepro tree is indexed whole, so the pool is reachable from the flat index" { + deb pool/main/x/xcat/xcatsn_2.20.0_amd64.deb + deb pool/main/x/xcat/xcat-client_2.20.0_all.deb + + run "$SCRIPT" "$REPO" + [ "$status" -eq 0 ] + [ "$(scanned)" = "." ] + [[ "$output" == *"2 package(s) indexed"* ]] + grep -q '^Package: xcatsn_2.20.0_amd64$' "$REPO/Packages" + [ -s "$REPO/Packages.gz" ] +} + +@test "a tree with one directory per release is indexed from the directory the os version names" { + deb ubuntu22.04/conserver-xcat_8.2.1-1_amd64.deb + deb ubuntu24.04/conserver-xcat_8.2.1-1_amd64.deb + deb ubuntu26.04/conserver-xcat_8.2.1-1_amd64.deb + + run "$SCRIPT" "$REPO" ubuntu24.04.4 + [ "$status" -eq 0 ] + [ "$(scanned)" = "ubuntu24.04" ] + [[ "$output" == *"1 package(s) indexed"* ]] +} + +@test "an os version no directory matches falls back to the whole tree" { + deb ubuntu22.04/conserver-xcat_8.2.1-1_amd64.deb + deb ubuntu26.04/conserver-xcat_8.2.1-1_amd64.deb + + run "$SCRIPT" "$REPO" ubuntu24.04.4 + [ "$status" -eq 0 ] + [ "$(scanned)" = "." ] +} + +@test "a tree with no deb fails instead of writing an empty index" { + mkdir -p "$REPO/pool" + + run "$SCRIPT" "$REPO" + [ "$status" -ne 0 ] + [[ "$output" == *"no deb package under"* ]] + [ ! -e "$REPO/Packages" ] +} + +@test "a failing dpkg-scanpackages fails the step and leaves no index" { + deb pool/main/x/xcat/xcatsn_2.20.0_amd64.deb + printf '#!/bin/sh\necho "cannot read" >&2\nexit 2\n' >"$BIN/dpkg-scanpackages" + chmod 0755 "$BIN/dpkg-scanpackages" + + run "$SCRIPT" "$REPO" + [ "$status" -ne 0 ] + [[ "$output" == *"dpkg-scanpackages failed"* ]] + [ ! -e "$REPO/Packages" ] +} + +@test "a directory that does not exist fails" { + run "$SCRIPT" "${BATS_TEST_TMPDIR}/absent" + [ "$status" -ne 0 ] + [[ "$output" == *"is not a directory"* ]] +} + +@test "no argument fails" { + run "$SCRIPT" + [ "$status" -ne 0 ] + [[ "$output" == *"usage:"* ]] +} diff --git a/xCAT-test/quick-servicenode-ub.txt b/xCAT-test/quick-servicenode-ub.txt new file mode 100644 index 000000000..bd603b5c4 --- /dev/null +++ b/xCAT-test/quick-servicenode-ub.txt @@ -0,0 +1,21 @@ +# The fast oracle of the Ubuntu 24.04 service node cell. +# +# xCAT-test/quick.sh -f xCAT-test/quick-servicenode-ub.txt +# +# RUN IT ON xcat-master-ub. otherpkgs_apt_source_trusted skips its two resolving assertions where +# apt-get is absent, so on an EL host this list silently proves less and the trusted=yes fix +# reads as unnecessary. +# +# KNOWN HOLES, where a wrong drop comes from. A first run dropped four real fixes: +# - the apt source line the postscript writes. otherpkgs_apt_source_trusted measures APT, not +# what xCAT wrote. STILL OPEN. +# - the NFS export casper needs. CLOSED by service_node_artifacts_ubuntu.t. +# - the xdsh PATH fix. STILL OPEN. +# - the named reload. CLOSED by aasn_named_service_action.t. + +xCAT-test/unit/service_node_artifacts_ubuntu.t +xCAT-test/unit/aasn_named_service_action.t +xCAT-test/unit/ubuntu_service_subiquity_template.t +xCAT-test/bats/makenamed_forwarders.bats +xCAT-test/bats/otherpkgs_apt_source_trusted.bats +xCAT-test/bats/stage_sn_apt_repo.bats diff --git a/xCAT-test/quick-servicenode.txt b/xCAT-test/quick-servicenode.txt new file mode 100644 index 000000000..0311a82f2 --- /dev/null +++ b/xCAT-test/quick-servicenode.txt @@ -0,0 +1,32 @@ +# The fast oracle of the service node change set: one test per fix, each written with its fix. +# +# Run it with: xCAT-test/quick.sh -f xCAT-test/quick-servicenode.txt +# +# These are the tests that failed before their fix landed. Together they stand in for the +# end-to-end service node suite, which provisions a management node, a service node and a +# compute node on two families and takes 45 to 90 minutes a run. +# +# What each one covers: +# check_provisioning_source the compute node was served by the service node, not the MN +# makenamed_forwarders a Debian service node does not forward DNS to its own stub +# otherpkgs_apt_source_trusted the staged apt source apt will accept +# stage_sn_apt_repo the Debian repository index, where createrepo does not exist +# servicenode_builder_repo the builder repository per EL release: crb, powertools, codeready +# svrutils_nfs_export_line a service node that mounts /install still exports it +# ubuntu_service_subiquity_template the service profile resolves a Subiquity template +# +# A SKIP is not a PASS. otherpkgs_apt_source_trusted skips its two resolving assertions where +# apt-get is absent, so on an EL builder that file proves less than it does on a Debian one. Run +# this list on the builder of the family the candidate touches, or read the skips. +# +# NOT covered, and known: the xdsh PATH fix, and the named working directory and reload fixes. +# A candidate that removes those passes this list and fails the end-to-end suite. Fill these in +# before trusting the list for a minimization. + +xCAT-test/bats/check_provisioning_source.bats +xCAT-test/bats/makenamed_forwarders.bats +xCAT-test/bats/otherpkgs_apt_source_trusted.bats +xCAT-test/bats/stage_sn_apt_repo.bats +xCAT-test/unit/servicenode_builder_repo.t +xCAT-test/unit/svrutils_nfs_export_line.t +xCAT-test/unit/ubuntu_service_subiquity_template.t diff --git a/xCAT-test/quick.sh b/xCAT-test/quick.sh new file mode 100755 index 000000000..e061de80d --- /dev/null +++ b/xCAT-test/quick.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# +# Run a fast subset of the unit tests: the Perl .t files with prove and the shell .bats files +# with bats, in one command, from the source tree. No build, no installed xCAT, no cluster. +# +# WHY THIS EXISTS. Delta debugging a change set runs the suite once per candidate. The +# end-to-end suite provisions machines and costs 45 to 90 minutes a run, so the rounds run on a +# fast oracle instead, and the slow suite runs once on the set that settles. This is that fast +# oracle, and it is useful on its own: it is the same two commands CI runs, without the wait. +# +# xCAT-test/quick.sh every unit test and every bats file +# xCAT-test/quick.sh path/a.t path/b.bats only those, in the order given +# xCAT-test/quick.sh -f list.txt the files named in list.txt, one per line +# +# Exit status is 0 only when every file passed. A missing file is an error, not a skip: a fast +# oracle that silently runs nothing reports PASS for a tree that contains none of the tests. + +set -u -o pipefail + +usage() { sed -n '3,20p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; } + +root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +files=() + +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) usage 0 ;; + -f|--from) shift; [ -r "${1:-}" ] || { echo "quick: cannot read list '${1:-}'" >&2; exit 2; } + while read -r line; do + case "$line" in ''|\#*) continue ;; esac + files+=("$line") + done < "$1" ;; + -*) echo "quick: unknown option '$1'" >&2; usage 2 ;; + *) files+=("$1") ;; + esac + shift +done + +cd "$root" || exit 2 + +if [ "${#files[@]}" -eq 0 ]; then + echo "[quick] whole fast suite: prove -r xCAT-test/unit, then bats -r xCAT-test/bats" + rc=0 + prove -r xCAT-test/unit || rc=1 + bats -r xCAT-test/bats || rc=1 + exit $rc +fi + +# A named file that is absent means the caller asked for a test this tree does not have. Say so +# and fail: reporting PASS here is how a minimization drops the commit that adds a test. +missing=() +for f in "${files[@]}"; do [ -e "$f" ] || missing+=("$f"); done +if [ "${#missing[@]}" -gt 0 ]; then + printf '[quick] MISSING from this tree, refusing to report a result:\n' >&2 + printf ' %s\n' "${missing[@]}" >&2 + exit 2 +fi + +perl_tests=(); bats_tests=() +for f in "${files[@]}"; do + case "$f" in + *.t) perl_tests+=("$f") ;; + *.bats) bats_tests+=("$f") ;; + *) echo "quick: '$f' is neither a .t nor a .bats" >&2; exit 2 ;; + esac +done + +rc=0 +if [ "${#perl_tests[@]}" -gt 0 ]; then + echo "[quick] prove ${#perl_tests[@]} Perl test(s)" + prove "${perl_tests[@]}" || rc=1 +fi +if [ "${#bats_tests[@]}" -gt 0 ]; then + echo "[quick] bats ${#bats_tests[@]} shell test(s)" + bats "${bats_tests[@]}" || rc=1 +fi + +[ $rc -eq 0 ] && echo "[quick] PASS (${#files[@]} file(s))" || echo "[quick] FAIL" +exit $rc diff --git a/xCAT-test/unit/aasn_named_service_action.t b/xCAT-test/unit/aasn_named_service_action.t new file mode 100644 index 000000000..2082b557d --- /dev/null +++ b/xCAT-test/unit/aasn_named_service_action.t @@ -0,0 +1,33 @@ +#!/usr/bin/env perl + +# setup_DNS writes named.conf through makenamed.conf and then brings the daemon up. It used to +# START the service. On Debian the package already runs named, so a start is a no-op: the daemon +# keeps serving the configuration it read at install time, the zone xCAT has just written is +# never loaded, and a compute node cannot resolve its service node. +# +# A minimization of this change set dropped that fix, because no fast test could see it. The +# decision lives in xCAT::SvrUtils, which a test can load; AAsn.pm cannot be loaded from a source +# tree at all, since it pulls in xCAT::Table and the rest of the server. + +use strict; +use warnings; + +use FindBin; +use Test::More; + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use xCAT::SvrUtils; + +can_ok('xCAT::SvrUtils', 'named_service_action') or do { done_testing(); exit 1 }; + +is(xCAT::SvrUtils::named_service_action('linux'), 'restart', + 'Linux restarts named, so the configuration just written is the one it serves'); +is(xCAT::SvrUtils::named_service_action('aix'), 'start', + 'AIX keeps the start it has always used'); +is(xCAT::SvrUtils::named_service_action(''), '', + 'an unknown platform does nothing rather than guessing an action'); +is(xCAT::SvrUtils::named_service_action(undef), '', + 'an undefined platform does nothing'); + +done_testing(); diff --git a/xCAT-test/unit/service_node_artifacts_ubuntu.t b/xCAT-test/unit/service_node_artifacts_ubuntu.t new file mode 100644 index 000000000..cfbad16ae --- /dev/null +++ b/xCAT-test/unit/service_node_artifacts_ubuntu.t @@ -0,0 +1,57 @@ +#!/usr/bin/env perl + +# The Ubuntu counterpart of service_node_artifacts_el.t: render the artifacts an Ubuntu service +# node needs and assert each, so the fast oracle fails for any wrong artifact rather than only +# for the defects someone has already hit. +# +# It exists because the first Ubuntu oracle dropped two real fixes. The apt test measured apt's +# behaviour rather than the line the postscript writes, and nothing asserted the export line at +# all, although casper mounts the install tree over NFS from the service node. + +use strict; +use warnings; + +use FindBin; +use Test::More; + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; + +# get_file_name takes genos LAST, and update_tables_with_templates passes 'subiquity' for every +# Ubuntu 20.04 and later osimage. Passing the os version there resolves the preseed instead, which +# is the mistake this test would otherwise make about its own subject. +my %SN = (osver => 'ubuntu24.04', arch => 'x86_64', profile => 'service', genos => 'subiquity'); +my $SHARE = "$FindBin::Bin/../../xCAT-server/share/xcat/install"; + +my $have = eval { require xCAT::SvrUtils; 1 } ? 1 : 0; +ok($have, 'xCAT::SvrUtils loads') or do { done_testing(); exit 1 }; + +# 1. The installer. Ubuntu 20.04 and later install with Subiquity, and xCAT decides that from the +# NAME of the template it resolved. Without a service.subiquity.tmpl the service profile falls +# back to the debian-installer preseed and the node boots the live image without casper. +my $tmpl = xCAT::SvrUtils::get_tmpl_file_name("$SHARE/ubuntu", $SN{profile}, $SN{osver}, + $SN{arch}, $SN{genos}); +ok(defined $tmpl && length $tmpl, 'the Ubuntu service profile resolves a template'); +like($tmpl, qr{subiquity}, '... and it is a Subiquity template, or the node never installs'); +like($tmpl, qr{/service[^/]*\.tmpl$}, '... of the service profile, not compute'); + +# The compute profile must keep resolving too: an assertion that only ever looks at one profile +# cannot tell a profile fix from a lookup fix. +my $compute = xCAT::SvrUtils::get_tmpl_file_name("$SHARE/ubuntu", 'compute', $SN{osver}, + $SN{arch}, $SN{genos}); +like($compute, qr{subiquity}, 'the compute profile still resolves its own Subiquity template'); + +# 2. The NFS export. casper mounts the install tree from the service node, so a service node that +# exports nothing stops the compute node at "Unable to find a live file system on the network". +my $can_export = xCAT::SvrUtils->can('nfs_export_line') ? 1 : 0; +ok($can_export, 'SvrUtils can render an export line, without which casper finds no filesystem'); + +SKIP: { + skip 'nfs_export_line absent', 3 unless $can_export; + my $re = xCAT::SvrUtils->nfs_export_line('/install', reexport => 1); + like($re, qr{\bfsid=\d+}, 're-exporting the mounted /install carries an fsid'); + like($re, qr{\bcrossmnt\b}, '... and crossmnt'); + like($re, qr{\bno_root_squash\b}, '... and no_root_squash, which the installer needs'); +} + +done_testing(); diff --git a/xCAT-test/unit/svrutils_nfs_export_line.t b/xCAT-test/unit/svrutils_nfs_export_line.t new file mode 100644 index 000000000..3adbfcbfc --- /dev/null +++ b/xCAT-test/unit/svrutils_nfs_export_line.t @@ -0,0 +1,52 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +# A service node with servicenode.nfsserver=1 is asked to "set up file services on this service +# node". When site.installloc is set it MOUNTS /install from the management node, and AAsn.pm +# then exported nothing at all, so the node served no NFS. An Ubuntu compute node boots the live +# installer with casper, which mounts the install tree over NFS from its own service node, and it +# stopped at "Unable to find a live file system on the network". Measured on xcat22-sn: +# showmount -e returned an empty list and /etc/exports was empty while nfs-kernel-server was +# active. +# +# Re-exporting an NFS mount is not the same as exporting a local directory. Linux requires an +# explicit fsid, because it cannot derive one from the underlying filesystem, and refuses the +# export without it. + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use xCAT::SvrUtils; + +can_ok('xCAT::SvrUtils', 'nfs_export_line') or done_testing() && exit; + +my $local = xCAT::SvrUtils->nfs_export_line('/install'); +is($local, '/install *(rw,no_root_squash,sync,no_subtree_check,insecure)', + 'a local directory keeps the options xCAT has always written'); + +my $reexport = xCAT::SvrUtils->nfs_export_line('/install', reexport => 1); +like($reexport, qr{^/install \*\(}, 're-export names the same directory and clients'); +like($reexport, qr{\brw\b}, '... stays read-write'); +like($reexport, qr{\bno_root_squash\b}, '... keeps no_root_squash, which the installer needs'); +like($reexport, qr{\binsecure\b}, '... keeps insecure, for clients on high ports'); +like($reexport, qr{\bfsid=\d+}, '... carries an fsid, without which exportfs refuses an NFS mount'); +like($reexport, qr{\bcrossmnt\b}, '... carries crossmnt, so the mount below it is followed'); + +# The fsid must be stable across runs, or every restart hands clients a new filesystem identity +# and their mounts go stale. +is(xCAT::SvrUtils->nfs_export_line('/install', reexport => 1), $reexport, + 'the same directory yields the same fsid every time'); + +my ($install_fsid) = $reexport =~ /fsid=(\d+)/; +my ($tftp_fsid) = xCAT::SvrUtils->nfs_export_line('/tftpboot', reexport => 1) =~ /fsid=(\d+)/; +isnt($tftp_fsid, $install_fsid, 'two directories do not share one fsid'); +cmp_ok($install_fsid, '>', 0, 'the fsid is not 0, which Linux reserves for the export root'); +# The guard that keeps it non-zero is only reachable when the checksum itself is 0, which an +# empty path produces. Drive it directly rather than claim a path that cannot reach it. +cmp_ok(xCAT::SvrUtils::_nfs_export_fsid(''), '>', 0, + 'a path whose checksum is 0 still yields a usable fsid'); + +done_testing(); diff --git a/xCAT-test/unit/ubuntu_service_subiquity_template.t b/xCAT-test/unit/ubuntu_service_subiquity_template.t new file mode 100644 index 000000000..3922a4ba3 --- /dev/null +++ b/xCAT-test/unit/ubuntu_service_subiquity_template.t @@ -0,0 +1,61 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +# xCAT decides that an Ubuntu osimage uses the Subiquity installer from the NAME of the template +# it resolved: xCAT_plugin::debian::using_subiquity matches /subiquity/ in the path. That decision +# controls the netboot kernel command line. A template whose name does not match it makes xCAT +# boot the live-server initrd with the debian-installer preseed arguments, without boot=casper and +# without nfsroot, and the node never finds a live filesystem. +# +# The compute profile has compute.subiquity.tmpl. The service profile had only service.tmpl, so an +# Ubuntu 24.04 service node took the preseed path. + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use xCAT::SvrUtils; + +my $plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/debian.pm"; +plan skip_all => 'debian.pm not found' unless -r $plugin; +eval { require $plugin; 1 } or plan skip_all => "could not load debian.pm: $@"; + +my $share = "$FindBin::Bin/../../xCAT-server/share/xcat/install/ubuntu"; +plan skip_all => "$share not found" unless -d $share; + +# get_file_name takes genos as its last argument. update_tables_with_templates passes 'subiquity' +# for every Ubuntu 20.04 and later osimage. +sub resolved { + my ($profile, $osver, $genos) = @_; + return xCAT::SvrUtils::get_tmpl_file_name($share, $profile, $osver, 'x86_64', $genos); +} + +for my $profile (qw(compute service)) { + my $tmpl = resolved($profile, 'ubuntu24.04', 'subiquity'); + ok(defined $tmpl && -r $tmpl, "$profile: Ubuntu 24.04 resolves an install template"); + ok(xCAT_plugin::debian::using_subiquity('ubuntu24.04', $tmpl), + "$profile: the Ubuntu 24.04 template xCAT resolved is a Subiquity one"); +} + +# Ubuntu 18.04 predates Subiquity and keeps the preseed path. genos stays the os version there. +for my $profile (qw(compute service)) { + my $tmpl = resolved($profile, 'ubuntu18.04', 'ubuntu18.04'); + ok(defined $tmpl && -r $tmpl, "$profile: Ubuntu 18.04 resolves an install template"); + ok(!xCAT_plugin::debian::using_subiquity('ubuntu18.04', $tmpl), + "$profile: Ubuntu 18.04 keeps the debian-installer template"); +} + +# The service template must carry the autoinstall document Subiquity reads. A file named for +# Subiquity that holds a preseed would satisfy using_subiquity and still not install. +my $service = resolved('service', 'ubuntu24.04', 'subiquity'); +SKIP: { + skip 'no service template resolved', 3 unless defined $service && -r $service; + my $text = do { local $/; open my $fh, '<', $service or die "$service: $!"; <$fh> }; + like($text, qr/^#cloud-config/, 'the service template is a cloud-config document'); + like($text, qr/^autoinstall:/m, '... carrying an autoinstall section'); + unlike($text, qr/^d-i /m, '... and no debian-installer preseed directives'); +} + +done_testing(); diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index 98c576259..81fef1a23 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -828,7 +828,9 @@ while [ $op_index -le $OTHERPKGS_INDEX ]; do else type=file fi - echo "deb $type://$whole_path ./" > $REPOFILE + # apt refuses an unsigned repository outright, and this one carries a + # Packages file and no Release. The rpm arm writes gpgcheck=0 for this. + echo "deb [trusted=yes] $type://$whole_path ./" > $REPOFILE fi fi fi