mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-10-02 00:41:42 +00:00
test(openeuler): cover image creation and boot artifacts
Exercise native profile registration and interrupted reimport recovery with SQLite. Render installer templates. Check image command failures and archive extraction. Retain installed and diskless compute and service cases and legacy boundaries.
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use FindBin;
|
||||
use File::Path qw(make_path);
|
||||
use File::Spec;
|
||||
use File::Temp qw(tempdir);
|
||||
use Storable qw(retrieve);
|
||||
use Test::More;
|
||||
|
||||
plan skip_all => 'requires Linux filesystem semantics and an unprivileged test user'
|
||||
unless $^O eq 'linux' && $>;
|
||||
my $repo = File::Spec->rel2abs("$FindBin::Bin/../..");
|
||||
my $dir = tempdir(CLEANUP => 1);
|
||||
my $driver = "$dir/driver.pl";
|
||||
write_file($driver, <<'PERL');
|
||||
use strict;
|
||||
use warnings;
|
||||
use Errno qw(EACCES EIO);
|
||||
use File::Copy ();
|
||||
use File::Basename qw(dirname);
|
||||
use FindBin;
|
||||
use Storable qw(nstore);
|
||||
our (@commands, $dracut_calls, $lock_calls);
|
||||
BEGIN {
|
||||
*CORE::GLOBAL::readpipe = sub {
|
||||
return "4.18.2\n" if $_[0] =~ /rpm --version/;
|
||||
return "059\n" if $_[0] =~ /^rpm --root .* -qi dracut /;
|
||||
return '' if $_[0] =~ /type -p pigz/;
|
||||
die "Unexpected command: $_[0]";
|
||||
};
|
||||
*CORE::GLOBAL::system = sub {
|
||||
my $command = join(' ', @_);
|
||||
push @commands, $command;
|
||||
if ($command =~ /^chroot (\S+) dracut .* -f (\S+) /) {
|
||||
$dracut_calls++;
|
||||
open(my $file, '>', "$1$2") or die $!;
|
||||
print {$file} "new initrd\n";
|
||||
close($file) or die $!;
|
||||
return 29 << 8 if $ENV{GENIMAGE_FAILURE} eq 'dracut';
|
||||
}
|
||||
if ($command =~ m{^sed -i -e '[^']+' \Q$ENV{GENIMAGE_DEST}\E/rootimg/etc/yum\.repos\.d/[A-Za-z0-9.-]+\.repo$}) {
|
||||
return CORE::system($command);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
*CORE::GLOBAL::rename = sub {
|
||||
my $failure = $ENV{GENIMAGE_FAILURE};
|
||||
if (($failure =~ /^(?:publish-initrd|rollback)$/
|
||||
&& $_[1] eq "$ENV{GENIMAGE_DEST}/initrd-stateless.gz")
|
||||
|| ($failure eq 'publish-kernel' && $_[1] eq "$ENV{GENIMAGE_DEST}/kernel")
|
||||
|| ($failure eq 'rollback' && $_[0] =~ m{/previous-kernel$})) {
|
||||
$! = EACCES;
|
||||
return 0;
|
||||
}
|
||||
return CORE::rename($_[0], $_[1]);
|
||||
};
|
||||
*CORE::GLOBAL::link = sub {
|
||||
if ($ENV{GENIMAGE_FAILURE} eq 'backup-kernel') {
|
||||
$! = EACCES;
|
||||
return 0;
|
||||
}
|
||||
return CORE::link($_[0], $_[1]);
|
||||
};
|
||||
}
|
||||
my $move = \&File::Copy::move;
|
||||
{
|
||||
no warnings 'redefine';
|
||||
*File::Copy::move = sub {
|
||||
if ($ENV{GENIMAGE_FAILURE} eq 'move-initrd' && $_[0] =~ m{/tmp/initrd\.\d+\.gz$}) {
|
||||
$! = EIO;
|
||||
return 0;
|
||||
}
|
||||
return $move->(@_);
|
||||
};
|
||||
}
|
||||
END {
|
||||
nstore({ commands => \@commands, dracut_calls => $dracut_calls || 0, lock_calls => $lock_calls || 0 },
|
||||
"$ENV{GENIMAGE_CASE}/result");
|
||||
}
|
||||
require xCAT::Utils;
|
||||
{
|
||||
no warnings qw(redefine once);
|
||||
*xCAT::Utils::acquire_lock_imageop = sub {
|
||||
$lock_calls++;
|
||||
return ($ENV{GENIMAGE_FAILURE} eq 'lock' ? 1 : 0, undef);
|
||||
};
|
||||
}
|
||||
$0 = $ENV{GENIMAGE_SCRIPT};
|
||||
$FindBin::Bin = dirname($0);
|
||||
do $0;
|
||||
die $@ if $@;
|
||||
PERL
|
||||
|
||||
for my $os (qw(openeuler20.03sp4 openeuler24.03sp3)) {
|
||||
for my $failure (qw(none kernel-copy dracut move-initrd backup-kernel publish-kernel publish-initrd no-dracut)) {
|
||||
my ($rc, $state, $output, $case) = run_case($os, $failure, 'vmlinuz', 1);
|
||||
my $label = "$os $failure";
|
||||
if ($failure eq 'none') {
|
||||
is($rc, 0, "$label completes") or diag($output);
|
||||
is(read_file("$case/image/kernel"), "new kernel\n", "$label publishes the kernel");
|
||||
is(read_file("$case/image/initrd-stateless.gz"), "new initrd\n", "$label publishes the initrd");
|
||||
} else {
|
||||
isnt($rc, 0, "$label fails");
|
||||
like($output, qr/Error: failed to (?:copy|generate|move|publish|preserve).*?(?:kernel|ramdisk|initrd)/i,
|
||||
"$label reports the failed boot file operation");
|
||||
is(read_file("$case/image/kernel"), "old kernel\n", "$label preserves the previous kernel");
|
||||
is(read_file("$case/image/initrd-stateless.gz"), "old initrd\n", "$label preserves the previous initrd");
|
||||
ok(grep({ /^findmnt .*\/dev\// } @{$state->{commands}}), "$label invokes mount cleanup");
|
||||
}
|
||||
is($state->{dracut_calls}, $failure =~ /^(?:kernel-copy|no-dracut)$/ ? 0 : 1,
|
||||
"$label only reaches dracut after a successful kernel copy");
|
||||
is($state->{lock_calls}, 1, "$label acquires the image operation lock");
|
||||
is_deeply([glob("$case/image/rootimg/tmp/initrd.*.gz")], [], "$label removes temporary dracut output");
|
||||
is_deeply([glob("$case/image/.genimage*")], [], "$label removes boot staging files");
|
||||
}
|
||||
}
|
||||
for my $kernel (qw(vmlinux image modules)) {
|
||||
my ($rc, $state, $output, $case) = run_case('openeuler24.03', 'none', $kernel, 0);
|
||||
is($rc, 0, "$kernel kernel layout completes") or diag($output);
|
||||
is(read_file("$case/image/kernel"), "new kernel\n", "$kernel layout publishes its kernel");
|
||||
is(read_file("$case/image/initrd-stateless.gz"), "new initrd\n", "$kernel layout publishes its initrd");
|
||||
}
|
||||
my ($rc, $state, $output, $case) = run_case('openeuler24.03sp3', 'publish-initrd', 'vmlinuz', 0);
|
||||
isnt($rc, 0, 'first build publication failure is fatal');
|
||||
ok(!-e "$case/image/kernel", 'first build failure removes the unpaired kernel');
|
||||
ok(!-e "$case/image/initrd-stateless.gz", 'first build failure does not expose an initrd');
|
||||
|
||||
($rc, $state, $output, $case) = run_case('openeuler24.03sp3', 'lock', 'vmlinuz', 1);
|
||||
isnt($rc, 0, 'lock refusal is fatal');
|
||||
like($output, qr/Could not acquire image operation lock/, 'lock refusal reports the contested image');
|
||||
is($state->{lock_calls}, 1, 'lock refusal uses the existing lock owner');
|
||||
is($state->{dracut_calls}, 0, 'lock refusal does not run dracut');
|
||||
is(read_file("$case/image/kernel"), "old kernel\n", 'lock refusal preserves kernel');
|
||||
is(read_file("$case/image/initrd-stateless.gz"), "old initrd\n", 'lock refusal preserves initrd');
|
||||
is_deeply($state->{commands}, [], 'lock refusal does not run mount cleanup against the active build');
|
||||
is_deeply([glob("$case/image/.genimage*")], [], 'lock refusal creates no staging directory');
|
||||
|
||||
($rc, $state, $output, $case) = run_case('openeuler24.03sp3', 'rollback', 'vmlinuz', 1);
|
||||
isnt($rc, 0, 'rollback failure is fatal');
|
||||
my @backups = glob("$case/image/.genimage*/previous-kernel");
|
||||
is(scalar(@backups), 1, 'rollback failure retains one previous kernel backup');
|
||||
is(read_file($backups[0]), "old kernel\n", 'retained backup contains the previous kernel');
|
||||
like($output, qr/kernel rollback failed: .*Boot files retained in \Q$case\E\/image\/\.genimage/,
|
||||
'rollback failure reports the recoverable backup directory');
|
||||
is(read_file("$case/image/initrd-stateless.gz"), "old initrd\n", 'rollback failure preserves previous initrd');
|
||||
|
||||
for my $failure (qw(none kernel-copy dracut move-initrd)) {
|
||||
($rc, $state, $output, $case) = run_case('rhels9.6', $failure, 'vmlinuz', 1);
|
||||
is($rc == 0 ? 0 : 1, $failure eq 'dracut' ? 1 : 0, "EL $failure retains its exit behavior") or diag($output);
|
||||
is(read_file("$case/image/kernel"), $failure eq 'kernel-copy' ? "old kernel\n" : "new kernel\n",
|
||||
"EL $failure retains kernel copy behavior");
|
||||
is($state->{dracut_calls}, $failure eq 'dracut' ? 1 : 2, "EL $failure retains both initrd modes");
|
||||
is($state->{lock_calls}, 0, "EL $failure retains its existing onlyinitrd lock behavior");
|
||||
}
|
||||
|
||||
my @legacy_repositories = qw(
|
||||
oracle-linux-ol8.repo oracle-linux-ol9.repo oracle-linux-ol10.repo
|
||||
uek-ol8.repo uek-ol9.repo uek-ol10.repo Rocky-AppStream.repo Rocky-BaseOS.repo
|
||||
Rocky-Extras.repo rocky.repo rocky-extras.repo CentOS-Base.repo centos.repo
|
||||
centos-addons.repo almalinux-ha.repo almalinux-nfv.repo almalinux-plus.repo
|
||||
almalinux-powertools.repo almalinux.repo almalinux-resilientstorage.repo almalinux-rt.repo
|
||||
);
|
||||
my @custom_repositories = qw(local-repository-0.repo xCAT-custom.repo administrator.repo);
|
||||
my $enabled = "[fixture]\nenabled=1\ngpgcheck=1\nbaseurl=https://repo.invalid/\n";
|
||||
my $disabled = "[fixture]\nenabled=0\ngpgcheck=1\nbaseurl=https://repo.invalid/\n";
|
||||
my $native = "[OS]\nenabled=1\ngpgcheck=1\n[update]\n \tenabled = 1\n[disabled]\nenabled=0\n#enabled=1\n";
|
||||
my $native_disabled = "[OS]\nenabled=0\ngpgcheck=1\n[update]\nenabled=0\n[disabled]\nenabled=0\n#enabled=1\n";
|
||||
for my $os (qw(openeuler20.03sp4 openeuler24.03sp3)) {
|
||||
for my $repository_case (
|
||||
['enabled', $native, $native_disabled],
|
||||
['already-disabled', $native_disabled, $native_disabled],
|
||||
['absent', undef, undef],
|
||||
) {
|
||||
my ($name, $input, $expected) = @$repository_case;
|
||||
subtest "$os repositories $name" => sub {
|
||||
my %repositories = map { $_ => $enabled } (@legacy_repositories, @custom_repositories);
|
||||
$repositories{'openEuler.repo'} = $input if defined $input;
|
||||
my ($status, $result, $log, $fixture) = run_case($os, 'none', 'vmlinuz', 0, \%repositories);
|
||||
my $repos = "$fixture/image/rootimg/etc/yum.repos.d";
|
||||
for my $pass (1, 2) {
|
||||
($status, $result, $log) = run_owner($os, 'none', $fixture) if $pass == 2;
|
||||
is($status, 0, "pass $pass completes the whole genimage owner") or diag($log);
|
||||
if (defined $expected) {
|
||||
is(read_file("$repos/openEuler.repo"), $expected,
|
||||
"pass $pass disables only active native repository assignments");
|
||||
} else {
|
||||
ok(!-e "$repos/openEuler.repo", "pass $pass does not create an absent vendor file");
|
||||
}
|
||||
is(read_file("$repos/$_"), $disabled, "pass $pass preserves policy for $_")
|
||||
for @legacy_repositories;
|
||||
is(read_file("$repos/$_"), $enabled, "pass $pass leaves $_ unchanged")
|
||||
for @custom_repositories;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
done_testing();
|
||||
|
||||
sub run_case {
|
||||
my ($os, $failure, $kernel, $old, $repositories) = @_;
|
||||
my $case = tempdir(DIR => $dir, CLEANUP => 1);
|
||||
my $root = "$case/image/rootimg";
|
||||
make_path(map { "$root/$_" } qw(boot lib/modules/fixture etc dev run tmp
|
||||
usr/lib/dracut/modules.d/98syslog usr/lib/dracut/modules.d/35network-manager));
|
||||
if ($failure eq 'no-dracut') {
|
||||
require File::Path;
|
||||
File::Path::remove_tree("$root/usr/lib/dracut");
|
||||
}
|
||||
write_file("$root/lib/modules/fixture/modules.dep", '');
|
||||
my $kernel_file = $kernel eq 'modules' ? "$root/lib/modules/fixture/vmlinuz" : "$root/boot/$kernel-fixture";
|
||||
write_file($kernel_file, "new kernel\n");
|
||||
chmod 0000, $kernel_file if $failure eq 'kernel-copy';
|
||||
if ($old) {
|
||||
write_file("$case/image/kernel", "old kernel\n");
|
||||
write_file("$case/image/initrd-stateless.gz", "old initrd\n");
|
||||
}
|
||||
if ($repositories) {
|
||||
make_path("$root/etc/yum.repos.d");
|
||||
write_file("$root/etc/yum.repos.d/$_", $repositories->{$_}) for keys %$repositories;
|
||||
}
|
||||
my @result = run_owner($os, $failure, $case);
|
||||
chmod 0644, $kernel_file;
|
||||
return (@result, $case);
|
||||
}
|
||||
|
||||
sub run_owner {
|
||||
my ($os, $failure, $case) = @_;
|
||||
local %ENV = %ENV;
|
||||
@ENV{qw(GENIMAGE_SCRIPT GENIMAGE_DEST GENIMAGE_CASE GENIMAGE_FAILURE XCATROOT)} =
|
||||
("$repo/xCAT-server/share/xcat/netboot/rh/genimage", "$case/image", $case, $failure, "$repo/xCAT-server");
|
||||
my $pid = fork();
|
||||
die "fork: $!" unless defined($pid);
|
||||
if (!$pid) {
|
||||
open(STDOUT, '>', "$case/output") or die $!;
|
||||
open(STDERR, '>&', STDOUT) or die $!;
|
||||
exec $^X, "-I$repo/perl-xCAT", "-I$repo/xCAT-server/lib/perl",
|
||||
"-I$repo/xCAT-server/share/xcat/netboot/imgutils", $driver,
|
||||
'-o', $os, '-a', 'x86_64', '-p', 'compute', '-k', 'fixture',
|
||||
'--onlyinitrd', '--rootimgdir', "$case/image", 'boot-fixture';
|
||||
die "exec: $!";
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
my $status = $?;
|
||||
return ($status, retrieve("$case/result"), read_file("$case/output"));
|
||||
}
|
||||
|
||||
sub read_file {
|
||||
my ($path) = @_;
|
||||
return undef unless -e $path;
|
||||
open(my $file, '<', $path) or die "read $path: $!";
|
||||
my $contents = do { local $/; <$file> };
|
||||
close($file) or die $!;
|
||||
return $contents;
|
||||
}
|
||||
|
||||
sub write_file {
|
||||
my ($path, $contents) = @_;
|
||||
open(my $file, '>', $path) or die "write $path: $!";
|
||||
print {$file} $contents or die $!;
|
||||
close($file) or die $!;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
use File::Slurper qw(read_text);
|
||||
use File::Temp qw(tempdir);
|
||||
use Storable qw(nstore retrieve);
|
||||
use Test::More;
|
||||
use XCAT::Test::File qw(repo_path);
|
||||
|
||||
$ENV{XCATROOT} = repo_path('xCAT-server');
|
||||
my $plugin = repo_path('xCAT-server/lib/xcat/plugins/genimage.pm');
|
||||
require $plugin;
|
||||
|
||||
sub dispatch {
|
||||
my ($os, $arch) = @_;
|
||||
my $dir = tempdir(CLEANUP => 1);
|
||||
my $out = "$dir/command";
|
||||
my $pid = fork();
|
||||
die "fork: $!" unless defined($pid);
|
||||
if (!$pid) {
|
||||
my @responses;
|
||||
no warnings qw(redefine once);
|
||||
local *xCAT::Table::new = sub {
|
||||
my ($class, $table) = @_;
|
||||
die "Unexpected table $table" unless $table eq 'osimage' || $table eq 'linuximage';
|
||||
return bless {}, 'Local::NoImageMutation';
|
||||
};
|
||||
local *xCAT::TableUtils::getInstallDir = sub { return '/install'; };
|
||||
local *xCAT::TableUtils::get_site_attribute = sub { return (); };
|
||||
local *xCAT::Utils::runcmd = sub { die 'Dry run attempted to execute an image build'; };
|
||||
xCAT_plugin::genimage::process_request({ command => ['genimage'], arg => [
|
||||
'-o', $os, '-a', $arch, '-p', 'compute', '--dryrun', '--tempfile', $out,
|
||||
] }, sub { push @responses, @_ }, sub { die 'Dry run attempted a database update'; });
|
||||
nstore(\@responses, "$dir/responses");
|
||||
exit 0;
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
die "Plugin child failed: $?" if $?;
|
||||
return (retrieve("$dir/responses"), -f $out ? read_text($out) : '');
|
||||
}
|
||||
|
||||
for my $os (qw(openeuler20.03sp4 openeuler22.03sp4 openeuler24.03sp1 openeuler24.03sp3 openeuler24.03sp4)) {
|
||||
my ($responses, $out) = dispatch($os, 'x86_64');
|
||||
ok(!grep({ $_->{error} } @$responses), "$os resolves the native image builder");
|
||||
like($out, qr{^cd \Q$ENV{XCATROOT}\E/share/xcat/netboot/openeuler; ./genimage -a x86_64 -o \Q$os\E -p compute\n},
|
||||
"$os keeps the exact SP and shared builder entry point");
|
||||
}
|
||||
{
|
||||
my ($responses, $out) = dispatch('openeuler24.03', 'ppc64le');
|
||||
ok(!grep({ $_->{error} } @$responses), 'POWER GA resolves the native image builder');
|
||||
like($out, qr{netboot/openeuler; ./genimage -a ppc64le -o openeuler24\.03 -p compute\n},
|
||||
'POWER keeps its architecture and GA release');
|
||||
}
|
||||
for my $case (['openeuler25.03', 'x86_64'], ['openeuler24.03sp0', 'x86_64'],
|
||||
['openeuler', 'x86_64'], ['openeuler24.03', 'aarch64']) {
|
||||
my ($responses, $out) = dispatch(@$case);
|
||||
ok(grep({ $_->{error} && $_->{errorcode}[0] == 1 } @$responses), "@$case is rejected");
|
||||
is($out, '', 'invalid native image does not produce a build command');
|
||||
}
|
||||
for my $case (['rhels9.6', 'rh'], ['rocky9.6', 'rocky'], ['leap15.6', 'sles']) {
|
||||
my ($os, $family) = @$case;
|
||||
my ($responses, $out) = dispatch($os, 'x86_64');
|
||||
ok(!grep({ $_->{error} } @$responses), "$os existing dispatch succeeds");
|
||||
like($out, qr{netboot/\Q$family\E; ./genimage -a x86_64 -o \Q$os\E -p compute\n},
|
||||
"$os existing family is preserved");
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use FindBin;
|
||||
use File::Path qw(make_path);
|
||||
use File::Spec;
|
||||
use File::Temp qw(tempdir);
|
||||
use Storable qw(retrieve);
|
||||
use Test::More;
|
||||
|
||||
plan skip_all => 'requires native Linux DNF and trusted RPM keys'
|
||||
unless $^O eq 'linux' && -x '/usr/bin/dnf' && -x '/usr/bin/rpm';
|
||||
my $repo = File::Spec->rel2abs("$FindBin::Bin/../..");
|
||||
my $dir = tempdir(CLEANUP => 1);
|
||||
my $driver = "$dir/driver.pl";
|
||||
write_file($driver, <<'PERL');
|
||||
use strict;
|
||||
use warnings;
|
||||
use Storable qw(nstore);
|
||||
our (@commands, $reached, $config, $restored);
|
||||
BEGIN {
|
||||
*CORE::GLOBAL::readpipe = sub {
|
||||
return "x86_64\n" if $_[0] eq 'uname -m';
|
||||
return "ext4\n" if $_[0] =~ /^df -T -P /;
|
||||
die "Unexpected command: $_[0]";
|
||||
};
|
||||
*CORE::GLOBAL::system = sub {
|
||||
my $command = join(' ', @_);
|
||||
push @commands, $command;
|
||||
if ($command =~ /^\s*(?:dnf|yum) /) {
|
||||
open(my $file, '<', "/tmp/genimage.$$.yum.conf") or die $!;
|
||||
$config = do { local $/; <$file> };
|
||||
close($file);
|
||||
my $phase = $command =~ / erase .*pre-remove/ ? 'pre-remove'
|
||||
: $command =~ / erase .*post-remove/ ? 'post-remove'
|
||||
: $command =~ / install .*native-extra/ ? 'extra-install'
|
||||
: $command =~ / install / ? 'os-install'
|
||||
: $command =~ / update / ? 'update' : 'clean';
|
||||
return 29 << 8 if $phase eq $ENV{GENIMAGE_FAILURE};
|
||||
return 0;
|
||||
}
|
||||
if ($command eq "rm -fr $ENV{GENIMAGE_ROOT}/bin/uname") {
|
||||
$restored = 1;
|
||||
return CORE::system(@_);
|
||||
}
|
||||
if ($restored && $command eq "mknod $ENV{GENIMAGE_ROOT}/dev/null c 1 3") {
|
||||
$reached = 1;
|
||||
exit 23;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
END {
|
||||
my $uname = '';
|
||||
if (open(my $file, '<', "$ENV{GENIMAGE_ROOT}/bin/uname")) {
|
||||
$uname = do { local $/; <$file> };
|
||||
close($file);
|
||||
}
|
||||
nstore({ commands => \@commands, reached => $reached, config => $config, uname => $uname },
|
||||
"$ENV{GENIMAGE_CASE}/result");
|
||||
unlink("/tmp/genimage.$$.yum.conf");
|
||||
}
|
||||
require xCAT::Utils;
|
||||
{
|
||||
no warnings qw(redefine once);
|
||||
*xCAT::Utils::acquire_lock_imageop = sub { return (0, undef); };
|
||||
}
|
||||
$0 = $ENV{GENIMAGE_SCRIPT};
|
||||
do $0;
|
||||
die $@ if $@;
|
||||
die "Image builder returned before the package boundary";
|
||||
PERL
|
||||
|
||||
for my $os (qw(openeuler20.03sp4 openeuler24.03sp3 rhels9.6)) {
|
||||
for my $failure (qw(os-install pre-remove extra-install post-remove update none)) {
|
||||
my ($rc, $state, $output) = run_case($os, $failure, 'https://packages.example/native');
|
||||
my $fatal = $failure ne 'none' && ($os =~ /^openeuler/ || $failure =~ /install$/);
|
||||
my $label = "$os $failure";
|
||||
if ($fatal) {
|
||||
isnt($rc, 23, "$label stops before postscripts");
|
||||
ok(!$state->{reached}, "$label does not continue into image finalization");
|
||||
like($output, qr/RPM package manager invocation failed/, "$label reports the failed transaction");
|
||||
ok(grep({ /^findmnt .*\/dev\// } @{$state->{commands}}),
|
||||
"$label invokes the image mount cleanup owner");
|
||||
} else {
|
||||
is($rc, 23, "$label reaches the controlled end of the package phase") or diag($output);
|
||||
ok($state->{reached}, "$label proceeds to postscripts");
|
||||
}
|
||||
is($state->{uname}, "original uname\n", "$label restores the image uname");
|
||||
if ($os =~ /^openeuler/ && $failure ne 'os-install') {
|
||||
like($state->{config}, qr{baseurl=https://packages\.example/native\n},
|
||||
"$label retains the explicit package URL");
|
||||
unlike($state->{config}, qr{baseurl=file://}, "$label does not create an unavailable local repository");
|
||||
}
|
||||
}
|
||||
}
|
||||
my ($rc, $state) = run_case('openeuler24.03sp3', 'none', '/var/tmp/native-packages');
|
||||
is($rc, 23, 'a native local package directory reaches postscripts');
|
||||
like($state->{config}, qr{baseurl=file:///var/tmp/native-packages/vendor\n},
|
||||
'native local package repositories retain their package-list subdirectory');
|
||||
|
||||
done_testing();
|
||||
|
||||
sub run_case {
|
||||
my ($os, $failure, $otherdir) = @_;
|
||||
my $case = tempdir(DIR => $dir, CLEANUP => 1);
|
||||
my $root = "$case/image/rootimg";
|
||||
make_path("$root/bin", "$root/etc", "$root/dev", "$root/run", "$root/sbin");
|
||||
write_file("$root/bin/uname", "original uname\n");
|
||||
chmod 0755, "$root/bin/uname";
|
||||
write_file("$case/os.pkglist", "native-os\n");
|
||||
write_file("$case/extra.pkglist", "-pre-remove\nvendor/native-extra\n--post-remove\n");
|
||||
local %ENV = %ENV;
|
||||
@ENV{qw(GENIMAGE_SCRIPT GENIMAGE_ROOT GENIMAGE_CASE GENIMAGE_FAILURE XCATROOT)} =
|
||||
("$repo/xCAT-server/share/xcat/netboot/rh/genimage", $root, $case, $failure, "$repo/xCAT-server");
|
||||
my $pid = fork();
|
||||
die "fork: $!" unless defined($pid);
|
||||
if (!$pid) {
|
||||
open(STDOUT, '>', "$case/output") or die $!;
|
||||
open(STDERR, '>&', STDOUT) or die $!;
|
||||
exec $^X, "-I$repo/perl-xCAT", "-I$repo/xCAT-server/lib/perl",
|
||||
"-I$repo/xCAT-server/share/xcat/netboot/imgutils", $driver,
|
||||
'-o', $os, '-a', 'x86_64', '-p', 'compute', '--srcdir', 'https://os.example/native',
|
||||
'--pkglist', "$case/os.pkglist", '--otherpkgdir', $otherdir,
|
||||
'--otherpkglist', "$case/extra.pkglist", '--rootimgdir', "$case/image", 'native-fixture';
|
||||
die "exec: $!";
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
my $status = $? >> 8;
|
||||
open(my $file, '<', "$case/output") or die $!;
|
||||
my $output = do { local $/; <$file> };
|
||||
close($file);
|
||||
return ($status, retrieve("$case/result"), $output);
|
||||
}
|
||||
|
||||
sub write_file {
|
||||
my ($path, $contents) = @_;
|
||||
open(my $file, '>', $path) or die "write $path: $!";
|
||||
print {$file} $contents or die $!;
|
||||
close($file) or die $!;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
use File::Path qw(make_path);
|
||||
use File::Temp qw(tempdir);
|
||||
use Test::More;
|
||||
use XCAT::Test::File qw(repo_path);
|
||||
|
||||
my $dir = tempdir(CLEANUP => 1);
|
||||
make_path("$dir/db", "$dir/install");
|
||||
$ENV{XCATROOT} = repo_path('xCAT-server');
|
||||
$ENV{XCATCFG} = "SQLite:$dir/db";
|
||||
require xCAT::Table;
|
||||
require xCAT::TableUtils;
|
||||
require xCAT::SvrUtils;
|
||||
|
||||
my $site = xCAT::Table->new('site', -create => 1);
|
||||
$site->setAttribs({ key => 'installdir' }, { value => "$dir/install" });
|
||||
$site->close();
|
||||
|
||||
for my $case (
|
||||
['openeuler20.03sp4', 'x86_64'], ['openeuler22.03sp4', 'x86_64'],
|
||||
['openeuler24.03sp1', 'x86_64'], ['openeuler24.03sp3', 'x86_64'],
|
||||
['openeuler24.03sp4', 'x86_64'], ['openeuler24.03', 'ppc64le'],
|
||||
) {
|
||||
my ($os, $arch) = @$case;
|
||||
subtest "$os $arch" => sub {
|
||||
my $media = "$dir/install/$os/$arch";
|
||||
my $distro = "$os-$arch";
|
||||
my @install = xCAT::SvrUtils->update_tables_with_templates($os, $arch, $media, $distro);
|
||||
is($install[0], 0, 'install profiles create real image records');
|
||||
my @diskless = xCAT::SvrUtils->update_tables_with_diskless_image($os, $arch, undef, 'netboot', $media, $distro);
|
||||
is($diskless[0], 0, 'diskless compute profile creates real image records');
|
||||
|
||||
my $images = xCAT::Table->new('osimage');
|
||||
my $linux = xCAT::Table->new('linuximage');
|
||||
for my $mode_profile (['install', 'compute'], ['install', 'service'], ['netboot', 'compute']) {
|
||||
my ($mode, $profile) = @$mode_profile;
|
||||
my $name = "$os-$arch-$mode-$profile";
|
||||
my $image = $images->getAttribs({ imagename => $name }, 'osvers', 'osarch', 'osdistroname', 'postscripts');
|
||||
is($image->{osvers}, $os, "$mode-$profile keeps exact release/SP");
|
||||
is($image->{osarch}, $arch, "$mode-$profile keeps native architecture");
|
||||
is($image->{osdistroname}, $distro, "$mode-$profile links its imported distribution");
|
||||
my $assets = $linux->getAttribs({ imagename => $name }, 'template', 'pkglist', 'pkgdir', 'postinstall', 'exlist', 'rootimgdir', 'otherpkglist');
|
||||
is($assets->{pkgdir}, $media, "$mode-$profile uses the imported package directory");
|
||||
ok(-r $assets->{pkglist}, "$mode-$profile selects an existing package list");
|
||||
if ($mode eq 'install') {
|
||||
ok(-r $assets->{template}, "$profile selects an existing installation template");
|
||||
} else {
|
||||
ok(-x $assets->{postinstall}, 'diskless postinstall resolves an executable shared script');
|
||||
ok(-r $assets->{exlist}, 'diskless exclude list resolves');
|
||||
is($assets->{rootimgdir}, "$dir/install/netboot/$os/$arch/compute", 'diskless root path keeps exact release and architecture');
|
||||
}
|
||||
is($image->{postscripts}, 'servicenode', 'install-service activates the Service Node postscript')
|
||||
if $profile eq 'service';
|
||||
if ($profile eq 'service') {
|
||||
like($assets->{otherpkglist}, qr{/service\.\Q$os\E\.\Q$arch\E\.otherpkgs\.pkglist$},
|
||||
'Service Node packages select the exact native release and architecture');
|
||||
ok(-r $assets->{otherpkglist}, 'Service Node package list exists');
|
||||
}
|
||||
}
|
||||
|
||||
my $install_name = "$os-$arch-install-compute";
|
||||
my $netboot_name = "$os-$arch-netboot-compute";
|
||||
$linux->setAttribs({ imagename => $install_name }, {
|
||||
template => '/admin/custom.tmpl', pkglist => '/admin/custom.pkglist', pkgdir => '/admin/media',
|
||||
});
|
||||
$linux->setAttribs({ imagename => $netboot_name }, {
|
||||
postinstall => '/admin/postinstall', rootimgdir => '/admin/retained-image', pkglist => '/admin/diskless.pkglist',
|
||||
});
|
||||
xCAT::SvrUtils->update_tables_with_templates($os, $arch, "$dir/reimport", $distro);
|
||||
xCAT::SvrUtils->update_tables_with_diskless_image($os, $arch, undef, 'netboot', "$dir/reimport", $distro);
|
||||
is_deeply($linux->getAttribs({ imagename => $install_name }, 'template', 'pkglist', 'pkgdir'),
|
||||
{ template => '/admin/custom.tmpl', pkglist => '/admin/custom.pkglist', pkgdir => '/admin/media' },
|
||||
'reimport preserves administrator install assets');
|
||||
is_deeply($linux->getAttribs({ imagename => $netboot_name }, 'postinstall', 'rootimgdir', 'pkglist'),
|
||||
{ postinstall => '/admin/postinstall', rootimgdir => '/admin/retained-image', pkglist => '/admin/diskless.pkglist' },
|
||||
'reimport preserves administrator diskless assets and retained image');
|
||||
|
||||
my @service = xCAT::SvrUtils->update_tables_with_diskless_image($os, $arch, 'service', 'netboot', $media, $distro);
|
||||
is($service[0], 0, 'the established explicit diskless Service Node workflow resolves');
|
||||
my $service_assets = $linux->getAttribs({ imagename => "$os-$arch-netboot-service" }, 'pkglist', 'rootimgdir', 'otherpkglist');
|
||||
like($service_assets->{pkglist}, qr{/service\.openeuler\.pkglist$}, 'diskless Service Node uses its service package list');
|
||||
is($service_assets->{rootimgdir}, "$dir/install/netboot/$os/$arch/service", 'diskless Service Node has an independent image root');
|
||||
ok(-r $service_assets->{otherpkglist}, 'diskless Service Node has a readable native xCAT package list');
|
||||
|
||||
for my $mode_profile (['install', 'compute'], ['install', 'service'],
|
||||
['netboot', 'compute'], ['netboot', 'service']) {
|
||||
my ($mode, $profile) = @$mode_profile;
|
||||
subtest "$mode-$profile reimport recovery" => sub {
|
||||
my $name = "$os-$arch-$mode-$profile";
|
||||
my $key = { imagename => $name };
|
||||
my @image_fields = qw(osvers osarch profile provmethod description synclists postscripts);
|
||||
my @asset_fields = qw(template pkgdir pkglist otherpkglist rootimgdir postinstall addkcmdline);
|
||||
my $reimport = sub {
|
||||
return $mode eq 'install'
|
||||
? xCAT::SvrUtils->update_tables_with_templates($os, $arch, $media, $distro)
|
||||
: xCAT::SvrUtils->update_tables_with_diskless_image($os, $arch, $profile, 'netboot', $media, $distro);
|
||||
};
|
||||
$images->setAttribs($key, {
|
||||
description => 'administrator image', synclists => '/admin/image.synclist',
|
||||
});
|
||||
my $original_image = $images->getAttribs($key, @image_fields);
|
||||
$linux->delEntries($key);
|
||||
ok(!$linux->getAttribs($key, 'imagename'), 'fixture leaves only the osimage record');
|
||||
my @result = $reimport->();
|
||||
is($result[0], 0, 'reimport repairs an interrupted image-pair creation');
|
||||
is_deeply($images->getAttribs($key, @image_fields), $original_image,
|
||||
'repair preserves the existing osimage customizations');
|
||||
my $repaired = $linux->getAttribs($key, @asset_fields);
|
||||
is($repaired->{pkgdir}, $media, 'the missing linuximage receives its native media directory');
|
||||
ok(-r $repaired->{pkglist}, 'the repaired image has a readable package list');
|
||||
if ($mode eq 'install') {
|
||||
ok(-r $repaired->{template}, 'the repaired installed image has a readable template');
|
||||
} else {
|
||||
is($repaired->{rootimgdir}, "$dir/install/netboot/$os/$arch/$profile",
|
||||
'the repaired diskless image has its native root directory');
|
||||
}
|
||||
|
||||
$linux->setAttribs($key, { pkgdir => '/admin/retained-media', addkcmdline => 'admin-option' });
|
||||
my $original_assets = $linux->getAttribs($key, @asset_fields);
|
||||
$images->delEntries($key);
|
||||
ok(!$images->getAttribs($key, 'imagename'), 'fixture leaves only the linuximage record');
|
||||
@result = $reimport->();
|
||||
is($result[0], 0, 'reimport creates the missing osimage record');
|
||||
is($images->getAttribs($key, 'osvers')->{osvers}, $os,
|
||||
'the restored osimage retains the exact native release');
|
||||
is_deeply($linux->getAttribs($key, @asset_fields), $original_assets,
|
||||
'repair preserves the existing linuximage customizations');
|
||||
|
||||
my $renamed_key = { imagename => "renamed-$name" };
|
||||
$images->setAttribs($renamed_key, $original_image);
|
||||
$linux->setAttribs($renamed_key, $original_assets);
|
||||
$images->delEntries($key);
|
||||
$linux->delEntries($key);
|
||||
@result = $reimport->();
|
||||
is($result[0], 0, 'a renamed matching image does not prevent importing defaults');
|
||||
ok($images->getAttribs($key, 'imagename'), 'the canonical osimage is created');
|
||||
ok($linux->getAttribs($key, 'imagename'), 'the canonical linuximage is created');
|
||||
is_deeply($images->getAttribs($renamed_key, @image_fields), $original_image,
|
||||
'the renamed osimage is preserved');
|
||||
is_deeply($linux->getAttribs($renamed_key, @asset_fields), $original_assets,
|
||||
'the renamed linuximage is preserved');
|
||||
};
|
||||
}
|
||||
$images->close();
|
||||
$linux->close();
|
||||
};
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use Digest::SHA qw(sha256_hex);
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path);
|
||||
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);
|
||||
|
||||
plan skip_all => 'requires Linux root with mount and network namespaces'
|
||||
unless $^O eq 'linux' && $> == 0;
|
||||
plan skip_all => 'mount and network namespaces unavailable'
|
||||
if system('unshare -mn -- true >/dev/null 2>&1');
|
||||
|
||||
my $tmp = tempdir(DIR => '/var/tmp', CLEANUP => !$ENV{XCAT_POST_REPOS_KEEP});
|
||||
diag("fixtures: $tmp") if $ENV{XCAT_POST_REPOS_KEEP};
|
||||
my $scriptdir = 'xCAT-server/share/xcat/install/scripts';
|
||||
my $postscript = slurp_repo_file("$scriptdir/post.rhels8");
|
||||
my $library = slurp_repo_file("$scriptdir/scriptlib");
|
||||
my $include = '#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/scriptlib#';
|
||||
$postscript =~ s/^\Q$include\E$/$library/m
|
||||
or BAIL_OUT('Unable to render the scriptlib include');
|
||||
my $preamble = <<'SH';
|
||||
compgen() { return 1; }
|
||||
nmcli() { return 0; }
|
||||
SH
|
||||
$postscript =~ s/\A(#![^\n]*\n)/$1$preamble/
|
||||
or BAIL_OUT('Unable to stage the network fixture');
|
||||
write_file("$tmp/post.rhels8", $postscript);
|
||||
is(system('/bin/bash', '-n', "$tmp/post.rhels8"), 0,
|
||||
'the complete rendered post owner has valid Bash syntax');
|
||||
write_file("$tmp/namespace", <<'SH');
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
[ "$(readlink /proc/self/ns/mnt)" != "$XCAT_POST_HOST_MOUNT" ]
|
||||
[ "$(readlink /proc/self/ns/net)" != "$XCAT_POST_HOST_NET" ]
|
||||
mount --make-rprivate /
|
||||
mount --bind "$XCAT_POST_REPOS_FIXTURE/etc" /etc
|
||||
exec /bin/bash "$XCAT_POST_REPOS_OWNER"
|
||||
SH
|
||||
chmod 0755, "$tmp/namespace" or die $!;
|
||||
my @legacy = qw(
|
||||
oracle-linux-ol8.repo oracle-linux-ol9.repo uek-ol8.repo uek-ol9.repo
|
||||
Rocky-AppStream.repo Rocky-BaseOS.repo Rocky-Extras.repo rocky.repo
|
||||
rocky-extras.repo CentOS-Base.repo centos.repo centos-addons.repo
|
||||
almalinux-ha.repo almalinux-nfv.repo almalinux-powertools.repo
|
||||
almalinux.repo almalinux-resilientstorage.repo almalinux-rt.repo
|
||||
);
|
||||
my $enabled = "[fixture]\nenabled=1\ngpgcheck=1\nbaseurl=https://repo.invalid/\n";
|
||||
my $disabled = "[fixture]\nenabled=0\ngpgcheck=1\nbaseurl=https://repo.invalid/\n";
|
||||
my $native = "[OS]\nenabled=1\ngpgcheck=1\n[update]\n \tenabled = 1\n[disabled]\nenabled=0\n#enabled=1\n";
|
||||
my $native_expected = "[OS]\nenabled=0\ngpgcheck=1\n[update]\nenabled=0\n[disabled]\nenabled=0\n#enabled=1\n";
|
||||
my $host_mount = readlink('/proc/self/ns/mnt');
|
||||
my $host_net = readlink('/proc/self/ns/net');
|
||||
my %host_repos = map { $_ => sha256_hex(read_file($_)) }
|
||||
glob('/etc/yum.repos.d/*.repo');
|
||||
|
||||
for my $case (
|
||||
['enabled', $native, $native_expected],
|
||||
['already-disabled', $native_expected, $native_expected],
|
||||
['absent', undef, undef],
|
||||
) {
|
||||
my ($name, $input, $expected) = @$case;
|
||||
my $fixture = "$tmp/$name";
|
||||
my $repos = "$fixture/etc/yum.repos.d";
|
||||
make_path($repos);
|
||||
copy('/etc/ld.so.cache', "$fixture/etc/ld.so.cache") or die $!
|
||||
if -f '/etc/ld.so.cache';
|
||||
write_file("$repos/$_", $enabled) for @legacy;
|
||||
write_file("$repos/openEuler.repo", $input) if defined $input;
|
||||
my @preserved = qw(local-repository-0.repo xCAT-custom.repo administrator.repo);
|
||||
write_file("$repos/$_", $enabled) for @preserved;
|
||||
subtest $name => sub {
|
||||
local %ENV = (%ENV, XCATDEBUGMODE => '0', MASTER_IP => '192.0.2.1',
|
||||
XCAT_POST_REPOS_FIXTURE => $fixture, XCAT_POST_REPOS_OWNER => "$tmp/post.rhels8",
|
||||
XCAT_POST_HOST_MOUNT => $host_mount, XCAT_POST_HOST_NET => $host_net,
|
||||
PATH => '/usr/sbin:/usr/bin:/sbin:/bin', LC_ALL => 'C');
|
||||
for my $pass (1, 2) {
|
||||
my $pid = fork(); die $! unless defined $pid;
|
||||
if (!$pid) {
|
||||
open(STDIN, '<', '/dev/null') or die $!;
|
||||
open(STDOUT, '>', "$fixture/stdout-$pass.log") or die $!;
|
||||
open(STDERR, '>', "$fixture/stderr-$pass.log") or die $!;
|
||||
exec('unshare', '-mn', '--', "$tmp/namespace") or die $!;
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
is($?, 0, "pass $pass runs the whole post owner in isolation")
|
||||
or diag(read_file("$fixture/stderr-$pass.log"));
|
||||
if (defined $expected) {
|
||||
is(read_file("$repos/openEuler.repo"), $expected,
|
||||
"pass $pass disables only active native repository assignments");
|
||||
} else {
|
||||
ok(!-e "$repos/openEuler.repo", "pass $pass does not create an absent vendor file");
|
||||
}
|
||||
is(read_file("$repos/$_"), $disabled, "pass $pass preserves policy for $_") for @legacy;
|
||||
is(read_file("$repos/$_"), $enabled, "pass $pass leaves $_ unchanged") for @preserved;
|
||||
}
|
||||
};
|
||||
}
|
||||
is_deeply({map { $_ => sha256_hex(read_file($_)) } glob('/etc/yum.repos.d/*.repo')},
|
||||
\%host_repos, 'host repository files remain unchanged');
|
||||
is(readlink('/proc/self/ns/mnt'), $host_mount, 'host mount namespace remains unchanged');
|
||||
is(readlink('/proc/self/ns/net'), $host_net, 'host network namespace remains unchanged');
|
||||
done_testing();
|
||||
|
||||
sub write_file {
|
||||
my ($path, $contents) = @_;
|
||||
open(my $fh, '>:raw', $path) or die "$path: $!";
|
||||
print {$fh} $contents;
|
||||
close($fh) or die "$path: $!";
|
||||
}
|
||||
|
||||
sub read_file {
|
||||
my ($path) = @_;
|
||||
open(my $fh, '<:raw', $path) or die "$path: $!";
|
||||
local $/;
|
||||
my $contents = <$fh>;
|
||||
close($fh) or die "$path: $!";
|
||||
return $contents // '';
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/share/xcat/netboot/imgutils";
|
||||
use Test::More;
|
||||
use imgutils;
|
||||
|
||||
for my $case (
|
||||
['openeuler20.03sp4', '20.03LTS_SP4'],
|
||||
['openeuler22.03sp4', '22.03LTS_SP4'],
|
||||
['openeuler24.03sp1', '24.03LTS_SP1'],
|
||||
['openeuler24.03sp3', '24.03LTS_SP3'],
|
||||
['openeuler24.03sp4', '24.03LTS_SP4'],
|
||||
['openeuler24.03', '24.03LTS'],
|
||||
) {
|
||||
my ($os, $release) = @$case;
|
||||
my $cmd = imgutils::rpm_installroot_command($os, '/var/tmp/native-image', '-y', 1);
|
||||
like($cmd, qr/^dnf -y /, "$os uses native DNF");
|
||||
like($cmd, qr/--releasever=\Q$release\E /, "$os retains the native RPM release value");
|
||||
like($cmd, qr{--installroot=/var/tmp/native-image/ }, "$os keeps the selected image root");
|
||||
unlike($cmd, qr/module_platform_id/, "$os does not fabricate an EL module platform");
|
||||
like($cmd, qr/--setopt=strict=1 /, "$os fails on missing requested packages");
|
||||
like($cmd, qr/'--setopt=\*\.gpgcheck=1' /, "$os verifies repository packages");
|
||||
like($cmd, qr/'--setopt=\*\.skip_if_unavailable=False' /, "$os requires all selected repositories");
|
||||
is(imgutils::el_major_version($os), undef, "$os is not an EL release");
|
||||
}
|
||||
|
||||
for my $os ('openeuler24.09', 'openeuler25.03', 'openeuler24.03sp0') {
|
||||
my $ok = eval { imgutils::rpm_installroot_command($os, '/var/tmp/native-image', '-y', 1); 1 };
|
||||
ok(!$ok, "$os cannot fall through to an unrelated package manager");
|
||||
like($@, qr/Unsupported openEuler image release/, 'invalid release reports its cause');
|
||||
}
|
||||
|
||||
my $ok = eval { imgutils::rpm_installroot_command('openeuler24.03sp3', '/var/tmp/native-image', '-y', 0); 1 };
|
||||
ok(!$ok, 'missing DNF fails before package installation');
|
||||
like($@, qr/openEuler image creation requires dnf/, 'missing native package manager is explicit');
|
||||
|
||||
my $repo = imgutils::rpm_repository_config('openeuler24.03sp3', 'compute-os',
|
||||
'https://mirror.example/OS/x86_64/', 'file:///tmp/trusted-keys');
|
||||
is($repo, "[compute-os]\nname=compute-os\nbaseurl=https://mirror.example/OS/x86_64/\n" .
|
||||
"gpgcheck=1\ngpgkey=file:///tmp/trusted-keys\nskip_if_unavailable=False\n\n",
|
||||
'native repository uses the builder trust set and requires availability');
|
||||
$ok = eval { imgutils::rpm_repository_config('openeuler24.03sp3', 'otherpkgs',
|
||||
'file:///install/otherpkgs'); 1 };
|
||||
ok(!$ok, 'native repository cannot silently omit its signing keys');
|
||||
is(imgutils::rpm_repository_config('rhels9.6', 'legacy-os', 'file:///install/rhels9.6/x86_64'),
|
||||
"[legacy-os]\nname=legacy-os\nbaseurl=file:///install/rhels9.6/x86_64\ngpgcheck=0\nskip_if_unavailable=True\n\n",
|
||||
'existing repository configuration is preserved');
|
||||
|
||||
done_testing();
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
no warnings 'once';
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
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);
|
||||
|
||||
my $dir = tempdir(CLEANUP => 1);
|
||||
make_path("$dir/db", "$dir/install", "$dir/custom");
|
||||
symlink(repo_path('xCAT/postscripts'), "$dir/install/postscripts") or die "postscripts link: $!";
|
||||
$ENV{XCATROOT} = repo_path('xCAT-server');
|
||||
$ENV{XCATCFG} = "SQLite:$dir/db";
|
||||
require xCAT::Table;
|
||||
require xCAT::Template;
|
||||
|
||||
my %site_values = (
|
||||
installdir => "$dir/install", tftpdir => "$dir/tftpboot", master => '192.0.2.1',
|
||||
timezone => 'UTC', xcatiport => 3002, xcatdport => 3001, httpport => 8080,
|
||||
xcatdebugmode => 0, nodestatus => 1,
|
||||
);
|
||||
my $site = xCAT::Table->new('site', -create => 1);
|
||||
for my $key (keys %site_values) {
|
||||
$site->setAttribs({ key => $key }, { value => $site_values{$key} });
|
||||
}
|
||||
$site->close();
|
||||
%::XCATSITEVALS = (%site_values, secureroot => 1, managedaddressmode => 'dhcp');
|
||||
|
||||
for my $entry (
|
||||
['nodelist', { node => 'oe-node', groups => 'all' }],
|
||||
['nodetype', { node => 'oe-node', os => 'openeuler24.03sp3', arch => 'x86_64', provmethod => 'oe-image' }],
|
||||
['noderes', { node => 'oe-node', xcatmaster => '192.0.2.1', nfsserver => '192.0.2.2', installnic => 'mac' }],
|
||||
['mac', { node => 'oe-node', mac => '52:54:00:12:34:56' }],
|
||||
) {
|
||||
my ($name, $values) = @$entry;
|
||||
my $tab = xCAT::Table->new($name, -create => 1);
|
||||
$tab->setAttribs({ node => 'oe-node' }, $values);
|
||||
$tab->close();
|
||||
}
|
||||
|
||||
for my $profile (qw(compute service)) {
|
||||
my $template = "$ENV{XCATROOT}/share/xcat/install/openeuler/$profile.openeuler.tmpl";
|
||||
my $pkglist = "$ENV{XCATROOT}/share/xcat/install/openeuler/$profile.openeuler.pkglist";
|
||||
my $custom = "$dir/custom/$profile.tmpl";
|
||||
write_text($custom, read_text($template));
|
||||
my $out = "$dir/$profile.ks";
|
||||
my $error = xCAT::Template->subvars($custom, $out, 'oe-node', $pkglist,
|
||||
'/install/custom-media,/install/supplementary', 'openeuler', undef, { xcatmaster => '192.0.2.1' });
|
||||
ok(!$error, "$profile renders with a template outside the shipped asset directory") or diag($error);
|
||||
my $ks = read_text($out);
|
||||
like($ks, qr/^network --onboot=yes --bootproto=dhcp --device=52:54:00:12:34:56 --hostname=oe-node$/m,
|
||||
"$profile renders its actual node network identity");
|
||||
like($ks, qr{url --url http://.*\$nextserver.*:8080//install/custom-media},
|
||||
"$profile generates the selected media URL with custom HTTP port");
|
||||
like($ks, qr{repo --name=pkg1 --baseurl=http://.*\$nextserver.*:8080//install/supplementary},
|
||||
"$profile includes the supplementary repository");
|
||||
like($ks, qr/^kernel$/m, "$profile includes the compute kernel package");
|
||||
like($ks, qr/^chrony$/m, "$profile includes native time support");
|
||||
like($ks, qr/^XCAT_INSTALL_PYTHON=\/usr\/bin\/python3$/m,
|
||||
"$profile selects the native installer interpreter");
|
||||
like($ks, qr/\$\{XCAT_INSTALL_PYTHON:-\/usr\/libexec\/platform-python\}/,
|
||||
"$profile executes the shared monitor with the selected interpreter");
|
||||
unlike($ks, qr/#(?:INCLUDE|TABLE|TABLEBLANKOKAY|CRYPT|ENV|XCATVAR|INSTALL_SOURCES)[^\n]*#/,
|
||||
"$profile leaves no unresolved input or repository substitutions");
|
||||
if ($profile eq 'service') {
|
||||
like($ks, qr/^perl-DBD-Pg$/m, 'service includes PostgreSQL support');
|
||||
like($ks, qr/^perl-DBD-MySQL$/m, 'service includes MySQL support');
|
||||
}
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
no warnings 'once';
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
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);
|
||||
|
||||
my $dir = tempdir(CLEANUP => 1);
|
||||
make_path("$dir/db", "$dir/install");
|
||||
$ENV{XCATROOT} = repo_path('xCAT-server');
|
||||
$ENV{XCATCFG} = "SQLite:$dir/db";
|
||||
require xCAT::Table;
|
||||
my $source = $ENV{XCAT_TEST_TEMPLATE_SOURCE}
|
||||
|| repo_path('xCAT-server/lib/perl/xCAT/Template.pm');
|
||||
require $source;
|
||||
|
||||
my %site_values = (
|
||||
installdir => "$dir/install", master => '192.0.2.1', httpport => 80,
|
||||
domain => 'example.invalid', autoulaprefix => 'fd00::',
|
||||
);
|
||||
my $site = xCAT::Table->new('site', -create => 1);
|
||||
for my $key (keys %site_values) {
|
||||
$site->setAttribs({ key => $key }, { value => $site_values{$key} });
|
||||
}
|
||||
$site->close();
|
||||
%::XCATSITEVALS = (%site_values, managedaddressmode => 'dhcp');
|
||||
write_text("$dir/network.tmpl", "#KICKSTARTNET#\n");
|
||||
|
||||
my $sequence = 0;
|
||||
sub render {
|
||||
my (%args) = @_;
|
||||
my $node = $args{node} || 'cn-' . ++$sequence;
|
||||
for my $entry (
|
||||
['nodelist', { groups => 'all' }],
|
||||
['noderes', {
|
||||
xcatmaster => '192.0.2.1', nfsserver => '192.0.2.1',
|
||||
installnic => $args{installnic} || '', primarynic => $args{primarynic} || '',
|
||||
}],
|
||||
['mac', { mac => $args{mac} || '52:54:00:12:34:56' }],
|
||||
) {
|
||||
my ($name, $values) = @$entry;
|
||||
my $table = xCAT::Table->new($name, -create => 1);
|
||||
$table->setAttribs({ node => $node }, $values);
|
||||
$table->close();
|
||||
}
|
||||
local $::XCATSITEVALS{managedaddressmode} = exists($args{mode}) ? $args{mode} : 'dhcp';
|
||||
my $output = "$dir/$node.ks";
|
||||
my $error = xCAT::Template->subvars("$dir/network.tmpl", $output, $node,
|
||||
undef, undef, $args{platform}, undef, { xcatmaster => '192.0.2.1' });
|
||||
ok(!$error, "$args{label} renders successfully") or diag($error);
|
||||
return (read_text($output), $node);
|
||||
}
|
||||
|
||||
for my $case (
|
||||
{ label => 'native DHCP by MAC', node => 'native-short', device => '52:54:00:12:34:56' },
|
||||
{ label => 'native DHCP preserves FQDN', node => 'native.example.invalid', installnic => 'enP1p12s0f0', device => 'enP1p12s0f0' },
|
||||
{ label => 'native DHCP by primary interface', primarynic => 'eth2', device => 'eth2' },
|
||||
{ label => 'native DHCP with explicit MAC', installnic => '52:54:AA:12:34:99', device => '52:54:aa:12:34:99' },
|
||||
{ label => 'native DHCP with absent address mode', mode => undef, installnic => 'eth0', device => 'eth0' },
|
||||
) {
|
||||
my ($output, $node) = render(%$case, platform => 'openeuler');
|
||||
is($output, "network --onboot=yes --bootproto=dhcp --device=$case->{device} --hostname=$node\n",
|
||||
"$case->{label} persists the existing node identity");
|
||||
}
|
||||
|
||||
for my $platform (undef, qw(rh SL centos alma ol fedora rocky)) {
|
||||
my $label = defined($platform) ? "$platform DHCP" : 'unspecified platform DHCP';
|
||||
my ($output) = render(label => $label, platform => $platform, installnic => 'eth0');
|
||||
is($output, "network --onboot=yes --bootproto=dhcp --device=eth0\n",
|
||||
"$label retains its exact previous output");
|
||||
}
|
||||
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *xCAT::NetworkUtils::getNodeNetworkCfg = sub {
|
||||
my (undef, $node) = @_;
|
||||
return ('192.0.2.20', $node, '192.0.2.1', '255.255.255.0');
|
||||
};
|
||||
local *xCAT::NetworkUtils::getNodeNameservers = sub {
|
||||
my (undef, $nodes) = @_;
|
||||
return { map { $_ => '192.0.2.1' } @$nodes };
|
||||
};
|
||||
for my $platform (qw(openeuler rh)) {
|
||||
my ($output, $node) = render(label => "$platform static", platform => $platform,
|
||||
mode => 'static', installnic => 'eth0');
|
||||
is($output, "network --onboot=yes --bootproto=static --device=eth0 --ip=192.0.2.20 --netmask=255.255.255.0 --gateway=192.0.2.1 --hostname=$node --nameserver=192.0.2.1\n",
|
||||
"$platform static retains one hostname and its previous addressing options");
|
||||
}
|
||||
}
|
||||
|
||||
for my $platform (qw(openeuler rh)) {
|
||||
my ($output) = render(label => "$platform autoula", platform => $platform,
|
||||
mode => 'autoula', installnic => 'eth0');
|
||||
is($output, "network --onboot=yes --bootproto=static --device=eth0 --noipv4 --ipv6=fd00::5054:00ff:fe12:3456\n",
|
||||
"$platform autoula retains its previous output");
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
no warnings 'once';
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
use File::Path qw(make_path);
|
||||
use File::Temp qw(tempdir);
|
||||
use Test::More;
|
||||
use XCAT::Test::File qw(repo_path);
|
||||
|
||||
plan skip_all => 'pre-install scripts require Linux bash' unless $^O eq 'linux' && -x '/bin/bash';
|
||||
my $tmp = tempdir(CLEANUP => 1);
|
||||
make_path("$tmp/db", "$tmp/bin");
|
||||
$ENV{XCATROOT} = repo_path('xCAT-server');
|
||||
$ENV{XCATCFG} = "SQLite:$tmp/db";
|
||||
require xCAT::Table;
|
||||
require xCAT::Template;
|
||||
%::XCATSITEVALS = (xcatdebugmode => 0, nodestatus => 0, secureroot => 1, httpport => 80, xcatiport => 3002);
|
||||
my $site = xCAT::Table->new('site', -create => 1);
|
||||
for my $key (keys %::XCATSITEVALS) {
|
||||
$site->setAttribs({key => $key}, {value => $::XCATSITEVALS{$key}});
|
||||
}
|
||||
$site->close();
|
||||
|
||||
sub write_file {
|
||||
my ($path, $contents) = @_;
|
||||
open(my $fh, '>', $path) or die "$path: $!";
|
||||
print {$fh} $contents;
|
||||
close($fh) or die "$path: $!";
|
||||
}
|
||||
sub read_file {
|
||||
my ($path) = @_;
|
||||
open(my $fh, '<', $path) or die "$path: $!";
|
||||
local $/;
|
||||
return <$fh> // '';
|
||||
}
|
||||
write_file("$tmp/bin/double", <<'SH');
|
||||
#!/bin/bash
|
||||
case "${0##*/}" in
|
||||
uname) printf '%s\n' "$XCAT_PART_ARCH" ;;
|
||||
blockdev) printf '%s\n' "$XCAT_PART_SECTORS" ;;
|
||||
udevadm)
|
||||
case "$*" in
|
||||
*--attribute-walk*) printf 'ATTR{size}=="%s"\nDRIVERS=="virtio_blk"\n' "$XCAT_PART_SECTORS" ;;
|
||||
*) printf 'DEVTYPE=disk\nDEVPATH=/devices/pci0000:00/virtio0/block/vda\nID_WWN=0x1234\n' ;;
|
||||
esac ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
SH
|
||||
chmod 0755, "$tmp/bin/double";
|
||||
for my $name (qw(uname blockdev udevadm mknod chown vgs vgchange vgremove logger python3)) {
|
||||
symlink 'double', "$tmp/bin/$name" or die $!;
|
||||
}
|
||||
my $small = 62914560;
|
||||
my $large = 8589934592;
|
||||
my @cases = (
|
||||
['native BIOS small', 'openeuler', 'x86_64', 0, $small, 1, 0, 0],
|
||||
['native BIOS large', 'openeuler', 'x86_64', 0, $large, 1, 0, 0],
|
||||
['native UEFI small', 'openeuler', 'x86_64', 1, $small, 0, 1, 0],
|
||||
['native UEFI large', 'openeuler', 'x86_64', 1, $large, 0, 1, 0],
|
||||
['native POWER small', 'openeuler', 'ppc64le', 0, $small, 0, 0, 1],
|
||||
['native POWER large', 'openeuler', 'ppc64le', 0, $large, 0, 0, 1],
|
||||
['legacy BIOS small', 'rhels8', 'x86_64', 0, $small, 0, 0, 0],
|
||||
['legacy BIOS large', 'rhels8', 'x86_64', 0, $large, 1, 0, 0],
|
||||
['legacy UEFI small', 'rhels8', 'x86_64', 1, $small, 0, 1, 0],
|
||||
['legacy UEFI large', 'rhels8', 'x86_64', 1, $large, 1, 1, 0],
|
||||
['legacy POWER large', 'rhels8', 'ppc64le', 0, $large, 1, 0, 1],
|
||||
['legacy POWER big endian', 'rhels8', 'ppc64', 0, $small, 0, 0, 1],
|
||||
['native BIOS static custom', 'openeuler', 'x86_64', 0, $small, 0, 0, 0, 'static'],
|
||||
['native BIOS script custom', 'openeuler', 'x86_64', 0, $small, 0, 0, 0, 'script'],
|
||||
['native UEFI custom', 'openeuler', 'x86_64', 1, $small, 0, 0, 0, 'static'],
|
||||
['native POWER custom', 'openeuler', 'ppc64le', 0, $large, 0, 0, 0, 'static'],
|
||||
);
|
||||
for my $case (@cases) {
|
||||
my ($label, $platform, $arch, $efi, $sectors, $bios, $esp, $prep, $custom) = @$case;
|
||||
my $fixture = tempdir(DIR => $tmp, CLEANUP => 1);
|
||||
make_path(map { "$fixture/$_" } qw(tmp dev proc sys/firmware var/log/xcat etc));
|
||||
make_path("$fixture/sys/firmware/efi") if $efi;
|
||||
write_file("$fixture/proc/cmdline", "inst.ks=http://192.0.2.1/node.ks\n");
|
||||
write_file("$fixture/proc/partitions", "major minor #blocks name\n252 0 " . ($sectors / 2) . " vda\n");
|
||||
my $template = "$fixture/input.tmpl";
|
||||
write_file($template, "#XCAT_PARTITION_START#\n%include /tmp/partitionfile\n#XCAT_PARTITION_END#\n%pre\n#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/pre.$platform#\n%end\n");
|
||||
my $partition = '';
|
||||
my $custom_data = "part / --fstype=ext4 --ondisk=vda --size=4096\nbootloader --boot-drive=vda\n";
|
||||
if ($custom) {
|
||||
$partition = "$fixture/custom";
|
||||
if ($custom eq 'script') {
|
||||
write_file($partition, "#!/bin/bash\ncat > \"\$XCAT_PART_FIXTURE/tmp/partitionfile\" <<'EOF'\n$custom_data" . "EOF\n");
|
||||
$partition = "s:$partition";
|
||||
} else {
|
||||
write_file($partition, $custom_data);
|
||||
}
|
||||
}
|
||||
local $ENV{PERSKCMDLINE} = '';
|
||||
local $ENV{MASTER_IP} = '192.0.2.1';
|
||||
my $error = xCAT::Template->subvars($template, "$fixture/output.ks", 'node', undef,
|
||||
'/install/media', $platform eq 'openeuler' ? 'openeuler' : 'rh', $partition,
|
||||
{xcatmaster => '192.0.2.1'});
|
||||
ok(!$error, "$label renders through the production Template entry point") or diag($error);
|
||||
my $rendered = read_file("$fixture/output.ks");
|
||||
my ($pre) = $rendered =~ /^%pre[^\n]*\n(.*?)^%end/ms;
|
||||
defined($pre) or die 'rendered Kickstart did not contain a pre-install script';
|
||||
$pre =~ s{(/(?:tmp|dev|proc|sys|etc|var)(?=/|\b)|/foo\.log)}{$fixture$1}g;
|
||||
$pre =~ s{/(?:usr/bin/python3|usr/libexec/platform-python)}{$tmp/bin/python3}g;
|
||||
write_file("$fixture/pre.sh", $pre);
|
||||
my ($rc, $output);
|
||||
{
|
||||
local %ENV = (%ENV, PATH => "$tmp/bin:/usr/bin:/bin:/usr/sbin:/sbin", XCAT_PART_ARCH => $arch,
|
||||
XCAT_PART_SECTORS => $sectors, XCAT_PART_FIXTURE => $fixture);
|
||||
delete @ENV{qw(XCAT_BIOSBOOT XCAT_BOOT_FSTYPE XCAT_ROOT_FSTYPE XCAT_INSTALL_PYTHON)};
|
||||
open(my $pipe, '-|', 'sh', '-c', 'exec "$@" 2>&1', 'sh', '/bin/bash', "$fixture/pre.sh") or die $!;
|
||||
$output = do {local $/; <$pipe>};
|
||||
close($pipe);
|
||||
$rc = $? >> 8;
|
||||
}
|
||||
is($rc, 0, "$label executes the complete generated pre-install script") or diag($output);
|
||||
my $layout = read_file("$fixture/tmp/partitionfile");
|
||||
is(scalar(() = $layout =~ /^part biosboot\b/gm), $bios, "$label BIOS boot partition count");
|
||||
is(scalar(() = $layout =~ m{^part /boot/efi\b}gm), $esp, "$label EFI system partition count");
|
||||
is(scalar(() = $layout =~ /^part prepboot\b/gm), $prep, "$label PReP boot partition count");
|
||||
if ($custom) {
|
||||
$layout =~ s/\s+\z//;
|
||||
$custom_data =~ s/\s+\z//;
|
||||
is($layout, $custom_data, "$label replaces all generated defaults with the administrator's layout");
|
||||
} else {
|
||||
my $disk = "$fixture/dev/vda";
|
||||
like($layout, qr/^ignoredisk --only-use=\Q$disk\E$/m, "$label retains actual selected disk");
|
||||
like($layout, qr/^part biosboot --ondisk=\Q$disk\E --size=1$/m, "$label reserves one MiB on the selected disk") if $bios;
|
||||
my $fstype = $platform eq 'openeuler' && $arch eq 'ppc64le' ? 'ext4' : 'xfs';
|
||||
like($layout, qr{^part /boot --fstype=$fstype }m, "$label retains boot filesystem policy");
|
||||
like($layout, qr{^logvol / --fstype=$fstype }m, "$label retains root filesystem policy");
|
||||
like($layout, qr/^bootloader --boot-drive=vda$/m, "$label retains selected boot drive");
|
||||
unlike($layout, qr/(?:disklabel|labeltype|--labeltype|--mbr)/, "$label does not force a disk label");
|
||||
}
|
||||
}
|
||||
done_testing();
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use FindBin;
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path);
|
||||
use File::Spec;
|
||||
use File::Temp qw(tempdir);
|
||||
use Test::More;
|
||||
|
||||
plan skip_all => 'requires Linux root and private mount namespaces'
|
||||
unless $^O eq 'linux' && $> == 0
|
||||
&& system('unshare', '--mount', '--propagation', 'private', '/bin/true') == 0;
|
||||
my $source = File::Spec->rel2abs("$FindBin::Bin/../../xCAT-server/share/xcat/netboot/rh/dracut_047/xcatroot");
|
||||
my $dir = tempdir(CLEANUP => 1);
|
||||
my $wrapper = "$dir/run";
|
||||
write_file($wrapper, <<'SH');
|
||||
#!/bin/bash
|
||||
set -e
|
||||
root=$1
|
||||
mount --bind /usr "$root/usr"
|
||||
mount -o remount,bind,ro "$root/usr"
|
||||
exec chroot "$root" /bin/bash -c '
|
||||
cd /
|
||||
/bin/bash /xcatroot eth0 none /sysroot
|
||||
rc=$?
|
||||
printf "XCATROOT_RC=%s\n" "$rc"
|
||||
printf "PAYLOAD="; cat /sysroot/native-marker
|
||||
if [ -f /sysroot/localdisk-ran ]; then echo LOCALDISK_RAN; fi
|
||||
if [ -f /hooks/initqueue/xcat.sh ]; then echo INITQUEUE_READY; fi
|
||||
exit "$rc"
|
||||
'
|
||||
SH
|
||||
chmod 0755, $wrapper;
|
||||
|
||||
for my $method (qw(cpio tar)) {
|
||||
for my $localdisk (qw(missing executable nonexecutable)) {
|
||||
my $case = tempdir(DIR => $dir, CLEANUP => 1);
|
||||
my $root = "$case/root";
|
||||
my $payload = "$case/payload";
|
||||
make_path(map { "$root/$_" } qw(usr lib etc tmp sysroot hooks/initqueue));
|
||||
symlink('usr/bin', "$root/bin") or die $!;
|
||||
symlink('usr/sbin', "$root/sbin") or die $!;
|
||||
symlink('usr/lib64', "$root/lib64") or die $!;
|
||||
make_path("$payload/etc/init.d", "$payload/etc/sysconfig/network-scripts", "$payload/var/lib/dhclient");
|
||||
write_file("$payload/native-marker", "native payload\n");
|
||||
write_file("$payload/etc/os-release", "ID=openEuler\nVERSION_ID=24.03\n");
|
||||
if ($localdisk ne 'missing') {
|
||||
write_file("$payload/etc/init.d/localdisk", "#!/bin/sh\ntouch /sysroot/localdisk-ran\n");
|
||||
chmod($localdisk eq 'executable' ? 0755 : 0644, "$payload/etc/init.d/localdisk");
|
||||
}
|
||||
write_file("$root/etc/resolv.conf", "nameserver 192.0.2.1\n");
|
||||
write_file("$root/tmp/dhclient.eth0.lease", "fixture lease\n");
|
||||
write_file("$root/lib/dracut-lib.sh", <<'SH');
|
||||
hookdir=/hooks
|
||||
logger() { :; }
|
||||
getarg()
|
||||
{
|
||||
case "$1" in
|
||||
XCAT=) echo 192.0.2.1:3001 ;;
|
||||
XCATIPORT=) echo 3002 ;;
|
||||
rootlimit=) echo 8M ;;
|
||||
xcatdebugmode=) echo 0 ;;
|
||||
ifname=) echo eth0:52:54:00:00:00:01 ;;
|
||||
nonodestatus) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
SH
|
||||
copy($source, "$root/xcatroot") or die $!;
|
||||
my $archive = "$root/rootimg.$method.gz";
|
||||
my @pack = $method eq 'cpio'
|
||||
? ('/bin/bash', '-o', 'pipefail', '-c', 'cd "$1" && find . -print0 | cpio -0 -o -H newc | gzip > "$2"', 'pack', $payload, $archive)
|
||||
: ('tar', '-C', $payload, '-czf', $archive, '.');
|
||||
is(system(@pack), 0, "$method $localdisk fixture archive is valid");
|
||||
my $pid = fork();
|
||||
die "fork: $!" unless defined($pid);
|
||||
if (!$pid) {
|
||||
open(STDOUT, '>', "$case/output") or die $!;
|
||||
open(STDERR, '>&', STDOUT) or die $!;
|
||||
exec 'unshare', '--mount', '--propagation', 'private', $wrapper, $root;
|
||||
die "exec: $!";
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
my $rc = $? >> 8;
|
||||
open(my $file, '<', "$case/output") or die $!;
|
||||
my $output = do { local $/; <$file> };
|
||||
close($file);
|
||||
my $label = "$method $localdisk localdisk";
|
||||
is($rc, 0, "$label completes the production xcatroot script") or diag($output);
|
||||
like($output, qr/^PAYLOAD=native payload$/m, "$label extracts the actual image payload");
|
||||
like($output, qr/^INITQUEUE_READY$/m, "$label reaches the initqueue handoff");
|
||||
unlike($output, qr{localdisk: (?:No such file|Permission denied)}, "$label avoids an invalid optional script invocation");
|
||||
if ($localdisk eq 'executable') {
|
||||
like($output, qr/^LOCALDISK_RAN$/m, "$label retains the legacy executable hook");
|
||||
} else {
|
||||
unlike($output, qr/^LOCALDISK_RAN$/m, "$label does not execute a missing or disabled hook");
|
||||
}
|
||||
}
|
||||
}
|
||||
done_testing();
|
||||
|
||||
sub write_file {
|
||||
my ($path, $contents) = @_;
|
||||
open(my $file, '>', $path) or die "write $path: $!";
|
||||
print {$file} $contents or die $!;
|
||||
close($file) or die $!;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
no warnings 'once';
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
use Cwd qw(getcwd);
|
||||
use File::Path qw(make_path);
|
||||
use File::Slurper qw(read_binary write_text);
|
||||
use File::Temp qw(tempdir);
|
||||
use Test::More;
|
||||
use XCAT::Test::File qw(repo_path);
|
||||
|
||||
plan skip_all => 'requires Linux cpio, gzip and tar'
|
||||
unless $^O eq 'linux' && -x '/usr/bin/cpio' && -x '/usr/bin/gzip' && -x '/usr/bin/tar';
|
||||
my $dir = tempdir(CLEANUP => 1);
|
||||
make_path("$dir/db", "$dir/root/lib/perl", "$dir/root/share", "$dir/install/postscripts");
|
||||
symlink(repo_path('perl-xCAT/xCAT'), "$dir/root/lib/perl/xCAT") or die $!;
|
||||
symlink(repo_path('xCAT-server/share/xcat'), "$dir/root/share/xcat") or die $!;
|
||||
write_text("$dir/install/postscripts/xcatdsklspost", "#!/bin/sh\nexit 0\n");
|
||||
$ENV{XCATROOT} = "$dir/root";
|
||||
$ENV{XCATCFG} = "SQLite:$dir/db";
|
||||
require xCAT::Table;
|
||||
require(repo_path('xCAT-server/lib/xcat/plugins/packimage.pm'));
|
||||
my $site = xCAT::Table->new('site', -create => 1);
|
||||
$site->setAttribs({ key => 'installdir' }, { value => "$dir/install" });
|
||||
$site->setAttribs({ key => 'secureroot' }, { value => '1' });
|
||||
my $passwd = xCAT::Table->new('passwd', -create => 1);
|
||||
$passwd->setAttribs({ key => 'system', username => 'root' }, { password => '*', cryptmethod => 'sha512' });
|
||||
my $images = xCAT::Table->new('osimage', -create => 1);
|
||||
my $linux = xCAT::Table->new('linuximage', -create => 1);
|
||||
my $sequence = 0;
|
||||
my ($unsquashfs) = grep { -x $_ } qw(/usr/bin/unsquashfs /usr/sbin/unsquashfs /sbin/unsquashfs);
|
||||
|
||||
for my $case (
|
||||
['cpio', 'cpio'], ['cpio', 'gzip'], ['cpio', 'find'],
|
||||
['tar', 'tar'], ['tar', 'gzip'], ['tar', 'find'],
|
||||
['cpio', ''], ['tar', ''],
|
||||
['cpio', 'xz', 'xz'], ['tar', 'pigz', 'pigz'],
|
||||
['cpio', '', 'xz'], ['tar', '', 'xz'],
|
||||
['cpio', '', 'pigz'], ['tar', '', 'pigz'],
|
||||
['squashfs', 'find'], ['squashfs', 'cpio'], ['squashfs', 'mksquashfs'],
|
||||
['squashfs', ''],
|
||||
) {
|
||||
my ($method, $failure, $compress) = @$case;
|
||||
$compress //= 'gzip';
|
||||
SKIP: {
|
||||
skip "requires $compress", 1 unless -x "/usr/bin/$compress";
|
||||
skip 'requires squashfs-tools', 1 if $method eq 'squashfs'
|
||||
&& (!$unsquashfs || (!-x '/usr/bin/mksquashfs' && !-x '/sbin/mksquashfs'));
|
||||
my $name = 'archive-' . ++$sequence;
|
||||
my $dest = "$dir/$name";
|
||||
make_path("$dest/rootimg/etc/rc.d/init.d", "$dest/rootimg/opt/xcat", "$dest/bin");
|
||||
symlink('rc.d/init.d', "$dest/rootimg/etc/init.d") or die $!;
|
||||
write_text("$dest/rootimg/etc/shadow", "root:*:1:0:99999:7:::\n");
|
||||
write_text("$dest/rootimg/etc/rc.d/init.d/statelite", "legacy StateLite script\n");
|
||||
write_text("$dest/rootimg/etc/rc.d/init.d/localdisk", "legacy StateLite storage script\n");
|
||||
write_text("$dest/rootimg/native-marker", "native image contents\n");
|
||||
my $suffix = $method eq 'squashfs' ? 'sfs' : "$method." . ($compress eq 'xz' ? 'xz' : 'gz');
|
||||
my $archive = "$dest/rootimg.$suffix";
|
||||
write_text($archive, "retained archive\n");
|
||||
write_text("$dest/rootimg.previous", "retained alternative\n");
|
||||
$images->setAttribs({ imagename => $name }, {
|
||||
osvers => 'openeuler24.03sp3', osarch => 'x86_64', profile => 'compute', provmethod => 'netboot' });
|
||||
$linux->setAttribs({ imagename => $name }, { rootimgdir => $dest });
|
||||
if ($failure) {
|
||||
write_text("$dest/bin/$failure", "#!/bin/sh\nprintf 'injected archive failure\\n' >&2\nexit 29\n");
|
||||
chmod 0755, "$dest/bin/$failure";
|
||||
}
|
||||
my $cwd = getcwd();
|
||||
my $mask = umask();
|
||||
my @staging = glob("/tmp/packimage.$$.????????");
|
||||
local $ENV{PATH} = "$dest/bin:$ENV{PATH}";
|
||||
no warnings 'redefine';
|
||||
local *xCAT::Utils::acquire_lock_imageop = sub { return (0, undef); };
|
||||
my @responses;
|
||||
my $result = xCAT_plugin::packimage::process_request(
|
||||
{ arg => ['--nosyncfiles', '-m', $method, '-c', $compress, $name] },
|
||||
sub { push @responses, @_ });
|
||||
my @errors = map { @{$_->{error} // []} } @responses;
|
||||
my $label = "$method $compress " . ($failure ? "$failure failure" : 'success');
|
||||
if ($failure) {
|
||||
ok($result && @errors, "$label is reported by the production pack command");
|
||||
is(archive_content($archive), "retained archive\n", "$label preserves the selected previous archive");
|
||||
is(archive_content("$dest/rootimg.previous"), "retained alternative\n", "$label preserves other previous formats");
|
||||
} else {
|
||||
ok(!$result && !@errors, "$label completes") or diag(@errors);
|
||||
isnt(archive_content($archive), "retained archive\n", "$label publishes new archive contents");
|
||||
my @integrity = $method eq 'squashfs' ? ($unsquashfs, '-s', $archive)
|
||||
: ("/usr/bin/$compress", '-t', $archive);
|
||||
is(system(@integrity), 0, "$label produces a valid archive");
|
||||
my @listing = $method eq 'cpio'
|
||||
? ('/bin/bash', '-o', 'pipefail', '-c', '"$1" -dc "$2" | cpio -t', 'archive-list', $compress, $archive)
|
||||
: $method eq 'squashfs' ? ($unsquashfs, '-l', $archive)
|
||||
: ('/usr/bin/tar', '-tf', $archive);
|
||||
open(my $pipe, '-|', @listing) or die "list archive: $!";
|
||||
my $contents = do { local $/; <$pipe> };
|
||||
close($pipe);
|
||||
is($?, 0, "$label produces a readable native archive");
|
||||
like($contents, qr/(?:^|\/)native-marker\n/m, "$label retains the image payload");
|
||||
unlike($contents, qr{etc/(?:rc\.d/)?init\.d/(?:statelite|localdisk)},
|
||||
"$label excludes StateLite scripts through the real init.d directory");
|
||||
ok(!-e "$dest/rootimg.previous", "$label retires stale formats only after successful publication");
|
||||
is((stat($archive))[2] & 0777, 0644, "$label publishes the expected archive mode");
|
||||
}
|
||||
is(getcwd(), $cwd, "$label restores the caller working directory");
|
||||
is(umask(), $mask, "$label restores the caller umask");
|
||||
is_deeply([glob("$dest/.packimage-*")], [], "$label leaves no partial archive");
|
||||
is_deeply([glob("/tmp/packimage.$$.????????")], \@staging, "$label removes temporary file staging");
|
||||
chdir($cwd) or die $!;
|
||||
umask($mask);
|
||||
}
|
||||
}
|
||||
done_testing();
|
||||
|
||||
sub archive_content {
|
||||
my ($path) = @_;
|
||||
return -f $path ? read_binary($path) : undef;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
|
||||
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);
|
||||
use xCAT::Table;
|
||||
|
||||
our ($capture, @commands);
|
||||
BEGIN {
|
||||
*CORE::GLOBAL::system = sub {
|
||||
return CORE::system(@_) unless $capture;
|
||||
push @commands, ['system', join(' ', @_)];
|
||||
return 0;
|
||||
};
|
||||
*CORE::GLOBAL::readpipe = sub {
|
||||
return CORE::readpipe($_[0]) unless $capture;
|
||||
push @commands, ['readpipe', $_[0]];
|
||||
return '';
|
||||
};
|
||||
}
|
||||
|
||||
my $fixture = tempdir(CLEANUP => 1);
|
||||
make_path("$fixture/db", "$fixture/root/lib/perl", "$fixture/root/share",
|
||||
"$fixture/install/postscripts");
|
||||
symlink(repo_path('perl-xCAT/xCAT'), "$fixture/root/lib/perl/xCAT") or die $!;
|
||||
symlink(repo_path('xCAT-server/share/xcat'), "$fixture/root/share/xcat") or die $!;
|
||||
write_text("$fixture/install/postscripts/xcatdsklspost", "fixture postscript\n");
|
||||
$ENV{XCATROOT} = "$fixture/root";
|
||||
$ENV{XCATCFG} = "SQLite:$fixture/db";
|
||||
require(repo_path('xCAT-server/lib/xcat/plugins/packimage.pm'));
|
||||
my $site = xCAT::Table->new('site', -create => 1);
|
||||
$site->setAttribs({key => 'installdir'}, {value => "$fixture/install"});
|
||||
$site->setAttribs({key => 'secureroot'}, {value => '1'});
|
||||
my $passwd = xCAT::Table->new('passwd', -create => 1);
|
||||
$passwd->setAttribs({key => 'system', username => 'root'},
|
||||
{password => '*', cryptmethod => 'sha512'});
|
||||
my $osimage = xCAT::Table->new('osimage', -create => 1);
|
||||
my $linuximage = xCAT::Table->new('linuximage', -create => 1);
|
||||
my $sequence = 0;
|
||||
|
||||
sub attempt {
|
||||
my ($os, $arch, $statelite) = @_;
|
||||
my $name = 'fixture-' . ++$sequence;
|
||||
my $dest = "$fixture/$name";
|
||||
my $root = "$dest/rootimg";
|
||||
make_path("$root/etc/init.d", "$root/opt/xcat", "$root/xcatpost",
|
||||
"$root/.statelite", "$root/usr/lib/dracut/modules.d/98xcat");
|
||||
write_text("$root/etc/shadow", "root:*:1:0:99999:7:::\n");
|
||||
write_text("$root/etc/init.d/statelite", "legacy script\n");
|
||||
write_text("$root/xcatpost/sentinel", "preserve on rejection\n");
|
||||
write_text("$root/usr/lib/dracut/modules.d/98xcat/install", "native module\n");
|
||||
write_text("$root/.statelite/litefile.save", "StateLite marker\n") if $statelite;
|
||||
$osimage->setAttribs({imagename => $name},
|
||||
{osvers => $os, osarch => $arch, profile => 'compute', provmethod => 'netboot'});
|
||||
$linuximage->setAttribs({imagename => $name}, {rootimgdir => $dest});
|
||||
my (@responses, $locks);
|
||||
$locks = 0;
|
||||
local $capture = 1;
|
||||
local @commands;
|
||||
no warnings qw(redefine once);
|
||||
local *xCAT::Utils::acquire_lock_imageop = sub { ++$locks; return (0, undef); };
|
||||
local *xCAT::Utils::runcmd = sub {
|
||||
push @commands, ['runcmd', $_[1]];
|
||||
$::RUNCMD_RC = 0;
|
||||
return ([]);
|
||||
};
|
||||
{
|
||||
my $output = '';
|
||||
open(my $stdout, '>', \$output) or die $!;
|
||||
local *STDOUT = $stdout;
|
||||
xCAT_plugin::packimage::process_request({arg => ['--nosyncfiles', '-c', 'fixture-stop', $name]},
|
||||
sub { push @responses, @_ });
|
||||
}
|
||||
return (\@responses, [@commands], $locks, $root);
|
||||
}
|
||||
|
||||
for my $case (['openeuler20.03sp4', 'x86_64'], ['openeuler22.03sp4', 'x86_64'],
|
||||
['openeuler24.03sp3', 'x86_64'], ['openeuler24.03', 'ppc64le']) {
|
||||
my ($responses, $commands, $locks, $root) = attempt(@$case, 0);
|
||||
ok(grep({ $_->{error} && $_->{error}[0] =~ /Invalid compress method/ } @$responses),
|
||||
"@$case reaches the archive boundary");
|
||||
is($locks, 1, 'native stateless image acquires its operation lock');
|
||||
ok(!grep({ $_->[1] =~ /ilitefile|\.statebackup|97xcat/ } @$commands),
|
||||
'native stateless image performs no StateLite lookup or restoration');
|
||||
is(read_text("$root/usr/lib/dracut/modules.d/98xcat/install"), "native module\n",
|
||||
'native dracut module remains intact');
|
||||
}
|
||||
|
||||
for my $case (['openeuler24.03sp3', 'x86_64', 1], ['openeuler24.03', 'ppc64le', 1],
|
||||
['openeuler25.03', 'x86_64', 0], ['openeuler24.03sp0', 'x86_64', 0],
|
||||
['openeuler24.03sp3', 's390x', 0]) {
|
||||
my ($responses, $commands, $locks, $root) = attempt(@$case);
|
||||
my $error = $case->[2] ? qr/StateLite images are not supported/ : qr/Unsupported openEuler image/;
|
||||
ok(grep({ $_->{error} && $_->{error}[0] =~ $error } @$responses), "@$case is rejected");
|
||||
is($locks, 0, 'rejection precedes image locking');
|
||||
is_deeply($commands, [], 'rejection precedes image commands');
|
||||
is(read_text("$root/xcatpost/sentinel"), "preserve on rejection\n",
|
||||
'rejected image retains its existing postscripts');
|
||||
}
|
||||
|
||||
for my $statelite (0, 1) {
|
||||
my ($responses, $commands) = attempt('rhels9.6', 'x86_64', $statelite);
|
||||
ok(grep({ $_->{error} && $_->{error}[0] =~ /Invalid compress method/ } @$responses),
|
||||
"existing EL image reaches the archive boundary, StateLite=$statelite");
|
||||
ok(grep({ $_->[0] eq 'runcmd' && $_->[1] =~ /^ilitefile / } @$commands),
|
||||
'existing EL StateLite lookup is preserved');
|
||||
}
|
||||
|
||||
done_testing();
|
||||
Reference in New Issue
Block a user