2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-02 00:41:42 +00:00

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.
This commit is contained in:
Vinícius Ferrão
2026-09-15 00:59:50 -03:00
parent 4a19444cd1
commit aff8b1191e
2 changed files with 267 additions and 0 deletions
+158
View File
@@ -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 $!;
}
+109
View File
@@ -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 $!;
}