From aff8b1191eef52edae8ae0bda5d8e6ca7b5f2166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:59:50 -0300 Subject: [PATCH] test(genesis): cover native build inputs and release checks Evaluate native and EL RPM requirements. Exercise complete dracut module requests. Run the spec install phase against generated release fixtures. Cover missing identity fields and timezone failures. --- xCAT-test/unit/genesis_openeuler.t | 158 +++++++++++++++++++++ xCAT-test/unit/genesis_openeuler_release.t | 109 ++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 xCAT-test/unit/genesis_openeuler.t create mode 100644 xCAT-test/unit/genesis_openeuler_release.t 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 $!; +}