2
0
mirror of https://github.com/xcat2/xcat-dep.git synced 2026-09-12 12:36:23 +00:00
Files
xcat-dep/t/genesis_openembedded_release.t
T
2026-08-24 22:30:32 -03:00

533 lines
20 KiB
Perl

use strict;
use warnings;
use Cwd qw(abs_path);
use File::Copy qw(copy);
use File::Path qw(make_path remove_tree);
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/lib";
use XCAT::BuildUtils qw(
command_exists
digest_file
read_binary
write_binary
);
use XCAT::GenesisRelease qw(
architectures
deb_package_name
rpm_package_name
validated_release_checksums
validate_architecture
validate_complete_release
validate_export
validate_release
verify_release_file
);
use XCAT::GenesisReleaseTest qw(
copy_tree
dies_like
make_export
run_capture
write_checksums
write_release_manifest
);
my $repo_root = abs_path("$FindBin::Bin/..");
my $packager = "$repo_root/genesis-openembedded/package";
my $builder = "$repo_root/genesis-openembedded/build";
my $verifier = "$repo_root/genesis-openembedded/verify-release";
my $revision = 'a' x 40;
my $version = '2.19.0';
my $release = 'snap202608210726';
my $epoch = 1787293573;
if ($ENV{XCAT_GENESIS_CI}) {
for my $command (qw(git dpkg-deb rpm rpmbuild tar)) {
BAIL_OUT("CI requires $command") unless command_exists($command);
}
}
is_deeply(
[ architectures() ],
[ qw(x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64) ],
'supported architectures keep their exact xCAT names',
);
is(rpm_package_name('ppc64le'), 'xCAT-genesis-openembedded-ppc64le',
'RPM package keeps ppc64le distinct');
is(deb_package_name('x86_64'), 'xcat-genesis-openembedded-x86-64',
'DEB package uses a legal spelling of x86_64');
dies_like(sub { validate_architecture('ppc') }, qr/Unsupported Genesis architecture/,
'legacy ppc alias is rejected');
my $tmp = tempdir(CLEANUP => 1);
my $export = make_export("$tmp/export", 'x86_64');
ok(validate_export($export, 'x86_64'), 'valid export passes');
dies_like(sub { validate_export($export, 'ppc64le') }, qr/architecture mismatch/,
'wrong architecture fails');
my $missing = make_export("$tmp/missing", 'x86_64');
unlink("$missing/image.vex.json") or die $!;
write_checksums($missing);
dies_like(sub { validate_export($missing, 'x86_64') }, qr/missing image\.vex\.json/,
'missing release evidence fails');
my $corrupt = make_export("$tmp/corrupt", 'x86_64');
write_binary("$corrupt/kernel", 'changed');
dies_like(sub { validate_export($corrupt, 'x86_64') }, qr/Checksum mismatch for kernel/,
'corrupt payload fails');
my $unexpected = make_export("$tmp/unexpected", 'x86_64');
write_binary("$unexpected/extra", 'not part of the export');
write_checksums($unexpected);
dies_like(sub { validate_export($unexpected, 'x86_64') }, qr/Unexpected Genesis export file/,
'unlisted export files fail');
my $linked = make_export("$tmp/linked", 'x86_64');
unlink("$linked/kernel") or die $!;
symlink('initramfs.cpio.gz', "$linked/kernel") or die $!;
dies_like(sub { validate_export($linked, 'x86_64') }, qr/Symbolic links are not allowed/,
'export symlinks fail');
my $riscv = make_export("$tmp/riscv", 'riscv64');
ok(-f "$riscv/fw_jump.elf", 'RISC-V export carries firmware');
ok(validate_export($riscv, 'riscv64'), 'RISC-V export passes');
my $release_dir = "$tmp/release";
make_path("$release_dir/rpm", "$release_dir/srpm", "$release_dir/deb");
for my $architecture (qw(x86_64 ppc64le)) {
my $rpm = rpm_package_name($architecture);
my $deb = deb_package_name($architecture);
write_binary("$release_dir/rpm/$rpm-$version-$release.noarch.rpm", "rpm $architecture");
write_binary("$release_dir/srpm/$rpm-$version-$release.src.rpm", "srpm $architecture");
write_binary("$release_dir/deb/${deb}_${version}-${release}_all.deb", "deb $architecture");
}
write_release_manifest(
$release_dir, $version, $release, $revision, $epoch,
'x86_64,ppc64le', 'deb,rpm',
);
write_checksums($release_dir);
my $manifest = validate_release($release_dir);
is($manifest->{xcat_revision}, $revision, 'release records xcat-core revision');
my $qualified_release = "$tmp/qualified-release";
my $qualified_release_name = '1+deb~1';
my $qualified_deb = deb_package_name('x86_64');
make_path("$qualified_release/deb");
write_binary(
"$qualified_release/deb/${qualified_deb}_${version}-${qualified_release_name}_all.deb",
'deb x86_64',
);
write_release_manifest(
$qualified_release, $version, $qualified_release_name, $revision, $epoch,
'x86_64', 'deb',
);
write_checksums($qualified_release);
ok(validate_release($qualified_release), 'package filenames accept valid release qualifiers');
my $checksum_reads = 0;
my $verified_checksums;
{
no warnings 'redefine';
my $reader = \&XCAT::GenesisRelease::read_checksum_manifest;
local *XCAT::GenesisRelease::read_checksum_manifest = sub {
$checksum_reads++;
return $reader->(@_);
};
$verified_checksums = validated_release_checksums($release_dir);
}
is($checksum_reads, 1, 'validated checksums use the verified manifest read');
my $verified_relative = "rpm/xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
my $verified_copy = "$tmp/verified-copy.rpm";
copy("$release_dir/$verified_relative", $verified_copy) or die $!;
ok(verify_release_file($verified_checksums, $verified_relative, $verified_copy),
'collected package matches the verified release');
write_binary($verified_copy, 'changed after verification');
dies_like(
sub { verify_release_file($verified_checksums, $verified_relative, $verified_copy) },
qr/Collected release file checksum mismatch/,
'release changes after verification are rejected',
);
copy("$release_dir/$verified_relative", $verified_copy) or die $!;
my $copied_file_log = "$tmp/copied-file.log";
is(
run_capture(
$copied_file_log,
$verifier,
'--checksum-file', "$release_dir/SHA256SUMS",
'--relative-file', $verified_relative,
'--copied-file', $verified_copy,
),
0,
'verifier accepts a copied release file',
);
write_binary($verified_copy, 'changed after verification');
isnt(
run_capture(
$copied_file_log,
$verifier,
'--checksum-file', "$release_dir/SHA256SUMS",
'--relative-file', $verified_relative,
'--copied-file', $verified_copy,
),
0,
'verifier rejects a changed copied file',
);
like(
read_binary($copied_file_log),
qr/Collected release file checksum mismatch/,
'copied-file failure names the checksum mismatch',
);
dies_like(
sub { validate_complete_release($release_dir) },
qr/Genesis release is missing supported architectures/,
'partial release cannot be published',
);
my $complete_release = "$tmp/complete-release";
make_path("$complete_release/rpm", "$complete_release/srpm", "$complete_release/deb");
for my $architecture (architectures()) {
my $rpm = rpm_package_name($architecture);
my $deb = deb_package_name($architecture);
write_binary("$complete_release/rpm/$rpm-$version-$release.noarch.rpm", "rpm $architecture");
write_binary("$complete_release/srpm/$rpm-$version-$release.src.rpm", "srpm $architecture");
write_binary("$complete_release/deb/${deb}_${version}-${release}_all.deb", "deb $architecture");
}
write_release_manifest(
$complete_release, $version, $release, $revision, $epoch,
join(',', architectures()), 'deb,rpm',
);
write_checksums($complete_release);
ok(validate_complete_release($complete_release), 'complete release can be published');
my $deb_only_release = "$tmp/deb-only-release";
make_path("$deb_only_release/deb");
for my $architecture (architectures()) {
my $deb = deb_package_name($architecture);
write_binary("$deb_only_release/deb/${deb}_${version}-${release}_all.deb", "deb $architecture");
}
write_release_manifest(
$deb_only_release, $version, $release, $revision, $epoch,
join(',', architectures()), 'deb',
);
write_checksums($deb_only_release);
my $verify_all_log = "$tmp/verify-all.log";
isnt(run_capture($verify_all_log, $verifier, $deb_only_release), 0,
'all-format verification rejects a single-format release');
like(read_binary($verify_all_log), qr/Release does not contain rpm packages/,
'all-format failure names the missing format');
my $bad_release = "$tmp/bad-release";
copy_tree($release_dir, $bad_release);
write_binary("$bad_release/rpm/stale.rpm", 'stale');
write_checksums($bad_release);
dies_like(sub { validate_release($bad_release) }, qr/Unexpected Genesis release artifact/,
'stale package fails');
my $missing_release = "$tmp/missing-release";
copy_tree($release_dir, $missing_release);
unlink("$missing_release/rpm/xCAT-genesis-openembedded-ppc64le-$version-$release.noarch.rpm") or die $!;
write_checksums($missing_release);
dies_like(sub { validate_release($missing_release) }, qr/Genesis release is missing/,
'incomplete architecture set fails');
SKIP: {
skip 'git is not installed', 2 unless command_exists('git');
my $source = "$tmp/dirty-xcat-core";
make_path("$source/xCAT-genesis-builder/oe");
write_binary("$source/Version", "$version\n");
write_binary("$source/xCAT-genesis-builder/oe/build", "#!/bin/sh\nexit 99\n");
write_binary("$source/xCAT-genesis-builder/oe/export", "#!/bin/sh\nexit 99\n");
for my $command (
[ 'git', '-C', $source, 'init', '-q' ],
[ 'git', '-C', $source, 'add', '.' ],
[ 'git', '-C', $source, '-c', 'user.name=xCAT test',
'-c', 'user.email=xcat-test@example.invalid', 'commit', '-qm', 'fixture' ],
) {
die "Cannot prepare test repository\n"
if run_capture("$tmp/git-fixture.log", @{$command});
}
write_binary("$source/untracked", "not part of the commit\n");
my $log = "$tmp/dirty-source.log";
isnt(
run_capture(
$log, $builder, '--xcat-source', $source,
'--output-dir', "$tmp/dirty-output",
),
0,
'release builder rejects untracked source files',
);
like(read_binary($log), qr/xcat-core checkout is not clean/,
'dirty checkout failure is explicit');
}
SKIP: {
skip 'rpmbuild and rpm are not installed', 18
unless command_exists('rpmbuild') && command_exists('rpm');
exercise_packager('rpm');
}
SKIP: {
skip 'rpmbuild and rpm are not installed', 2
unless command_exists('rpmbuild') && command_exists('rpm');
skip 'root can traverse an unsearchable working directory', 2 if $> == 0;
exercise_packager_from_unsearchable_cwd();
}
SKIP: {
skip 'dpkg-deb is not installed', 12 unless command_exists('dpkg-deb');
exercise_packager('deb');
}
SKIP: {
skip 'git and dpkg-deb are not installed', 6
unless command_exists('git') && command_exists('dpkg-deb');
exercise_builder_tmpdir();
}
done_testing();
sub exercise_packager {
my ($format) = @_;
my $first = "$tmp/$format-first";
my $second = "$tmp/$format-second";
my $package_revision = sprintf('%032x%08x', time, $$);
my @command = (
$packager,
'--architecture', 'x86_64',
'--export-dir', $export,
'--version', $version,
'--release', $release,
'--revision', $package_revision,
'--source-date-epoch', $epoch,
'--format', $format,
);
my ($legacy_rpm_top, $legacy_rpm_work);
if ($format eq 'rpm') {
$legacy_rpm_top =
"/var/tmp/xcat-genesis-rpmbuild-$package_revision-x86_64-$version-$release";
$legacy_rpm_work = "$legacy_rpm_top/BUILD/stale";
make_path("$legacy_rpm_top/BUILD");
write_binary($legacy_rpm_work, 'stale work');
}
my $original_umask = umask(0022);
is(system(@command, '--output-dir', $first), 0,
"$format package builds with umask 0022");
umask(0002);
is(system(@command, '--output-dir', $second), 0,
"$format package rebuilds with umask 0002");
umask($original_umask);
is(sprintf('%04o', (stat($first))[2] & 0x0fff), '0755',
"$format output is readable and searchable");
is(sprintf('%04o', (stat($second))[2] & 0x0fff), '0755',
"$format rebuilt output is readable and searchable");
if (defined($legacy_rpm_work)) {
ok(-e $legacy_rpm_work, 'RPM package ignores the legacy fixed work path');
remove_tree($legacy_rpm_top);
}
my ($relative, $source_relative);
if ($format eq 'rpm') {
$relative = "rpm/xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
$source_relative = "srpm/xCAT-genesis-openembedded-x86_64-$version-$release.src.rpm";
} else {
$relative = "deb/xcat-genesis-openembedded-x86-64_${version}-${release}_all.deb";
}
ok(-f "$first/$relative", "$format binary exists");
is(digest_file("$first/$relative"), digest_file("$second/$relative"),
"$format binary is reproducible across umasks");
if ($format eq 'rpm') {
ok(-f "$first/$source_relative", 'source RPM exists');
is(digest_file("$first/$source_relative"), digest_file("$second/$source_relative"),
'source RPM is reproducible across umasks');
}
my $release_root = "$tmp/$format-release";
make_path($release_root);
copy_tree($first, $release_root);
write_release_manifest(
$release_root, $version, $release, $package_revision, $epoch, 'x86_64', $format,
);
write_checksums($release_root);
ok(validate_release($release_root), "$format release layout passes");
is(system($verifier, '--format', $format, $release_root), 0,
"$format package metadata passes");
my $contents_log = "$tmp/$format-contents.log";
if ($format eq 'rpm') {
is(run_capture($contents_log, 'rpm', '-qpl', "$first/$relative"), 0,
'RPM payload can be listed');
like(read_binary($contents_log),
qr{/opt/xcat/share/xcat/netboot/genesis-openembedded/x86_64/kernel},
'RPM uses the OpenEmbedded staging namespace');
like(read_binary($contents_log),
qr{^/usr/share/doc/xCAT-genesis-openembedded-x86_64/?$}m,
'RPM owns its documentation directory');
my $ownership_log = "$tmp/rpm-ownership.log";
is(
run_capture(
$ownership_log, 'rpm', '-qp',
'--qf', '[%{FILEUSERNAME}:%{FILEGROUPNAME}\n]',
"$first/$relative",
),
0,
'RPM file ownership can be read',
);
is_deeply(
[ grep { $_ ne 'root:root' } split(/\n/, read_binary($ownership_log)) ],
[],
'RPM owns every payload path as root',
);
is(run_capture($contents_log, 'rpm', '-qp', '--scripts', "$first/$relative"), 0,
'RPM script metadata can be read');
like(
read_binary($contents_log),
qr{posttrans scriptlet.*\n/usr/libexec/xcat/genesis-openembedded-activate-x86_64 x86_64\n}s,
'RPM refreshes the installed architecture after the transaction',
);
like(
read_binary($contents_log),
qr{postuninstall scriptlet.*mknb x86_64 --remove-openembedded}s,
'RPM retires published artifacts after erasing the image',
);
} else {
is(run_capture($contents_log, 'dpkg-deb', '-c', "$first/$relative"), 0,
'DEB payload can be listed');
like(read_binary($contents_log),
qr{/opt/xcat/share/xcat/netboot/genesis-openembedded/x86_64/kernel},
'DEB uses the OpenEmbedded staging namespace');
my $control = "$tmp/deb-control";
make_path($control);
is(run_capture($contents_log, 'dpkg-deb', '-e', "$first/$relative", $control), 0,
'DEB control files can be extracted');
opendir(my $control_dh, $control) or die $!;
my @control_files = sort grep { $_ ne '.' && $_ ne '..' } readdir($control_dh);
closedir($control_dh) or die $!;
is_deeply(\@control_files, [ qw(control md5sums postinst postrm) ],
'DEB contains the expected installation and removal scripts');
like(
read_binary("$control/postinst"),
qr{^#!/bin/sh\n/usr/libexec/xcat/genesis-openembedded-activate-x86_64 x86_64\nexit 0\n$},
'DEB refreshes the installed architecture after configuration',
);
like(
read_binary("$control/postrm"),
qr{mknb x86_64 --remove-openembedded},
'DEB retires published artifacts after erasing the image',
);
}
}
sub exercise_packager_from_unsearchable_cwd {
my $cwd = "$tmp/unsearchable-cwd";
my $output = "$tmp/cwd-independent-rpm";
my $log = "$tmp/cwd-independent-rpm.log";
make_path($cwd);
my $pid = fork();
die "Cannot fork: $!\n" unless defined($pid);
if ($pid == 0) {
chdir($cwd) or die "Cannot enter test directory: $!\n";
chmod(0000, $cwd) or die "Cannot restrict test directory: $!\n";
open(STDOUT, '>:raw', $log) or die $!;
open(STDERR, '>&', STDOUT) or die $!;
exec(
$packager,
'--architecture', 'x86_64',
'--export-dir', $export,
'--output-dir', $output,
'--version', $version,
'--release', $release,
'--revision', $revision,
'--source-date-epoch', $epoch,
'--format', 'rpm',
) or die "Cannot run packager: $!\n";
}
waitpid($pid, 0);
my $status = $? >> 8;
chmod(0700, $cwd) or die "Cannot restore test directory: $!\n";
is($status, 0, 'RPM package ignores an inaccessible inherited working directory');
ok(
-f "$output/rpm/xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm",
'RPM package is published from an inaccessible inherited working directory',
);
}
sub exercise_builder_tmpdir {
my $source = "$tmp/tmpdir-xcat-core";
my $oe = "$source/xCAT-genesis-builder/oe";
make_path($oe);
write_binary("$source/Version", "$version\n");
write_binary(
"$oe/build",
<<'BUILD',
#!/bin/sh
set -eu
expected=$XCAT_GENESIS_WORK_DIR/build/tmp
[ "${TMPDIR:-}" = "$expected" ] || exit 41
mkdir -p "$TMPDIR/deploy"
BUILD
);
write_binary(
"$oe/export",
<<'EXPORT',
#!/bin/sh
set -eu
architecture=$1
deploy=$2
output=$3
[ "$deploy" = "$XCAT_GENESIS_WORK_DIR/build/tmp/deploy" ] || exit 42
mkdir -p "$output"
printf '%s\n' kernel >"$output/kernel"
printf '%s\n' initramfs >"$output/initramfs.cpio.gz"
printf '%s\n' packages >"$output/image.manifest"
printf '%s\n' '{}' >"$output/image.spdx.json"
printf '%s\n' '{}' >"$output/image.vex.json"
printf '%s\n' licenses >"$output/license.manifest"
printf 'format=xcat-genesis\nversion=1\narchitecture=%s\n' "$architecture" \
>"$output/xcat-genesis.manifest"
(
cd "$output"
sha256sum -- * >SHA256SUMS
)
EXPORT
);
chmod(0755, "$oe/build", "$oe/export") or die $!;
for my $command (
[ 'git', '-C', $source, 'init', '-q' ],
[ 'git', '-C', $source, 'add', '.' ],
[ 'git', '-C', $source, '-c', 'user.name=xCAT test',
'-c', 'user.email=xcat-test@example.invalid', 'commit', '-qm', 'fixture' ],
) {
die "Cannot prepare test repository\n"
if run_capture("$tmp/tmpdir-git.log", @{$command});
}
my $ambient_tmp = "$tmp/ambient-tmp";
my $persistent_work = "$tmp/persistent-work";
my $output = "$tmp/tmpdir-release";
my $log = "$tmp/tmpdir-builder.log";
make_path($ambient_tmp);
my $status;
{
local $ENV{TMPDIR} = $ambient_tmp;
$status = run_capture(
$log, $builder, '--xcat-source', $source,
'--output-dir', $output, '--work-dir', $persistent_work,
'--format', 'deb',
);
}
is($status, 0, 'release builder isolates the OpenEmbedded tmpdir');
unlike(read_binary($log), qr/Invalid OpenEmbedded deploy directory/,
'release builder finds the configured deploy directory');
ok(-d "$persistent_work/openembedded/build/tmp",
'release builder preserves the requested work directory');
is((stat($output))[2] & oct('07777'), oct('0755'),
'release directory is readable by other users');
my $built = validate_release($output);
is($built->{architectures}, 'x86_64', 'isolated build keeps the target architecture');
is($built->{formats}, 'deb', 'isolated build keeps the requested format');
}