diff --git a/.github/workflows/xcat_test.yml b/.github/workflows/xcat_test.yml index 89f910c64..ebd003245 100644 --- a/.github/workflows/xcat_test.yml +++ b/.github/workflows/xcat_test.yml @@ -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 diff --git a/xCAT-test/fixtures/subiquity/command b/xCAT-test/fixtures/subiquity/command new file mode 100644 index 000000000..e294478db --- /dev/null +++ b/xCAT-test/fixtures/subiquity/command @@ -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 diff --git a/xCAT-test/fixtures/subiquity/run.pl b/xCAT-test/fixtures/subiquity/run.pl new file mode 100644 index 000000000..9d815202c --- /dev/null +++ b/xCAT-test/fixtures/subiquity/run.pl @@ -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; diff --git a/xCAT-test/native/ubuntu_subiquity_template.t b/xCAT-test/native/ubuntu_subiquity_template.t new file mode 100644 index 000000000..b5b19978d --- /dev/null +++ b/xCAT-test/native/ubuntu_subiquity_template.t @@ -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(); diff --git a/xCAT-test/unit/ubuntu_subiquity_template.t b/xCAT-test/unit/ubuntu_subiquity_template.t deleted file mode 100644 index 466e8ae21..000000000 --- a/xCAT-test/unit/ubuntu_subiquity_template.t +++ /dev/null @@ -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 from table " -> "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();