diff --git a/xCAT-test/unit/debian_subiquity_boot_params.t b/xCAT-test/unit/debian_subiquity_boot_params.t index 7a658864a..c22430565 100644 --- a/xCAT-test/unit/debian_subiquity_boot_params.t +++ b/xCAT-test/unit/debian_subiquity_boot_params.t @@ -77,6 +77,29 @@ my $resolver = sub { $_[0] eq 'mn.cluster' ? '10.0.0.1' : undef }; 'while the cloud-init seed URL keeps the name, where DNS works' ); } +# A dual-stack management node has both an A and an AAAA record, and getipaddr answers with +# whichever the resolver returns first. casper mounts the live filesystem with klibc's nfsmount, +# which takes an IPv4 address, and it takes everything after the first colon in nfsroot= as the +# path -- so an IPv6 address there is not a slower path, it is an unparseable one. This install +# asks for ip=dhcp, so the address must be IPv4. +{ + no warnings 'redefine'; + local *xCAT::NetworkUtils::getipaddr = sub { + my ($class, $host, %opt) = @_; + return $opt{OnlyV4} ? '10.0.0.1' : '2001:db8::1'; + }; + + my ($kcmdline, $err) = T::subiquity_boot_params( + 'nofb utf8 auto xcatd=mn.cluster', 'mn.cluster', + '/install/ubuntu24.04/x86_64', '80', 'cn1' ); + ok( !defined $err, 'a dual-stack install server produces a boot config' ) + or diag("error was: $err"); + like( $kcmdline, qr{nfsroot=10\.0\.0\.1:/install/ubuntu24\.04/x86_64}, + 'and nfsroot carries the IPv4 address of the install server' ); + unlike( $kcmdline, qr/nfsroot=[^ ]*::/, + 'never the IPv6 address, which klibc nfsmount cannot use' ); +} + # The guard the original commit added must survive: a name that does not resolve is an error, # not a command line with a name in nfsroot. { diff --git a/xCAT-test/unit/ubuntu_resolvconf_ip.t b/xCAT-test/unit/ubuntu_resolvconf_ip.t index 325c835ab..a82340add 100644 --- a/xCAT-test/unit/ubuntu_resolvconf_ip.t +++ b/xCAT-test/unit/ubuntu_resolvconf_ip.t @@ -9,7 +9,9 @@ use Test::More; # A nameserver line in /etc/resolv.conf must hold an IP address: glibc's resolver discards an # entry naming a host. Writing the xcatmaster *name* left the installer -- and the in-target # apt-get that inherits the file -- with no usable DNS, so the install hung resolving -# archive.ubuntu.com. The template resolves the name to an address first. +# archive.ubuntu.com. The template resolves the name to an address first, and when the name does +# not resolve it keeps the resolver the live installer already got from DHCP, because a name +# written into resolv.conf resolves nothing. # # Run the template's own shell for that step and inspect the file it writes. @@ -20,16 +22,22 @@ open(my $fh, '<', $tmpl) or die "open $tmpl: $!"; my $source = do { local $/; <$fh> }; close $fh; -my ($fragment) = $source =~ m{^(\s*xcatmaster_host=.*?echo "nameserver [^\n]*\n)}ms; +my ($fragment) = $source =~ m{^(\s*xcatmaster_host=.*?\n)\s*echo "=== early-commands complete}ms; BAIL_OUT('the template does not build /etc/resolv.conf from the xcatmaster') unless $fragment; -# $NODE and the xcatmaster come from the xCAT template renderer; stand in for both. +# $NODE, the xcatmaster and the domain come from the xCAT template renderer; stand in for them. sub write_resolv_conf { my (%opt) = @_; my $root = tempdir(CLEANUP => 1); + # What DHCP left behind in the live installer, which the step either replaces or keeps. + open my $seed, '>', "$root/resolv.conf" or die $!; + print {$seed} "nameserver 192.168.0.53\n"; + close $seed; + my $script = $fragment; $script =~ s/\#TABLE:noderes:\$NODE:xcatmaster\#/$opt{xcatmaster}/; + $script =~ s/\#TABLE:site:key=domain:value\#/cluster/; $script =~ s{/etc/resolv\.conf}{$root/resolv.conf}g; # The fragment contains `rm -f /etc/resolv.conf` and this suite runs as root in CI, so a @@ -59,32 +67,38 @@ sub write_resolv_conf { # --- the case the fix exists for ------------------------------------------- { my $r = write_resolv_conf(xcatmaster => 'xcatmn', resolves => '10.0.0.1'); - is($r->{content}, "nameserver 10.0.0.1\n", + like($r->{content}, qr/^nameserver 10\.0\.0\.1$/m, 'the nameserver line holds the address, which glibc will actually use'); unlike($r->{content}, qr/nameserver \s+ xcatmn/x, 'the nameserver line never holds a host name, which glibc discards'); + like($r->{content}, qr/^domain cluster$/m, 'and the search domain is written'); } # --- more than one address: the first is taken ----------------------------- { my $r = write_resolv_conf(xcatmaster => 'xcatmn', resolves => '10.0.0.1'); - like($r->{content}, qr/^nameserver \d+\.\d+\.\d+\.\d+$/m, - 'exactly one IPv4 address is written'); + my @ns = ($r->{content} =~ /^nameserver (\S+)$/mg); + is_deeply(\@ns, ['10.0.0.1'], 'exactly one IPv4 address is written'); } -# --- resolution fails: fall back rather than write an empty entry ---------- +# --- resolution fails: keep the resolver DHCP gave the live installer ------ +# Writing the name back was the original defect. It leaves the installer with no DNS, and the +# in-target apt inherits the same file. { my $r = write_resolv_conf(xcatmaster => 'xcatmn'); - is($r->{content}, "nameserver xcatmn\n", - 'an unresolvable xcatmaster falls back to its name rather than an empty nameserver'); + unlike($r->{content}, qr/^nameserver \s* xcatmn/xm, + 'an unresolvable xcatmaster is never written as a nameserver'); unlike($r->{content}, qr/^nameserver\s*$/m, 'no empty nameserver line is written'); + like($r->{content}, qr/^nameserver 192\.168\.0\.53$/m, + 'the resolver DHCP gave the live installer is kept instead'); } # --- an xcatmaster already given as an address is left alone --------------- { my $r = write_resolv_conf(xcatmaster => '10.0.0.1', resolves => '10.0.0.1'); - is($r->{content}, "nameserver 10.0.0.1\n", 'an address-valued xcatmaster is written as is'); + like($r->{content}, qr/^nameserver 10\.0\.0\.1$/m, + 'an address-valued xcatmaster is written as is'); } done_testing(); diff --git a/xCAT-test/unit/ubuntu_subiquity_boot_flip.t b/xCAT-test/unit/ubuntu_subiquity_boot_flip.t index 7cc1a9ab2..cf98f2e8e 100644 --- a/xCAT-test/unit/ubuntu_subiquity_boot_flip.t +++ b/xCAT-test/unit/ubuntu_subiquity_boot_flip.t @@ -19,10 +19,9 @@ use Test::More; my $tmpl = "$FindBin::Bin/../../xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl"; plan skip_all => 'compute.subiquity.tmpl not found' unless -r $tmpl; -# The template addresses the install-monitor on 3002, and on any management node xcatd is -# already listening there -- so binding it here made the whole file skip_all exactly where the -# suite runs. Take an ephemeral port from the kernel instead and rewrite the extracted command -# to use it: what is under test is the retry-and-log behaviour, not the port number. +# The install monitor listens on site.xcatiport, and on any management node xcatd is already +# there -- so binding it here made the whole file skip_all exactly where the suite runs. Take an +# ephemeral port from the kernel instead and hand it to the template as the site value. my $probe = IO::Socket::INET->new( LocalAddr => '127.0.0.1', LocalPort => 0, Proto => 'tcp', Listen => 5, ReuseAddr => 1) @@ -38,26 +37,20 @@ close $fh; my ($command) = $source =~ m{- \['bash', '-c', '(.*?/dev/tcp/.*?)'\]}; BAIL_OUT('no late-command in the template performs the boot flip over /dev/tcp') unless $command; -# Read the port out of the template rather than hard-coding it, so a template that moves the -# install-monitor still gets covered instead of silently testing the wrong port. -my ($TEMPLATE_PORT) = $command =~ m{/dev/tcp/\$xm/(\d+)}; -BAIL_OUT('could not read the install-monitor port from the boot-flip command') - unless $TEMPLATE_PORT; -is($TEMPLATE_PORT, 3002, 'the template addresses the install-monitor port xcatd listens on'); - # Run the command with the install server pointed at our stand-in, and its log inside a scratch -# tree. Everything else is the template's own text. +# tree. site.xcatiport is substituted the way the template renderer substitutes it. Everything +# else is the template's own text. sub run_flip { my (%opt) = @_; my $root = tempdir(CLEANUP => 1); mkdir "$root/target"; mkdir "$root/target/var"; mkdir "$root/target/var/log"; mkdir "$root/target/var/log/xcat"; + my $site_port = exists $opt{site_port} ? $opt{site_port} : $XCATD_PORT; + my $script = $command; $script =~ s/\#XCATVAR:XCATMASTER\#/127.0.0.1/; - # point the flip at the ephemeral listener, in both the /dev/tcp target and the log message - $script =~ s{/dev/tcp/\$xm/\Q$TEMPLATE_PORT\E\b}{/dev/tcp/\$xm/$XCATD_PORT}; - $script =~ s{\$xm:\Q$TEMPLATE_PORT\E\b}{\$xm:$XCATD_PORT}g; + $script =~ s/\#TABLEBLANKOKAY:site:key=xcatiport:value\#/$site_port/; $script =~ s{/target/var/log/xcat/xcat\.log}{$root/target/var/log/xcat/xcat.log}; $script =~ s/sleep 5/sleep 1/; # shorten the retry pause, keep the retry @@ -75,10 +68,11 @@ sub run_flip { my $c = $srv->accept() or last; $c->autoflush(1); if ($opt{mute}) { sleep 600; close $c; next } # accept and hold, never answer - print {$c} "ready\n"; + # xcatd greets with "ready", then answers every request with "done". + print {$c} ($opt{greeting} || "ready\n"); my $line = <$c>; print {$seen} $line if defined $line; - print {$c} "ok\n" unless $opt{no_ack}; + print {$c} ($opt{ack} || "done\n") unless $opt{no_ack}; close $c; } close $seen; @@ -95,10 +89,10 @@ sub run_flip { if ($pid) { kill 'TERM', $pid; waitpid($pid, 0) } my $received = ''; - if (open my $rh, '<', "$root/received") { local $/; $received = <$rh>; close $rh } + if (open my $rh, '<', "$root/received") { local $/; $received = <$rh> || ''; close $rh } my $log = ''; - if (open my $lh, '<', "$root/target/var/log/xcat/xcat.log") { local $/; $log = <$lh>; close $lh } + if (open my $lh, '<', "$root/target/var/log/xcat/xcat.log") { local $/; $log = <$lh> || ''; close $lh } return { rc => $rc, timed_out => $timed_out, log => $log, received => $received }; } @@ -112,6 +106,16 @@ sub run_flip { ok(!$r->{timed_out}, 'the exchange completes rather than hanging the late-command'); } +# --- the port comes from site.xcatiport ------------------------------------ +# The case above already proves it: the stand-in listens on an ephemeral port, not on 3002, and +# the exchange only completes because the template asks site for the port. What is left is the +# default, for a site table that does not carry the key. +{ + my $r = run_flip(site_port => '', listen => 0, cap => 90); + like($r->{log}, qr/:3002\b/, + 'an unset site.xcatiport falls back to the port xcatd listens on by default'); +} + # --- xcatd never answers: the failure is recorded, not swallowed ----------- { my $r = run_flip(listen => 0); @@ -122,9 +126,27 @@ sub run_flip { 'the log names the install server and port that could not be reached'); } +# --- something else is listening on the port ------------------------------- +# Only xcatd's install monitor answers "nodeset next". A service that accepts the +# connection and talks its own protocol must not be counted as a flipped node. +{ + my $r = run_flip(listen => 5, greeting => "220 smtp\n"); + like($r->{log}, qr/FAILED to flip/, + 'a peer that does not greet with "ready" is not treated as the install monitor'); + is($r->{received}, '', + 'and the flip token is never sent to it'); +} + +# --- the peer greets but does not acknowledge the request ------------------ +{ + my $r = run_flip(listen => 5, ack => "?\n"); + like($r->{log}, qr/FAILED to flip/, + 'a reply other than "done" is not counted as an accepted request'); +} + # --- xcatd accepts but never acknowledges ---------------------------------- { - my $r = run_flip(listen => 1, no_ack => 1); + my $r = run_flip(listen => 5, no_ack => 1); like($r->{log}, qr/FAILED to flip/, 'a connection without an acknowledgement counts as a failure, not a success'); }