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();