2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-21 15:18:19 +00:00

Merge pull request #7906 from VersatusHPC/fix/openeuler-core-mock-resultdir-owner

fix(xcat-core): the openEuler ppc64le core build runs rpmbuild as root
This commit is contained in:
Daniel Hilst
2026-10-15 18:15:42 -03:00
committed by GitHub
13 changed files with 470 additions and 24 deletions
+12
View File
@@ -55,6 +55,18 @@ jobs:
- name: Check parsed package relations
if: ${{ !cancelled() && steps.package-deps.outcome == 'success' }}
run: prove xCAT-test/native/package_relations.t
# Ubuntu ships no mock, so the mock result directory test runs as root on Fedora.
- name: Run mock result directory test
run: |
set -o pipefail
docker run --rm -v "$PWD:/src:ro" -w /src fedora:42 bash -ec '
dnf -y -q install mock perl perl-File-Slurper >/dev/null
prove -v xCAT-test/native/mock_resultdir_owner.t \
xCAT-test/native/build_sources_other_uid.t' 2>&1 | tee mock.tap
if grep -iE '# skip' mock.tap; then
echo 'a mock result directory test was skipped' >&2
exit 1
fi
rpm_lifecycle_test:
runs-on: ubuntu-24.04
timeout-minutes: 30
+128 -14
View File
@@ -14,7 +14,9 @@ package XCAT::BuildUtils;
use strict;
use warnings;
use Exporter 'import';
use Cwd ();
use File::Copy qw(copy move);
use File::Find qw(find);
use File::Basename qw(basename);
use File::Path qw(make_path remove_tree);
use File::Temp qw(tempdir tempfile);
@@ -34,7 +36,7 @@ our @EXPORT_OK = qw(
reprepro_distributions reprepro_options
lock_id_for take_build_lock
build_sources_base build_sources_dir prepare_build_sources_dir
remove_build_sources_dir sweep_build_sources_dirs
remove_build_sources_dir sweep_build_sources_dirs share_build_sources_dir
sh_quote clean_debian_residue git_revision
backup_file restore_file
sh sh_or_die usage
@@ -42,7 +44,7 @@ our @EXPORT_OK = qw(
buildinfo_text
targetarch_from_target
openeuler_build_target openeuler_repo_subdir
mock_config_text
mock_config_text mock_build_owner prepare_mock_resultdirs
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
@@ -981,18 +983,19 @@ sub genesis_log_errors {
Descriptions: The directory that holds the per-process rpmbuild SOURCES directories.
Arguments:
$home - the home directory; defaults to $ENV{HOME}
$checkout - the source checkout; defaults to the working directory
Returns: the absolute path of the base directory.
=cut
#-------------------------------------------------------------------------------
# mock reads the sources as chrootuid, and $HOME of a root build is /root, mode 0550.
sub build_sources_base {
my ($home) = @_;
$home = $ENV{HOME} unless defined $home && length $home;
die "build_sources_base: no home directory\n" unless defined $home && length $home;
return "$home/rpmbuild/sources";
my ($checkout) = @_;
$checkout = Cwd::getcwd() unless defined $checkout && length $checkout;
die "build_sources_base: no checkout directory\n" unless defined $checkout && length $checkout;
return "$checkout/dist/sources";
}
#-------------------------------------------------------------------------------
@@ -1013,7 +1016,7 @@ Returns: the absolute path of the staging directory.
#-------------------------------------------------------------------------------
# Keyed by the process that stages. buildrpms.pl forks a child per package and target,
# and mock --sources copies the whole directory, so a shared directory lets a peer's tar
# truncate an archive mid-copy. A pid is unique only on one host, and $HOME can be on NFS.
# truncate an archive mid-copy. A pid is unique only on one host, and the checkout can be on NFS.
sub build_sources_dir {
my ($base, $pid, $host) = @_;
die "build_sources_dir: no base directory\n" unless defined $base && length $base;
@@ -1047,13 +1050,38 @@ sub prepare_build_sources_dir {
my $dir = build_sources_dir(@_);
# A directory with this name belongs to a dead process that had the same pid.
remove_tree($dir) if -e $dir;
make_path($dir);
chmod 0755, make_path($dir), $dir;
die "build_sources_dir: $dir was not created\n" unless -d $dir;
return $dir;
}
#-------------------------------------------------------------------------------
=head3 share_build_sources_dir
Descriptions: Make a staging directory and its contents readable by every user,
whatever the umask was when they were written.
Arguments:
$dir - the directory from prepare_build_sources_dir
Returns: nothing.
=cut
#-------------------------------------------------------------------------------
# mock copies --sources into the chroot as chrootuid, not as the uid that staged them.
sub share_build_sources_dir {
my ($dir) = @_;
find({ no_chdir => 1, wanted => sub {
return if -l $_;
my $mode = (lstat $_)[2] & 07777;
chmod $mode | (-d _ ? 0555 : 0444), $_;
} }, $dir);
}
#-------------------------------------------------------------------------------
=head3 remove_build_sources_dir
Descriptions: Delete the staging directory of one build process.
@@ -1153,11 +1181,97 @@ sub mock_config_text {
# that only surfaces later as a confusing MN install failure. 'simple' isolation is a plain
# chroot -- reliable for these RPM builds -- and sidesteps the nspawn cgroup race entirely.
$text .= "config_opts['isolation'] = 'simple'\n";
# mock creates --resultdir and its logs as chrootuid, and buildrpms.pl passes a relative
# one under a tree it owns as root. This assignment replaces any uid the target declares,
# so the in-chroot build user becomes uid 0 as well.
$text .= "config_opts['chrootuid'] = 0\n";
# chrootgid is left unset on purpose: groupadd for the in-chroot group fails on gid 0.
return $text;
}
#-------------------------------------------------------------------------------
=head3 mock_build_owner
Descriptions: Report the uid and gid mock builds as, from mock's own
configuration loader. The loader resolves include() and
the assignments around it as a build does.
Arguments:
$chroot - the name passed to mock -r
$configdir - the mock configuration directory; mock's default when undef
Returns: ($uid, $gid). With no chrootuid, $uid is the caller, as in mock:
as root, the user that SUDO_UID, USERHELPER_UID or PKEXEC_UID
names. $gid is undef when the loader reports none.
=cut
#-------------------------------------------------------------------------------
my $MOCK_OWNER_LOADER = <<'PYTHON';
import sys
import os
try:
from mockbuild.config import MOCKCONFDIR, load_config
from mockbuild.uid import setup_uid_manager
except ImportError:
sys.exit('mock is not installed: cannot import mockbuild.config')
args = (sys.argv[2] or MOCKCONFDIR, sys.argv[1])
if os.geteuid() == 0:
# As mock main(): load the configuration as the user that sudo names.
opts = setup_uid_manager().run_in_subprocess_without_privileges(load_config, *args)
else:
opts = load_config(*args)
print(opts['chrootuid'], opts.get('chrootgid', ''))
PYTHON
sub mock_build_owner {
my ($chroot, $configdir) = @_;
open my $pipe, '-|', 'python3', '-c', $MOCK_OWNER_LOADER, $chroot, $configdir // ''
or die "Cannot run python3 to load the mock configuration $chroot: $!\n";
my $out = do { local $/; <$pipe> } // '';
close $pipe or die "mock cannot load the configuration $chroot\n";
my ($uid, $gid) = $out =~ /\A(\d+) (\d*)\n\z/
or die "mock gives no chrootuid for $chroot\n";
return ($uid, length $gid ? $gid : undef);
}
#-------------------------------------------------------------------------------
=head3 prepare_mock_resultdirs
Descriptions: Give the --resultdir directories, and the files a previous
build left in them, to the uid and gid mock builds as.
mock writes its logs and packages there as chrootuid. A
root build that stopped part way leaves root-owned 0644
files that a non-root build cannot append to or replace.
Arguments:
$chroot - the name passed to mock -r
$configdir - the mock configuration directory; mock's default when undef
@dirs - the result directories, created when missing
Returns: nothing. Dies when a chown fails. The parent directories
that this sub creates get mode 0755.
=cut
#-------------------------------------------------------------------------------
sub prepare_mock_resultdirs {
my ($chroot, $configdir, @dirs) = @_;
my ($uid, $gid) = mock_build_owner($chroot, $configdir);
my %resultdir = map { $_ => 1 } @dirs;
# make_path applies the umask, and mock must pass through the new parents.
for my $parent (grep { !$resultdir{$_} } make_path(@dirs)) {
chmod 0755, $parent or die "Cannot make $parent traversable: $!\n";
}
return if $uid == $>;
$gid //= -1;
for my $dir (@dirs) {
opendir(my $dh, $dir) or die "Cannot read $dir: $!\n";
my @files = grep { -f $_ && !-l $_ } map { "$dir/$_" } readdir $dh;
closedir $dh;
for my $path ($dir, @files) {
chown($uid, $gid, $path) == 1
or die "FATAL: cannot give $path to uid $uid: $!\n";
}
# mock writes the packages as chrootuid:chrootgid.
chmod 0775, $dir;
}
}
+11 -1
View File
@@ -55,7 +55,7 @@ use File::Temp qw(tempdir tempfile);
use FindBin qw($Bin);
use lib "$Bin/build-utils/lib";
use XCAT::BuildUtils qw(git_revision source_date_epoch sh sh_or_die usage buildinfo_text
build_sources_base prepare_build_sources_dir remove_build_sources_dir
build_sources_base prepare_build_sources_dir remove_build_sources_dir share_build_sources_dir
sweep_build_sources_dirs stage_xcat_probe_sources stage_xcatsn_templates
stage_genesis_base_sources
write_script read_line targetarch_from_target
@@ -330,6 +330,14 @@ sub createmockconfig {
mock_config_text($pkg, "$target$ext", read_text("/etc/mock/$target.cfg"), $SOURCE_DATE_EPOCH));
}
# mock writes --resultdir as chrootuid, and this script runs as root.
sub prepare_mock_resultdirs {
my ($pkg, $target) = @_;
my $ext = $opts{mock_uniqueext} ? "-$opts{mock_uniqueext}" : "";
XCAT::BuildUtils::prepare_mock_resultdirs("$pkg-$target$ext", undef,
"dist/$target/rpms", "dist/$target/rpms/SRPMS");
}
sub buildsources_genesis_base($) {
stage_genesis_base_sources(".", "$SOURCES/xCAT-genesis-base-build-support.tar.bz2",
$SOURCE_DATE_EPOCH);
@@ -525,7 +533,9 @@ sub buildall {
# The parent deletes this directory when it reaps the child.
$SOURCES = prepare_build_sources_dir($SOURCES_BASE, $$);
createmockconfig($pkg, $target);
prepare_mock_resultdirs($pkg, $target);
buildsources($pkg, $target);
share_build_sources_dir($SOURCES);
buildspkgs($pkg, $target);
# --source-only stops here: buildspkgs has produced the src.rpm, and the binary
# rebuild is the only thing buildpkgs does. Everything upstream of this point --
+9
View File
@@ -112,6 +112,15 @@ sudo prove xCAT-test/native/ip_forwarding.t \
xCAT-test/native/syslog_openeuler*.t
```
The mock result directory case needs the mock Python library, and root for its
ownership checks. The staged sources case needs root to read the sources as uid
1000. The `xcat_test` workflow runs both as root in a Fedora container:
```
sudo prove xCAT-test/native/mock_resultdir_owner.t \
xCAT-test/native/build_sources_other_uid.t
```
The PostgreSQL case starts a private server with only a Unix socket. Run it as
an ordinary user with `XCAT_TEST_PG_BINDIR` set to the directory containing
`initdb`, `pg_ctl`, `postgres`, `psql` and `createdb`:
@@ -0,0 +1,50 @@
#!/usr/bin/env perl
# mock reads the staged sources as the uid of the target's chrootuid while buildrpms.pl
# runs as root. A process with another uid must read them. CI runs this file as root.
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../build-utils/lib";
use File::Path qw(make_path);
use File::Slurper qw(read_text write_text);
use File::Temp qw(tempdir);
use POSIX ();
use Test::More;
use XCAT::BuildUtils qw(build_sources_base prepare_build_sources_dir share_build_sources_dir);
plan skip_all => 'requires root to switch to another uid' if $> != 0;
# Flush before the fork below, or the child repeats this file's TAP output on exit.
$| = 1;
my $checkout = tempdir(CLEANUP => 1);
chmod 0755, $checkout or die "chmod $checkout: $!\n";
# A root $HOME the build uid cannot enter, as on the build hosts.
make_path("$checkout/root");
chmod 0550, "$checkout/root" or die "chmod: $!\n";
local $ENV{HOME} = "$checkout/root";
my $umask = umask 077;
my $dir = prepare_build_sources_dir(build_sources_base($checkout), $$, 'hosta');
make_path("$dir/sub");
write_text("$dir/xCAT-2.20.0.tar.gz", "payload\n");
write_text("$dir/sub/xcat.conf", "conf\n");
share_build_sources_dir($dir);
umask $umask;
my $pid = fork // die "fork: $!\n";
if ($pid == 0) {
POSIX::setgid(1000);
POSIX::setuid(1000);
POSIX::_exit(2) if $> != 1000;
my $ok = eval {
opendir(my $dh, $dir) or die;
read_text("$dir/xCAT-2.20.0.tar.gz") eq "payload\n"
&& read_text("$dir/sub/xcat.conf") eq "conf\n";
};
POSIX::_exit($ok ? 0 : 1);
}
waitpid($pid, 0);
is($? >> 8, 0, 'uid 1000 lists the staging directory and reads every staged file');
done_testing;
@@ -1,6 +1,8 @@
#!/bin/bash
set -eu
mount --make-rslave /
# mockbuild/ stands in for the mock Python package that the build loads.
export PYTHONPATH="$(dirname "$0")${PYTHONPATH:+:$PYTHONPATH}"
etc="$BUILD_FIXTURE/etc"
mkdir -p "$etc/mock" "$etc/yum.repos.d"
for file in passwd group nsswitch.conf ld.so.cache; do
@@ -0,0 +1,11 @@
# Stand-in for mock's configuration loader in the namespace build fixture.
# Without chrootuid, mock builds as the caller.
import os
MOCKCONFDIR = '/etc/mock'
def load_config(config_path, name):
if not os.path.isfile(os.path.join(config_path, name + '.cfg')):
raise SystemExit('no mock configuration ' + name)
return {'chrootuid': os.getuid()}
@@ -0,0 +1,8 @@
# Stand-in for mock's uid manager. The namespace has no sudo caller to drop to.
class _UidManager:
def run_in_subprocess_without_privileges(self, func, *args):
return func(*args)
def setup_uid_manager():
return _UidManager()
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env perl
# mock_build_owner must give the uid and gid mock itself builds as, so it runs mock's own
# loader. prepare_mock_resultdirs must leave nothing in a result directory that this uid
# cannot write. CI runs this file as root in a Fedora container.
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../build-utils/lib";
use File::Path qw(make_path);
use File::Slurper qw(write_text);
use File::Temp qw(tempdir);
use POSIX ();
use Test::More;
use XCAT::BuildUtils qw(mock_build_owner prepare_mock_resultdirs);
plan skip_all => 'requires the mock Python library'
if system('python3 -c "import mockbuild.config" >/dev/null 2>&1') != 0;
# Flush before the forks below, or each child repeats this file's TAP output on exit.
$| = 1;
# mock takes its build user from these. Each case below sets the ones it needs.
delete @ENV{qw(SUDO_UID SUDO_GID SUDO_USER USERHELPER_UID PKEXEC_UID)};
my $conf = tempdir(CLEANUP => 1);
# Under sudo, mock reads its configuration as the caller.
chmod 0755, $conf or die "Cannot make $conf readable: $!\n";
make_path("$conf/templates");
write_text("$conf/templates/owner.tpl",
"config_opts['chrootuid'] = 1000\nconfig_opts['chrootgid'] = 1000\n");
my %cfg = (
'inline-comment' => "config_opts['chrootuid'] = 1001 # build user\n"
. "config_opts['chrootgid'] = 1001 # build group\n",
'double-quoted' => qq(config_opts["chrootuid"] = 1001\nconfig_opts["chrootgid"] = 1001\n),
'included' => "include('templates/owner.tpl')\n",
'set-after-include' => "include('templates/owner.tpl')\n"
. "config_opts['chrootuid'] = 1001\nconfig_opts['chrootgid'] = 1001\n",
'include-after-set' => "config_opts['chrootuid'] = 1001\nconfig_opts['chrootgid'] = 1001\n"
. "include('templates/owner.tpl')\n",
'plain' => "config_opts['root'] = 'plain'\n",
);
write_text("$conf/$_.cfg", $cfg{$_}) for keys %cfg;
is_deeply([ mock_build_owner('inline-comment', $conf) ], [ 1001, 1001 ],
'an assignment with an inline comment counts');
is_deeply([ mock_build_owner('double-quoted', $conf) ], [ 1001, 1001 ],
'an assignment with a double-quoted key counts');
is_deeply([ mock_build_owner('included', $conf) ], [ 1000, 1000 ],
'an include resolves against the configuration directory');
is_deeply([ mock_build_owner('set-after-include', $conf) ], [ 1001, 1001 ],
'an assignment after an include overrides it');
is_deeply([ mock_build_owner('include-after-set', $conf) ], [ 1000, 1000 ],
'an include after an assignment overrides it');
my $mock_gid = (getgrnam 'mock')[2];
is_deeply([ mock_build_owner('plain', $conf) ], [ $<, $mock_gid ],
'with no chrootuid, mock builds as the caller and the mock group');
SKIP: {
skip 'giving files to uid 1000 requires root', 13 if $> != 0;
# A root build that stopped part way left root-owned 0644 logs and a source rpm.
my $tmp = tempdir(CLEANUP => 1);
chmod 0755, $tmp or die "Cannot make $tmp traversable: $!\n";
my @dirs = ("$tmp/dist/openeuler-24.03-ppc64le/rpms", "$tmp/dist/openeuler-24.03-ppc64le/rpms/SRPMS");
make_path(@dirs);
my @logs = map { my $d = $_; map { "$d/$_" } qw(build.log root.log state.log hw_info.log installed_pkgs.log) } @dirs;
my @stale = (@logs, "$dirs[1]/xCAT-2.19.1-1.src.rpm");
for my $file (@stale) {
write_text($file, "interrupted\n");
chmod 0644, $file;
}
prepare_mock_resultdirs('included', $conf, @dirs);
ok(writable_as(1000, 1000, $_), "uid 1000 can append to $_") for @stale[0 .. $#logs];
ok(writable_as(1000, 1000, $stale[-1]), 'uid 1000 can replace the stale source rpm');
ok(writable_as(1000, 1000, "$_/new.log"), "uid 1000 can create a file in $_") for @dirs;
}
SKIP: {
skip 'creating directories for uid 1000 requires root', 2 if $> != 0;
# A first build under umask 077: the helper creates every directory below the checkout.
my $tmp = tempdir(CLEANUP => 1);
chmod 0755, $tmp or die "Cannot make $tmp traversable: $!\n";
my @dirs = ("$tmp/dist/openeuler-24.03-ppc64le/rpms", "$tmp/dist/openeuler-24.03-ppc64le/rpms/SRPMS");
my $umask = umask 077;
prepare_mock_resultdirs('included', $conf, @dirs);
umask $umask;
ok(writable_as(1000, 1000, "$_/new.log"), "uid 1000 can create a file in the fresh $_") for @dirs;
}
SKIP: {
my $nobody = getpwnam 'nobody';
skip 'acting as the sudo caller requires root and a nobody user', 8
if $> != 0 || !defined $nobody;
# sudo mock with no chrootuid builds as SUDO_UID and the mock group.
local $ENV{SUDO_UID} = $nobody;
local $ENV{SUDO_GID} = (getpwnam 'nobody')[3];
local $ENV{SUDO_USER} = 'nobody';
is_deeply([ mock_build_owner('plain', $conf) ], [ $nobody, $mock_gid ],
'under sudo with no chrootuid, mock builds as the sudo caller and the mock group');
my $tmp = tempdir(CLEANUP => 1);
chmod 0755, $tmp or die "Cannot make $tmp traversable: $!\n";
my @dirs = ("$tmp/dist/openeuler-24.03sp4-x86_64/rpms", "$tmp/dist/openeuler-24.03sp4-x86_64/rpms/SRPMS");
make_path(@dirs);
my @logs = map { my $d = $_; map { "$d/$_" } qw(build.log root.log state.log) } @dirs;
for my $file (@logs) {
write_text($file, "interrupted\n");
chmod 0644, $file;
}
prepare_mock_resultdirs('plain', $conf, @dirs);
ok(writable_as($nobody, $mock_gid, $_), "the sudo caller can append to $_") for @logs;
ok(writable_as($nobody, $mock_gid, "$dirs[1]/new.log"), 'the sudo caller can create a file in SRPMS');
}
done_testing();
# Fork, take the uid and gid, and append one line to $file.
sub writable_as {
my ($uid, $gid, $file) = @_;
my $pid = fork // die "fork: $!\n";
if ($pid == 0) {
$) = "$gid $gid"; # drop root from the supplementary groups
POSIX::setgid($gid) or POSIX::_exit(2);
POSIX::setuid($uid) or POSIX::_exit(2);
open(my $fh, '>>', $file) or POSIX::_exit(1);
print {$fh} "retry\n" or POSIX::_exit(1);
close $fh or POSIX::_exit(1);
POSIX::_exit(0);
}
waitpid($pid, 0);
return $? == 0;
}
+65 -5
View File
@@ -1,11 +1,26 @@
#!/usr/bin/env perl
# mock creates --resultdir as chrootuid, and buildrpms.pl passes a relative one under a
# root-owned tree. These assertions read the RENDERED configuration, not the builder.
# mock creates --resultdir and its logs as chrootuid, and buildrpms.pl passes a relative
# one under a tree it owns as root. These assertions read the RENDERED configuration, and
# check which owner the result directories get. native/mock_resultdir_owner.t runs mock's
# loader and the real chown.
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../build-utils/lib";
use Test::More;
use File::Path qw(make_path);
use File::Slurper qw(write_text);
use File::Temp qw(tempdir);
# Record chown instead of doing it, so the test runs without root.
our @CHOWNED;
BEGIN {
*CORE::GLOBAL::chown = sub {
my ($uid, $gid, @files) = @_;
push @CHOWNED, map { [ $uid, $gid, $_ ] } @files;
return scalar @files;
};
}
use XCAT::BuildUtils qw(mock_config_text);
my $ppc = <<'CFG';
@@ -13,6 +28,7 @@ config_opts['root'] = 'openeuler-24.03-ppc64le'
config_opts['target_arch'] = 'ppc64le'
config_opts['chrootuid'] = 1000
config_opts['chrootgid'] = 1000
config_opts['useradd'] = '/usr/sbin/useradd -o -m -u {{chrootuid}} -g {{chrootgid}} -d {{chroothome}} -N {{chrootuser}}'
config_opts['use_bootstrap'] = False
CFG
@@ -33,12 +49,12 @@ sub effective {
}
my $out = mock_config_text('xCAT', 'openeuler-24.03-ppc64le', $ppc, 1790988617);
is(effective($out, 'chrootuid'), '0', 'the ppc64le core build writes its resultdir as root');
is(effective($out, 'chrootgid'), '1000', 'the group the target declared is left alone');
is(effective($out, 'chrootuid'), '1000', 'the build uid the target declares is preserved');
is(effective($out, 'chrootgid'), '1000', 'and so is the group');
is(effective($out, 'root'), '"xCAT-openeuler-24.03-ppc64le"', 'the chroot is still named per package');
my $plain = mock_config_text('xCAT', 'openeuler-24.03sp4-x86_64', $x86, 1790988617);
is(effective($plain, 'chrootuid'), '0', 'a target that declares no uid is pinned to root too');
is(effective($plain, 'chrootuid'), undef, 'a target that declares no uid is left to mock');
is(effective($plain, 'chrootgid'), undef, 'and no group is invented for it');
is(effective($plain, 'target_arch'), "'x86_64'", 'nothing else in the base configuration changes');
@@ -51,4 +67,48 @@ like(mock_config_text('perl-xCAT', 'openeuler-24.03-ppc64le', $ppc, 1),
unlike(mock_config_text('perl-xCAT', 'opensuse-leap-15.6-x86_64', $x86, 1),
qr/perl-generators/, 'and still does not on a SUSE target');
# prepare_mock_resultdirs gives the result directories, and the files a previous
# build left in them, to the owner mock's loader reports.
my $tmp = tempdir(CLEANUP => 1);
my @dirs = ("$tmp/rpms", "$tmp/rpms/SRPMS");
make_path(@dirs);
write_text("$_/build.log", "interrupted\n") for @dirs;
write_text("$tmp/rpms/SRPMS/xCAT-2.19.1-1.src.rpm", '');
my $chroot = 'xCAT-openeuler-24.03-ppc64le';
my (@asked, @owner);
no warnings 'redefine';
local *XCAT::BuildUtils::mock_build_owner = sub { push @asked, [@_]; return @owner };
use warnings 'redefine';
@owner = (1000, 135);
@CHOWNED = ();
XCAT::BuildUtils::prepare_mock_resultdirs($chroot, undef, @dirs);
is_deeply(\@asked, [ [ $chroot, undef ] ], 'the owner comes from the chroot mock builds with');
is_deeply([ sort { $a->[2] cmp $b->[2] } @CHOWNED ],
[ map { [ 1000, 135, $_ ] } sort(@dirs, "$tmp/rpms/build.log",
"$tmp/rpms/SRPMS/build.log", "$tmp/rpms/SRPMS/xCAT-2.19.1-1.src.rpm") ],
'both directories and the files left in them go to that uid and gid');
is((stat $_)[2] & 07777, 0775, "$_ is group-writable") for @dirs;
@owner = ($>, 135);
@CHOWNED = ();
XCAT::BuildUtils::prepare_mock_resultdirs($chroot, undef, "$tmp/caller");
is_deeply(\@CHOWNED, [], 'nothing changes owner when mock builds as the caller');
ok(-d "$tmp/caller", 'the directory is still created');
# A first build under umask 077. mock must reach the result directories through
# the parents the helper creates. The existing checkout keeps its mode.
my $fresh = tempdir(CLEANUP => 1);
my $fresh_mode = (stat $fresh)[2] & 07777;
my $target = "$fresh/dist/openeuler-24.03-ppc64le";
@owner = (1000, 135);
@CHOWNED = ();
my $umask = umask 077;
XCAT::BuildUtils::prepare_mock_resultdirs($chroot, undef, "$target/rpms", "$target/rpms/SRPMS");
umask $umask;
is((stat $_)[2] & 07777, 0755, "the new parent $_ is traversable")
for "$fresh/dist", $target;
is((stat $fresh)[2] & 07777, $fresh_mode, 'the existing checkout keeps its mode');
done_testing();
+4 -2
View File
@@ -49,7 +49,9 @@ our @STAGES;
sub createrepo_dir { push @main::CREATEREPO, $_[0]; }
# The four stages buildall drives. Each records that it was reached.
sub createmockconfig { push @main::STAGES, 'createmockconfig'; }
sub prepare_mock_resultdirs { push @main::STAGES, 'prepare_mock_resultdirs'; }
sub buildsources { push @main::STAGES, 'buildsources'; }
sub share_build_sources_dir { push @main::STAGES, 'share_build_sources_dir'; }
sub buildspkgs { push @main::STAGES, 'buildspkgs'; }
sub buildpkgs { push @main::STAGES, 'buildpkgs'; }
# buildall assigns a staging directory per build process.
@@ -128,11 +130,11 @@ sub stages_for {
}
is_deeply( stages_for(0),
[qw(createmockconfig buildsources buildspkgs buildpkgs)],
[qw(createmockconfig prepare_mock_resultdirs buildsources share_build_sources_dir buildspkgs buildpkgs)],
'a normal run builds the source rpm and then rebuilds it into binaries' );
is_deeply( stages_for(1),
[qw(createmockconfig buildsources buildspkgs)],
[qw(createmockconfig prepare_mock_resultdirs buildsources share_build_sources_dir buildspkgs)],
'a source-only run stops once the source rpm exists' );
ok( !grep( { $_ eq 'buildpkgs' } @{ stages_for(1) } ),
+34 -2
View File
@@ -4,6 +4,7 @@
use strict;
use warnings;
use Cwd ();
use File::Path qw(make_path);
use File::Slurper qw(write_text);
use File::Temp qw(tempdir);
@@ -12,11 +13,23 @@ use lib "$FindBin::Bin/../../build-utils/lib";
use Test::More;
use XCAT::BuildUtils qw(build_sources_base build_sources_dir prepare_build_sources_dir
remove_build_sources_dir sweep_build_sources_dirs);
remove_build_sources_dir sweep_build_sources_dirs share_build_sources_dir);
# mock reads the staged sources as the uid of the target's chrootuid, and $HOME of a
# root build is /root, mode 0550. The base belongs to the checkout, not to $HOME.
my $home = tempdir(CLEANUP => 1);
local $ENV{HOME} = "$home/home";
my $base = build_sources_base($home);
is($base, "$home/rpmbuild/sources", 'the base directory is under the rpmbuild tree');
is($base, "$home/dist/sources", 'the base directory is under the dist tree of the checkout');
unlike($base, qr{^\Q$ENV{HOME}\E/}, 'the base directory is not under $HOME');
{
my $cwd = Cwd::getcwd();
chdir $home or die "chdir $home: $!\n";
my $default = build_sources_base();
chdir $cwd or die "chdir $cwd: $!\n";
is($default, Cwd::abs_path($home) . "/dist/sources",
'the base directory defaults to the dist tree of the working directory');
}
# Two live processes on one host never have the same pid. The host name keeps two
# hosts apart when $HOME is on NFS.
@@ -89,4 +102,23 @@ ok(!-e $reaped, 'the default check deletes the directory of a reaped process');
is_deeply([sweep_build_sources_dirs("$home/absent", 'hosta', $is_alive)], [],
'a missing base directory is not an error');
# Another uid must traverse every directory and read every file, whatever the umask.
{
my $root = tempdir(CLEANUP => 1);
my $umask = umask 077;
my $sbase = build_sources_base($root);
my $dir = prepare_build_sources_dir($sbase, 401, 'hosta');
make_path("$dir/sub");
write_text("$dir/xCAT-2.20.0.tar.gz", "payload\n");
write_text("$dir/sub/xcat.conf", "conf\n");
share_build_sources_dir($dir);
umask $umask;
for my $d ("$root/dist", $sbase, $dir, "$dir/sub") {
is((stat $d)[2] & 0055, 0055, "other users can list and enter $d");
}
for my $f ("$dir/xCAT-2.20.0.tar.gz", "$dir/sub/xcat.conf") {
is((stat $f)[2] & 0044, 0044, "other users can read $f");
}
}
done_testing;