From cf90df27239b179cad2470fc6c531e53c754d85c Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:53:03 -0300 Subject: [PATCH 1/8] test(xcat-dep): the native owner tests skip mock 6.8 and never check result ownership native/openeuler-power-inputs.t gates its whole native owner section on "from mockbuild.util import load_config". mock 6.8 keeps load_config in mockbuild.config, so the import fails, $can_owner is false and 52 assertions skip on the only architecture that can run them. native/fixtures/power-mock.py imports it the same way and cannot start at all. The fake mock also created --resultdir as the user that started it and wrote no logs, so it could not see what mock 6.8 does: it reloads its uid manager from chrootuid, then creates --resultdir and opens state.log, build.log and root.log in it as that uid. Both now use mockbuild.config, and the fake mock creates --resultdir and opens those three logs as chrootuid. Against the unfixed tree the native owner fails: mock: [Errno 13] Permission denied: .../native-results/native-leaf/state.log ERROR: Step failed (rc=1): Build native prerequisite: native-leaf Failed 14/115 subtests The directory is root root 755 and chrootuid is 1000, which is the catalog's declared build_uid for that node. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- native/fixtures/power-mock.py | 35 +++++++++++++++++++++++++++------ native/openeuler-power-inputs.t | 2 +- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/native/fixtures/power-mock.py b/native/fixtures/power-mock.py index 9abe53e..42a0326 100644 --- a/native/fixtures/power-mock.py +++ b/native/fixtures/power-mock.py @@ -1,13 +1,36 @@ #!/usr/bin/python3 import json, os, pathlib, shutil, sys -import mockbuild -from mockbuild.util import load_config +from mockbuild.config import load_config args = sys.argv[1:] call = {'mock': args} +config = None def value(name): return args[args.index(name)+1] + +def resultdir(path): + """Create --resultdir and open its logs the way mock does. + + mock 6.8 reloads its uid manager from chrootuid, so it creates --resultdir and opens + state.log, build.log and root.log in it as that uid, not as the user that started mock. + """ + uid, gid = config['chrootuid'], config['chrootgid'] + pid = os.fork() + if pid == 0: + try: + if os.geteuid() == 0: + os.setregid(gid, gid) + os.setresuid(uid, uid, 0) + os.makedirs(path, exist_ok=True) + for name in ('state.log', 'build.log', 'root.log'): + open(os.path.join(path, name), 'a+').close() + except OSError as why: + print('mock: %s' % why, file=sys.stderr) + os._exit(1) + os._exit(0) + if os.waitpid(pid, 0)[1] != 0: + sys.exit(1) + if '--rebuild' in args or '--buildsrpm' in args: - load_config('/etc/mock', value('-r'), None, 'native-contract', - str(pathlib.Path(mockbuild.__file__).parent)) + config = load_config('/etc/mock', value('-r')) call['config_rc'] = 0 if '--rebuild' in args: name = pathlib.Path(value('--rebuild')).name.split('-1-1.oe2403')[0] @@ -16,13 +39,13 @@ if '--buildsrpm' in args: call['spec'] = pathlib.Path(value('--spec')).read_text() with open(os.environ['NATIVE_CALLS'], 'a') as f: f.write(json.dumps(call) + '\n') if '--buildsrpm' in args: - dest = pathlib.Path(value('--resultdir')); dest.mkdir(parents=True, exist_ok=True) + dest = pathlib.Path(value('--resultdir')); resultdir(dest) source = json.loads(pathlib.Path(os.environ['NATIVE_OUTPUTS']).read_text())['native-leaf'][1] shutil.copyfile(source, dest / pathlib.Path(source).name) sys.exit(0) if '--rebuild' not in args: sys.exit(0) if name == os.environ.get('NATIVE_FAIL'): sys.exit(42) -dest = pathlib.Path(value('--resultdir')); dest.mkdir(parents=True, exist_ok=True) +dest = pathlib.Path(value('--resultdir')); resultdir(dest) if name == os.environ.get('NATIVE_EMPTY'): sys.exit(0) fixtures = json.loads(pathlib.Path(os.environ['NATIVE_OUTPUTS']).read_text()) for source in fixtures[name]: shutil.copyfile(source, dest / pathlib.Path(source).name) diff --git a/native/openeuler-power-inputs.t b/native/openeuler-power-inputs.t index b13f3c4..1f0be88 100644 --- a/native/openeuler-power-inputs.t +++ b/native/openeuler-power-inputs.t @@ -294,7 +294,7 @@ for my $case (['publisher-elf', qr/ELF payload/], ['publisher-arch', qr/not a no my @namespace = ('unshare', ($> == 0 ? () : ('--user', '--map-root-user')), '--mount', '--propagation', 'private'); my $can_owner = $host_arch eq 'ppc64le' - && run_capture("$tmp/mock-loader.log", 'python3', '-c', 'from mockbuild.util import load_config') == 0 + && run_capture("$tmp/mock-loader.log", 'python3', '-c', 'from mockbuild.config import load_config') == 0 && run_capture("$tmp/namespace.log", @namespace, 'true') == 0; SKIP: { skip 'Whole native owner requires POWER, native Mock and a private mount namespace', 52 unless $can_owner; From 63784cfbee55fc4e87092f525045b4fb3b2b85da Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:27:11 -0300 Subject: [PATCH 2/8] test(xcat-dep): nothing proves a native dep builder can write its mock result directories ipxe-xcat/mockbuild.pl runs as root and calls make_path on $work_dir/srpm and $work_dir/rpm, then gives each one to mock as --resultdir. The native openEuler overlay sets chrootuid to the catalog's build_uid and chrootgid to the native build gid, so mock reloads to that uid and opens state.log, build.log and root.log in those directories. A root-made directory with the default umask is root root 0755 and mock cannot write them. grub2-xcat, ipmitool-xcat, syslinux-xcat and goconserver create their result directories the same way. What keeps those directories writable is the step wrapper, not the builder: native_owner_command gives the step the native build gid and umask 0002, so every directory the builder creates below it is group writable. No test measured that, so removing either one is silent. Move native_owner_command into MockBuildUtils so a test can call it in process, and assert the wrapper carries setpriv --regid 1000, --clear-groups and umask 0002, that setpriv precedes the umask and the umask precedes the builder, and that the overlay is still bound over the target config so chrootuid stays as the catalog declared it. This commit is the move and the test only. Five of the six assertions fail here, because the gid and the umask arrive in the next commit. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .github/workflows/genesis-openembedded.yml | 1 + MockBuildUtils.pm | 20 +++++++++++ mockbuild-all.pl | 4 +-- t/openeuler_native_result_permissions.t | 39 ++++++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 t/openeuler_native_result_permissions.t diff --git a/.github/workflows/genesis-openembedded.yml b/.github/workflows/genesis-openembedded.yml index f3ffdf6..9a008c5 100644 --- a/.github/workflows/genesis-openembedded.yml +++ b/.github/workflows/genesis-openembedded.yml @@ -78,6 +78,7 @@ jobs: sudo -E prove -v -It/lib t/common-repo-gate.t prove -v t/openeuler.t prove -v t/openeuler_mock_template_include.t + prove -v t/openeuler_native_result_permissions.t prove -v -Ilib t/openeuler_native_trust_dbpath.t prove -v -Ilib t/openeuler_native_cpio_scan.t bats t/verify-xcatsn-install.bats diff --git a/MockBuildUtils.pm b/MockBuildUtils.pm index 2dca2e3..ae71337 100644 --- a/MockBuildUtils.pm +++ b/MockBuildUtils.pm @@ -25,6 +25,7 @@ our @EXPORT_OK = qw( cross_copy_genesis finalize_xcat_dep bump_dep_release_suffix createrepo_c_cmd sign_and_index_repo build_mock_uniqueext rpm_in_cell resolve_mock_cfg + native_owner_command NATIVE_BUILD_GID openeuler_build_target openeuler_repo_subdir recover_common_repository ); @@ -101,6 +102,25 @@ sub sh_quote { return "'$s'"; } +# The host group the native openEuler builds run as: xcatnative, gid 1000. The native mock overlays +# set chrootgid to it and chrootuid to the catalog's build_uid, so mock creates --resultdir and opens +# state.log, build.log and root.log in it as that uid and group. +use constant NATIVE_BUILD_GID => 1000; + +# native_owner_command: wrap a native build step so it runs against $overlay instead of the target's +# own mock configuration, and with the identity mock will write results as. +# +# A dep builder makes its own result directories before mock starts -- ipxe-xcat/mockbuild.pl calls +# make_path on $work_dir/srpm and $work_dir/rpm and passes each one to mock as --resultdir. The +# script runs as root, so without the gid and the umask below those directories are root root 0755 +# and mock, reloaded to chrootuid 1000, cannot open its logs in them. +sub native_owner_command { + my ($overlay, $mock_cfg, $command) = @_; + my $script = 'mount --bind ' . sh_quote($overlay) . ' ' . sh_quote($mock_cfg) + . ' && exec sh -c ' . sh_quote($command); + return 'unshare --mount --propagation private -- sh -c ' . sh_quote($script); +} + # print_step: print a step banner. sub print_step { my ($msg) = @_; diff --git a/mockbuild-all.pl b/mockbuild-all.pl index 176999a..54b3e27 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -1247,9 +1247,7 @@ sub source_rpm_build_command { sub native_owner_command { my ($plan, $target, $command, $uid) = @_; my $overlay = $plan->{overlays}{$uid} // die "Missing native mock overlay\n"; - my $script = 'mount --bind ' . sh_quote($overlay) . ' ' . sh_quote("/etc/mock/$target.cfg") - . ' && exec sh -c ' . sh_quote($command); - return 'unshare --mount --propagation private -- sh -c ' . sh_quote($script); + return MockBuildUtils::native_owner_command($overlay, "/etc/mock/$target.cfg", $command); } sub native_overlay { diff --git a/t/openeuler_native_result_permissions.t b/t/openeuler_native_result_permissions.t new file mode 100644 index 0000000..0e0418b --- /dev/null +++ b/t/openeuler_native_result_permissions.t @@ -0,0 +1,39 @@ +#!/usr/bin/perl +# A dep builder creates its own result directories before mock starts. ipxe-xcat/mockbuild.pl calls +# make_path on $work_dir/srpm and $work_dir/rpm and gives each one to mock as --resultdir, and it +# runs as root. The native openEuler overlay keeps chrootuid at the catalog's build_uid and +# chrootgid at the native build gid, so mock opens state.log, build.log and root.log in those +# directories as that uid. They are writable only if the step itself carries the native build gid +# and a group-writable umask. native_owner_command is where the step gets both. +use strict; +use warnings; +use Test::More; +use FindBin qw($RealBin); +use lib "$RealBin/.."; +use MockBuildUtils qw(native_owner_command NATIVE_BUILD_GID); + +my $gid = NATIVE_BUILD_GID; +my $overlay = '/work/native-1000.cfg'; +my $cfg = '/etc/mock/openeuler-24.03-ppc64le.cfg'; +my $step = "perl ipxe-xcat/mockbuild.pl --work-dir /tmp/ipxe-xcat --result-dir /results/ipxe-xcat"; +my $cmd = native_owner_command($overlay, $cfg, $step); + +like($cmd, qr/\bsetpriv\b[^'"]*--regid\s+\Q$gid\E\b/, + "the step runs under setpriv --regid $gid, so it creates its result directories in the native build group"); +like($cmd, qr/\bsetpriv\b[^'"]*--clear-groups\b/, + 'setpriv --clear-groups drops the supplementary groups root started with'); +like($cmd, qr/\bumask\s+0002\b/, + 'the step runs with umask 0002, so a directory it creates stays group writable'); + +my $umask = index($cmd, 'umask 0002'); +my $setpriv = index($cmd, 'setpriv'); +my $builder = index($cmd, 'ipxe-xcat/mockbuild.pl'); +ok($setpriv >= 0 && $setpriv < $umask, + 'setpriv comes before umask 0002, so the umask applies under the native build gid'); +ok($umask >= 0 && $umask < $builder, + 'umask 0002 comes before the builder, so make_path in the builder inherits it'); + +like($cmd, qr/mount --bind \S*\Q$overlay\E\S*\s+\S*\Q$cfg\E/, + 'the wrapper binds the overlay it was given over the target config, so chrootuid stays as the catalog declared it'); + +done_testing(); From 5d3fcfe9064edb303afedc16103209b93fd8ccb4 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:01:12 -0300 Subject: [PATCH 3/8] fix(xcat-dep): the openEuler ppc64le native prerequisites cannot write their logs Every xcat-dep-build-openeuler24.03-ppc64le build fails at the first native prerequisite: PermissionError: [Errno 13] Permission denied: .../native-results/procenv/state.log mock 6.8 reloads its uid manager from chrootuid ("fix: reload uidManager with chrootuid/chrootgid from config"), so mock now creates --resultdir and opens its own state.log, build.log and root.log there as chrootuid, which the openEuler ppc64le overlay sets to the catalog's build_uid. mockbuild-all.pl creates those directories as root, mode 755, so mock cannot open a log in them and dies in resetLogging before the build starts. It is not specific to the shared filesystem: the same failure reproduces on /tmp. make_native_path gives every directory mock writes the native build gid and makes it group writable, and MockBuildUtils::native_owner_command runs the step under setpriv --regid with a 0002 umask, so what the step creates below those directories inherits both. The uid a step builds as now comes from the catalog's build_uid instead of the step id, which also builds xnba-undi as root, as its own declaration says. t/openeuler_native_result_permissions.t fails five of its six assertions without this change. native/openeuler-power-inputs.t fails too, with "mock: [Errno 13] Permission denied: .../native-results/native-leaf/state.log", but only on ppc64le: its native owner section skips 52 of its assertions on x86_64, which is the architecture the GitHub workflow runs on. The unit test is what keeps the fix covered there. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- MockBuildUtils.pm | 5 +++-- mockbuild-all.pl | 41 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/MockBuildUtils.pm b/MockBuildUtils.pm index ae71337..4280d23 100644 --- a/MockBuildUtils.pm +++ b/MockBuildUtils.pm @@ -116,9 +116,10 @@ use constant NATIVE_BUILD_GID => 1000; # and mock, reloaded to chrootuid 1000, cannot open its logs in them. sub native_owner_command { my ($overlay, $mock_cfg, $command) = @_; - my $script = 'mount --bind ' . sh_quote($overlay) . ' ' . sh_quote($mock_cfg) + my $script = 'umask 0002 && mount --bind ' . sh_quote($overlay) . ' ' . sh_quote($mock_cfg) . ' && exec sh -c ' . sh_quote($command); - return 'unshare --mount --propagation private -- sh -c ' . sh_quote($script); + return 'unshare --mount --propagation private -- setpriv --regid ' . NATIVE_BUILD_GID + . ' --clear-groups -- sh -c ' . sh_quote($script); } # print_step: print a step banner. diff --git a/mockbuild-all.pl b/mockbuild-all.pl index 54b3e27..28914a3 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -19,6 +19,7 @@ use FindBin qw($RealBin); use lib $RealBin, "$RealBin/lib"; use XCAT::NFSLock (); use MockBuildUtils qw(sh_quote print_step version_matches required_pkgs rpm_in_cell resolve_mock_cfg + NATIVE_BUILD_GID carry_over_rpms rpm_name rpm_arch rpm_source_rpm rpm_digests_ok install_deps_packages install_deps_command missing_perl_modules read_manifest derive_target_from_repo_path @@ -156,6 +157,21 @@ my @HELD_LOCKS; my $LOCK_OWNER_PID; my ($COMMON_STAGE, $COMMON_DESTINATION, $COMMON_BACKUP); my %NATIVE_PLANS; + +# Create a directory mock writes for a native build. mock 6.8 reloads its uid manager from +# chrootuid, so it creates --resultdir and opens state.log, build.log and root.log in it as +# chrootuid/chrootgid, not as the user that started mock. This script runs as root, so such a +# directory carries the native build gid and is group writable. native_owner_command gives the step +# the same gid and a 0002 umask, so what the step creates below it inherits both. +sub make_native_path { + my ($native, @dirs) = @_; + make_path(@dirs); + return unless $native; + for my $dir (@dirs) { + chown -1, NATIVE_BUILD_GID, $dir or die "Cannot set the native build group on $dir: $!\n"; + chmod 0775, $dir or die "Cannot make $dir writable by the native build group: $!\n"; + } +} for my $sig (qw(INT TERM HUP)) { $SIG{$sig} = sub { exit 1; }; } @@ -739,6 +755,7 @@ if (!$skip_build) { my $script = $builder->{script}; my $step_result = "$build_root/$name"; my $step_log = "$log_root/$name"; + make_native_path($native, $step_result) if $native && !$dry_run; my $step_uniqueext = build_mock_uniqueext($run_id, ++$build_step_seq, $name); my $mock_cfg = $builder->{noarch} ? $profile->{noarch_cfg} : $target; my $cmd = $builder->{srpm} @@ -778,6 +795,7 @@ if (!$skip_build) { if (!$skip_perl && @perl_pkgs) { my $perl_result = "$build_root/perl/$arch"; my $perl_log = "$log_root/perl/$arch"; + make_native_path($native, $perl_result) if $native && !$dry_run; my $perl_uniqueext = build_mock_uniqueext($run_id, ++$build_step_seq, 'perl-list6'); # Bound the perl builder's OWN internal parallelism to this target's budget; otherwise it # forks one mock build per perl package (~7), which -- multiplied by parallel EL targets -- @@ -858,7 +876,8 @@ if (!$skip_build) { '--gpg-home', shell_quote(File::Spec->rel2abs($gpg_home ne '' ? $gpg_home : $ENV{GNUPGHOME} || "$ENV{HOME}/.gnupg"))) : ()), ); - $cmd = native_owner_command($native, $target, $cmd, 0) if $native; + $cmd = native_owner_command($native, $target, $cmd, + $native->{nodes}{'xCAT-genesis-base'}{build_uid}) if $native; push @build_steps, { id => 'genesis', step => 'Build xCAT-genesis-base (per-target, OS-dependent)', @@ -873,7 +892,8 @@ if (!$skip_build) { if (@build_steps) { if ($native) { for my $step (grep { $_->{id} ne 'genesis' } @build_steps) { - $step->{cmd} = native_owner_command($native, $target, $step->{cmd}, 1000); + $step->{cmd} = native_owner_command($native, $target, $step->{cmd}, + native_step_build_uid($native, $step)); } } # Prefer the caller-supplied cap (global budget / active targets). Fall back to the old @@ -1200,9 +1220,10 @@ sub install_mock_cfg { sub source_rpm_build_command { my ($builder, $target, $uniqueext, $result, $log, $work) = @_; + my $native = $NATIVE_PLANS{$target}; my $cfg = "$work/mock-deterministic.cfg"; if (!$dry_run) { - make_path($work, $result); + make_native_path($native, $work, $result); open my $fh, '>', $cfg or die "Cannot write $cfg: $!\n"; print {$fh} "include('/etc/mock/$target.cfg')\n"; print {$fh} "config_opts['environment']['SOURCE_DATE_EPOCH'] = '$SOURCE_DATE_EPOCH'\n"; @@ -1234,7 +1255,7 @@ sub source_rpm_build_command { . ' -i ' . sh_quote($path), log => "$log/spec-patch.log"); } my $restamped = "$work/restamp-srpm"; - make_path($restamped) unless $dry_run; + make_native_path($native, $restamped) unless $dry_run; $prefix = "$mock --buildsrpm --spec " . sh_quote($specs[0]) . ' --sources ' . sh_quote("$top/SOURCES") . ' --resultdir ' . sh_quote($restamped) . ' && set -- ' . sh_quote($restamped) . '/*.src.rpm' @@ -1244,6 +1265,17 @@ sub source_rpm_build_command { return $prefix . "$mock --rebuild $srpm --resultdir " . sh_quote($result); } +# The uid a step builds as, from the catalog. xnba-undi declares 0, like xCAT-genesis-base, and the +# step id does not say so: reading the uid off the id built it as 1000 against its own declaration. +sub native_step_build_uid { + my ($plan, $step) = @_; + my %uids = map { ($plan->{nodes}{$_}{build_uid} // 1000) => 1 } + sort keys %{$step->{native_results} // {}}; + die "Step $step->{id} mixes native build uids: " . join(', ', sort keys %uids) . "\n" + if keys(%uids) > 1; + return (keys %uids)[0] // 1000; +} + sub native_owner_command { my ($plan, $target, $command, $uid) = @_; my $overlay = $plan->{overlays}{$uid} // die "Missing native mock overlay\n"; @@ -1303,6 +1335,7 @@ sub build_native_inputs { my $node = $plan->{nodes}{$name}; next unless $node->{type} eq 'srpm'; my $result = "$work/native-results/$name"; + make_native_path($plan, "$work/native-results") unless $dry_run; my $log = "$logs/native/$name"; my $uniqueext = build_mock_uniqueext("$target-$run_id", ++$sequence, $name); my %builder = (%$node, srpm => $node->{staged} // "$work/native-inputs/$name/" . basename($node->{url})); From e5ceff876b2455d34d33457c5cec60429fba5291 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:38:34 -0300 Subject: [PATCH 4/8] test(xcat-dep): the openEuler native overlays enable a plugin the host cannot run Every openEuler ppc64le native prerequisite after procenv itself fails before rpmbuild starts: FileNotFoundError: [Errno 2] No such file or directory: '/usr/bin/procenv' raised from mock's procenv plugin prebuild hook. The plugin runs ["/usr/bin/procenv"] through mockbuild.util.do with no chroot prefix, so it executes the BUILD HOST's copy, and xcat-master-ppc has none. The plugin also appends procenv to the buildroot's preexisting_deps itself, so the chroot already gets the package: a run that installed it, with "Installing : procenv-0.60-1.ppc64le" in root.log, failed on the same line. mock ships procenv_enable False. The overlays turn it on, which makes every native build depend on a host package that neither repository declares, for a procenv.log that nothing reads. The overlay text was built inline in native_overlay, so there was nothing a unit test could call. native_overlay_text in XCAT::NativeInputs now renders one overlay and reproduces today's values exactly, and native_overlay writes what it returns. openeuler_native_procenv_plugin.t asserts the plugin is left at its default in all three overlays, that the prerequisite repository and the bind mount survive, and that the uid split does not move. Two of its eleven checks fail on this commit, which is the defect. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .github/workflows/genesis-openembedded.yml | 1 + lib/XCAT/NativeInputs.pm | 38 ++++++++++++++++++- mockbuild-all.pl | 14 +------ t/openeuler_native_procenv_plugin.t | 43 ++++++++++++++++++++++ 4 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 t/openeuler_native_procenv_plugin.t diff --git a/.github/workflows/genesis-openembedded.yml b/.github/workflows/genesis-openembedded.yml index 9a008c5..d419823 100644 --- a/.github/workflows/genesis-openembedded.yml +++ b/.github/workflows/genesis-openembedded.yml @@ -79,6 +79,7 @@ jobs: prove -v t/openeuler.t prove -v t/openeuler_mock_template_include.t prove -v t/openeuler_native_result_permissions.t + prove -v -Ilib t/openeuler_native_procenv_plugin.t prove -v -Ilib t/openeuler_native_trust_dbpath.t prove -v -Ilib t/openeuler_native_cpio_scan.t bats t/verify-xcatsn-install.bats diff --git a/lib/XCAT/NativeInputs.pm b/lib/XCAT/NativeInputs.pm index 065c136..2dacb62 100644 --- a/lib/XCAT/NativeInputs.pm +++ b/lib/XCAT/NativeInputs.pm @@ -14,7 +14,7 @@ use File::Temp qw(tempdir); use JSON::PP; our @EXPORT_OK = qw(load_inputs stage_inputs verify_input rpm_identity validate_outputs publisher_trust - trust_dbpath scan_cpio_for_elf); + trust_dbpath scan_cpio_for_elf native_overlay_text); sub read_file { my ($path) = @_; @@ -300,4 +300,40 @@ sub validate_outputs { return \%seen; } +#------------------------------------------------------------------------------- + +=head3 native_overlay_text + + Descriptions: Render one native mock overlay configuration. + + Arguments: + $purpose - 1000, 0 or 'procenv'. 'procenv' is the overlay that builds + procenv itself. + $base - path of the snapshotted base mock configuration to include. + $prereqs - path of the local repository holding the built prerequisites. + + Returns: the overlay file content. + +=cut + +#------------------------------------------------------------------------------- +sub native_overlay_text { + my ($purpose, $base, $prereqs) = @_; + my $bootstrap = $purpose eq 'procenv'; + my $uid = $bootstrap ? 1000 : $purpose; + my $url = $prereqs; + $url =~ s{([^A-Za-z0-9_./~-])}{sprintf('%%%02X', ord($1))}ge; + my $repo = "\n[xcat-native-inputs]\nname=xCAT native build prerequisites\nbaseurl=file://$url\n" + . "gpgkey=file://$url/repodata/repomd.xml.key\ngpgcheck=1\nrepo_gpgcheck=1\n" + . "enabled=1\nskip_if_unavailable=0\n"; + my $out = 'include(' . JSON::PP->new->encode($base) . ")\n"; + $out .= "config_opts['chrootuid'] = $uid\nconfig_opts['chrootgid'] = 1000\n"; + $out .= "config_opts['dnf.conf'] += \"\"\"$repo\"\"\"\n"; + $out .= "config_opts['plugin_conf']['bind_mount_enable'] = True\n"; + $out .= "config_opts['plugin_conf']['procenv_enable'] = " . ($bootstrap ? 'False' : 'True') . "\n"; + $out .= "config_opts['plugin_conf']['bind_mount_opts']['dirs'].append(" + . '(' . JSON::PP->new->encode($prereqs) . ', ' . JSON::PP->new->encode($prereqs) . "))\n"; + return $out; +} + 1; diff --git a/mockbuild-all.pl b/mockbuild-all.pl index 28914a3..8ae2150 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -1289,21 +1289,9 @@ sub native_overlay { return if $dry_run; my $base = "$work/native-base.cfg"; copy("/etc/mock/$target.cfg", $base) or die "Cannot snapshot native mock configuration: $!\n"; - my $url = $prereqs; - $url =~ s{([^A-Za-z0-9_./~-])}{sprintf('%%%02X', ord($1))}ge; - my $repo = "\n[xcat-native-inputs]\nname=xCAT native build prerequisites\nbaseurl=file://$url\n" - . "gpgkey=file://$url/repodata/repomd.xml.key\ngpgcheck=1\nrepo_gpgcheck=1\n" - . "enabled=1\nskip_if_unavailable=0\n"; for my $key (1000, 0, 'procenv') { - my $uid = $key eq 'procenv' ? 1000 : $key; open my $fh, '>', $plan->{overlays}{$key} or die "Cannot write native overlay: $!\n"; - print {$fh} 'include(' . JSON::PP->new->encode($base) . ")\n"; - print {$fh} "config_opts['chrootuid'] = $uid\nconfig_opts['chrootgid'] = 1000\n"; - print {$fh} "config_opts['dnf.conf'] += \"\"\"$repo\"\"\"\n"; - print {$fh} "config_opts['plugin_conf']['bind_mount_enable'] = True\n"; - print {$fh} "config_opts['plugin_conf']['procenv_enable'] = " . ($key eq 'procenv' ? 'False' : 'True') . "\n"; - print {$fh} "config_opts['plugin_conf']['bind_mount_opts']['dirs'].append(" - . '(' . JSON::PP->new->encode($prereqs) . ', ' . JSON::PP->new->encode($prereqs) . "))\n"; + print {$fh} XCAT::NativeInputs::native_overlay_text($key, $base, $prereqs); close $fh or die "Cannot close native overlay: $!\n"; } open my $ledger, '>', "$work/native-overlays.json" or die "Cannot record native overlays: $!\n"; diff --git a/t/openeuler_native_procenv_plugin.t b/t/openeuler_native_procenv_plugin.t new file mode 100644 index 0000000..256906e --- /dev/null +++ b/t/openeuler_native_procenv_plugin.t @@ -0,0 +1,43 @@ +#!/usr/bin/perl +# Every openEuler ppc64le native prerequisite after procenv itself fails before rpmbuild starts: +# +# FileNotFoundError: [Errno 2] No such file or directory: '/usr/bin/procenv' +# +# raised from mock's procenv plugin prebuild hook. The plugin runs ["/usr/bin/procenv"] through +# mockbuild.util.do with no chroot prefix, so it executes the BUILD HOST's copy, and +# xcat-master-ppc has none. Installing procenv in the chroot does not help: mock already appends it +# to preexisting_deps itself, and the run that did so still failed with the same line while +# root.log recorded "Installing : procenv-0.60-1.ppc64le". +# +# mock ships procenv_enable False. The overlays turned it on, which made every native build depend +# on a host package that neither repository declares, for a procenv.log nothing reads. They now +# leave it at the default. +# +# native_overlay_text is pure, so this runs no mock. +use strict; +use warnings; +use Test::More; +use FindBin qw($RealBin); +use lib "$RealBin/../lib"; +use XCAT::NativeInputs qw(native_overlay_text); + +my $base = '/opt/xcat-ci-shared/builds/oe264/work/native-base.cfg'; +my $prereqs = '/opt/xcat-ci-shared/builds/oe264/work/native-prerequisites'; + +for my $purpose (1000, 0, 'procenv') { + my $text = native_overlay_text($purpose, $base, $prereqs); + unlike($text, qr/procenv_enable'\]\s*=\s*True/, + "overlay $purpose does not enable the procenv plugin, which needs a host binary"); + # The overlay must still carry what the native build does depend on. + like($text, qr/\[xcat-native-inputs\]/, "overlay $purpose still declares the prerequisite repository"); + like($text, qr/bind_mount_enable'\]\s*=\s*True/, "overlay $purpose still binds the prerequisite tree"); +} + +# The uid split is declared in the inputs and must not move: genesis builds as root, the rest as +# 1000, and the procenv overlay runs as 1000 too. +is((native_overlay_text(1000, $base, $prereqs) =~ /chrootuid'\]\s*=\s*(\d+)/)[0], 1000, + 'the unprivileged overlay still runs as uid 1000'); +is((native_overlay_text(0, $base, $prereqs) =~ /chrootuid'\]\s*=\s*(\d+)/)[0], 0, + 'the genesis overlay still runs as root'); + +done_testing(); From d26549495b9121701f9b528f74fd52d2f2002784 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:39:14 -0300 Subject: [PATCH 5/8] fix(xcat-dep): the openEuler native builds need procenv on the build host Every native prerequisite after procenv itself died before rpmbuild started: FileNotFoundError: [Errno 2] No such file or directory: '/usr/bin/procenv' mock's procenv plugin executes ["/usr/bin/procenv"] on the build host, not in the chroot, so the build needed a host package that neither this repository nor xcat-core-ci-cd declares. The plugin is pure provenance: it writes procenv.log into the result directory and nothing reads it. mock ships it off. native_overlay_text now leaves procenv_enable at mock's default for every overlay, so a native build depends on nothing outside the two repositories. The procenv node stays in the inputs: mock appends procenv to the buildroot's preexisting_deps whenever the plugin is on, so the package is still what makes the plugin usable if the host is ever provisioned for it. openeuler_native_procenv_plugin.t passes with this commit and failed two checks without it. Measured on xcat-master-ppc: run 264b installed procenv-0.60-1.ppc64le into the docbook-style-dsssl chroot and failed on the same line, which is what showed the chroot was never the problem. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- lib/XCAT/NativeInputs.pm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/XCAT/NativeInputs.pm b/lib/XCAT/NativeInputs.pm index 2dacb62..e3eff5c 100644 --- a/lib/XCAT/NativeInputs.pm +++ b/lib/XCAT/NativeInputs.pm @@ -330,7 +330,11 @@ sub native_overlay_text { $out .= "config_opts['chrootuid'] = $uid\nconfig_opts['chrootgid'] = 1000\n"; $out .= "config_opts['dnf.conf'] += \"\"\"$repo\"\"\"\n"; $out .= "config_opts['plugin_conf']['bind_mount_enable'] = True\n"; - $out .= "config_opts['plugin_conf']['procenv_enable'] = " . ($bootstrap ? 'False' : 'True') . "\n"; + # mock's procenv plugin runs the HOST's /usr/bin/procenv, not the chroot's, and it installs + # procenv into the buildroot itself through preexisting_deps. Enabling it made every native + # build need a host package neither repository declares, for a log nothing reads. mock's + # default is off; leave it there. + $out .= "config_opts['plugin_conf']['procenv_enable'] = False\n"; $out .= "config_opts['plugin_conf']['bind_mount_opts']['dirs'].append(" . '(' . JSON::PP->new->encode($prereqs) . ', ' . JSON::PP->new->encode($prereqs) . "))\n"; return $out; From bd5ac471759a76927b11635eb8c04afe3403b3d6 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:47:09 -0300 Subject: [PATCH 6/8] test(xcat-dep): the openEuler ppc64le manifest can omit a package xCAT requires packages-manifest.conf says which packages mockbuild-all.pl builds for a target. Nothing compared the openEuler ppc64le section with the EL section of the same architecture, so a package xCAT requires by name could be absent and the dep build would still pass. The omission appears much later, as "nothing provides" when dnf installs xCAT on the management node. Assert that the openEuler ppc64le section declares every package alma+epel-10-ppc64le declares, apart from two exemptions that no core rpm requires, and that the native input catalog declares an owner for every package that section names. Both assertions fail today, on ipxe-xcat. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- t/openeuler.t | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/t/openeuler.t b/t/openeuler.t index a2ea7a8..4e6417e 100644 --- a/t/openeuler.t +++ b/t/openeuler.t @@ -2,7 +2,8 @@ use strict; use warnings; use FindBin qw($RealBin); -use lib "$RealBin/.."; +use lib "$RealBin/..", "$RealBin/../lib"; +use XCAT::NativeInputs qw(load_inputs); use Test::More; use MockBuildUtils qw(openeuler_build_target openeuler_repo_subdir install_deps_command install_deps_packages derive_target_from_repo_path read_manifest); @@ -55,4 +56,26 @@ for my $path ( is(derive_target_from_repo_path($path), undef, "$path has no native target"); } +# xCAT and xCATsn name the netboot loaders and the console backend as Requires on every arch, so a +# package missing from a target section is not a smaller build: it is "nothing provides " when +# dnf installs xCAT on the management node. The EL section of the same arch is the reference set, +# because openEuler is rpm-md and installs the same core packages. Both exemptions were measured +# against the built core rpms with rpm -qp --requires, not read off a spec conditional. +my %exempt = ( + 'conserver-xcat' => 'xCAT requires goconserver; no core rpm requires conserver-xcat', + 'elilo-xcat' => 'no core rpm requires elilo-xcat', +); +my $reference = 'alma+epel-10-ppc64le'; +ok(scalar keys %{$manifest{$reference}}, "$reference is the reference section and is not empty"); +for my $pkg (sort keys %{$manifest{$reference}}) { + next if $exempt{$pkg}; + ok(exists $manifest{'openeuler-24.03-ppc64le'}{$pkg}, + "openeuler-24.03-ppc64le declares $pkg, which $reference also declares"); +} + +# A manifest entry alone is not enough. The ppc64le target resolves its whole manifest through the +# native input catalog, which refuses a name no node produces, so the catalog has to declare an +# owner for the package as well. +my $plan = eval { load_inputs("$RealBin/..", $manifest{'openeuler-24.03-ppc64le'}) }; +ok($plan, 'every package the ppc64le manifest requires has a native output owner') or diag($@); done_testing(); From 3254455ff2f6044f6abda354ef61c10fa3e6eaf0 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:47:09 -0300 Subject: [PATCH 7/8] fix(xcat-dep): openEuler ppc64le has no ipxe-xcat and xCAT cannot install dnf install xCAT on an openEuler 24.03 ppc64le management node stops with "nothing provides ipxe-xcat >= 2.0.0-1 needed by xCAT-2.20.0". xCAT and xCATsn require ipxe-xcat by name on ppc64le, both EL ppc64le sections of packages-manifest.conf list it, and the openEuler section does not, so mockbuild-all.pl never builds it and the dep repository never carries it. Declare the package in four places, because each one refuses it separately: the manifest section, the openEuler target profile that selects which dep builders run, the native input catalog that resolves an owner for every manifest name, and the catalog owner allow-list. ipxe-xcat.spec is BuildArch noarch with no BuildRequires and no compile step, so the owner builds as uid 1000 like grub2-xcat. elilo-xcat and conserver-xcat stay out. No core rpm requires either one: xCAT uses goconserver for consoles, and elilo is an EFI loader for architectures this cell does not serve. t/openeuler.t covers both halves and fails without this change. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- lib/XCAT/NativeInputs.pm | 2 +- mockbuild-all.pl | 4 ++-- openeuler/24.03-ppc64le.inputs.json | 11 +++++++++++ packages-manifest.conf | 1 + 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/XCAT/NativeInputs.pm b/lib/XCAT/NativeInputs.pm index e3eff5c..1c3d8cf 100644 --- a/lib/XCAT/NativeInputs.pm +++ b/lib/XCAT/NativeInputs.pm @@ -79,7 +79,7 @@ sub load_inputs { die "Invalid native input type '$type'\n" unless $type =~ /\A(?:srpm|publisher|owner)\z/; if ($type eq 'owner') { die "Unsupported native build owner '$name'\n" unless grep { $_ eq $name } - qw(goconserver grub2-xcat ipmitool-xcat syslinux-xcat xnba-undi xCAT-genesis-base + qw(goconserver grub2-xcat ipmitool-xcat ipxe-xcat syslinux-xcat xnba-undi xCAT-genesis-base perl-Crypt-Rijndael perl-Crypt-SSLeay perl-HTTP-Async perl-IO-Stty perl-Net-HTTPS-NB perl-Net-Telnet); } if ($type ne 'publisher') { diff --git a/mockbuild-all.pl b/mockbuild-all.pl index 8ae2150..b2a0420 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -1167,8 +1167,8 @@ sub target_profile { noarch_cfg => $target, forcearch => 0, epel => 0, - dep_builders => [qw(grub2-xcat ipmitool-xcat syslinux-xcat goconserver conserver-xcat xnba-undi python3-scp)], - required => [qw(ipmitool-xcat syslinux-xcat grub2-xcat xnba-undi + dep_builders => [qw(grub2-xcat ipmitool-xcat syslinux-xcat goconserver conserver-xcat xnba-undi ipxe-xcat python3-scp)], + required => [qw(ipmitool-xcat syslinux-xcat grub2-xcat xnba-undi ipxe-xcat perl-IO-Stty perl-HTTP-Async perl-Net-HTTPS-NB)], }; } diff --git a/openeuler/24.03-ppc64le.inputs.json b/openeuler/24.03-ppc64le.inputs.json index 8c0af3c..cfcc92b 100644 --- a/openeuler/24.03-ppc64le.inputs.json +++ b/openeuler/24.03-ppc64le.inputs.json @@ -1075,6 +1075,17 @@ ], "build_uid": 1000 }, + { + "name": "ipxe-xcat", + "type": "owner", + "outputs": [ + "ipxe-xcat" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, { "name": "xnba-undi", "type": "owner", diff --git a/packages-manifest.conf b/packages-manifest.conf index ccff757..284df2c 100644 --- a/packages-manifest.conf +++ b/packages-manifest.conf @@ -327,6 +327,7 @@ xCAT-genesis-base=>= 2:2.18.0 goconserver=>= 0.3.3-snap202011021058 grub2-xcat=1.0 ipmitool-xcat=>= 1.8.18-4 +ipxe-xcat=2.0.0 syslinux-xcat=>= 6.03-1 xnba-undi=>= 1.21.1-1 perl-Crypt-Rijndael=1.13 From 8351d2e3d46776465669126266ee3978a73a68b1 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 6 Oct 2026 21:36:00 -0300 Subject: [PATCH 8/8] fix(xcat-dep): the openEuler x86_64 targets have no ipxe-xcat either dnf install xCAT on an openEuler 20.03sp4, 22.03sp4 or 24.03sp4 x86_64 management node stops with "nothing provides ipxe-xcat >= 2.0.0-1 needed by xCAT-2.20.0". All three gating cells died at the install step in under two minutes and published no test result at all. xCAT.spec gained Requires: ipxe-xcat for x86_64 in xcat-core 32a688b3, and the three x86_64 sections of packages-manifest.conf never declared the package. The ppc64le section was fixed in the commit before this one; the openEuler branch of target_profile already covers every openEuler target, so the manifest was the only gap. t/openeuler.t now asserts each x86_64 section declares it, and fails 3 of 82 without these lines. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- packages-manifest.conf | 3 +++ t/openeuler.t | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/packages-manifest.conf b/packages-manifest.conf index 284df2c..8e0e5b8 100644 --- a/packages-manifest.conf +++ b/packages-manifest.conf @@ -255,6 +255,7 @@ perl-Sys-Virt=11.10.0 goconserver=>= 0.3.3-snap202011021058 grub2-xcat=1.0 ipmitool-xcat=>= 1.8.18-4 +ipxe-xcat=2.0.0 syslinux-xcat=>= 6.03-1 xnba-undi=>= 1.21.1-1 perl-Crypt-Rijndael=1.13 @@ -271,6 +272,7 @@ xCAT-genesis-base=>= 2:2.18.0 goconserver=>= 0.3.3-snap202011021058 grub2-xcat=1.0 ipmitool-xcat=>= 1.8.18-4 +ipxe-xcat=2.0.0 syslinux-xcat=>= 6.03-1 xnba-undi=>= 1.21.1-1 perl-Crypt-Rijndael=1.13 @@ -313,6 +315,7 @@ xCAT-genesis-base=>= 2:2.18.0 goconserver=>= 0.3.3-snap202011021058 grub2-xcat=1.0 ipmitool-xcat=>= 1.8.18-4 +ipxe-xcat=2.0.0 syslinux-xcat=>= 6.03-1 xnba-undi=>= 1.21.1-1 perl-Crypt-Rijndael=1.13 diff --git a/t/openeuler.t b/t/openeuler.t index 4e6417e..1d9fa41 100644 --- a/t/openeuler.t +++ b/t/openeuler.t @@ -78,4 +78,10 @@ for my $pkg (sort keys %{$manifest{$reference}}) { # owner for the package as well. my $plan = eval { load_inputs("$RealBin/..", $manifest{'openeuler-24.03-ppc64le'}) }; ok($plan, 'every package the ppc64le manifest requires has a native output owner') or diag($@); +# xCAT requires ipxe-xcat on x86_64 (xCAT.spec, upstream 32a688b3), so an x86_64 openEuler section +# without it builds a repo where "dnf install xCAT" answers "nothing provides ipxe-xcat". +for my $target (map { "openeuler-$_-x86_64" } qw(20.03sp4 22.03sp4 24.03sp4)) { + ok(exists $manifest{$target}{'ipxe-xcat'}, "$target declares ipxe-xcat, which xCAT requires on x86_64"); +} + done_testing();