mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-09-04 20:17:55 +00:00
test(subiquity): three values the Ubuntu install path accepts and cannot use
The Subiquity diskful path builds three values from configuration, and nothing checks that the value it builds is one the consumer can use. The boot flip addresses port 3002. xcatd's install monitor listens on site.xcatiport, so a cluster that moves the port loses the flip and every node PXE-loops. The exchange also counts any reply as an accepted request, so a different service on that port reads as a flipped node. xcatd greets with "ready" and answers every request with "done". subiquity_nfsroot_server resolves the install server without a family. A dual-stack management node answers with its IPv6 address, and nfsroot=<v6>:<path> is unparseable, because klibc nfsmount takes everything after the first colon as the path. The DNS setup writes the xcatmaster name as a nameserver when getent fails, which is the case the step exists to prevent. ubuntu_subiquity_boot_flip.t drives the template's own late-command against a stand-in monitor. ubuntu_resolvconf_ip.t runs the DNS step and reads the file it writes. debian_subiquity_boot_params.t drives the helpers with a dual-stack resolver. All three fail on this commit. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
@@ -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.
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 <node> 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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user