2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-08-03 16:06:59 +00:00

fix(buildrpms): stage xCAT-probe helper modules before target builds

xcatprobe subcommands (xcatmn, discovery, osdeploy) and hierarchy.pm load
xCAT:: perl modules but only add $XCATROOT/probe/lib/perl to @INC, not the
main $XCATROOT/lib/perl where perl-xCAT ships them. The RPM builder stopped
embedding those helpers, so /opt/xcat/probe/lib/perl/xCAT/ was absent on
installed systems and the subcommands were reported "Unsupported sub command".

Stage GlobalDef.pm, NetworkUtils.pm and ServiceNodeUtils.pm into a temporary
xCAT-probe tree and build its source tarball once, before the parallel target
builds fork, so workers only read a complete archive. Add package-payload
regression tests. Adopts the approach from upstream PR #7581.

The remaining buildrpms.pl changes are deployment-compliance refactoring
(signed, self-contained core repo output).

Closes #7579

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
(cherry picked from commit 0c3c44f28b)
This commit is contained in:
Daniel Hilst
2026-07-09 21:07:14 -03:00
committed by Vinícius Ferrão
parent c94c9ef6b5
commit 432ae0fd53
4 changed files with 275 additions and 29 deletions
+135 -29
View File
@@ -19,7 +19,7 @@ sub install_deps {
esac
dnf install -y perl-generators https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm
dnf install -y \$(/usr/lib/rpm/perl.req $0)
dnf install -y tar mock nginx createrepo podman rpmdevtools rpm-sign
dnf install -y tar mock nginx createrepo_c podman rpmdevtools rpm-sign
systemctl enable --now nginx
@@ -40,6 +40,7 @@ use Data::Dumper;
use File::Copy qw(cp);
use File::Path qw(make_path remove_tree);
use File::Slurper qw(read_text write_text);
use File::Temp qw(tempdir tempfile);
use FindBin qw($Bin);
use Getopt::Long qw(GetOptions);
use POSIX qw(strftime);
@@ -58,6 +59,11 @@ my $SOURCES = "$ENV{HOME}/rpmbuild/SOURCES";
system('mkdir', '-p', map { "$ENV{HOME}/rpmbuild/$_" } qw(SOURCES SPECS BUILD BUILDROOT RPMS SRPMS));
my $VERSION = read_text("Version");
my $PWD = Cwd::cwd();
my @XCAT_PROBE_HELPERS = qw(
GlobalDef.pm
NetworkUtils.pm
ServiceNodeUtils.pm
);
chomp($VERSION);
@@ -141,7 +147,7 @@ my %opts = (
configure_nginx => 0,
force => 0,
gpg_home => "",
gpg_key_name => "xCAT Automatic Signing Key",
gpg_key_name => "xCAT Signing Key",
gpg_sign => 0,
help => 0,
mock_uniqueext => "",
@@ -173,6 +179,7 @@ GetOptions(
"verbose" => \$opts{verbose},
"xcat_dep_path=s" => \$opts{xcat_dep_path},
"setup_local_repos" => \$opts{setup_local_repos},
"finalize-core=s" => \$opts{finalize_core},
) or usage();
# --package REPLACES the default set (build exactly what was asked), so
@@ -314,6 +321,38 @@ sub buildsources_genesis_base($) {
remove_tree($staging_parent);
}
sub prepare_xcat_probe_source_tar {
my $staging_parent = tempdir("xcat-probe-source.XXXXXX", TMPDIR => 1, CLEANUP => 1);
my $staging_root = "$staging_parent/xCAT-probe";
my $helper_dir = "$staging_root/lib/perl/xCAT";
my $source_tarball = "$SOURCES/xCAT-probe-$VERSION.tar.gz";
sh(qq(cp -a "xCAT-probe" "$staging_root"))
and die "Error staging xCAT-probe sources";
remove_tree($helper_dir) if -e $helper_dir;
make_path($helper_dir);
chmod 0755, $helper_dir;
for my $helper (@XCAT_PROBE_HELPERS) {
my $destination = "$helper_dir/$helper";
cp "perl-xCAT/xCAT/$helper", $destination;
chmod 0644, $destination;
}
my ($archive_fh, $archive_path) = tempfile(
".xCAT-probe-$VERSION.XXXXXX",
DIR => $SOURCES,
UNLINK => 1,
);
close $archive_fh;
sh(qq(tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="\@$SOURCE_DATE_EPOCH" --use-compress-program="gzip -n" -cf "$archive_path" -C "$staging_parent" xCAT-probe))
and die "Error creating $source_tarball";
chmod 0644, $archive_path;
rename $archive_path, $source_tarball;
}
sub buildsources {
my ($pkg, $target) = @_;
@@ -349,6 +388,9 @@ EOF
EOF
# xCATsn.spec consumes templates from xCAT shared templates payload.
sh qq(tar --sort=name --owner=0 --group=0 --mtime="\@$SOURCE_DATE_EPOCH" -czf "$SOURCES/templates.tar.gz" xCAT/templates) unless -f "$SOURCES/templates.tar.gz";
} elsif ($pkg eq "xCAT-probe") {
# Prepared once before target builds fork so workers only read a complete archive.
return;
} else {
sh qq(tar --sort=name --owner=0 --group=0 --mtime="\@$SOURCE_DATE_EPOCH" -czf "$SOURCES/$pkg-$VERSION.tar.gz" $pkg);
}
@@ -364,8 +406,8 @@ sub buildspkgs {
my $diskcache = (
$pkg eq 'xCAT-genesis-scripts' || $pkg eq 'xCAT-genesis-base'
) ? "dist/$target/srpms/$pkg-$genesis_tarch-$VERSION-$RELEASE.src.rpm"
: "dist/$target/srpms/$pkg-$VERSION-$RELEASE.src.rpm";
) ? "dist/$target/rpms/SRPMS/$pkg-$genesis_tarch-$VERSION-$RELEASE.src.rpm"
: "dist/$target/rpms/SRPMS/$pkg-$VERSION-$RELEASE.src.rpm";
return if -f $diskcache and not $opts{force};
my $dir = sub {
@@ -393,7 +435,7 @@ mock -r $chroot \\
--buildsrpm \\
--spec $dir/$pkg.spec \\
--sources $SOURCES \\
--resultdir "dist/$target/srpms/"
--resultdir "dist/$target/rpms/SRPMS/"
EOF
}
@@ -448,7 +490,7 @@ mock -r $chroot \\
--define "clamp_mtime_to_source_date_epoch 1" \\
--define "_buildhost xcat-build" \\
--resultdir "dist/$target/rpms/" \\
--rebuild dist/$target/srpms/$spkgname
--rebuild dist/$target/rpms/SRPMS/$spkgname
EOF
}
@@ -567,40 +609,79 @@ sub setup_local_repos {
}
# Index one repo dir with deterministic, upstream-matching metadata. createrepo_c's
# defaults already emit primary/filelists/other as *.xml.zst plus *.sqlite.bz2
# (--database), exactly the upstream shape; --set-timestamp-to-revision pins the
# repomd timestamp to SOURCE_DATE_EPOCH.
sub createrepo_dir {
my ($dir, $extra) = @_;
$extra //= '';
sh(qq(createrepo_c --update --database )
. qq(--revision "$SOURCE_DATE_EPOCH" --set-timestamp-to-revision $extra "$dir"))
and die "Failed to createrepo_c $dir\n";
}
# A core repo dir holds binaries flat plus a SRPMS/ subdir carrying its own
# repodata (the upstream xcat.org layout). mock --rebuild re-emits the .src.rpm
# into the binary resultdir, but the canonical copy lives in SRPMS/, so drop the
# top-level strays; then index the binaries EXCLUDING the SRPMS/ subdir so no
# src.rpm enters the binary repomd, and index the SRPMS repo separately.
sub index_repo {
my ($repodir) = @_;
say "Creating repository $repodir";
# Drop the top-level stray src.rpm and the mock logs (build.log/root.log/...)
# that mock leaves in the resultdir, so the dir is directly deployable (upstream
# ships neither). The canonical src.rpm lives in SRPMS/.
unlink($_) for glob("$repodir/*.src.rpm"), glob("$repodir/*.log"),
glob("$repodir/SRPMS/*.log");
createrepo_dir($repodir, "--excludes 'SRPMS/*' --excludes '*.src.rpm'");
createrepo_dir("$repodir/SRPMS") if -d "$repodir/SRPMS";
}
sub update_repo {
my ($target) = @_;
say "Creating repository dist/$target/rpms";
`find dist/$target/rpms -name ".src.rpm" -delete`;
`createrepo --update dist/$target/rpms`;
index_repo("dist/$target/rpms");
}
sub sign_rpms {
my ($target) = @_;
my $key_name = $opts{gpg_key_name};
my $repodir = "dist/$target/rpms";
sign_repo_dir("dist/$target/rpms", $opts{gpg_key_name});
}
# Sign every rpm in a core repo dir -- the top-level binaries AND SRPMS/*.src.rpm --
# then re-index (signing rewrites the rpms, invalidating checksums) and detach-sign
# + export the key into BOTH the binary and the SRPMS repodata dirs.
sub sign_repo_dir {
my ($repodir, $key_name) = @_;
say "Signing RPMs in $repodir";
my @rpms = glob("$repodir/*.rpm");
if (@rpms) {
my $rpm_list = join " ", map { qq("$_") } @rpms;
sh(qq(rpmsign --define "%_gpg_name $key_name" --addsign $rpm_list))
my @bin = glob("$repodir/*.rpm");
if (@bin) {
sh(qq(rpmsign --define "%_gpg_name $key_name" --addsign )
. join(" ", map { qq("$_") } @bin))
and die "Failed to sign RPMs in $repodir";
}
my @src = glob("$repodir/SRPMS/*.src.rpm");
if (@src) {
sh(qq(rpmsign --define "%_gpg_name $key_name" --addsign )
. join(" ", map { qq("$_") } @src))
and die "Failed to sign SRPMs in $repodir/SRPMS";
}
# rpmsign --addsign rewrites the rpm files, so the checksums recorded by the
# earlier createrepo no longer match and dnf rejects them. Regenerate the repo
# metadata now (after signing, before signing repomd.xml) so it stays consistent.
say "Regenerating repo metadata after signing $repodir";
sh(qq(createrepo --update "$repodir"))
and die "Failed to regenerate repo metadata after signing";
# Regenerate both indexes (binary + SRPMS) after signing, before signing repomd.
index_repo($repodir);
say "Signing repomd.xml for $target";
my $repomd = "$repodir/repodata/repomd.xml";
unlink "$repomd.asc" if -f "$repomd.asc";
sh(qq(gpg -a --detach-sign --default-key "$key_name" "$repomd"))
and die "Failed to sign $repomd";
sh(qq(gpg -a --export "$key_name" > "$repomd.key"))
and die "Failed to export public key";
for my $rd ("$repodir/repodata",
(-d "$repodir/SRPMS/repodata" ? ("$repodir/SRPMS/repodata") : ())) {
my $repomd = "$rd/repomd.xml";
next unless -f $repomd;
say "Signing $repomd";
unlink "$repomd.asc" if -f "$repomd.asc";
sh(qq(gpg -a --detach-sign --default-key "$key_name" "$repomd"))
and die "Failed to sign $repomd";
sh(qq(gpg -a --export "$key_name" > "$rd/repomd.xml.key"))
and die "Failed to export public key to $rd";
}
}
# Emit the deployable repo metadata into dist/$target/rpms: xcat-core.repo,
@@ -609,7 +690,11 @@ sub sign_rpms {
# cluster-test.pl to re-collect / re-createrepo the dist output.
sub write_repo_metadata {
my ($target) = @_;
my $repodir = "dist/$target/rpms";
write_repo_metadata_dir("dist/$target/rpms");
}
sub write_repo_metadata_dir {
my ($repodir) = @_;
return unless -d $repodir;
# Shipped baseurl points at xcat.org; mklocalrepo.sh rewrites baseurl/gpgkey to
@@ -665,6 +750,23 @@ COMMIT_ID_LONG=$GITINFO
EOF
}
# Turn an already-populated core dir into a signed repo in the upstream xcat.org
# layout, reusing the same index/sign/metadata code as a per-target build. Used to
# assemble the flat MULTI-ARCH core: the caller rsyncs each arch's dist/<t>/rpms/
# (excluding repodata/) into <dir> first, then this does the single final
# createrepo_c + repomd signing so no packages are moved by hand.
sub finalize_core {
my $dir = $opts{finalize_core};
die "FATAL: --finalize-core dir '$dir' does not exist\n" unless -d $dir;
index_repo($dir);
if ($opts{gpg_sign}) {
$ENV{GNUPGHOME} = $opts{gpg_home} if $opts{gpg_home};
sign_repo_dir($dir, $opts{gpg_key_name});
}
write_repo_metadata_dir($dir);
return 0;
}
sub main {
usage(verbose => 2, exitval => 0) if $opts{help};
my $mode = repo_mode();
@@ -673,6 +775,10 @@ sub main {
return exit(configure_nginx()) if $opts{configure_nginx};
return exit(setup_local_repos()) if $opts{setup_local_repos};
return exit(finalize_core()) if $opts{finalize_core};
prepare_xcat_probe_source_tar()
if grep { $_ eq "xCAT-probe" } $opts{packages}->@*;
my @rpms = product($opts{packages}, $opts{targets});
my $pm = Parallel::ForkManager->new($opts{nproc});
@@ -1,3 +1,4 @@
setup_vm
#INCLUDE:rhels_x86_daily.bundle#
xcatprobe_work
clean_up_env
@@ -5,6 +5,8 @@ cmd:mkdir -p /tmp/xcatprobe_l
cmd:xcatprobe -l
check:rc==0
check:output=~Supported sub commands are:
cmd:for module in GlobalDef.pm NetworkUtils.pm ServiceNodeUtils.pm; do test -r "/opt/xcat/probe/lib/perl/xCAT/$module" || exit 1; done
check:rc==0
cmd:xcatprobe -l|grep -v "Supported sub commands are" |awk '/^[[:graph:]]/ {print $1}'|sort > /tmp/xcatprobe_l/subcmd_from_xcatprobe_l
cmd:ls -l /opt/xcat/probe/subcmds/ |awk '/^-/ {print $9}'|sort > /tmp/xcatprobe_l/subcmd_under_subcmds_dir
cmd:diff -y /tmp/xcatprobe_l/subcmd_from_xcatprobe_l /tmp/xcatprobe_l/subcmd_under_subcmds_dir
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Copy qw(copy);
use File::Path qw(make_path);
use File::Spec;
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
my $repo_root = File::Spec->rel2abs(File::Spec->catdir($FindBin::Bin, '..', '..'));
my @helpers = qw(
GlobalDef.pm
NetworkUtils.pm
ServiceNodeUtils.pm
);
my @affected_subcommands = qw(
code_template
discovery
osdeploy
xcatmn
);
my $builder = read_file('buildrpms.pl');
like($builder, qr/sub prepare_xcat_probe_source_tar\b/, 'RPM builder has dedicated xCAT-probe source preparation');
like(
$builder,
qr/for my \$helper \(\@XCAT_PROBE_HELPERS\).*?cp "perl-xCAT\/xCAT\/\$helper", \$destination;/s,
'RPM builder copies every declared helper into the staged package tree'
);
like($builder, qr/tempfile\(.*?DIR\s*=>\s*\$SOURCES/s, 'RPM builder writes a unique archive in the source directory');
like($builder, qr/--use-compress-program="gzip -n"/, 'RPM builder normalizes gzip metadata');
like($builder, qr/rename\s+\$archive_path,\s*\$source_tarball/, 'RPM builder publishes the source archive atomically');
like(
$builder,
qr/elsif \(\$pkg eq "xCAT-probe"\)\s*\{.*?\breturn;/s,
'target workers reuse the source archive prepared before the fork'
);
my $prepare_call = rindex($builder, 'prepare_xcat_probe_source_tar()');
my $worker_fanout = index($builder, 'Parallel::ForkManager->new');
ok(
$prepare_call >= 0 && $worker_fanout >= 0 && $prepare_call < $worker_fanout,
'xCAT-probe source preparation runs before worker processes fork'
);
for my $helper (@helpers) {
my $source = File::Spec->catfile($repo_root, 'perl-xCAT', 'xCAT', $helper);
ok(-f $source, "$helper source exists");
like($builder, qr/^\s*\Q$helper\E\s*$/m, "RPM builder stages $helper");
}
my $tmpdir = tempdir(CLEANUP => 1);
my $xcatroot = File::Spec->catdir($tmpdir, 'opt', 'xcat');
my $probe_root = File::Spec->catdir($xcatroot, 'probe');
my $bin_dir = File::Spec->catdir($xcatroot, 'bin');
my $subcmd_dir = File::Spec->catdir($probe_root, 'subcmds');
my $helper_dir = File::Spec->catdir($probe_root, 'lib', 'perl', 'xCAT');
make_path($probe_root, $bin_dir);
copy_tree(File::Spec->catdir($repo_root, 'xCAT-probe', 'lib'), File::Spec->catdir($probe_root, 'lib'));
copy_tree(File::Spec->catdir($repo_root, 'xCAT-probe', 'subcmds'), $subcmd_dir);
my $xcatprobe_source = File::Spec->catfile($repo_root, 'xCAT-probe', 'xcatprobe');
my $xcatprobe = File::Spec->catfile($bin_dir, 'xcatprobe');
copy($xcatprobe_source, $xcatprobe) or die "copy $xcatprobe_source: $!";
chmod 0755, $xcatprobe or die "chmod $xcatprobe: $!";
make_path($helper_dir, File::Spec->catdir($subcmd_dir, 'bin'));
for my $helper (@helpers) {
my $source = File::Spec->catfile($repo_root, 'perl-xCAT', 'xCAT', $helper);
my $destination = File::Spec->catfile($helper_dir, $helper);
copy($source, $destination) or die "copy $source: $!";
chmod 0644, $destination or die "chmod $destination: $!";
}
my $xcatclient = File::Spec->catfile($bin_dir, 'xcatclient');
write_file($xcatclient, "#!/bin/sh\nprintf '[ok]:dummy xcatclient\\n'\n");
chmod 0755, $xcatclient or die "chmod $xcatclient: $!";
local $ENV{XCATROOT} = $xcatroot;
local $ENV{PATH} = "$bin_dir:$ENV{PATH}";
local $ENV{PERL5LIB};
local $ENV{PERL5OPT};
local $ENV{PERLLIB};
delete $ENV{PERL5LIB};
delete $ENV{PERL5OPT};
delete $ENV{PERLLIB};
for my $subcommand (@affected_subcommands) {
my $command = File::Spec->catfile($subcmd_dir, $subcommand);
my ($rc, $output) = run_command($command, '-T');
is($rc, 0, "$subcommand self-test exits successfully") or diag($output);
like($output, qr/^\[ok\]\s*:/m, "$subcommand self-test reports ready");
}
my ($list_rc, $list_output) = run_command($xcatprobe, '-l');
is($list_rc, 0, 'xcatprobe list exits successfully') or diag($list_output);
my %listed = map { /^([^\s].*?)\s/ ? ($1 => 1) : () } split /\n/, $list_output;
for my $subcommand (@affected_subcommands) {
ok($listed{$subcommand}, "xcatprobe lists $subcommand") or diag($list_output);
}
done_testing();
sub read_file {
my ($file) = @_;
my $path = File::Spec->catfile($repo_root, $file);
open(my $fh, '<', $path) or die "open $path: $!";
my $contents = do { local $/; <$fh> };
close($fh) or die "close $path: $!";
return $contents;
}
sub copy_tree {
my ($source, $destination) = @_;
my $rc = system('cp', '-R', $source, $destination);
is($rc, 0, "copied $source into the package fixture")
or BAIL_OUT("unable to create package fixture from $source");
}
sub run_command {
my (@command) = @_;
open(my $fh, '-|', @command) or die "run @command: $!";
my $output = do { local $/; <$fh> };
close($fh);
return ($? >> 8, $output // '');
}
sub write_file {
my ($path, $contents) = @_;
open(my $fh, '>', $path) or die "open $path: $!";
print {$fh} $contents;
close($fh) or die "close $path: $!";
}