diff --git a/xCAT-test/unit/debian_subiquity_netboot.t b/xCAT-test/unit/debian_subiquity_netboot.t new file mode 100644 index 000000000..5b3b3029c --- /dev/null +++ b/xCAT-test/unit/debian_subiquity_netboot.t @@ -0,0 +1,47 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +# Regression: the Ubuntu (subiquity) diskful install netboots the live installer over NFS. +# The kernel command line built in debian.pm must satisfy TWO casper requirements or the +# installer never boots and the node PXE-loops forever (surfacing to the test as +# "ssh: connect ... port 22: Connection refused"): +# +# 1. boot=casper -- without it casper does not process netboot=nfs at all; it scans the +# local disks, finds nothing, and panics "Unable to find a medium containing a live +# file system", dropping to the initramfs emergency shell. +# +# 2. nfsroot must be a literal IP, NOT a hostname. casper mounts the live filesystem with +# klibc's nfsmount, which does NOT resolve hostnames -- it fails with +# "nfsmount: can't parse IP address ''". (busybox/util-linux `mount -t nfs` +# resolves names, which is why manual mounts work while casper's does not.) The instserver +# must therefore be passed through xCAT::NetworkUtils->getipaddr() before it is put in +# nfsroot=. The ds=...http URL is fetched later by cloud-init in the booted live system, +# where normal DNS works, so only nfsroot needs the IP. + +sub slurp { my ($p) = @_; local $/; open my $fh, '<', $p or return undef; <$fh> } + +my $deb = slurp('xCAT-server/lib/xcat/plugins/debian.pm'); +plan skip_all => 'debian.pm not found' unless defined $deb; + +# The exact netboot fragment is unique to the subiquity install cmdline, so match it directly +# instead of trying to slice one of the several using_subiquity() blocks. +like($deb, qr/\bboot=casper\b/, + 'subiquity netboot kcmdline includes boot=casper (else casper scans local disks and panics)'); +like($deb, qr/autoinstall ip=dhcp boot=casper netboot=nfs nfsroot=\$\{nfsip\}:/, + 'subiquity kcmdline is: autoinstall ip=dhcp boot=casper netboot=nfs nfsroot='); +like($deb, qr/getipaddr\(\$instserver\)/, + 'instserver is resolved to an IP via getipaddr for nfsroot (klibc nfsmount cannot resolve hostnames)'); +unlike($deb, qr/nfsroot=\$\{instserver\}:/, + 'nfsroot does NOT use the bare instserver hostname (would break klibc nfsmount)'); + +# The subiquity netboot cmdline must include 'toram' so casper copies the squashfs into RAM and +# unmounts the NFS source -- otherwise the end-of-install reboot wedges in systemd-shutdown on a +# D-state process doing I/O to the still-mounted NFS root, and the node never boots the installed disk. +like($deb, qr/\btoram\b/, + 'subiquity netboot cmdline includes toram (run from RAM, no NFS root at shutdown -> no reboot hang)'); +unlike($deb, qr/nfsroot=[^ ]*,soft/, + 'nfsroot does NOT append ,soft (casper takes the whole nfsroot value as the path, which breaks the mount)'); + +done_testing(); diff --git a/xCAT-test/unit/postage_ubuntu_syncfiles_postboot.t b/xCAT-test/unit/postage_ubuntu_syncfiles_postboot.t new file mode 100644 index 000000000..00cde9e77 --- /dev/null +++ b/xCAT-test/unit/postage_ubuntu_syncfiles_postboot.t @@ -0,0 +1,59 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use FindBin; +use Test::More; + +# Regression: on Ubuntu/Debian the DISKFUL install runs a node's postscripts inside the +# installer's in-target chroot -- before the node has booted as itself. The syncfiles +# postscript works by asking the management node to scp files INTO the running node, which +# cannot happen in that phase: the not-yet-booted node has no sshd for the MN to reach, so the +# push times out, syncfiles exits 1, and the node reports status=failed even though the OS +# installed perfectly. +# +# syncfiles must therefore move to the postbootscripts set, which runs on the booted node +# where ssh is already listening. Two things bound that move: +# +# * It applies to the DISKFUL install path only. netboot and statelite already run their +# postscripts on the booted node, so moving syncfiles there changes working behaviour for +# no reason. EL/SLES are unaffected either way -- their postscripts already run on the +# booted node. +# * syncfiles must run BEFORE the existing postbootscripts, since a postbootscript may +# consume the files it synchronises. Appending it would invert that. + +my $repo_root = File::Spec->rel2abs( + File::Spec->catdir( $FindBin::Bin, '..', '..' ) +); +my $path = File::Spec->catfile( $repo_root, 'xCAT-server', 'lib', 'perl', 'xCAT', 'Postage.pm' ); +plan skip_all => "Postage.pm not found" unless -f $path; + +my $src = do { local $/; open my $fh, '<', $path or die $!; <$fh> }; + +# Isolate the deferral block so the assertions below cannot accidentally match code elsewhere. +my ($block) = $src =~ /(\n[^\n]*ubuntu\|debian[^\n]*\n(?:.*?\n)*?[^\n]*syncfiles(?:.*?\n)*?\s*\}\n\s*\}\n)/; +ok( defined $block, 'found the syncfiles deferral block in makescript' ) + or do { done_testing(); exit }; + +like( $block, qr/\$os\s*=~\s*.\^\(\?:ubuntu\|debian\)/, + 'the move is guarded to ubuntu/debian nodes only' ); + +like( $block, qr/\$diskful_install|\bnodesetstate\b|\bprovmethod\b/, + 'the move is guarded to the diskful install path (not netboot/statelite)' ); + +like( $block, qr/\$postscripts\s*=~\s*s\/\^\[[^\]]*\]\*syncfiles/, + 'syncfiles is stripped from the in-target postscripts list' ); + +like( $block, qr/\$postbootscripts\s*=\s*"[^"]*syncfiles[^"]*"\s*\.\s*\$postbootscripts/, + 'syncfiles is PREPENDED to postbootscripts, so it runs before scripts that consume its files' ); + +unlike( $block, qr/\$postbootscripts\s*\.=\s*"syncfiles/, + 'syncfiles is not appended after the existing postbootscripts' ); + +# The strip and the re-add must be paired, so a node with no syncfiles postscript never +# acquires a spurious one. +like( $block, qr/if\s*\(.*?\$postscripts.*?syncfiles.*?\)\s*\{\s*.*?\$postbootscripts/s, + 'syncfiles is only added to postbootscripts when it was present in postscripts' ); + +done_testing(); diff --git a/xCAT-test/unit/ubuntu_resolvconf_ip.t b/xCAT-test/unit/ubuntu_resolvconf_ip.t new file mode 100644 index 000000000..d35600d8b --- /dev/null +++ b/xCAT-test/unit/ubuntu_resolvconf_ip.t @@ -0,0 +1,42 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +# Regression: the Ubuntu (subiquity) diskful install writes the installer's /etc/resolv.conf +# from compute.subiquity.tmpl's "DNS setup" early-command. A nameserver line in +# /etc/resolv.conf MUST be an IP address -- glibc's resolver does NOT resolve a hostname +# given on a nameserver line, it simply discards the entry. The template used to write the +# xcatmaster *name* there: +# +# echo "nameserver #TABLE:noderes:$NODE:xcatmaster#" >/etc/resolv.conf +# +# which left the installer (and the in-target apt-get update, which inherits this resolv.conf) +# with no usable DNS. The install then hangs resolving archive.ubuntu.com -- the node never +# finishes, and the test sees "ssh: connect ... port 22: Connection refused". (Everything the +# early-command did BEFORE this point still worked because the live installer's DHCP-provided +# resolv.conf can resolve the xcatmaster name -- so the failure only surfaces at the very next +# DNS-dependent step, the in-target apt.) +# +# The fix resolves the xcatmaster hostname to an IPv4 address (via getent, while DNS still +# works) and writes that IP into resolv.conf. + +sub slurp { my ($p) = @_; local $/; open my $fh, '<', $p or return undef; <$fh> } + +my $tmpl = slurp('xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl'); +plan skip_all => 'compute.subiquity.tmpl not found' unless defined $tmpl; + +# The old, broken form -- a bare hostname token written straight into the nameserver line -- +# must be gone. +unlike($tmpl, qr/nameserver \s+ \#TABLE:noderes:\$NODE:xcatmaster\# /x, + 'resolv.conf nameserver is NOT the bare xcatmaster hostname token (glibc cannot resolve a name there)'); + +# The xcatmaster name must be resolved to an IPv4 address before it is used as a nameserver. +like($tmpl, qr/getent \s+ ahostsv4 /x, + 'DNS setup resolves the xcatmaster to an IPv4 address via getent ahostsv4'); + +# And the nameserver line must be written from that resolved IP variable. +like($tmpl, qr/nameserver \s+ \$xcatmaster_ip/x, + 'resolv.conf nameserver is written from the resolved xcatmaster IP'); + +done_testing(); diff --git a/xCAT-test/unit/ubuntu_subiquity_apt_sources.t b/xCAT-test/unit/ubuntu_subiquity_apt_sources.t new file mode 100644 index 000000000..f857af48e --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_apt_sources.t @@ -0,0 +1,69 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; + +use Test::More; + +# Regression: on an ONLINE Subiquity install of a classic-sources release (20.04 / 22.04), +# Subiquity renders the target's /etc/apt/sources.list from the install media alone +# ("deb file:///cdrom"). A live-server ISO does not carry everything xCAT's postscripts need -- +# chrony, for one -- so curtin's in-target apt fails with "E: Unable to locate package chrony" +# and the install crashes. `sources_list:` is a curtin key that Subiquity's autoinstall schema +# ignores, so the online archive has to be added through `sources:`, which Subiquity honours by +# writing /etc/apt/sources.list.d/*.list. +# +# That fix belongs to the classic-sources releases ONLY. On a Deb822 release (24.04 / 26.04) +# the primary mirror already lands in /etc/apt/sources.list.d/ubuntu.sources, so adding legacy +# .list files on top produces duplicate apt entries for the same suites -- exactly the source +# conflict the Deb822 branch elsewhere in this function goes out of its way to avoid. + +require xCAT::Template; + +my $MIRROR = 'http://br.archive.ubuntu.com/ubuntu'; + +sub apt_config_for { + my (%opt) = @_; + no warnings 'redefine'; + local *xCAT::Template::ubuntu_subiquity_apt_mirror = sub { $opt{mirror} }; + local *xCAT::Template::ubuntu_subiquity_uses_deb822_sources = sub { $opt{deb822} }; + local *xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { () }; + local *xCAT::Template::ubuntu_subiquity_uses_generated_cdrom_source = sub { 0 }; + return xCAT::Template::ubuntu_subiquity_apt_config('/some/media/dir'); +} + +# --- online, classic sources (20.04 / 22.04): the archive must be added via sources: --- +my $classic = apt_config_for( mirror => $MIRROR, deb822 => 0 ); + +like( $classic, qr/^\s*mirror-selection:/m, + 'classic online install still sets the primary mirror' ); +like( $classic, qr/^\s*sources:/m, + 'classic online install adds apt sources (sources_list is ignored by Subiquity)' ); +like( $classic, qr/xcat-ubuntu-archive\.list:/, + 'classic online install writes an xCAT-owned archive source' ); +like( $classic, qr/xcat-ubuntu-updates\.list:/, + 'classic online install writes an xCAT-owned updates source' ); +like( $classic, qr/deb \Q$MIRROR\E \$RELEASE main restricted universe multiverse/, + 'the archive source uses the configured mirror and curtin\'s $RELEASE token' ); + +# --- online, Deb822 (24.04 / 26.04): no legacy .list files on top of ubuntu.sources --- +my $deb822 = apt_config_for( mirror => $MIRROR, deb822 => 1 ); + +like( $deb822, qr/^\s*mirror-selection:/m, + 'Deb822 online install still sets the primary mirror' ); +unlike( $deb822, qr/xcat-ubuntu-archive\.list:/, + 'Deb822 online install does NOT add a legacy archive .list (it would duplicate ubuntu.sources)' ); +unlike( $deb822, qr/xcat-ubuntu-updates\.list:/, + 'Deb822 online install does NOT add a legacy updates .list' ); + +# --- offline is untouched by any of this --- +my $offline = apt_config_for( mirror => '', deb822 => 0 ); +like( $offline, qr/fallback: offline-install/, + 'the offline path is unchanged' ); +unlike( $offline, qr/xcat-ubuntu-archive\.list:/, + 'the offline path adds no online archive source' ); + +done_testing(); diff --git a/xCAT-test/unit/ubuntu_subiquity_boot_flip.t b/xCAT-test/unit/ubuntu_subiquity_boot_flip.t new file mode 100644 index 000000000..6d57d5e30 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_boot_flip.t @@ -0,0 +1,56 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use FindBin; +use Test::More; + +# Regression: after Subiquity installs the OS the node MUST be flipped to local-disk boot, or it +# PXE-loops straight back into the installer and the installed OS -- with its sshd -- never +# boots. The diskful case then only ever reports +# "ssh: connect to host port 22: Connection refused". +# +# xCAT performs that flip when the node reports "next" to xcatd on the install-monitor port, +# which makes xcatd run "nodeset next" and rewrite the node's xNBA script to fall through +# to the local disk. The in-target post-script tries this through updateflag.awk, but that +# depends on gawk's |& / /inet coprocess and on /usr/bin/awk being gawk -- on Ubuntu +# /usr/bin/awk is normally mawk, which has neither, so the flip fails silently. +# +# The template must therefore trigger the flip itself from the live installer, with no gawk +# dependency, and must NOT report success when it did not happen. + +my $repo_root = File::Spec->rel2abs( + File::Spec->catdir( $FindBin::Bin, '..', '..' ) +); +my $path = File::Spec->catfile( + $repo_root, 'xCAT-server', 'share', 'xcat', 'install', 'ubuntu', 'compute.subiquity.tmpl' +); +plan skip_all => 'compute.subiquity.tmpl not found' unless -f $path; + +my $tmpl = do { local $/; open my $fh, '<', $path or die $!; <$fh> }; + +like( $tmpl, qr{/dev/tcp/\$xm/3002}, + 'the flip contacts xcatd on the install-monitor port from the installer' ); +like( $tmpl, qr{printf "next}, + 'it sends the "next" token that makes xcatd run "nodeset next"' ); +like( $tmpl, qr{xm=#XCATVAR:XCATMASTER#}, + "the flip targets the node's own xcatmaster" ); +like( $tmpl, qr{^\s*-\s*\['bash',\s*'-c',}m, + 'the flip runs under bash (dash has no /dev/tcp) via an argv list command' ); + +# A failed flip must be visible. The original form broke out of its retry loop as soon as the +# socket connected -- never on a confirmed exchange -- and ended in `true`, so a total failure +# looked exactly like success and the node silently reinstalled forever. +like( $tmpl, qr{ok=1}, + 'success is recorded only after the exchange completes, not merely on connect' ); +like( $tmpl, qr{FAILED to flip}, + 'a failed flip is reported into the install log rather than passing silently' ); + +# The magic-SysRq forced reboot must be gone: toram already unmounts the NFS live root, so the +# shutdown hang it worked around cannot occur, and an unconditional timed reboot would race a +# slow install. +unlike( $tmpl, qr{sysrq-trigger}, + 'no unconditional magic-SysRq reboot (toram removes the hang it worked around)' ); + +done_testing(); diff --git a/xCAT-test/unit/ubuntu_subiquity_bootparams.t b/xCAT-test/unit/ubuntu_subiquity_bootparams.t index bea2de236..fccc9502f 100644 --- a/xCAT-test/unit/ubuntu_subiquity_bootparams.t +++ b/xCAT-test/unit/ubuntu_subiquity_bootparams.t @@ -11,7 +11,8 @@ plan skip_all => "debian.pm not found" unless -f $plugin_path; my $src = do { local $/; open my $fh, '<', $plugin_path or die $!; <$fh> }; -like($src, qr/autoinstall ip=dhcp netboot=nfs/, 'subiquity bootparams enable autoinstall'); +like($src, qr/autoinstall ip=dhcp boot=casper netboot=nfs/, + 'subiquity bootparams enable autoinstall (boot=casper is required for casper to process netboot=nfs)'); like($src, qr/ds=nocloud-net;s=http:\/\//, 'subiquity bootparams point at NoCloud seed'); like($src, qr/\$kcmdline\s*\.=\s*" ---";/, 'subiquity bootparams end with installer argument separator');