mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-10-21 15:18:19 +00:00
test(ubuntu): execute rendered Subiquity command sequences
Render Subiquity configuration with real database rows and execute the emitted commands in private namespaces. Check resulting files, failure ordering and the install-monitor handshake without rewriting shell fragments.
This commit is contained in:
@@ -9,7 +9,7 @@ jobs:
|
||||
- name: Refresh package indexes
|
||||
run: sudo apt-get update
|
||||
- name: Install dependencies
|
||||
run: sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests bats bubblewrap cpio xz-utils gzip gawk mawk rpm rsyslog build-essential fakeroot reprepro devscripts debhelper libcapture-tiny-perl libfile-slurper-perl libjson-perl libyaml-pp-perl libparallel-forkmanager-perl libsoap-lite-perl libdbi-perl libdbd-sqlite3-perl libcgi-pm-perl quilt openssh-server dpkg looptools genometools software-properties-common grub-common grub-efi-amd64-bin squashfs-tools pigz
|
||||
run: sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests bats bubblewrap cpio xz-utils gzip gawk mawk rpm rsyslog build-essential fakeroot reprepro devscripts debhelper libcapture-tiny-perl libfile-slurper-perl libjson-perl libyaml-pp-perl libparallel-forkmanager-perl libsoap-lite-perl libdbi-perl libdbd-sqlite3-perl libcgi-pm-perl quilt openssh-server dpkg looptools genometools software-properties-common grub-common grub-efi-amd64-bin squashfs-tools pigz python3-yaml
|
||||
- name: Enable postscript test namespaces
|
||||
run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
||||
- name: Install lifecycle test dependencies
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
run: |
|
||||
log=$(mktemp)
|
||||
trap 'rm -f -- "$log"' EXIT
|
||||
prove -v xCAT-test/native/install_profile_riscv64.t xCAT-test/native/netboot_profile_riscv64.t xCAT-test/native/builddebs_riscv64.t | tee "$log"
|
||||
prove -v xCAT-test/native/install_profile_riscv64.t xCAT-test/native/netboot_profile_riscv64.t xCAT-test/native/builddebs_riscv64.t xCAT-test/native/ubuntu_subiquity_template.t | tee "$log"
|
||||
! grep -qi skipped "$log"
|
||||
rpm_lifecycle_test:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
case "${0##*/}" in
|
||||
ip) exit 0 ;;
|
||||
getent)
|
||||
[ "$*" = 'ahostsv4 master' ] || exit 64
|
||||
[ "$XCAT_TEST_SCENARIO" != unresolved ] || exit 2
|
||||
printf '127.0.0.1 STREAM master\n'
|
||||
;;
|
||||
wget)
|
||||
printf '%s\n' "$*" >>/fixture/downloads
|
||||
output=
|
||||
timeout=
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-T) timeout=$2; shift 2 ;;
|
||||
-O) output=$2; shift 2 ;;
|
||||
http://127.0.0.1:8080/install/autoinst/*) url=$1; shift ;;
|
||||
*) exit 64 ;;
|
||||
esac
|
||||
done
|
||||
[ "$timeout" = 30 ] || exit 64
|
||||
name=${url##*/}
|
||||
[ -n "$output" ] || output=$name
|
||||
case "$XCAT_TEST_SCENARIO:$name" in
|
||||
fail-disk:getinstdisk|fail-pre:node.pre) exit 8 ;;
|
||||
empty-disk:getinstdisk|empty-pre:node.pre) : >"$output"; exit 0 ;;
|
||||
esac
|
||||
cp "/fixture/payloads/$name" "$output"
|
||||
;;
|
||||
curtin)
|
||||
[ "$1 $2 $3" = 'in-target --target /target' ] || exit 64
|
||||
shift 3
|
||||
case "$*" in
|
||||
update-grub2) cp /target/etc/default/grub /fixture/grub-input ;;
|
||||
/root/post.script) exec /target/root/post.script ;;
|
||||
*) exit 64 ;;
|
||||
esac
|
||||
;;
|
||||
*) exit 64 ;;
|
||||
esac
|
||||
@@ -0,0 +1,46 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use File::Copy qw(copy);
|
||||
use File::Slurper qw(read_text write_text);
|
||||
use IO::Socket::INET;
|
||||
use JSON::PP qw(decode_json encode_json);
|
||||
use POSIX qw(_exit);
|
||||
|
||||
alarm 90;
|
||||
my $commands = decode_json(read_text('/fixture/commands.json'));
|
||||
copy('/fixture/autoinstall.yaml', '/autoinstall.yaml') or die $!;
|
||||
my $listener = IO::Socket::INET->new(
|
||||
LocalAddr => '127.0.0.1', LocalPort => 3002, Listen => 1, ReuseAddr => 1,
|
||||
) or die "Cannot start install monitor: $!";
|
||||
my $pid = fork();
|
||||
die "fork: $!" unless defined $pid;
|
||||
if (!$pid) {
|
||||
alarm 60;
|
||||
my $client = $listener->accept() or _exit(1);
|
||||
$client->autoflush(1);
|
||||
print {$client} "ready\n";
|
||||
my $request = <$client>;
|
||||
write_text('/fixture/monitor-request', $request // '');
|
||||
print {$client} "done\n" if defined($request) && $request eq "next\n";
|
||||
close($client);
|
||||
_exit(0);
|
||||
}
|
||||
close($listener);
|
||||
my @statuses;
|
||||
my $status = 0;
|
||||
for my $phase (qw(early-commands late-commands)) {
|
||||
for my $command (@{$commands->{$phase}}) {
|
||||
my @arguments = ref($command) eq 'ARRAY' ? @$command : ('/bin/sh', '-c', $command);
|
||||
system(@arguments);
|
||||
$status = $? == -1 ? 255 : $? & 127 ? 128 + ($? & 127) : $? >> 8;
|
||||
push @statuses, [$phase, $status];
|
||||
last if $status;
|
||||
}
|
||||
last if $status;
|
||||
}
|
||||
kill 'TERM', $pid if $status;
|
||||
waitpid($pid, 0);
|
||||
write_text('/fixture/monitor-status', "$?\n");
|
||||
write_text('/fixture/statuses.json', encode_json(\@statuses));
|
||||
copy('/autoinstall.yaml', '/fixture/final-autoinstall.yaml') or die $!;
|
||||
exit $status;
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use File::Copy qw(copy);
|
||||
use Capture::Tiny qw(capture);
|
||||
use File::Path qw(make_path);
|
||||
use File::Slurper qw(read_text write_text);
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib", "$FindBin::Bin/../../perl-xCAT", "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
use JSON::PP qw(encode_json decode_json);
|
||||
use Test::More;
|
||||
use XCAT::Test::File qw(repo_path);
|
||||
use XCAT::Test::Sandbox qw(sandbox_root sandbox_run);
|
||||
use XCAT::Test::Template qw(template_database set_row render_template);
|
||||
|
||||
plan skip_all => 'Subiquity command execution requires Linux' unless $^O eq 'linux';
|
||||
my $database = template_database();
|
||||
require xCAT::SvrUtils;
|
||||
set_row('noderes', {node => 'node'}, {xcatmaster => 'master'});
|
||||
write_text("$database/extra.pkglist", "htop\nwget\n");
|
||||
write_text("$database/image.pkglist", "#INCLUDE:$database/extra.pkglist#\nvim\n");
|
||||
|
||||
sub load_yaml {
|
||||
my ($file) = @_;
|
||||
my ($stdout, $stderr, $status) = capture {
|
||||
system('/usr/bin/python3', '-I', '-c',
|
||||
'import json, sys, yaml; json.dump(yaml.safe_load(open(sys.argv[1])), sys.stdout)', $file)
|
||||
};
|
||||
die "Cannot parse $file: $stderr" if $status;
|
||||
return decode_json($stdout);
|
||||
}
|
||||
|
||||
sub render {
|
||||
my ($version, $arch, $nic, $primary) = @_;
|
||||
set_row('nodetype', {node => 'node'}, {os => "ubuntu$version", arch => $arch, provmethod => 'image'});
|
||||
set_row('noderes', {node => 'node'}, {installnic => $nic, primarynic => $primary});
|
||||
my $template = xCAT::SvrUtils->get_tmpl_file_name(
|
||||
repo_path('xCAT-server/share/xcat/install/ubuntu'), 'compute', "ubuntu$version", $arch, 'subiquity');
|
||||
my $output = "$database/autoinstall.yaml";
|
||||
render_template($database, $template, $output, 'node', "$database/image.pkglist",
|
||||
"/install/ubuntu$version/$arch", 'ubuntu', undef, {xcatmaster => '127.0.0.1'}, osarch => $arch);
|
||||
my $text = read_text($output);
|
||||
like($text, qr/\A#cloud-config\n/, 'renders a cloud-config document');
|
||||
unlike($text, qr/#(?:TABLE|CRYPT|SUBIQUITY|INCLUDE|XCATVAR)[^\n]*#/, 'resolves all template inputs');
|
||||
return (load_yaml($output)->{autoinstall}, $text);
|
||||
}
|
||||
|
||||
for my $release (qw(20.04 22.04 24.04 26.04)) {
|
||||
subtest "Ubuntu $release data" => sub {
|
||||
my ($config) = render($release, 'x86_64', '', '');
|
||||
is($config->{version}, 1, 'uses autoinstall version 1');
|
||||
is_deeply($config->{identity}, {
|
||||
realname => 'xCAT Admin', username => 'xcatadm', hostname => 'node', password => '*',
|
||||
}, 'supplies a noninteractive identity with a locked password');
|
||||
is($config->{kernel}{package}, 'linux-generic', 'selects the generic kernel');
|
||||
ok($config->{ssh}{'install-server'}, 'installs the SSH server');
|
||||
ok(!$config->{'user-data'}{package_update} && !$config->{'user-data'}{package_upgrade}, 'leaves automatic package updates disabled');
|
||||
is($config->{'user-data'}{timezone}, 'UTC', 'uses the site timezone');
|
||||
my %packages;
|
||||
$packages{$_}++ for @{$config->{packages}};
|
||||
is($packages{$_}, 1, "installs $_ once") for qw(openssh-server wget htop vim);
|
||||
ok(!$packages{'nfs-common'}, 'does not require NFS tools from the offline media');
|
||||
ok(!$config->{apt}{geoip}, 'keeps mirror geolocation disabled');
|
||||
is($config->{apt}{'mirror-selection'}{primary}[0]{uri}, 'http://archive.ubuntu.com/ubuntu', 'selects the x86 archive');
|
||||
if ($release eq '20.04' || $release eq '22.04') {
|
||||
is($config->{apt}{sources}{'xcat-ubuntu-archive.list'}{source},
|
||||
'deb http://archive.ubuntu.com/ubuntu $RELEASE main restricted universe multiverse',
|
||||
'defers the legacy apt suite to the installer release');
|
||||
} else {
|
||||
ok(!exists $config->{apt}{sources}{'xcat-ubuntu-archive.list'}, 'does not duplicate the Deb822 primary mirror');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
my @cases = (
|
||||
['named', 'eno2', '', 'eno2', 0],
|
||||
['primary', '', 'ens3', 'ens3', 0],
|
||||
['mac', '', '', undef, 0],
|
||||
['literal-mac', '52:54:00:12:34:56', 'ens3', undef, 0],
|
||||
['unresolved', 'eno2', '', 'eno2', 0],
|
||||
['fail-disk', '', '', undef, 8],
|
||||
['empty-disk', '', '', undef, 1],
|
||||
['disk-failure', '', '', undef, 9],
|
||||
['fail-pre', '', '', undef, 8],
|
||||
['empty-pre', '', '', undef, 1],
|
||||
['pre-failure', '', '', undef, 7],
|
||||
['no-partition', '', '', undef, 1],
|
||||
['post-failure', '', '', undef, 42],
|
||||
);
|
||||
for my $case (@cases) {
|
||||
my ($name, $nic, $primary, $rename, $expected_status) = @$case;
|
||||
subtest "installer $name" => sub {
|
||||
my ($config) = render('24.04', 'riscv64', $nic, $primary);
|
||||
is($config->{apt}{'mirror-selection'}{primary}[0]{uri}, 'http://ports.ubuntu.com/ubuntu-ports', 'selects the RISC-V ports archive');
|
||||
my $root = sandbox_root();
|
||||
make_path(map { "$root/$_" } qw(payloads target/etc/default target/etc/apt/sources.list.d target/etc/apt/apt.conf.d target/root));
|
||||
write_text("$root/commands.json", encode_json($config));
|
||||
# Subiquity removes the cloud-config wrapper before executing early commands.
|
||||
my ($rewritten, $stderr, $rewrite_status) = capture {
|
||||
system('/usr/bin/python3', '-I', '-c',
|
||||
'import json, sys, yaml; yaml.safe_dump(json.load(open(sys.argv[1])), sys.stdout, explicit_end=True)',
|
||||
"$root/commands.json")
|
||||
};
|
||||
die "Cannot serialize autoinstall config: $stderr" if $rewrite_status;
|
||||
write_text("$root/autoinstall.yaml", $rewritten);
|
||||
write_text("$root/etc/resolv.conf", "nameserver 192.0.2.53\n");
|
||||
my $hosts = "127.0.0.1 localhost\n";
|
||||
$hosts .= "127.0.1.1 oldname\n" if $name eq 'named';
|
||||
write_text("$root/target/etc/hosts", $hosts);
|
||||
write_text("$root/target/etc/default/grub", "GRUB_TIMEOUT=5\n");
|
||||
my @temporary_sources = qw(xcat-otherpkgs-0.list xcat-otherpkgs-1.sources xcat-pkgdir-0.list xcat-pkgdir-1.sources);
|
||||
write_text("$root/target/etc/apt/sources.list.d/$_", "temporary\n") for @temporary_sources;
|
||||
write_text("$root/target/etc/apt/sources.list.d/admin.list", "preserved\n");
|
||||
write_text("$root/target/etc/apt/apt.conf.d/94curtin-config", "temporary\n");
|
||||
write_text("$root/payloads/getinstdisk", <<'SH');
|
||||
#!/bin/sh
|
||||
[ "$XCAT_TEST_SCENARIO" != disk-failure ] || exit 9
|
||||
printf /dev/vda >/tmp/xcat.install_disk
|
||||
SH
|
||||
write_text("$root/payloads/node.pre", <<'SH');
|
||||
#!/bin/sh
|
||||
[ "$(cat /tmp/xcat.install_disk 2>/dev/null)" = /dev/vda ] || exit 3
|
||||
[ "$XCAT_TEST_SCENARIO" != pre-failure ] || exit 7
|
||||
printf ran >/fixture/pre-ran
|
||||
[ "$XCAT_TEST_SCENARIO" != no-partition ] || exit 0
|
||||
printf 'storage:\n layout:\n name: direct\n' >/tmp/partitionfile
|
||||
SH
|
||||
write_text("$root/payloads/node.post", <<'SH');
|
||||
#!/bin/sh
|
||||
printf ran >/fixture/post-ran
|
||||
[ "$XCAT_TEST_SCENARIO" != post-failure ] || exit 42
|
||||
SH
|
||||
for my $command (qw(ip getent wget curtin)) {
|
||||
copy(repo_path('xCAT-test/fixtures/subiquity/command'), "$root/bin/$command") or die $!;
|
||||
chmod 0755, "$root/bin/$command" or die $!;
|
||||
}
|
||||
copy(repo_path('xCAT-test/fixtures/subiquity/run.pl'), "$root/run.pl") or die $!;
|
||||
my ($status, $output) = sandbox_run($root, {env => {XCAT_TEST_SCENARIO => $name}}, $^X, '/fixture/run.pl');
|
||||
unless (is($status, $expected_status, 'the complete command sequence returns the expected status')) {
|
||||
diag($output);
|
||||
diag(read_text("$root/$_")) for grep { -f "$root/$_" } qw(tmp/pre-install.log target/var/log/xcat/xcat.log statuses.json);
|
||||
return;
|
||||
}
|
||||
my $statuses = decode_json(read_text("$root/statuses.json"));
|
||||
is($statuses->[-1][1], $expected_status, 'the result comes from an installer command');
|
||||
if ($expected_status) {
|
||||
ok(!-e "$root/monitor-request", 'does not advance the boot chain after failure');
|
||||
is(read_text("$root/target/etc/apt/apt.conf.d/94curtin-config"), "temporary\n", 'does not run later cleanup after failure');
|
||||
ok(!-e "$root/post-ran", 'does not run the postscript after early failure') unless $name eq 'post-failure';
|
||||
return;
|
||||
}
|
||||
is(read_text("$root/monitor-request"), "next\n", 'advances through the real monitor handshake');
|
||||
is(read_text("$root/monitor-status"), "0\n", 'completes the monitor exchange');
|
||||
is(read_text("$root/pre-ran"), 'ran', 'executes the downloaded pre-script');
|
||||
is(read_text("$root/post-ran"), 'ran', 'executes the downloaded postscript');
|
||||
is(load_yaml("$root/final-autoinstall.yaml")->{storage}{layout}{name}, 'direct', 'injects the supplied storage layout');
|
||||
is(read_text("$root/etc/resolv.conf"), ($name eq 'unresolved' ? "nameserver 192.0.2.53\n" : "nameserver 127.0.0.1\n") . "domain example.invalid\n", 'writes a numeric resolver or retains DHCP DNS');
|
||||
my $network = load_yaml("$root/target/etc/netplan/00-xcat-install.yaml")->{network};
|
||||
my %interface = (match => {macaddress => '52:54:00:12:34:56'}, dhcp4 => JSON::PP::true, 'dhcp4-overrides' => {'use-domains' => JSON::PP::true});
|
||||
$interface{'set-name'} = $rename if defined $rename;
|
||||
is_deeply($network, {version => 2, ethernets => {'xcat-install' => \%interface}}, 'writes the resolved interface and DHCP policy');
|
||||
is((stat("$root/target/etc/netplan/00-xcat-install.yaml"))[2] & oct('0777'), oct('0600'), 'restricts netplan permissions');
|
||||
is(read_text("$root/target/etc/hostname"), "node\n", 'writes the node hostname');
|
||||
is(read_text("$root/target/etc/hosts"), "127.0.0.1 localhost\n127.0.1.1 node\n", 'replaces or appends the local hostname');
|
||||
ok(-f "$root/target/etc/cloud/cloud-init.disabled", 'disables target cloud-init');
|
||||
is(read_text("$root/grub-input"), "GRUB_TIMEOUT=5\nGRUB_CMDLINE_LINUX=\"console=ttyS0 quiet\"\n", 'passes the quoted kernel command line to update-grub2');
|
||||
ok(!-e "$root/target/etc/apt/sources.list.d/$_", "removes $_ after the postscript") for @temporary_sources;
|
||||
ok(!-e "$root/target/etc/apt/apt.conf.d/94curtin-config", 'removes installer apt settings');
|
||||
is(read_text("$root/target/etc/apt/sources.list.d/admin.list"), "preserved\n", 'retains administrator sources');
|
||||
};
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -1,120 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use Test::More;
|
||||
|
||||
my $tmpl_path = defined $ENV{XCATROOT} ? "$ENV{XCATROOT}/share/xcat/install/ubuntu/compute.subiquity.tmpl" : '';
|
||||
$tmpl_path = "xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl"
|
||||
unless -f $tmpl_path;
|
||||
|
||||
plan skip_all => "compute.subiquity.tmpl not found" unless -f $tmpl_path;
|
||||
|
||||
my $tmpl = do { local $/; open my $fh, '<', $tmpl_path or die $!; <$fh> };
|
||||
|
||||
like($tmpl, qr/^#cloud-config/, 'template starts with #cloud-config');
|
||||
like($tmpl, qr/autoinstall:/, 'template has autoinstall: key');
|
||||
like($tmpl, qr/version:\s*1/, 'template has version: 1');
|
||||
|
||||
like($tmpl, qr/^\s*identity:/m, 'template has an identity section so subiquity does not prompt');
|
||||
like($tmpl, qr/^ - #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL#\n/m, 'the packages list carries the osimage pkglist through the autoinstall token');
|
||||
like($tmpl, qr/^ - openssh-server\n/m, '... and keeps openssh-server, which xCAT needs on the node');
|
||||
like($tmpl, qr/^ - wget\n/m, '... and wget, which the early and late commands use');
|
||||
like($tmpl, qr/kernel:/, 'template has kernel section');
|
||||
like($tmpl, qr/package:\s*linux-generic/, 'template specifies linux-generic kernel');
|
||||
like($tmpl, qr/#UBUNTU_SUBIQUITY_APT_CONFIG#/, 'template renders apt section from osimage context');
|
||||
unlike($tmpl, qr/^\s*apt:/m, 'template does not carry a static apt section');
|
||||
unlike($tmpl, qr/WARN: no partitionfile/, 'template does not silently fall back when xCAT pre-script fails');
|
||||
unlike($tmpl, qr/INSTALL_DISK=""/, 'template does not guess an install disk in early-commands');
|
||||
unlike($tmpl, qr/geoip:\s*true/, 'template does not enable geoip');
|
||||
|
||||
like($tmpl, qr/ssh:/, 'template has ssh section');
|
||||
like($tmpl, qr/install-server:\s*true/, 'template enables ssh install-server');
|
||||
|
||||
unlike($tmpl, qr/package_update:\s*true/, 'template does not enable package_update');
|
||||
unlike($tmpl, qr/^\s*-\s+nfs-common\s*$/m, 'template does not require nfs-common from offline ISO packages');
|
||||
|
||||
# YAML safety: use printf with single-quoted arguments instead of shell-specific
|
||||
# escape sequences. dash does not portably interpret printf \xNN.
|
||||
unlike($tmpl, qr/echo.*GRUB_CMDLINE.*\\"/, 'no escaped double quotes in echo GRUB line');
|
||||
unlike($tmpl, qr/\\\\x22/, 'template does not rely on non-portable printf hex escapes');
|
||||
like($tmpl, qr/printf ''%s\\n'' ''GRUB_CMDLINE_LINUX="#TABLEBLANKOKAY:bootparams:\$NODE:kcmdline#"''/, 'GRUB line uses portable printf quoting');
|
||||
like($tmpl, qr/\/target\/etc\/netplan\/00-xcat-install\.yaml/, 'template writes an xCAT-owned target netplan file');
|
||||
# Regression: an UNSET noderes.installnic must not fatally break Subiquity xnba generation, and
|
||||
# the interface must still be resolved in xCAT's order (installnic -> primarynic -> mac.mac). The
|
||||
# template read #TABLE:noderes:$NODE:installnic#, and Template.pm's tabdb raises "Unable to find
|
||||
# requested field <installnic> from table <noderes>" -> "Failed to generate xnba configurations"
|
||||
# when the node carries no installnic, so the Ubuntu diskful install never started. The resolution
|
||||
# now happens in Perl (xCAT::Template, via xCAT::NetworkUtils::gen_net_boot_params) and reaches the
|
||||
# template already resolved, so no part of that order is re-derived in shell. The resolution itself
|
||||
# and the netplan it produces are covered by ubuntu_subiquity_installnic.t.
|
||||
unlike($tmpl, qr/noderes:\$NODE:(?:installnic|primarynic)/,
|
||||
'the template does not read installnic/primarynic itself (fatal when unset, and it would have to redo the fallback)');
|
||||
like($tmpl, qr/installnic="#SUBIQUITYINSTALLNIC#"/,
|
||||
'the target netplan uses the interface name xCAT resolved for this node');
|
||||
like($tmpl, qr/installmac="#SUBIQUITYINSTALLMAC#"/,
|
||||
'the target netplan uses the MAC address xCAT resolved for this node');
|
||||
like($tmpl, qr/if \[ -z "\$\{installnic\}" \]; then/,
|
||||
'no resolved interface name means match by MAC alone, with no set-name rename');
|
||||
unlike($tmpl, qr/tr ''A-F'' ''a-f''/,
|
||||
'the shell no longer normalizes the MAC (Template.pm resolves mac.mac entries)');
|
||||
unlike($tmpl, qr/cut -d''\|'' -f1/,
|
||||
'the shell no longer splits mac.mac entries (Template.pm resolves them for this node)');
|
||||
like($tmpl, qr/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;/, 'target netplan printf stays on one shell line');
|
||||
like($tmpl, qr/" macaddress: \\"\$\{installmac\}\\""/, 'target netplan matches by MAC address');
|
||||
like($tmpl, qr/" set-name: \$\{installnic\}"/, 'target netplan sets the expected installnic name');
|
||||
like($tmpl, qr/"\s+dhcp4: true"/, 'target netplan enables DHCPv4 on installnic');
|
||||
like($tmpl, qr/"\s+use-domains: true"/, 'target netplan takes the search domain DHCP offers, so short names resolve on the node');
|
||||
like($tmpl, qr/printf ''%s\\n'' ''#HOSTNAME#'' >\/target\/etc\/hostname/, 'template writes target hostname before disabling cloud-init');
|
||||
like($tmpl, qr/sed -i ''s\/\^127\\\.0\\\.1\\\.1\.\*\/127\.0\.1\.1 #HOSTNAME#\/'' \/target\/etc\/hosts/, 'template updates target hosts entry for hostname');
|
||||
like($tmpl, qr/touch \/target\/etc\/cloud\/cloud-init\.disabled/, 'target cloud-init is disabled after target netplan is written');
|
||||
|
||||
# Regression: downloaded files are required and checked with test -s before use.
|
||||
like($tmpl, qr/wget -T 30 -O \/tmp\/getinstdisk http:\/\/#XCATVAR:XCATMASTER#/, 'getinstdisk download is required');
|
||||
like($tmpl, qr/test -s \/tmp\/getinstdisk/, 'getinstdisk checked with -s not -x');
|
||||
like($tmpl, qr/wget -T 30 -O \/tmp\/pre\.sh http:\/\/#XCATVAR:XCATMASTER#/, 'pre.sh download is required');
|
||||
like($tmpl, qr/test -s \/tmp\/pre\.sh/, 'pre.sh checked with -s not -x');
|
||||
like($tmpl, qr/test -s \/tmp\/partitionfile/, 'partitionfile from pre-script is required');
|
||||
unlike($tmpl, qr/wget .*?\|\| true/, 'xCAT control artifact downloads are not masked');
|
||||
unlike($tmpl, qr/if \[ -x \/tmp\/getinstdisk \]/, 'getinstdisk not checked with -x');
|
||||
unlike($tmpl, qr/if \[ -x \/tmp\/pre\.sh \]/, 'pre.sh not checked with -x');
|
||||
|
||||
# Regression: apt configuration is generated by Template.pm so release-specific
|
||||
# Subiquity behavior can be handled without cloning this template per release.
|
||||
unlike($tmpl, qr/noble-|jammy-|focal-/, 'template avoids release-specific apt suite names');
|
||||
like($tmpl, qr/#UBUNTU_SUBIQUITY_APT_CONFIG#/, 'template keeps dynamic apt renderer marker');
|
||||
like($tmpl, qr{2>&1'\n(?:\s*#[^\n]*\n)*\s*- rm -f (?:/target/etc/apt/sources\.list\.d/xcat-(?:otherpkgs|pkgdir)-\*\.(?:list|sources)\s+){4}/target/etc/apt/apt\.conf\.d/94curtin-config$}m,
|
||||
'the installer sources for the otherpkgs repository and the pkgdir mirrors, and the apt configuration curtin wrote, are removed from the target by a late-command of their own, after the post script block');
|
||||
|
||||
# the post script block: its status is the post script's, so a failed post script stops the install
|
||||
{
|
||||
my ($block) = $tmpl =~ /\n - '(\{\n.*?\n \} >>\/target\/var\/log\/xcat\/xcat\.log 2>&1)'\n/s;
|
||||
ok( defined $block, 'the post script block is found' ) or last;
|
||||
$block =~ s/''/'/g;
|
||||
require File::Temp;
|
||||
my $root = File::Temp->newdir();
|
||||
mkdir "$root/bin" or die;
|
||||
for my $tool ( 'curtin', 'wget' ) {
|
||||
open( my $fh, '>', "$root/bin/$tool" ) or die;
|
||||
print {$fh} $tool eq 'curtin' ? "#!/bin/sh\ncase \"\$*\" in *post.script*) exit 42;; esac\nexit 0\n" : "#!/bin/sh\nfor a; do case \"\$a\" in http*) touch \"\${a##*/}\";; esac; done\nexit 0\n";
|
||||
close $fh; chmod 0755, "$root/bin/$tool";
|
||||
}
|
||||
( my $script = $block ) =~ s{/target}{$root/target}g;
|
||||
$script =~ s{/tmp/pre-install\.log}{$root/pre-install.log}g;
|
||||
for my $token ( [ '#SUBIQUITYINSTALLNIC#', '' ], [ '#SUBIQUITYINSTALLMAC#', '52:54:00:00:00:01' ], [ '#HOSTNAME#', 'cn1' ], [ '#XCATVAR:XCATMASTER#', '192.0.2.10' ],
|
||||
[ '#COLONHTTPPORT#', '' ], [ '#TABLEBLANKOKAY:bootparams:$NODE:kcmdline#', '' ] ) {
|
||||
$script =~ s/\Q$token->[0]\E/$token->[1]/g;
|
||||
}
|
||||
require File::Path;
|
||||
File::Path::make_path( map { "$root/target/$_" } qw(etc/default root var/log/xcat) );
|
||||
open( my $hosts, '>', "$root/target/etc/hosts" ) or die; print {$hosts} "127.0.0.1 localhost\n"; close $hosts;
|
||||
open( my $pre, '>', "$root/pre-install.log" ) or die; close $pre;
|
||||
my $cwd = File::Spec->rel2abs('.');
|
||||
chdir $root or die;
|
||||
local $ENV{PATH} = "$root/bin:$ENV{PATH}";
|
||||
system( 'sh', '-c', $script );
|
||||
my $status = $? >> 8;
|
||||
chdir $cwd or die;
|
||||
is( $status, 42, 'a failing post script fails the late-command block, so Subiquity stops the install instead of switching the node to disk boot' );
|
||||
}
|
||||
|
||||
done_testing();
|
||||
Reference in New Issue
Block a user