diff --git a/.github/workflows/xcat_test.yml b/.github/workflows/xcat_test.yml index 7159916c7..9d874410d 100644 --- a/.github/workflows/xcat_test.yml +++ b/.github/workflows/xcat_test.yml @@ -70,6 +70,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 | tee "$log" + ! grep -qi skipped "$log" rpm_lifecycle_test: runs-on: ubuntu-24.04 timeout-minutes: 30 diff --git a/xCAT-test/fixtures/install-profile/command b/xCAT-test/fixtures/install-profile/command new file mode 100644 index 000000000..3f8fe9f2e --- /dev/null +++ b/xCAT-test/fixtures/install-profile/command @@ -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 'xcatlib.sh\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 diff --git a/xCAT-test/lib/XCAT/Test/Sandbox.pm b/xCAT-test/lib/XCAT/Test/Sandbox.pm new file mode 100644 index 000000000..9bcc0734e --- /dev/null +++ b/xCAT-test/lib/XCAT/Test/Sandbox.pm @@ -0,0 +1,51 @@ +package XCAT::Test::Sandbox; + +use strict; +use warnings; +use Exporter qw(import); +use File::Path qw(make_path); +use File::Temp qw(tempdir); +use XCAT::Test::Process qw(run_command); + +our @EXPORT_OK = qw(sandbox_root sandbox_run); + +sub sandbox_root { + my $root = tempdir(CLEANUP => 1); + make_path(map { "$root/$_" } qw(bin boot etc log target tmp)); + return $root; +} + +sub sandbox_run { + my ($root, @command) = @_; + my $options = ref($command[0]) eq 'HASH' ? shift @command : {}; + my $bwrap = -x '/usr/bin/bwrap' ? '/usr/bin/bwrap' : '/bin/bwrap'; + die 'Install bubblewrap to run this test' unless -x $bwrap; + my @sandbox = ($bwrap, '--unshare-all', '--die-with-parent', '--new-session', + '--tmpfs', '/', '--proc', '/proc', '--dev', '/dev', '--tmpfs', '/run', + '--setenv', 'PATH', '/fixture/bin:/usr/bin:/bin', + '--setenv', 'LC_ALL', 'C', '--bind', $root, '/fixture', '--chdir', '/fixture'); + for my $directory (qw(/usr /bin /sbin /lib /lib64)) { + push @sandbox, '--ro-bind', $directory, $directory if -d $directory; + } + for my $mount (qw(boot etc target tmp)) { + push @sandbox, '--bind', "$root/$mount", "/$mount"; + } + push @sandbox, '--bind', "$root/log", '/var/log'; + push @sandbox, '--ro-bind', '/etc/alternatives', '/etc/alternatives' if -d '/etc/alternatives'; + my @mounts; + for my $kind (qw(read_only writable)) { + my $flag = $kind eq 'read_only' ? '--ro-bind' : '--bind'; + push @mounts, map { [$flag, $_, $options->{$kind}{$_}] } + keys %{$options->{$kind} // {}}; + } + for my $mount (sort { length($a->[2]) <=> length($b->[2]) || $a->[2] cmp $b->[2] } @mounts) { + push @sandbox, @$mount; + } + for my $name (keys %{$options->{env} // {}}) { + push @sandbox, '--setenv', $name, $options->{env}{$name}; + } + local %ENV; + return run_command(@sandbox, '/usr/bin/timeout', '--kill-after=5', '45', @command); +} + +1; diff --git a/xCAT-test/lib/XCAT/Test/Template.pm b/xCAT-test/lib/XCAT/Test/Template.pm new file mode 100644 index 000000000..eed942869 --- /dev/null +++ b/xCAT-test/lib/XCAT/Test/Template.pm @@ -0,0 +1,79 @@ +package XCAT::Test::Template; + +use strict; +use warnings; +use Exporter qw(import); +use File::Path qw(make_path); +use File::Temp qw(tempdir); +use File::Slurper qw(write_text); +use JSON::PP qw(encode_json); +use XCAT::Test::File qw(repo_path); +use XCAT::Test::Sandbox qw(sandbox_root sandbox_run); + +our @EXPORT_OK = qw(template_database set_row render_template); + +sub template_database { + my $dir = tempdir(CLEANUP => 1); + make_path("$dir/db", "$dir/install"); + symlink(repo_path('xCAT/postscripts'), "$dir/install/postscripts") or die $!; + $ENV{XCATROOT} = repo_path('xCAT-server'); + $ENV{XCATCFG} = "SQLite:$dir/db"; + require xCAT::Table; + require xCAT::Template; + require xCAT::Postage; + my %site = ( + installdir => "$dir/install", tftpdir => "$dir/tftpboot", + master => '192.0.2.1', timezone => 'UTC', domain => 'example.invalid', + xcatiport => 3002, xcatdport => 3001, httpport => 8080, + xcatdebugmode => 0, nodestatus => 1, secureroot => 1, + managedaddressmode => 'dhcp', + ); + set_row('site', {key => $_}, {value => $site{$_}}) for keys %site; + %::XCATSITEVALS = %site; + set_row('nodelist', {node => 'node'}, {groups => 'all'}); + set_row('noderes', {node => 'node'}, { + xcatmaster => '192.0.2.1', nfsserver => '192.0.2.1', installnic => 'mac', + }); + set_row('mac', {node => 'node'}, {mac => '52:54:00:12:34:56'}); + set_row('bootparams', {node => 'node'}, {kcmdline => 'console=ttyS0 quiet'}); + return $dir; +} + +sub set_row { + my ($name, $key, $values) = @_; + my $table = xCAT::Table->new($name, -create => 1) or die "Cannot create $name"; + $table->setAttribs($key, $values); + $table->close(); + return; +} + +sub render_template { + my ($database, @arguments) = @_; + my $root = sandbox_root(); + write_text("$root/arguments.json", encode_json(\@arguments)); + write_text("$root/render.pl", <<'PERL'); +use strict; +use warnings; +use File::Slurper qw(read_text); +use JSON::PP qw(decode_json); +use xCAT::Template; +use xCAT::Postage; +my $site = xCAT::Table->new('site'); +my @attributes = $site->getAllAttribs('key', 'value'); +%::XCATSITEVALS = map { $_->{key} => $_->{value} } @attributes; +$site->close(); +my $arguments = decode_json(read_text('/fixture/arguments.json')); +my $error = xCAT::Template->subvars(@$arguments); +die "$error\n" if $error; +PERL + my $source = repo_path('.'); + my ($status, $output) = sandbox_run($root, { + read_only => {$source => $source, repo_path('xCAT/postscripts') => '/install/postscripts'}, + writable => {$database => $database}, + env => {XCATROOT => $ENV{XCATROOT}, XCATCFG => $ENV{XCATCFG}}, + }, $^X, '-I' . repo_path('perl-xCAT'), '-I' . repo_path('xCAT-server/lib/perl'), '/fixture/render.pl'); + die "Template rendering failed ($status): $output" if $status; + return; +} + +1; diff --git a/xCAT-test/native/install_profile_riscv64.t b/xCAT-test/native/install_profile_riscv64.t new file mode 100644 index 000000000..15b035011 --- /dev/null +++ b/xCAT-test/native/install_profile_riscv64.t @@ -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(); diff --git a/xCAT-test/unit/install_profile_riscv64.t b/xCAT-test/unit/install_profile_riscv64.t deleted file mode 100644 index 117110baf..000000000 --- a/xCAT-test/unit/install_profile_riscv64.t +++ /dev/null @@ -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();