2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-21 07:08:12 +00:00

Merge master into fix/remove-xcat-genesis-builder

Master merged #7835, which changed the Genesis base files under
xCAT-genesis-builder/, the directory this branch removes. The merge
keeps master's content at this branch's paths:

- GenesisBuildRoot.pm, genesis_payload_verification.t and
  genesis_ubuntu_build_root.t take master's newer versions, pointed at
  xCAT-genesis-base/.
- verify-genesis-payload and GenesisPayload.pm are identical on both
  sides; the copies at the old path are dropped.
- buildrpms.pl keeps stage_genesis_base_sources(), which already stages
  what master added inline.
- builddebs.pl and the new genesis_deb_per_codename.t now use
  xCAT-genesis-base/.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
Daniel Hilst
2026-09-24 22:16:30 -03:00
7 changed files with 701 additions and 73 deletions
+134
View File
@@ -36,6 +36,9 @@ our @EXPORT_OK = qw(
rewrite_file write_script read_line
buildinfo_text
targetarch_from_target
genesis_chroot_name genesis_target_arch genesis_build_plan
genesis_log_errors genesis_log_deny_rules deb_belongs_to_dist
genesis_dists genesis_dist_reason
);
# Both builders echo the commands they run under --verbose. Set once, after
@@ -740,4 +743,135 @@ sub targetarch_from_target {
return $parts[-1];
}
# ------------------------------------------------------------------ Genesis --
#
# The Genesis image carries the kernel and the kernel modules of the release it boots,
# because dracut copies them out of the root it runs in. So the Ubuntu Genesis deb is built
# once per Ubuntu codename, inside that codename's chroot. A single build on the build host
# gives every codename the build host's kernel.
# genesis_chroot_name: the schroot chroot that builds the Genesis deb for one codename.
#
# Same name xcat-dep's sbuild-all.pl ensure_chroots creates, so both repositories build in
# the same chroots and neither has to bootstrap a second set.
sub genesis_chroot_name {
my ($codename, $arch) = @_;
die "genesis_chroot_name: a codename is required\n"
unless defined $codename && length $codename;
die "genesis_chroot_name: an architecture is required\n"
unless defined $arch && length $arch;
return "$codename-$arch-sbuild";
}
# genesis_target_arch: the directory xCAT reads the Genesis image from, for a deb
# architecture. mknb reads /opt/xcat/share/xcat/netboot/genesis/<arch>, and that name is
# the rpm architecture, not the deb one.
my %GENESIS_TARGET_ARCH = (
amd64 => 'x86_64',
ppc64el => 'ppc64',
);
sub genesis_target_arch {
my ($arch) = @_;
my $target = $GENESIS_TARGET_ARCH{ $arch // '' };
die "genesis_target_arch: no Genesis image directory for '" . ($arch // '') . "'\n"
unless $target;
return $target;
}
# genesis_build_plan: one Genesis build per codename, for one architecture.
#
# The set of codenames comes from the caller, so a pipeline builds exactly the releases it
# publishes.
sub genesis_build_plan {
my ($dists, $arch) = @_;
my @dists = @{ $dists || [] };
die "genesis_build_plan: at least one codename is required\n" unless @dists;
die "genesis_build_plan: an architecture is required\n"
unless defined $arch && length $arch;
my %seen;
return map {
{
codename => $_,
arch => $arch,
chroot => genesis_chroot_name($_, $arch),
package => "xcat-genesis-base-$arch",
target => genesis_target_arch($arch),
}
} grep { !$seen{$_}++ } @dists;
}
# genesis_log_deny_rules / genesis_log_errors: what a Genesis build log says when the build
# failed but the exit status did not.
#
# dracut reports a command it cannot install with a FAILED: line and returns 0. apt has the
# same shape: a missing package leaves a diagnostic and a zero status behind a `|| true`. So
# the log is the gate, not the exit status.
my @GENESIS_LOG_DENY = (
[ qr/\bFAILED:/ => 'dracut could not install a command' ],
[ qr/Cannot find module/ => 'a kernel module the build names is absent' ],
[ qr/dracut: Cannot/ => 'dracut refused the request' ],
[ qr/command not found/ => 'the build root has no such command' ],
[ qr/E: Unable to locate package/ => 'apt has no such package' ],
[ qr/Unable to correct problems/ => 'apt could not resolve the build root' ],
);
sub genesis_log_deny_rules { return @GENESIS_LOG_DENY; }
# The package built once per codename. It is the only one: it carries the kernel and the kernel
# modules of the root that built it.
my $GENESIS_IMAGE_DEB = qr{\Axcat-genesis-base-};
# Releases whose stock chroot cannot build that package, and why. focal ships debhelper 12 and
# xCAT-genesis-base declares debhelper-compat (= 13), so sbuild stops on the build dependencies
# before dracut runs.
my %GENESIS_DIST_UNSUPPORTED = (
focal => 'debhelper 12 cannot satisfy debhelper-compat (= 13)',
);
# genesis_dists: the releases of @dists a Genesis image can be built on.
sub genesis_dists {
my (@dists) = @_;
return grep { !exists $GENESIS_DIST_UNSUPPORTED{$_} } @dists;
}
# genesis_dist_reason: why a release was left out, or undef.
sub genesis_dist_reason { return $GENESIS_DIST_UNSUPPORTED{ $_[0] // '' }; }
# deb_belongs_to_dist: whether a built .deb may be published into one release.
#
# Almost every xcat-core deb is Architecture:all and the same file serves every release, so the
# answer is yes. The Genesis image is not: it is built per codename and carries that codename in
# its version (2.19.0-snap...~noble). Publishing all three into every suite lets apt serve the
# newest, which is the image of another release.
#
# Only that package is asked. A ~ in a version is Debian's prerelease separator before it is
# anything else, and --release takes whatever the caller gives it, so reading every ~ as a
# codename drops a whole `--release 1~rc1` build from every suite.
sub deb_belongs_to_dist {
my ($deb, $dist) = @_;
return 1 unless defined $deb && defined $dist && $dist ne '';
my $base = basename($deb);
my ($name) = $base =~ /\A([^_]+)_/;
return 1 unless defined $name && $name =~ $GENESIS_IMAGE_DEB;
return 1 unless $base =~ /_[^_]*~([A-Za-z0-9.]+)_[^_]*\.deb\z/;
return $1 eq $dist ? 1 : 0;
}
sub genesis_log_errors {
my ($text) = @_;
return () unless defined $text && length $text;
my @found;
for my $line (split /\n/, $text) {
for my $rule (@GENESIS_LOG_DENY) {
my ($pattern, $why) = @{$rule};
next unless $line =~ $pattern;
push @found, { line => $line, why => $why };
last;
}
}
return @found;
}
1;
+202 -13
View File
@@ -8,9 +8,10 @@
# The central fact this design rests on: xcat-core debs are Perl. They are byte-identical
# for every Ubuntu release, so they are built ONCE and the same files are published into
# every codename. Only xCAT, xCATsn and xCAT-genesis-scripts carry an architecture, and
# even there the difference is packaging metadata, not compiled output. That is why this
# needs no sbuild and no per-codename chroot -- unlike xcat-dep, whose compiled packages
# genuinely differ per release.
# even there the difference is packaging metadata, not compiled output.
#
# The Genesis image is the one exception. --genesis builds it once per codename, inside
# that codename's sbuild chroot.
use strict;
use warnings;
use feature 'say';
@@ -38,6 +39,8 @@ use XCAT::BuildUtils qw(
reprepro_distributions reprepro_options
lock_id_for take_build_lock sh_quote
sh sh_or_die usage rewrite_file write_script read_line buildinfo_text
genesis_build_plan genesis_log_errors deb_belongs_to_dist
genesis_dists genesis_dist_reason
);
# The xcat-core packages that ship as debs. xCAT-openbmc-py, xCAT-rmc and xCAT-release
@@ -61,10 +64,14 @@ my @PACKAGES = qw(
my @DISTS = default_dists();
my %opts;
my (@cli_packages, @cli_dists);
my (@cli_packages, @cli_dists, @cli_genesis_dists);
GetOptions(
"dist=s@" => \@cli_dists,
"package=s@" => \@cli_packages,
"genesis" => \$opts{genesis},
"genesis-only" => \$opts{genesis_only},
"genesis-dist=s@" => \@cli_genesis_dists,
"genesis-arch=s" => \$opts{genesis_arch},
"dest=s" => \$opts{dest},
"builddir=s" => \$opts{builddir},
"release=s" => \$opts{release},
@@ -83,6 +90,23 @@ $opts{packages} = @cli_packages ? \@cli_packages : \@PACKAGES;
$opts{dists} = @cli_dists ? \@cli_dists : \@DISTS;
$opts{gpg_key_name} //= 'xCAT Signing Key';
# The Genesis step is off unless it is asked for, so today's runs keep their behaviour.
$opts{genesis} = 1 if $opts{genesis_only};
# A release named on the command line is built as asked. The default list is the one the rest
# of the build uses, and not every release on it can build the image, so those are dropped and
# named rather than failing the run on its first codename.
if (@cli_genesis_dists) {
$opts{genesis_dists} = \@cli_genesis_dists;
} else {
$opts{genesis_dists} = [ genesis_dists($opts{dists}->@*) ];
for my $dist ($opts{dists}->@*) {
my $why = genesis_dist_reason($dist) or next;
say "genesis: leaving out $dist -- $why";
}
}
die "FATAL: --genesis-dist needs --genesis\n" if @cli_genesis_dists && !$opts{genesis};
die "FATAL: --genesis-arch needs --genesis\n" if $opts{genesis_arch} && !$opts{genesis};
for my $pkg ($opts{packages}->@*) {
die "FATAL: unknown package '$pkg'. Known: @PACKAGES\n"
unless grep { $_ eq $pkg } @PACKAGES;
@@ -268,6 +292,132 @@ sub collect_debs {
return $moved;
}
# ----------------------------------------------------------- the Genesis deb --
#
# dracut copies the kernel, the kernel modules and every command out of the root it runs in,
# so the Genesis image belongs to the release that built it. One build on the build host
# serves every codename with the build host's kernel. This step builds one image per
# codename, inside that codename's sbuild chroot.
#
# The chroots are the ones xcat-dep's sbuild-all.pl creates on the Ubuntu build host
# (<codename>-<arch>-sbuild). A session is a disposable overlay, so the next codename starts
# from the pristine base.
# Where the build runs inside the chroot. A directory of its own at the chroot root, because
# every other candidate is shared: /build and /opt/xcat-ci-shared are bind mounts the sbuild
# chroots give to every session.
my $GENESIS_STAGE = '/xcat-genesis-build';
sub host_deb_arch {
my $arch = `dpkg --print-architecture 2>/dev/null` // '';
chomp $arch;
die "FATAL: dpkg does not report a host architecture\n" unless $arch;
return $arch;
}
# begin_chroot_session: start a disposable schroot session and return its id and its root.
sub begin_chroot_session {
my ($chroot) = @_;
my $id = `schroot --begin-session --chroot @{[ sh_quote($chroot) ]} 2>&1` // '';
my $rc = $? >> 8;
chomp $id;
die "FATAL: cannot start a session in chroot '$chroot' (exit $rc): $id\n"
. " sbuild-all.pl ensure_chroots creates it; run it on this host first.\n"
if $rc != 0 || $id !~ /\A\S+\z/;
my $root = `schroot --location -c @{[ sh_quote("session:$id") ]} 2>/dev/null` // '';
chomp $root;
unless ($root && -d $root) {
sh("schroot --end-session -c " . sh_quote("session:$id") . " >/dev/null 2>&1");
die "FATAL: schroot reports no location for session:$id\n";
}
return ($id, $root);
}
# genesis_build_log_problems: what the log says went wrong when the exit status did not.
#
# Report the first few offending lines only, so a build console stays readable. The message
# names the log file that has the rest.
sub genesis_build_log_problems {
my ($logfile) = @_;
my $text = -f $logfile ? read_text($logfile) : '';
my @errors = genesis_log_errors($text);
return '' unless @errors;
my $shown = @errors > 10 ? 10 : scalar @errors;
my $report = "FATAL: the Genesis build log reports " . scalar(@errors) . " error(s):\n";
$report .= " $_->{line}\n ($_->{why})\n" for @errors[0 .. $shown - 1];
$report .= " ... " . (@errors - $shown) . " more\n" if @errors > $shown;
$report .= " the whole log is at $logfile\n";
return $report;
}
sub build_one_genesis_deb {
my ($step, $pkgdir) = @_;
my ($codename, $chroot) = ($step->{codename}, $step->{chroot});
say "Building $step->{package} for $codename in $chroot";
my ($id, $root) = begin_chroot_session($chroot);
my $logfile = "$pkgdir/$step->{package}-$codename.buildlog";
my $err;
eval {
# Copy the builder in rather than bind-mount the checkout: the build rewrites
# debian/control and debian/changelog, and it must not rewrite them in the tree the
# pipeline builds from.
my $stage = "$root$GENESIS_STAGE";
sh_or_die("rm -rf " . sh_quote($stage) . " && mkdir -p "
. sh_quote("$stage/xCAT-genesis-base"),
"FATAL: cannot make the build directory in session:$id\n");
sh_or_die("cp -a " . sh_quote("$ROOT/xCAT-genesis-base") . "/. "
. sh_quote("$stage/xCAT-genesis-base") . "/",
"FATAL: cannot copy xCAT-genesis-base into session:$id\n");
copy("$ROOT/Version", "$stage/Version")
or die "FATAL: cannot copy Version into session:$id: $!\n";
write_text("$stage/Release", "$RELEASE\n");
# --expect-codename is the guard that keeps the image and the root together: the
# builder stops when the root it woke up in is not the release it was asked for.
my $cmd = join ' ',
'schroot', '--run-session', '-c', sh_quote("session:$id"), '-u', 'root', '-d', '/',
'--', '/bin/bash', "$GENESIS_STAGE/xCAT-genesis-base/builddeb-genesis-base",
'--expect-codename', sh_quote($codename), '--outdir', "$GENESIS_STAGE/out";
my $rc = sh("$cmd > " . sh_quote($logfile) . " 2>&1");
# The log is read whether or not the command failed: dracut exits 0 with FAILED:
# lines in its log.
my $problems = genesis_build_log_problems($logfile);
if ($rc != 0) {
die "FATAL: the Genesis build for $codename failed (exit $rc); log: $logfile\n"
. $problems;
}
die $problems if $problems;
my @debs = glob("$root$GENESIS_STAGE/out/*.deb");
die "FATAL: the Genesis build for $codename produced no .deb; log: $logfile\n"
unless @debs;
for my $deb (@debs) {
my $dest = "$pkgdir/" . basename($deb);
copy($deb, $dest) or die "FATAL: cannot collect $deb: $!\n";
say " $dest";
}
1;
} or $err = $@;
sh("schroot --end-session -c " . sh_quote("session:$id") . " >/dev/null 2>&1");
die $err if $err;
return;
}
sub build_genesis_debs {
my ($pkgdir) = @_;
my $arch = $opts{genesis_arch} || host_deb_arch();
my @plan = genesis_build_plan($opts{genesis_dists}, $arch);
say "Genesis: @{[ scalar @plan ]} build(s) for $arch: "
. join(' ', map { $_->{codename} } @plan);
build_one_genesis_deb($_, $pkgdir) for @plan;
return scalar @plan;
}
# ------------------------------------------------------------- apt assembly --
sub gpg_key_id {
my ($name) = @_;
@@ -304,6 +454,9 @@ sub assemble_repo {
for my $deb (@debs) {
# A release that predates an architecture must not be handed its packages.
next if basename($deb) =~ /_(\w+)\.deb\z/ && $1 ne 'all' && !$ok{$1};
# Nor an image built for another release: the Genesis deb carries the codename it
# was built on, because it carries that release's kernel.
next unless deb_belongs_to_dist($deb, $dist);
sh_or_die("cd " . sh_quote($repodir) . " && reprepro -b ./ includedeb "
. sh_quote($dist) . ' ' . sh_quote($deb),
"FATAL: reprepro could not add $deb to $dist\n");
@@ -369,16 +522,25 @@ unlink glob("$ROOT/*.deb"), glob("$ROOT/*.buildinfo"), glob("$ROOT/*.changes"),
say "xcat-core $PKGVER -> $dest";
say "releases: @{[ join ' ', $opts{dists}->@* ]}";
for my $pkg ($opts{packages}->@*) {
for my $arch (deb_package_arches($pkg)) {
build_package($pkg, $arch, $pkgdir);
unless ($opts{genesis_only}) {
for my $pkg ($opts{packages}->@*) {
for my $arch (deb_package_arches($pkg)) {
build_package($pkg, $arch, $pkgdir);
}
collect_debs($pkg, $pkgdir);
}
collect_debs($pkg, $pkgdir);
}
my $count = assemble_repo($pkgdir, $repo);
write_repo_metadata($repo);
say "published $count package(s) into @{[ scalar $opts{dists}->@* ]} release(s) at $repo";
build_genesis_debs($pkgdir) if $opts{genesis};
if ($opts{genesis_only}) {
say "Genesis debs are in $pkgdir";
}
else {
my $count = assemble_repo($pkgdir, $repo);
write_repo_metadata($repo);
say "published $count package(s) into @{[ scalar $opts{dists}->@* ]} release(s) at $repo";
}
__END__
@@ -399,8 +561,15 @@ xcat-core packages are Perl. The same binary serves every Ubuntu release, so eac
package is built B<once> and the resulting C<.deb> files are published into every
codename the repository declares. Only C<xCAT>, C<xCATsn> and C<xCAT-genesis-scripts>
carry an architecture, and there the difference is packaging metadata rather than
compiled output. Consequently this builder needs no C<sbuild> and no per-codename
chroot. (xcat-dep is different: its packages are compiled, so it builds per codename.)
compiled output.
C<xcat-genesis-base> is the exception. dracut copies the kernel, the kernel modules
and every command out of the root it runs in, so the Genesis image belongs to the
release that built it. With C<--genesis> this builder makes one image per codename,
each inside that codename's C<< <codename>-<arch>-sbuild >> schroot -- the chroots
xcat-dep's C<sbuild-all.pl> creates on the Ubuntu build host. The build refuses to run
in a root of another release, and it reads its own log: dracut reports a command it
cannot install with a C<FAILED:> line and still exits 0.
Replaced C<build-ubunturepo>, removed in 2.19. The GSA upload paths, the C<PROMOTE>/C<PREGA> release
flows and the C<-d> xcat-dep repository mode were not carried over: publishing is done
@@ -418,6 +587,25 @@ Publish into this release. Repeatable. Defaults to focal, jammy, noble and resol
Build only this package. Repeatable. Defaults to every xcat-core deb package.
=item B<--genesis>
Also build C<xcat-genesis-base-E<lt>archE<gt>>, one C<.deb> per codename, each inside
that codename's schroot. Off by default.
=item B<--genesis-only>
Build the Genesis debs and nothing else, and assemble no repository. This is what
xcat-dep needs: it consumes the debs with C<sbuild-all.pl --genesis-deb>.
=item B<--genesis-dist>=I<CODENAME>
Build the Genesis image for this release. Repeatable. Defaults to the C<--dist> list.
=item B<--genesis-arch>=I<ARCH>
Build the Genesis image for this Debian architecture. Defaults to the architecture of
the build host, because the chroot has to match it.
=item B<--dest>=I<DIR>
Write the build under this directory: packages in C<debs/>, the apt repository in
@@ -459,5 +647,6 @@ This message.
./builddebs.pl
./builddebs.pl --dist noble --package perl-xCAT
./builddebs.pl --dest /srv/out --gpg-sign --gpg-home /keys/xcat-gpg-home
./builddebs.pl --genesis-only --genesis-dist jammy --genesis-dist noble --dest /srv/out
=cut
@@ -63,6 +63,20 @@ ppc64el, and every release the repository serves declares the architecture;
``xCAT-genesis-scripts`` keeps the two architectures it has control files for,
because riscv64 Genesis ships as an OpenEmbedded package instead.
The Genesis image is the exception, and it is off unless it is asked for::
./builddebs.pl --genesis-only --genesis-dist jammy --genesis-dist noble
``dracut`` copies the kernel, the kernel modules and every command out of the root
it runs in, so the image belongs to the release that built it. ``--genesis`` builds
one image per ``--genesis-dist`` codename inside that codename's
``<codename>-<arch>-sbuild`` schroot -- the chroots xcat-dep's ``sbuild-all.pl``
creates on the Ubuntu build host -- for the architecture of the build host. It
refuses to run in a root of another release, it fails the build on an error in the
log even when the exit status is 0, and it checks the extracted payload against the
commands the dracut module installs. ``--genesis-only`` builds the images and
nothing else, which is what xcat-dep consumes with ``--genesis-deb``.
Helpers shared by both builders live in ``build-utils/lib/XCAT/BuildUtils.pm``.
``buildcore.sh`` builds the architecture specific packages (``xCAT``, ``xCATsn``,
+103 -14
View File
@@ -1,13 +1,35 @@
#!/bin/bash
# Build xcat-genesis-base .deb package natively on Ubuntu.
# Must run as root on an Ubuntu system (22.04, 24.04, or 26.04).
# Parallel to buildrpm for EL targets.
# Build the xcat-genesis-base .deb for ONE Ubuntu codename.
#
# dracut copies the kernel, the kernel modules and every command out of the root it runs
# in, so this must run inside a root of the target codename. builddebs.pl --genesis starts
# it in that codename's <codename>-<arch>-sbuild chroot, one build per codename.
# --expect-codename is what stops a run on the build host: a single build there gives every
# Ubuntu release the build host's kernel.
#
# Parallel to xCAT-genesis-base.spec, which does the same for EL.
set -euo pipefail
DIR=$(readlink -f "$(dirname "$0")")
OS_RELEASE=${OS_RELEASE:-/etc/os-release}
expect_codename=""
outdir=""
while [ $# -gt 0 ]; do
case "$1" in
--expect-codename) expect_codename=${2:-}; shift 2 ;;
--expect-codename=*) expect_codename=${1#*=}; shift ;;
--outdir) outdir=${2:-}; shift 2 ;;
--outdir=*) outdir=${1#*=}; shift ;;
-h|--help)
echo "usage: builddeb-genesis-base [--expect-codename <codename>] [--outdir <dir>]"
exit 0 ;;
*) echo "ERROR: unknown argument: $1" >&2; exit 2 ;;
esac
done
BUILDARCH=$(dpkg --print-architecture)
TRIPLET=$(dpkg-architecture -qDEB_HOST_MULTIARCH)
case "$BUILDARCH" in
amd64) TARCH=x86_64 ;;
@@ -31,7 +53,20 @@ rewrite_control() {
VERSION=$(cat "$DIR/../Version" 2>/dev/null || echo "2.18.0")
RELEASE=$(cat "$DIR/../Release" 2>/dev/null || echo "snap$(date +%Y%m%d%H%M)")
CODENAME=$(. /etc/os-release && echo "$VERSION_CODENAME")
CODENAME=$(. "$OS_RELEASE" && echo "${VERSION_CODENAME:-}")
if [ -z "$CODENAME" ]; then
echo "ERROR: $OS_RELEASE names no VERSION_CODENAME" >&2
exit 1
fi
# The image is only valid for the release whose kernel it carries. Refuse to build a
# codename's image anywhere but in that codename's root.
if [ -n "$expect_codename" ] && [ "$expect_codename" != "$CODENAME" ]; then
echo "ERROR: this root is $CODENAME, not $expect_codename." >&2
echo " The image would carry the $CODENAME kernel and boot under $expect_codename." >&2
exit 1
fi
echo "Building xcat-genesis-base for $BUILDARCH ($TARCH) on Ubuntu $CODENAME"
@@ -45,6 +80,18 @@ REQUIRED_PACKAGES=$(perl -I"$DIR/lib" -MXCAT::GenesisBuildRoot=required_packages
-e 'print "$_\n" for required_packages(@ARGV)' "$BUILDARCH" "$CODENAME")
apt-get install -y --no-install-recommends $REQUIRED_PACKAGES
# dpkg-architecture comes from dpkg-dev, which the line above installs.
TRIPLET=$(dpkg-architecture -qDEB_HOST_MULTIARCH)
# ncurses-base put its terminfo entries under /lib/terminfo before 24.04, and both dracut's own
# terminfo module and the 97xcat module ask for /usr/share/terminfo. On jammy the build stopped
# on /usr/share/terminfo/l/linux. Give this build root the path they ask for; the chroot is
# thrown away when the build ends.
if [ -d /lib/terminfo ]; then
mkdir -p /usr/share/terminfo
cp -an /lib/terminfo/. /usr/share/terminfo/ 2>/dev/null || true
fi
# Set up dracut module
if [ -d /usr/lib/dracut/modules.d ]; then
DRACUT_PARENT=/usr/lib/dracut/modules.d
@@ -76,6 +123,8 @@ if [ "$BUILDARCH" != "amd64" ]; then
sed -i '/efibootmgr dmidecode/d' "$DRACUTMODDIR/module-setup.sh"
fi
# linux-image-generic pulls exactly one kernel into this root, and this root belongs to
# $CODENAME, so this is the target release's kernel.
KERNELVERSION=$(ls -1 /lib/modules | sort -V | tail -n 1)
if [ -z "$KERNELVERSION" ]; then
echo "ERROR: no kernel modules found in /lib/modules" >&2
@@ -128,24 +177,53 @@ if [ ! -e "$KERNEL_IMAGE" ]; then
for candidate in \
"/boot/vmlinux-$KERNELVERSION" \
"/usr/lib/modules/$KERNELVERSION/vmlinuz" \
"/lib/modules/$KERNELVERSION/vmlinuz" \
"$(find /usr/lib/modules/"$KERNELVERSION" -maxdepth 2 -name 'vmlinuz*' -o -name 'vmlinux*' 2>/dev/null | head -n 1)" \
"$(find /lib/modules/"$KERNELVERSION" -maxdepth 2 -name 'vmlinuz*' -o -name 'vmlinux*' 2>/dev/null | head -n 1)" \
"$(ls -1 /boot/vmlinuz-* /boot/vmlinux-* 2>/dev/null | sort -V | tail -n 1)"
"/lib/modules/$KERNELVERSION/vmlinuz"
do
if [ -n "$candidate" ] && [ -e "$candidate" ]; then
if [ -e "$candidate" ]; then
KERNEL_IMAGE="$candidate"
break
fi
done
fi
if [ ! -e "$KERNEL_IMAGE" ]; then
echo "ERROR: cannot find kernel image" >&2
echo "ERROR: cannot find the image of kernel $KERNELVERSION" >&2
exit 1
fi
echo "Adding kernel $KERNEL_IMAGE"
cp "$KERNEL_IMAGE" "$GENESIS_ROOT/kernel"
# dracut_install reports a missing command and returns, so a hole reaches the .deb with
# nothing in the log but one line. Read the commands back from the module and check them
# against the payload, the way xCAT-genesis-base.spec does for EL.
bash "$DIR/verify-genesis-payload" --commands-from "$DRACUTMODDIR/module-setup.sh" "$GENESIS_FS" \
usr/sbin/dhclient bin/sh sbin/xcatroot sbin/dhclient-script etc/rsyslog.conf
# What the verifier cannot know: that this image belongs to this chroot's kernel. An image
# built against another root's /lib/modules boots and then finds no driver for its NIC.
if [ ! -d "$GENESIS_FS/lib/modules/$KERNELVERSION" ]; then
echo "ERROR: the image carries no /lib/modules/$KERNELVERSION" >&2
ls -1 "$GENESIS_FS/lib/modules" 2>/dev/null >&2 || true
exit 1
fi
if [ -z "$(find "$GENESIS_FS/lib/modules/$KERNELVERSION" -name '*.ko*' -print -quit)" ]; then
echo "ERROR: /lib/modules/$KERNELVERSION in the image holds no kernel module" >&2
exit 1
fi
for stray in "$GENESIS_FS"/lib/modules/*; do
[ -d "$stray" ] || continue
if [ "$(basename "$stray")" != "$KERNELVERSION" ]; then
echo "ERROR: the image carries $(basename "$stray"), not the $KERNELVERSION of this root" >&2
exit 1
fi
done
# The 97xcat hooks are what makes the image xCAT's. dracut drops a module whose check()
# fails without a word, and the image then boots to a plain dracut shell.
if [ -z "$(find "$GENESIS_FS" -path '*/hooks/cmdline/*xcat-cmdline.sh' -print -quit)" ]; then
echo "ERROR: the image carries no 97xcat cmdline hook" >&2
exit 1
fi
find "$GENESIS_TMPDIR" -type c -delete
# Stage for dpkg-buildpackage
@@ -154,6 +232,11 @@ cp -a "$GENESIS_TMPDIR/opt" "$DIR/"
# Adjust control file for target arch
rewrite_control "$DIR/debian/control" "$BUILDARCH"
# debian/dirs names the image directory, which is the rpm architecture.
echo "/opt/xcat/share/xcat/netboot/genesis/$TARCH/" > "$DIR/debian/dirs"
# dch reads debian/control from the current directory, not from the file it writes.
cd "$DIR"
PKG_VERSION="${VERSION}-${RELEASE}~${CODENAME}"
rm -f "$DIR/debian/changelog"
@@ -163,8 +246,14 @@ dch --create --package "xcat-genesis-base-$BUILDARCH" \
"Native Ubuntu build on $CODENAME $BUILDARCH"
echo "Building .deb package..."
cd "$DIR"
dpkg-buildpackage -rfakeroot -uc -us -b
echo "Build complete. .deb files:"
ls -la "$DIR/../"xcat-genesis-base*.deb 2>/dev/null || echo "Check parent directory for .deb files"
if [ -n "$outdir" ]; then
mkdir -p "$outdir"
mv "$DIR/../"xcat-genesis-base-*_*.deb "$outdir/"
echo "Build complete. .deb files in $outdir:"
ls -la "$outdir"
else
echo "Build complete. .deb files:"
ls -la "$DIR/../"xcat-genesis-base*.deb 2>/dev/null || echo "Check parent directory for .deb files"
fi
+22 -7
View File
@@ -16,13 +16,13 @@ my @BASE_PACKAGES = qw(
dosfstools e2fsprogs lvm2 mdadm net-tools
bc psmisc rsync wget cpio
isc-dhcp-client ifenslave
systemd-sysv hwdata btrfs-progs netcat-openbsd iputils-ping fdisk ncurses-term
dpkg-dev debhelper fakeroot devscripts vim-tiny
);
# hwclock moved out of util-linux into util-linux-extra. focal and jammy have no such package,
# and naming it there fails the whole install. util-linux only Suggests it, and the build
# passes --no-install-recommends, so the releases that split it must name it.
my @OPTIONAL_PACKAGES = qw(util-linux-extra);
# Two commands the image needs changed package between releases: nslookup left dnsutils for
# bind9-dnsutils in 22.04, and hwclock left util-linux for util-linux-extra in 23.04.
my @RENAMED_PACKAGES = ([qw(bind9-dnsutils dnsutils)], [qw(util-linux-extra util-linux)]);
#-------------------------------------------------------------------------------
@@ -48,14 +48,18 @@ sub apt_carries {
=head3 required_packages
Descriptions: list the packages the Genesis build root needs.
An optional package is listed only when the release carries it.
For a renamed package, apt says which name the release carries, so the list does
not branch on the codename. 26.04 moved the backward-compatibility zone names that
the dracut module installs out of tzdata into tzdata-legacy.
Arguments:
$arch: the dpkg architecture (amd64, ppc64el)
$codename: the release name
$codename: the release name, used in the error message
$carries: optional code ref. It takes a package name and returns true when the
release carries that package. The default is apt_carries.
Returns:
the package names, in install order.
Dies with "ERROR: <codename> carries none of these packages: ..." when the release
carries neither name of a renamed package.
=cut
@@ -66,7 +70,18 @@ sub required_packages {
my @packages = @BASE_PACKAGES;
push @packages, qw(dmidecode efibootmgr) if $arch eq 'amd64';
push @packages, grep { $carries->($_) } @OPTIONAL_PACKAGES;
RENAMED:
for my $alternatives (@RENAMED_PACKAGES) {
for my $package (@$alternatives) {
if ($carries->($package)) {
push @packages, $package;
next RENAMED;
}
}
die "ERROR: $codename carries none of these packages: @$alternatives\n";
}
push @packages, 'tzdata-legacy' if $carries->('tzdata-legacy');
return @packages;
}
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env perl
# The Genesis image carries the kernel and the kernel modules of the root that built it, so
# builddebs.pl --genesis builds one image per codename, in that codename's chroot. Each
# assertion here reads the value the code returns.
use strict;
use warnings;
use File::Path qw(make_path);
use File::Slurper qw(read_text write_text);
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/../../build-utils/lib";
use Test::More;
use XCAT::Test::File qw(repo_path);
require XCAT::BuildUtils;
my @WANTED = qw(genesis_chroot_name genesis_target_arch genesis_build_plan genesis_log_errors);
for my $sub (@WANTED) {
ok(XCAT::BuildUtils->can($sub), "XCAT::BuildUtils provides $sub");
}
unless (scalar(grep { XCAT::BuildUtils->can($_) } @WANTED) == scalar @WANTED) {
diag('builddebs.pl has no Genesis step: the per-codename build does not exist yet');
done_testing();
exit;
}
# --- one build per codename, in that codename's chroot ---------------------------------
my @dists = qw(jammy noble resolute);
my @plan = XCAT::BuildUtils::genesis_build_plan(\@dists, 'amd64');
is(scalar @plan, scalar @dists, 'one Genesis build per codename');
is_deeply([ map { $_->{codename} } @plan ], \@dists, 'the plan keeps the codename order');
is_deeply([ map { $_->{chroot} } @plan ],
[ 'jammy-amd64-sbuild', 'noble-amd64-sbuild', 'resolute-amd64-sbuild' ],
'each codename builds in its own sbuild chroot');
is_deeply([ map { $_->{package} } @plan ],
[ ('xcat-genesis-base-amd64') x 3 ],
'every codename produces the same package name');
my @ppc = XCAT::BuildUtils::genesis_build_plan(['noble'], 'ppc64el');
is($ppc[0]{chroot}, 'noble-ppc64el-sbuild', 'the architecture selects the chroot');
is($ppc[0]{package}, 'xcat-genesis-base-ppc64el', 'the architecture is in the package name');
is($ppc[0]{target}, 'ppc64', 'ppc64el reads its image from the ppc64 directory');
is(XCAT::BuildUtils::genesis_target_arch('amd64'), 'x86_64',
'amd64 reads its image from the x86_64 directory');
is(scalar(() = XCAT::BuildUtils::genesis_build_plan([qw(noble noble)], 'amd64')), 1,
'a repeated codename does not build twice');
ok(!eval { XCAT::BuildUtils::genesis_build_plan([], 'amd64'); 1 },
'a plan with no codename is an error');
ok(!eval { XCAT::BuildUtils::genesis_target_arch('riscv64'); 1 },
'an architecture with no Genesis image directory is an error');
# --- a per-codename image reaches only its own suite --------------------------------------
ok(XCAT::BuildUtils->can('deb_belongs_to_dist'),
'XCAT::BuildUtils decides which suite a deb belongs to');
if (XCAT::BuildUtils->can('deb_belongs_to_dist')) {
my $noble = 'xcat-genesis-base-amd64_2.19.0-snap202609121200~noble_all.deb';
ok(XCAT::BuildUtils::deb_belongs_to_dist($noble, 'noble'),
'the noble image is published into noble');
ok(!XCAT::BuildUtils::deb_belongs_to_dist($noble, 'jammy'),
'the noble image is not published into jammy');
# Everything else in xcat-core is the same file for every release.
ok(XCAT::BuildUtils::deb_belongs_to_dist('perl-xcat_2.19.0-snap1_all.deb', 'jammy'),
'a deb with no codename in its version reaches every suite');
ok(XCAT::BuildUtils::deb_belongs_to_dist('xcat_2.19.0-snap1_amd64.deb', 'resolute'),
'an architecture deb reaches every suite');
# A `~` in a version is not a codename. Debian uses it for a prerelease, and --release
# takes whatever the caller gives it, so `--release 1~rc1` puts one in every package
# name. Reading it as a codename drops the whole build from every suite, and the run
# still reports the packages it published.
for my $dist (qw(focal jammy noble resolute)) {
ok(XCAT::BuildUtils::deb_belongs_to_dist('xcat-client_2.19.0-1~rc1_all.deb', $dist),
"a prerelease version reaches $dist");
}
ok(XCAT::BuildUtils::deb_belongs_to_dist('xcat-genesis-scripts-amd64_2.19.0-1~rc1_amd64.deb', 'noble'),
'a prerelease Genesis SCRIPTS deb reaches every suite: only the image is per codename');
# And the rule the exclusion exists for still holds under a prerelease version.
ok(XCAT::BuildUtils::deb_belongs_to_dist('xcat-genesis-base-amd64_2.19.0-1~rc1~noble_amd64.deb', 'noble'),
'a prerelease Genesis image reaches its own suite');
ok(!XCAT::BuildUtils::deb_belongs_to_dist('xcat-genesis-base-amd64_2.19.0-1~rc1~noble_amd64.deb', 'jammy'),
'and no other');
}
# --- the releases a Genesis image can be built on ------------------------------------------
# A plain --genesis run takes the release list the rest of the build uses. focal is on it and
# cannot build the package: it ships debhelper 12.10 and xCAT-genesis-base declares
# debhelper-compat (= 13), so sbuild stops on the build dependencies before dracut runs and
# the run ends on its first release.
ok(XCAT::BuildUtils->can('genesis_dists'),
'XCAT::BuildUtils says which releases a Genesis image can be built on');
if (XCAT::BuildUtils->can('genesis_dists')) {
is_deeply([ XCAT::BuildUtils::genesis_dists(XCAT::BuildUtils::default_dists()) ],
[qw(jammy noble resolute)],
'the default plan leaves out the release whose chroot cannot build the package');
is_deeply([ XCAT::BuildUtils::genesis_dists(qw(jammy noble)) ], [qw(jammy noble)],
'a list with none of them is unchanged');
is_deeply([ XCAT::BuildUtils::genesis_dists('focal') ], [],
'a list of only that release plans nothing');
}
# --- the log guard ---------------------------------------------------------------------
#
# dracut prints FAILED: for a command it cannot install and exits 0.
my $dracut_log = <<'LOG';
Installing build dependencies...
dracut: Executing: /usr/bin/dracut --compress gzip -m xcat base -N -f /tmp/genesis.rfs 6.8.0-45-generic
dracut-install: ERROR: installing 'dhclient'
dracut: FAILED: /usr/lib/dracut/dracut-install -D /var/tmp/dracut.XXXX -a dhclient
dracut: *** Creating initramfs image file '/tmp/genesis.rfs' done ***
Extracting initramfs...
LOG
my @errors = XCAT::BuildUtils::genesis_log_errors($dracut_log);
ok(scalar @errors, 'a dracut log with a FAILED: line is an error');
like($errors[0]{line}, qr/FAILED:/, 'the offending line is reported');
ok(length $errors[0]{why}, 'the reason is named');
is_deeply([ XCAT::BuildUtils::genesis_log_errors(<<'LOG') ], [], 'a clean build log is not an error');
Installing build dependencies...
dracut: *** Creating initramfs image file '/tmp/genesis.rfs' done ***
Extracting initramfs...
dpkg-deb: building package 'xcat-genesis-base-amd64'
LOG
for my $case (
[ 'E: Unable to locate package isc-dhcp-client' => 'a package apt cannot find' ],
[ 'dracut: Cannot find module directory /lib/modules/6.8.0' => 'a module directory dracut cannot find' ],
[ '/build/builddeb-genesis-base: line 9: dch: command not found' => 'a command the build root lacks' ],
[ 'E: Unable to correct problems, you have held broken packages.' => 'a build root apt cannot resolve' ],
)
{
my ($line, $what) = @{$case};
ok(scalar XCAT::BuildUtils::genesis_log_errors("before\n$line\nafter\n"),
"the log guard catches $what");
}
is_deeply([ XCAT::BuildUtils::genesis_log_errors(undef) ], [], 'no log is not an error');
# --- the builder refuses a root of another release -------------------------------------
my $builder = repo_path('xCAT-genesis-base/builddeb-genesis-base');
if (!-f $builder) {
fail('xCAT-genesis-base/builddeb-genesis-base is missing');
done_testing();
exit;
}
my $tmp = tempdir(CLEANUP => 1);
mkdir "$tmp/bin";
# dpkg is shadowed so the guard is tested on any host, and so the test cannot reach apt.
write_text("$tmp/bin/dpkg", "#!/bin/sh\necho amd64\n");
chmod 0755, "$tmp/bin/dpkg";
sub build_in_a_root_of {
my ($codename, $expected) = @_;
write_text("$tmp/os-release", "ID=ubuntu\nVERSION_CODENAME=$codename\n");
my $err = "$tmp/err";
my $cmd = sprintf('PATH=%s:$PATH OS_RELEASE=%s /bin/bash %s --expect-codename %s >/dev/null 2>%s',
"'$tmp/bin'", "'$tmp/os-release'", "'$builder'", "'$expected'", "'$err'");
system('/bin/bash', '-c', $cmd);
return ($? >> 8, -f $err ? read_text($err) : '');
}
my ($rc, $err) = build_in_a_root_of('jammy', 'noble');
isnt($rc, 0, 'the builder refuses to build noble in a jammy root');
like($err, qr/jammy/, 'the message names the root it woke up in');
like($err, qr/noble/, 'the message names the release that was asked for');
done_testing();
+51 -39
View File
@@ -5,10 +5,13 @@
#
# XCAT::GenesisBuildRoot::required_packages lists the packages of the build root, and
# XCAT::GenesisPayload::module_commands reads the mandatory commands from the module, as
# verify-genesis-payload does.
# verify-genesis-payload does. The refusal of a root of another release is in
# genesis_deb_per_codename.t.
use strict;
use warnings;
use File::Slurper qw(read_text);
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/../../xCAT-genesis-base/lib";
@@ -18,13 +21,16 @@ use XCAT::GenesisBuildRoot qw(required_packages);
use XCAT::GenesisPayload qw(module_commands);
use XCAT::Test::File qw(repo_path);
my $module = repo_path('xCAT-genesis-base/dracut_105/ubuntu/module-setup.sh');
my $builder = repo_path('xCAT-genesis-base/builddeb-genesis-base');
my $module = repo_path('xCAT-genesis-base/dracut_105/ubuntu/module-setup.sh');
# Mandatory commands a minimal Ubuntu server root does NOT already provide, and the package
# that supplies each one on every release xCAT builds for.
my %PACKAGE_FOR = (
dhclient => 'isc-dhcp-client',
ifenslave => 'ifenslave',
# Mandatory commands a minimal Ubuntu server root does NOT already provide, and the packages
# that supply each one. hwclock has two names: it left util-linux for util-linux-extra in
# 23.04.
my %PACKAGES_FOR = (
dhclient => ['isc-dhcp-client'],
ifenslave => ['ifenslave'],
hwclock => [ 'util-linux-extra', 'util-linux' ],
);
# A release carries exactly the names in its list.
@@ -32,48 +38,54 @@ sub release {
my %carried = map { $_ => 1 } @_;
return sub { $carried{ $_[0] } };
}
my $NOBLE = release(qw(bind9-dnsutils dnsutils util-linux-extra util-linux tzdata-legacy));
my $FOCAL = release(qw(dnsutils util-linux));
# hwclock is not in that list because the package that carries it moved. Measured on the
# four Ubuntu management nodes: focal and jammy have it in util-linux, which is essential
# and always in the build root, and no util-linux-extra exists to install; noble and
# resolute have it in util-linux-extra. util-linux only Suggests that package, and this
# build passes --no-install-recommends, so the releases that split it must name it and the
# releases that did not must not.
my @noble = required_packages('amd64', 'noble', release('util-linux-extra'));
my @jammy = required_packages('amd64', 'jammy', release());
my @noble = required_packages('amd64', 'noble', $NOBLE);
is_deeply([ @noble[ -5 .. -1 ] ],
[qw(dmidecode efibootmgr bind9-dnsutils util-linux-extra tzdata-legacy)],
'amd64 adds dmidecode and efibootmgr, then the first name the release carries');
my @focal = required_packages('amd64', 'focal', $FOCAL);
is_deeply([ @focal[ -2 .. -1 ] ], [qw(dnsutils util-linux)],
'a release without the new names gets dnsutils and util-linux, and no tzdata-legacy');
my @ppc = required_packages('ppc64el', 'noble', $NOBLE);
is_deeply([ @ppc[ -3 .. -1 ] ], [qw(bind9-dnsutils util-linux-extra tzdata-legacy)],
'ppc64el gets neither dmidecode nor efibootmgr');
is_deeply([ @ppc[ 0 .. $#ppc - 3 ] ], [ @noble[ 0 .. $#noble - 5 ] ],
'ppc64el and amd64 share the base packages');
my @asked;
required_packages('amd64', 'noble', sub { push @asked, $_[0]; $NOBLE->($_[0]) });
is_deeply(\@asked, [qw(bind9-dnsutils util-linux-extra tzdata-legacy)],
'apt is asked for the older name only when the newer one is absent');
ok(!eval { required_packages('amd64', 'oddball', release('bind9-dnsutils')); 1 },
'a release that carries no hwclock package fails');
is($@, "ERROR: oddball carries none of these packages: util-linux-extra util-linux\n",
'the failure names the release and the missing alternatives');
# --- every mandatory command has a package in the build root -----------------------------
# An absolute path is a data file, not a command.
my %mandatory = map { $_ => 1 } grep { !m{^/} } module_commands($module);
my %packages = map { $_ => 1 } @jammy;
for my $command (sort keys %PACKAGE_FOR) {
my %packages = map { $_ => 1 } @noble;
for my $command (sort keys %PACKAGES_FOR) {
my @provider = @{ $PACKAGES_FOR{$command} };
ok($mandatory{$command}, "the Ubuntu dracut module installs '$command' unconditionally");
ok($packages{ $PACKAGE_FOR{$command} },
"the build root installs $PACKAGE_FOR{$command}, which provides '$command'");
ok(scalar(grep { $packages{$_} } @provider),
"the build root installs @{[ join ' or ', @provider ]}, which provides '$command'");
}
ok(grep({ $_ eq 'util-linux' } @focal), 'a release before 23.04 gets hwclock from util-linux');
# doxcat asks dhclient for the provisioning lease.
ok($mandatory{dhclient} && $packages{'isc-dhcp-client'},
'the Genesis image can obtain a DHCP lease');
ok($mandatory{hwclock}, "the Ubuntu dracut module installs 'hwclock' unconditionally");
# Naming a package apt cannot locate fails the whole install, and the script runs under
# set -e, so an unconditional util-linux-extra stops the build on focal and jammy.
is_deeply([ grep { $_ eq 'util-linux-extra' } @jammy ], [],
'a release without util-linux-extra does not get it');
is_deeply([ @noble[ 0 .. $#noble - 1 ] ], \@jammy,
'a release that carries util-linux-extra gets the same list ...');
is($noble[-1], 'util-linux-extra', '... with util-linux-extra last');
my @ppc = required_packages('ppc64el', 'noble', release('util-linux-extra'));
is_deeply([ @noble[ -3 .. -1 ] ], [qw(dmidecode efibootmgr util-linux-extra)],
'amd64 adds dmidecode and efibootmgr');
is_deeply([ @ppc[ 0 .. $#ppc - 1 ] ], [ @noble[ 0 .. $#noble - 3 ] ],
'ppc64el gets neither dmidecode nor efibootmgr');
my @asked;
required_packages('amd64', 'noble', sub { push @asked, $_[0]; 1 });
is_deeply(\@asked, ['util-linux-extra'], 'apt is asked only about the optional package');
# The build cannot run here, so the call to the payload gate is read from the script. The
# gate itself is exercised by genesis_payload_verification.t.
like(read_text($builder),
qr{^bash "\$DIR/verify-genesis-payload" --commands-from "\$DRACUTMODDIR/module-setup\.sh" "\$GENESIS_FS"}m,
'builddeb-genesis-base verifies the payload it packages against the module');
done_testing();