From f4f596537ede774148decc8a4cfb014cc620a462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:26:48 -0300 Subject: [PATCH] test(postscripts): exercise rendered postinit service policy Run complete rendered installers and their emitted postboot scripts in private Linux filesystems. Cover service selection, flag aliases, defaults, debug output, ordering and failures, and select the test in CI. --- github_action_xcat_test.pl | 8 +- .../native/fixtures/postinstall-render.pl | 32 ++++ xCAT-test/native/postinstall_bootstrap.t | 173 ++++++++++++++++++ 3 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 xCAT-test/native/fixtures/postinstall-render.pl create mode 100644 xCAT-test/native/postinstall_bootstrap.t diff --git a/github_action_xcat_test.pl b/github_action_xcat_test.pl index 74ef58b35..4b49f95d3 100644 --- a/github_action_xcat_test.pl +++ b/github_action_xcat_test.pl @@ -500,16 +500,14 @@ sub run_bats_tests{ #-------------------------------------------------------- # Fuction name: run_source_integration_tests -# Description: Run the integration tests that read a file the Debian -# packages do not install. They run against the pre-build copy -# of the source tree, after install_xcat() put Apache on the -# runner. A skip fails the phase: the runner has every +# Description: Run source-only integration and native tests after package +# installation. A skip fails the phase: the runner has every # prerequisite. # Attributes: # Return code: 0 all tests passed, 1 otherwise #-------------------------------------------------------- sub run_source_integration_tests{ - my $cmd = "cd $unitsrc && prove -v xCAT-test/integration/wsapi_apache_access.t"; + my $cmd = "cd $unitsrc && prove -v xCAT-test/integration/wsapi_apache_access.t xCAT-test/native/postinstall_bootstrap.t"; print "[run_source_integration_tests] running $cmd\n"; my @output = runcmd("$cmd"); print Dumper \@output; diff --git a/xCAT-test/native/fixtures/postinstall-render.pl b/xCAT-test/native/fixtures/postinstall-render.pl new file mode 100644 index 000000000..a5ea981d3 --- /dev/null +++ b/xCAT-test/native/fixtures/postinstall-render.pl @@ -0,0 +1,32 @@ +use strict; +use warnings; +use FindBin; +use lib "$FindBin::Bin/../../../perl-xCAT", "$FindBin::Bin/../../../xCAT-server/lib/perl"; + +$ENV{XCATROOT} = "$FindBin::Bin/../../../xCAT-server"; +$ENV{XCATCFG} = 'SQLite:/fixture/db'; +require xCAT::Table; +require xCAT::Template; +require xCAT::Postage; + +my %site = (installdir => '/install', tftpdir => '/tftpboot', master => '192.0.2.1', + xcatiport => 3002, httpport => 80, xcatdebugmode => 0, nodestatus => 0); +for my $key (sort keys %site) { + my $table = xCAT::Table->new('site', -create => 1) or die 'Cannot create site'; + $table->setAttribs({key => $key}, {value => $site{$key}}); + $table->close(); +} +%::XCATSITEVALS = %site; +for my $row ( + ['nodelist', {groups => 'all'}], + ['nodetype', {os => 'rhels8', arch => 'x86_64', provmethod => 'install'}], + ['noderes', {xcatmaster => '192.0.2.1'}], +) { + my $table = xCAT::Table->new($row->[0], -create => 1) or die "Cannot create $row->[0]"; + $table->setAttribs({node => 'node'}, $row->[1]); + $table->close(); +} +my $error = xCAT::Template->subvars( + "$ENV{XCATROOT}/share/xcat/install/scripts/$ARGV[0]", '/fixture/installer', + 'node', undef, '/install/media', 'rh', undef, {xcatmaster => '192.0.2.1'}); +die "Cannot render installer: $error" if $error; diff --git a/xCAT-test/native/postinstall_bootstrap.t b/xCAT-test/native/postinstall_bootstrap.t new file mode 100644 index 000000000..28f9a10f6 --- /dev/null +++ b/xCAT-test/native/postinstall_bootstrap.t @@ -0,0 +1,173 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Capture::Tiny qw(capture); +use File::Copy qw(copy); +use File::Path qw(make_path); +use File::Slurper qw(read_binary write_binary); +use File::Temp qw(tempdir); +use FindBin; +use lib "$FindBin::Bin/../lib"; +use Test::More; +use XCAT::Test::File qw(repo_path); + +plan skip_all => 'installer execution requires Linux' unless $^O eq 'linux'; +die 'Install bubblewrap to run this test' unless -x '/usr/bin/bwrap'; + +sub executable { + my ($path, $contents) = @_; + write_binary($path, "#!/bin/sh\n$contents"); + chmod 0755, $path or die "Cannot chmod $path: $!"; + return; +} + +sub fixture { + my $root = tempdir(CLEANUP => 1); + make_path(map { "$root/$_" } qw(bin db opt/xcat etc/systemd/system/multi-user.target.wants + etc/init.d etc/rc2.d etc/rc.d/rc3.d etc/rc.d/rc4.d etc/rc.d/rc5.d + xcatpost log/xcat boot)); + write_binary("$root/etc/hosts", "127.0.0.1 localhost\n"); + executable("$root/bin/service-control", <<'SH'); +name=${0##*/} +[ "$*" != --version ] || exit 0 +printf '%s|%s\n' "$name" "$*" >>/fixture/events +case "$name:$*" in + systemctl:disable*|chkconfig:*off|update-rc.d:*remove) + [ "${SERVICE_RC:-0}" -ne 0 ] || touch /fixture/disabled + exit "${SERVICE_RC:-0}" + ;; +esac +SH + symlink('service-control', "$root/bin/$_") or die $! + for qw(systemctl chkconfig update-rc.d); + executable("$root/bin/download", <<'SH'); +case "$*" in + *mypostscript.node*) cp /fixture/downloaded /xcatpost/mypostscript.node ;; +esac +SH + symlink('download', "$root/bin/$_") or die $! for qw(wget curl); + executable("$root/bin/quiet", 'exit 0' . "\n"); + symlink('quiet', "$root/bin/$_") or die $! + for qw(logger sleep ping updateflag.awk); + executable("$root/bin/ip", "printf '1: eth0: link/ether 52:54:00:12:34:56\\n'\n"); + write_binary("$root/xcatpost/xcatlib.sh", <<'SH'); +msgutil_r() { printf 'message|%s|%s|%s\n' "$1" "$2" "$3" >>/fixture/events; } +SH + write_binary("$root/downloaded", <<'SH'); +MASTER=192.0.2.1 +MASTER_IP='192.0.2.1' +OSVER='rhels8' +RUNBOOTSCRIPTS='no' +NODESTATUS='no' +XCATDEBUGMODE='0' +SH + return $root; +} + +sub isolated { + my ($root, $stage, $environment, @command) = @_; + my @args = ('/usr/bin/bwrap', '--unshare-all', '--die-with-parent', '--new-session', + '--tmpfs', '/', '--proc', '/proc', '--dev', '/dev', '--tmpfs', '/tmp', + '--tmpfs', '/run', '--bind', $root, '/fixture', '--chdir', '/fixture', + '--setenv', 'PATH', '/fixture/bin:/usr/bin:/bin:/usr/sbin:/sbin', + '--setenv', 'LC_ALL', 'C'); + if ($stage eq 'render') { + push @args, '--ro-bind', repo_path('xCAT/postscripts'), '/install/postscripts', + '--ro-bind', repo_path('.'), repo_path('.'); + } + for my $directory (qw(/usr /bin /sbin /lib /lib64)) { + if (-l $directory) { + push @args, '--symlink', readlink($directory), $directory; + } elsif (-d $directory) { + push @args, '--ro-bind', $directory, $directory; + } + } + push @args, '--bind', "$root/$_", "/$_" for qw(opt etc xcatpost boot); + push @args, '--bind', "$root/log", '/var/log'; + push @args, '--ro-bind', '/etc/alternatives', '/etc/alternatives' if -d '/etc/alternatives'; + push @args, '--setenv', $_, $environment->{$_} for sort keys %$environment; + local %ENV = (PATH => '/usr/bin:/bin', LC_ALL => 'C'); + my ($out, $err, $status) = capture { system(@args, @command) }; + return ($status, $out . $err); +} + +my @cases = ( + {name => 'disabled', run => 'no', status => 'no', disable => 1}, + {name => 'node reporting', run => 'no', status => 'yes', ubuntu_only => 1}, + {name => 'empty status defaults enabled', run => 'no', status => '', ubuntu_only => 1}, + {name => 'run enabled', run => 'yes', status => 'no'}, + {name => 'numeric run enabled', run => '1', status => 'no'}, + {name => 'short run enabled', run => 'Y', status => 'no'}, + {name => 'numeric node enabled', run => 'no', status => '1', ubuntu_only => 1}, + {name => 'short node enabled', run => 'no', status => 'Y', ubuntu_only => 1}, + {name => 'uppercase flags', run => 'NO', status => 'NO', disable => 1}, + {name => 'debug one', run => 'no', status => 'no', disable => 1, debug => 1}, + {name => 'debug two', run => 'no', status => 'no', disable => 1, debug => 2}, + {name => 'debug while retained', run => 'yes', status => 'no', debug => 1}, + {name => 'service failure', run => 'no', status => 'no', disable => 1, service_rc => 5}, + {name => 'postscript failure', run => 'no', status => 'no', disable => 1, post_rc => 7}, + {name => 'missing node file', run => 'no', status => 'no', no_node => 1, ubuntu_only => 1}, + {name => 'missing postboot file', no_post => 1, disable => 1}, +); + +for my $owner (qw(post.xcat post.xcat.ng post.xcat.rhels10)) { + my $build = fixture(); + my ($status, $output) = isolated($build, 'render', {}, $^X, + repo_path('xCAT-test/native/fixtures/postinstall-render.pl'), $owner); + is($status, 0, "$owner renders with the production renderer") or diag($output); + ($status, $output) = isolated($build, 'client', {}, '/bin/bash', '-n', '/fixture/installer'); + is($status, 0, "$owner rendered installer parses") or diag($output); + ($status, $output) = isolated($build, 'client', {}, '/bin/bash', '/fixture/installer'); + is($status, 0, "$owner executes the complete installer script") or diag($output); + ok(-x "$build/opt/xcat/xcatinstallpost", "$owner emits the executable postboot script"); + ($status, $output) = isolated($build, 'client', {}, '/bin/bash', '-n', '/opt/xcat/xcatinstallpost'); + is($status, 0, "$owner emitted script parses") or diag($output); + for my $os (qw(rhels8 ubuntu20.04)) { + for my $case (@cases) { + subtest "$owner $os $case->{name}" => sub { + my $root = fixture(); + copy("$build/opt/xcat/xcatinstallpost", "$root/opt/xcat/xcatinstallpost") or die $!; + my $run = $case->{run} // 'no'; + my $node = $case->{status} // 'no'; + my $debug = $case->{debug} // 0; + my $post_rc = $case->{post_rc} // 0; + write_binary("$root/xcatpost/mypostscript", "NODESTATUS='$node'\n") unless $case->{no_node}; + unless ($case->{no_post}) { + write_binary("$root/xcatpost/mypostscript.post", + "OSVER='$os'\nRUNBOOTSCRIPTS='$run'\nXCATDEBUGMODE='$debug'\n" + . "MASTER_IP='192.0.2.2'\nMASTER='192.0.2.2'\nNODE='node'\n" + . "MACADDRESS='52:54:00:12:34:56'\n" + . "printf 'postboot|%s\\n' \"\$(test -f /fixture/disabled && echo disabled || echo enabled)\" >>/fixture/events\n" + . "exit $post_rc\n"); + } + write_binary("$root/opt/xcat/xcatinfo", "XCATSERVER=192.0.2.1\n"); + my ($rc, $text) = isolated($root, 'client', {SERVICE_RC => $case->{service_rc} // 0}, + '/bin/bash', '/opt/xcat/xcatinstallpost'); + is($rc, 0, 'preserves the postboot runner exit status') or diag($text); + my $events = -e "$root/events" ? scalar read_binary("$root/events") : ''; + note($events) if $ENV{XCAT_POST_TEST_TRACE}; + my $ubuntu = !$case->{no_post} && $owner eq 'post.xcat' && $os =~ /^ubuntu/; + my $disable = $case->{disable} || ($ubuntu && $case->{ubuntu_only}); + my $command = $owner ne 'post.xcat' ? 'systemctl|disable xcatpostinit1.service' + : $ubuntu ? 'update-rc.d|-f xcatpostinit1 remove' : 'chkconfig|xcatpostinit1 off'; + my @commands = grep { /^(?:systemctl|chkconfig|update-rc.d)\|/ } split /\n/, $events; + is_deeply(\@commands, $disable ? [$command] : [], 'uses the caller-specific service policy'); + unless ($case->{no_post}) { + my $state = $disable && !$case->{service_rc} ? 'disabled' : 'enabled'; + like($events, qr/^postboot\|$state$/m, 'postscript observes the service state'); + if ($disable) { + ok(index($events, $command) < index($events, 'postboot|'), + 'disables before executing the postscript'); + } + my @messages = grep { /^message\|.*\|debug\|(?:service xcatpostinit1 disabled|systemctl disable|update-rc.d)/ } + split /\n/, $events; + my $message = $owner ne 'post.xcat' ? 'systemctl disable xcatpostinit1.service' + : $ubuntu ? 'update-rc.d -f xcatpostinit1 remove' : 'service xcatpostinit1 disabled'; + is_deeply(\@messages, $debug && ($ubuntu || $disable) + ? ["message|192.0.2.2|debug|$message"] : [], 'preserves debug output and master selection'); + } + }; + } + } +} +done_testing();