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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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 --
|
||||
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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) } ),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user