From 4317b5ec75fab75d0befcde0fead5c38a1376962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:19:20 -0300 Subject: [PATCH 1/2] refactor(postscripts): share RPM package manager discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT/postscripts/ospkgs | 6 +----- xCAT/postscripts/otherpkgs | 6 +----- xCAT/postscripts/xcatpkgutils.sh | 12 ++++++++++++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/xCAT/postscripts/ospkgs b/xCAT/postscripts/ospkgs index e50b3c4da..3d164d3f3 100755 --- a/xCAT/postscripts/ospkgs +++ b/xCAT/postscripts/ospkgs @@ -910,11 +910,7 @@ elif ( pmatch "$OSVER" "ubuntu*" ); then fi else #check if yum or dnf is installed - if [ -x /usr/bin/dnf ]; then - yumcmd="dnf" - elif [ -x /usr/bin/yum ]; then - yumcmd="yum" - else + if ! yumcmd=$(xcat_find_rpm_package_manager); then echo "Please install yum or dnf on $NODE." exit 1; fi diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index 6dbc9707a..eedbc09f2 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -509,12 +509,8 @@ else result=`rpm --version 2>/dev/null` if [ $? -eq 0 ]; then hasrpm=1 - if [ -x /usr/bin/dnf ]; then + if yumcmd=$(xcat_find_rpm_package_manager); then hasyum=1 - yumcmd="dnf" - elif [ -x /usr/bin/yum ]; then - hasyum=1 - yumcmd="yum" else result=`rpm -q zypper` if [ "$?" = "0" ]; then diff --git a/xCAT/postscripts/xcatpkgutils.sh b/xCAT/postscripts/xcatpkgutils.sh index 8c91b5239..7b3798b9f 100755 --- a/xCAT/postscripts/xcatpkgutils.sh +++ b/xCAT/postscripts/xcatpkgutils.sh @@ -2,6 +2,18 @@ # EPL license http://www.eclipse.org/legal/epl-v10.html # Shared POSIX shell helpers for the ospkgs and otherpkgs postscripts. + +xcat_find_rpm_package_manager() +{ + if [ -x "${1:-/usr/bin}/dnf" ]; then + printf '%s\n' dnf + elif [ -x "${1:-/usr/bin}/yum" ]; then + printf '%s\n' yum + else + return 1 + fi +} + # Keep the marker assignment last so callers know the whole library loaded. # shellcheck disable=SC2034 XCATPKGUTILS_LOADED=1 From e4cc04a455a7f82ce118fd17a733cddd69ff208b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:21:14 -0300 Subject: [PATCH 2/2] test(postscripts): cover RPM package manager discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/package_manager_discovery.t | 250 +++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 xCAT-test/unit/package_manager_discovery.t diff --git a/xCAT-test/unit/package_manager_discovery.t b/xCAT-test/unit/package_manager_discovery.t new file mode 100644 index 000000000..552efccad --- /dev/null +++ b/xCAT-test/unit/package_manager_discovery.t @@ -0,0 +1,250 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Copy qw(copy); +use File::Path qw(make_path); +use File::Slurper qw(read_text write_text); +use File::Spec; +use File::Temp qw(tempdir); +use FindBin; +use lib "$FindBin::Bin/../lib"; +use Test::More; + +use XCAT::Test::File qw(repo_path); + +my $postscripts = repo_path(File::Spec->catdir('xCAT', 'postscripts')); +my $library = File::Spec->catfile( $postscripts, 'xcatpkgutils.sh' ); +my $loader = File::Spec->catfile( $postscripts, 'xcatpkgutils-loader.sh' ); + +my $tmpdir = tempdir( CLEANUP => 1 ); +my $dnf_first = File::Spec->catdir( $tmpdir, 'dnf-first' ); +my $yum_only = File::Spec->catdir( $tmpdir, 'yum-only' ); +my $neither = File::Spec->catdir( $tmpdir, 'neither' ); +make_path( $dnf_first, $yum_only, $neither ); +write_executable( File::Spec->catfile( $dnf_first, 'dnf' ), "#!/bin/sh\n" ); +write_executable( File::Spec->catfile( $dnf_first, 'yum' ), "#!/bin/sh\n" ); +write_executable( File::Spec->catfile( $yum_only, 'yum' ), "#!/bin/sh\n" ); + +SKIP: { + skip 'the shared discovery helper is introduced by the production commit', 3 + unless helper_available(); + is_deeply( + [ run_helper($dnf_first) ], + [ 0, "dnf\n" ], + 'dnf is preferred when both RPM package managers are executable' + ); + is_deeply( + [ run_helper($yum_only) ], + [ 0, "yum\n" ], + 'yum is used when dnf is unavailable' + ); + is_deeply( + [ run_helper($neither) ], + [ 1, '' ], + 'discovery fails without output when neither package manager is executable' + ); +} + +my $caller_dir = File::Spec->catdir( $tmpdir, 'callers' ); +my $test_bin = File::Spec->catdir( $caller_dir, 'bin' ); +make_path($test_bin); +stage_callers($caller_dir); +write_executable( File::Spec->catfile( $test_bin, 'logger' ), "#!/bin/sh\nexit 0\n" ); +write_executable( File::Spec->catfile( $test_bin, 'mount' ), "#!/bin/sh\nexit 1\n" ); +write_executable( + File::Spec->catfile( $test_bin, 'uname' ), + "#!/bin/sh\nprintf '%s\\n' Linux\n" +); +write_executable( File::Spec->catfile( $test_bin, 'dpkg' ), "#!/bin/sh\nexit 1\n" ); +write_executable( + File::Spec->catfile( $test_bin, 'rpm' ), + <<'SH' +#!/bin/sh +case "$*" in + --version) exit 0 ;; + -q\ zypper) + [ "$XCAT_PM_SCENARIO" = zypper ] && exit 0 + exit 1 + ;; +esac +if [ -n "${XCAT_RPM_TRACE:-}" ]; then + printf '%s\n' "$*" >> "$XCAT_RPM_TRACE" + exit 0 +fi +exit 1 +SH +); +write_executable( File::Spec->catfile( $test_bin, 'dnf' ), "#!/bin/sh\nexit 0\n" ); +write_executable( File::Spec->catfile( $test_bin, 'yum' ), "#!/bin/sh\nexit 0\n" ); +write_executable( File::Spec->catfile( $test_bin, 'zypper' ), "#!/bin/sh\nexit 0\n" ); + +my $bash_env = File::Spec->catfile( $caller_dir, 'bash-env.sh' ); +# Fake only the absolute executable probes; the staged callers and library stay unchanged. +write_text( + $bash_env, + <<'SH' +function [ +{ + case "$1:$2:$XCAT_PM_SCENARIO" in + -x:/usr/bin/dnf:dnf) return 0 ;; + -x:/usr/bin/dnf:*) return 1 ;; + -x:/usr/bin/yum:dnf|-x:/usr/bin/yum:yum) return 0 ;; + -x:/usr/bin/yum:*) return 1 ;; + esac + builtin [ "$@" +} + +exit() +{ + if builtin [ -n "${XCAT_PM_STATE_TRACE:-}" ]; then + printf '%s|%s|%s|%s|%s\n' "${yumcmd:-}" "${hasrpm:-}" "${hasyum:-}" \ + "${haszypper:-}" "${supdatecommand:-}" \ + > "$XCAT_PM_STATE_TRACE" + fi + builtin exit "$@" +} +SH +); + +my %common_env = ( + ARCH => 'x86_64', + BASH_ENV => $bash_env, + INSTALLDIR => 'INSTALLDIR', + NFSSERVER => 'package-test-server', + NODE => 'node1', + OSVER => 'rocky9', + PATH => "$test_bin:$ENV{PATH}", +); + +for my $scenario ( + [ dnf => 'dnf||||' ], + [ yum => 'yum||||' ], + ) +{ + my ( $name, $expected_state ) = @{$scenario}; + my ( $status, $output, $state ) = run_caller( 'ospkgs', $name ); + is( $status, 0, "ospkgs completes with $name" ) or diag($output); + is( $state, $expected_state, "ospkgs selects $name" ); +} + +my ( $ospkgs_status, $ospkgs_output, $ospkgs_state ) = + run_caller( 'ospkgs', 'neither' ); +is( $ospkgs_status, 1, 'ospkgs still stops when neither dnf nor yum is available' ); +is( $ospkgs_state, '||||', 'ospkgs leaves package-manager state empty on failure' ); +like( + $ospkgs_output, + qr/^Please install yum or dnf on node1\.$/m, + 'ospkgs retains its package-manager installation error' +); + +for my $scenario ( + [ dnf => 'dnf|1|1|0|rpm -Uvh --replacepkgs' ], + [ yum => 'yum|1|1|0|rpm -Uvh --replacepkgs' ], + [ zypper => '|1|0|1|rpm -Uvh --replacepkgs' ], + [ rpm => '|1|0|0|rpm -Uvh --replacepkgs' ], + ) +{ + my ( $name, $expected_state ) = @{$scenario}; + my ( $status, $output, $state ) = run_caller( 'otherpkgs', $name ); + is( $status, 0, "otherpkgs completes with the $name discovery outcome" ) + or diag($output); + is( $state, $expected_state, "otherpkgs retains the $name discovery outcome" ); +} + +like( + read_text( File::Spec->catfile( $caller_dir, 'otherpkgs-rpm-command.trace' ) ), + qr/^-Uvh --replacepkgs package-test\*$/m, + 'otherpkgs executes its raw RPM installation fallback' +); + +done_testing(); + +sub run_helper { + my ($directory) = @_; + return run_command( + '/bin/sh', '-c', '. "$1"; xcat_find_rpm_package_manager "$2"', + 'package-manager-discovery-test', $library, $directory + ); +} + +sub helper_available { + return system( + '/bin/sh', '-c', + '. "$1"; command -v xcat_find_rpm_package_manager >/dev/null 2>&1', + 'package-manager-discovery-test', $library + ) == 0; +} + +sub run_caller { + my ( $caller, $scenario ) = @_; + my $trace = File::Spec->catfile( $caller_dir, "$caller-$scenario.trace" ); + local %ENV = ( %ENV, %common_env ); + $ENV{XCAT_PM_SCENARIO} = $scenario; + $ENV{XCAT_PM_STATE_TRACE} = $trace; + + my @arguments; + if ( $caller eq 'ospkgs' ) { + delete @ENV{qw(OTHERPKGS OTHERPKGS_INDEX UPDATENODE)}; + $ENV{OSPKGS} = 'package-test'; + @arguments = ('--keeprepo'); + } else { + delete @ENV{qw(OSPKGS OTHERPKGS)}; + $ENV{OSVER} = 'custom9'; + if ( $scenario eq 'rpm' ) { + $ENV{OTHERPKGS1} = 'package-test'; + $ENV{OTHERPKGS_INDEX} = 1; + $ENV{XCAT_RPM_TRACE} = File::Spec->catfile( + $caller_dir, 'otherpkgs-rpm-command.trace' + ); + } else { + delete @ENV{qw(OTHERPKGS1 XCAT_RPM_TRACE)}; + $ENV{OTHERPKGS_INDEX} = 0; + } + $ENV{UPDATENODE} = 1; + } + + my ( $status, $output ) = run_command( + File::Spec->catfile( $caller_dir, $caller ), @arguments + ); + my $state = read_text($trace); + chomp($state); + return ( $status, $output, $state ); +} + +sub stage_callers { + my ($directory) = @_; + for my $source ( + $library, $loader, + map { File::Spec->catfile( $postscripts, $_ ) } qw(ospkgs otherpkgs) + ) + { + my $filename = ( File::Spec->splitpath($source) )[2]; + my $destination = File::Spec->catfile( $directory, $filename ); + copy( $source, $destination ) + or die "Unable to stage $source as $destination: $!"; + chmod 0755, $destination + or die "Unable to make $destination executable: $!"; + } +} + +sub write_executable { + my ( $path, $contents ) = @_; + write_text( $path, $contents ); + chmod 0755, $path or die "Unable to make $path executable: $!"; +} + +sub run_command { + my (@command) = @_; + my $pid = open( my $pipe, '-|' ); + die "Unable to fork for @command: $!" unless defined($pid); + if ( $pid == 0 ) { + open( STDERR, '>&', STDOUT ) or die "Unable to merge stderr: $!"; + exec { $command[0] } @command; + die "Unable to execute @command: $!"; + } + + my $output = do { local $/; <$pipe> } // ''; + close($pipe); + return ( $? >> 8, $output ); +}