diff --git a/xCAT-test/unit/genesis_openeuler.t b/xCAT-test/unit/genesis_openeuler.t new file mode 100644 index 000000000..03a4ceda2 --- /dev/null +++ b/xCAT-test/unit/genesis_openeuler.t @@ -0,0 +1,158 @@ +use strict; +use warnings; + +use Cwd qw(abs_path); +use File::Path qw(make_path); +use File::Temp qw(tempdir); +use FindBin qw($RealBin); +use Test::More; + +plan skip_all => 'Linux native RPM and user namespace tools required' + unless $^O eq 'linux' && system('sh', '-c', 'command -v rpmspec >/dev/null && command -v unshare >/dev/null') == 0; + +my $root = abs_path("$RealBin/../.."); +my $spec = $ENV{XCAT_GENESIS_SPEC} // "$root/xCAT-genesis-builder/xCAT-genesis-base.spec"; +my $module = $ENV{XCAT_GENESIS_MODULE} // "$root/xCAT-genesis-builder/dracut_105/el/module-setup.sh"; +my $tmp = tempdir(CLEANUP => 1); +my $sequence = 0; + +for my $arch (qw(x86_64 ppc64le)) { + my ($status, $requires) = query_spec($spec, $arch, 2, 0); + is($status, 0, "$arch native spec renders successfully"); + my %requires = map { $_ => 1 } split /\n/, $requires; + ok($requires{kernel}, "$arch native kernel provides the boot image and module tree"); + ok(!grep($requires{$_}, qw(kernel-core kernel-modules kernel-modules-extra)), + "$arch native spec does not select EL kernel splits or unrelated module providers"); + ok($requires{tar}, "$arch explicitly requests its archive tool"); + ok($requires{openssl}, "$arch explicitly requests its TLS command"); + ok($requires{tzdata}, "$arch explicitly requests the native timezone database"); + is(!!$requires{dmidecode}, $arch eq 'x86_64' ? 1 : '', "$arch retains the correct DMI tool requirement"); + is(!!$requires{efibootmgr}, $arch eq 'x86_64' ? 1 : '', "$arch retains the correct EFI tool requirement"); + SKIP: { + skip 'Set XCAT_GENESIS_BASE_SPEC for a committed legacy baseline', 3 + unless $ENV{XCAT_GENESIS_BASE_SPEC}; + for my $rhel (8, 9, 10) { + my ($old_status, $old_requires) = query_spec($ENV{XCAT_GENESIS_BASE_SPEC}, $arch, 0, $rhel); + my ($new_status, $new_requires) = query_spec($spec, $arch, 0, $rhel); + is_deeply([$new_status, $new_requires], [$old_status, $old_requires], + "$arch EL$rhel BuildRequires match the committed baseline"); + } + } +} + +SKIP: { + my ($probe_status) = run('unshare', '--user', '--map-root-user', '--mount', 'true'); + skip 'Unprivileged user/mount namespaces unavailable', 13 if $probe_status; + make_path("$tmp/native-etc", "$tmp/legacy-etc", "$tmp/zones/Etc", "$tmp/zones/Native", "$tmp/modules/fixture", "$tmp/empty-zones"); + write_file("$tmp/native-etc/openEuler-release", "openEuler release 24.03 (LTS-SP3)\n"); + write_file("$tmp/native-etc/os-release", "ID=openEuler\n"); + write_file("$tmp/legacy-etc/redhat-release", "Rocky Linux release 10\n"); + write_file("$tmp/legacy-etc/os-release", "ID=rocky\n"); + write_file("$tmp/zones/Etc/UTC", "native UTC fixture\n"); + write_file("$tmp/zones/Native/Fixture", "native timezone fixture\n"); + symlink('Etc/UTC', "$tmp/zones/UTC") or die $!; + write_file("$tmp/modules/fixture/modules.dep", join("\n", + 'kernel/drivers/infiniband/ulp/ipoib/ib_ipoib.ko.xz:', + 'kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko:', + 'kernel/drivers/infiniband/hw/mlx5/mlx5_ib.ko.zst:', '')); + my $wrapper = "$tmp/module-trace.sh"; + write_file($wrapper, <<'SH'); +mount --make-rslave / || exit +mount --bind "$2" /etc || exit +mount --bind "$3" /usr/share/zoneinfo || exit +mount --bind "$4" /lib/modules || exit +source "$1" +moddir=${1%/*} +dracut_install() { printf 'file\t%s\n' "$@"; } +inst() { + [[ ${FAIL_TIMEZONE:-} == "$1" ]] && return 73 + printf 'file\t%s\n' "${2:-$1}" +} +find() { + [[ ${FAIL_FIND:-0} == 1 ]] && return 74 + command find "$@" +} +inst_script() { printf 'script\t%s\n' "${1##*/}"; } +inst_dir() { printf 'directory\t%s\n' "$@"; } +inst_hook() { printf 'hook\t%s\t%s\t%s\n' "$1" "$2" "${3##*/}"; } +instmods() { printf 'module\t%s\n' "$@"; } +kernel=fixture +if [[ $5 == kernel ]]; then installkernel; else install; fi +SH + my ($native_status, $native) = trace_module($module, "$tmp/native-etc", 'install'); + is($native_status, 0, 'native full dracut module installation requests succeed'); + like($native, qr{^file\t/etc/openEuler-release$}m, 'native release file is a mandatory install request'); + like($native, qr{^file\t/etc/os-release$}m, 'native os-release is a mandatory install request'); + unlike($native, qr{^file\t/etc/redhat-release$}m, 'native payload retains its own distribution identity'); + my @zones = sort grep { /^file\t\/usr\/share\/zoneinfo\// } split /\n/, $native; + is_deeply(\@zones, [map { "file\t/usr/share/zoneinfo/$_" } qw(Etc/UTC Native/Fixture UTC)], + 'native payload copies every packaged timezone file and symlink'); + { + local $ENV{FAIL_TIMEZONE} = '/usr/share/zoneinfo/Native/Fixture'; + my ($status, $output) = trace_module($module, "$tmp/native-etc", 'install'); + is($status, 73, 'a native timezone install failure fails dracut'); + unlike($output, qr{^file\t/sbin/xcatroot$}m, 'a later successful copy cannot hide a timezone failure'); + } + { + local $ENV{FAIL_FIND} = 1; + my ($status, $output) = trace_module($module, "$tmp/native-etc", 'install'); + is($status, 74, 'a native timezone enumeration failure fails dracut'); + unlike($output, qr{^file\t/sbin/xcatroot$}m, 'installation stops when timezone enumeration fails'); + } + my ($empty_status) = trace_module($module, "$tmp/native-etc", 'install', "$tmp/empty-zones"); + isnt($empty_status, 0, 'an empty native timezone database fails dracut'); + my ($kernel_status, $drivers) = trace_module($module, "$tmp/native-etc", 'kernel'); + is_deeply([$kernel_status, $drivers], [0, "module\tib_ipoib\nmodule\te1000e\nmodule\tmlx5_ib\n"], + 'the full kernel installer retains Ethernet and InfiniBand modules across compression formats'); + my ($legacy_status, $legacy) = trace_module($module, "$tmp/legacy-etc", 'install'); + is($legacy_status, 0, 'legacy full module installation requests succeed'); + SKIP: { + skip 'Set XCAT_GENESIS_BASE_MODULE for a committed legacy baseline', 1 + unless $ENV{XCAT_GENESIS_BASE_MODULE}; + my ($old_status, $old) = trace_module($ENV{XCAT_GENESIS_BASE_MODULE}, "$tmp/legacy-etc", 'install'); + my @old_requests = sort split /\n/, $old; + my @new_requests = sort split /\n/, $legacy; + is_deeply([$legacy_status, \@new_requests], [$old_status, \@old_requests], + 'all legacy install requests match the committed module baseline'); + } + + sub trace_module { + my ($path, $etc, $mode, $zones) = @_; + return run('unshare', '--user', '--map-root-user', '--mount', 'bash', + "$tmp/module-trace.sh", $path, $etc, $zones // "$tmp/zones", "$tmp/modules", $mode); + } +} + +done_testing(); + +sub query_spec { + my ($path, $arch, $native, $rhel) = @_; + return run('rpmspec', '-q', '--buildrequires', '--target', $arch, + '--define', 'version 2.19.0', '--define', 'release oevalidation', + '--define', "openEuler $native", '--define', "rhel $rhel", $path); +} + +sub run { + my @command = @_; + my $log = "$tmp/command-" . ++$sequence; + my $pid = fork(); + die $! unless defined($pid); + if (!$pid) { + open(STDOUT, '>', $log) or die $!; + open(STDERR, '>', "$log.stderr") or die $!; + exec(@command) or die $!; + } + waitpid($pid, 0); + my $status = $? >> 8; + open(my $fh, '<', $log) or die $!; + my $output = do { local $/; <$fh> } // ''; + close($fh); + return ($status, $output); +} + +sub write_file { + my ($path, $text) = @_; + open(my $fh, '>', $path) or die $!; + print {$fh} $text; + close($fh) or die $!; +} diff --git a/xCAT-test/unit/genesis_openeuler_release.t b/xCAT-test/unit/genesis_openeuler_release.t new file mode 100644 index 000000000..c21be6dda --- /dev/null +++ b/xCAT-test/unit/genesis_openeuler_release.t @@ -0,0 +1,109 @@ +use strict; +use warnings; + +use Cwd qw(abs_path); +use File::Basename qw(dirname); +use File::Copy qw(copy); +use File::Find qw(find); +use File::Path qw(make_path); +use File::Temp qw(tempdir); +use FindBin qw($RealBin); +use Test::More; + +plan skip_all => 'requires native openEuler, an unprivileged user, rpmbuild and user namespaces' + unless $^O eq 'linux' && $> && -f '/etc/openEuler-release' + && -d '/usr/lib/dracut' && system('sh', '-c', 'command -v rpmbuild >/dev/null && unshare -Ur -m true') == 0; + +my $root = abs_path("$RealBin/../.."); +my $spec = $ENV{XCAT_GENESIS_RELEASE_SPEC} // "$root/xCAT-genesis-builder/xCAT-genesis-base.spec"; +my $tmp = tempdir(CLEANUP => 1); +my $support = "$tmp/build/xCAT-genesis-base-build-support"; +make_path("$support/dracut_105/el", "$tmp/bin"); +find({ no_chdir => 1, wanted => sub { + return unless -f $_; + my $relative = substr($_, length("$root/xCAT-genesis-builder/dracut_105/el/")); + my $target = "$support/dracut_105/el/$relative"; + make_path(dirname($target)); + copy($_, $target) or die "copy $_: $!"; +} }, "$root/xCAT-genesis-builder/dracut_105/el"); +copy("$root/xCAT-genesis-builder/80-net-name-slot.rules", "$support/80-net-name-slot.rules") or die $!; +write_file("$tmp/bin/dracut", <<'SH'); +#!/bin/bash +set -eu +while (($#)); do + if [[ $1 == -f ]]; then + shift + image=$1 + fi + kernel=$1 + shift +done +tree="$GENESIS_RELEASE_CASE/initrd" +mkdir -p "$tree/etc" "$tree/lib/modules/$kernel/kernel" "$tree/usr/share/zoneinfo" "$tree/usr/lib/dracut/hooks" +cp /etc/openEuler-release "$tree/etc/" +case "$GENESIS_RELEASE_FAILURE" in + ID|VERSION_ID) sed "/^${GENESIS_RELEASE_FAILURE}=/d" /etc/os-release > "$tree/etc/os-release" ;; + none) cp /etc/os-release "$tree/etc/os-release" ;; +esac +printf 'kernel/fixture.ko:\n' > "$tree/lib/modules/$kernel/modules.dep" +printf 'fixture\n' > "$tree/lib/modules/$kernel/kernel/fixture.ko" +cp -L /usr/share/zoneinfo/UTC "$tree/usr/share/zoneinfo/UTC" +(cd "$tree" && find . -print0 | cpio --null -o -H newc | gzip > "$image") +SH +chmod 0755, "$tmp/bin/dracut" or die $!; +write_file("$tmp/run.sh", <<'SH'); +set -eu +mount --make-rslave / +mount --bind "$GENESIS_RELEASE_CASE/dracut" /usr/lib/dracut +if [[ -d /usr/share/dracut ]]; then + mount --bind "$GENESIS_RELEASE_CASE/dracut" /usr/share/dracut +fi +mount --bind "$GENESIS_RELEASE_CASE/run" /run +for directory in /usr/share/perl5 /usr/lib64/perl5 /usr/local/lib64/perl5 /usr/local/share/perl5 /usr/share/ntp/lib; do + if [[ -d $directory ]]; then + mount --bind "$GENESIS_RELEASE_CASE/empty" "$directory" + fi +done +exec rpmbuild -bi --short-circuit --nodeps --target x86_64 \ + --define 'version 2.19.0' --define 'release oereleasetest' \ + --define "_topdir $GENESIS_RELEASE_CASE/rpm" \ + --define "_builddir $GENESIS_RELEASE_BUILD" \ + "$GENESIS_RELEASE_SPEC" +SH + +for my $failure (qw(none ID VERSION_ID)) { + my $case = "$tmp/$failure"; + make_path("$case/dracut/modules.d", "$case/run", "$case/rpm", "$case/empty"); + local %ENV = %ENV; + @ENV{qw(GENESIS_RELEASE_CASE GENESIS_RELEASE_FAILURE GENESIS_RELEASE_BUILD GENESIS_RELEASE_SPEC)} = + ($case, $failure, "$tmp/build", $spec); + $ENV{PATH} = "$tmp/bin:$ENV{PATH}"; + my $pid = fork(); + die "fork: $!" unless defined $pid; + if (!$pid) { + open(STDOUT, '>', "$case/output") or die $!; + open(STDERR, '>&', STDOUT) or die $!; + exec 'unshare', '--user', '--map-root-user', '--mount', 'bash', "$tmp/run.sh"; + die "exec: $!"; + } + waitpid($pid, 0); + my $status = $?; + open(my $output, '<', "$case/output") or die $!; + my $text = do { local $/; <$output> }; + close($output) or die $!; + if ($failure eq 'none') { + is($status, 0, 'complete native spec install accepts both release identity fields') or diag($text); + } else { + isnt($status, 0, "complete native spec install rejects missing $failure") or diag($text); + like($text, qr/\Q$failure\E: unbound variable/, "$failure cannot inherit its value from the build host") or diag($text); + } +} + +done_testing(); + +sub write_file { + my ($path, $text) = @_; + open(my $file, '>', $path) or die "write $path: $!"; + print {$file} $text or die $!; + close($file) or die $!; +}