2
0
mirror of https://github.com/xcat2/xcat-dep.git synced 2026-09-30 23:05:17 +00:00

Merge master into fix/riscv64-net-dns-key-record

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
Daniel Hilst
2026-09-28 13:15:45 -03:00
42 changed files with 3050 additions and 408 deletions
@@ -35,6 +35,10 @@ jobs:
perl -c mockbuild-all.pl
perl -c BuildUtils.pm
perl -c sbuild-all.pl
perl -c ipxe-xcat/mockbuild.pl
perl -c ipxe-xcat/sbuild.pl
perl -c ipxe-xcat/verify-payload.pl
rpmspec -P ipxe-xcat/ipxe-xcat.spec >/dev/null
rpmspec -P \
-D 'genesis_arch x86_64' \
-D 'version 2.19.0' \
@@ -44,6 +48,9 @@ jobs:
genesis-openembedded/build \
genesis-openembedded/package \
genesis-openembedded/verify-release \
ipxe-xcat/mockbuild.pl \
ipxe-xcat/sbuild.pl \
ipxe-xcat/verify-payload.pl \
lib/XCAT/BuildUtils.pm \
lib/XCAT/GenesisRelease.pm \
mockbuild-all.pl \
@@ -51,6 +58,7 @@ jobs:
t/common-repo-gate.t \
t/genesis_openembedded_release.t \
t/genesis_openembedded_consumer.t \
t/ipxe_xcat_payload.t \
t/lib/XCAT/GenesisReleaseTest.pm
- name: Run package tests
@@ -58,8 +66,12 @@ jobs:
prove -v t/build_utils.t
prove -v t/build_timeout.t
prove -v t/sbuild-all.t
prove -v t/goconserver_cross_build.t
prove -v t/ipxe_xcat_payload.t
prove -v t/mockbuild-all.t
prove -v t/net_dns_rr_types.t
prove -v t/repo-lock-race.t
prove -v t/nfslock.t
prove -v -It/lib t/genesis_openembedded_release.t
sudo -E prove -v -It/lib t/genesis_openembedded_consumer.t
prove -v t/riscv64_perl_cell.t
+29 -19
View File
@@ -50,6 +50,7 @@ This guide uses the following placeholders consistently:
- `<REPO_ROOT>/goconserver/mockbuild.pl`
- `<REPO_ROOT>/conserver/mockbuild.pl`
- `<REPO_ROOT>/xnba/mockbuild.pl`
- `<REPO_ROOT>/ipxe-xcat/mockbuild.pl`
- `<REPO_ROOT>/mockbuild-perl-packages.pl`
- `<XCAT_SOURCE>/buildrpms.pl` — only to build the OS-dependent `xCAT-genesis-base` package (unless `--skip-genesis` is set); the full xCAT core is built separately by the xcat-core pipeline, not here.
@@ -103,7 +104,7 @@ Use these flags to skip specific operations:
- `--skip-genesis`
- Skips the `xCAT-genesis-base` build (`<XCAT_SOURCE>/buildrpms.pl --package xCAT-genesis-base`).
- `--skip-xcat-dep`
- Skips non-perl xcat-dep package builders (`elilo`, `grub2-xcat`, `ipmitool-xcat`, `syslinux-xcat`, `goconserver`, `conserver-xcat`, `xnba-undi`).
- Skips non-perl xcat-dep package builders (`elilo`, `grub2-xcat`, `ipmitool-xcat`, `syslinux-xcat`, `goconserver`, `conserver-xcat`, `xnba-undi`, `ipxe-xcat`).
- `--skip-perl`
- Skips `<REPO_ROOT>/mockbuild-perl-packages.pl`.
- With any of the three flags above, the run repository, the tarball and the deployed cell keep
@@ -138,8 +139,10 @@ Use these flags to skip specific operations:
- Adds extra artifact roots to the collection phase (repeatable).
- `--dry-run`
- Prints planned actions without executing them.
- `--force-unlock`
- Removes a stale lock after the previous publisher has been checked.
- `--try-unlock-timeout <N>`
- Waits about N seconds for a lock that a live process holds, in retries of 3 seconds with
at least one retry, then fails and prints the command that removes the lock. A lock whose
owner is proven dead on this host is taken over at once.
# Prerequisites
@@ -245,12 +248,19 @@ published once under `xcat-dep/common`. Source RPMs stay in the verified
release directory. Existing per-EL repositories keep the old Genesis packages
and contain no OpenEmbedded copies.
The build holds separate locks for its work area and the published repository.
It prepares the complete common repository in a temporary directory, then
The build locks its work area (`<output>/.lock`), each repository cell it deploys
(`<repo-dep>/rh<N>/.<arch>.lock`) and, while it publishes, the common repository
(`<repo-dep>/.common-publish.lock`). The per-arch runs of one build lock different
cells, so they run in parallel. A lock whose owner is dead is taken over only on the
owner's host. From any other host the build waits `--try-unlock-timeout` seconds,
then fails with the command that removes the lock. The protocol is documented at the
top of `lib/XCAT/NFSLock.pm`.
The build prepares the complete common repository in a temporary directory, then
replaces the previous repository only after package verification, metadata
generation, and signing have succeeded. If a stopped publisher leaves staging
or backup directories behind, rerun it with ``--force-unlock`` to recover the
previous repository before starting a new publication.
or backup directories behind, the next run recovers the previous repository
when no other run holds the common lock.
Repository publication requires all eight current architectures. Version 1
release manifests remain readable, but they cannot replace the current
@@ -459,7 +469,7 @@ missing, and then builds the `[rocky-10-riscv64-xcat]` section of `packages-mani
| goconserver | cross-compiled on the host (`GOARCH=riscv64`), packaged with `rpmbuild --target riscv64` |
| grub2-xcat (noarch) | built in the native, EPEL-free `rocky-10-x86_64` chroot |
| perl list6 + EPEL gap (`--epel-gap`) | `mockbuild-perl-packages.pl --target-arch riscv64 --noarch-mock-cfg rocky-10-x86_64 --epel-gap`: XS modules in the riscv64 chroot, noarch modules in the native chroot |
| elilo-xcat, syslinux-xcat, xnba-undi (noarch) | built in the native `rocky-10-x86_64` chroot, like grub2-xcat: a riscv64 management node serves the x86 nodes of a mixed cluster. The target is cross-built on x86_64 only, as its mock config states |
| elilo-xcat, ipxe-xcat, syslinux-xcat, xnba-undi (noarch) | built in the native `rocky-10-x86_64` chroot, like grub2-xcat: a riscv64 management node serves the x86 nodes of a mixed cluster. The target is cross-built on x86_64 only, as its mock config states |
There is no EPEL for riscv64, so the perl deps of xCAT that EL10 otherwise takes from EPEL
are built here as well (`--epel-gap` in `mockbuild-perl-packages.pl`: perl-Crypt-Blowfish,
@@ -611,16 +621,16 @@ Codename ↔ version (the single supported set — `BuildUtils` is the source of
`mk-build-deps`, so version constraints, `a | b` alternatives and arch qualifiers are honoured) are
all **fatal** on failure — and since nothing survives the session, a package whose `debian/control`
forgets a `Build-Depends` cannot build green on a sibling package's leftovers.
- **Per-arch package sets (`debs-manifest.conf`).** One `[<codename>-<arch>]` section per target. The
noarch boot components (`syslinux-xcat`/`grub2-xcat`/`elilo-xcat`/`xnba-undi`, `Architecture:all`)
are built ONCE on amd64 — single producer, their source is x86-only — and assembled into every
arch's `Packages` index. They are listed for **ppc64el too, as required-present**, so the gate
verifies the ppc repo actually carries them (a ppc MN needs them for netboot, matching the EL
manifest). `build_one_codename` **skips** an `Architecture:all` package on any non-amd64 arch
(detected via `control_binary_arch`), so ppc64el and riscv64 build only the genuinely
arch-specific compiled deps (`ipmitool-xcat`, `conserver-xcat`, `goconserver`) yet still verify the
boot components they need. The riscv64 sections require the same four boot components as
ppc64el: a riscv64 management node serves the x86 nodes of a mixed cluster.
- **Per-arch package sets (`debs-manifest.conf`).** One `[<codename>-<arch>]` section per target.
The noarch boot components (`syslinux-xcat`/`grub2-xcat`/`elilo-xcat`/`xnba-undi`/`ipxe-xcat`,
`Architecture:all`) are built ONCE on amd64 — single producer, most of them from x86-only source —
and assembled into every arch's `Packages` index. They are listed for **ppc64el too, as
required-present**, so the gate verifies the ppc repo actually carries them (a ppc MN needs them
for netboot, matching the EL manifest). `build_one_codename` **skips** an `Architecture:all`
package on any non-amd64 arch (detected via `control_binary_arch`), so ppc64el and riscv64 build
only the genuinely arch-specific compiled deps (`ipmitool-xcat`, `conserver-xcat`, `goconserver`)
yet still verify the boot components they need. The riscv64 sections require the same five boot
components as ppc64el: a riscv64 management node serves the x86 nodes of a mixed cluster.
- **Fail-hard.** Any required chroot / package / artifact failure, or any version-pin mismatch, fails
the whole run non-zero.
- **Genesis keeps its maintained packaging.** A native `xcat-genesis-base` deb is INGESTED as-is when
@@ -686,7 +696,7 @@ needs no `--mirror`.
| ipmitool-xcat, conserver-xcat | `dpkg-buildpackage` in the emulated riscv64 chroot |
| goconserver | same chroot, compiled by the Go toolchain the chroot installs for riscv64 |
| grub2-xcat (`Architecture:all`) | built once on amd64 and assembled into the riscv64 index; listed in the riscv64 manifest sections as required-present, because a riscv64 management node needs it to netboot |
| syslinux-xcat, elilo-xcat, xnba-undi (`Architecture:all`) | built once on amd64 and assembled into the riscv64 index; required-present like grub2-xcat, because a riscv64 management node serves the x86 nodes of a mixed cluster |
| syslinux-xcat, elilo-xcat, xnba-undi, ipxe-xcat (`Architecture:all`) | built once on amd64 and assembled into the riscv64 index; required-present like grub2-xcat, because a riscv64 management node serves the x86 nodes of a mixed cluster |
| xcat-genesis-base | not built: no riscv64 section names it, and the build skips the step when the manifest does not ask for it, so `--skip-genesis` is unnecessary here |
The riscv64 ipmitool-xcat deb is installed into the chroot that built it and
+24 -50
View File
@@ -37,7 +37,7 @@ our @EXPORT_OK = qw(
supported_arches is_supported_arch
chroot_name chroot_sources_list chroot_is_disposable chroot_build_script
chroot_build_timeout
control_field genesis_deb_control
control_field genesis_debs_for_codename
deb_field deb_version deb_hash cross_copy_genesis_deb
build_deb_in_chroot
);
@@ -476,53 +476,6 @@ sub control_field {
return undef;
}
# genesis_deb_control: build the DEBIAN/control text for the cross-arch-converted xcat-genesis-base
# deb, PRESERVING the maintained packaging's semantics (Depends/Breaks/Replaces/Section/Priority)
# instead of hand-rolling a bare 5-field control (the bug in build-dep-debs.sh flagged by review
# concern #2). $maintained is the text of xCAT-genesis-builder/debian/control (or undef when it
# cannot be located — then a minimal-but-honest control is produced and the caller should warn).
# $pkgname is e.g. xcat-genesis-base-ppc64el, $version the deb version, $arch 'all'. Pure/testable.
sub genesis_deb_control {
my ($maintained, $pkgname, $version, $arch) = @_;
$arch ||= 'all';
my %f = (
Package => $pkgname,
Version => $version,
Architecture => $arch,
Section => 'admin',
Priority => 'optional',
Maintainer => 'xCAT <xcat-user@lists.sourceforge.net>',
);
if (defined $maintained && $maintained ne '') {
for my $k (qw(Section Priority Maintainer Depends Pre-Depends Recommends
Suggests Breaks Replaces Conflicts Provides)) {
my $v = control_field($maintained, $k);
$f{$k} = $v if defined $v && $v ne '';
}
my $desc = control_field($maintained, 'Description');
$f{Description} = $desc if defined $desc && $desc ne '';
}
$f{Description} ||= 'xCAT Genesis netboot image (converted from the rpm for cross-arch netboot)';
# ${misc:Depends} is a debhelper substitution var that only resolves during a real dpkg build;
# in a hand-assembled control it would ship literally, so drop it from a preserved Depends.
for my $k (qw(Depends Pre-Depends Recommends Suggests)) {
next unless defined $f{$k};
$f{$k} =~ s/\$\{[^}]+\}//g;
$f{$k} =~ s/^[,\s]+|[,\s]+$//g;
$f{$k} =~ s/\s*,\s*,\s*/, /g;
delete $f{$k} if $f{$k} eq '';
}
my @order = qw(Package Version Section Priority Architecture Maintainer
Pre-Depends Depends Recommends Suggests Breaks Replaces Conflicts
Provides Description);
my $out = '';
for my $k (@order) {
next unless defined $f{$k} && $f{$k} ne '';
$out .= "$k: $f{$k}\n";
}
return $out;
}
# ---------------------------------------------------------------------------------------------------
# Built-.deb inspection + cross-arch genesis provisioning (filesystem; tested with real dpkg-deb).
# ---------------------------------------------------------------------------------------------------
@@ -596,9 +549,30 @@ sub deb_hash {
# Idempotent; content is compared by deb_hash so a stale same-name deb is refreshed rather than
# mistaken for up to date. $sign is an optional coderef($deb_path) invoked on each copied deb; pass
# undef to skip. Mirrors MockBuildUtils::cross_copy_genesis for the apt world.
# $codename, when given, narrows the set to the image built for that release -- see
# genesis_debs_for_codename.
# genesis_debs_for_codename: the Genesis debs that belong to ONE Ubuntu release.
#
# The Genesis image carries the kernel of the root that built it, so xcat-core builds one deb per
# codename and stamps the codename into the version (2.19.0-snap...~noble). Staging all of them into
# every suite publishes three images per suite, and apt serves the newest -- the image of another
# release. A deb with no codename in its version predates the native build and serves every release.
sub genesis_debs_for_codename {
my ($debs, $codename) = @_;
my @debs = @{ $debs || [] };
return @debs unless @debs && defined $codename && $codename ne '';
my $marked = qr/_[^_]*~[A-Za-z0-9.]+_[^_]*\.deb\z/;
return @debs unless grep { basename($_) =~ $marked } @debs;
return grep {
my $base = basename($_);
$base =~ /_[^_]*~\Q$codename\E_[^_]*\.deb\z/ || $base !~ $marked;
} @debs;
}
sub cross_copy_genesis_deb {
my ($from, $to, $arch, $sign) = @_;
my @src = glob("$from/xcat-genesis-base-$arch\_*.deb");
my ($from, $to, $arch, $sign, $codename) = @_;
my @src = genesis_debs_for_codename(
[ glob("$from/xcat-genesis-base-$arch\_*.deb") ], $codename);
return 0 if !@src;
my %want = map { basename($_) => $_ } @src;
my @existing = glob("$to/xcat-genesis-base-$arch\_*.deb");
+91 -4
View File
@@ -6,10 +6,13 @@ package MockBuildUtils;
use strict;
use warnings;
use Exporter 'import';
use File::Basename qw(basename);
use File::Basename qw(basename dirname);
use File::Copy qw(copy);
use File::Glob qw(bsd_glob);
use File::Find;
use File::Path qw(remove_tree);
use lib dirname(__FILE__) . '/lib';
use XCAT::NFSLock ();
use Sys::Hostname;
use Digest::MD5 qw(md5_hex);
@@ -23,7 +26,8 @@ our @EXPORT_OK = qw(
parse_evr evr_cmp evr_constraint_ok parse_pin rpmkeys_checksig_problem
rpm_version rpm_release rpm_sigmd5 rpm_is_signed restamp_release_line
cross_copy_genesis finalize_xcat_dep bump_dep_release_suffix
build_mock_uniqueext rpm_in_cell
build_mock_uniqueext rpm_in_cell resolve_mock_cfg
recover_common_repository
);
# install_deps_packages($os_id): the host packages mockbuild-all.pl needs to run at all, for the
@@ -545,6 +549,15 @@ sub finalize_xcat_dep {
my ($x86_64_repo, $ppc64le_repo, %opt) = @_;
my $sign = $opt{sign};
my $reindex = $opt{reindex};
# The arches whose cells this run writes. The others are read only: each host finalizes the
# cells it deploys, so it never holds a cell lock that only another host could reclaim.
my %known = map { $_->{arch} => 1 } @GENESIS_ARCHES;
my @only = @{ $opt{only} // [ map { $_->{arch} } @GENESIS_ARCHES ] };
for my $a (@only) {
die "FATAL: [finalize] no cross-arch genesis for arch '$a'\n" unless $known{$a};
}
my %write = map { $_ => 1 } @only;
my @dst_arches = grep { $write{ $_->{arch} } } @GENESIS_ARCHES;
print_step('Finalize xcat-dep: cross-arch genesis-base provisioning (issue #7610)');
print "x86_64-repo: $x86_64_repo\n";
print "ppc64le-repo: $ppc64le_repo\n";
@@ -585,7 +598,7 @@ sub finalize_xcat_dep {
# N-way cross-copy: put each arch's genesis into EVERY other arch's repo dir.
my @summary;
for my $src (@GENESIS_ARCHES) {
for my $dst (@GENESIS_ARCHES) {
for my $dst (@dst_arches) {
next if $src->{arch} eq $dst->{arch};
my $n = cross_copy_genesis($adir{$src->{arch}}, $adir{$dst->{arch}}, $src->{tarch}, $sign);
push @summary, "$n $src->{tarch} -> $dst->{arch}";
@@ -596,7 +609,7 @@ sub finalize_xcat_dep {
# rpm on disk (so cross_copy_genesis now returns 0) yet ABSENT from repomd.xml -- which no
# signature gate catches. Re-indexing is cheap (tiny repos) and idempotent, and heals that
# partial state; skipped only when no signer/indexer was injected.
if ($reindex) { $reindex->($adir{$_->{arch}}) for @GENESIS_ARCHES; }
if ($reindex) { $reindex->($adir{$_->{arch}}) for @dst_arches; }
print "[finalize] $osdir: " . join(', ', @summary) . "\n";
$pairs++;
}
@@ -723,4 +736,78 @@ sub build_mock_uniqueext {
return sprintf("mba-%02d-%s-%s", $idx, $run_part, $label_part);
}
#--------------------------------------------------------------------------------
=head3 recover_common_repository
Descriptions:
Put back the common tree that an interrupted publication moved aside, and
remove the staging trees it left. Runs only under the common lock, so it
never removes the staging tree of a run that is still publishing.
Arguments:
$base: the --repo-dep directory
Returns:
1 when the recovery ran, 0 when another run holds the common lock.
=cut
#--------------------------------------------------------------------------------
sub recover_common_repository {
my ($base) = @_;
my $path = "$base/.common-publish.lock";
my $lock = eval { XCAT::NFSLock->acquire($path, label => 'common lock') };
unless ($lock) {
# The lock is live or unproven: this is not the place to offer its removal.
print "common recovery skipped: another run holds $path\n";
return 0;
}
my $destination = "$base/common";
my @backups = sort {
((stat($a))[9] // 0) <=> ((stat($b))[9] // 0)
} grep { -d $_ && !-l $_ } bsd_glob("$base/.common.previous.*");
if (!-e $destination && !-l $destination && @backups) {
my $backup = pop(@backups);
unless (rename($backup, $destination)) {
my $error = $!;
$lock->release;
die "Cannot restore interrupted common repository $backup: $error\n";
}
}
remove_tree($_) for grep { -d $_ && !-l $_ } @backups;
for my $staging (bsd_glob("$base/.common.*")) {
next if $staging =~ m{/\.common\.previous\.};
remove_tree($staging) if -d $staging && !-l $staging;
}
$lock->release;
return 1;
}
# resolve_mock_cfg($os_id, $rel, $arch[, $cfg_dir]): the mock config for EL release $rel on this
# host, <id>+epel-<rel>-<arch>. /etc/os-release says 'almalinux' where mock-core-configs names the
# file 'alma', so the short form is tried too. $cfg_dir defaults to /etc/mock.
sub resolve_mock_cfg {
my ($os_id, $rel, $arch, $cfg_dir) = @_;
$cfg_dir //= '/etc/mock';
my %short_forms = (
almalinux => 'alma',
'centos-stream' => 'centos-stream',
rocky => 'rocky',
);
# Resolve by CONFIG-FILE existence, not by running `mock --print-root-path`: the latter can fail
# transiently (bootstrap chroot setup, a concurrent mock holding a lock) and made el10 flakily
# "resolve" to the long form that has no .cfg. Checking <cfg_dir>/<cfg>.cfg is deterministic.
for my $id ($os_id, (exists $short_forms{$os_id} ? ($short_forms{$os_id}) : ())) {
my $candidate = "${id}+epel-${rel}-${arch}";
if (-f "$cfg_dir/${candidate}.cfg") {
print "Mock config resolved: $candidate\n" if $id ne $os_id;
return $candidate;
}
}
my $short = $short_forms{$os_id} // $os_id;
die "Could not find mock config for ${os_id}+epel-${rel}-${arch} "
. "(tried $cfg_dir/${os_id}+epel-${rel}-${arch}.cfg and $cfg_dir/${short}+epel-${rel}-${arch}.cfg)\n";
}
1;
+16 -3
View File
@@ -31,10 +31,11 @@
# PER-ARCH SETS (review concern #3 -- the arch matrix must be valid):
# * Compiled, arch-specific deps that genuinely build on BOTH arches are listed for amd64 AND
# ppc64el: ipmitool-xcat, conserver-xcat, goconserver (debian/control Architecture: any / *-ppc64el).
# * The noarch boot components (syslinux-xcat, grub2-xcat, elilo-xcat, xnba-undi) are
# * The noarch boot components (syslinux-xcat, grub2-xcat, elilo-xcat, xnba-undi, ipxe-xcat) are
# Architecture:all: their SOURCE is x86-only (syslinux compiles with nasm/gcc-multilib; elilo/xnba
# are x86/EFI loaders; grub2-xcat is config/scripts), so they are BUILT ONCE on amd64 -- SINGLE
# PRODUCER, concern #3b -- and, being arch:all, assembled into EVERY arch's Packages index. They
# are x86/EFI loaders; grub2-xcat is config/scripts; ipxe-xcat repackages the iPXE release
# tree), so they are BUILT ONCE on amd64 -- SINGLE PRODUCER, concern #3b -- and, being
# arch:all, assembled into EVERY arch's Packages index. They
# ARE listed for ppc64el too, as REQUIRED-PRESENT: a ppc MN needs them for netboot, so the gate
# must verify the ppc repo carries them (matching the EL manifest and the historical 2.16 ppc dep
# repo, minus the obsolete yaboot-xcat). sbuild-all.pl's build phase SKIPS an Architecture:all
@@ -67,6 +68,7 @@ syslinux-xcat=3.86-2
grub2-xcat=2.12-2
elilo-xcat=3.14-6
xnba-undi=1.21.1-1
ipxe-xcat=2.0.0-1
xcat-genesis-base=2.*
[focal-ppc64el]
@@ -77,6 +79,7 @@ syslinux-xcat=3.86-2
grub2-xcat=2.12-2
elilo-xcat=3.14-6
xnba-undi=1.21.1-1
ipxe-xcat=2.0.0-1
xcat-genesis-base=2.*
[focal-riscv64]
@@ -87,6 +90,7 @@ syslinux-xcat=3.86-2
grub2-xcat=2.12-2
elilo-xcat=3.14-6
xnba-undi=1.21.1-1
ipxe-xcat=2.0.0-1
# ============================ jammy (ubuntu22.04) ============================
[jammy-amd64]
@@ -97,6 +101,7 @@ syslinux-xcat=3.86-2
grub2-xcat=2.12-2
elilo-xcat=3.14-6
xnba-undi=1.21.1-1
ipxe-xcat=2.0.0-1
xcat-genesis-base=2.*
[jammy-ppc64el]
@@ -107,6 +112,7 @@ syslinux-xcat=3.86-2
grub2-xcat=2.12-2
elilo-xcat=3.14-6
xnba-undi=1.21.1-1
ipxe-xcat=2.0.0-1
xcat-genesis-base=2.*
[jammy-riscv64]
@@ -117,6 +123,7 @@ syslinux-xcat=3.86-2
grub2-xcat=2.12-2
elilo-xcat=3.14-6
xnba-undi=1.21.1-1
ipxe-xcat=2.0.0-1
# ============================ noble (ubuntu24.04) ============================
[noble-amd64]
@@ -127,6 +134,7 @@ syslinux-xcat=3.86-2
grub2-xcat=2.12-2
elilo-xcat=3.14-6
xnba-undi=1.21.1-1
ipxe-xcat=2.0.0-1
xcat-genesis-base=2.*
[noble-ppc64el]
@@ -137,6 +145,7 @@ syslinux-xcat=3.86-2
grub2-xcat=2.12-2
elilo-xcat=3.14-6
xnba-undi=1.21.1-1
ipxe-xcat=2.0.0-1
xcat-genesis-base=2.*
[noble-riscv64]
@@ -147,6 +156,7 @@ syslinux-xcat=3.86-2
grub2-xcat=2.12-2
elilo-xcat=3.14-6
xnba-undi=1.21.1-1
ipxe-xcat=2.0.0-1
# ============================ resolute (ubuntu26.04) =========================
[resolute-amd64]
@@ -157,6 +167,7 @@ syslinux-xcat=3.86-2
grub2-xcat=2.12-2
elilo-xcat=3.14-6
xnba-undi=1.21.1-1
ipxe-xcat=2.0.0-1
xcat-genesis-base=2.*
[resolute-ppc64el]
@@ -167,6 +178,7 @@ syslinux-xcat=3.86-2
grub2-xcat=2.12-2
elilo-xcat=3.14-6
xnba-undi=1.21.1-1
ipxe-xcat=2.0.0-1
xcat-genesis-base=2.*
[resolute-riscv64]
@@ -177,6 +189,7 @@ syslinux-xcat=3.86-2
grub2-xcat=2.12-2
elilo-xcat=3.14-6
xnba-undi=1.21.1-1
ipxe-xcat=2.0.0-1
# [shared] is NOT a build target. It describes the ONE pool the OpenEmbedded Genesis release is
# published into (pool/main/xcat-genesis-openembedded), which every suite indexes and which no
+23 -6
View File
@@ -44,7 +44,14 @@ $build_timestamp = time() unless defined $build_timestamp;
# The maintained debian/ is at ./debian in the copied package dir; the upstream source is cloned fresh
# at the pinned SHA into ./gcsrc, the maintained debian/ copied in, and dpkg-buildpackage run there
# (its .deb(s) land in the copied package dir, which the collector picks up).
my $build = <<'BUILD';
# The build script below is lifted by t/goconserver_cross_build.t and run with the commands it
# calls shadowed. Keep the marker: the test dies when it can no longer find this region.
my $host_deb_arch = `dpkg --print-architecture 2>/dev/null`;
chomp $host_deb_arch;
die "FATAL: cannot read the build host architecture from dpkg\n"
unless $host_deb_arch =~ /^[a-z0-9]+$/;
my $build = "HOST_DEB_ARCH=$host_deb_arch\n" . <<'BUILD';
set -e
VERSION=0.3.3
REPO=https://github.com/xcat2/goconserver.git
@@ -52,13 +59,23 @@ REF=6166fe5ec1c5b3c20475e322a9f0e8e93c87e45f
GO_PIN=1.25.12
# pinned modern Go toolchain (static CGO-free build, portable across codenames; reproducible compiler)
go_arch=$(dpkg --print-architecture); [ "$go_arch" = ppc64el ] && go_arch=ppc64le
echo "installing pinned go${GO_PIN} (${go_arch}) for the goconserver build"
rm -rf /usr/local/go
curl -fsSL "https://go.dev/dl/go${GO_PIN}.linux-${go_arch}.tar.gz" | tar -C /usr/local -xz
export PATH=/usr/local/go/bin:$PATH
# The toolchain is the BUILD HOST's and the target comes from GOARCH, because riscv64 has no build
# host: its chroot runs under qemu-user, and a riscv64 `go build` there parks in futex_wait and never
# returns. Go cross-compiles a CGO-free binary, and a Go toolchain is statically linked, so the host
# one runs inside the foreign chroot at native speed. HOST_DEB_ARCH is stamped in by sbuild.pl: it
# cannot be read here, because qemu makes the chroot's dpkg and uname both answer for the target.
deb_to_goarch() { case "$1" in ppc64el) echo ppc64le;; *) echo "$1";; esac; }
go_host_arch=$(deb_to_goarch "$HOST_DEB_ARCH")
go_target_arch=$(deb_to_goarch "$(dpkg --print-architecture)")
echo "installing pinned go${GO_PIN} (${go_host_arch}) to compile for ${go_target_arch}"
gotoolchain="$PWD/.gotoolchain"
mkdir -p "$gotoolchain"
curl -fsSL "https://go.dev/dl/go${GO_PIN}.linux-${go_host_arch}.tar.gz" | tar -C "$gotoolchain" --strip-components=1 -xz
export PATH="$gotoolchain/bin:$PATH"
export GOTOOLCHAIN=local # use exactly the pinned toolchain; never auto-download another
export GOOS=linux GOARCH="$go_target_arch"
go version
go env GOHOSTARCH GOARCH
if [ -n "${SOURCE_DATE_EPOCH:-}" ]; then
SNAP_TS=$(date -d "@$SOURCE_DATE_EPOCH" --utc '+%Y%m%d%H%M')
+2
View File
@@ -0,0 +1,2 @@
# The licence texts stay byte-identical to their upstream sources.
licenses/** -whitespace
+87
View File
@@ -0,0 +1,87 @@
ipxe-xcat
=========
This package installs the ipxeboot.tar.gz tree of the iPXE v2.0.0 release
under /tftpboot/xcat/ipxe. Nothing is rebuilt, and only the two shims are
replaced: see The shim. The x86_64-sb
and arm64-sb builds carry their Secure Boot signatures inside the files, and
the shim finds snponly.efi and ipxe.efi by name in its own directory, so the
package keeps every name, symlink and byte of the release tree.
Files
-----
ipxeboot-2.0.0.tar.gz
The ipxeboot.tar.gz asset of
https://github.com/ipxe/ipxe/releases/tag/v2.0.0, renamed. Its SHA-256,
01a526d4cc791fc30362259c609d6c506cc64a7bdff51b9a5eb788354e17eee1, is the
digest that GitHub publishes for the asset.
ipxe-2.0.0-source.tar.gz
The source archive of tag v2.0.0, commit
12798ec29aa8a64d8675c4378b99f5fe28447afb, from
https://github.com/ipxe/ipxe/archive/refs/tags/v2.0.0.tar.gz. Its content
equals "git archive --prefix=ipxe-2.0.0/ v2.0.0". snpnet.c and undinet.c
are GPL2_OR_LATER without the UBDL, so snponly.efi and undionly.kpxe are
GPLv2+ as a whole. The package installs this archive with the binaries.
ipxe-shimx64.efi, ipxe-shimaa64.efi
The ipxe-shimx64.efi and ipxe-shimaa64.efi assets of
https://github.com/ipxe/shim/releases/tag/ipxe-16.1, as ipxe replaced
them on 2026-05-27. Their SHA-256 values are the digests that GitHub
publishes for the assets.
SHA256SUMS
The SHA-256 of both archives and both shims. Both builders check it
before the build.
payload.sha256
One line for each directory, file and symlink of the release tree, with
the SHA-256 of each file and the target of each symlink. After the build,
both builders unpack the RPM or deb and compare its /tftpboot/xcat/ipxe
with this list, entry for entry, with verify-payload.pl. A difference
fails the build.
licenses/
ipxe/COPYING, COPYING.GPLv2 and COPYING.UBDL are from iPXE tag v2.0.0.
shim/COPYRIGHT is from ipxe/shim tag ipxe-16.1.
shim/openssl/LICENSE is from openssl/openssl tag OpenSSL_1_0_2k, the
OpenSSL version that shim 16.1 carries in Cryptlib.
shim/gnu-efi/README.efilib is from rhboot/gnu-efi commit dc7fd96, the
gnu-efi commit that tag ipxe-16.1 pins.
The shim
--------
The release tree carries shim 16.1 as x86_64-sb/shimx64.efi and
arm64-sb/shimaa64.efi, signed by the Microsoft Corporation UEFI CA 2011
only. Firmware that trusts only the UEFI CA 2023 refuses them with Secure
Boot on. On 2026-05-27 ipxe replaced the ipxe/shim ipxe-16.1 release assets
with a build signed by both CAs. Both builders install ipxe-shimx64.efi and
ipxe-shimaa64.efi over the shims of the tree, under the same names, so the
ipxe-shim.efi and snponly-shim.efi links still point to them. payload.sha256
lists the digests of the replacements.
Update to a new release
-----------------------
1. Download ipxeboot.tar.gz from the release, and compare its SHA-256 with
the digest on the release page.
2. Download the source archive of the tag, and compare its content with
"git archive" of the tag.
3. Replace both archives. Download ipxe-shimx64.efi and ipxe-shimaa64.efi
from the latest ipxe/shim release, and compare their SHA-256 with the
digests on its page. Drop them and their install lines when the shims of
the new tree carry the UEFI CA 2023 signature.
4. Write SHA256SUMS with sha256sum.
5. Write payload.sha256 from the tree with the shims in place:
mkdir tree
tar -xzf ipxeboot-<version>.tar.gz --strip-components=1 -C tree
cp ipxe-shimx64.efi tree/x86_64-sb/shimx64.efi
cp ipxe-shimaa64.efi tree/arm64-sb/shimaa64.efi
./verify-payload.pl --generate tree > payload.sha256
6. Update licenses/ when the release changes its licence texts or its shim.
7. Set the version in ipxe-xcat.spec and debian/changelog, and the ipxe-xcat
pins in packages-manifest.conf and debs-manifest.conf.
+4
View File
@@ -0,0 +1,4 @@
9ed6d029be901a0ccc87cb2e5f9c774620f30f84ebdd507c6dd3e1e6229b7bd5 ipxe-2.0.0-source.tar.gz
31c6d8ef9ed24dc810dd7b951cf86b2e9036a02dea8a32548c0e1c0ae4d6f1c9 ipxe-shimaa64.efi
5eecca2780bd49c900565e124516a1bd666ec5e012825f34991b6ba1ef2fa6cf ipxe-shimx64.efi
01a526d4cc791fc30362259c609d6c506cc64a7bdff51b9a5eb788354e17eee1 ipxeboot-2.0.0.tar.gz
+6
View File
@@ -0,0 +1,6 @@
ipxe-xcat (2.0.0-1) unstable; urgency=medium
* Package the ipxeboot.tar.gz tree of the iPXE v2.0.0 release, with the
ipxe/shim 16.1 shims signed by both Microsoft UEFI CAs.
-- xCAT <xcat-user@lists.sourceforge.net> Fri, 25 Sep 2026 12:00:00 +0000
+1
View File
@@ -0,0 +1 @@
12
+16
View File
@@ -0,0 +1,16 @@
Source: ipxe-xcat
Section: admin
Priority: optional
Maintainer: xCAT <xcat-user@lists.sourceforge.net>
Build-Depends: debhelper (>= 12)
Standards-Version: 4.5.0
Homepage: https://ipxe.org/
Package: ipxe-xcat
Architecture: all
Depends: ${misc:Depends}
Description: iPXE network boot binaries from the upstream release
The ipxeboot.tar.gz tree of the iPXE 2.0.0 release, installed unchanged
under /tftpboot/xcat/ipxe. It carries the signed Secure Boot builds and
their shim. The source archive of the release tag is installed with the
documentation.
+37
View File
@@ -0,0 +1,37 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: iPXE
Source: https://github.com/ipxe/ipxe/releases/tag/v2.0.0
Comment: ipxeboot-2.0.0.tar.gz is the ipxeboot.tar.gz asset of the iPXE v2.0.0
release, unchanged. ipxe-2.0.0-source.tar.gz is the source archive of tag
v2.0.0, installed as /usr/share/doc/ipxe-xcat/ipxe-2.0.0-source.tar.gz.
.
The files x86_64-sb/shimx64.efi and arm64-sb/shimaa64.efi in the release tree
are shim 16.1, built by the iPXE project from ipxe/shim tag ipxe-16.1. Shim is
under the BSD licence in licenses/shim/COPYRIGHT and includes OpenSSL 1.0.2k
(licenses/shim/openssl/LICENSE) and gnu-efi (licenses/shim/gnu-efi/README.efilib).
.
The licence texts are installed in /usr/share/doc/ipxe-xcat/licenses/.
Files: *
Copyright: Michael Brown <mbrown@fensystems.co.uk> and the iPXE contributors
License: GPL-2+
iPXE files are licensed under the GNU General Public License, version 2 or
(at your option) any later version, unless the file states another licence.
Some files are licensed under version 2 only, some under BSD or MIT terms,
and most may also be used under the Unmodified Binary Distribution Licence
(licenses/ipxe/COPYING.UBDL). Each file in the source archive states its
own licence.
.
On Debian systems, the complete text of the GNU General Public License
version 2 can be found in /usr/share/common-licenses/GPL-2.
Files: debian/*
Copyright: xCAT contributors
License: GPL-2+
This packaging is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.
.
On Debian systems, the complete text of the GNU General Public License
version 2 can be found in /usr/share/common-licenses/GPL-2.
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/make -f
# The payload is the upstream release tree, byte for byte: nothing strips it, and dh_compress and
# dh_fixperms leave /tftpboot and the licence texts alone.
VERSION := $(shell dpkg-parsechangelog -S Version | sed 's/-[^-]*$$//')
DEST := debian/ipxe-xcat
DOC := $(DEST)/usr/share/doc/ipxe-xcat
build build-arch build-indep:
clean:
dh_testdir
dh_clean
binary-arch:
binary-indep:
dh_testdir
dh_testroot
dh_prep
install -d $(DEST)/tftpboot/xcat/ipxe $(DOC)
tar -xzf ipxeboot-$(VERSION).tar.gz --no-same-owner --strip-components=1 -C $(DEST)/tftpboot/xcat/ipxe
install -m 0644 ipxe-shimx64.efi $(DEST)/tftpboot/xcat/ipxe/x86_64-sb/shimx64.efi
install -m 0644 ipxe-shimaa64.efi $(DEST)/tftpboot/xcat/ipxe/arm64-sb/shimaa64.efi
install -m 0644 ipxe-$(VERSION)-source.tar.gz $(DOC)/
cp -R licenses $(DOC)/licenses
dh_installdocs
dh_installchangelogs
dh_compress -Xlicenses/
dh_fixperms -Xtftpboot/
dh_installdeb
dh_gencontrol
dh_md5sums
dh_builddeb
binary: binary-indep binary-arch
.PHONY: build build-arch build-indep clean binary-arch binary-indep binary
+1
View File
@@ -0,0 +1 @@
3.0 (native)
Binary file not shown.
Binary file not shown.
Binary file not shown.
+59
View File
@@ -0,0 +1,59 @@
# The payload is the upstream release, byte for byte: signed EFI files must not be stripped or
# otherwise touched by the build-root policy scripts.
%global debug_package %{nil}
%global __os_install_post %{nil}
Name: ipxe-xcat
Version: 2.0.0
Release: 1
Summary: iPXE network boot binaries from the upstream release
License: GPL-2.0-only AND GPL-2.0-or-later AND BSD-2-Clause AND BSD-2-Clause-Patent AND BSD-3-Clause AND MIT AND OpenSSL
URL: https://ipxe.org/
BuildArch: noarch
Source0: ipxeboot-%{version}.tar.gz
Source1: ipxe-%{version}-source.tar.gz
Source2: licenses/ipxe/COPYING
Source3: licenses/ipxe/COPYING.GPLv2
Source4: licenses/ipxe/COPYING.UBDL
Source5: licenses/shim/COPYRIGHT
Source6: licenses/shim/openssl/LICENSE
Source7: licenses/shim/gnu-efi/README.efilib
Source8: ipxe-shimx64.efi
Source9: ipxe-shimaa64.efi
%description
The ipxeboot.tar.gz tree of the iPXE %{version} release, installed under
/tftpboot/xcat/ipxe. It carries the signed Secure Boot builds and their
shim. The shims are the ipxe/shim 16.1 assets signed by both Microsoft UEFI
CAs, 2011 and 2023, and every other file is unchanged. The source archive
of the release tag is installed with the documentation.
%prep
%setup -q -c -T
install -D -m 0644 %{SOURCE2} licenses/ipxe/COPYING
install -D -m 0644 %{SOURCE3} licenses/ipxe/COPYING.GPLv2
install -D -m 0644 %{SOURCE4} licenses/ipxe/COPYING.UBDL
install -D -m 0644 %{SOURCE5} licenses/shim/COPYRIGHT
install -D -m 0644 %{SOURCE6} licenses/shim/openssl/LICENSE
install -D -m 0644 %{SOURCE7} licenses/shim/gnu-efi/README.efilib
%build
%install
mkdir -p %{buildroot}/tftpboot/xcat/ipxe
tar -xzf %{SOURCE0} --no-same-owner --strip-components=1 -C %{buildroot}/tftpboot/xcat/ipxe
install -m 0644 %{SOURCE8} %{buildroot}/tftpboot/xcat/ipxe/x86_64-sb/shimx64.efi
install -m 0644 %{SOURCE9} %{buildroot}/tftpboot/xcat/ipxe/arm64-sb/shimaa64.efi
install -D -m 0644 %{SOURCE1} %{buildroot}%{_pkgdocdir}/ipxe-%{version}-source.tar.gz
%files
/tftpboot/xcat/ipxe
%license licenses/ipxe licenses/shim
%dir %{_pkgdocdir}
%doc %{_pkgdocdir}/ipxe-%{version}-source.tar.gz
%changelog
* Fri Sep 25 2026 xCAT <xcat-user@lists.sourceforge.net> - 2.0.0-1
- Package the ipxeboot.tar.gz tree of the iPXE v2.0.0 release, with the
ipxe/shim 16.1 shims signed by both Microsoft UEFI CAs
Binary file not shown.
+12
View File
@@ -0,0 +1,12 @@
In general iPXE files are licensed under the GPL. For historical
reasons, individual files may contain their own licence declarations.
Most builds of iPXE do not contain all iPXE code (in particular, most
builds will include only one driver), and so the overall licence can
vary depending on what target you are building.
The resultant applicable licence(s) for any particular build can be
determined by using "make bin/xxxxxxx.yyy.licence"; for example:
make bin/rtl8139.rom.licence
to determine the resultant licence(s) for the build bin/rtl8139.rom
+339
View File
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
+59
View File
@@ -0,0 +1,59 @@
UNMODIFIED BINARY DISTRIBUTION LICENCE
PREAMBLE
The GNU General Public License provides a legal guarantee that
software covered by it remains free (in the sense of freedom, not
price). It achieves this guarantee by imposing obligations on anyone
who chooses to distribute the software.
Some of these obligations may be seen as unnecessarily burdensome. In
particular, when the source code for the software is already publicly
and freely available, there is minimal value in imposing upon each
distributor the obligation to provide the complete source code (or an
equivalent written offer to provide the complete source code).
This Licence allows for the distribution of unmodified binaries built
from publicly available source code, without imposing the obligations
of the GNU General Public License upon anyone who chooses to
distribute only the unmodified binaries built from that source code.
The extra permissions granted by this Licence apply only to unmodified
binaries built from source code which has already been made available
to the public in accordance with the terms of the GNU General Public
Licence. Nothing in this Licence allows for the creation of
closed-source modified versions of the Program. Any modified versions
of the Program are subject to the usual terms and conditions of the
GNU General Public License.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
This Licence applies to any Program or other work which contains a
notice placed by the copyright holder saying it may be distributed
under the terms of this Unmodified Binary Distribution Licence. All
terms used in the text of this Licence are to be interpreted as they
are used in version 2 of the GNU General Public License as published
by the Free Software Foundation.
If you have made this Program available to the public in both source
code and executable form in accordance with the terms of the GNU
General Public License as published by the Free Software Foundation;
either version 2 of the License, or (at your option) any later
version, then you are hereby granted an additional permission to use,
copy, and distribute the unmodified executable form of this Program
(the "Unmodified Binary") without restriction, including the right to
permit persons to whom the Unmodified Binary is furnished to do
likewise, subject to the following conditions:
- when started running, the Program must display an announcement which
includes the details of your existing publication of the Program
made in accordance with the terms of the GNU General Public License.
For example, the Program could display the URL of the publicly
available source code from which the Unmodified Binary was built.
- when exercising your right to grant permissions under this Licence,
you do not need to refer directly to the text of this Licence, but
you may not grant permissions beyond those granted to you by this
Licence.
+30
View File
@@ -0,0 +1,30 @@
Copyright 2012 Red Hat, Inc <mjg@redhat.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.
Significant portions of this code are derived from Tianocore
(http://tianocore.sf.net) and are Copyright 2009-2012 Intel
Corporation.
@@ -0,0 +1,30 @@
The files in the "lib" and "inc" subdirectories are using the EFI Application
Toolkit distributed by Intel at http://developer.intel.com/technology/efi
This code is covered by the following agreement:
Copyright (c) 1998-2000 Intel Corporation
Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and
the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions
and the following disclaimer in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTEL BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE. THE EFI SPECIFICATION AND ALL OTHER INFORMATION
ON THIS WEB SITE ARE PROVIDED "AS IS" WITH NO WARRANTIES, AND ARE SUBJECT
TO CHANGE WITHOUT NOTICE.
+127
View File
@@ -0,0 +1,127 @@
LICENSE ISSUES
==============
The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
the OpenSSL License and the original SSLeay license apply to the toolkit.
See below for the actual license texts. Actually both licenses are BSD-style
Open Source licenses. In case of any license issues related to OpenSSL
please contact openssl-core@openssl.org.
OpenSSL License
---------------
/* ====================================================================
* Copyright (c) 1998-2016 The OpenSSL Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. All advertising materials mentioning features or use of this
* software must display the following acknowledgment:
* "This product includes software developed by the OpenSSL Project
* for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
*
* 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For written permission, please contact
* openssl-core@openssl.org.
*
* 5. Products derived from this software may not be called "OpenSSL"
* nor may "OpenSSL" appear in their names without prior written
* permission of the OpenSSL Project.
*
* 6. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by the OpenSSL Project
* for use in the OpenSSL Toolkit (http://www.openssl.org/)"
*
* THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
* EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
* ====================================================================
*
* This product includes cryptographic software written by Eric Young
* (eay@cryptsoft.com). This product includes software written by Tim
* Hudson (tjh@cryptsoft.com).
*
*/
Original SSLeay License
-----------------------
/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
* All rights reserved.
*
* This package is an SSL implementation written
* by Eric Young (eay@cryptsoft.com).
* The implementation was written so as to conform with Netscapes SSL.
*
* This library is free for commercial and non-commercial use as long as
* the following conditions are aheared to. The following conditions
* apply to all code found in this distribution, be it the RC4, RSA,
* lhash, DES, etc., code; not just the SSL code. The SSL documentation
* included with this distribution is covered by the same copyright terms
* except that the holder is Tim Hudson (tjh@cryptsoft.com).
*
* Copyright remains Eric Young's, and as such any Copyright notices in
* the code are not to be removed.
* If this package is used in a product, Eric Young should be given attribution
* as the author of the parts of the library used.
* This can be in the form of a textual message at program startup or
* in documentation (online or textual) provided with the package.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* "This product includes cryptographic software written by
* Eric Young (eay@cryptsoft.com)"
* The word 'cryptographic' can be left out if the rouines from the library
* being used are not cryptographic related :-).
* 4. If you include any Windows specific code (or a derivative thereof) from
* the apps directory (application code) you must include an acknowledgement:
* "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
*
* THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* The licence and distribution terms for any publically available version or
* derivative of this code cannot be changed. i.e. this code cannot simply be
* copied and put under another distribution licence
* [including the GNU Public Licence.]
*/
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/perl
# ipxe-xcat/mockbuild.pl -- build the ipxe-xcat noarch RPM with mock from the committed release
# archives. The archives are checked against SHA256SUMS before the build, and the built RPM payload
# against payload.sha256 before anything is copied to --result-dir.
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 FindBin qw($RealBin);
use Getopt::Long qw(GetOptions);
use lib "$RealBin/..", "$RealBin/../lib";
use MockBuildUtils qw(resolve_mock_cfg);
use XCAT::BuildUtils qw(capture_command digest_file print_step require_command run_command shell_quote);
my $pkg_dir = abs_path($RealBin);
my $repo_root = abs_path("$pkg_dir/..");
my $spec_file = "$pkg_dir/ipxe-xcat.spec";
my $work_dir = '/tmp/ipxe-xcat-mockbuild';
my $mock_cfg = '';
my $mock_uniqueext = '';
my $result_dir = "$repo_root/build-output/list3/ipxe-xcat";
my $log_dir = "$repo_root/build-logs/list3/ipxe-xcat";
my $build_timestamp;
GetOptions(
'work-dir=s' => \$work_dir,
'mock-cfg=s' => \$mock_cfg,
'mock-uniqueext=s' => \$mock_uniqueext,
'result-dir=s' => \$result_dir,
'log-dir=s' => \$log_dir,
'build-timestamp=i' => \$build_timestamp,
) or die usage();
die "Run as root (current uid=$>)\n" if $> != 0;
require_command($_) for qw(mock rpm rpm2cpio cpio bash sha256sum);
my ($version, @sources) = spec_sources($spec_file);
die "Could not parse Version from $spec_file\n" if !$version;
if (!$mock_cfg) {
my $os_id = capture_command('bash', '-c', 'source /etc/os-release; echo $ID');
my $arch = capture_command('uname', '-m');
$mock_cfg = resolve_mock_cfg($os_id, 10, $arch);
}
my @uniqueext = $mock_uniqueext ne '' ? ('--uniqueext', $mock_uniqueext) : ();
my $epoch = $build_timestamp;
if (!defined $epoch) {
$epoch = `git -C \Q$repo_root\E log -1 --format=%ct HEAD 2>/dev/null` // '';
chomp $epoch;
$epoch = time() if $epoch !~ /^\d+$/;
}
$ENV{SOURCE_DATE_EPOCH} = $epoch;
print_step('Configuration');
print "pkg_dir: $pkg_dir\n";
print "version: $version\n";
print "work_dir: $work_dir\n";
print "result_dir: $result_dir\n";
print "log_dir: $log_dir\n";
print "mock_cfg: $mock_cfg\n";
print_step('Check the release archives');
run_command('bash', '-c', 'cd ' . shell_quote($pkg_dir) . ' && sha256sum --check --strict SHA256SUMS');
print_step('Stage the sources');
remove_tree($work_dir) if -d $work_dir;
my $sources_dir = "$work_dir/sources";
make_path($sources_dir, $result_dir, $log_dir);
my %staged;
for my $source (@sources) {
my $name = basename($source);
die "Two Source files share the name $name\n" if $staged{$name}++;
copy("$pkg_dir/$source", "$sources_dir/$name")
or die "Failed to copy $pkg_dir/$source: $!\n";
}
run_command('bash', '-c', 'cd ' . shell_quote($sources_dir)
. ' && sha256sum --check --strict ' . shell_quote("$pkg_dir/SHA256SUMS"));
my $det_cfg = "$work_dir/mock-deterministic.cfg";
open(my $cfg_fh, '>', $det_cfg) or die "Cannot write $det_cfg: $!\n";
print {$cfg_fh} "include('/etc/mock/${mock_cfg}.cfg')\n";
print {$cfg_fh} "config_opts['environment']['SOURCE_DATE_EPOCH'] = '$epoch'\n";
close($cfg_fh) or die "Cannot write $det_cfg: $!\n";
my @defines = map { ('--define', $_) }
('use_source_date_epoch_as_buildtime 1', 'clamp_mtime_to_source_date_epoch 1', '_buildhost xcat-build');
print_step('Build the SRPM with mock');
my $srpm_out = "$work_dir/srpm";
make_path($srpm_out);
run_mock('mock', '-r', $det_cfg, @uniqueext, '--buildsrpm', '--spec', $spec_file,
'--sources', $sources_dir, '--resultdir', $srpm_out, @defines);
my @srpms = glob("$srpm_out/ipxe-xcat-*.src.rpm");
die "Expected one SRPM in $srpm_out, found " . scalar(@srpms) . "\n" if @srpms != 1;
print_step('Rebuild the RPM with mock');
my $rpm_out = "$work_dir/rpm";
make_path($rpm_out);
run_mock('mock', '-r', $det_cfg, @uniqueext, '--rebuild', $srpms[0], '--resultdir', $rpm_out, @defines);
my @rpms = glob("$rpm_out/ipxe-xcat-$version-*.noarch.rpm");
die "Expected one ipxe-xcat noarch RPM in $rpm_out, found " . scalar(@rpms) . "\n" if @rpms != 1;
my $rpm = $rpms[0];
print_step('Check the RPM payload');
my $payload = "$work_dir/payload";
make_path($payload);
run_command('bash', '-o', 'pipefail', '-c', 'cd ' . shell_quote($payload)
. ' && rpm2cpio ' . shell_quote($rpm) . ' | cpio -idm --quiet');
run_command('perl', "$pkg_dir/verify-payload.pl", "$payload/tftpboot/xcat/ipxe",
"$pkg_dir/payload.sha256");
check_installed("$payload/usr/share/doc/ipxe-xcat/ipxe-$version-source.tar.gz",
"$pkg_dir/ipxe-$version-source.tar.gz");
for my $licence (grep { m{^licenses/} } @sources) {
(my $installed = $licence) =~ s{^licenses/}{};
check_installed("$payload/usr/share/licenses/ipxe-xcat/$installed", "$pkg_dir/$licence");
}
print_step('Collect the results');
for my $file ($rpm, $srpms[0]) {
copy($file, $result_dir) or die "Failed to copy $file to $result_dir: $!\n";
print "Copied: $result_dir/" . basename($file) . "\n";
}
for my $log (qw(build.log root.log state.log)) {
copy("$rpm_out/$log", "$log_dir/$log") if -f "$rpm_out/$log";
copy("$srpm_out/$log", "$log_dir/srpm-$log") if -f "$srpm_out/$log";
}
print_step('Completed');
exit 0;
sub usage {
return <<"USAGE";
Usage: $0 [options]
--work-dir PATH Temporary work directory (default: $work_dir)
--mock-cfg NAME Mock config (default: the EL10 config of this host)
--mock-uniqueext TEXT mock --uniqueext suffix for concurrent builds
--result-dir PATH Output directory for the RPM and SRPM
--log-dir PATH Output directory for the mock logs
--build-timestamp EPOCH SOURCE_DATE_EPOCH for a reproducible build
USAGE
}
sub spec_sources {
my ($path) = @_;
open(my $fh, '<', $path) or die "Cannot read $path: $!\n";
my ($version, @sources) = ('');
while (my $line = <$fh>) {
$version = $1 if $line =~ /^Version:\s*(\S+)/;
push @sources, $1 if $line =~ /^Source\d*:\s*(\S+)/;
}
close($fh);
s/%\{version\}/$version/g for @sources;
return ($version, @sources);
}
sub check_installed {
my ($installed, $committed) = @_;
die "Missing from the RPM payload: $installed\n" if !-f $installed || -l $installed;
die "The RPM payload changed $installed\n" if digest_file($installed) ne digest_file($committed);
}
# mock exits 30 when its package manager failed, most often a transient mirror error: retry once.
sub run_mock {
my (@command) = @_;
my $ok = eval { run_command(@command) };
return 1 if $ok;
die $@ if $@ !~ /\(rc=30\)/;
print "mock failed with rc=30 (package manager); retrying once\n";
return run_command(@command);
}
+54
View File
@@ -0,0 +1,54 @@
# ipxe-xcat payload manifest, written by verify-payload.pl --generate
dir - arm32
file 2a8d50db2b3b5f02302302b3161931c1142301f42d52c64c0005974b602b7092 arm32/ipxe-legacy.efi
file fa58061710e6fab23fc27c6d200d510453c3f14f3bc0715fa6804111f1e5fc74 arm32/ipxe.efi
file 206310f30306569ed2c6e0ef015b3ad24230b303a1ad1171e19c117c914a65d6 arm32/snponly.efi
dir - arm64
dir - arm64-sb
link shimaa64.efi arm64-sb/ipxe-shim.efi
file 01b2438e6dc354c343590c308f1d4f000bc098be4d2237540432023abdbddcb8 arm64-sb/ipxe.efi
file 31c6d8ef9ed24dc810dd7b951cf86b2e9036a02dea8a32548c0e1c0ae4d6f1c9 arm64-sb/shimaa64.efi
link shimaa64.efi arm64-sb/snponly-shim.efi
file bed8da8639b45eeec55d41627349863efa5085a2113bb1193838b8a463e5f49e arm64-sb/snponly.efi
file cff2abf51b7b491d7c5e2ef68c45003a0b4c5e866cb5b20f162569746d53390d arm64/ipxe-legacy.efi
file a9cb6df506a68f3afa4bb94cf6cb8e3862a5a2ff3bf1d8027fea83f7e1d49217 arm64/ipxe.efi
file 03665b4184e4a0b8b9a889de0c7fb833fc48b02385e3fa40a4460db1122f1b36 arm64/snponly.efi
dir - i386
file 9dc4aee199f582fb8f55cf8bfb4622998c5e71c2dde149d5bbdfe33121c925d8 i386/ipxe-legacy.efi
file df9bab29487d20af3fe9dda46afc775009ba1c089d2216098b9a6e82d906939b i386/ipxe-legacy.pxe
file ea63c6d3745e751e8db4f9c8d4c97efa883fb6d0fc57c697b3d13ad98c1bfe0c i386/ipxe.efi
file a0f144a5f7e10e567d189606c6c2320c1f43a228f9fb6148e7e52f5009fd789f i386/ipxe.pxe
file 81a086995758b889fba7f13d7c5255dc7eb87ed38350b8c03112b9f56556de98 i386/snponly.efi
file ef6ff013f988c52feb3e543dccf0bdc50cd1d68b8a4ab84b8975cef33f64e848 i386/undionly.kpxe
link x86_64/ipxe-legacy.efi ipxe-legacy.efi
link x86_64/ipxe-legacy.pxe ipxe-legacy.pxe
link x86_64/ipxe.efi ipxe.efi
link x86_64/ipxe.pxe ipxe.pxe
dir - loong64
file 03b078b2ba00e97427b5d99b14035bdb7abdb48631aba8759cf95c24a2620b7e loong64/ipxe-legacy.efi
file c326c8bf54d8fc40fa6730907ee07cb44e8151ecc1397e1ddd2c0edef7cb2872 loong64/ipxe.efi
file fd7cb2df195ad5cc3a09f41fab23713afa23624ff664f83de0e7ca0fd5776fb7 loong64/snponly.efi
dir - riscv32
file 41f76da3efc3293887a701f35cac389c04a3a8110743673e37451e3c6e24e507 riscv32/ipxe-legacy.efi
file 4afd51329213e50a4aaf24b397e8aba67910ca1e94ea3a6fab0e089d91ac69b8 riscv32/ipxe.efi
file 0a901453bde1392051e53bb1e75584a4eb7cc9c75a27a2b34d7b26ad21734f8f riscv32/snponly.efi
dir - riscv64
file a72923e2ff600bd94628ef5f97a7ddd5c23fabdc9736fdaa2c22cfb7f0fe908d riscv64/ipxe-legacy.efi
file 53c926c45043c1f2ad03aa70aa22921accc3202865c791cc6023fa17d92f8f24 riscv64/ipxe.efi
file 95b9f6466e0d88fafe642c46d3d6024e4107cac24d2197664b62610f21a48585 riscv64/snponly.efi
link x86_64-sb sb
link x86_64/snponly.efi snponly.efi
link x86_64/undionly.kpxe undionly.kpxe
dir - x86_64
dir - x86_64-sb
link shimx64.efi x86_64-sb/ipxe-shim.efi
file 6558e37887516b246d6a97122e8d18bedfe4197b7ba7f67bf1bf102a16678d33 x86_64-sb/ipxe.efi
file 5eecca2780bd49c900565e124516a1bd666ec5e012825f34991b6ba1ef2fa6cf x86_64-sb/shimx64.efi
link shimx64.efi x86_64-sb/snponly-shim.efi
file b1e67c3e4a1e8708ddfd0079ad4505e3a02245acb55ee9a95437ab3c507be82a x86_64-sb/snponly.efi
file 1d567f2e89a3c0cf0a184549918a3e799a47dc542df385c56e6b464e4765fae2 x86_64/ipxe-legacy.efi
file fc37ef1e4a4bd5d22f8db3c84e1718ac8b03fe20156cf2fc4b9e2ae5e4a348db x86_64/ipxe-legacy.pxe
file 868aa34057ff416ebf2fdfb5781de035e2c540477c04039198a9f8a9c6130034 x86_64/ipxe.efi
file 0fb21c695c82fd70d4f89732ddcd285063776e2bbd71de9a2c39e9cfe4b180b8 x86_64/ipxe.pxe
file f61c2ce34e05d7d857633df2e512d547df75b6aa18b2da152a7c9af222cfe28f x86_64/snponly.efi
file 4186562d21ff54e970d905751c9f36d628e73a51a94afe4a6a42f925b0df448c x86_64/undionly.kpxe
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env perl
# ipxe-xcat/sbuild.pl -- per-package Ubuntu/Debian builder for ipxe-xcat, the apt analogue of
# ipxe-xcat/mockbuild.pl. Invoked by sbuild-all.pl per (codename,arch); also runnable standalone.
# The build runs on a copy of the package tree inside the <codename>-<arch>-sbuild chroot, and it
# checks the archives before dpkg-buildpackage and the built payload after it, so a deb that
# differs from the release never reaches --result-dir.
use strict;
use warnings;
use Cwd qw(abs_path);
use File::Basename qw(basename);
use Getopt::Long qw(GetOptions);
use FindBin qw($RealBin);
use lib "$RealBin/..";
use BuildUtils qw(chroot_name build_deb_in_chroot);
my $pkg_dir = abs_path($RealBin);
my $pkg = basename($pkg_dir);
my ($codename, $arch, $chroot, $result_dir, $log_dir) = ('', '', '', '', '');
my ($build_timestamp, $build_number, $skip_install) = (undef, undef, 0);
# --log-dir, --build-number and --skip-install keep the command line sbuild-all.pl passes to every
# builder; this package has no use for them.
GetOptions(
'codename=s' => \$codename, 'arch=s' => \$arch, 'chroot=s' => \$chroot,
'result-dir=s' => \$result_dir, 'log-dir=s' => \$log_dir,
'build-timestamp=i' => \$build_timestamp, 'build-number=i' => \$build_number,
'skip-install!' => \$skip_install,
) or die "bad options\n";
$arch ||= `dpkg --print-architecture 2>/dev/null`; chomp $arch; $arch ||= 'amd64';
die "FATAL: --codename required\n" unless $codename;
$chroot ||= chroot_name($codename, $arch);
$result_dir ||= "$pkg_dir/../build-output/sbuild/$codename/$arch";
$build_timestamp = time() unless defined $build_timestamp;
# ipxe-xcat is Architecture:all and is built once on amd64 (see debs-manifest.conf).
my $build = <<'BUILD';
set -e
sha256sum --check --strict SHA256SUMS
dpkg-buildpackage -uc -us -b
version=$(dpkg-parsechangelog -S Version)
payload=$(mktemp -d)
dpkg-deb -x "../ipxe-xcat_${version}_all.deb" "$payload"
perl ./verify-payload.pl "$payload/tftpboot/xcat/ipxe" payload.sha256
source_archive="ipxe-${version%-*}-source.tar.gz"
grep -F " $source_archive" SHA256SUMS \
| (cd "$payload/usr/share/doc/ipxe-xcat" && sha256sum --check --strict -)
for licence in $(cd licenses && find . -type f); do
cmp "licenses/$licence" "$payload/usr/share/doc/ipxe-xcat/licenses/$licence"
done
rm -rf "$payload"
BUILD
build_deb_in_chroot(
pkg => $pkg, chroot => $chroot, pkg_dir => $pkg_dir, result_dir => $result_dir,
build_timestamp => $build_timestamp, build => $build,
);
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/perl
# verify-payload.pl -- compare an unpacked ipxe-xcat tree with payload.sha256.
#
# verify-payload.pl --generate <tree> > payload.sha256
# verify-payload.pl <tree> <payload.sha256>
#
# Each manifest line is "<type>\t<value>\t<path>", sorted by path: "file" with the SHA-256 of the
# content, "link" with the symlink target, "dir" with "-". Paths are relative to <tree>, and
# symlinks are never followed. The check exits 0 when the tree matches the manifest entry for
# entry, 1 when it differs, and 2 on a usage or read error.
use strict;
use warnings;
use Digest::SHA ();
use File::Find ();
use Getopt::Long qw(GetOptions);
my $generate = 0;
GetOptions('generate' => \$generate) or usage();
if ($generate) {
usage() if @ARGV != 1;
my $tree = scan_tree($ARGV[0]);
print "# ipxe-xcat payload manifest, written by verify-payload.pl --generate\n";
for my $path (sort keys %{$tree}) {
print join("\t", @{ $tree->{$path} }, $path), "\n";
}
exit 0;
}
usage() if @ARGV != 2;
my ($root, $manifest_file) = @ARGV;
my $expected = read_manifest($manifest_file);
my $found = scan_tree($root);
my @problems;
for my $path (sort keys %{$expected}) {
my ($type, $value) = @{ $expected->{$path} };
if (!exists $found->{$path}) {
push @problems, "missing: $path";
next;
}
my ($found_type, $found_value) = @{ $found->{$path} };
if ($found_type ne $type) {
push @problems, "type changed: $path (expected $type, found $found_type)";
} elsif ($type eq 'file' && $found_value ne $value) {
push @problems, "content changed: $path";
} elsif ($type eq 'link' && $found_value ne $value) {
push @problems, "link target changed: $path (expected $value, found $found_value)";
}
}
push @problems, map { "unexpected: $_" } grep { !exists $expected->{$_} } sort keys %{$found};
if (@problems) {
print STDERR "$_\n" for @problems;
print STDERR "payload does not match $manifest_file: " . scalar(@problems) . " difference(s)\n";
exit 1;
}
print "payload matches $manifest_file: " . scalar(keys %{$expected}) . " entries\n";
exit 0;
sub usage {
print STDERR "Usage: $0 --generate <tree>\n $0 <tree> <payload.sha256>\n";
exit 2;
}
sub fail {
my ($message) = @_;
print STDERR "$0: $message\n";
exit 2;
}
sub scan_tree {
my ($dir) = @_;
$dir =~ s{/+\z}{} if $dir ne '/';
fail("not a directory: $dir") if -l $dir || !-d $dir;
my %entries;
File::Find::find({
no_chdir => 1,
wanted => sub {
my $path = $File::Find::name;
return if $path eq $dir;
my $relative = substr($path, length($dir) + 1);
lstat($path) or fail("cannot stat $path: $!");
if (-l _) {
my $target = readlink($path);
fail("cannot read link $path: $!") if !defined $target;
$entries{$relative} = ['link', $target];
} elsif (-d _) {
$entries{$relative} = ['dir', '-'];
} elsif (-f _) {
my $sha = Digest::SHA->new(256);
eval { $sha->addfile($path, 'b'); 1 } or fail("cannot read $path: $@");
$entries{$relative} = ['file', $sha->hexdigest];
} else {
$entries{$relative} = ['other', '-'];
}
},
}, $dir);
return \%entries;
}
sub read_manifest {
my ($file) = @_;
open(my $fh, '<', $file) or fail("cannot read $file: $!");
my %entries;
while (my $line = <$fh>) {
chomp $line;
next if $line =~ /^\s*(?:#|$)/;
my ($type, $value, $path) = split(/\t/, $line, 3);
fail("$file line $.: malformed entry")
if !defined $path || $path eq '' || $type !~ /^(?:file|link|dir)$/
|| ($type eq 'file' && $value !~ /^[0-9a-f]{64}$/);
fail("$file line $.: duplicate path $path") if exists $entries{$path};
$entries{$path} = [$type, $value];
}
close($fh);
return \%entries;
}
+494
View File
@@ -0,0 +1,494 @@
package XCAT::NFSLock;
# NFS lock protocol
#
# A lock on a shared, possibly re-exported, NFS tree. flock and fcntl are not
# available there. The protocol uses mkdir, rmdir, unlink and plain file
# writes. It uses no rename.
#
# Names in this module
# lock.d the lock path given to acquire
# lock.d/metadata the metadata of the owner
# lock.borrow <lock path>.borrow, beside lock.d
# R, T, δ the options retries, delay and jitter
#
# Assumptions
# 1. mkdir(path) is atomic and exclusive among contenders, and successful
# namespace changes eventually become visible.
# 2. machine-id is unique among participating hosts.
# Cloned VMs and images can share it by accident unless it is regenerated.
# 3. Metadata writes eventually become readable completely and consistently.
# 4. A host never declares one of its own live process incarnations dead.
# 5. A process cannot die:
# - after it creates lock.d, until it publishes valid metadata;
# - while it holds lock.borrow, until it removes it.
# 6. A crashed worker leaves recoverable state. The owning host eventually
# returns and retries. Eventually one worker and its release complete.
# 7. All participants follow the protocol.
#
# Metadata
# lock.d/metadata contains:
# machine-id, boot-id, pid, pstart, token, hash
#
# Ownership identity: (machine-id, boot-id, pid, pstart, token)
# token is random per acquisition.
# hash = HASH(canonical(SORT(k, v))) over all fields except hash.
#
# If the metadata is missing, cannot be parsed or hashed, or the hash does not
# match, assume a partial or inconsistent read and retry. Never infer stale
# ownership from invalid metadata.
#
# Retry
# R = max retries, T = base delay, δ = jitter
# R > 0, δ >= 0, T >= 3, T > 2δ
#
# Generic retry:
# if retries >= R: fail
# sleep(T + rand(-δ, δ))
# retries++
# goto 1
#
# Protocol
# 1. mkdir lock.d
# - success: write valid metadata, go to 8
# - EEXIST: continue
# - other error: fail
# 2. Read and validate the metadata.
# - invalid or missing: retry
# - different machine-id: retry
# - same host: save the observed ownership identity
# 3. mkdir lock.borrow
# - failure: retry
# 4. Read and validate the metadata again.
# - invalid, missing, or ownership identity changed: rmdir lock.borrow, retry
# 5. Prove that the recorded (boot-id, pid, pstart) is dead.
# - not provably dead: rmdir lock.borrow, retry
# - dead: continue
# 6. Replace the metadata with the identity of this process and a fresh token.
# 7. rmdir lock.borrow
# 8. Call the worker.
# 9. mkdir lock.borrow
# - failure: sleep(T + rand(-δ, δ)), retry step 9
# 10. unlink lock.d/metadata
# 11. rmdir lock.d (the actual unlock)
# 12. rmdir lock.borrow
#
# Core invariants
# lock.d exists => locked
# lock.d absent => acquirable
# invalid metadata => retry only
# different machine-id => never recover here
# lock.borrow exists => ownership transition or release in progress
#
# Steps 1 to 7 are acquire, step 8 is the caller, steps 9 to 12 are release.
# release does steps 10 and 11 only when the metadata names this acquisition.
#
# Log
# Unless quiet => 1, each lock event prints one line to the selected output handle:
# [nfslock] <UTC time> <host> pid=<pid> <event> <label> <lock.d> [detail]
# Events: acquired (step 1), took-over (step 6), wait (a retry), released (step 11),
# release-skipped. The time has milliseconds, so the lines of two hosts sort into one order.
use strict;
use warnings;
use Cwd qw(abs_path);
use Digest::SHA qw(sha256_hex);
use Errno qw(EEXIST ENOENT);
use Exporter 'import';
use File::Basename qw(dirname basename);
use POSIX qw(strftime);
use Sys::Hostname qw(hostname);
use Time::HiRes ();
our @EXPORT_OK = qw(this_process format_metadata parse_metadata owner_is_dead process_start);
my @IDENTITY = qw(machine-id boot-id pid pstart token);
#--------------------------------------------------------------------------------
=head3 acquire
Descriptions:
Take the lock at $path with steps 1 to 7 of the protocol.
Arguments:
$path: path of lock.d
%opt:
label => word for messages (default "lock")
delay => T, seconds, 3 or more (default 3)
jitter => δ, seconds, 0 or more and less than T/2 (default 0.5)
retries => R, more than 0 (default: timeout / T, at least 1)
timeout => seconds to wait, used when retries is not given (default 0)
quiet => 1 to print no log lines for this lock
Returns:
A lock object. Dies after R retries, naming the lock and its owner.
=cut
#--------------------------------------------------------------------------------
sub acquire {
my ($class, $path, %opt) = @_;
my $label = $opt{label} // 'lock';
my $delay = $opt{delay} // 3;
my $jitter = $opt{jitter} // 0.5;
die "Invalid delay $delay for $label: must be 3s or more\n" unless $delay >= 3;
die "Invalid jitter $jitter for $label: must be 0 or more\n" unless $jitter >= 0;
die "Invalid jitter $jitter for $label: must be less than half the delay\n"
unless $delay > 2 * $jitter;
my $retries = $opt{retries};
unless (defined($retries)) {
my $timeout = $opt{timeout} // 0;
$retries = int($timeout / $delay);
$retries++ if $retries * $delay < $timeout;
$retries = 1 if $retries < 1;
}
die "Invalid retries $retries for $label: must be a whole number more than 0\n"
unless $retries =~ /\A[1-9][0-9]*\z/;
# The last component names the lock itself. basename would turn '' into './' and drop a
# trailing slash, so the raw path is checked.
my ($name) = ($path // '') =~ m{(?:\A|/)([^/]+)\z};
die "Invalid $label path '" . ($path // '') . "'\n"
if !defined($name) || $name eq '.' || $name eq '..';
my $abs = _absolute($path);
my $borrow = "$abs.borrow";
my $self = bless {
path => $abs,
label => $label,
pid => $$,
delay => $delay,
jitter => $jitter,
quiet => $opt{quiet} ? 1 : 0,
}, $class;
my $here = _here();
my $seen;
for (my $count = 0 ; ; $count++) {
# Step 1.
if (mkdir($abs)) {
$self->{identity} = this_process();
if (eval { _write_metadata($abs, $self->{identity}); 1 }) {
$self->_log('acquired');
return $self;
}
my $error = $@;
unlink("$abs/metadata");
rmdir($abs);
die $error;
}
die "Cannot create $label $abs: $!\n" unless $! == EEXIST;
# Step 2.
my $observed = _read_metadata($abs);
$seen = $observed if defined($observed);
if (defined($observed) && $observed->{'machine-id'} eq $here->{'machine-id'} && mkdir($borrow)) {
# Steps 3 to 7.
my $taken = eval {
my $again = _read_metadata($abs);
return 0 unless defined($again) && _key($again) eq _key($observed);
return 0 unless owner_is_dead($again, $here);
$self->{identity} = this_process();
_write_metadata($abs, $self->{identity});
1;
};
my $error = $@;
rmdir($borrow);
die $error unless defined($taken);
if ($taken) {
$self->_log('took-over', 'from dead pid ' . $observed->{pid});
return $self;
}
}
last if $count >= $retries;
$self->_log('wait', sprintf('retry %d/%d, owner %s', $count + 1, $retries, _describe($seen)));
_sleep(_wait($delay, $jitter));
}
my $who = _describe($seen);
my $s = $retries == 1 ? 'retry' : 'retries';
die "Trying to unlock $abs failed after $retries $s; $label owned by $who.\n"
. "If you are sure it is safe, remove the lock: rm -rf $abs\n";
}
#--------------------------------------------------------------------------------
=head3 release
Descriptions:
Steps 9 to 12 of the protocol. Waits while another process holds
lock.borrow. A forked child of the owner does nothing, and a lock whose
metadata names another acquisition is left alone.
Arguments:
none
Returns:
1 when the lock was removed, 0 otherwise.
=cut
#--------------------------------------------------------------------------------
sub release {
my ($self) = @_;
return 0 if $self->{released} || $$ != $self->{pid};
my $path = $self->{path};
my $borrow = "$path.borrow";
while (1) {
# Step 9.
if (mkdir($borrow)) {
my $current = _read_metadata($path);
if (defined($current) && _key($current) eq _key($self->{identity})) {
$self->{released} = 1;
unlink("$path/metadata");
my $removed = rmdir($path);
my $error = $!;
rmdir($borrow);
warn "Cannot remove $path: $error\n" unless $removed;
$self->_log($removed ? 'released' : 'release-skipped', $removed ? () : ("rmdir: $error"));
return $removed ? 1 : 0;
}
rmdir($borrow);
# Valid metadata of another acquisition, or no lock.d at all: this lock is gone.
if (defined($current) || !-d $path) {
$self->{released} = 1;
$self->_log('release-skipped', defined($current) ? 'owned by ' . _describe($current) : 'no lock.d');
return 0;
}
}
elsif ($! != EEXIST) {
warn "Cannot create $borrow: $!\n";
return 0;
}
_sleep(_wait($self->{delay}, $self->{jitter}));
}
}
sub path { return $_[0]{path} }
#--------------------------------------------------------------------------------
=head3 this_process
Descriptions:
The ownership identity of this process with a fresh token.
Arguments:
none
Returns:
A hash ref with machine-id, boot-id, pid, pstart and token.
=cut
#--------------------------------------------------------------------------------
sub this_process {
my $here = _here();
return {
'machine-id' => $here->{'machine-id'},
'boot-id' => $here->{'boot-id'},
pid => $$,
pstart => process_start($$) // die("Cannot read the start time of pid $$\n"),
token => _token(),
};
}
#--------------------------------------------------------------------------------
=head3 format_metadata
Descriptions:
The content of lock.d/metadata: one "key=value" line per field, sorted
by key, and the hash line.
Arguments:
$fields: hash ref with machine-id, boot-id, pid, pstart and token
Returns:
The metadata string.
=cut
#--------------------------------------------------------------------------------
sub format_metadata {
my ($fields) = @_;
my $canonical = _canonical($fields);
return $canonical . 'hash=' . sha256_hex($canonical) . "\n";
}
#--------------------------------------------------------------------------------
=head3 parse_metadata
Descriptions:
Validate the content of lock.d/metadata. A partial read, an unknown or
repeated field, a malformed value and a hash that does not match all
make the metadata invalid.
Arguments:
$text: the content of lock.d/metadata, or undef
Returns:
A hash ref of the identity fields, or undef when the metadata is invalid.
=cut
#--------------------------------------------------------------------------------
sub parse_metadata {
my ($text) = @_;
return undef unless defined($text) && $text =~ /\n\z/;
my %field;
for my $line (split(/\n/, $text)) {
my ($key, $value) = $line =~ /\A([a-z-]+)=([^=\s]+)\z/ or return undef;
return undef if exists($field{$key});
$field{$key} = $value;
}
my $hash = delete($field{hash});
return undef unless defined($hash) && keys(%field) == @IDENTITY;
for my $key (@IDENTITY) {
return undef unless defined($field{$key});
}
return undef unless $field{pid} =~ /\A[1-9][0-9]*\z/ && $field{pstart} =~ /\A[0-9]+\z/;
return undef unless $hash eq sha256_hex(_canonical(\%field));
return \%field;
}
#--------------------------------------------------------------------------------
=head3 owner_is_dead
Descriptions:
Step 5: decide from facts alone whether a recorded owner is dead. Only
the owner's machine can know. Any other machine answers "not proven".
Arguments:
$owner: a hash ref from parse_metadata, or undef
$here: hash ref with machine-id, boot-id and start_of, a code ref that
returns the start time of a pid on this machine, or undef when
no such process exists
Returns:
1 when the owner is provably dead, 0 otherwise.
=cut
#--------------------------------------------------------------------------------
sub owner_is_dead {
my ($owner, $here) = @_;
return 0 unless defined($owner);
return 0 unless $owner->{'machine-id'} eq $here->{'machine-id'};
return 1 unless $owner->{'boot-id'} eq $here->{'boot-id'};
my $start = $here->{start_of}->($owner->{pid});
return 1 unless defined($start);
return $start eq $owner->{pstart} ? 0 : 1;
}
#--------------------------------------------------------------------------------
=head3 process_start
Descriptions:
The start time of a process, field 22 of /proc/<pid>/stat, in clock ticks
since boot. A reused pid has a later start time.
Arguments:
$pid: the process id
Returns:
The start time, or undef when no such process exists.
=cut
#--------------------------------------------------------------------------------
sub process_start {
my ($pid) = @_;
open(my $fh, '<', "/proc/$pid/stat") or return undef;
my $stat = <$fh>;
close($fh);
return undef unless defined($stat);
# The command name, field 2, may contain spaces and parentheses.
$stat =~ s/\A.*\)\s+//s or return undef;
my @field = split(/\s+/, $stat);
return $field[19];
}
sub _log {
my ($self, $event, $detail) = @_;
return if $self->{quiet};
my $now = Time::HiRes::time();
my $time = strftime('%Y-%m-%dT%H:%M:%S', gmtime($now)) . sprintf('.%03dZ', ($now - int($now)) * 1000);
my $host = (split(/\./, hostname() || 'unknown'))[0];
my $line = join(' ', '[nfslock]', $time, $host, "pid=$$", $event, $self->{label}, $self->{path},
defined($detail) ? $detail : ());
print "$line\n";
}
sub _canonical {
my ($fields) = @_;
return join('', map { "$_=$fields->{$_}\n" } sort @IDENTITY);
}
sub _key {
my ($fields) = @_;
return join("\n", map { $fields->{$_} } @IDENTITY);
}
# A reader can see this file half written. The hash makes that read invalid.
sub _write_metadata {
my ($path, $identity) = @_;
my $file = "$path/metadata";
my $fh;
open($fh, '>', $file) && print({$fh} format_metadata($identity)) && close($fh)
or die "Cannot write $file: $!\n";
}
sub _read_metadata {
my ($path) = @_;
open(my $fh, '<', "$path/metadata") or return undef;
local $/;
my $text = <$fh>;
close($fh);
return parse_metadata($text);
}
sub _wait {
my ($delay, $jitter) = @_;
return $delay + (2 * rand() - 1) * $jitter;
}
sub _sleep {
my ($seconds) = @_;
Time::HiRes::sleep($seconds);
}
sub _token {
if (open(my $fh, '<:raw', '/dev/urandom')) {
my $read = read($fh, my $bytes, 16);
close($fh);
return unpack('H*', $bytes) if defined($read) && $read == 16;
}
return join('', map { sprintf('%08x', int(rand(2**32))) } 1 .. 4);
}
# Assumption 2 needs a real machine-id. A host without one cannot take part.
sub _here {
return {
'machine-id' => _first_line('/etc/machine-id')
// die("Cannot read /etc/machine-id: an NFS lock needs a machine id\n"),
'boot-id' => _first_line('/proc/sys/kernel/random/boot_id')
// die("Cannot read the boot id of this machine\n"),
start_of => \&process_start,
};
}
sub _first_line {
my ($file) = @_;
open(my $fh, '<', $file) or return undef;
my $line = <$fh>;
close($fh);
return undef unless defined($line);
chomp($line);
return length($line) ? $line : undef;
}
sub _describe {
my ($owner) = @_;
return 'an unknown owner' unless defined($owner);
return sprintf('pid %s on machine %s', $owner->{pid}, $owner->{'machine-id'});
}
# An absolute path in the message, without resolving the lock itself.
sub _absolute {
my ($path) = @_;
my $dir = abs_path(dirname($path)) // dirname($path);
return "$dir/" . basename($path);
}
1;
+89 -114
View File
@@ -15,7 +15,8 @@ use Parallel::ForkManager;
use POSIX qw(strftime);
use FindBin qw($RealBin);
use lib $RealBin, "$RealBin/lib";
use MockBuildUtils qw(sh_quote print_step version_matches required_pkgs rpm_in_cell
use XCAT::NFSLock ();
use MockBuildUtils qw(sh_quote print_step version_matches required_pkgs rpm_in_cell resolve_mock_cfg
carry_over_rpms rpm_name rpm_arch rpm_source_rpm rpm_digests_ok
install_deps_packages install_deps_command missing_perl_modules
read_manifest verify_repo_packages verify_repo_signature verify_rpm_signatures
@@ -130,10 +131,12 @@ my $gpg_sign = 0;
my $gpg_key_name = 'xCAT Signing Key';
my $gpg_home = '';
my $gpg_program = '';
my $force_unlock = 0;
# Seconds to wait for a lock that a live process holds. 0 fails at once.
my $try_unlock_timeout = 0;
# --finalize-xcat-dep: post-build cross-arch genesis provisioning (issue #7610). Takes the two
# per-arch repo roots and cross-populates the noarch xCAT-genesis-base between them.
my $finalize_xcat_dep = 0;
my @finalize_arch;
my $x86_64_repo = '';
my $ppc64le_repo = '';
# --verify-repo=<repo>: standalone, build-free completeness + signature gate over one already-built
@@ -159,8 +162,9 @@ GetOptions(
'gpg-sign!' => \$gpg_sign,
'gpg-key-name=s' => \$gpg_key_name,
'gpg-home=s' => \$gpg_home,
'force-unlock!' => \$force_unlock,
'try-unlock-timeout=i' => \$try_unlock_timeout,
'finalize-xcat-dep!' => \$finalize_xcat_dep,
'finalize-arch=s' => \@finalize_arch,
'x86_64-repo=s' => \$x86_64_repo,
'ppc64le-repo=s' => \$ppc64le_repo,
'verify-repo=s' => \$verify_repo,
@@ -189,6 +193,7 @@ GetOptions(
) or die usage();
die "Run as root (uid=$>)\n" if $> != 0 && !$finalize_xcat_dep && !$verify_repo;
die "--try-unlock-timeout must be >= 0\n" if $try_unlock_timeout < 0;
# --skip-build collects a prior build's artifacts from that build's per-target tree, so it must
# know the target. Without --target the default is "all three EL targets", and each would collect
# the same artifacts and cross-publish them into every repo (foreign-EL / foreign-arch rpms).
@@ -253,9 +258,19 @@ if ($finalize_xcat_dep) {
my $ppc = abs_path($ppc64le_repo) or die "--ppc64le-repo '$ppc64le_repo' not found\n";
die "--x86_64-repo '$x86' is not a directory\n" if !-d $x86;
die "--ppc64le-repo '$ppc' is not a directory\n" if !-d $ppc;
# finalize rewrites the per-arch cells a build deploys, so it takes the same cell locks. With
# --finalize-arch it writes, and locks, only the cells of those arches.
@finalize_arch = map { split /[\s,]+/ } @finalize_arch;
@finalize_arch = qw(x86_64 ppc64le) unless @finalize_arch;
my %cell;
for my $root ($x86, $ppc) {
$cell{ abs_path($_) } = 1 for grep { -d } map { glob("$root/*/$_") } @finalize_arch;
}
take_lock(cell_lock_path($_), 'repository cell lock') for sort keys %cell;
# Inject the per-rpm gpg re-sign and the repo re-index as callbacks so the finalize logic in
# MockBuildUtils stays free of this script's gpg/createrepo state.
finalize_xcat_dep($x86, $ppc,
only => \@finalize_arch,
sign => ($gpg_sign ? sub {
my ($rpm) = @_;
local $ENV{GNUPGHOME} = $gpg_home if $gpg_home;
@@ -272,7 +287,7 @@ if ($finalize_xcat_dep) {
unless ($no_verify_repo) {
my %seen;
for my $root ($x86, $ppc) {
my @cells = (glob("$root/rh*/x86_64"), glob("$root/rh*/ppc64le"));
my @cells = map { glob("$root/rh*/$_") } @finalize_arch;
for my $d (sort @cells) {
next unless -d $d;
my $abs = abs_path($d);
@@ -320,11 +335,10 @@ make_path($repo_dep) if !-d $repo_dep;
$repo_dep = abs_path($repo_dep)
or die "Cannot resolve --repo-dep directory\n";
# Fail-fast lock on the output base so a second run against the same --output aborts instead of
# racing on the shared NFS tree. Held for the whole invocation; released by the exit handlers.
acquire_output_lock($output_base, $force_unlock);
acquire_repository_lock($repo_dep, $force_unlock)
if $repo_dep ne $output_base;
# Locks are taken in one order: the output tree, then each repository cell in name order, then
# common. The exit handlers release them.
take_lock("$output_base/.lock", 'output lock');
MockBuildUtils::recover_common_repository($repo_dep) unless $dry_run;
$xcat_src = resolve_xcat_source($xcat_src, $repo_root);
@@ -412,12 +426,22 @@ my %forcearch_targets = (
arch => 'riscv64',
# x86_64 only, as the mock config admits: syslinux-xcat builds on x86 and ppc64le alone.
noarch_cfg => 'rocky-10-x86_64',
dep_builders => [qw(elilo-xcat grub2-xcat ipmitool-xcat syslinux-xcat goconserver conserver-xcat xnba-undi)],
required => [qw(ipmitool-xcat syslinux-xcat grub2-xcat xnba-undi
dep_builders => [qw(elilo-xcat grub2-xcat ipmitool-xcat syslinux-xcat goconserver conserver-xcat xnba-undi ipxe-xcat)],
required => [qw(ipmitool-xcat syslinux-xcat grub2-xcat xnba-undi ipxe-xcat
perl-IO-Stty perl-HTTP-Async perl-Net-HTTPS-NB)],
},
);
# Each target deploys one cell, <repo-dep>/rh<rel>/<arch>, and locks only that cell: the per-arch
# runs of one build share --repo-dep and never wait on each other.
my @cell_locks = map {
my $cell = target_cell($_);
make_path(dirname($cell));
cell_lock_path($cell);
} @build_targets;
my %cell_lock_seen;
take_lock($_, 'repository cell lock') for grep { !$cell_lock_seen{$_}++ } sort @cell_locks;
# NOTE: no dhcp- packages are built here. DHCP backend selection is an install-time
# rich dep in xCAT.spec (kea if system-release>=10 else /usr/sbin/dhcpd), so there is
# nothing arch/EL-specific to build or to exclude for el10.
@@ -426,8 +450,7 @@ print_step('Targets to build');
print " $_\n" for @build_targets;
print "output_base: $output_base\n";
print "deploy repo-dep: $repo_dep\n";
print "output lock: $output_base/.lock (held)\n";
print "repository lock: $repo_dep/.lock (held)\n";
print "locks held: $_\n" for map { $_->path } @HELD_LOCKS;
print "gpg_sign: $gpg_sign\n";
print "gpg_key_name: $gpg_key_name\n" if $gpg_sign;
print "gpg_home: " . ($gpg_home ne '' ? $gpg_home : '(default keyring)') . "\n" if $gpg_sign;
@@ -536,10 +559,11 @@ if (!$skip_build && !$dry_run && -d $run_root) {
remove_tree($run_root);
}
# All dep builders run natively on every arch. xnba-undi and grub2-xcat are noarch packagings of
# committed artifacts (an x86 UNDI ROM / the grub2 resource tarball) with no arch-specific build
# step, so ppc builds them the same as x86 -- no cross-arch import. A forcearch target builds
# only the builders its profile lists; the noarch ones run in the profile's native chroot.
# All dep builders run natively on every arch. xnba-undi, grub2-xcat and ipxe-xcat are noarch
# packagings of committed artifacts (an x86 UNDI ROM / the grub2 resource tarball / the iPXE release
# tree) with no arch-specific build step, so ppc builds them the same as x86 -- no cross-arch
# import. A forcearch target builds only the builders its profile lists; the noarch ones run in
# the profile's native chroot.
# syslinux-xcat is noarch too, and its spec builds on x86 and ppc64le only.
my @dep_builders = (
{ name => 'elilo-xcat', script => "$repo_root/elilo/mockbuild.pl", noarch => 1 },
@@ -549,6 +573,7 @@ my @dep_builders = (
{ name => 'goconserver', script => "$repo_root/goconserver/mockbuild.pl" },
{ name => 'conserver-xcat', script => "$repo_root/conserver/mockbuild.pl" },
{ name => 'xnba-undi', script => "$repo_root/xnba/mockbuild.pl", noarch => 1 },
{ name => 'ipxe-xcat', script => "$repo_root/ipxe-xcat/mockbuild.pl", noarch => 1 },
);
my %profile_builds = map { $_ => 1 } @{ $profile->{dep_builders} };
@@ -887,7 +912,7 @@ if (!$skip_genesis && !$dry_run) {
# A skipped builder built nothing this run, so everything it published in the cell joins the run
# repository here, ahead of the bump check, createrepo, the tarballs and the deploy gate.
if (!$dry_run && ($skip_genesis || $skip_perl || $skip_xcat_dep)) {
my $published = "$repo_dep/rh$rel/$arch";
my $published = target_cell($target);
if (-d $published) {
my %skipped = (genesis => $skip_genesis, perl => $skip_perl, dep => $skip_xcat_dep);
# Only an rpm the configured key signed, by signer id and by rpmkeys --checksig, may be
@@ -1040,11 +1065,11 @@ sub target_profile {
noarch_cfg => $target,
forcearch => 0,
epel => 1,
dep_builders => [qw(elilo-xcat grub2-xcat ipmitool-xcat syslinux-xcat goconserver conserver-xcat xnba-undi)],
dep_builders => [qw(elilo-xcat grub2-xcat ipmitool-xcat syslinux-xcat goconserver conserver-xcat xnba-undi ipxe-xcat)],
# xCAT Requires all of these on every arch, and every one of them builds natively on
# every arch (the noarch deps -- grub2-xcat, xnba-undi -- just repackage committed
# every arch (the noarch deps -- grub2-xcat, xnba-undi, ipxe-xcat -- just repackage committed
# artifacts), so a self-sufficient per-arch build produces the whole set.
required => [qw(ipmitool-xcat syslinux-xcat grub2-xcat xnba-undi
required => [qw(ipmitool-xcat syslinux-xcat grub2-xcat xnba-undi ipxe-xcat
perl-IO-Stty perl-HTTP-Async perl-Net-HTTPS-NB)],
};
}
@@ -1077,7 +1102,7 @@ sub deploy_target {
my $rel = $info->{rel};
my $src = $info->{repo_dir};
my $tarch = $info->{profile}{arch};
my $dest = "$repo_dep/rh$rel/$tarch";
my $dest = target_cell($tgt);
print_step("Deploy $tgt -> $dest");
return if $dry_run;
@@ -1148,6 +1173,8 @@ sub publish_genesis_common_repo {
return;
}
# Every architecture run can publish common, so common has its own lock.
take_lock("$repo_dep/.common-publish.lock", 'common lock');
$COMMON_STAGE = tempdir('.common.XXXXXXXX', DIR => $repo_dep, CLEANUP => 0);
my $published = publish_genesis_release_packages('rpm', $COMMON_STAGE);
verify_genesis_release_packages('rpm', $COMMON_STAGE);
@@ -1259,12 +1286,20 @@ sub publish_file {
die $error;
}
# createrepo_c command with upstream-matching, deterministic metadata. The tool's
# defaults emit primary/filelists/other as *.xml.zst plus *.sqlite.bz2 (--database),
# exactly the upstream shape; --set-timestamp-to-revision pins repomd to SOURCE_DATE_EPOCH.
# createrepo_c command with deterministic metadata: primary/filelists/other as *.xml.zst, with
# --set-timestamp-to-revision pinning repomd to SOURCE_DATE_EPOCH.
#
# NO --database. It writes *.sqlite.bz2, and SQLite needs POSIX locks. The build tree lives on the
# shared shared tree, an NFS mount the hypervisor re-exports, and the kernel refuses locks there:
# every attempt answers errno 524. createrepo_c died with "Cannot open .repodata/primary.sqlite:
# Can not create db_info table: disk I/O error" on every target once the build hosts moved off
# virtiofs -- xcat-dep-el-cd #161 and #162 both failed that way and staged nothing.
#
# --database is deprecated in createrepo_c 1.1.2 and --no-database is its default. dnf on el8+ and
# zypper read the XML.
sub createrepo_c_cmd {
my ($dir) = @_;
return 'createrepo_c --update --database '
return 'createrepo_c --update '
. '--revision ' . shell_quote($SOURCE_DATE_EPOCH) . ' --set-timestamp-to-revision '
. shell_quote($dir);
}
@@ -1417,7 +1452,11 @@ Options:
--output-root PATH Override the derived build tree root (default: <output>/mockbuild-all)
--repo-dep PATH Override the deployable output root; rh8/rh9/rh10/<arch> and common
are assembled and signed here (default: <output>/xcat-dep)
--force-unlock Remove a stale <output>/.lock before acquiring it
--try-unlock-timeout N Wait about N seconds (default 0: one retry of 3s) for a lock
that a live process holds, then fail with the command that
removes it. A lock whose owner is proven dead on this host
is taken over at once. Locks: <output>/.lock, one <repo-dep>/rh<N>/.<arch>.lock
per target, and <repo-dep>/.common-publish.lock for common
--finalize-xcat-dep Post-build cross-arch genesis mode (builds nothing). Requires
--x86_64-repo and --ppc64le-repo. For each matching <os>/x86_64 and
<os>/ppc64le repo pair, copies the noarch xCAT-genesis-base-ppc64
@@ -1426,6 +1465,9 @@ Options:
ppc64le repo (dropping any stale foreign-arch genesis), then
re-indexes + re-signs. Restores the 2.17 cross-arch genesis
(issue #7610). Honors --gpg-sign/--gpg-key-name/--gpg-home. Use alone.
--finalize-arch ARCH (finalize) Write, lock and verify only the cells of ARCH (x86_64 or
ppc64le; repeatable). Run it on the host that builds ARCH, so a
dead finalize leaves locks that host can reclaim. Default: both
--x86_64-repo PATH (finalize) x86_64 repo root holding <os>/x86_64 (e.g. rh9/x86_64)
--ppc64le-repo PATH (finalize) ppc64le repo root holding <os>/ppc64le
--verify-repo PATH Standalone completeness + signature gate over the per-target repo at PATH
@@ -2129,28 +2171,6 @@ sub collect_srpms {
return ($copied, $skipped_non_src, $missing_roots);
}
sub resolve_mock_cfg {
my ($os_id, $rel, $arch) = @_;
my %short_forms = (
almalinux => 'alma',
'centos-stream' => 'centos-stream',
rocky => 'rocky',
);
# Resolve by CONFIG-FILE existence, not by running `mock --print-root-path`: the latter can fail
# transiently (bootstrap chroot setup, a concurrent mock holding a lock) and made el10 flakily
# "resolve" to the long form that has no .cfg. Checking /etc/mock/<cfg>.cfg is deterministic.
for my $id ($os_id, (exists $short_forms{$os_id} ? ($short_forms{$os_id}) : ())) {
my $candidate = "${id}+epel-${rel}-${arch}";
if (-f "/etc/mock/${candidate}.cfg") {
print "Mock config resolved: $candidate\n" if $id ne $os_id;
return $candidate;
}
}
my $short = $short_forms{$os_id} // $os_id;
die "Could not find mock config for ${os_id}+epel-${rel}-${arch} "
. "(tried /etc/mock/${os_id}+epel-${rel}-${arch}.cfg and /etc/mock/${short}+epel-${rel}-${arch}.cfg)\n";
}
sub resolve_xcat_source {
my ($requested, $root) = @_;
# Prefer the sibling ../xcat-core (the real layout: source/xcat-core beside source/xcat-dep)
@@ -2169,72 +2189,29 @@ sub resolve_xcat_source {
return eval { abs_path($requested) } || $requested;
}
# Fail-fast advisory lock on the output base. Uses an atomic mkdir (portable and reliable over
# NFS, unlike flock) of "<base>/.lock". A second run against the same --output dies immediately
# rather than racing on the shared tree. Only the process that created the lock removes it.
sub acquire_named_lock {
my ($base, $label, $force) = @_;
my $lock = "$base/.lock";
if ($force && -d $lock) {
print "force-unlock: removing stale lock $lock\n";
_rmdir_lock($lock);
}
if (mkdir $lock) {
push(@HELD_LOCKS, $lock);
$LOCK_OWNER_PID //= $$;
my $host = capture_command('uname', '-n') || 'unknown';
if (open my $fh, '>', "$lock/owner") {
print {$fh} "host=$host\npid=$$\nepoch=" . time() . "\n";
close $fh;
}
return;
}
# mkdir failed: either it already exists (locked) or a real error.
if (-d $lock) {
my $info = '';
if (open my $fh, '<', "$lock/owner") { local $/; $info = <$fh>; close $fh; }
$info =~ s/\s+/ /g;
die "$label $base is locked ($lock): $info\n"
. "another mockbuild-all run owns it; use a different destination or --force-unlock if stale.\n";
}
die "Cannot create lock $lock: $!\n";
# A lock the run holds until it exits. The owner pid lets the exit handlers tell the run from the
# build workers it forks, which inherit the lock list.
sub take_lock {
my ($path, $label) = @_;
my $lock = XCAT::NFSLock->acquire($path, timeout => $try_unlock_timeout, label => $label);
push(@HELD_LOCKS, $lock);
$LOCK_OWNER_PID //= $$;
return $lock;
}
sub acquire_output_lock {
my ($base, $force) = @_;
acquire_named_lock($base, 'output', $force);
# The repository cell a target deploys. The deploy, the carry-over and the cell lock all take the
# path from here, so the lock covers the directory the deploy writes.
sub target_cell {
my ($target) = @_;
my $profile = target_profile($target);
return "$repo_dep/rh$profile->{rel}/$profile->{arch}";
}
sub acquire_repository_lock {
my ($base, $force) = @_;
_recover_common_repository($base) if $force;
acquire_named_lock($base, 'repository', $force);
}
sub _recover_common_repository {
my ($base) = @_;
my $destination = "$base/common";
my @backups = sort {
((stat($a))[9] // 0) <=> ((stat($b))[9] // 0)
} grep { -d $_ && !-l $_ } bsd_glob("$base/.common.previous.*");
if (!-e $destination && !-l $destination && @backups) {
my $backup = pop(@backups);
rename($backup, $destination)
or die "Cannot restore interrupted common repository $backup: $!\n";
}
remove_tree($_) for grep { -d $_ && !-l $_ } @backups;
for my $staging (bsd_glob("$base/.common.*")) {
next if $staging =~ m{/\.common\.previous\.};
remove_tree($staging) if -d $staging && !-l $staging;
}
}
sub _rmdir_lock {
my ($lock) = @_;
unlink "$lock/owner";
rmdir $lock;
# The lock of a repository cell <dir>/<arch> sits beside it, as <dir>/.<arch>.lock: the deploy
# replaces the cell directory itself.
sub cell_lock_path {
my ($cell) = @_;
return dirname($cell) . '/.' . basename($cell) . '.lock';
}
# Release locks on every exit path, but only from the process that acquired them.
@@ -2254,9 +2231,7 @@ sub _restore_common_repository {
sub _release_locks_if_owner {
return unless defined($LOCK_OWNER_PID) && $$ == $LOCK_OWNER_PID;
for my $lock (reverse(@HELD_LOCKS)) {
_rmdir_lock($lock) if -d $lock;
}
$_->release for reverse(@HELD_LOCKS);
}
END {
_restore_common_repository();
+8 -1
View File
@@ -61,6 +61,7 @@ elilo-xcat=3.14
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
ipxe-xcat=2.0.0
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-HTML-Form=6.07
@@ -75,6 +76,7 @@ elilo-xcat=3.14
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
ipxe-xcat=2.0.0
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-HTML-Form=6.07
@@ -89,6 +91,7 @@ elilo-xcat=3.14
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
ipxe-xcat=2.0.0
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-HTTP-Async=>= 0.30-3
@@ -103,6 +106,7 @@ elilo-xcat=3.14
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
ipxe-xcat=2.0.0
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-HTTP-Async=>= 0.30-3
@@ -117,6 +121,7 @@ elilo-xcat=3.14
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
ipxe-xcat=2.0.0
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-Crypt-SSLeay=0.72
@@ -133,6 +138,7 @@ elilo-xcat=3.14
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
ipxe-xcat=2.0.0
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-Crypt-SSLeay=0.72
@@ -147,7 +153,7 @@ xCAT-genesis-base=>= 2:2.18.0
# EPEL-fed EL10 sections above in one way (see BUILD.md, "riscv64"): riscv64 has no EPEL, so the
# perl deps EL10 otherwise takes from EPEL are built here too (perl-Crypt-Blowfish ...
# perl-Path-Class below). perl-Path-Class is a build dep of perl-Crypt-SSLeay only. The x86 boot
# loaders (elilo-xcat, syslinux-xcat, xnba-undi) are noarch and are listed like on ppc64le: a
# loaders (elilo-xcat, ipxe-xcat, syslinux-xcat, xnba-undi) are noarch and are listed like on ppc64le: a
# riscv64 management node serves the x86 nodes of a mixed cluster.
# The per-EL perl set is the EL10 one plus perl-HTML-Form: EPEL supplies it on the other
# architectures, nothing supplies it on riscv64, and perl-xCAT requires perl(HTML::Form).
@@ -158,6 +164,7 @@ elilo-xcat=3.14
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
ipxe-xcat=2.0.0
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-Crypt-SSLeay=0.72
+70 -140
View File
@@ -5,7 +5,7 @@
# * mk-dep-chroots.sh -> the "ensure chroots" phase (auto-initializes per-codename sbuild chroots
# on first run; idempotent).
# * build-dep-debs.sh -> the per-package build phase (drives each <dep>/sbuild.pl in the matching
# chroot) + the metadata-preserving genesis phase.
# chroot) + the genesis-ingest phase.
# * build-apt-repo.sh -> the apt-repo assembly + signing phase (in Perl, focal supported).
#
# Design (mirrors mockbuild-all.pl + fixes the PR #63 review):
@@ -17,14 +17,14 @@
# two arches build concurrently on their two hosts, so a per-arch build that also published would
# interleave wipes of the same pool/dists/Release; and a partial or failed build must never reach
# the published repo, nor stale debs accumulate in it (concern #1).
# 3. Per-arch package sets come from the manifest: the x86 boot components (syslinux/elilo/xnba,
# 3. Per-arch package sets come from the manifest: the x86 boot components (syslinux/elilo/xnba/ipxe-xcat,
# Architecture:all) are built once on amd64 (single producer); ppc64el builds only the genuinely
# arch-specific compiled deps (concern #3).
# 4. Any required chroot / package / artifact failure, or any version-pin mismatch, fails the whole
# run non-zero (concern #4).
# 5. The genesis-base deb keeps the maintained Debian packaging semantics -- a native deb is ingested
# as-is when provided; a converted rpm keeps the maintained control (Depends/Breaks/Replaces) and
# maintainer scripts (concern #2).
# 5. The genesis-base debs are built by xcat-core, one per Ubuntu codename, and are ingested here
# as they are. Each is staged into the suite it was built for: the image carries the kernel of
# the root that built it, so one image cannot serve several releases.
use strict;
use warnings;
use Cwd qw(abs_path);
@@ -36,9 +36,9 @@ use File::Temp qw(tempdir);
use Getopt::Long qw(GetOptions);
use Pod::Usage qw(pod2usage);
use POSIX qw(strftime);
use Fcntl qw(:flock);
use FindBin qw($RealBin);
use lib $RealBin, "$RealBin/lib";
use XCAT::NFSLock ();
# NOTE: XCAT::GenesisRelease (the shared reader/validator of an OpenEmbedded Genesis package release,
# the same module mockbuild-all.pl uses for the rpm side) is loaded ON DEMAND in the --genesis-release
# block, NOT with `use` here. It pulls in XCAT::BuildUtils, which needs File::Slurper, and the Ubuntu
@@ -53,12 +53,11 @@ use BuildUtils qw(sh_quote print_step version_matches required_pkgs read_manifes
codename_to_version known_codenames supported_arches is_supported_arch
chroot_name chroot_sources_list
chroot_is_disposable
control_field genesis_deb_control
control_field
deb_field deb_version deb_hash cross_copy_genesis_deb);
my $script_dir = abs_path(dirname(__FILE__));
my $repo_root = $script_dir;
my $xcat_src = "$repo_root/../xcat-core";
my $output_root = '';
my $apt_dir = '';
my $manifest = '';
@@ -108,17 +107,13 @@ my $genesis_release_checksums; # its verified SHA256SUMS, read once at sta
# They are Architecture:all and identical for all suites, so a per-suite copy would multiply hundreds
# of megabytes by the number of codenames for no gain.
my $GENESIS_POOL_RELATIVE = 'pool/main/xcat-genesis-openembedded';
my @genesis_debs; # native xcat-genesis-base-<arch> deb(s): path or URL (preferred)
my $genesis_rpm = ''; # fallback: native-arch genesis rpm to convert
my $genesis_rpm_ppc = ''; # fallback: cross-arch ppc genesis rpm to convert (amd64 host)
my $require_ppc_genesis = 0;
# File-scoped exclusive run-lock handle. MUST be file-scoped (not a lexical inside a block) so the
# flock lives for the WHOLE process -- a lexical would close the FH and release the lock early.
my @genesis_debs; # native xcat-genesis-base-<arch> deb(s): path or URL
# Seconds to wait for a concurrent publisher before giving up (--publish-lock-wait). Long by default:
# the other holder is a real publish (assemble + gate + swap), and waiting it out is almost always
# better than failing the run.
my $PUBLISH_LOCK_WAIT = 1800;
my $RUN_LOCK_FH;
# The per-arch run lock, held until the END block releases it.
my @RUN_LOCKS;
# Builder map: manifest binary-package name -> the in-tree package dir that carries <dir>/sbuild.pl
# and the maintained debian/. (goconserver's dir == its binary name.)
@@ -130,13 +125,13 @@ my %PKG_DIR = (
'grub2-xcat' => 'grub2-xcat',
'elilo-xcat' => 'elilo',
'xnba-undi' => 'xnba',
'ipxe-xcat' => 'ipxe-xcat',
);
# Build the GetOptions map from the shared standard_options() spec (so the flag vocabulary matches
# mockbuild-all.pl), plus the apt/sbuild-specific options this orchestrator adds.
my %DEST = (
'repo-root' => \$repo_root,
'xcat-source' => \$xcat_src,
'output' => \$output_root, # alias of --output-root
'output-root' => \$output_root,
'manifest' => \$manifest,
@@ -161,9 +156,6 @@ my %DEST = (
'gpg-key-id' => \$gpg_key_id,
'genesis-release' => \$genesis_release,
'genesis-deb' => \@genesis_debs,
'genesis-rpm' => \$genesis_rpm,
'genesis-rpm-ppc' => \$genesis_rpm_ppc,
'require-ppc-genesis' => \$require_ppc_genesis,
);
my %spec; # option-spec-string => destination ref
for my $s (standard_options()) {
@@ -180,9 +172,6 @@ $spec{'gpg-key-id=s'} = \$gpg_key_id;
$spec{'parallel-targets=i'} = \$parallel_targets;
$spec{'build-timeout=i'} = \$build_timeout; # per-package wall-clock bound (0 = unbounded)
$spec{'genesis-deb=s'} = \@genesis_debs;
$spec{'genesis-rpm=s'} = \$genesis_rpm;
$spec{'genesis-rpm-ppc=s'} = \$genesis_rpm_ppc;
$spec{'require-ppc-genesis!'} = \$require_ppc_genesis;
$spec{'install-deps!'} = \$install_deps; # make this host able to run at all, then exit
$spec{'publish!'} = \$publish; # run the finalization (assemble+sign+gate+tarball)
$spec{'publish-lock-wait=i'} = \$PUBLISH_LOCK_WAIT; # seconds to queue behind another publisher
@@ -195,6 +184,9 @@ $spec{'help|h'} = sub { pod2usage(-verbose => 1, -exitval => 0);
$spec{'man'} = sub { pod2usage(-verbose => 2, -exitval => 0); };
GetOptions(%spec) or pod2usage(-verbose => 1, -exitval => 2);
# A signal exits through END, so the publish lock is released. The build phase installs its own
# forwarding handlers for the duration of the build.
$SIG{$_} = sub { exit 1 } for qw(INT TERM HUP);
# --install-deps: make THIS host able to run the script, then exit. It comes first because
# everything below assumes the toolchain is present, and a host that lacks it would fail with a
@@ -221,7 +213,6 @@ if ($install_deps) {
# Configuration
# ---------------------------------------------------------------------------------------------------
$repo_root = abs_path($repo_root);
$xcat_src = abs_path($xcat_src) if -d $xcat_src;
$manifest ||= "$repo_root/debs-manifest.conf";
$arch ||= `dpkg --print-architecture 2>/dev/null`; chomp $arch;
$arch ||= 'amd64';
@@ -348,18 +339,20 @@ for my $cn (@dist_list) {
my $staging = "$output_root/staging";
unless ($dry_run) { make_path($staging); }
# Fail-fast PER-ARCH run lock. Within ONE pipeline run the amd64 and ppc64el stages run CONCURRENTLY
# on their own hosts against the SAME --output-root (different arch subdirs), so a single shared lock
# would wrongly serialize them (or deadlock). Lock per-arch instead: <output_root>/.sbuild-all.<arch>.lock
# is only ever contended by same-arch stages, which all run on the SAME host -- so a plain local flock
# is authoritative (no cross-host NFS lockd needed). This still blocks a SECOND run's same-arch stage
# (cron vs manual) from racing on this arch's staging + the shared apt tree. Not taken under --dry-run.
# PER-ARCH run locks. The amd64 and ppc64el stages of one run build concurrently on their own hosts
# against one --output-root, so the lock is per arch: a second run of the same arch stops. A publish
# reads the staging of every expected arch, so it also waits for the run lock of each one. The locks
# are taken in name order. flock on the shared tree fails with ENOTSUPP through the NFS re-export, so
# these are XCAT::NFSLock. Not taken under --dry-run.
unless ($dry_run) {
make_path($output_root);
my $lockfile = "$output_root/.sbuild-all.$arch.lock";
open($RUN_LOCK_FH, '>', $lockfile) or die "FATAL: cannot open run lock $lockfile: $!\n";
unless (flock($RUN_LOCK_FH, LOCK_EX | LOCK_NB)) {
die "FATAL: another sbuild-all ($arch) is already running (lock held): $lockfile\n";
my %lock_arch = ($arch => 1);
if ($publish) { $lock_arch{$_} = 1 for @{ resolve_expect_arches('publish', $apt_dir) }; }
for my $a (sort keys %lock_arch) {
# A build of this arch fails fast. A publish queues behind the builders it reads from.
my $wait = ($publish && !($a eq $arch && !$skip_build)) ? $PUBLISH_LOCK_WAIT : 0;
push(@RUN_LOCKS, XCAT::NFSLock->acquire("$output_root/.sbuild-all.$a.nfslock",
timeout => $wait, label => "sbuild-all ($a) run lock"));
}
}
@@ -369,7 +362,6 @@ $ENV{XCAT_DEP_BUILD_TIMEOUT} = $build_timeout if defined $build_timeout;
print_step('Configuration');
print " repo-root: $repo_root\n";
print " xcat-source: $xcat_src\n";
print " arch: $arch\n";
print " dists: @dist_list\n";
print " manifest: $manifest\n";
@@ -569,7 +561,7 @@ sub build_one_codename {
for my $pkg (@pkgs) {
my $dir = $PKG_DIR{$pkg}
or die "FATAL: no builder dir mapped for manifest package '$pkg'\n";
# arch:all single-producer packages (grub2-xcat/syslinux-xcat/elilo-xcat/xnba-undi) are built
# arch:all single-producer packages (grub2-xcat/syslinux-xcat/elilo-xcat/xnba-undi/ipxe-xcat) are built
# ONCE on amd64 -- their source is x86-only (syslinux compiles with nasm/gcc-multilib) -- and,
# being Architecture:all, are assembled into every arch's Packages index. They stay REQUIRED in
# the ppc64el manifest so the gate verifies the ppc repo actually carries them, but are NOT
@@ -677,49 +669,13 @@ sub build_deps {
}
# ---------------------------------------------------------------------------------------------------
# Phase: genesis-base deb (concern #2: preserve maintained packaging; native ingest preferred)
# Phase: genesis-base deb (built natively by xcat-core, one image per Ubuntu codename)
#
# The Genesis image carries the kernel of the root that built it. xcat-core's builddebs.pl --genesis
# builds one deb per codename inside that codename's chroot; this phase only ingests them and stages
# each one into the suite it belongs to. The rpm->deb conversion that came before it gave every
# Ubuntu release the EL kernel, so it is gone.
# ---------------------------------------------------------------------------------------------------
# maintained_genesis_control($arch): the maintained xCAT-genesis-builder/debian/control text, with the
# arch-specific package/relationship names remapped to $arch (the tree carries the amd64 control).
sub maintained_genesis_control {
my ($a) = @_;
my $f = "$xcat_src/xCAT-genesis-builder/debian/control";
return undef unless -f $f;
local $/; open my $fh, '<', $f or return undef; my $t = <$fh>; close $fh;
# The tree carries the amd64 control; any other arch is the same text with the arch renamed.
$t =~ s/amd64/$a/g if $a ne 'amd64';
return $t;
}
# convert_genesis_rpm($rpm, $pkgname, $arch, $outdir): rpm2cpio-extract the noarch genesis rpm and
# repackage as a .deb whose DEBIAN/control PRESERVES the maintained Depends/Breaks/Replaces and whose
# maintainer scripts (postinst/prerm/preinst/postrm) are copied from the maintained debian/ -- so the
# converted deb keeps the install/upgrade semantics the bare 5-field shim dropped (concern #2).
sub convert_genesis_rpm {
my ($rpm, $pkgname, $a, $outdir) = @_;
my $work = tempdir(CLEANUP => 1);
my $get = ($rpm =~ m{^https?://})
? "curl -fsSL " . sh_quote($rpm) . " | rpm2cpio"
: "rpm2cpio " . sh_quote($rpm);
run("cd $work && $get | cpio -idm --quiet");
my $ver = `rpm -qp --qf '%{VERSION}-%{RELEASE}' ${\ sh_quote($rpm)} 2>/dev/null`; chomp $ver;
$ver ||= "2.18.0-snap$snap_ts";
$ver =~ s/\.(el|fc)\d+.*$//; # drop the EL dist tag from the rpm Release
my $pkgd = "$work/pkg"; make_path("$pkgd/DEBIAN", "$pkgd/opt/xcat");
run("cp -a $work/opt/xcat/. $pkgd/opt/xcat/ 2>/dev/null || true", nofail => 1);
my $control = genesis_deb_control(maintained_genesis_control($a), $pkgname, $ver, 'all');
if (!$dry_run) {
open my $fh, '>', "$pkgd/DEBIAN/control" or die "write control: $!\n"; print $fh $control; close $fh;
# preserve maintainer scripts from the maintained packaging (install/upgrade behavior)
my $mdeb = "$xcat_src/xCAT-genesis-builder/debian";
for my $s (qw(postinst preinst postrm prerm)) {
next unless -f "$mdeb/$s";
copy("$mdeb/$s", "$pkgd/DEBIAN/$s"); chmod 0755, "$pkgd/DEBIAN/$s";
}
}
make_path($outdir);
run("dpkg-deb --build " . sh_quote($pkgd) . " " . sh_quote("$outdir/${pkgname}_${ver}_all.deb"));
return "$outdir/${pkgname}_${ver}_all.deb";
}
# genesis_in_manifest(): whether the legacy Genesis deb belongs to this run at all. It is named
# per target in the manifest, and riscv64 does not name it: its Genesis is the OpenEmbedded package
# published once into the shared pool. Without this, a plain --arch riscv64 run reaches
@@ -747,35 +703,12 @@ sub build_genesis {
print " ingested native genesis deb: $base\n";
$produced_native = 1 if $base =~ /^\Q$native_arch_pkg\E_/;
}
# 2) else convert the native-arch rpm (metadata-preserving)
if (!$produced_native) {
if ($genesis_rpm) {
print " converting native-arch genesis rpm -> deb (preserving control + scripts)\n";
convert_genesis_rpm($genesis_rpm, $native_arch_pkg, $arch, $gen);
} elsif (!@genesis_debs) {
die "FATAL: no native genesis for $arch: pass --genesis-deb (preferred) or --genesis-rpm\n";
}
}
# 3) cross-arch ppc genesis on the amd64 host (#7610): convert the ppc rpm if given
if ($arch eq 'amd64') {
my $have_ppc = grep { basename($_) =~ /^xcat-genesis-base-ppc64el_/ } glob("$gen/*.deb");
if (!$have_ppc && $genesis_rpm_ppc) {
print " converting cross-arch ppc64el genesis rpm -> deb (#7610)\n";
convert_genesis_rpm($genesis_rpm_ppc, 'xcat-genesis-base-ppc64el', 'ppc64el', $gen);
$have_ppc = 1;
}
if (!$have_ppc) {
my $msg = "no ppc64el genesis (pass --genesis-deb/--genesis-rpm-ppc): an amd64 MN cannot "
. "netboot ppc nodes (#7610)";
die "FATAL: $msg\n" if $require_ppc_genesis;
warn "WARN: $msg\n";
}
}
# stage the arch:all genesis deb(s) into every codename (this host's arch subdir; the cross-arch
# ppc genesis produced on the amd64 host rides in the amd64 subdir and is picked up by assemble).
# Use BuildUtils::cross_copy_genesis_deb -- the tested, hash-based, stale-dropping copier -- once
# per genesis package-arch present in $gen (the native-arch one, plus the cross-converted ppc64el
# one on the amd64 host). It refreshes a stale same-name deb by content and is idempotent.
die "FATAL: no Genesis deb for $arch: pass --genesis-deb.\n"
. " xcat-core builds them with `builddebs.pl --genesis-only --genesis-dist <codename>`.\n"
unless $produced_native || $dry_run;
# Stage each suite's OWN image. cross_copy_genesis_deb is the tested, hash-based, stale-dropping
# copier; the codename narrows it to the deb built on that release. A deb with no codename in its
# version serves every suite, which is how a package published before the native build is reused.
my %gen_arches;
for my $d (glob("$gen/*.deb")) {
$gen_arches{$1}++ if basename($d) =~ /^xcat-genesis-base-([a-z0-9]+)_/;
@@ -788,7 +721,10 @@ sub build_genesis {
}
make_path($dst);
for my $ga (sort keys %gen_arches) {
my $n = cross_copy_genesis_deb($gen, $dst, $ga, undef);
my $n = cross_copy_genesis_deb($gen, $dst, $ga, undef, $cn);
die "FATAL: no xcat-genesis-base-$ga image built for $cn: xcat-core builds one per\n"
. " codename, so --genesis-dist must name every release this run publishes.\n"
unless $n || glob("$dst/xcat-genesis-base-$ga\_*.deb");
print " staged xcat-genesis-base-$ga -> $cn/$arch ($n newly copied)\n";
}
}
@@ -1079,29 +1015,26 @@ sub verify_assembled_repo {
# complete repo or the new complete repo -- never a half-wiped pool or an index that does not
# match its Release. A failed gate leaves the published tree untouched.
#
# The lock file lives on the shared tree; within a host flock() is authoritative, which is what
# matters, since the pipeline's finalization step always runs on one host (the amd64 Ubuntu builder).
# Publishers of one tree can run on different hosts, and flock through an NFS re-export does not
# exclude them, so the publish lock is an XCAT::NFSLock. The END block releases it.
# ---------------------------------------------------------------------------------------------------
my $PUBLISH_LOCK_FH;
my $PUBLISH_LOCK;
sub acquire_publish_lock {
make_path($output_root);
my $lockfile = "$output_root/.sbuild-all.publish.lock";
open($PUBLISH_LOCK_FH, '>', $lockfile) or die "FATAL: cannot open publish lock $lockfile: $!\n";
unless (flock($PUBLISH_LOCK_FH, LOCK_EX | LOCK_NB)) {
print " publish lock is held by another run -- waiting up to ${PUBLISH_LOCK_WAIT}s: $lockfile\n";
local $SIG{ALRM} = sub {
die "FATAL: timed out after ${PUBLISH_LOCK_WAIT}s waiting for the publish lock $lockfile\n";
};
alarm($PUBLISH_LOCK_WAIT);
my $ok = flock($PUBLISH_LOCK_FH, LOCK_EX);
alarm(0);
die "FATAL: cannot take the publish lock $lockfile: $!\n" unless $ok;
}
my $lockfile = "$output_root/.sbuild-all.publish.nfslock";
print " taking the publish lock, waiting up to ${PUBLISH_LOCK_WAIT}s: $lockfile\n";
$PUBLISH_LOCK = XCAT::NFSLock->acquire($lockfile,
timeout => $PUBLISH_LOCK_WAIT, label => 'publish lock');
print " publish lock acquired: $lockfile\n";
return $lockfile;
}
END {
$PUBLISH_LOCK->release if $PUBLISH_LOCK;
$_->release for reverse(@RUN_LOCKS);
}
# assemble_into($dir, $expect_arches): (re)assemble every --dists codename inside $dir from the
# validated staging tree, index it per expected binary-<arch>, write + sign Release. $dir is the SIDE
# tree, never the published one.
@@ -1307,7 +1240,7 @@ sub publish_repo {
print_step('Publish apt repo (locked, assembled aside, swapped in atomically)');
my $expect = resolve_expect_arches('publish', $apt_dir);
if ($dry_run) {
print " [dry-run] would lock $output_root/.sbuild-all.publish.lock, assemble @dist_list for "
print " [dry-run] would lock $output_root/.sbuild-all.publish.nfslock, assemble @dist_list for "
. join(' ', @$expect) . " into $apt_dir.publish-<run-id>.<pid>, gate it, then rename it "
. "onto $apt_dir\n";
return;
@@ -1391,7 +1324,10 @@ sbuild-all.pl - build, validate, sign and assemble the xcat-dep Ubuntu/Debian ap
# STEP 1 -- per arch, on that arch's build host: build + validate into staging (does NOT publish):
sbuild-all.pl --arch amd64 --dists "focal jammy noble resolute" \
--xcat-source ../xcat-core --genesis-rpm <xCAT-genesis-base rpm>
--genesis-deb <xcat-genesis-base-amd64_..~focal_all.deb> \
--genesis-deb <xcat-genesis-base-amd64_..~jammy_all.deb> \
--genesis-deb <xcat-genesis-base-amd64_..~noble_all.deb> \
--genesis-deb <xcat-genesis-base-amd64_..~resolute_all.deb>
sbuild-all.pl --arch ppc64el --dists "focal jammy noble resolute" --skip-genesis
sbuild-all.pl --arch riscv64 --dists "focal jammy noble resolute"
@@ -1404,7 +1340,7 @@ sbuild-all.pl - build, validate, sign and assemble the xcat-dep Ubuntu/Debian ap
sbuild-all.pl --target noble-amd64 ... # equivalent single-target form
# single host, build AND publish in one go (add --publish explicitly):
sbuild-all.pl --arch amd64 --dists noble --genesis-rpm <rpm> --publish --expect-arch amd64 \
sbuild-all.pl --arch amd64 --dists noble --genesis-deb <deb> --publish --expect-arch amd64 \
--gpg-sign --gpg-key-id <id> --gpg-home <dir>
# verify an already-published tree out of band (signatures checked by DEFAULT):
@@ -1472,10 +1408,11 @@ failure (see L</"Each package builds in a clean, disposable chroot">).
=item Genesis
Produces the C<xcat-genesis-base> deb: a native deb is ingested as-is when provided
(C<--genesis-deb>); otherwise the rpm is converted while B<preserving the maintained control>
(Depends/Breaks/Replaces) and maintainer scripts. The amd64 host also converts the cross-arch
ppc64el genesis (issue #7610) unless C<--require-ppc-genesis> gates it. Skipped with C<--skip-genesis>.
Ingests the C<xcat-genesis-base> debs named by C<--genesis-deb> and stages each one into the
suite it was built for. The Genesis image carries the kernel of the root that built it, so
xcat-core builds one deb per Ubuntu codename (C<builddebs.pl --genesis-only --genesis-dist
E<lt>codenameE<gt>>) and stamps the codename into the version. A run that publishes a codename
with no image for it stops. Skipped with C<--skip-genesis>.
An B<OpenEmbedded Genesis package release> is a separate, verified input published by
C<--genesis-release>; it is not built here.
@@ -1542,9 +1479,9 @@ Build a single target; the arch must match C<--arch>.
Per-target manifest. Default: C<< <repo-root>/debs-manifest.conf >>.
=item B<--repo-root> / B<--xcat-source> C<path>
=item B<--repo-root> C<path>
xcat-dep root (default: the script's dir) / xcat-core root (for the maintained genesis packaging).
xcat-dep root (default: the script's dir).
=item B<--output-root> / B<--apt-dir> C<path>
@@ -1558,15 +1495,8 @@ build hosts).
=item B<--genesis-deb> C<path|url>
Native C<xcat-genesis-base> deb to ingest (repeatable; preferred over conversion).
=item B<--genesis-rpm> / B<--genesis-rpm-ppc> C<path|url>
Native-arch genesis rpm to convert / cross-arch ppc genesis rpm to convert on amd64 (issue #7610).
=item B<--require-ppc-genesis>
Make a missing ppc64el genesis fatal (default: warn).
An C<xcat-genesis-base> deb that xcat-core built. Repeatable: pass one per codename, and one per
architecture on a host that stages another architecture's image.
=item B<--genesis-release> C<dir>
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env perl
# The Genesis image carries the kernel of the release that built it, so xcat-core builds one deb
# per Ubuntu codename and stamps the codename into the version. Staging all of them into every
# suite publishes three images per suite and lets apt pick the newest, which is the image of
# another release.
#
# sbuild-all.pl also kept an rpm->deb fallback, the EL image converted with rpm2cpio, which gave
# an Ubuntu node an image built from an EL kernel. It is removed.
use strict;
use warnings;
use File::Basename qw(basename);
use File::Path qw(make_path);
use File::Spec;
use File::Temp qw(tempdir);
use FindBin;
use lib $FindBin::Bin . '/..';
use Test::More;
require BuildUtils;
my $root = File::Spec->rel2abs("$FindBin::Bin/..");
ok(BuildUtils->can('genesis_debs_for_codename'),
'BuildUtils selects the Genesis deb of one codename');
unless (BuildUtils->can('genesis_debs_for_codename')) {
diag('sbuild-all.pl stages every Genesis deb into every suite');
done_testing();
exit;
}
my @built = map { "/staging/$_" } qw(
xcat-genesis-base-amd64_2.19.0-snap202609121200~jammy_all.deb
xcat-genesis-base-amd64_2.19.0-snap202609121200~noble_all.deb
xcat-genesis-base-amd64_2.19.0-snap202609121200~resolute_all.deb
);
is_deeply([ BuildUtils::genesis_debs_for_codename(\@built, 'noble') ],
[ '/staging/xcat-genesis-base-amd64_2.19.0-snap202609121200~noble_all.deb' ],
'noble takes the image built on noble');
is_deeply([ BuildUtils::genesis_debs_for_codename(\@built, 'jammy') ],
[ '/staging/xcat-genesis-base-amd64_2.19.0-snap202609121200~jammy_all.deb' ],
'jammy takes the image built on jammy');
is_deeply([ BuildUtils::genesis_debs_for_codename(\@built, 'focal') ], [],
'a release with no image of its own takes none');
my @unmarked = ('/staging/xcat-genesis-base-amd64_2.19.0-snap202609121200_all.deb');
is_deeply([ BuildUtils::genesis_debs_for_codename(\@unmarked, 'noble') ], \@unmarked,
'a deb built for no particular release serves every release');
is_deeply([ BuildUtils::genesis_debs_for_codename(\@built, undef) ], \@built,
'with no codename every deb is taken');
is_deeply([ BuildUtils::genesis_debs_for_codename([], 'noble') ], [],
'no deb is no deb');
# cross_copy_genesis_deb stages into one suite, so it must take the codename too.
my $tmp = tempdir(CLEANUP => 1);
my ($from, $to) = ("$tmp/from", "$tmp/to");
make_path($from, $to);
for my $deb (@built) {
open my $fh, '>', "$from/" . basename($deb) or die $!;
print {$fh} basename($deb);
close $fh;
}
BuildUtils::cross_copy_genesis_deb($from, $to, 'amd64', undef, 'noble');
my @staged = map { basename($_) } glob("$to/*.deb");
is_deeply(\@staged, [ 'xcat-genesis-base-amd64_2.19.0-snap202609121200~noble_all.deb' ],
'only the codename its own image is staged into a suite');
# The rpm fallback is gone: the option it hangs on is not accepted any more.
for my $option (qw(--genesis-rpm --genesis-rpm-ppc --require-ppc-genesis)) {
my $out = qx{cd '$root' && perl ./sbuild-all.pl $option x --dry-run 2>&1};
isnt($? >> 8, 0, "sbuild-all.pl rejects $option");
like($out, qr/Unknown option/i, "$option is not an option any more");
}
done_testing();
+95 -26
View File
@@ -4,7 +4,7 @@ use warnings;
use Cwd qw(abs_path);
use File::Basename qw(basename);
use File::Copy qw(copy);
use File::Path qw(make_path);
use File::Path qw(make_path remove_tree);
use Fcntl qw(:flock);
use File::Temp qw(tempdir);
use FindBin;
@@ -23,6 +23,7 @@ use XCAT::BuildUtils qw(
read_binary
write_binary
);
use XCAT::NFSLock ();
use XCAT::GenesisRelease qw(
architectures
deb_package_name
@@ -73,6 +74,7 @@ SKIP: {
test_skip_build_collects_results();
test_dry_run_release();
test_rpm_repository_lock();
test_finalize_cell_lock();
test_rpm_signal_cleanup();
}
@@ -169,6 +171,9 @@ sub test_rpm_consumer {
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,
@@ -186,6 +191,7 @@ sub test_rpm_consumer {
);
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',
@@ -938,11 +944,16 @@ sub test_dry_run_release {
sub test_rpm_repository_lock {
my $output = "$tmp/rpm-lock-output";
my $repository = "$tmp/rpm-shared-repository";
my $target = 'test+epel-10-' . capture_command('uname', '-m');
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);
make_path("$repository/.lock");
write_binary("$repository/.lock/owner", "host=other\npid=1\nepoch=1\n");
# 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"))
@@ -963,9 +974,11 @@ sub test_rpm_repository_lock {
'--no-verify-repo',
'--skip-createrepo', '--skip-tarball', '--dry-run',
);
isnt($status, 0, 'a shared RPM repository cannot have two publishers');
like(read_binary($log), qr/repository \Q$repository\E is locked/,
'the lock failure names the shared repository');
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";
@@ -981,12 +994,54 @@ sub test_rpm_repository_lock {
'--target', $target,
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
'--no-verify-repo',
'--skip-createrepo', '--skip-tarball', '--dry-run', '--force-unlock',
'--skip-createrepo', '--skip-tarball', '--dry-run',
);
is($forced_status, 0, '--force-unlock recovers an interrupted RPM publication');
ok(-f "$repository/common/marker",
'the interrupted common repository is restored before publication');
ok(!-d $staging, 'abandoned common repository staging is removed');
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 {
@@ -995,13 +1050,10 @@ sub test_rpm_signal_cleanup {
my $signal_bin = "$tmp/rpm-signal-bin";
my $log = "$tmp/rpm-signal.log";
make_path($repository, $signal_bin);
write_binary(
"$signal_bin/uname",
"#!/bin/sh\n"
. "if [ \"\$1\" = -m ]; then sleep 60; exit 1; fi\n"
. "exec /usr/bin/uname \"\$@\"\n",
);
chmod(0755, "$signal_bin/uname") or die $!;
# 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);
@@ -1026,7 +1078,7 @@ sub test_rpm_signal_cleanup {
my $locked = 0;
for (1 .. 200) {
if (-d "$output/.lock" && -d "$repository/.lock") {
if (-d "$output/.lock" && -d $cell_lock) {
$locked = 1;
last;
}
@@ -1037,7 +1089,7 @@ sub test_rpm_signal_cleanup {
waitpid($pid, 0);
is($? >> 8, 1, 'SIGTERM follows the publisher cleanup exit path');
ok(!-d "$output/.lock", 'SIGTERM releases the output lock');
ok(!-d "$repository/.lock", 'SIGTERM releases the repository lock');
ok(!-d $cell_lock, 'SIGTERM releases the repository cell lock');
}
sub test_publish_lock {
@@ -1046,9 +1098,25 @@ sub test_publish_lock {
stage_legacy_deb("$tmp/lock-deb", $output);
make_path($output);
my $lockfile = "$output/.sbuild-all.publish.lock";
open(my $held, '>', $lockfile) or die "Cannot create $lockfile: $!\n";
flock($held, LOCK_EX | LOCK_NB) or die "Cannot hold $lockfile: $!\n";
# 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(
@@ -1056,11 +1124,11 @@ sub test_publish_lock {
extra => [ '--publish-lock-wait', '2' ],
);
isnt($locked_status, 0, 'a locked apt tree is not published into');
like(read_binary($locked_log), qr/waiting for the publish lock \Q$lockfile\E/,
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');
close($held);
$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.
@@ -1074,6 +1142,7 @@ sub test_publish_lock {
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');
}
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/perl
# Behaviour test for the Go toolchain the Ubuntu goconserver build uses.
#
# riscv64 has no build host, so its chroot runs under qemu-user. A Go toolchain built FOR riscv64
# therefore runs emulated, and `go build` parks its threads in futex_wait and never finishes: three
# xcat-dep-ubuntu-cd riscv64 cells burned the whole 9000s budget with no CPU ticks at all. Go
# cross-compiles, so the toolchain must be the BUILD HOST's and the target must come from GOARCH.
#
# The test LIFTS the build shell out of goconserver/sbuild.pl, RUNS it, and asserts on what the run
# asked for -- the toolchain tarball it fetched, and the environment the real debian/rules passed to
# `go build`. It never matches the source of the thing it tests.
#
# Every command that could write outside the scratch tree is shadowed by a recorder that refuses the
# write and reports it, so a build that reaches for /usr/local is a FAILED assertion here rather than
# damage to the host running the suite.
use strict;
use warnings;
use Test::More;
use File::Temp qw(tempdir);
use File::Path qw(make_path);
use FindBin qw($RealBin);
my $pkg_dir = "$RealBin/../goconserver";
plan skip_all => 'goconserver/sbuild.pl not found' unless -f "$pkg_dir/sbuild.pl";
plan skip_all => 'bash is not available' unless -x '/bin/bash';
my $LIFT = 'lifted by t/goconserver_cross_build.t';
# The build shell, produced by the real builder code. The whole marked region is evaluated, so the
# architecture the builder stamps into the script comes from the builder rather than from this test.
# die (never BAIL_OUT) when the lift stops matching: prove stops the WHOLE suite on a bail-out, and a
# silent miss would leave this file covering nothing.
sub build_script {
open(my $fh, '<', "$pkg_dir/sbuild.pl") or die "read sbuild.pl: $!";
my $src = do { local $/; <$fh> };
close($fh);
my ($region) = $src =~ /^\#[^\n]*\Q$LIFT\E[^\n]*\n(.*?^BUILD$)/ms
or die "goconserver/sbuild.pl no longer marks its build script with '$LIFT' -- "
. "this test can no longer reach the code it covers\n";
my $build = eval "$region\n\$build"; ## no critic
die "could not evaluate the lifted build script: $@\n" if $@;
die "the lifted build script is empty\n" unless defined $build && $build =~ /\S/;
return $build;
}
sub write_stub {
my ($dir, $name, $body) = @_;
open(my $fh, '>', "$dir/$name") or die "write $dir/$name: $!";
print {$fh} "#!/bin/bash\n$body\n";
close($fh);
chmod(0755, "$dir/$name") or die "chmod $dir/$name: $!";
}
# run_build($target_arch): run the build shell with the chroot's architecture reported as
# $target_arch, and return what it asked the outside world to do.
sub run_build {
my ($target_arch) = @_;
my $root = tempdir(CLEANUP => 1);
my ($bin, $rec, $work) = ("$root/bin", "$root/rec", "$root/work");
make_path($bin, $rec, $work);
# The build runs with CWD = a copy of the package dir, and reads ../gomod and ./debian from it.
system('cp', '-rL', "$pkg_dir/$_", "$work/$_") == 0 or die "stage $_: $!" for qw(gomod debian);
# dpkg answers for the CHROOT, which is the architecture the build must produce.
write_stub($bin, 'dpkg', qq{
[ "\$1" = --print-architecture ] && { echo '$target_arch'; exit 0; }
exec /usr/bin/dpkg "\$\@"
});
write_stub($bin, 'curl', qq{
for a in "\$\@"; do case "\$a" in http*) echo "\$a" >> '$rec/curl-urls';; esac; done
exit 0
});
# tar and rm police their target: anything outside the scratch tree is recorded, not performed.
write_stub($bin, 'tar', qq{
dest=''; prev=''
for a in "\$\@"; do [ "\$prev" = -C ] && dest="\$a"; prev="\$a"; done
case "\$dest" in '$root'/*) ;; *) echo "tar -C \$dest" >> '$rec/escapes';; esac
exit 0
});
write_stub($bin, 'rm', qq{
for a in "\$\@"; do
case "\$a" in
-*|'$root'/*) ;;
/*) echo "rm \$a" >> '$rec/escapes'; exit 0;;
esac
done
exec /bin/rm "\$\@"
});
# Only `git init <dir>` has to have an effect; the clone has no source this test needs.
write_stub($bin, 'git', qq{
if [ "\$1" = init ]; then shift
for a in "\$\@"; do case "\$a" in -*) ;; *) mkdir -p "\$a";; esac; done
fi
exit 0
});
write_stub($bin, 'dch', 'exit 0');
# The real debian/rules has to see the environment, so run its build target for real.
write_stub($bin, 'dpkg-buildpackage', 'make -f debian/rules override_dh_auto_build');
# The downloaded toolchain never lands, so `go` always resolves here. It records the environment
# of each invocation, which is the thing under test.
write_stub($bin, 'go', qq{
echo "GOARCH=\${GOARCH-} GOOS=\${GOOS-} ARGV=\$*" >> '$rec/go-calls'
exit 0
});
open(my $fh, '>', "$root/build.sh") or die "write build.sh: $!";
print {$fh} build_script();
close($fh);
my $rc = system('/bin/bash', '-c',
"cd '$work' && PATH=\"$bin:\$PATH\" SOURCE_DATE_EPOCH=1789413339 "
. "bash '$root/build.sh' > '$root/build.log' 2>&1");
my $slurp = sub {
my ($f) = @_;
return () unless -f "$rec/$f";
open(my $h, '<', "$rec/$f") or return ();
my @l = <$h>; close($h); chomp @l; return @l;
};
open(my $lh, '<', "$root/build.log") or die "read build.log: $!";
my $log = do { local $/; <$lh> };
close($lh);
return { rc => $rc, log => $log // '',
curl => [ $slurp->('curl-urls') ],
go_calls => [ $slurp->('go-calls') ],
escapes => [ $slurp->('escapes') ] };
}
# The architecture the toolchain must be built for: this machine's, in Go's spelling.
my $host_deb = `dpkg --print-architecture 2>/dev/null` // '';
chomp $host_deb;
plan skip_all => 'dpkg is not available' unless $host_deb =~ /^[a-z0-9]+$/;
my $host_go = $host_deb eq 'ppc64el' ? 'ppc64le' : $host_deb;
# ---- the cell that failed: a riscv64 chroot on this build host ----------------------------------
my $r = run_build('riscv64');
my ($toolchain) = grep { m{/go[\d.]+\.linux-} } @{ $r->{curl} };
ok(defined $toolchain, 'the build fetches a pinned Go toolchain')
or diag("curl was asked for: @{ $r->{curl} }\n$r->{log}");
SKIP: {
skip 'no toolchain download to inspect', 1 unless defined $toolchain;
like($toolchain, qr/\.linux-\Q$host_go\E\.tar\.gz$/,
"the toolchain is built for the build host ($host_go), so it runs natively not under qemu")
or diag("fetched: $toolchain");
}
# compiles_for($result, $goarch, $label): every `go build` the run reached was told to emit $goarch.
# A run that reached NO `go build` fails here: an empty list would otherwise satisfy any claim.
sub compiles_for {
my ($res, $goarch, $label) = @_;
my @builds = grep { /ARGV=.*\bbuild\b/ } @{ $res->{go_calls} };
unless (@builds) {
fail("$label -- the run never reached `go build`");
diag("go was called: @{ $res->{go_calls} }\nrc=$res->{rc}\n$res->{log}");
return;
}
is_deeply([ grep { !/\bGOARCH=\Q$goarch\E\b/ } @builds ], [], $label)
or diag("go build calls:\n" . join("\n", @builds));
}
compiles_for($r, 'riscv64',
'every `go build` is told to emit riscv64, so the native toolchain cross-compiles');
is_deeply($r->{escapes}, [],
'the build writes and deletes only inside its own build tree')
or diag("escaped the build tree:\n" . join("\n", @{ $r->{escapes} }));
# ---- a chroot of the host's own architecture still builds natively ------------------------------
my $n = run_build($host_deb);
my ($native) = grep { m{/go[\d.]+\.linux-} } @{ $n->{curl} };
like($native // '', qr/\.linux-\Q$host_go\E\.tar\.gz$/,
'a native cell fetches the same toolchain');
compiles_for($n, $host_go, 'a native cell compiles for its own architecture');
# ---- dpkg and Go spell the POWER architecture differently ----------------------------------------
my $p = run_build('ppc64el');
compiles_for($p, 'ppc64le', 'the dpkg name ppc64el reaches go as ppc64le');
done_testing;
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/perl
# Behaviour test for ipxe-xcat/verify-payload.pl, the check between a built ipxe-xcat package and the
# release tree that the package must carry byte for byte.
#
# A manifest is written for a small fixture tree with the checker's own --generate mode. Each case
# damages a copy of the tree in one way and runs the check as a subprocess: the check must fail and
# name the path. A check that compared only names would pass every damage case below. The last block
# holds the committed payload.sha256 and SHA256SUMS to the committed release archives.
use strict;
use warnings;
use Test::More;
use Digest::SHA ();
use File::Basename qw(dirname);
use File::Copy qw(copy);
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use FindBin qw($RealBin);
use IPC::Open3 qw(open3);
my $pkg_dir = "$RealBin/../ipxe-xcat";
my $checker = "$pkg_dir/verify-payload.pl";
plan skip_all => 'ipxe-xcat/verify-payload.pl not found' unless -f $checker;
my $tmp = tempdir(CLEANUP => 1);
# Runs the checker; returns its exit code and its merged stdout and stderr.
sub run_checker {
my (@args) = @_;
my $pid = open3(my $in, my $out, undef, $^X, $checker, @args);
close($in);
my $text = do { local $/; <$out> } // '';
waitpid($pid, 0);
return ($? >> 8, $text);
}
sub write_file {
my ($path, $content) = @_;
make_path(dirname($path));
open(my $fh, '>:raw', $path) or die "write $path: $!";
print {$fh} $content;
close($fh) or die "close $path: $!";
}
sub make_tree {
my ($root) = @_;
write_file("$root/i386/undionly.kpxe", "undi\x00\x01");
write_file("$root/x86_64-sb/snponly.efi", "MZ signed snponly");
write_file("$root/x86_64-sb/shimx64.efi", "MZ signed shim");
symlink('shimx64.efi', "$root/x86_64-sb/ipxe-shim.efi") or die "symlink: $!";
symlink('x86_64-sb', "$root/sb") or die "symlink: $!";
symlink('i386/undionly.kpxe', "$root/undionly.kpxe") or die "symlink: $!";
}
sub copy_tree {
my ($name) = @_;
my $copy = "$tmp/$name";
system('cp', '-a', "$tmp/pristine", $copy) == 0 or die "cp -a to $copy failed";
return $copy;
}
make_tree("$tmp/pristine");
my ($code, $manifest_text) = run_checker('--generate', "$tmp/pristine");
is($code, 0, '--generate succeeds on a tree of files, directories and symlinks');
my $manifest = "$tmp/payload.sha256";
write_file($manifest, $manifest_text);
my @entries = grep { !/^#/ } split(/\n/, $manifest_text);
is(scalar(@entries), 8, 'the manifest has one entry for each directory, file and symlink');
like($manifest_text, qr/^link\tx86_64-sb\tsb$/m, 'a symlink is recorded with its target, not followed');
($code, my $output) = run_checker("$tmp/pristine", $manifest);
is($code, 0, 'the unchanged tree passes') or diag($output);
like($output, qr/payload matches .*: 8 entries/, 'the check reports the number of entries');
($code, $output) = run_checker("$tmp/pristine/", $manifest);
is($code, 0, 'a trailing slash on the tree path does not change the result') or diag($output);
my @damage = (
['one changed byte',
sub { write_file("$_[0]/i386/undionly.kpxe", "undi\x00\x02") },
qr{^content changed: i386/undionly\.kpxe$}m],
['a missing file',
sub { unlink("$_[0]/x86_64-sb/snponly.efi") or die $! },
qr{^missing: x86_64-sb/snponly\.efi$}m],
['an extra file',
sub { write_file("$_[0]/x86_64-sb/extra.efi", 'extra') },
qr{^unexpected: x86_64-sb/extra\.efi$}m],
['an extra directory',
sub { make_path("$_[0]/arm64") },
qr{^unexpected: arm64$}m],
['a symlink with another target',
sub { unlink("$_[0]/sb") or die $!; symlink('i386', "$_[0]/sb") or die $! },
qr{^link target changed: sb \(expected x86_64-sb, found i386\)$}m],
['a symlink replaced by a copy of its target',
sub { unlink("$_[0]/x86_64-sb/ipxe-shim.efi") or die $!;
write_file("$_[0]/x86_64-sb/ipxe-shim.efi", "MZ signed shim") },
qr{^type changed: x86_64-sb/ipxe-shim\.efi \(expected link, found file\)$}m],
);
my $n = 0;
for my $case (@damage) {
my ($name, $apply, $message) = @{$case};
my $copy = copy_tree('damaged-' . ++$n);
$apply->($copy);
($code, $output) = run_checker($copy, $manifest);
is($code, 1, "$name fails the check");
like($output, $message, "$name is reported by path") or diag($output);
}
write_file("$tmp/malformed.sha256", "file\tnot-a-digest\ti386/undionly.kpxe\n");
($code) = run_checker("$tmp/pristine", "$tmp/malformed.sha256");
is($code, 2, 'a malformed manifest line is an error, not a mismatch');
write_file("$tmp/duplicate.sha256", "dir\t-\ti386\ndir\t-\ti386\n");
($code) = run_checker("$tmp/pristine", "$tmp/duplicate.sha256");
is($code, 2, 'a duplicate manifest path is an error');
($code) = run_checker("$tmp/pristine/sb", $manifest);
is($code, 2, 'a tree path that is a symlink is refused');
# The committed manifest and checksums must describe the committed archives.
my @releases = glob("$pkg_dir/ipxeboot-*.tar.gz");
is(scalar(@releases), 1, 'the package directory holds one release archive');
SKIP: {
skip 'no release archive', 4 if @releases != 1;
my $release = "$tmp/release";
make_path($release);
is(system('tar', '-xzf', $releases[0], '--strip-components=1', '-C', $release), 0,
'the release archive unpacks');
# The builders install the ipxe/shim shims, signed by both UEFI CAs, over those of the release.
my %shims = ('ipxe-shimx64.efi' => 'x86_64-sb/shimx64.efi', 'ipxe-shimaa64.efi' => 'arm64-sb/shimaa64.efi');
($code, $output) = run_checker($release, "$pkg_dir/payload.sha256");
is($code, 1, 'payload.sha256 does not describe the shims of the bare release tree');
copy("$pkg_dir/$_", "$release/$shims{$_}") or die "copy $_: $!" for sort keys %shims;
($code, $output) = run_checker($release, "$pkg_dir/payload.sha256");
is($code, 0, 'payload.sha256 matches the release tree with the committed shims') or diag($output);
open(my $fh, '<', "$pkg_dir/SHA256SUMS") or die "read SHA256SUMS: $!";
my %sums = map { /^([0-9a-f]{64}) (\S+)$/ ? ($2, $1) : () } <$fh>;
close($fh);
my %actual = map { ($_, Digest::SHA->new(256)->addfile("$pkg_dir/$_", 'b')->hexdigest) }
grep { -f "$pkg_dir/$_" } keys %sums;
is_deeply(\%actual, \%sums, 'SHA256SUMS matches the committed archives and shims');
}
done_testing();
+46 -2
View File
@@ -10,9 +10,10 @@ use lib "$RealBin/..";
use File::Temp qw(tempdir);
use File::Path qw(make_path);
use File::Basename qw(basename);
use File::Slurper qw(write_text);
use MockBuildUtils qw(install_deps_packages install_deps_command missing_perl_modules
required_pkgs version_matches rpm_sigmd5 rpm_version rpm_release rpm_is_signed
rpm_arch rpm_in_cell
rpm_arch rpm_in_cell resolve_mock_cfg
skipped_builder carry_over_rpms source_package
restamp_release_line cross_copy_genesis finalize_xcat_dep read_manifest
verify_repo_packages verify_repo_signature verify_rpm_signatures
@@ -31,6 +32,24 @@ sub quiet(&) {
return wantarray ? @r : $r[0];
}
# ---- resolve_mock_cfg: /etc/os-release says almalinux, mock-core-configs names the file alma -----
{
my $dir = tempdir(CLEANUP => 1);
my $touch = sub { open(my $fh, '>', "$dir/$_[0].cfg") or die "$_[0]: $!"; close($fh); };
$touch->('alma+epel-10-x86_64');
$touch->('rocky+epel-9-x86_64');
is(eval { resolve_mock_cfg('almalinux', 10, 'x86_64', $dir) }, 'alma+epel-10-x86_64',
'an AlmaLinux host resolves to the alma config file');
is(eval { resolve_mock_cfg('rocky', 9, 'x86_64', $dir) }, 'rocky+epel-9-x86_64',
'an id that names its config file resolves to it');
$touch->('almalinux+epel-10-x86_64');
is(eval { resolve_mock_cfg('almalinux', 10, 'x86_64', $dir) }, 'almalinux+epel-10-x86_64',
'a config file named after the os-release id is preferred');
ok(!eval { resolve_mock_cfg('almalinux', 8, 'x86_64', $dir); 1 },
'a release with no config file is an error');
like($@, qr{\Q$dir/alma+epel-8-x86_64.cfg\E}, 'the error names the config files it tried');
}
# ---- required_pkgs: a skipped builder's packages are not required (clean --skip-* runs) -------
my @all = qw(elilo-xcat ipmitool-xcat perl-IO-Stty perl-Sys-Virt xCAT-genesis-base);
is_deeply([required_pkgs(\@all, 0, 0, 0)], \@all,
@@ -363,6 +382,27 @@ SPEC
is($tarch{ppc64le}, 'ppc64', 'GENESIS_ARCHES: ppc64le maps to xCAT tarch ppc64');
}
# ---- finalize_xcat_dep only => [...]: write only the cells of the named arches -----------------
# Each host finalizes the cells it deploys, so a cell lock is never held from another host.
{
my $tmp = tempdir(CLEANUP => 1);
my ($x, $p) = ("$tmp/r/rh9/x86_64", "$tmp/r/rh9/ppc64le");
make_path($x, $p);
write_text("$x/xCAT-genesis-base-x86_64-2.19.1-1.noarch.rpm", "x86 genesis\n");
write_text("$p/xCAT-genesis-base-ppc64-2.19.1-1.noarch.rpm", "ppc genesis\n");
my @reindexed;
quiet { finalize_xcat_dep("$tmp/r", "$tmp/r", only => ['ppc64le'],
reindex => sub { push @reindexed, $_[0] }) };
ok(-f "$p/xCAT-genesis-base-x86_64-2.19.1-1.noarch.rpm", 'the ppc64le cell gets the x86_64 genesis');
ok(!-e "$x/xCAT-genesis-base-ppc64-2.19.1-1.noarch.rpm", 'the x86_64 cell is not written');
is_deeply(\@reindexed, [$p], 'only the ppc64le cell is re-indexed');
my $bad = eval { quiet { finalize_xcat_dep("$tmp/r", "$tmp/r", only => ['riscv64']) }; 1 };
ok(!$bad, 'an arch outside the cross-arch matrix is refused');
like($@, qr/\AFATAL: \[finalize\] no cross-arch genesis for arch 'riscv64'/,
'the refusal names the arch');
}
# ---- restamp_release_line: CD --build-number Release stamping (PR #62 review point 1) ----------
# A fresh stamp is appended after the Release token, preserving any %{?dist} macro.
{
@@ -415,6 +455,10 @@ is(rpm_release(tempdir(CLEANUP => 1), 'nonexistent-pkg'), undef, 'rpm_release is
cmp_ok(scalar(@targets), '>=', 1, 'packages-manifest.conf has at least one target section');
ok(!grep({ $_ eq 'common' } @targets), 'the shared-repo section is not treated as a build target');
my @missing = grep { !exists $m{$_}{'conserver-xcat'} } @targets;
# The upstream iPXE loaders ship beside xnba-undi, so a target that publishes one publishes both.
my @no_ipxe_xcat = grep { exists $m{$_}{'xnba-undi'} && !exists $m{$_}{'ipxe-xcat'} } @targets;
is_deeply(\@no_ipxe_xcat, [], 'every target that lists xnba-undi also lists ipxe-xcat')
or diag("missing ipxe-xcat in: @no_ipxe_xcat");
is_deeply(\@missing, [], 'conserver-xcat is present in every manifest target section')
or diag("missing conserver-xcat in: @missing");
@@ -422,7 +466,7 @@ is(rpm_release(tempdir(CLEANUP => 1), 'nonexistent-pkg'), undef, 'rpm_release is
# carries, at the same pins: a riscv64 MN serves the x86 nodes of a mixed cluster too.
my ($ppc) = grep { /^[a-z+]+-10-ppc64le$/ } @targets;
ok(defined $ppc, 'an EL10 ppc64le target section exists to compare against') or $ppc = '';
for my $boot (qw(elilo-xcat grub2-xcat syslinux-xcat xnba-undi)) {
for my $boot (qw(elilo-xcat grub2-xcat ipxe-xcat syslinux-xcat xnba-undi)) {
is($m{'rocky-10-riscv64-xcat'}{$boot}, $m{$ppc}{$boot},
"$boot pinned in the riscv64 target as in the EL10 ppc64le target");
}
+310
View File
@@ -0,0 +1,310 @@
#!/usr/bin/perl
# XCAT::NFSLock: the NFS lock protocol at the top of lib/XCAT/NFSLock.pm.
use strict;
use warnings;
use Test::More;
use FindBin qw($RealBin);
use lib "$RealBin/../lib";
use File::Temp qw(tempdir);
use File::Slurper qw(read_text write_text);
use POSIX ();
use Time::HiRes ();
my $renames = 0;
my @mkdirs;
BEGIN {
no warnings 'once';
*CORE::GLOBAL::rename = sub { $renames++; return CORE::rename($_[0], $_[1]) };
*CORE::GLOBAL::mkdir = sub { push(@mkdirs, $_[0]); return @_ > 1 ? CORE::mkdir($_[0], $_[1]) : CORE::mkdir($_[0]) };
}
use XCAT::NFSLock qw(this_process format_metadata parse_metadata owner_is_dead process_start);
# The lock logs to the selected handle. Keep the log out of the TAP stream, and read it back.
open(my $log_fh, '>', \my $logged) or die "Cannot capture the log: $!";
select($log_fh);
sub clear_log { $logged = ''; seek($log_fh, 0, 0) }
my $dir = tempdir(CLEANUP => 1);
my $me = this_process();
is($me->{pid}, $$, 'the identity names this process');
is($me->{pstart}, process_start($$), 'the identity carries the start time of this process');
isnt(this_process()->{token}, $me->{token}, 'each identity has a fresh token');
# No process can have a pid above the kernel's pid_max (2**22 at most).
my $gone = 2**22 + 7;
is(process_start($gone), undef, 'a pid that names no process has no start time');
my $parent = getppid();
my $parent_start = process_start($parent);
sub metadata {
my (%f) = @_;
return format_metadata({ %$me, token => 'ab' x 16, %f });
}
sub stage {
my ($name, $text) = @_;
my $path = "$dir/$name";
mkdir($path) or die "Cannot stage $path: $!";
write_text("$path/metadata", $text) if defined($text);
return $path;
}
# Record the waits instead of sleeping. $on_sleep runs at each wait.
my @slept;
our $on_sleep;
{
no warnings 'redefine';
*XCAT::NFSLock::_sleep = sub { push(@slept, $_[0]); $on_sleep->() if $on_sleep };
}
# Metadata: fields, hash, validation.
{
my $text = metadata();
is_deeply(parse_metadata($text), { %$me, token => 'ab' x 16 }, 'valid metadata parses to its identity');
like($text, qr/\Aboot-id=.*\nmachine-id=.*\npid=.*\npstart=.*\ntoken=.*\nhash=[0-9a-f]{64}\n\z/,
'the fields are sorted and the hash comes last');
is(parse_metadata(substr($text, 0, length($text) - 10)), undef, 'a partial read is invalid');
is(parse_metadata($text =~ s/pid=\d+/pid=1/r), undef, 'a changed field no longer matches the hash');
is(parse_metadata($text =~ s/^token=.*\n//mr), undef, 'a missing field is invalid');
is(parse_metadata("extra=1\n$text"), undef, 'an unknown field is invalid');
is(parse_metadata(undef), undef, 'missing metadata is invalid');
}
# A free lock is taken and released without leftovers.
{
my $path = "$dir/free.lock";
my $lock = XCAT::NFSLock->acquire($path);
my $meta = parse_metadata(read_text("$path/metadata"));
is($meta && $meta->{pid}, $$, 'a free lock is taken with the identity of this process');
is($lock->release, 1, 'the owner releases its lock');
ok(!-e $path, 'the released lock.d is gone');
ok(!-e "$path.borrow", 'release removes lock.borrow');
is($lock->release, 0, 'a second release does nothing');
}
for my $bad ('', "$dir/.", "$dir/..", "$dir/") {
eval { XCAT::NFSLock->acquire($bad); 1 };
like($@, qr/\AInvalid lock path/, "a lock path that names no entry is refused: '$bad'");
}
for my $case (
[ { retries => 0 }, qr/\AInvalid retries 0 for lock/ ],
[ { retries => 1.5 }, qr/\AInvalid retries 1\.5 for lock/ ],
[ { delay => 2.9 }, qr/\AInvalid delay 2\.9 for lock: must be 3s or more/ ],
[ { jitter => -1 }, qr/\AInvalid jitter -1 for lock: must be 0 or more/ ],
[ { delay => 3, jitter => 1.5 }, qr/\AInvalid jitter 1\.5 for lock: must be less than half the delay/ ],
)
{
my ($opt, $error) = @$case;
eval { XCAT::NFSLock->acquire("$dir/bad-option.lock", %$opt); 1 };
like($@, $error, 'an invalid retry option is refused: ' . join(',', %$opt));
ok(!-e "$dir/bad-option.lock", 'an invalid option creates no lock');
}
# Retry: R waits of T ± δ, then an error that names the lock.
{
my $path = stage('retried.lock', metadata(pid => $parent, pstart => $parent_start));
@slept = ();
eval { XCAT::NFSLock->acquire($path, retries => 4, delay => 5, jitter => 2, label => 'cell lock'); 1 };
like($@, qr/\ATrying to unlock \Q$path\E failed after 4 retries; cell lock owned by pid $parent on machine /,
'the error names the lock, the retries and the owner');
is(scalar(@slept), 4, 'acquire waits R times');
is(scalar(grep { $_ >= 3 && $_ <= 7 } @slept), 4, 'each wait is within T ± δ');
@slept = ();
eval { XCAT::NFSLock->acquire($path, timeout => 10); 1 };
like($@, qr/failed after 4 retries;/, 'a timeout of 10s with the default delay of 3s is 4 retries');
@slept = ();
eval { XCAT::NFSLock->acquire($path); 1 };
like($@, qr/failed after 1 retry;/, 'a lock with no timeout still retries once');
}
# The lock stays with an owner that is not proven dead.
for my $case (
[ 'live owner on this machine', metadata(pid => $parent, pstart => $parent_start) ],
[ 'owner on another machine', metadata('machine-id' => 'elsewhere', pid => $gone) ],
[ 'partial metadata', substr(metadata(pid => $gone), 0, 40) ],
[ 'metadata with a bad hash', metadata(pid => $gone) =~ s/hash=(.)/'hash=' . ($1 eq '0' ? '1' : '0')/er ],
[ 'no metadata', undef ],
)
{
my ($name, $text) = @$case;
(my $file = "$name.lock") =~ s/\s+/-/g;
my $path = stage($file, $text);
@mkdirs = ();
eval { XCAT::NFSLock->acquire($path, retries => 2); 1 };
like($@, qr/\ATrying to unlock \Q$path\E failed after 2 retries;/, "$name: the lock is not taken");
is(scalar(grep { $_ eq "$path.borrow" } @mkdirs), $name =~ /live owner/ ? 3 : 0,
"$name: lock.borrow is tried only for an owner on this machine with valid metadata");
is(-e "$path/metadata" ? read_text("$path/metadata") : undef, $text, "$name: the metadata is unchanged");
ok(!-e "$path.borrow", "$name: lock.borrow is not left behind");
}
# A dead owner on this machine loses the lock.
for my $case (
[ 'process gone', metadata(pid => $gone) ],
[ 'pid reused', metadata(pid => $parent, pstart => $parent_start + 1) ],
[ 'machine rebooted', metadata('boot-id' => 'an-earlier-boot', pid => $parent, pstart => $parent_start) ],
)
{
my ($name, $text) = @$case;
(my $file = "$name.lock") =~ s/\s+/-/g;
my $path = stage($file, $text);
@slept = ();
my $lock = eval { XCAT::NFSLock->acquire($path) };
ok($lock, "$name: the lock of a dead owner is taken") or diag($@);
is(scalar(@slept), 0, "$name: the lock is taken without a wait");
my $meta = parse_metadata(read_text("$path/metadata"));
is($meta && $meta->{pid}, $$, "$name: the metadata now names this process");
isnt($meta && $meta->{token}, 'ab' x 16, "$name: the new metadata has a fresh token");
ok(!-e "$path.borrow", "$name: lock.borrow is removed");
is($lock->release, 1, "$name: the new owner releases the lock") if $lock;
}
# Another process holds lock.borrow: this one does not take the lock.
{
my $dead = metadata(pid => $gone);
my $path = stage('borrowed.lock', $dead);
mkdir("$path.borrow") or die "Cannot stage $path.borrow: $!";
eval { XCAT::NFSLock->acquire($path, retries => 2); 1 };
like($@, qr/\ATrying to unlock /, 'a lock under another borrower is not taken');
is(read_text("$path/metadata"), $dead, 'the metadata under another borrower is unchanged');
ok(-d "$path.borrow", 'the other borrower keeps lock.borrow');
}
# The owner changed between step 2 and step 4: this attempt does not take the lock,
# even when the new owner is dead too.
{
my $path = stage('changed.lock', metadata(pid => $gone));
my $other = metadata(pid => $gone, token => 'cd' x 16);
my $read = \&XCAT::NFSLock::_read_metadata;
my $reads = 0;
no warnings 'redefine';
local *XCAT::NFSLock::_read_metadata = sub {
write_text("$path/metadata", $other) if ++$reads == 2;
return $read->(@_);
};
@slept = ();
my $lock = eval { XCAT::NFSLock->acquire($path, retries => 1) };
ok($lock, 'the lock is taken on the next attempt') or diag($@);
is(scalar(@slept), 1, 'a changed owner costs one retry');
ok(!-e "$path.borrow", 'lock.borrow is removed');
$lock->release if $lock;
}
# release waits for a process that holds lock.borrow.
{
my $path = "$dir/release-borrowed.lock";
my $lock = XCAT::NFSLock->acquire($path);
mkdir("$path.borrow") or die "Cannot stage $path.borrow: $!";
@slept = ();
local $on_sleep = sub { rmdir("$path.borrow") };
is($lock->release, 1, 'release removes the lock once lock.borrow is free');
is(scalar(@slept), 1, 'release waits while lock.borrow is held');
ok(!-e $path && !-e "$path.borrow", 'lock.d and lock.borrow are gone');
}
# A forked child of the owner does not release the lock.
{
my $path = "$dir/forked.lock";
my $lock = XCAT::NFSLock->acquire($path);
my $child = fork() // die "Cannot fork: $!";
POSIX::_exit($lock->release ? 1 : 0) if $child == 0;
waitpid($child, 0);
is($? >> 8, 0, 'the child reports that it released nothing');
ok(-d $path, 'the lock survives the child');
is($lock->release, 1, 'the owner still releases it');
}
# The metadata names another acquisition: release leaves the lock alone.
{
my $path = "$dir/replaced.lock";
my $lock = XCAT::NFSLock->acquire($path);
my $other = metadata(pid => $parent, pstart => $parent_start);
write_text("$path/metadata", $other);
is($lock->release, 0, 'release does not remove a lock of another acquisition');
is(read_text("$path/metadata"), $other, 'the other owner keeps its lock');
ok(!-e "$path.borrow", 'release removes lock.borrow');
}
# One process takes the lock once: a second acquire is another owner.
{
my $path = "$dir/twice.lock";
my $first = XCAT::NFSLock->acquire($path);
my $second = eval { XCAT::NFSLock->acquire($path) };
ok(!$second, 'a second acquire in the same process does not take a held lock');
is($first->release, 1, 'the first owner still holds and releases it');
}
# A waiter takes the lock once its live owner releases it.
{
my $path = "$dir/handover.lock";
pipe(my $ready_r, my $ready_w) or die "Cannot pipe: $!";
my $child = fork() // die "Cannot fork: $!";
if ($child == 0) {
close($ready_r);
my $held = XCAT::NFSLock->acquire($path);
syswrite($ready_w, "x");
Time::HiRes::sleep(0.5);
$held->release;
POSIX::_exit(0);
}
close($ready_w);
sysread($ready_r, my $byte, 1);
local $on_sleep = sub { waitpid($child, 0) };
my $lock = eval { XCAT::NFSLock->acquire($path, retries => 1) };
ok($lock, 'a waiter takes the lock after its owner releases it') or diag($@);
$lock->release if $lock;
}
# owner_is_dead decides from facts only.
{
my %here = ('machine-id' => 'm1', 'boot-id' => 'b1', start_of => sub { $_[0] == 10 ? 100 : undef });
my %rec = ('machine-id' => 'm1', 'boot-id' => 'b1', pid => 10, pstart => 100);
is(owner_is_dead(undef, \%here), 0, 'invalid metadata is not proven dead');
is(owner_is_dead({%rec}, \%here), 0, 'a live owner is not dead');
is(owner_is_dead({ %rec, 'machine-id' => 'm2', pid => 11 }, \%here), 0,
'another machine proves nothing, even for a pid that is free here');
is(owner_is_dead({ %rec, 'boot-id' => 'b0' }, \%here), 1, 'an owner from an earlier boot is dead');
is(owner_is_dead({ %rec, pid => 11 }, \%here), 1, 'an owner whose pid is gone is dead');
is(owner_is_dead({ %rec, pstart => 99 }, \%here), 1, 'an owner whose pid was reused is dead');
}
# The log names each event, in the order it happened.
{
my $stamp = qr/\[nfslock\] \d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z \S+ pid=$$/;
my $path = "$dir/logged.lock";
clear_log();
my $lock = XCAT::NFSLock->acquire($path, label => 'cell lock');
$lock->release;
my @lines = split(/\n/, $logged);
is(scalar(@lines), 2, 'a lock taken and released logs two lines');
like($lines[0], qr/\A$stamp acquired cell lock \Q$path\E\z/, 'the first line is the acquisition');
like($lines[1], qr/\A$stamp released cell lock \Q$path\E\z/, 'the second line is the release');
my $held = stage('logged-wait.lock', metadata(pid => $parent, pstart => $parent_start));
clear_log();
eval { XCAT::NFSLock->acquire($held, retries => 2); 1 };
my @waits = $logged =~ /^$stamp wait lock \Q$held\E (retry \d\/\d), owner pid $parent on machine /mg;
is_deeply(\@waits, ['retry 1/2', 'retry 2/2'], 'each retry logs a wait that names the owner');
my $dead = stage('logged-dead.lock', metadata(pid => $gone));
clear_log();
my $taken = XCAT::NFSLock->acquire($dead);
like($logged, qr/^$stamp took-over lock \Q$dead\E from dead pid $gone$/m, 'a takeover names the dead owner');
$taken->release;
clear_log();
my $quiet = XCAT::NFSLock->acquire("$dir/quiet.lock", quiet => 1);
$quiet->release;
eval { XCAT::NFSLock->acquire($held, retries => 1, quiet => 1); 1 };
is($logged, '', 'quiet => 1 logs nothing');
}
is($renames, 0, 'the lock never renames');
done_testing();
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/perl
# Every architecture run of a dep build can publish the shared common/ tree. Recovery of an
# interrupted publication removes staging trees, so it runs only under the common lock, and never
# while another run holds that lock.
use strict;
use warnings;
use Test::More;
use FindBin qw($RealBin);
use lib "$RealBin/..", "$RealBin/../lib";
use File::Temp qw(tempdir);
use File::Path qw(make_path);
use File::Slurper qw(read_text write_text);
use MockBuildUtils qw(recover_common_repository);
use XCAT::NFSLock qw(this_process format_metadata process_start);
my $me = this_process();
# No process can have a pid above the kernel's pid_max (2**22 at most).
my $gone = 2**22 + 7;
my $parent = getppid();
sub record {
my (%f) = @_;
return format_metadata({ %$me, %f });
}
# A repository left by an interrupted publication: common/ moved aside, a staging tree beside it.
sub interrupted {
my ($holder) = @_;
my $base = tempdir(CLEANUP => 1);
make_path("$base/.common.previous.999", "$base/.common.staging");
write_text("$base/.common.previous.999/marker", "previous repository\n");
if (defined($holder)) {
make_path("$base/.common-publish.lock");
write_text("$base/.common-publish.lock/metadata", $holder);
}
return $base;
}
{
my $base = interrupted(undef);
is(recover_common_repository($base), 1, 'recovery runs when nobody holds the common lock');
is(read_text("$base/common/marker"), "previous repository\n", 'the interrupted common tree is restored');
ok(!-e "$base/.common.staging", 'the abandoned staging tree is removed');
ok(!-e "$base/.common-publish.lock", 'recovery releases the common lock');
}
for my $case (
[ 'a live run on this host', record(pid => $parent, pstart => process_start($parent)) ],
[ 'a run on another host', record('machine-id' => 'elsewhere', pid => $gone) ],
)
{
my ($name, $holder) = @$case;
my $base = interrupted($holder);
open(my $capture, '>', \my $printed) or die "Cannot capture output: $!";
my $previous = select($capture);
my $ran = recover_common_repository($base);
select($previous);
is($ran, 0, "recovery does not run while $name holds the common lock");
like($printed, qr/^common recovery skipped: another run holds \Q$base\E\/\.common-publish\.lock$/m,
"the skip names the lock $name holds");
ok(-d "$base/.common.staging", "the staging tree of $name is left in place");
ok(!-e "$base/common", "the common tree stays where $name put it");
is(read_text("$base/.common-publish.lock/metadata"), $holder, "$name keeps the common lock");
}
{
my $base = interrupted(record(pid => $gone));
is(recover_common_repository($base), 1, 'recovery takes the common lock of a run that died on this host');
ok(!-e "$base/.common.staging", 'the staging tree of the dead run is removed');
ok(-d "$base/common", 'the common tree of the dead run is restored');
}
done_testing();
+21 -43
View File
@@ -20,7 +20,7 @@ use BuildUtils qw(install_deps_packages install_deps_command missing_perl_module
supported_arches is_supported_arch
codename_to_version version_to_codename known_codenames
chroot_name chroot_sources_list chroot_is_disposable chroot_build_script
control_field genesis_deb_control
control_field
deb_field deb_version deb_hash cross_copy_genesis_deb
build_deb_in_chroot);
@@ -88,41 +88,6 @@ is(chroot_name('noble', 'amd64'), 'noble-amd64-sbuild', 'chroot_name shape');
is(control_field($ctrl, 'Replaces'), undef, 'absent field -> undef');
}
# ---- genesis_deb_control: PRESERVE the maintained packaging semantics (concern #2) --------------
{
# The real xCAT-genesis-builder/debian/control fields that the bare 5-field shim used to drop.
my $maintained = <<'CTRL';
Source: xcat-genesis-base-amd64
Section: admin
Priority: optional
Maintainer: xCAT <xcat-user@lists.sourceforge.net>
Package: xcat-genesis-base-amd64
Architecture: all
Depends: ${misc:Depends}
Replaces: xcat-genesis-amd64
Breaks: xcat-genesis-amd64, xcat-genesis-scripts-amd64 (<< 2.13.10)
Description: xCAT Genesis netboot image
base platform.
CTRL
my $c = genesis_deb_control($maintained, 'xcat-genesis-base-amd64', '2.18.0-snap1', 'all');
like($c, qr/^Package: xcat-genesis-base-amd64$/m, 'Package set');
like($c, qr/^Version: 2\.18\.0-snap1$/m, 'Version set');
like($c, qr/^Architecture: all$/m, 'Architecture set');
like($c, qr/^Replaces: xcat-genesis-amd64$/m, 'Replaces PRESERVED (was dropped by the shim)');
like($c, qr/^Breaks: xcat-genesis-amd64, xcat-genesis-scripts-amd64 \(<< 2\.13\.10\)$/m,
'Breaks PRESERVED with its version constraint');
unlike($c, qr/\$\{misc:Depends\}/, 'unresolved ${misc:Depends} substvar dropped (would ship literal)');
like($c, qr/^Maintainer: xCAT /m, 'Maintainer preserved');
}
# With no maintained control available, an honest minimal control is still produced.
{
my $c = genesis_deb_control(undef, 'xcat-genesis-base-ppc64el', '2.18.0-snap1', 'all');
like($c, qr/^Package: xcat-genesis-base-ppc64el$/m, 'minimal control still names the package');
like($c, qr/^Architecture: all$/m, 'minimal control still arch:all');
unlike($c, qr/^Replaces:/m, 'no Replaces invented when the maintained control is absent');
}
# ---- verify_repo_packages: PURE completeness decision (no I/O; manifest = source of truth) -------
{
my %req = ('ipmitool-xcat' => '1.8.18', 'goconserver' => '0.3.3', 'xcat-genesis-base' => '*');
@@ -337,14 +302,15 @@ SKIP: {
is_deeply(\@miss_go, [], 'goconserver present in every manifest target')
or diag("missing goconserver in: @miss_go");
# The noarch boot components (syslinux-xcat, grub2-xcat, elilo-xcat, xnba-undi) are Architecture:all
# single-producer (built ONCE on amd64) but REQUIRED-PRESENT on EVERY target incl. ppc64el and
# riscv64, so the gate verifies those repos actually carry them (matches the EL manifest + the 2.16
# ppc dep repo; a ppc or riscv64 MN serves the x86 nodes of a mixed cluster). It is the BUILD PHASE
# -- not the manifest -- that avoids rebuilding them off amd64 (build_one_codename skips an
# Architecture:all package on non-amd64; see the control_binary_arch test below).
# The noarch boot components (syslinux-xcat, grub2-xcat, elilo-xcat, xnba-undi, ipxe-xcat) are
# Architecture:all single-producer (built ONCE on amd64) but REQUIRED-PRESENT on EVERY target
# incl. ppc64el and riscv64, so the gate verifies those repos actually carry them (matches the
# EL manifest + the 2.16 ppc dep repo; a ppc or riscv64 MN serves the x86 nodes of a mixed
# cluster). It is the BUILD PHASE -- not the manifest -- that avoids rebuilding them off amd64
# (build_one_codename skips an Architecture:all package on non-amd64; see the
# control_binary_arch test below).
for my $t (@targets) {
for my $boot (qw(syslinux-xcat grub2-xcat elilo-xcat xnba-undi)) {
for my $boot (qw(syslinux-xcat grub2-xcat elilo-xcat xnba-undi ipxe-xcat)) {
ok(exists $m{$t}{$boot}, "$boot required-present on $t (arch:all, verified on every arch)");
}
}
@@ -407,6 +373,17 @@ SKIP: {
ok(!index_has_native_arch(undef, 'amd64'), 'undef index text -> not built (no crash)');
}
# ipxe-xcat is built once on amd64 like the other boot components. Its control file must declare
# Architecture: all, or every other arch would rebuild it.
{
open my $fh, '<', "$FindBin::Bin/../ipxe-xcat/debian/control" or die "ipxe-xcat/debian/control: $!";
my $ctl = do { local $/; <$fh> };
close $fh;
is(control_binary_arch($ctl, 'ipxe-xcat'), 'all', 'ipxe-xcat is Architecture:all');
ok(!skip_arch_all_on($ctl, 'ipxe-xcat', 'amd64'), 'ipxe-xcat is built on amd64');
ok(skip_arch_all_on($ctl, 'ipxe-xcat', $_), "ipxe-xcat is not rebuilt on $_") for qw(ppc64el riscv64);
}
# ---- control_binary_arch: PURE Architecture lookup for a specific BINARY package in debian/control --
# Drives build_one_codename's "skip arch:all on non-amd64" (single-producer) decision. Must pick the
# right binary paragraph -- e.g. the syslinux SOURCE is 'any' but the syslinux-xcat subpackage is 'all'.
@@ -741,6 +718,7 @@ STUB
'grub2-xcat' => 'grub2-xcat',
'elilo-xcat' => 'elilo',
'xnba-undi' => 'xnba',
'ipxe-xcat' => 'ipxe-xcat',
);
my %manifest = read_manifest("$root/debs-manifest.conf");
my %seen;