2
0
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:
Daniel Hilst
2026-10-16 20:19:26 -03:00
committed by GitHub
13 changed files with 722 additions and 401 deletions
+10 -1
View File
@@ -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
View File
@@ -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
+41
View File
@@ -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
+46
View File
@@ -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;
+35 -1
View File
@@ -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.
+69
View File
@@ -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();
+265
View File
@@ -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();
@@ -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();
-88
View File
@@ -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();
-178
View File
@@ -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();
-120
View File
@@ -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();