mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-10-20 22:55:40 +00:00
Merge pull request #7923 from VersatusHPC/test/r26-profile-rendering
test(provisioning): exercise rendered profiles and installer commands
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
|
||||
@@ -83,6 +83,15 @@ jobs:
|
||||
echo 'a mock result directory test was skipped' >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: Run profile artifact tests
|
||||
if: ${{ !cancelled() }}
|
||||
working-directory: ${{ runner.temp }}/xcat-core-unitsrc
|
||||
shell: bash
|
||||
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 xCAT-test/native/ubuntu_subiquity_template.t | tee "$log"
|
||||
! grep -qi skipped "$log"
|
||||
rpm_lifecycle_test:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ language:
|
||||
| Perl unit tests | `xCAT-test/unit/*.t` | `prove -r xCAT-test/unit` |
|
||||
| Shell unit tests | `xCAT-test/bats/*.bats` | `bats -r xCAT-test/bats` |
|
||||
| Black-box service tests | `xCAT-test/blackboxtest/` | `python3 -m unittest discover -s tests`, then `blackboxtest run <conf>` on a management node |
|
||||
| Native host tests | `xCAT-test/native/*.t`, `xCAT-test/native/*.bats` | Explicit lab runs; see [native/README.md](native/README.md) |
|
||||
| Native host tests | `xCAT-test/native/*.t`, `xCAT-test/native/*.bats` | Selected CI cases and explicit lab runs; see [native/README.md](native/README.md) |
|
||||
| CLI functional tests | `xCAT-test/autotest/testcase/` and `xCAT-test/autotest/bundle/` | `xcattest -f <cluster.conf> -t <case>` or `xcattest -f <cluster.conf> -b <bundle>` |
|
||||
|
||||
Use Perl `.t` tests for Perl modules, Perl scripts, templates, and repository
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/bin/sh
|
||||
|
||||
case ${0##*/} in
|
||||
curl)
|
||||
output=
|
||||
url=
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case $1 in
|
||||
--fail) shift ;;
|
||||
--retry|--max-time) shift 2 ;;
|
||||
-o) output=$2; shift 2 ;;
|
||||
http://*) url=$1; shift ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
if [ "$#" -eq 0 ]; then
|
||||
case $url in
|
||||
*/postscripts/)
|
||||
printf '<a href="xcatlib.sh">xcatlib.sh</a>\n'
|
||||
exit 0 ;;
|
||||
*/postscripts/xcatlib.sh)
|
||||
exec cp /fixture/xcatlib.sh "$output" ;;
|
||||
*/mypostscripts/mypostscript.node)
|
||||
exec cp /fixture/mypostscript.node "$output" ;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
logger|updateflag.awk|nmcli) exit 0 ;;
|
||||
sleep)
|
||||
case $* in
|
||||
''|*[!0-9]*) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
printf '%s %s\n' "${0##*/}" "$*" >> /fixture/unexpected-command
|
||||
exit 64
|
||||
@@ -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;
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
These tests use the source checkout with RPM tools, Linux namespaces or real
|
||||
service binaries. They are separate from `unit/` and are not discovered by the
|
||||
default pull-request unit run. Run them explicitly on a disposable Linux host.
|
||||
unit runner. CI selects specific cases explicitly; run the others on a
|
||||
disposable Linux host.
|
||||
|
||||
The postinstall case requires bubblewrap and the Perl modules used by the xCAT
|
||||
template renderer. It runs complete rendered installer and postboot scripts in
|
||||
@@ -282,3 +283,36 @@ The unit test retains checks of the vendored release key, requested kernel
|
||||
fragments and console service configuration. The BATS tests retain runtime,
|
||||
console, hardware and packet behavior. None of these checks replaces a full
|
||||
image build, inspection of its resolved kernel configuration or a boot test.
|
||||
|
||||
## Profile artifacts
|
||||
|
||||
These cases render install profiles, run netboot postscripts, build a Debian
|
||||
repository, and execute rendered Subiquity commands. Run them as an ordinary
|
||||
user with Bubblewrap and unprivileged user namespaces enabled:
|
||||
|
||||
```
|
||||
prove 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
|
||||
```
|
||||
|
||||
The renderers need the system Perl with the xCAT dependencies, XML::Simple,
|
||||
DBD::SQLite, Capture::Tiny, and File::Slurper. The sandbox clears the environment
|
||||
and does not mount perlbrew or local::lib directories. The Subiquity case also
|
||||
needs /usr/bin/python3 with PyYAML; on EL8, install python36 for that path.
|
||||
The Debian case needs debhelper, devscripts, fakeroot, and reprepro, and skips
|
||||
on non-Debian hosts. Each case creates its own temporary files and database.
|
||||
The temporary directory must allow execution; set TMPDIR if /tmp is noexec.
|
||||
Sandbox commands require GNU timeout and have a 45-second limit.
|
||||
Pull-request CI runs these cases and rejects whole-file skips. The permissive
|
||||
SELinux case records an existing postinstall defect as TODO, without changing
|
||||
production behavior.
|
||||
|
||||
The netboot case executes the selected postinstall file directly. Its private
|
||||
/bin/sh uses Bash, as on EL, including when the test host uses another shell.
|
||||
|
||||
The Subiquity case replaces downloads and external commands, and uses a
|
||||
loopback install-monitor peer. It does not install an operating system.
|
||||
Architecture names in these fixtures do not qualify native firmware or boot
|
||||
behavior on those architectures.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use File::Path qw(make_path);
|
||||
use File::Slurper qw(read_text write_text);
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use Test::More;
|
||||
use XCAT::Test::File qw(repo_path);
|
||||
use XCAT::Test::Process qw(run_command);
|
||||
use XCAT::Test::Sandbox qw(sandbox_root sandbox_run);
|
||||
|
||||
plan skip_all => 'requires a Debian-family package builder'
|
||||
unless $^O eq 'linux' && -f '/etc/debian_version';
|
||||
require Dpkg::Control::Info;
|
||||
|
||||
my $build = tempdir(CLEANUP => 1);
|
||||
make_path("$build/source", "$build/home");
|
||||
for my $entry (qw(builddebs.pl build-utils Version Release xCAT-buildkit)) {
|
||||
system('cp', '-a', repo_path($entry), "$build/source/") == 0 or die "Cannot copy $entry";
|
||||
}
|
||||
{
|
||||
local %ENV = (%ENV, HOME => "$build/home", SOURCE_DATE_EPOCH => 1750000000,
|
||||
PATH => '/usr/bin:/bin', LC_ALL => 'C');
|
||||
my ($status, $output) = run_command($^X, "$build/source/builddebs.pl",
|
||||
'--package', 'xCAT-buildkit', '--dist', 'noble', '--release', 'test1',
|
||||
'--dest', "$build/output");
|
||||
is($status, 0, 'the real Debian builder creates a package and repository') or diag($output);
|
||||
die 'the builder did not complete' if $status;
|
||||
}
|
||||
my $repository = "$build/output/xcat-core";
|
||||
ok(-x "$repository/mklocalrepo.sh", 'the builder delivers an executable repository helper');
|
||||
ok(-f "$repository/dists/noble/Release", 'the helper accompanies a published repository');
|
||||
|
||||
for my $case (['riscv64', 'riscv64'], ['ppc64le', 'ppc64el'], ['x86_64', 'amd64']) {
|
||||
my ($machine, $architecture) = @$case;
|
||||
for my $release (qw(noble jammy)) {
|
||||
subtest "$machine $release" => sub {
|
||||
my $root = sandbox_root();
|
||||
make_path("$root/etc/apt/sources.list.d");
|
||||
write_text("$root/etc/lsb-release", "DISTRIB_CODENAME=$release\n");
|
||||
write_text("$root/bin/uname", "#!/bin/sh\n[ \"\$*\" = -m ] || exit 2\nprintf '%s\\n' '$machine'\n");
|
||||
chmod 0755, "$root/bin/uname" or die $!;
|
||||
write_text("$root/etc/apt/sources.list.d/administrator.list", "preserved\n");
|
||||
for my $pass (1, 2) {
|
||||
my ($status, $output) = sandbox_run($root,
|
||||
{read_only => {$repository => '/repo'}}, '/repo/mklocalrepo.sh');
|
||||
is($status, 0, "pass $pass executes the generated helper unchanged") or diag($output);
|
||||
is(read_text("$root/etc/apt/sources.list.d/xcat-core.list"),
|
||||
"deb [arch=$architecture] file:///repo $release main\n",
|
||||
"pass $pass selects the host architecture and distribution");
|
||||
is(read_text("$root/etc/apt/sources.list.d/administrator.list"), "preserved\n",
|
||||
"pass $pass preserves unrelated sources");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for my $case (['xCAT', 'xcat'], ['xCATsn', 'xcatsn']) {
|
||||
my ($component, $package) = @$case;
|
||||
my $control = Dpkg::Control::Info->new(repo_path("$component/debian/control"));
|
||||
my ($binary) = grep { $_->{Package} eq $package } $control->get_packages();
|
||||
ok($binary, "$package has a binary package stanza");
|
||||
my %architecture = map { $_ => 1 } split /\s+/, $binary->{Architecture} // '';
|
||||
ok($architecture{$_}, "$package supports $_") for qw(riscv64 amd64 ppc64el);
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path);
|
||||
use File::Slurper qw(read_text write_text);
|
||||
use File::Spec;
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../perl-xCAT", "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
use Test::More;
|
||||
use Text::ParseWords qw(shellwords);
|
||||
|
||||
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 => 'profile execution requires Linux' unless $^O eq 'linux';
|
||||
my $database = template_database();
|
||||
require xCAT::SvrUtils;
|
||||
my $password = '$6$fixture$encrypted';
|
||||
set_row('passwd', {key => 'system', username => 'root'}, {password => $password});
|
||||
set_row('site', {key => 'secureroot'}, {value => 0});
|
||||
|
||||
my $install = File::Spec->catdir( 'xCAT-server', 'share', 'xcat', 'install' );
|
||||
my @rendered_profiles;
|
||||
|
||||
sub installer_settings {
|
||||
my ($text) = @_;
|
||||
$text =~ s/^%(?:packages|pre|post|addon)\b.*?^%end[^\n]*\n?//msg;
|
||||
return [map { [shellwords($_)] } grep { /\S/ && !/^\s*#/ } split /\n/, $text];
|
||||
}
|
||||
|
||||
sub package_set {
|
||||
my ($text) = @_;
|
||||
my ($packages) = $text =~ /^%packages[^\n]*\n(.*?)^%end/ms;
|
||||
return {map { $_ => 1 } grep { length && !/^#/ } split /\n/, $packages // ''};
|
||||
}
|
||||
|
||||
for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) {
|
||||
my ( $dir, $osbase ) = @$family;
|
||||
for my $profile (qw(compute service)) {
|
||||
subtest "$osbase $profile" => sub {
|
||||
my $base = repo_path("$install/$dir");
|
||||
my $tmpl = xCAT::SvrUtils->get_tmpl_file_name($base, $profile, "$osbase.2", 'riscv64');
|
||||
is($tmpl, "$base/$profile.$osbase.riscv64.tmpl", 'selects the architecture-specific template');
|
||||
my $pkglist = xCAT::SvrUtils->get_pkglist_file_name($base, $profile, "$osbase.2", 'riscv64');
|
||||
is($pkglist, "$base/$profile.$osbase.riscv64.pkglist", 'selects the architecture-specific package list');
|
||||
set_row('nodetype', {node => 'node'}, {os => "$osbase.2", arch => 'riscv64', provmethod => 'install'});
|
||||
my $output = "$database/$osbase-$profile.ks";
|
||||
render_template($database, $tmpl, $output, 'node',
|
||||
$pkglist, '/install/media', $dir, undef,
|
||||
{xcatmaster => '192.0.2.1'});
|
||||
my $rendered = read_text($output);
|
||||
my $shared_output = "$database/$osbase-$profile-shared.ks";
|
||||
render_template($database, "$base/$profile.$osbase.tmpl", $shared_output, 'node',
|
||||
"$base/$profile.$osbase.pkglist", '/install/media', $dir, undef,
|
||||
{xcatmaster => '192.0.2.1'});
|
||||
my $shared = read_text($shared_output);
|
||||
is_deeply(installer_settings($rendered), installer_settings($shared),
|
||||
'preserves the shared installer settings');
|
||||
my $expected_packages = package_set($shared);
|
||||
$expected_packages->{$_} = 1 for qw(grub2-efi-riscv64 efibootmgr);
|
||||
is_deeply(package_set($rendered), $expected_packages,
|
||||
'adds the RISC-V boot packages to the complete shared package set');
|
||||
my @pre = $rendered =~ /^%pre[^\n]*\n(.*?)^%end/msg;
|
||||
my @shared_pre = $shared =~ /^%pre[^\n]*\n(.*?)^%end/msg;
|
||||
is_deeply(\@pre, \@shared_pre, 'delivers the shared pre-install artifact unchanged');
|
||||
unlike($rendered, qr/#(?:INCLUDE|TABLE|CRYPT|ENV|XCATVAR)[^\n]*#/, 'resolves template inputs');
|
||||
like($rendered, qr/^rootpw --iscrypted \Q$password\E$/m, 'renders the configured password');
|
||||
like($rendered, qr/^timezone UTC --utc$/m, 'renders the site timezone');
|
||||
my ($options, $packages) = $rendered =~ /^%packages([^\n]*)\n(.*?)^%end/ms;
|
||||
like($options // '', qr/(?:^|\s)--ignoremissing(?:\s|$)/, 'tolerates unavailable installer packages');
|
||||
my %packages = map { $_ => 1 } grep { length && !/^#/ } split /\n/, $packages // '';
|
||||
ok($packages{'grub2-efi-riscv64'} && $packages{efibootmgr}, 'includes RISC-V boot packages');
|
||||
ok(!$packages{'grub2-efi-x64'} && !$packages{'shim-x64'} && !$packages{'grub2-efi-aa64'}, 'excludes foreign boot packages');
|
||||
like($rendered, qr/^%addon com_redhat_kdump --disable\n%end$/m, 'disables the unsupported default crash reservation');
|
||||
my @posts;
|
||||
my @all_posts;
|
||||
while ($rendered =~ /^%post([^\n]*)\n(.*?)^%end/msg) {
|
||||
my ($flags, $body) = ($1, $2);
|
||||
push @all_posts, [$flags, $body];
|
||||
push @posts, [$flags, $body] if $flags =~ /(?:^|\s)--erroronfail(?:\s|$)/;
|
||||
}
|
||||
push @rendered_profiles, ["$osbase $profile", \@all_posts];
|
||||
is(scalar @posts, 1, 'has one fatal boot-loader check');
|
||||
return unless @posts == 1;
|
||||
my ($interpreter) = $posts[0][0] =~ /--interpreter=(\S+)/;
|
||||
is($interpreter, '/bin/bash', 'runs the check with the requested interpreter');
|
||||
for my $loader ('BOOT/BOOTRISCV64.EFI', 'rocky/grubriscv64.efi', 'rocky/grubx64.efi', '') {
|
||||
my $root = sandbox_root();
|
||||
make_path("$root/boot/efi/EFI/BOOT", "$root/boot/efi/EFI/rocky", "$root/log/xcat");
|
||||
write_text("$root/boot/efi/EFI/$loader", "loader\n") if $loader;
|
||||
write_text("$root/check.sh", $posts[0][1]);
|
||||
my ($rc, $log) = sandbox_run($root, $interpreter, '/fixture/check.sh');
|
||||
my $valid = $loader && $loader ne 'rocky/grubx64.efi';
|
||||
is($rc, $valid ? 0 : 1, $valid ? "accepts $loader" : "rejects an ESP without a RISC-V loader ($loader)") or diag($log);
|
||||
if (!$valid) {
|
||||
like(read_text("$root/log/xcat/xcat.log"), qr/no grub2 UEFI boot loader/, 'explains the missing loader');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
my $post = File::Spec->catfile( $install, 'scripts', 'post.rhels10.riscv64' );
|
||||
my $post_path = repo_path($post);
|
||||
ok( -r $post_path, 'post.rhels10.riscv64 exists' );
|
||||
|
||||
is(system('sh', '-n', $post_path), 0, 'post.rhels10.riscv64 parses as POSIX shell');
|
||||
|
||||
my $tools = tempdir(CLEANUP => 1);
|
||||
my $efi_log = File::Spec->catfile($tools, 'efibootmgr.log');
|
||||
|
||||
sub fake_command {
|
||||
my ($name, $body) = @_;
|
||||
my $path = File::Spec->catfile($tools, $name);
|
||||
write_text($path, "#!/bin/sh\n$body");
|
||||
chmod 0755, $path or die "Unable to make $path executable: $!";
|
||||
return $path;
|
||||
}
|
||||
|
||||
my $uname = fake_command('uname', <<'SH');
|
||||
printf '%s\n' "${XCAT_TEST_ARCH:-riscv64}"
|
||||
SH
|
||||
my $findmnt = fake_command('findmnt', <<'SH');
|
||||
printf '/dev/vda1\n'
|
||||
SH
|
||||
my $lsblk = fake_command('lsblk', <<'SH');
|
||||
case "$*" in
|
||||
*PKNAME*) printf "vda\n" ;;
|
||||
*PARTN*) printf "1\n" ;;
|
||||
esac
|
||||
SH
|
||||
my $efibootmgr = fake_command('efibootmgr', <<'SH');
|
||||
printf '%s\n' "$*" >> "$XCAT_EFI_LOG"
|
||||
case "$*" in
|
||||
'') exit 0 ;;
|
||||
'-v')
|
||||
printf 'Boot0001* Rocky HD(1,GPT,...)/File(\\EFI\\rocky\\shimx64.efi)\n'
|
||||
printf 'Boot0002* OldRiscv HD(1,GPT,...)/File(\\EFI\\rocky\\grubriscv64.efi)\n'
|
||||
printf 'Boot0003* Network PXE\n'
|
||||
exit 0
|
||||
;;
|
||||
*'-c'*) exit "${XCAT_EFI_CREATE_RC:-0}" ;;
|
||||
esac
|
||||
exit 0
|
||||
SH
|
||||
|
||||
sub installed_root {
|
||||
my ($with_loader, $root) = @_;
|
||||
$root //= tempdir(CLEANUP => 1);
|
||||
make_path(File::Spec->catdir($root, 'boot', 'efi', 'EFI', 'rocky'));
|
||||
make_path(File::Spec->catdir($root, 'etc'));
|
||||
write_text(File::Spec->catfile($root, 'etc', 'os-release'), "NAME=\"Rocky Linux\"\n");
|
||||
if ($with_loader) {
|
||||
write_text(
|
||||
File::Spec->catfile($root, 'boot', 'efi', 'EFI', 'rocky', 'grubriscv64.efi'),
|
||||
"riscv loader\n",
|
||||
);
|
||||
}
|
||||
return $root;
|
||||
}
|
||||
|
||||
for my $rendered (@rendered_profiles) {
|
||||
my ($label, $posts) = @$rendered;
|
||||
subtest "$label post-install sequence" => sub {
|
||||
my $root = installed_root(1, sandbox_root());
|
||||
make_path("$root/etc/systemd/system/multi-user.target.wants", "$root/etc/yum.repos.d");
|
||||
write_text("$root/etc/yum.repos.d/rocky.repo", "[baseos]\nname=BaseOS\nenabled=1\n");
|
||||
write_text("$root/tmp/pre-install.log", "pre-install fixture diagnostic\n");
|
||||
copy(repo_path('xCAT/postscripts/xcatlib.sh'), "$root/xcatlib.sh") or die $!;
|
||||
write_text("$root/mypostscript.node", "#!/bin/bash\nMASTER=192.0.2.1\ntouch /fixture/postscript-ran\n");
|
||||
for my $name (qw(uname findmnt lsblk efibootmgr)) {
|
||||
copy("$tools/$name", "$root/bin/$name") or die $!;
|
||||
chmod 0755, "$root/bin/$name" or die $!;
|
||||
}
|
||||
for my $name (qw(curl logger sleep updateflag.awk nmcli openssl)) {
|
||||
copy(repo_path('xCAT-test/fixtures/install-profile/command'), "$root/bin/$name") or die $!;
|
||||
chmod 0755, "$root/bin/$name" or die $!;
|
||||
}
|
||||
my $index = 0;
|
||||
for my $post (@$posts) {
|
||||
my ($interpreter) = $post->[0] =~ /--interpreter=(\S+)/;
|
||||
$interpreter //= '/bin/sh';
|
||||
write_text("$root/post.sh", $post->[1]);
|
||||
my ($rc, $log) = sandbox_run($root, {
|
||||
env => {XCAT_EFI_LOG => '/fixture/efi.log'},
|
||||
}, 'timeout', '30', $interpreter, '/fixture/post.sh');
|
||||
is($rc, 0, 'post block ' . ++$index . ' completes')
|
||||
or diag($log, read_text("$root/log/xcat/xcat.log"));
|
||||
}
|
||||
my $fallback = "$root/boot/efi/EFI/BOOT/BOOTRISCV64.EFI";
|
||||
is(-f $fallback ? read_text($fallback) : '', "riscv loader\n",
|
||||
'the rendered profile installs the fallback EFI loader');
|
||||
my $calls = -f "$root/efi.log" ? read_text("$root/efi.log") : '';
|
||||
like($calls, qr/^-q -c -d \/dev\/vda -p 1 -L Rocky Linux -l \\EFI\\rocky\\grubriscv64\.efi$/m,
|
||||
'the rendered profile registers the vendor EFI loader');
|
||||
ok(!-e "$root/unexpected-command", 'external fixtures accept the requested operations');
|
||||
ok(-e "$root/postscript-ran", 'runs the downloaded postscript from the rendered profile');
|
||||
is(read_text("$root/etc/yum.repos.d/rocky.repo"), "[baseos]\nname=BaseOS\nenabled=0\n",
|
||||
'disables the installed vendor repository');
|
||||
like(read_text("$root/log/xcat/xcat.log"), qr/^pre-install fixture diagnostic$/m,
|
||||
'preserves the pre-install diagnostics in the deployment log');
|
||||
};
|
||||
}
|
||||
|
||||
sub run_post {
|
||||
my ($root, %extra) = @_;
|
||||
local %ENV = (
|
||||
%ENV,
|
||||
XCAT_INSTALL_ROOT => $root,
|
||||
XCAT_UNAME => $uname,
|
||||
XCAT_EFIBOOTMGR => $efibootmgr,
|
||||
XCAT_FINDMNT => $findmnt,
|
||||
XCAT_LSBLK => $lsblk,
|
||||
XCAT_EFI_LOG => $efi_log,
|
||||
XCAT_TEST_ARCH => 'riscv64',
|
||||
%extra,
|
||||
);
|
||||
open(my $fh, '-|', 'sh', $post_path) or die "Unable to run $post_path: $!";
|
||||
my $output = do { local $/; <$fh> };
|
||||
close($fh);
|
||||
return ($? >> 8, $output);
|
||||
}
|
||||
|
||||
my $root = installed_root(1);
|
||||
my ($status, $output) = run_post($root);
|
||||
is($status, 0, 'the RISC-V fix-up completes successfully');
|
||||
is(
|
||||
read_text(File::Spec->catfile($root, 'boot', 'efi', 'EFI', 'BOOT', 'BOOTRISCV64.EFI')),
|
||||
"riscv loader\n",
|
||||
'the distro loader is copied to the removable-media fallback path',
|
||||
);
|
||||
like($output, qr/UEFI boot entry "Rocky Linux"/, 'the created UEFI entry is reported');
|
||||
my $efi_calls = read_text($efi_log);
|
||||
like($efi_calls, qr/^-q -b 0001 -B$/m, 'the invalid x86 boot entry is removed');
|
||||
like($efi_calls, qr/^-q -b 0002 -B$/m, 'the prior RISC-V boot entry is removed');
|
||||
unlike($efi_calls, qr/0003/, 'unrelated firmware entries are retained');
|
||||
like(
|
||||
$efi_calls,
|
||||
qr/^-q -c -d \/dev\/vda -p 1 -L Rocky Linux -l \\EFI\\rocky\\grubriscv64\.efi$/m,
|
||||
'the new entry points at the distro RISC-V loader on the ESP disk',
|
||||
);
|
||||
|
||||
unlink($efi_log);
|
||||
my $x86_root = installed_root(1);
|
||||
($status, $output) = run_post($x86_root, XCAT_TEST_ARCH => 'x86_64');
|
||||
is($status, 0, 'the post-install script is a no-op on x86_64');
|
||||
is($output, '', 'the x86_64 no-op reports nothing');
|
||||
ok(!-e $efi_log, 'the x86_64 no-op never invokes efibootmgr');
|
||||
|
||||
my $bare_root = installed_root(0);
|
||||
($status, $output) = run_post($bare_root);
|
||||
is($status, 0, 'a missing distro loader remains nonfatal');
|
||||
like($output, qr/no \\EFI\\\*\\grubriscv64\.efi/, 'a missing distro loader is diagnosed');
|
||||
|
||||
unlink($efi_log);
|
||||
($status, $output) = run_post($root, XCAT_EFI_CREATE_RC => 1);
|
||||
is($status, 0, 'an efibootmgr create failure remains nonfatal');
|
||||
like($output, qr/firmware will use \\EFI\\BOOT\\BOOTRISCV64\.EFI/, 'the fallback path is reported after an efibootmgr failure');
|
||||
|
||||
done_testing();
|
||||
+45
-12
@@ -8,15 +8,18 @@ use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
use File::Spec;
|
||||
use File::Path qw(make_path);
|
||||
use File::Slurper qw(read_text write_text);
|
||||
use File::Temp qw(tempdir);
|
||||
use Test::More;
|
||||
|
||||
use XCAT::Test::File qw(repo_path slurp_repo_file);
|
||||
use xCAT::SvrUtils;
|
||||
|
||||
# EL10 riscv64 diskless and service profiles are plain data files resolved by
|
||||
# imgutils::get_profile_def_filename (osver.arch first, then osbase.arch, then
|
||||
# the arch-less fallbacks). Pin that the riscv64 files exist, win the lookup
|
||||
# for rocky10/rhels10 point releases, and carry the right content.
|
||||
use XCAT::Test::Sandbox qw(sandbox_root sandbox_run);
|
||||
plan skip_all => 'postinstall execution requires Linux' unless $^O eq 'linux';
|
||||
my $database = tempdir(CLEANUP => 1);
|
||||
$ENV{XCATROOT} = repo_path('xCAT-server');
|
||||
$ENV{XCATCFG} = "SQLite:$database";
|
||||
require xCAT::SvrUtils;
|
||||
|
||||
my $share_relative = File::Spec->catdir( 'xCAT-server', 'share', 'xcat' );
|
||||
my $share = repo_path($share_relative);
|
||||
@@ -24,7 +27,6 @@ my $imgutils_relative = File::Spec->catfile(
|
||||
$share_relative, 'netboot', 'imgutils', 'imgutils.pm'
|
||||
);
|
||||
my $imgutils = repo_path($imgutils_relative);
|
||||
plan skip_all => "$imgutils not found" unless -r $imgutils;
|
||||
require $imgutils;
|
||||
|
||||
my @families = (
|
||||
@@ -64,11 +66,42 @@ for my $family (@families) {
|
||||
unlike( $exlist, qr{^\./lib/kdb/}m, "$dir/$profile.$osbase riscv64 exlist has no kdb typo" );
|
||||
is( scalar( () = $exlist =~ m{^\./usr/share/man\*$}mg ), 1, "$dir/$profile.$osbase riscv64 exlist lists usr/share/man once" );
|
||||
|
||||
my $postinstall = slurp_repo_file(
|
||||
File::Spec->catfile( $base_relative, "$profile.$osbase.riscv64.postinstall" )
|
||||
);
|
||||
like( $postinstall, qr/^#!\/bin\/sh/, "$dir/$profile.$osbase riscv64 postinstall is a shell script" );
|
||||
like( $postinstall, qr/SELINUX=disabled/, "$dir/$profile.$osbase riscv64 postinstall disables SELinux in the image" );
|
||||
my $postinstall = imgutils::get_profile_def_filename($osver, $profile, 'riscv64', $base, 'postinstall');
|
||||
for my $mode (qw(enforcing permissive disabled absent)) {
|
||||
subtest "$osver $profile SELinux $mode" => sub {
|
||||
my $root = sandbox_root();
|
||||
my $image = "$root/target";
|
||||
make_path("$image/etc/selinux", "$root/etc/selinux");
|
||||
write_text("$root/etc/fstab", "host fstab\n");
|
||||
write_text("$root/etc/selinux/config", "SELINUX=enforcing\n# host policy\n");
|
||||
write_text("$image/etc/fstab", "obsolete\n");
|
||||
write_text("$image/etc/selinux/config", "SELINUX=$mode\nSELINUXTYPE=targeted\n")
|
||||
unless $mode eq 'absent';
|
||||
for my $pass (1, 2) {
|
||||
# EL uses Bash as /bin/sh for these postinstall scripts.
|
||||
my ($status, $output) = sandbox_run($root,
|
||||
{read_only => {repo_path('.') => repo_path('.'), '/bin/bash' => '/bin/sh'}},
|
||||
$postinstall, '/target', $osver, 'riscv64', $profile);
|
||||
is($status, 0, "pass $pass executes the selected script") or diag($output);
|
||||
is(read_text("$root/etc/fstab"), "host fstab\n", "pass $pass leaves the host fstab unchanged");
|
||||
is(read_text("$root/etc/selinux/config"), "SELINUX=enforcing\n# host policy\n", "pass $pass leaves host SELinux unchanged");
|
||||
if ($mode eq 'absent') {
|
||||
ok(!-e "$image/etc/selinux/config", "pass $pass leaves absent SELinux configuration absent");
|
||||
} else {
|
||||
local $TODO = 'The existing postinstall duplicates SELINUX= for permissive mode'
|
||||
if $mode eq 'permissive';
|
||||
is(read_text("$image/etc/selinux/config"), "SELINUX=disabled\nSELINUXTYPE=targeted\n",
|
||||
"pass $pass disables SELinux without changing the policy type");
|
||||
}
|
||||
my @mounts = map { [split /\s+/] } grep { /\S/ } split /\n/, read_text("$image/etc/fstab");
|
||||
is_deeply(\@mounts, [
|
||||
[qw(proc /proc proc rw 0 0)],
|
||||
[qw(sysfs /sys sysfs rw 0 0)],
|
||||
['devpts', '/dev/pts', 'devpts', 'rw,gid=5,mode=620', 0, 0],
|
||||
], "pass $pass writes the diskless virtual filesystems once");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
my $otherpkgs = File::Spec->catfile( $base, "service.$osbase.riscv64.otherpkgs.pkglist" );
|
||||
@@ -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,88 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Spec;
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin;
|
||||
use Test::More;
|
||||
|
||||
# A riscv64 management node needs an xcat and xcatsn deb built for the architecture and a
|
||||
# mklocalrepo.sh that points the host at the matching repository instead of amd64.
|
||||
#
|
||||
# The generated script is extracted from builddebs.pl and run with a stub uname ahead of
|
||||
# $PATH, so the mapping under test is the shipped code. Only the path it writes is
|
||||
# redirected into the sandbox, because it writes an apt source list.
|
||||
|
||||
my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) );
|
||||
my $builder = File::Spec->catfile( $repo_root, 'builddebs.pl' );
|
||||
plan skip_all => "builddebs.pl not found" unless -f $builder;
|
||||
|
||||
my $src = do { local $/; open my $fh, '<', $builder or die $!; <$fh> };
|
||||
|
||||
# BAIL_OUT rather than skip: a rename that stops this matching must fail loudly instead of
|
||||
# silently covering nothing.
|
||||
my ($script) = $src =~ /write_script\("\$repodir\/mklocalrepo\.sh", <<'SCRIPT'\);\n(.*?)\nSCRIPT\n/ms;
|
||||
BAIL_OUT('could not extract mklocalrepo.sh from builddebs.pl') unless defined $script;
|
||||
|
||||
my $dir = tempdir( CLEANUP => 1 );
|
||||
my $run = 0;
|
||||
|
||||
# Run the generated script for one host architecture and return the apt source line it wrote.
|
||||
sub sources_line_for {
|
||||
my ($uname) = @_;
|
||||
$run++;
|
||||
my $root = File::Spec->catdir( $dir, "run$run" );
|
||||
mkdir $root;
|
||||
mkdir "$root/bin";
|
||||
|
||||
open( my $stub, '>', "$root/bin/uname" ) or die $!;
|
||||
print {$stub} "#!/bin/bash\necho $uname\n";
|
||||
close($stub);
|
||||
chmod 0755, "$root/bin/uname";
|
||||
|
||||
my $release = File::Spec->catfile( $root, 'lsb-release' );
|
||||
open( my $rel, '>', $release ) or die $!;
|
||||
print {$rel} "DISTRIB_CODENAME=noble\n";
|
||||
close($rel);
|
||||
|
||||
my $listed = File::Spec->catfile( $root, 'sources.list' );
|
||||
( my $sandboxed = $script ) =~ s{/etc/lsb-release}{$release};
|
||||
$sandboxed =~ s{/etc/apt/sources\.list\.d/\S+}{$listed};
|
||||
|
||||
my $harness = File::Spec->catfile( $root, 'harness.sh' );
|
||||
open( my $fh, '>', $harness ) or die $!;
|
||||
print {$fh} "#!/bin/bash\n$sandboxed\n";
|
||||
close($fh);
|
||||
|
||||
local $ENV{PATH} = "$root/bin:$ENV{PATH}";
|
||||
system( '/bin/bash', $harness );
|
||||
open( my $out, '<', $listed ) or die $!;
|
||||
my $line = do { local $/; <$out> };
|
||||
close($out);
|
||||
return $line;
|
||||
}
|
||||
|
||||
for my $case (
|
||||
[ 'riscv64', 'riscv64' ],
|
||||
[ 'ppc64le', 'ppc64el' ],
|
||||
[ 'x86_64', 'amd64' ],
|
||||
)
|
||||
{
|
||||
my ( $uname, $want ) = @$case;
|
||||
like( sources_line_for($uname), qr/^deb \[arch=\Q$want\E\] /,
|
||||
"mklocalrepo.sh gives a $uname host the $want repository" );
|
||||
}
|
||||
|
||||
# The debs themselves must exist for the architecture.
|
||||
for my $case ( [ 'xCAT', 'xcat' ], [ 'xCATsn', 'xcatsn' ] ) {
|
||||
my ( $component, $package ) = @$case;
|
||||
my $control = File::Spec->catfile( $repo_root, $component, 'debian', 'control' );
|
||||
my $text = do { local $/; open my $fh, '<', $control or die $!; <$fh> };
|
||||
my ($arches) = $text =~ /^Package: \Q$package\E\nArchitecture: (.*)$/m;
|
||||
ok( defined $arches, "$package declares an architecture" );
|
||||
like( $arches || '', qr/\briscv64\b/, "$package is built for riscv64" );
|
||||
like( $arches || '', qr/\bamd64\b.*\bppc64el\b/, "$package keeps amd64 and ppc64el" );
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -1,178 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Path qw(make_path);
|
||||
use File::Slurper qw(read_text write_text);
|
||||
use File::Spec;
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use Test::More;
|
||||
|
||||
use XCAT::Test::File qw(repo_path slurp_repo_file);
|
||||
|
||||
# EL10 riscv64 kickstart installs: the EL10 anaconda has no RISC-V EFI
|
||||
# platform, so the riscv64 templates tolerate its x86 boot loader package
|
||||
# request, pull grub2-efi-riscv64/efibootmgr in through the riscv64 package
|
||||
# lists, and run a %post that points the UEFI boot entry at grubriscv64.efi
|
||||
# and places the removable-media fallback loader.
|
||||
|
||||
my $install = File::Spec->catdir( 'xCAT-server', 'share', 'xcat', 'install' );
|
||||
|
||||
for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) {
|
||||
my ( $dir, $osbase ) = @$family;
|
||||
for my $profile (qw(compute service)) {
|
||||
my $tmpl = File::Spec->catfile( $install, $dir, "$profile.$osbase.riscv64.tmpl" );
|
||||
my $pkglist = File::Spec->catfile( $install, $dir, "$profile.$osbase.riscv64.pkglist" );
|
||||
my $base = File::Spec->catfile( $install, $dir, "$profile.$osbase.tmpl" );
|
||||
ok( -r repo_path($tmpl), "$dir/$profile.$osbase.riscv64.tmpl exists" );
|
||||
ok( -r repo_path($pkglist), "$dir/$profile.$osbase.riscv64.pkglist exists" );
|
||||
my $t = slurp_repo_file($tmpl);
|
||||
like( $t, qr/^%packages --ignoremissing$/m, "$dir/$profile.$osbase riscv64 template tolerates anaconda's x86 boot loader package request" );
|
||||
like( $t, qr/^#INCLUDE_DEFAULT_PKGLIST#$/m, "$dir/$profile.$osbase riscv64 template still includes the default package list" );
|
||||
like( $t, qr{^#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post\.rhels10\.riscv64#$}m, "$dir/$profile.$osbase riscv64 template runs the riscv64 UEFI boot entry fix-up" );
|
||||
like( $t, qr/^%addon com_redhat_kdump --disable\n%end$/m, "$dir/$profile.$osbase riscv64 template keeps the installer from writing a crash kernel reservation EL10 riscv64 cannot honour" );
|
||||
like( $t, qr/^%post --erroronfail --interpreter=\/bin\/bash$/m, "$dir/$profile.$osbase riscv64 template fails the install when no boot loader reached the ESP" );
|
||||
like( $t, qr{/boot/efi/EFI/BOOT/BOOTRISCV64\.EFI}, "$dir/$profile.$osbase riscv64 template checks the removable-media loader" );
|
||||
like( $t, qr{/boot/efi/EFI/\*/grubriscv64\.efi}, "$dir/$profile.$osbase riscv64 template also accepts a vendor directory loader" );
|
||||
like( $t, qr/^rootpw --iscrypted #CRYPT:passwd:key=system,username=root:password#$/m, "$dir/$profile.$osbase riscv64 template keeps the shared root password directive" );
|
||||
|
||||
# the riscv64 template is the shared one plus the riscv64 changes
|
||||
my $b = slurp_repo_file($base);
|
||||
( my $t_norm = $t ) =~ s/^# riscv64:.*\n(?:#.*\n)*%addon com_redhat_kdump --disable\n%end\n\n//m;
|
||||
$t_norm =~ s/^# riscv64:.*\n(?:#.*\n)*//m;
|
||||
$t_norm =~ s/^%packages --ignoremissing$/%packages/m;
|
||||
$t_norm =~ s{^#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post\.rhels10\.riscv64#\n}{}m;
|
||||
$t_norm =~ s/\n# --ignoremissing above.*?\n%post --erroronfail.*?\n%end\n//s;
|
||||
is( $t_norm, $b, "$dir/$profile.$osbase riscv64 template only differs from the shared template in the riscv64 changes" );
|
||||
|
||||
my $p = slurp_repo_file($pkglist);
|
||||
like( $p, qr/^grub2-efi-riscv64$/m, "$dir/$profile.$osbase riscv64 package list installs grub2-efi-riscv64" );
|
||||
like( $p, qr/^efibootmgr$/m, "$dir/$profile.$osbase riscv64 package list installs efibootmgr" );
|
||||
unlike( $p, qr/shim|grub2-efi-x64|grub2-efi-aa64/, "$dir/$profile.$osbase riscv64 package list asks for no other architecture's boot loader" );
|
||||
my $shared = slurp_repo_file(
|
||||
File::Spec->catfile( $install, $dir, "$profile.$osbase.pkglist" )
|
||||
);
|
||||
( my $p_norm = $p ) =~ s/^grub2-efi-riscv64\n//m;
|
||||
$p_norm =~ s/^efibootmgr\n//m;
|
||||
s/\s+\z/\n/ for ( $p_norm, $shared );
|
||||
is( $p_norm, $shared, "$dir/$profile.$osbase riscv64 package list is the shared list plus the boot loader packages" );
|
||||
}
|
||||
}
|
||||
|
||||
my $post = File::Spec->catfile( $install, 'scripts', 'post.rhels10.riscv64' );
|
||||
my $post_path = repo_path($post);
|
||||
ok( -r $post_path, 'post.rhels10.riscv64 exists' );
|
||||
|
||||
is(system('sh', '-n', $post_path), 0, 'post.rhels10.riscv64 parses as POSIX shell');
|
||||
|
||||
my $tools = tempdir(CLEANUP => 1);
|
||||
my $efi_log = File::Spec->catfile($tools, 'efibootmgr.log');
|
||||
|
||||
sub fake_command {
|
||||
my ($name, $body) = @_;
|
||||
my $path = File::Spec->catfile($tools, $name);
|
||||
write_text($path, "#!/bin/sh\n$body");
|
||||
chmod 0755, $path or die "Unable to make $path executable: $!";
|
||||
return $path;
|
||||
}
|
||||
|
||||
my $uname = fake_command('uname', <<'SH');
|
||||
printf '%s\n' "${XCAT_TEST_ARCH:-riscv64}"
|
||||
SH
|
||||
my $findmnt = fake_command('findmnt', <<'SH');
|
||||
printf '/dev/vda1\n'
|
||||
SH
|
||||
my $lsblk = fake_command('lsblk', <<'SH');
|
||||
case "$*" in
|
||||
*PKNAME*) printf "vda\n" ;;
|
||||
*PARTN*) printf "1\n" ;;
|
||||
esac
|
||||
SH
|
||||
my $efibootmgr = fake_command('efibootmgr', <<'SH');
|
||||
printf '%s\n' "$*" >> "$XCAT_EFI_LOG"
|
||||
case "$*" in
|
||||
'') exit 0 ;;
|
||||
'-v')
|
||||
printf 'Boot0001* Rocky HD(1,GPT,...)/File(\\EFI\\rocky\\shimx64.efi)\n'
|
||||
printf 'Boot0002* OldRiscv HD(1,GPT,...)/File(\\EFI\\rocky\\grubriscv64.efi)\n'
|
||||
printf 'Boot0003* Network PXE\n'
|
||||
exit 0
|
||||
;;
|
||||
*'-c'*) exit "${XCAT_EFI_CREATE_RC:-0}" ;;
|
||||
esac
|
||||
exit 0
|
||||
SH
|
||||
|
||||
sub installed_root {
|
||||
my ($with_loader) = @_;
|
||||
my $root = tempdir(CLEANUP => 1);
|
||||
make_path(File::Spec->catdir($root, 'boot', 'efi', 'EFI', 'rocky'));
|
||||
make_path(File::Spec->catdir($root, 'etc'));
|
||||
write_text(File::Spec->catfile($root, 'etc', 'os-release'), "NAME=\"Rocky Linux\"\n");
|
||||
if ($with_loader) {
|
||||
write_text(
|
||||
File::Spec->catfile($root, 'boot', 'efi', 'EFI', 'rocky', 'grubriscv64.efi'),
|
||||
"riscv loader\n",
|
||||
);
|
||||
}
|
||||
return $root;
|
||||
}
|
||||
|
||||
sub run_post {
|
||||
my ($root, %extra) = @_;
|
||||
local %ENV = (
|
||||
%ENV,
|
||||
XCAT_INSTALL_ROOT => $root,
|
||||
XCAT_UNAME => $uname,
|
||||
XCAT_EFIBOOTMGR => $efibootmgr,
|
||||
XCAT_FINDMNT => $findmnt,
|
||||
XCAT_LSBLK => $lsblk,
|
||||
XCAT_EFI_LOG => $efi_log,
|
||||
XCAT_TEST_ARCH => 'riscv64',
|
||||
%extra,
|
||||
);
|
||||
open(my $fh, '-|', 'sh', $post_path) or die "Unable to run $post_path: $!";
|
||||
my $output = do { local $/; <$fh> };
|
||||
close($fh);
|
||||
return ($? >> 8, $output);
|
||||
}
|
||||
|
||||
my $root = installed_root(1);
|
||||
my ($status, $output) = run_post($root);
|
||||
is($status, 0, 'the RISC-V fix-up completes successfully');
|
||||
is(
|
||||
read_text(File::Spec->catfile($root, 'boot', 'efi', 'EFI', 'BOOT', 'BOOTRISCV64.EFI')),
|
||||
"riscv loader\n",
|
||||
'the distro loader is copied to the removable-media fallback path',
|
||||
);
|
||||
like($output, qr/UEFI boot entry "Rocky Linux"/, 'the created UEFI entry is reported');
|
||||
my $efi_calls = read_text($efi_log);
|
||||
like($efi_calls, qr/^-q -b 0001 -B$/m, 'the invalid x86 boot entry is removed');
|
||||
like($efi_calls, qr/^-q -b 0002 -B$/m, 'the prior RISC-V boot entry is removed');
|
||||
unlike($efi_calls, qr/0003/, 'unrelated firmware entries are retained');
|
||||
like(
|
||||
$efi_calls,
|
||||
qr/^-q -c -d \/dev\/vda -p 1 -L Rocky Linux -l \\EFI\\rocky\\grubriscv64\.efi$/m,
|
||||
'the new entry points at the distro RISC-V loader on the ESP disk',
|
||||
);
|
||||
|
||||
unlink($efi_log);
|
||||
my $x86_root = installed_root(1);
|
||||
($status, $output) = run_post($x86_root, XCAT_TEST_ARCH => 'x86_64');
|
||||
is($status, 0, 'the post-install script is a no-op on x86_64');
|
||||
is($output, '', 'the x86_64 no-op reports nothing');
|
||||
ok(!-e $efi_log, 'the x86_64 no-op never invokes efibootmgr');
|
||||
|
||||
my $bare_root = installed_root(0);
|
||||
($status, $output) = run_post($bare_root);
|
||||
is($status, 0, 'a missing distro loader remains nonfatal');
|
||||
like($output, qr/no \\EFI\\\*\\grubriscv64\.efi/, 'a missing distro loader is diagnosed');
|
||||
|
||||
unlink($efi_log);
|
||||
($status, $output) = run_post($root, XCAT_EFI_CREATE_RC => 1);
|
||||
is($status, 0, 'an efibootmgr create failure remains nonfatal');
|
||||
like($output, qr/firmware will use \\EFI\\BOOT\\BOOTRISCV64\.EFI/, 'the fallback path is reported after an efibootmgr failure');
|
||||
|
||||
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