diff --git a/xCAT-test/unit/openeuler_genimage_bootfiles.t b/xCAT-test/unit/openeuler_genimage_bootfiles.t new file mode 100644 index 000000000..067e869ed --- /dev/null +++ b/xCAT-test/unit/openeuler_genimage_bootfiles.t @@ -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 $!; +} diff --git a/xCAT-test/unit/openeuler_genimage_dispatch.t b/xCAT-test/unit/openeuler_genimage_dispatch.t new file mode 100644 index 000000000..649447214 --- /dev/null +++ b/xCAT-test/unit/openeuler_genimage_dispatch.t @@ -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(); diff --git a/xCAT-test/unit/openeuler_genimage_transactions.t b/xCAT-test/unit/openeuler_genimage_transactions.t new file mode 100644 index 000000000..a9f80d160 --- /dev/null +++ b/xCAT-test/unit/openeuler_genimage_transactions.t @@ -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 $!; +} diff --git a/xCAT-test/unit/openeuler_image_creation.t b/xCAT-test/unit/openeuler_image_creation.t new file mode 100644 index 000000000..577811bd3 --- /dev/null +++ b/xCAT-test/unit/openeuler_image_creation.t @@ -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(); diff --git a/xCAT-test/unit/openeuler_install_repositories.t b/xCAT-test/unit/openeuler_install_repositories.t new file mode 100644 index 000000000..ebbe23df2 --- /dev/null +++ b/xCAT-test/unit/openeuler_install_repositories.t @@ -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 // ''; +} diff --git a/xCAT-test/unit/openeuler_installroot.t b/xCAT-test/unit/openeuler_installroot.t new file mode 100644 index 000000000..ef7f141d1 --- /dev/null +++ b/xCAT-test/unit/openeuler_installroot.t @@ -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(); diff --git a/xCAT-test/unit/openeuler_kickstart.t b/xCAT-test/unit/openeuler_kickstart.t new file mode 100644 index 000000000..29a9920af --- /dev/null +++ b/xCAT-test/unit/openeuler_kickstart.t @@ -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(); diff --git a/xCAT-test/unit/openeuler_kickstart_hostname.t b/xCAT-test/unit/openeuler_kickstart_hostname.t new file mode 100644 index 000000000..8ffa66bbc --- /dev/null +++ b/xCAT-test/unit/openeuler_kickstart_hostname.t @@ -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(); diff --git a/xCAT-test/unit/openeuler_pre_partition.t b/xCAT-test/unit/openeuler_pre_partition.t new file mode 100644 index 000000000..92d0cb89f --- /dev/null +++ b/xCAT-test/unit/openeuler_pre_partition.t @@ -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(); diff --git a/xCAT-test/unit/openeuler_xcatroot.t b/xCAT-test/unit/openeuler_xcatroot.t new file mode 100644 index 000000000..9239083d6 --- /dev/null +++ b/xCAT-test/unit/openeuler_xcatroot.t @@ -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 $!; +} diff --git a/xCAT-test/unit/packimage_openeuler_archive.t b/xCAT-test/unit/packimage_openeuler_archive.t new file mode 100644 index 000000000..8f4111a93 --- /dev/null +++ b/xCAT-test/unit/packimage_openeuler_archive.t @@ -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; +} diff --git a/xCAT-test/unit/packimage_openeuler_stateless.t b/xCAT-test/unit/packimage_openeuler_stateless.t new file mode 100644 index 000000000..46bfc7825 --- /dev/null +++ b/xCAT-test/unit/packimage_openeuler_stateless.t @@ -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();