mirror of
https://github.com/xcat2/xcat-dep.git
synced 2026-09-30 14:55:17 +00:00
56631f8f36
The apt publish reads staging/<codename>/<arch> of every expected arch, but takes only the run lock of the host arch. A ppc64el run can refill its staging while the publish assembles from it. Finalize runs on one host and takes the cell locks of both arches. If it dies, only that host can reclaim the locks of the other arch's cells, and the builds that own those cells wait on them and fail. These tests fail until the fix: the publish must refuse while any expected arch holds its run lock, and finalize --finalize-arch must write and lock only the cells of that arch. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
1279 lines
54 KiB
Perl
1279 lines
54 KiB
Perl
use strict;
|
|
use warnings;
|
|
|
|
use Cwd qw(abs_path);
|
|
use File::Basename qw(basename);
|
|
use File::Copy qw(copy);
|
|
use File::Path qw(make_path remove_tree);
|
|
use Fcntl qw(:flock);
|
|
use File::Temp qw(tempdir);
|
|
use FindBin;
|
|
use POSIX ();
|
|
use Test::More;
|
|
use Time::HiRes qw(sleep);
|
|
|
|
use lib "$FindBin::Bin/..";
|
|
use lib "$FindBin::Bin/../lib";
|
|
use lib "$FindBin::Bin/lib";
|
|
use BuildUtils qw(read_manifest);
|
|
use XCAT::BuildUtils qw(
|
|
capture_command
|
|
command_exists
|
|
digest_file
|
|
read_binary
|
|
write_binary
|
|
);
|
|
use XCAT::NFSLock ();
|
|
use XCAT::GenesisRelease qw(
|
|
architectures
|
|
deb_package_name
|
|
rpm_package_name
|
|
);
|
|
use XCAT::GenesisReleaseTest qw(
|
|
build_package_release
|
|
run_capture
|
|
write_checksums
|
|
write_forkmanager_stub
|
|
write_release_manifest
|
|
);
|
|
|
|
my $repo_root = abs_path("$FindBin::Bin/..");
|
|
my $packager = "$repo_root/genesis-openembedded/package";
|
|
my $rpm_consumer = "$repo_root/mockbuild-all.pl";
|
|
my $deb_consumer = "$repo_root/sbuild-all.pl";
|
|
my $revision = 'b' x 40;
|
|
my $version = '2.19.0';
|
|
my $release = 'snap202608210726';
|
|
my $epoch = 1787293573;
|
|
my $tmp = tempdir(CLEANUP => 1);
|
|
# every suite sbuild-all.pl knows; publishing a Genesis release must cover all of them
|
|
my @APT_SUITES = qw(focal jammy noble resolute);
|
|
|
|
test_activation_helper();
|
|
|
|
if ($ENV{XCAT_GENESIS_CI}) {
|
|
die('CI requires Linux root') unless $^O eq 'linux' && $> == 0;
|
|
for my $command (qw(apt-ftparchive bash createrepo_c dpkg-deb gpg rpm rpmbuild)) {
|
|
die("CI requires $command") unless command_exists($command);
|
|
}
|
|
}
|
|
|
|
SKIP: {
|
|
skip 'RPM repository tools require a root Linux builder', 64
|
|
unless $^O eq 'linux'
|
|
&& $> == 0
|
|
&& command_exists('rpmbuild')
|
|
&& command_exists('rpmsign')
|
|
&& command_exists('rpm')
|
|
&& command_exists('createrepo_c');
|
|
test_rpm_consumer();
|
|
test_signed_common_rpm_repository();
|
|
test_legacy_rpm_consumer();
|
|
test_partial_rpm_release();
|
|
test_failed_build_release();
|
|
test_skip_build_collects_results();
|
|
test_dry_run_release();
|
|
test_rpm_repository_lock();
|
|
test_finalize_cell_lock();
|
|
test_rpm_signal_cleanup();
|
|
}
|
|
|
|
SKIP: {
|
|
skip 'APT repository tools are not installed', 63
|
|
unless $^O eq 'linux'
|
|
&& command_exists('dpkg-deb')
|
|
&& command_exists('apt-ftparchive');
|
|
test_deb_consumer();
|
|
test_version_1_deb_consumer();
|
|
test_legacy_deb_consumer();
|
|
test_partial_deb_release();
|
|
test_publish_lock();
|
|
}
|
|
|
|
done_testing();
|
|
|
|
# mockbuild-all.pl builds and gates each target against <repo-root>/packages-manifest.conf, and a
|
|
# target with no section there is fatal -- so these runs, which use a synthetic target, need a
|
|
# section to exist at all. Its CONTENT is deliberately not meaningful: the dependency packages here
|
|
# are copies of one rpm, so no set of names describes them the way a real manifest describes a real
|
|
# build. The runs therefore pass --no-verify-repo and the completeness gate is covered where it can
|
|
# be tested honestly, against purpose-built rpms, in t/verify-repo-el.t. What these runs exercise is
|
|
# the Genesis release path.
|
|
#
|
|
# They pass --skip-genesis rather than the removed --skip-xcat: this script no longer builds the
|
|
# xCAT core, so xcat-core's buildrpms.pl is required only for the per-EL xCAT-genesis-base build.
|
|
sub write_target_manifest {
|
|
my ($root, $target) = @_;
|
|
make_path($root);
|
|
my %manifest = read_manifest("$repo_root/packages-manifest.conf");
|
|
my $common = join('', map { "$_=$manifest{common}{$_}\n" }
|
|
sort keys %{ $manifest{common} // {} });
|
|
write_binary(
|
|
"$root/packages-manifest.conf",
|
|
"[$target]\n" . rpm_package_name(capture_command('uname', '-m'))
|
|
. "=*\n\n[common]\n$common",
|
|
);
|
|
}
|
|
|
|
sub test_rpm_consumer {
|
|
my $release_root = make_package_release("$tmp/rpm", 'rpm');
|
|
my $package = "xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
|
my $output = "$tmp/rpm output";
|
|
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
|
my $run = "$target-consumer";
|
|
my $run_repo = "$output/mockbuild-all/$run/repo/" . capture_command('uname', '-m');
|
|
my $source_repo = "$output/mockbuild-all/$run/repo-src";
|
|
my $deploy_repo = "$output/xcat-dep/rh10/" . capture_command('uname', '-m');
|
|
my $common_repo = "$output/xcat-dep/common";
|
|
make_path($run_repo, $source_repo, $deploy_repo, $common_repo);
|
|
write_binary("$run_repo/xCAT-genesis-openembedded-stale.noarch.rpm", 'stale');
|
|
write_binary("$source_repo/xCAT-genesis-openembedded-stale.src.rpm", 'stale');
|
|
write_binary("$deploy_repo/xCAT-genesis-openembedded-stale.noarch.rpm", 'stale');
|
|
write_binary("$common_repo/xCAT-genesis-openembedded-stale.noarch.rpm", 'stale');
|
|
write_binary("$common_repo/xCAT-genesis-openembedded-stale.src.rpm", 'stale');
|
|
|
|
my $rpm_scripts = capture_command(
|
|
'rpm', '-qp', '--scripts', "$release_root/rpm/$package"
|
|
);
|
|
like($rpm_scripts, qr{genesis-openembedded-activate-x86_64 x86_64},
|
|
'the RPM refreshes its architecture after a package transaction');
|
|
like($rpm_scripts, qr{mknb x86_64 --remove-openembedded},
|
|
'the RPM removes published artifacts when the image is erased');
|
|
like($rpm_scripts, qr{/proc/1/root},
|
|
'the RPM removal path refuses to operate from a chroot');
|
|
like(
|
|
capture_command('rpm', '-qpl', "$release_root/rpm/$package"),
|
|
qr{/usr/libexec/xcat/genesis-openembedded-activate-x86_64$}m,
|
|
'the RPM contains its architecture-specific activation helper',
|
|
);
|
|
like(
|
|
capture_command('rpm', '-qpl', "$release_root/rpm/$package"),
|
|
qr{^/usr/libexec/xcat/?$}m,
|
|
'the RPM owns its private helper directory',
|
|
);
|
|
|
|
my $dependencies = "$tmp/rpm-dependencies";
|
|
my $scratch_repo_root = "$tmp/rpm-repo-root";
|
|
make_rpm_dependencies($dependencies, "$release_root/rpm/$package");
|
|
write_target_manifest($scratch_repo_root, $target);
|
|
write_binary(
|
|
"$dependencies/xCAT-genesis-openembedded-x86_64-$version-old.noarch.rpm",
|
|
'stale OpenEmbedded RPM',
|
|
);
|
|
write_binary(
|
|
"$dependencies/xCAT-genesis-openembedded-x86_64-$version-old.src.rpm",
|
|
'stale OpenEmbedded SRPM',
|
|
);
|
|
|
|
my @perl_lib;
|
|
push(@perl_lib, write_forkmanager_stub("$tmp/perl-stub"))
|
|
unless eval { require Parallel::ForkManager; 1 };
|
|
push(@perl_lib, $ENV{PERL5LIB})
|
|
if defined($ENV{PERL5LIB}) && $ENV{PERL5LIB} ne '';
|
|
local $ENV{PERL5LIB} = join(':', @perl_lib);
|
|
# An interrupted publication left a staging tree. A real run recovers it.
|
|
my $abandoned = "$output/xcat-dep/.common.abandoned";
|
|
make_path($abandoned);
|
|
my $log = "$tmp/rpm-consumer.log";
|
|
my $status = run_capture(
|
|
$log,
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $scratch_repo_root,
|
|
'--output', $output,
|
|
'--target', $target,
|
|
'--run-id', 'consumer',
|
|
'--build-timestamp', $epoch,
|
|
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
|
'--no-verify-repo',
|
|
'--skip-createrepo', '--skip-tarball',
|
|
'--genesis-release', $release_root,
|
|
'--collect-dir', $dependencies,
|
|
);
|
|
|
|
is($status, 0, 'RPM repository accepts a verified Genesis release');
|
|
ok(!-e $abandoned, 'the run removes the staging tree of an interrupted publication');
|
|
is(
|
|
sprintf('%04o', (stat($common_repo))[2] & 0x0fff),
|
|
'0755',
|
|
'the common repository is traversable by an unprivileged server',
|
|
);
|
|
is(digest_file("$common_repo/$package"), digest_file("$release_root/rpm/$package"),
|
|
'deployed RPM matches the release');
|
|
is(
|
|
sprintf('%04o', (stat("$common_repo/$package"))[2] & 0x0fff),
|
|
sprintf('%04o', (stat("$release_root/rpm/$package"))[2] & 0x0fff),
|
|
'deployed RPM keeps the release file mode',
|
|
);
|
|
opendir(my $deploy_dh, $common_repo) or die $!;
|
|
my @staging_files = grep { /^\.xcat-deploy\./ } readdir($deploy_dh);
|
|
closedir($deploy_dh) or die $!;
|
|
is_deeply(\@staging_files, [], 'RPM deployment leaves no staging files');
|
|
ok(!-e "$run_repo/xCAT-genesis-openembedded-stale.noarch.rpm",
|
|
'stale run RPM is removed');
|
|
ok(!-e "$source_repo/xCAT-genesis-openembedded-stale.src.rpm",
|
|
'stale source RPM is removed');
|
|
ok(!-e "$deploy_repo/xCAT-genesis-openembedded-stale.noarch.rpm",
|
|
'stale deployed RPM is removed');
|
|
ok(-f "$common_repo/repodata/repomd.xml", 'RPM repository metadata is generated');
|
|
ok(-f "$deploy_repo/xCAT-genesis-base-x86_64-1.noarch.rpm",
|
|
'legacy Genesis package remains available');
|
|
my @expected_packages = sort map {
|
|
rpm_package_name($_) . "-$version-$release.noarch.rpm"
|
|
} architectures();
|
|
my @common_packages = genesis_rpm_names($common_repo);
|
|
is_deeply(\@common_packages, \@expected_packages,
|
|
'common repository contains one complete Genesis release');
|
|
is_deeply(
|
|
[ genesis_rpm_names($deploy_repo) ],
|
|
[],
|
|
'per-EL repository contains no OpenEmbedded Genesis packages',
|
|
);
|
|
is_deeply(
|
|
[ genesis_rpm_names($run_repo) ],
|
|
[],
|
|
'target staging repository contains no OpenEmbedded Genesis packages',
|
|
);
|
|
is_deeply(
|
|
[ genesis_rpm_names($source_repo) ],
|
|
[],
|
|
'target source repository contains no OpenEmbedded Genesis packages',
|
|
);
|
|
my $common_config = read_binary("$common_repo/xcat-dep-common.repo");
|
|
like($common_config, qr/^\[xcat-dep-common\]$/m,
|
|
'common repository has its own repository ID');
|
|
like($common_config, qr{/xcat-dep/common$}m,
|
|
'common repository configuration uses the shared URL');
|
|
like($common_config, qr/^skip_if_unavailable=1$/m,
|
|
'the published common repository stays optional during outages');
|
|
like($common_config, qr/^repo_gpgcheck=0$/m,
|
|
'unsigned test metadata is declared explicitly');
|
|
ok(-x "$common_repo/mklocalrepo.sh", 'common repository supports offline setup');
|
|
my $local_repo_helper = read_binary("$common_repo/mklocalrepo.sh");
|
|
unlike($local_repo_helper, qr/\[\[/,
|
|
'the offline helper does not require Bash conditionals');
|
|
unlike($local_repo_helper, qr/`/,
|
|
'the offline helper uses POSIX command substitution');
|
|
like(read_binary("$common_repo/buildinfo.txt"), qr/^TARGET=common$/m,
|
|
'common repository records its target');
|
|
like(read_binary("$output/mockbuild-all/$run/summary.txt"), qr/^copied_rpms=8$/m,
|
|
'repository summary counts the collected dependencies');
|
|
|
|
my $retained = "$common_repo/xCAT-genesis-openembedded-retained.noarch.rpm";
|
|
my $repomd = "$common_repo/repodata/repomd.xml";
|
|
my $repomd_before = digest_file($repomd);
|
|
write_binary($retained, 'previous complete release');
|
|
my $fail_bin = "$tmp/rpm-fail-bin";
|
|
make_path($fail_bin);
|
|
write_binary(
|
|
"$fail_bin/createrepo_c",
|
|
<<'SH',
|
|
#!/bin/sh
|
|
for argument in "$@"; do last=$argument; done
|
|
case "$last" in
|
|
*/common|*/.common.*) exit 1 ;;
|
|
esac
|
|
exec "$XCAT_TEST_CREATEREPO" "$@"
|
|
SH
|
|
);
|
|
chmod(0755, "$fail_bin/createrepo_c") or die $!;
|
|
local $ENV{XCAT_TEST_CREATEREPO} = capture_command(
|
|
'sh', '-c', 'command -v createrepo_c'
|
|
);
|
|
local $ENV{PATH} = "$fail_bin:$ENV{PATH}";
|
|
my $failed_log = "$tmp/rpm-publication-failure.log";
|
|
my $failed_status = run_capture(
|
|
$failed_log,
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $scratch_repo_root,
|
|
'--output', $output,
|
|
'--target', $target,
|
|
'--run-id', 'publication-failure',
|
|
'--build-timestamp', $epoch,
|
|
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
|
'--no-verify-repo',
|
|
'--skip-createrepo', '--skip-tarball',
|
|
'--genesis-release', $release_root,
|
|
'--collect-dir', $dependencies,
|
|
);
|
|
isnt($failed_status, 0, 'a failed RPM metadata build aborts publication');
|
|
ok(-f $retained, 'a failed RPM publication keeps the previous package set');
|
|
is(digest_file($repomd), $repomd_before,
|
|
'a failed RPM publication keeps the previous metadata');
|
|
}
|
|
|
|
# The apt consumer is sbuild-all.pl's PUBLISH phase: it stages nothing here -- it assembles the
|
|
# already-staged debs plus the Genesis release into a side tree, gates it, and swaps that tree onto
|
|
# the published repository with a single rename. Every run below is therefore a publish-only run
|
|
# (--skip-build) over a hand-made staging tree, with the manifest reduced to the one package
|
|
# --skip-genesis drops, so the completeness gate has nothing to demand of a fixture.
|
|
|
|
sub run_apt_consumer {
|
|
my (%args) = @_;
|
|
my @dists = @{ $args{dists} // \@APT_SUITES };
|
|
my @build_mode = $args{build} ? ('--dry-run') : ('--skip-build');
|
|
my $manifest = $args{manifest};
|
|
unless ($manifest) {
|
|
$manifest = "$args{output}/manifest.conf";
|
|
write_apt_manifest($manifest);
|
|
}
|
|
return run_capture(
|
|
$args{log},
|
|
$^X, $deb_consumer,
|
|
'--repo-root', $repo_root,
|
|
'--output-root', $args{output},
|
|
'--apt-dir', $args{apt_dir},
|
|
'--manifest', $manifest,
|
|
'--dists', join(' ', @dists),
|
|
'--arch', 'amd64',
|
|
@build_mode, '--skip-genesis', '--skip-tarball',
|
|
'--publish', '--expect-arch', 'amd64 ppc64el',
|
|
($args{verify} ? () : ('--no-verify-repo')),
|
|
@{ $args{extra} // [] },
|
|
);
|
|
}
|
|
|
|
sub write_apt_manifest {
|
|
my ($path, $mutate) = @_;
|
|
my %shipped = read_manifest("$repo_root/debs-manifest.conf");
|
|
die('debs-manifest.conf has no [shared] section')
|
|
unless exists $shipped{shared};
|
|
my %shared = %{ $shipped{shared} };
|
|
$mutate->(\%shared) if $mutate;
|
|
make_path((File::Basename::dirname($path)));
|
|
write_binary(
|
|
$path,
|
|
join('', map { "[$_-amd64]\nxcat-genesis-base=*\n" } @APT_SUITES)
|
|
. "\n[shared]\n"
|
|
. join('', map { "$_=$shared{$_}\n" } sort keys %shared),
|
|
);
|
|
}
|
|
|
|
# Stage one legacy genesis-base deb for the noble suite only -- enough for the runs that publish
|
|
# no Genesis release.
|
|
sub stage_legacy_deb {
|
|
my ($root, $output) = @_;
|
|
my $staging = "$output/staging/noble/amd64";
|
|
make_path($staging);
|
|
make_legacy_deb($root, "$staging/xcat-genesis-base-amd64_1_all.deb");
|
|
return $staging;
|
|
}
|
|
|
|
# Stage one legacy genesis-base deb for each suite, standing in for a completed build.
|
|
sub stage_apt_suites {
|
|
my ($output, $package_root, $content) = @_;
|
|
my $package = "$package_root.deb";
|
|
make_legacy_deb($package_root, $package, $content);
|
|
for my $codename (@APT_SUITES) {
|
|
my $staged = "$output/staging/$codename/amd64";
|
|
make_path($staged);
|
|
copy($package, "$staged/xcat-genesis-base-amd64_1_all.deb") or die $!;
|
|
}
|
|
return "$output/staging/noble/amd64";
|
|
}
|
|
|
|
sub test_deb_consumer {
|
|
my $release_root = make_package_release("$tmp/deb", 'deb');
|
|
my $package = "xcat-genesis-openembedded-x86-64_${version}-${release}_all.deb";
|
|
my $apt_root = "$tmp/apt";
|
|
my $output = "$tmp/deb-output";
|
|
my $staged = stage_apt_suites($output, "$tmp/dummy-deb");
|
|
my $shared_pool = "$apt_root/pool/main/xcat-genesis-openembedded";
|
|
make_path($shared_pool);
|
|
write_binary("$staged/xcat-genesis-openembedded-stale.deb", 'stale');
|
|
write_binary("$shared_pool/xcat-genesis-openembedded-old.deb", 'stale');
|
|
|
|
my $postinst = capture_command(
|
|
'dpkg-deb', '--info', "$release_root/deb/$package", 'postinst'
|
|
);
|
|
isnt($postinst, '', 'the DEB includes a post-installation script');
|
|
like($postinst, qr{genesis-openembedded-activate-x86_64 x86_64},
|
|
'the DEB refreshes its architecture after configuration');
|
|
my $postrm = capture_command(
|
|
'dpkg-deb', '--info', "$release_root/deb/$package", 'postrm'
|
|
);
|
|
like($postrm, qr{mknb x86_64 --remove-openembedded},
|
|
'the DEB removes published artifacts when the image is erased');
|
|
like($postrm, qr{/proc/1/root},
|
|
'the DEB removal path refuses to operate from a chroot');
|
|
|
|
local $ENV{SOURCE_DATE_EPOCH} = $epoch;
|
|
my $log = "$tmp/deb-consumer.log";
|
|
my $status = run_apt_consumer(
|
|
log => $log, output => $output, apt_dir => $apt_root,
|
|
extra => [ '--genesis-release', $release_root ],
|
|
);
|
|
my $pool_package = "$shared_pool/$package";
|
|
my $amd64 = "$apt_root/dists/noble/main/binary-amd64/Packages";
|
|
|
|
is($status, 0, 'APT repository accepts a verified Genesis release');
|
|
is(digest_file($pool_package), digest_file("$release_root/deb/$package"),
|
|
'pooled DEB matches the release');
|
|
is(
|
|
sprintf('%04o', (stat($pool_package))[2] & 0x0fff),
|
|
'0644',
|
|
'the pooled DEB is readable by an unprivileged server',
|
|
);
|
|
ok(!-e "$shared_pool/xcat-genesis-openembedded-old.deb",
|
|
'stale pooled DEB is removed');
|
|
my @expected_packages = sort map {
|
|
deb_package_name($_) . "_${version}-${release}_all.deb"
|
|
} architectures();
|
|
is_deeply([ genesis_deb_names($shared_pool) ], \@expected_packages,
|
|
'shared APT pool contains one complete Genesis release');
|
|
like(read_binary($log),
|
|
qr/\[verify-repo\] shared pool complete: 8 packages present/,
|
|
'the shared pool is gated against the manifest\'s [shared] section');
|
|
my @suite_packages;
|
|
for my $codename (@APT_SUITES) {
|
|
push(@suite_packages, map { "$codename/$_" }
|
|
genesis_deb_names("$apt_root/pool/main/$codename"));
|
|
}
|
|
is_deeply(\@suite_packages, [], 'suite pools contain no OpenEmbedded Genesis packages');
|
|
for my $codename (@APT_SUITES) {
|
|
for my $architecture (qw(amd64 ppc64el)) {
|
|
my $packages = read_binary(
|
|
"$apt_root/dists/$codename/main/binary-$architecture/Packages"
|
|
);
|
|
like($packages,
|
|
qr{^Filename: pool/main/xcat-genesis-openembedded/\Q$package\E$}m,
|
|
"$codename $architecture index uses the shared Genesis package",
|
|
);
|
|
}
|
|
}
|
|
is_deeply(
|
|
[ grep { !-f "$apt_root/dists/$_/Release" } @APT_SUITES ],
|
|
[],
|
|
'APT Release metadata is generated for every suite',
|
|
);
|
|
like(read_binary($amd64), qr/^Package: xcat-genesis-base-amd64$/m,
|
|
'legacy Genesis DEB remains available');
|
|
ok(!-e "$apt_root/pool/main/noble/xcat-genesis-openembedded-stale.deb",
|
|
'a staged OpenEmbedded DEB is not published into a suite pool');
|
|
# The published package must be a file of its own: sharing an inode with the release
|
|
# would make a later write through either path change what the other one holds.
|
|
my @pooled = stat($pool_package);
|
|
my @released = stat("$release_root/deb/$package");
|
|
isnt("$pooled[0]:$pooled[1]", "$released[0]:$released[1]",
|
|
'pooled DEB is published independently of the release file');
|
|
my $publication_log = read_binary($log);
|
|
my $verified_at = index($publication_log, 'published + verified');
|
|
my $indexed_at = index($publication_log, 'assembled + ');
|
|
ok($verified_at >= 0 && $verified_at < $indexed_at,
|
|
'staged DEBs are verified before any suite index is generated');
|
|
|
|
# A release rewrites the shared pool every suite indexes, so it cannot be published for
|
|
# some suites only -- the others would be left indexing files the new release retired.
|
|
my $before_subset = digest_file($pool_package);
|
|
my $subset_log = "$tmp/deb-subset.log";
|
|
my $subset_status = run_apt_consumer(
|
|
log => $subset_log, output => $output, apt_dir => $apt_root,
|
|
dists => ['noble'],
|
|
extra => [ '--genesis-release', $release_root ],
|
|
);
|
|
isnt($subset_status, 0, 'Genesis publication rejects a partial suite update');
|
|
like(read_binary($subset_log), qr/must\n?\s*cover all of them/,
|
|
'partial suite failure explains the consistency requirement');
|
|
is(digest_file($pool_package), $before_subset,
|
|
'partial suite failure leaves the shared package unchanged');
|
|
|
|
# A later suite rebuild carries no release and must keep indexing the shared pool.
|
|
my $refresh_log = "$tmp/deb-refresh.log";
|
|
my $refresh_status = run_apt_consumer(
|
|
log => $refresh_log, output => $output, apt_dir => $apt_root,
|
|
dists => ['noble'],
|
|
);
|
|
is($refresh_status, 0, 'a suite refresh reuses the shared Genesis pool');
|
|
is(digest_file($pool_package), $before_subset,
|
|
'a suite refresh leaves the shared package unchanged');
|
|
like(read_binary($amd64),
|
|
qr{^Filename: pool/main/xcat-genesis-openembedded/\Q$package\E$}m,
|
|
'the refreshed suite still indexes the shared Genesis package');
|
|
|
|
# The repository changes with ONE rename, at the very end: a publication that fails while it
|
|
# assembles must leave every published byte -- packages, indexes and key -- exactly as it was.
|
|
# Signing with a key the keyring does not hold fails inside that phase.
|
|
my $package_before = digest_file(
|
|
"$apt_root/pool/main/noble/xcat-genesis-base-amd64_1_all.deb");
|
|
my $metadata_before = digest_file($amd64);
|
|
my $empty_keyring = "$tmp/apt-empty-keyring";
|
|
make_path($empty_keyring);
|
|
chmod(0700, $empty_keyring) or die $!;
|
|
my $failed_log = "$tmp/deb-failed-publish.log";
|
|
my $failed_status = run_apt_consumer(
|
|
log => $failed_log, output => $output, apt_dir => $apt_root,
|
|
dists => ['noble'],
|
|
extra => [ '--gpg-sign', '--gpg-key-id', 'absent@example.invalid',
|
|
'--gpg-home', $empty_keyring ],
|
|
);
|
|
isnt($failed_status, 0, 'a publication that cannot be signed is not published');
|
|
like(read_binary($failed_log), qr/NOT publishing/,
|
|
'the refusal says the published repository was left alone');
|
|
is(digest_file("$apt_root/pool/main/noble/xcat-genesis-base-amd64_1_all.deb"),
|
|
$package_before, 'a failed publication leaves the published package bytes');
|
|
is(digest_file($amd64), $metadata_before,
|
|
'a failed publication leaves the published package index');
|
|
is(digest_file($pool_package), $before_subset,
|
|
'a failed publication leaves the shared Genesis pool');
|
|
|
|
# A rebuilt deb keeps its filename; the published copy must become the new one.
|
|
my $legacy_pool = "$apt_root/pool/main/noble/xcat-genesis-base-amd64_1_all.deb";
|
|
my $legacy_before = digest_file($legacy_pool);
|
|
make_legacy_deb(
|
|
"$tmp/rebuilt-legacy-deb",
|
|
"$staged/xcat-genesis-base-amd64_1_all.deb",
|
|
'rebuilt repository test package',
|
|
);
|
|
my $replace_log = "$tmp/deb-replace.log";
|
|
my $replace_status = run_apt_consumer(
|
|
log => $replace_log, output => $output, apt_dir => $apt_root,
|
|
dists => ['noble'],
|
|
);
|
|
is($replace_status, 0, 'a rebuilt DEB may keep its published filename');
|
|
isnt(digest_file($legacy_pool), $legacy_before,
|
|
'the rebuilt DEB replaces the earlier package');
|
|
|
|
# A staged package under a release package's own name is dropped, not published.
|
|
my $collision = "$tmp/apt-collision";
|
|
my $collision_output = "$tmp/deb-collision-output";
|
|
my $collision_staged = stage_apt_suites($collision_output, "$tmp/collision-deb");
|
|
write_binary("$collision_staged/$package", 'different');
|
|
my $collision_log = "$tmp/deb-collision.log";
|
|
my $collision_status = run_apt_consumer(
|
|
log => $collision_log, output => $collision_output, apt_dir => $collision,
|
|
extra => [ '--genesis-release', $release_root ],
|
|
);
|
|
is($collision_status, 0, 'verified release replaces a stale staged package');
|
|
is(digest_file("$collision/pool/main/xcat-genesis-openembedded/$package"),
|
|
digest_file("$release_root/deb/$package"),
|
|
'pooled package still matches the verified release');
|
|
}
|
|
|
|
sub test_version_1_deb_consumer {
|
|
my @release_architectures = grep { $_ ne 's390x' } architectures();
|
|
my $release_root = make_package_release(
|
|
"$tmp/deb-version-1", 'deb', @release_architectures,
|
|
);
|
|
write_release_manifest(
|
|
$release_root, $version, $release, $revision, $epoch,
|
|
join(',', @release_architectures), 'deb', 1,
|
|
);
|
|
write_checksums($release_root);
|
|
|
|
my $apt_root = "$tmp/apt-version-1";
|
|
my $output = "$tmp/deb-version-1-output";
|
|
stage_apt_suites($output, "$tmp/deb-version-1-legacy");
|
|
my $log = "$tmp/deb-version-1.log";
|
|
my $status = run_apt_consumer(
|
|
log => $log, output => $output, apt_dir => $apt_root,
|
|
extra => [ '--genesis-release', $release_root ],
|
|
);
|
|
my $pool = "$apt_root/pool/main/xcat-genesis-openembedded";
|
|
|
|
isnt($status, 0, 'APT refuses a version 1 release for the current repository');
|
|
like(read_binary($log),
|
|
qr/Genesis release version 1 omits currently supported architectures: s390x/,
|
|
'the version 1 refusal identifies the missing architecture');
|
|
ok(!-d $pool, 'a version 1 release publishes no shared pool');
|
|
|
|
my $current_release = make_package_release(
|
|
"$tmp/deb-current-manifest", 'deb', architectures(),
|
|
);
|
|
|
|
my $missing_manifest = "$tmp/deb-version-1-missing.conf";
|
|
write_apt_manifest(
|
|
$missing_manifest,
|
|
sub { delete $_[0]->{ deb_package_name('s390x') } },
|
|
);
|
|
my $missing_apt = "$tmp/apt-version-1-missing";
|
|
my $missing_output = "$tmp/deb-version-1-missing-output";
|
|
stage_apt_suites($missing_output, "$tmp/deb-version-1-missing-legacy");
|
|
my $missing_log = "$tmp/deb-version-1-missing.log";
|
|
my $missing_status = run_apt_consumer(
|
|
log => $missing_log, output => $missing_output, apt_dir => $missing_apt,
|
|
manifest => $missing_manifest,
|
|
build => 1,
|
|
extra => [ '--genesis-release', $current_release ],
|
|
);
|
|
isnt($missing_status, 0,
|
|
'a current release does not hide an incomplete shared manifest');
|
|
like(read_binary($missing_log), qr/\[shared\] is missing supported packages: .*s390x/,
|
|
'the shared manifest failure identifies the missing current package');
|
|
unlike(read_binary($missing_log), qr/Ensure sbuild chroots/,
|
|
'an incomplete shared manifest is rejected before building');
|
|
|
|
my $unknown_manifest = "$tmp/deb-version-1-unknown.conf";
|
|
write_apt_manifest(
|
|
$unknown_manifest,
|
|
sub { $_[0]->{'xcat-genesis-openembedded-unknown'} = '2.*' },
|
|
);
|
|
my $unknown_apt = "$tmp/apt-version-1-unknown";
|
|
my $unknown_output = "$tmp/deb-version-1-unknown-output";
|
|
stage_apt_suites($unknown_output, "$tmp/deb-version-1-unknown-legacy");
|
|
my $unknown_log = "$tmp/deb-version-1-unknown.log";
|
|
my $unknown_status = run_apt_consumer(
|
|
log => $unknown_log, output => $unknown_output, apt_dir => $unknown_apt,
|
|
manifest => $unknown_manifest,
|
|
extra => [ '--genesis-release', $current_release ],
|
|
);
|
|
isnt($unknown_status, 0, 'an unknown shared manifest package is refused');
|
|
like(read_binary($unknown_log),
|
|
qr/\[shared\] has unsupported packages: xcat-genesis-openembedded-unknown/,
|
|
'the shared manifest failure identifies the unknown package');
|
|
ok(!-d "$unknown_apt/pool/main/xcat-genesis-openembedded",
|
|
'an unknown shared manifest package publishes nothing');
|
|
|
|
my $non_genesis_manifest = "$tmp/deb-non-genesis-missing.conf";
|
|
write_apt_manifest(
|
|
$non_genesis_manifest,
|
|
sub { $_[0]->{'xcat-release'} = '2.*' },
|
|
);
|
|
my $non_genesis_apt = "$tmp/apt-non-genesis-missing";
|
|
my $non_genesis_output = "$tmp/deb-non-genesis-missing-output";
|
|
stage_apt_suites($non_genesis_output, "$tmp/deb-non-genesis-missing-legacy");
|
|
my $non_genesis_log = "$tmp/deb-non-genesis-missing.log";
|
|
my $non_genesis_status = run_apt_consumer(
|
|
log => $non_genesis_log,
|
|
output => $non_genesis_output,
|
|
apt_dir => $non_genesis_apt,
|
|
manifest => $non_genesis_manifest,
|
|
extra => [ '--genesis-release', $current_release ],
|
|
);
|
|
isnt($non_genesis_status, 0, 'every shared manifest package is verified');
|
|
like(read_binary($non_genesis_log), qr/MISSING xcat-release/,
|
|
'the shared gate identifies a missing non-Genesis package');
|
|
ok(!-d "$non_genesis_apt/pool/main/xcat-genesis-openembedded",
|
|
'a missing non-Genesis package prevents APT publication');
|
|
}
|
|
|
|
sub test_signed_common_rpm_repository {
|
|
my $release_root = make_package_release("$tmp/rpm-signed", 'rpm');
|
|
my $package = "xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
|
my $output = "$tmp/rpm-signed-output";
|
|
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
|
my $dependencies = "$tmp/rpm-signed-dependencies";
|
|
my $scratch_repo_root = "$tmp/rpm-signed-repo-root";
|
|
my $common = "$output/xcat-dep/common";
|
|
my $gpg_home = "$tmp/rpm-signing-key";
|
|
my $identity = 'xCAT repository test <xcat-repository-test@example.invalid>';
|
|
make_rpm_dependencies($dependencies, "$release_root/rpm/$package");
|
|
make_path($gpg_home);
|
|
write_target_manifest($scratch_repo_root, $target);
|
|
chmod(0700, $gpg_home) or die $!;
|
|
system(
|
|
'gpg', '--batch', '--homedir', $gpg_home, '--passphrase', '',
|
|
'--quick-generate-key', $identity, 'rsa2048', 'sign', '0',
|
|
) == 0 or die "could not create the repository test key";
|
|
|
|
my @perl_lib;
|
|
push(@perl_lib, write_forkmanager_stub("$tmp/perl-signed-stub"))
|
|
unless eval { require Parallel::ForkManager; 1 };
|
|
push(@perl_lib, $ENV{PERL5LIB})
|
|
if defined($ENV{PERL5LIB}) && $ENV{PERL5LIB} ne '';
|
|
local $ENV{PERL5LIB} = join(':', @perl_lib);
|
|
|
|
my $log = "$tmp/rpm-signed-consumer.log";
|
|
my $status = run_capture(
|
|
$log,
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $scratch_repo_root,
|
|
'--xcat-source', $repo_root,
|
|
'--output', $output,
|
|
'--target', $target,
|
|
'--run-id', 'signed-consumer',
|
|
'--build-timestamp', $epoch,
|
|
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
|
'--no-verify-repo',
|
|
'--skip-createrepo', '--skip-tarball',
|
|
'--collect-dir', $dependencies,
|
|
'--genesis-release', $release_root,
|
|
'--gpg-sign', '--gpg-key-name', $identity, '--gpg-home', $gpg_home,
|
|
);
|
|
|
|
diag(read_binary($log)) if $status;
|
|
is($status, 0, 'the common RPM repository can be signed');
|
|
ok(-f "$common/repodata/repomd.xml.key",
|
|
'the common repository exports its own public key');
|
|
is(
|
|
system(
|
|
'gpg', '--batch', '--homedir', $gpg_home, '--verify',
|
|
"$common/repodata/repomd.xml.asc",
|
|
"$common/repodata/repomd.xml",
|
|
) >> 8,
|
|
0,
|
|
'the common repository metadata signature verifies',
|
|
);
|
|
like(
|
|
read_binary("$common/xcat-dep-common.repo"),
|
|
qr{^gpgkey=https://xcat\.org/files/xcat/repos/yum/devel/xcat-dep/common/repodata/repomd\.xml\.key$}m,
|
|
'the common repository file points to its own exported key',
|
|
);
|
|
like(read_binary("$common/xcat-dep-common.repo"), qr/^repo_gpgcheck=1$/m,
|
|
'the signed common repository requires metadata verification');
|
|
like(read_binary("$common/xcat-dep-common.repo"), qr/^skip_if_unavailable=1$/m,
|
|
'the signed common repository remains optional during outages');
|
|
}
|
|
|
|
sub test_legacy_rpm_consumer {
|
|
my $release_root = make_package_release("$tmp/rpm-legacy", 'rpm', 'x86_64');
|
|
my $package = "xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
|
my $dependencies = "$tmp/rpm-legacy-dependencies";
|
|
my $output = "$tmp/rpm-legacy-output";
|
|
my $scratch_repo_root = "$tmp/rpm-legacy-repo-root";
|
|
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
|
my $deployed = "$output/xcat-dep/rh10/" . capture_command('uname', '-m');
|
|
make_rpm_dependencies($dependencies, "$release_root/rpm/$package");
|
|
write_target_manifest($scratch_repo_root, $target);
|
|
write_binary("$scratch_repo_root/Gitepoch", '');
|
|
|
|
my @perl_lib;
|
|
push(@perl_lib, write_forkmanager_stub("$tmp/perl-legacy-stub"))
|
|
unless eval { require Parallel::ForkManager; 1 };
|
|
push(@perl_lib, $ENV{PERL5LIB})
|
|
if defined($ENV{PERL5LIB}) && $ENV{PERL5LIB} ne '';
|
|
local $ENV{PERL5LIB} = join(':', @perl_lib);
|
|
|
|
my $log = "$tmp/rpm-legacy-consumer.log";
|
|
my $status = run_capture(
|
|
$log,
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $scratch_repo_root,
|
|
'--output', $output,
|
|
'--target', $target,
|
|
'--run-id', 'legacy',
|
|
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
|
'--no-verify-repo',
|
|
'--skip-createrepo', '--skip-tarball',
|
|
'--collect-dir', $dependencies,
|
|
);
|
|
|
|
is($status, 0, 'RPM legacy repository accepts an empty Gitepoch');
|
|
ok(-f "$deployed/xCAT-genesis-base-x86_64-1.noarch.rpm",
|
|
'RPM legacy path still deploys the existing Genesis package');
|
|
is_deeply(
|
|
[ glob("$deployed/xCAT-genesis-openembedded-*.rpm") ],
|
|
[],
|
|
'RPM legacy path does not add OpenEmbedded packages',
|
|
);
|
|
ok(!-d "$output/xcat-dep/common",
|
|
'RPM legacy path does not create the common repository');
|
|
}
|
|
|
|
sub test_legacy_deb_consumer {
|
|
my $apt_root = "$tmp/apt-legacy";
|
|
my $output = "$tmp/deb-legacy-output";
|
|
stage_legacy_deb("$tmp/legacy-dummy-deb", $output);
|
|
|
|
local $ENV{SOURCE_DATE_EPOCH} = $epoch;
|
|
my $log = "$tmp/deb-legacy-consumer.log";
|
|
my $status = run_apt_consumer(log => $log, output => $output, apt_dir => $apt_root);
|
|
my $packages = "$apt_root/dists/noble/main/binary-amd64/Packages";
|
|
|
|
is($status, 0, 'APT repository keeps working without a Genesis release');
|
|
like(read_binary($packages), qr/^Package: xcat-genesis-base-amd64$/m,
|
|
'APT legacy path still indexes the existing Genesis package');
|
|
unlike(read_binary($packages), qr/^Package: xcat-genesis-openembedded-/m,
|
|
'APT legacy path does not add OpenEmbedded packages');
|
|
}
|
|
|
|
sub test_partial_rpm_release {
|
|
my $release_root = make_package_release("$tmp/rpm-partial", 'rpm', 'x86_64');
|
|
my $output = "$tmp/partial-output";
|
|
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
|
my $common = "$output/xcat-dep/common";
|
|
my $existing = "$common/xCAT-genesis-openembedded-existing.noarch.rpm";
|
|
make_path($common);
|
|
write_binary($existing, 'existing release');
|
|
|
|
my $log = "$tmp/rpm-partial.log";
|
|
my $status = run_capture(
|
|
$log,
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $repo_root,
|
|
'--output', $output,
|
|
'--target', $target,
|
|
'--run-id', 'partial',
|
|
'--build-timestamp', $epoch,
|
|
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
|
'--no-verify-repo',
|
|
'--skip-createrepo', '--skip-tarball',
|
|
'--genesis-release', $release_root,
|
|
);
|
|
|
|
isnt($status, 0, 'RPM repository rejects a partial Genesis release');
|
|
like(read_binary($log), qr/Genesis release version 2 omits currently supported architectures/,
|
|
'RPM partial-release failure names the missing architectures');
|
|
ok(-f $existing, 'partial release does not remove the deployed package');
|
|
}
|
|
|
|
sub test_partial_deb_release {
|
|
my $release_root = make_package_release("$tmp/deb-partial", 'deb', 'x86_64');
|
|
my $apt_root = "$tmp/partial-apt";
|
|
my $output = "$tmp/deb-partial-output";
|
|
my $pool = "$apt_root/pool/main/noble";
|
|
my $existing = "$pool/xcat-genesis-base-existing.deb";
|
|
stage_legacy_deb("$tmp/partial-deb", $output);
|
|
make_path($pool);
|
|
write_binary($existing, 'existing release');
|
|
|
|
my $log = "$tmp/deb-partial.log";
|
|
my $status = run_apt_consumer(
|
|
log => $log, output => $output, apt_dir => $apt_root,
|
|
extra => [ '--genesis-release', $release_root ],
|
|
);
|
|
|
|
isnt($status, 0, 'APT repository rejects a partial Genesis release');
|
|
like(read_binary($log), qr/Genesis release version 2 omits currently supported architectures/,
|
|
'APT partial-release failure names the missing architectures');
|
|
ok(-f $existing, 'partial DEB release does not remove the published package');
|
|
}
|
|
|
|
sub test_failed_build_release {
|
|
my $release_root = make_package_release("$tmp/rpm-empty", 'rpm');
|
|
my $package = "xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
|
my $output = "$tmp/empty-output";
|
|
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
|
my $common = "$output/xcat-dep/common";
|
|
my $scratch_repo_root = "$tmp/rpm-empty-root";
|
|
my $collected = "$tmp/rpm-empty-collect";
|
|
my $run_repo = "$output/mockbuild-all/$target-empty/repo/" . capture_command('uname', '-m');
|
|
my $stale = "$run_repo/ipmitool-xcat-0-stale.noarch.rpm";
|
|
make_path($common, $scratch_repo_root, $collected, $run_repo);
|
|
write_target_manifest($scratch_repo_root, $target);
|
|
write_binary($stale, 'package left by an earlier run');
|
|
|
|
my $log = "$tmp/rpm-empty.log";
|
|
my $status = run_capture(
|
|
$log,
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $scratch_repo_root,
|
|
'--output', $output,
|
|
'--target', $target,
|
|
'--run-id', 'empty',
|
|
'--build-timestamp', $epoch,
|
|
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
|
'--no-verify-repo',
|
|
'--skip-createrepo', '--skip-tarball',
|
|
'--genesis-release', $release_root,
|
|
'--collect-dir', $collected,
|
|
);
|
|
|
|
isnt($status, 0, 'a run that built nothing fails even with a Genesis release');
|
|
like(read_binary($log), qr/No binary RPMs were collected/,
|
|
'the failure names the empty collection, not the missing dependencies');
|
|
ok(!-e "$common/$package",
|
|
'a run that built nothing publishes no release package');
|
|
ok(!-e $stale,
|
|
'a package left by an earlier run is cleared from the staging repository');
|
|
}
|
|
|
|
# --skip-build collects THIS target's previously built artifacts out of its own build-results tree,
|
|
# so a run that skips building must not destroy the tree it collects from. (Where this test came
|
|
# from the --skip-build roots were the legacy build-output/list* directories and build-results was
|
|
# only ever kept, never collected -- which is why the empty-collection case above can no longer
|
|
# leave an rpm there.)
|
|
sub test_skip_build_collects_results {
|
|
my $release_root = make_package_release("$tmp/rpm-kept", 'rpm');
|
|
my $package = "xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
|
my $output = "$tmp/kept-output";
|
|
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
|
my $scratch_repo_root = "$tmp/rpm-kept-root";
|
|
my $results = "$output/mockbuild-all/$target-kept/build-results/ipmitool-xcat";
|
|
my $kept = "$results/ipmitool-xcat-1.noarch.rpm";
|
|
my $deployed = "$output/xcat-dep/rh10/" . capture_command('uname', '-m');
|
|
make_path($results);
|
|
copy("$release_root/rpm/$package", $kept) or die $!;
|
|
write_target_manifest($scratch_repo_root, $target);
|
|
|
|
my $log = "$tmp/rpm-kept.log";
|
|
my $status = run_capture(
|
|
$log,
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $scratch_repo_root,
|
|
'--output', $output,
|
|
'--target', $target,
|
|
'--run-id', 'kept',
|
|
'--build-timestamp', $epoch,
|
|
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
|
'--no-verify-repo',
|
|
'--skip-createrepo', '--skip-tarball',
|
|
);
|
|
|
|
is($status, 0, 'a run that skips building collects its own build results');
|
|
ok(-e $kept, 'the build results it collects from are kept');
|
|
ok(-e "$deployed/" . basename($kept), 'the collected package reaches the deployed repo');
|
|
}
|
|
|
|
sub test_dry_run_release {
|
|
my $release_root = make_package_release("$tmp/rpm-dry", 'rpm');
|
|
my $package = "xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
|
my $output = "$tmp/dry-output";
|
|
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
|
my $run_repo = "$output/mockbuild-all/$target-dry/repo/" . capture_command('uname', '-m');
|
|
my $dependencies = "$tmp/rpm-dry-dependencies";
|
|
my $scratch_repo_root = "$tmp/rpm-dry-root";
|
|
make_rpm_dependencies($dependencies, "$release_root/rpm/$package");
|
|
write_target_manifest($scratch_repo_root, $target);
|
|
|
|
my $log = "$tmp/rpm-dry.log";
|
|
my $status = run_capture(
|
|
$log,
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $scratch_repo_root,
|
|
'--output', $output,
|
|
'--target', $target,
|
|
'--run-id', 'dry',
|
|
'--build-timestamp', $epoch,
|
|
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
|
'--no-verify-repo',
|
|
'--skip-createrepo', '--skip-tarball',
|
|
'--genesis-release', $release_root,
|
|
'--collect-dir', $dependencies,
|
|
'--dry-run',
|
|
);
|
|
my $printed = read_binary($log);
|
|
|
|
is($status, 0, 'a dry run accepts a verified Genesis release');
|
|
like($printed,
|
|
qr{^DRY-RUN publish Genesis release package: .*\Q$package\E -> .*/xcat-dep/common/\Q$package\E$}m,
|
|
'the dry run reports the shared package destination');
|
|
like($printed, qr/^Collected binary RPMs: 8$/m,
|
|
'the dry run counts the packages collected for the target repository');
|
|
ok(!-e "$run_repo/$package", 'the dry run installs nothing');
|
|
}
|
|
|
|
sub test_rpm_repository_lock {
|
|
my $output = "$tmp/rpm-lock-output";
|
|
my $repository = "$tmp/rpm-shared-repository";
|
|
my $arch = capture_command('uname', '-m');
|
|
my $target = "test+epel-10-$arch";
|
|
my $scratch_repo_root = "$tmp/rpm-lock-repo-root";
|
|
write_target_manifest($scratch_repo_root, $target);
|
|
# The cell this target deploys is locked by a run on another machine.
|
|
my $cell_lock = "$repository/rh10/.$arch.lock";
|
|
make_path($cell_lock);
|
|
write_binary("$cell_lock/metadata",
|
|
XCAT::NFSLock::format_metadata({ 'machine-id' => 'another-machine', 'boot-id' => 'b',
|
|
pid => 1, pstart => 1, token => 't' }));
|
|
|
|
my @perl_lib;
|
|
push(@perl_lib, write_forkmanager_stub("$tmp/perl-lock-stub"))
|
|
unless eval { require Parallel::ForkManager; 1 };
|
|
push(@perl_lib, $ENV{PERL5LIB})
|
|
if defined($ENV{PERL5LIB}) && $ENV{PERL5LIB} ne '';
|
|
local $ENV{PERL5LIB} = join(':', @perl_lib);
|
|
|
|
my $log = "$tmp/rpm-repository-lock.log";
|
|
my $status = run_capture(
|
|
$log,
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $scratch_repo_root,
|
|
'--output', $output,
|
|
'--repo-dep', $repository,
|
|
'--target', $target,
|
|
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
|
'--no-verify-repo',
|
|
'--skip-createrepo', '--skip-tarball', '--dry-run',
|
|
);
|
|
isnt($status, 0, 'a repository cell cannot have two publishers');
|
|
like(read_binary($log), qr/^Trying to unlock \Q$cell_lock\E failed after 1 retry;/m,
|
|
'the lock failure names the locked cell');
|
|
ok(-d $cell_lock, 'a lock held on another machine is left in place');
|
|
remove_tree($cell_lock);
|
|
|
|
my $backup = "$repository/.common.previous.999";
|
|
my $staging = "$repository/.common.abandoned";
|
|
make_path($backup, $staging);
|
|
write_binary("$backup/marker", "previous repository\n");
|
|
my $forced_log = "$tmp/rpm-repository-force.log";
|
|
my $forced_status = run_capture(
|
|
$forced_log,
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $scratch_repo_root,
|
|
'--output', "$tmp/rpm-force-output",
|
|
'--repo-dep', $repository,
|
|
'--target', $target,
|
|
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
|
'--no-verify-repo',
|
|
'--skip-createrepo', '--skip-tarball', '--dry-run',
|
|
);
|
|
is($forced_status, 0, 'a dry run starts beside an interrupted RPM publication')
|
|
or diag(read_binary($forced_log));
|
|
ok(-f "$backup/marker", 'a dry run leaves the moved-aside common repository in place');
|
|
ok(-d $staging, 'a dry run leaves the abandoned staging tree in place');
|
|
ok(!-e "$repository/common", 'a dry run does not restore the common repository');
|
|
}
|
|
|
|
sub test_finalize_cell_lock {
|
|
my $x86 = "$tmp/finalize-x86";
|
|
my $ppc = "$tmp/finalize-ppc";
|
|
make_path("$x86/rh10/x86_64", "$ppc/rh10/ppc64le");
|
|
# A build on another machine is still deploying the x86_64 cell.
|
|
my $cell_lock = "$x86/rh10/.x86_64.lock";
|
|
make_path($cell_lock);
|
|
write_binary("$cell_lock/metadata",
|
|
XCAT::NFSLock::format_metadata({ 'machine-id' => 'another-machine', 'boot-id' => 'b',
|
|
pid => 1, pstart => 1, token => 't' }));
|
|
|
|
my $log = "$tmp/finalize-lock.log";
|
|
my $status = run_capture(
|
|
$log,
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $repo_root,
|
|
'--finalize-xcat-dep', '--x86_64-repo', $x86, '--ppc64le-repo', $ppc,
|
|
);
|
|
isnt($status, 0, 'finalize does not rewrite a cell that a build holds');
|
|
like(read_binary($log), qr/^Trying to unlock \Q$cell_lock\E failed after 1 retry;/m,
|
|
'finalize names the cell lock it waited for');
|
|
ok(-d $cell_lock, 'the build keeps its cell lock');
|
|
ok(!-e "$ppc/rh10/.ppc64le.lock", 'finalize releases the cell locks it took');
|
|
|
|
# Finalize of the ppc64le cells takes only their locks: the x86_64 cell lock of another machine
|
|
# does not stop it. It stops at the next check, the missing genesis rpm.
|
|
my $arch_log = "$tmp/finalize-arch.log";
|
|
my $arch_status = run_capture(
|
|
$arch_log,
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $repo_root,
|
|
'--finalize-xcat-dep', '--finalize-arch', 'ppc64le',
|
|
'--x86_64-repo', $x86, '--ppc64le-repo', $ppc,
|
|
);
|
|
isnt($arch_status, 0, 'finalize of an empty ppc64le cell fails');
|
|
unlike(read_binary($arch_log), qr/^Trying to unlock/m, 'finalize of ppc64le cells takes no x86_64 cell lock');
|
|
like(read_binary($arch_log), qr/no x86_64 xCAT-genesis-base rpm/, 'finalize of ppc64le cells reaches the genesis check');
|
|
ok(-d $cell_lock, 'the x86_64 cell lock of the other machine is left in place');
|
|
ok(!-e "$ppc/rh10/.ppc64le.lock", 'finalize releases the ppc64le cell lock');
|
|
}
|
|
|
|
sub test_rpm_signal_cleanup {
|
|
my $output = "$tmp/rpm-signal-output";
|
|
my $repository = "$tmp/rpm-signal-repository";
|
|
my $signal_bin = "$tmp/rpm-signal-bin";
|
|
my $log = "$tmp/rpm-signal.log";
|
|
make_path($repository, $signal_bin);
|
|
# nproc runs after every lock is taken and before any build starts.
|
|
write_binary("$signal_bin/nproc", "#!/bin/sh\nsleep 60\nexit 1\n");
|
|
chmod(0755, "$signal_bin/nproc") or die $!;
|
|
my $cell_lock = "$repository/rh10/." . capture_command('uname', '-m') . '.lock';
|
|
|
|
my $pid = fork();
|
|
die "Cannot fork signal test: $!" unless defined($pid);
|
|
if ($pid == 0) {
|
|
POSIX::setpgid(0, 0);
|
|
local $ENV{PATH} = "$signal_bin:$ENV{PATH}";
|
|
open(STDOUT, '>', $log) or die "open $log: $!";
|
|
open(STDERR, '>&', fileno(STDOUT)) or die "redirect stderr: $!";
|
|
exec(
|
|
$^X, $rpm_consumer,
|
|
'--repo-root', $repo_root,
|
|
'--xcat-source', $repo_root,
|
|
'--output', $output,
|
|
'--repo-dep', $repository,
|
|
'--target', 'test+epel-10-x86_64',
|
|
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
|
'--no-verify-repo',
|
|
'--skip-createrepo', '--skip-tarball', '--dry-run',
|
|
);
|
|
exit 127;
|
|
}
|
|
|
|
my $locked = 0;
|
|
for (1 .. 200) {
|
|
if (-d "$output/.lock" && -d $cell_lock) {
|
|
$locked = 1;
|
|
last;
|
|
}
|
|
sleep(0.05);
|
|
}
|
|
ok($locked, 'the signal test reaches the locked publication phase');
|
|
kill('TERM', -$pid);
|
|
waitpid($pid, 0);
|
|
is($? >> 8, 1, 'SIGTERM follows the publisher cleanup exit path');
|
|
ok(!-d "$output/.lock", 'SIGTERM releases the output lock');
|
|
ok(!-d $cell_lock, 'SIGTERM releases the repository cell lock');
|
|
}
|
|
|
|
sub test_publish_lock {
|
|
my $apt_root = "$tmp/apt-lock";
|
|
my $output = "$tmp/deb-lock-output";
|
|
stage_legacy_deb("$tmp/lock-deb", $output);
|
|
make_path($output);
|
|
|
|
# This process is a live build run on the same host. The publish reads the staging of every
|
|
# expected arch, so it waits for the run lock of each one. The run lock is an XCAT::NFSLock,
|
|
# because flock on the shared tree fails with ENOTSUPP through the NFS re-export.
|
|
for my $build_arch (qw(amd64 ppc64el)) {
|
|
my $run_lock = "$output/.sbuild-all.$build_arch.nfslock";
|
|
my $running = XCAT::NFSLock->acquire($run_lock);
|
|
my $run_log = "$tmp/deb-run-locked-$build_arch.log";
|
|
my $run_status = run_apt_consumer(log => $run_log, output => $output, apt_dir => $apt_root,
|
|
extra => [ '--publish-lock-wait', '2' ]);
|
|
isnt($run_status, 0, "a publish does not start while a $build_arch run holds its lock");
|
|
like(read_binary($run_log), qr/^Trying to unlock \Q$run_lock\E failed after 1 retry;/m,
|
|
"the refusal names the $build_arch run lock");
|
|
ok(!-d "$apt_root/dists", "nothing is published while a $build_arch run holds its lock");
|
|
$running->release;
|
|
}
|
|
|
|
# This process is a live publisher on the same host.
|
|
my $lockfile = "$output/.sbuild-all.publish.nfslock";
|
|
my $held = XCAT::NFSLock->acquire($lockfile);
|
|
|
|
my $locked_log = "$tmp/deb-locked.log";
|
|
my $locked_status = run_apt_consumer(
|
|
log => $locked_log, output => $output, apt_dir => $apt_root,
|
|
extra => [ '--publish-lock-wait', '2' ],
|
|
);
|
|
isnt($locked_status, 0, 'a locked apt tree is not published into');
|
|
like(read_binary($locked_log), qr/^Trying to unlock \Q$lockfile\E failed after 1 retry;/m,
|
|
'the refusal names the lock another run owns');
|
|
ok(!-d "$apt_root/dists", 'nothing is published while another run holds the lock');
|
|
|
|
$held->release;
|
|
|
|
# sbuild-all.pl never publishes in place: it assembles a COMPLETE side tree and renames it onto
|
|
# the repository, so there is no half-written state to recover and no per-file backup to restore.
|
|
# A side tree left by a run that died is simply inert -- the next run builds and publishes its own.
|
|
my $abandoned = "$apt_root.publish-abandoned.999";
|
|
make_path("$abandoned/dists/noble");
|
|
write_binary("$abandoned/dists/noble/Release", "abandoned\n");
|
|
|
|
my $freed_log = "$tmp/deb-freed.log";
|
|
my $freed_status = run_apt_consumer(
|
|
log => $freed_log, output => $output, apt_dir => $apt_root, dists => ['noble']);
|
|
is($freed_status, 0, 'the publish runs once the lock is released');
|
|
ok(-f "$apt_root/dists/noble/Release", 'the released lock lets the tree be published');
|
|
ok(!-e $lockfile, 'the publisher releases the publish lock when it exits');
|
|
isnt(read_binary("$apt_root/dists/noble/Release"), "abandoned\n",
|
|
'a side tree abandoned by a dead run is never published');
|
|
}
|
|
|
|
sub test_activation_helper {
|
|
my $activation = read_binary("$repo_root/genesis-openembedded/activate");
|
|
unlike($activation, qr/XCAT_GENESIS_ROOT/,
|
|
'the root package helper has no environment-controlled execution root');
|
|
$activation =~ s/\ngenesis_activation_main "\$\@"\s*\z/\n/
|
|
or die('the activation helper has no reusable main boundary');
|
|
|
|
my $driver = "$tmp/activation-driver";
|
|
my $log = "$tmp/activation.log";
|
|
my $output = "$tmp/activation-output.log";
|
|
write_binary(
|
|
$driver,
|
|
$activation
|
|
. <<'SH',
|
|
genesis_is_host_root() { return 0; }
|
|
genesis_mknb_exists() { return 0; }
|
|
genesis_is_service_node() { [ "${XCAT_TEST_SERVICE_NODE:-0}" = 1 ]; }
|
|
genesis_uses_shared_tftp() { [ "${XCAT_TEST_SHAREDTFTP:-0}" = 1 ]; }
|
|
genesis_run_mknb() {
|
|
printf '%s\n' "$1" >>"$XCAT_TEST_LOG"
|
|
return "${XCAT_TEST_MKNB_STATUS:-0}"
|
|
}
|
|
genesis_activation_main "$@"
|
|
SH
|
|
);
|
|
chmod(0755, $driver) or die "Cannot make activation driver executable: $!";
|
|
|
|
local $ENV{XCAT_TEST_LOG} = $log;
|
|
my $status = run_capture($output, $driver, 'x86_64');
|
|
is($status, 0, 'the activation helper accepts a local-TFTP node');
|
|
is(read_binary($log), "x86_64\n", 'the activation helper runs mknb for one architecture');
|
|
|
|
write_binary($log, '');
|
|
$status = run_capture($output, $driver, 's390x');
|
|
is($status, 0, 'the activation helper accepts s390x');
|
|
is(read_binary($log), "s390x\n", 'the activation helper runs mknb for s390x');
|
|
|
|
write_binary($log, '');
|
|
$status = run_capture($output, $driver, 'unsupported');
|
|
is($status, 0, 'an unsupported architecture does not fail the package transaction');
|
|
is(read_binary($log), '', 'an unsupported architecture does not run mknb');
|
|
like(read_binary($output), qr/Invalid Genesis architecture: unsupported/,
|
|
'the activation helper reports an unsupported architecture');
|
|
|
|
write_binary($log, '');
|
|
local $ENV{XCAT_TEST_SERVICE_NODE} = 1;
|
|
local $ENV{XCAT_TEST_SHAREDTFTP} = 1;
|
|
$status = run_capture($output, $driver, 'ppc64le');
|
|
is($status, 0, 'a shared-TFTP service node is accepted');
|
|
is(read_binary($log), '', 'a shared-TFTP service node does not rebuild Genesis');
|
|
|
|
local $ENV{XCAT_TEST_SERVICE_NODE} = 0;
|
|
local $ENV{XCAT_TEST_MKNB_STATUS} = 1;
|
|
$status = run_capture($output, $driver, 'ppc64le');
|
|
is($status, 0, 'an mknb failure does not fail the package transaction');
|
|
}
|
|
|
|
sub make_package_release {
|
|
my ($root, $format, @requested_architectures) = @_;
|
|
@requested_architectures = architectures() unless @requested_architectures;
|
|
return build_package_release(
|
|
root => $root,
|
|
format => $format,
|
|
architectures => \@requested_architectures,
|
|
packager => $packager,
|
|
version => $version,
|
|
release => $release,
|
|
revision => $revision,
|
|
epoch => $epoch,
|
|
);
|
|
}
|
|
|
|
sub genesis_rpm_names {
|
|
my ($directory) = @_;
|
|
return () unless -d $directory;
|
|
opendir(my $dh, $directory) or die $!;
|
|
my @names = sort grep { /^xCAT-genesis-openembedded-.*\.rpm$/ } readdir($dh);
|
|
closedir($dh) or die $!;
|
|
return @names;
|
|
}
|
|
|
|
sub genesis_deb_names {
|
|
my ($directory) = @_;
|
|
return () unless -d $directory;
|
|
opendir(my $dh, $directory) or die $!;
|
|
my @names = sort grep { /^xcat-genesis-openembedded-.*\.deb$/ } readdir($dh);
|
|
closedir($dh) or die $!;
|
|
return @names;
|
|
}
|
|
|
|
sub make_rpm_dependencies {
|
|
my ($directory, $package) = @_;
|
|
make_path($directory);
|
|
for my $name (qw(
|
|
ipmitool-xcat syslinux-xcat grub2-xcat xnba-undi
|
|
perl-IO-Stty perl-HTTP-Async perl-Net-HTTPS-NB
|
|
xCAT-genesis-base-x86_64
|
|
)) {
|
|
copy($package, "$directory/$name-1.noarch.rpm") or die $!;
|
|
}
|
|
}
|
|
|
|
sub make_legacy_deb {
|
|
my ($root, $output, $description) = @_;
|
|
$description //= 'repository test package';
|
|
make_path("$root/DEBIAN");
|
|
write_binary(
|
|
"$root/DEBIAN/control",
|
|
"Package: xcat-genesis-base-amd64\nVersion: 1\nArchitecture: all\n"
|
|
. "Maintainer: xCAT <xcat-user\@lists.sourceforge.net>\n"
|
|
. "Description: $description\n",
|
|
);
|
|
die "Cannot build legacy test DEB\n"
|
|
if run_capture(
|
|
"$root.log", 'dpkg-deb', '--root-owner-group', '--build', $root, $output,
|
|
);
|
|
}
|
|
|
|
sub make_apt_inputs {
|
|
my ($apt_root, $package_root) = @_;
|
|
my $package = "$package_root.deb";
|
|
make_legacy_deb($package_root, $package);
|
|
for my $version (qw(ubuntu22.04 ubuntu24.04 ubuntu26.04)) {
|
|
my $input = "$apt_root/$version";
|
|
make_path($input);
|
|
copy($package, "$input/xcat-genesis-base-amd64_1_all.deb") or die $!;
|
|
}
|
|
return "$apt_root/ubuntu24.04";
|
|
}
|