2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-04 20:17:55 +00:00

Merge pull request #7767 from VersatusHPC/refactor/commandutils-executable-finder

refactor(utils): centralize executable lookup
This commit is contained in:
Daniel Hilst
2026-09-03 14:55:14 -03:00
committed by GitHub
20 changed files with 346 additions and 84 deletions
+1
View File
@@ -363,6 +363,7 @@ then
#3 symbolic link can't work during package
if [ $file_low = "xcat-probe" ]; then
mkdir -p ${CURDIR}/lib/perl/xCAT/
cp -f ${CURDIR}/../perl-xCAT/xCAT/CommandUtils.pm ${CURDIR}/lib/perl/xCAT/
cp -f ${CURDIR}/../perl-xCAT/xCAT/NetworkUtils.pm ${CURDIR}/lib/perl/xCAT/
cp -f ${CURDIR}/../perl-xCAT/xCAT/GlobalDef.pm ${CURDIR}/lib/perl/xCAT/
cp -f ${CURDIR}/../perl-xCAT/xCAT/ServiceNodeUtils.pm ${CURDIR}/lib/perl/xCAT/
+1
View File
@@ -141,6 +141,7 @@ sub usage {
}
use constant XCAT_PROBE_HELPERS => qw(
CommandUtils.pm
GlobalDef.pm
NetworkUtils.pm
ServiceNodeUtils.pm
+1
View File
@@ -65,6 +65,7 @@ system('mkdir', '-p', map { "$ENV{HOME}/rpmbuild/$_" } qw(SOURCES SPECS BUILD BU
my $VERSION = read_line("Version") // die "Cannot read Version\n";
my $PWD = Cwd::cwd();
my @XCAT_PROBE_HELPERS = qw(
CommandUtils.pm
GlobalDef.pm
NetworkUtils.pm
ServiceNodeUtils.pm
+54
View File
@@ -0,0 +1,54 @@
package xCAT::CommandUtils;
use strict;
use warnings;
our @SYSTEM_FALLBACK_DIRS = qw(
/usr/sbin
/usr/bin
/sbin
/bin
);
=head1 NAME
xCAT::CommandUtils - dependency-light command lookup helpers
=head1 FUNCTIONS
=head2 find_executable
Find an executable by searching PATH in order, followed by the standard system
directories. Pass C<path =E<gt> $value> to search that value instead of the
process PATH. Like the callers this function replaces, empty entries and an
entry named C<0> are ignored. Pass C<fallback_dirs =E<gt> []> to disable the
standard fallbacks, or supply an array reference to replace them. The matching
candidate path is returned, or undef when none is found.
=cut
sub find_executable {
my ( $command, %args ) = @_;
return unless defined($command) && length($command);
my $path = exists( $args{path} ) ? $args{path} : $ENV{PATH};
foreach my $dir ( split /:/, $path || '' ) {
next unless $dir;
my $candidate = "$dir/$command";
return $candidate if -x $candidate;
}
my $fallback_dirs = exists( $args{fallback_dirs} )
? $args{fallback_dirs}
: \@SYSTEM_FALLBACK_DIRS;
foreach my $dir ( @{ $fallback_dirs || [] } ) {
next unless defined($dir) && length($dir);
my $candidate = "$dir/$command";
return $candidate if -x $candidate;
}
return;
}
1;
+2 -12
View File
@@ -3,6 +3,7 @@ package xCAT::DHCP::Backend;
use strict;
use warnings;
use xCAT::CommandUtils;
use xCAT::StringUtils qw(trim);
my %valid_backend = map { $_ => 1 } qw(auto isc kea);
@@ -142,18 +143,7 @@ sub _osver {
sub _command_exists {
my ($command) = @_;
foreach my $dir ( split /:/, $ENV{PATH} || '' ) {
next unless $dir;
my $path = "$dir/$command";
return 1 if -x $path;
}
foreach my $path ( "/usr/sbin/$command", "/usr/bin/$command", "/sbin/$command", "/bin/$command" ) {
return 1 if -x $path;
}
return 0;
return xCAT::CommandUtils::find_executable($command) ? 1 : 0;
}
1;
+2 -12
View File
@@ -8,6 +8,7 @@ use File::Basename;
use File::Path qw/make_path/;
use Math::BigInt;
use Text::ParseWords qw/shellwords/;
use xCAT::CommandUtils;
use xCAT::DHCP::Range;
use xCAT::NetworkUtils;
@@ -1206,18 +1207,7 @@ sub _expand_kea_build_path {
sub _command_path {
my ($command) = @_;
foreach my $dir ( split /:/, $ENV{PATH} || '' ) {
next unless $dir;
my $path = "$dir/$command";
return $path if -x $path;
}
foreach my $path ( "/usr/sbin/$command", "/usr/bin/$command", "/sbin/$command", "/bin/$command" ) {
return $path if -x $path;
}
return;
return xCAT::CommandUtils::find_executable($command);
}
sub _shell_quote {
+2 -5
View File
@@ -8,6 +8,7 @@ use File::Copy;
use Time::Local;
use Socket;
use List::Util qw/sum/;
use xCAT::CommandUtils;
#-----------------------------------------
@@ -175,11 +176,7 @@ sub _netplan_get {
sub _command_available {
my $cmd = shift;
for my $dir (split /:/, $ENV{PATH} || '') {
next unless $dir;
return 1 if -x "$dir/$cmd";
}
return 0;
return xCAT::CommandUtils::find_executable( $cmd, fallback_dirs => [] ) ? 1 : 0;
}
sub _capture_command {
+2 -12
View File
@@ -24,6 +24,7 @@ use Getopt::Long;
Getopt::Long::Configure("bundling");
Getopt::Long::Configure("pass_through");
use Socket;
use xCAT::CommandUtils;
my $candoipv6 = eval {
require Socket6;
1;
@@ -3185,18 +3186,7 @@ sub local_ipv4_routes
sub kea_command_path
{
my ($command) = @_;
foreach my $dir (split /:/, $ENV{PATH} || '') {
next unless $dir;
my $path = "$dir/$command";
return $path if -x $path;
}
foreach my $path ( "/usr/sbin/$command", "/usr/bin/$command", "/sbin/$command", "/bin/$command" ) {
return $path if -x $path;
}
return;
return xCAT::CommandUtils::find_executable($command);
}
sub kea_subnet4_intent
@@ -5,7 +5,7 @@ cmd:mkdir -p /tmp/xcatprobe_l
cmd:xcatprobe -l
check:rc==0
check:output=~Supported sub commands are:
cmd:for module in GlobalDef.pm NetworkUtils.pm ServiceNodeUtils.pm; do test -r "/opt/xcat/probe/lib/perl/xCAT/$module" || exit 1; done
cmd:for module in CommandUtils.pm GlobalDef.pm NetworkUtils.pm ServiceNodeUtils.pm; do test -r "/opt/xcat/probe/lib/perl/xCAT/$module" || exit 1; done
check:rc==0
cmd:xcatprobe -l|grep -v "Supported sub commands are" |awk '/^[[:graph:]]/ {print $1}'|sort > /tmp/xcatprobe_l/subcmd_from_xcatprobe_l
cmd:ls -l /opt/xcat/probe/subcmds/ |awk '/^-/ {print $9}'|sort > /tmp/xcatprobe_l/subcmd_under_subcmds_dir
@@ -8,9 +8,10 @@ use File::Temp qw/tempfile/;
use JSON ();
use Test::More;
use xCAT::CommandUtils;
use xCAT::DHCP::Backend::Kea;
my $kea_dhcp4 = command_path('kea-dhcp4');
my $kea_dhcp4 = xCAT::CommandUtils::find_executable('kea-dhcp4');
plan skip_all => 'kea-dhcp4 is not installed' unless $kea_dhcp4;
my $validation_dir = validation_temp_dir($kea_dhcp4);
@@ -106,7 +107,7 @@ ok( !$result->{error}, 'generated Kea DHCPv4 config validates with kea-dhcp4 -t'
unlink $path;
SKIP: {
skip 'kea-dhcp6 is not installed', 1 unless command_path('kea-dhcp6');
skip 'kea-dhcp6 is not installed', 1 unless xCAT::CommandUtils::find_executable('kea-dhcp6');
my $dhcp6_json = $backend->render_dhcp6_config(
{
interfaces => ['*'],
@@ -150,7 +151,7 @@ SKIP: {
}
SKIP: {
skip 'kea-dhcp-ddns is not installed', 1 unless command_path('kea-dhcp-ddns');
skip 'kea-dhcp-ddns is not installed', 1 unless xCAT::CommandUtils::find_executable('kea-dhcp-ddns');
my $ddns_json = $backend->render_ddns_config(
{
'tsig-keys' => [
@@ -180,7 +181,7 @@ SKIP: {
}
SKIP: {
skip 'kea-ctrl-agent is not installed', 1 unless command_path('kea-ctrl-agent');
skip 'kea-ctrl-agent is not installed', 1 unless xCAT::CommandUtils::find_executable('kea-ctrl-agent');
my $ctrl_agent_json = $backend->render_ctrl_agent_config(
{
dhcp6 => 1,
@@ -195,21 +196,6 @@ SKIP: {
}
done_testing();
sub command_path {
my ($command) = @_;
foreach my $dir ( split /:/, $ENV{PATH} || '' ) {
next unless $dir;
return "$dir/$command" if -x "$dir/$command";
}
foreach my $path ( "/usr/sbin/$command", "/usr/bin/$command", "/sbin/$command", "/bin/$command" ) {
return $path if -x $path;
}
return;
}
sub validation_temp_dir {
my ($kea_dhcp4) = @_;
@@ -11,14 +11,15 @@ use POSIX qw/WNOHANG _exit setgid setuid/;
use Test::More;
use Time::HiRes qw/sleep time/;
use xCAT::CommandUtils;
use xCAT::DHCP::Backend::Kea;
plan skip_all => 'set XCAT_KEA_LIVE_SMOKE=1 to run live Kea daemon smoke test'
unless $ENV{XCAT_KEA_LIVE_SMOKE};
plan skip_all => 'live Kea daemon smoke test must run as root' unless $> == 0;
my $kea_dhcp4 = command_path('kea-dhcp4');
my $kea_ctrl = command_path('kea-ctrl-agent');
my $kea_dhcp4 = xCAT::CommandUtils::find_executable('kea-dhcp4');
my $kea_ctrl = xCAT::CommandUtils::find_executable('kea-ctrl-agent');
plan skip_all => 'kea-dhcp4 and kea-ctrl-agent are required'
unless $kea_dhcp4 && $kea_ctrl;
@@ -192,21 +193,6 @@ SKIP: {
stop_daemons(\%children);
done_testing();
sub command_path {
my ($command) = @_;
foreach my $dir ( split /:/, $ENV{PATH} || '' ) {
next unless $dir;
return "$dir/$command" if -x "$dir/$command";
}
foreach my $path ( "/usr/sbin/$command", "/usr/bin/$command", "/sbin/$command", "/bin/$command" ) {
return $path if -x $path;
}
return;
}
sub start_daemon {
my ( $account, $command, $log, @args ) = @_;
my $pid = fork();
+210
View File
@@ -0,0 +1,210 @@
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/../../perl-xCAT";
use Cwd qw/getcwd/;
use File::Path qw/make_path/;
use File::Slurper qw/write_text/;
use File::Temp qw/tempdir/;
use Test::More;
use xCAT::CommandUtils;
is_deeply(
\@xCAT::CommandUtils::SYSTEM_FALLBACK_DIRS,
[qw(/usr/sbin /usr/bin /sbin /bin)],
'the built-in system fallback order matches the replaced callers'
);
my $root = tempdir(CLEANUP => 1);
my $first_dir = "$root/first";
my $second_dir = "$root/second";
my $fallback_dir = "$root/fallback";
make_path( $first_dir, $second_dir, $fallback_dir, "$root/relative" );
sub write_executable {
my ($path) = @_;
write_text( $path, "#!/bin/sh\nexit 0\n" );
chmod 0755, $path or die "Unable to make $path executable: $!";
return $path;
}
my $first_tool = write_executable("$first_dir/xcat-command-utils-tool");
my $second_tool = write_executable("$second_dir/xcat-command-utils-tool");
is(
xCAT::CommandUtils::find_executable(
'xcat-command-utils-tool',
path => "$first_dir:$second_dir",
fallback_dirs => [],
),
$first_tool,
'PATH entries are searched in order'
);
chmod 0644, $first_tool or die "Unable to remove execute permission from $first_tool: $!";
is(
xCAT::CommandUtils::find_executable(
'xcat-command-utils-tool',
path => "$first_dir:$second_dir",
fallback_dirs => [],
),
$second_tool,
'non-executable candidates are skipped'
);
{
local $ENV{PATH} = $second_dir;
is(
xCAT::CommandUtils::find_executable('xcat-command-utils-tool'),
$second_tool,
'the process PATH is used by default'
);
is(
xCAT::CommandUtils::find_executable(
'xcat-command-utils-tool',
path => $first_dir,
fallback_dirs => [],
),
undef,
'an explicit path overrides the process PATH'
);
}
my $fallback_tool = write_executable("$fallback_dir/xcat-command-utils-fallback");
is(
xCAT::CommandUtils::find_executable(
'xcat-command-utils-fallback',
path => '',
fallback_dirs => [ '', $fallback_dir ],
),
$fallback_tool,
'custom fallback directories are searched after PATH and ignore empty entries'
);
is(
xCAT::CommandUtils::find_executable(
'xcat-command-utils-fallback',
path => '',
fallback_dirs => [],
),
undef,
'fallback lookup can be disabled'
);
is(
xCAT::CommandUtils::find_executable(
'bin',
path => '',
fallback_dirs => [''],
),
undef,
'empty fallback entries are ignored instead of matching executable root directories'
);
is(
xCAT::CommandUtils::find_executable(
'xcat-command-utils-tool',
path => '',
fallback_dirs => [$first_dir],
),
undef,
'non-executable fallback candidates are skipped'
);
my $fallback_order_first = write_executable("$first_dir/xcat-command-utils-fallback-order");
write_executable("$second_dir/xcat-command-utils-fallback-order");
is(
xCAT::CommandUtils::find_executable(
'xcat-command-utils-fallback-order',
path => '',
fallback_dirs => [ $first_dir, $second_dir ],
),
$fallback_order_first,
'custom fallback directories are searched in order'
);
my $path_collision = write_executable("$second_dir/xcat-command-utils-collision");
write_executable("$fallback_dir/xcat-command-utils-collision");
is(
xCAT::CommandUtils::find_executable(
'xcat-command-utils-collision',
path => $second_dir,
fallback_dirs => [$fallback_dir],
),
$path_collision,
'PATH matches take precedence over fallback matches'
);
my $system_shell = xCAT::CommandUtils::find_executable( 'sh', path => '' );
my ($expected_system_shell) = grep { -x "$_/sh" } qw(/usr/sbin /usr/bin /sbin /bin);
is(
$system_shell,
defined($expected_system_shell) ? "$expected_system_shell/sh" : undef,
'standard system directories are searched by default'
);
my $original_dir = getcwd();
chdir($root) or die "Unable to enter $root: $!";
is(
xCAT::CommandUtils::find_executable(
'bin',
path => ":$second_dir",
fallback_dirs => [],
),
undef,
'empty PATH entries are ignored instead of matching executable root directories'
);
write_executable("$root/relative/xcat-command-utils-relative");
is(
xCAT::CommandUtils::find_executable(
'xcat-command-utils-relative',
path => 'relative',
fallback_dirs => [],
),
'relative/xcat-command-utils-relative',
'relative PATH entries preserve the existing candidate path shape'
);
make_path("$root/0");
write_executable("$root/0/xcat-command-utils-zero-entry");
is(
xCAT::CommandUtils::find_executable(
'xcat-command-utils-zero-entry',
path => "0:$second_dir",
fallback_dirs => [],
),
undef,
'a PATH component named zero retains the existing falsy-entry behavior'
);
chdir($original_dir) or die "Unable to restore $original_dir: $!";
my $marker = "$root/shell-was-invoked";
my $literal_name = 'xcat-command-utils;touch shell-was-invoked';
my $literal_tool = write_executable("$second_dir/$literal_name");
chdir($root) or die "Unable to enter $root for shell-safety lookup: $!";
is(
xCAT::CommandUtils::find_executable(
$literal_name,
path => $second_dir,
fallback_dirs => [],
),
$literal_tool,
'command names are treated as filesystem paths without shell interpretation'
);
ok( !-e $marker, 'executable lookup never invokes a shell' );
chdir($original_dir) or die "Unable to restore $original_dir: $!";
is(
xCAT::CommandUtils::find_executable( undef, path => $second_dir ),
undef,
'an undefined command is not searched'
);
is(
xCAT::CommandUtils::find_executable( '', path => $second_dir ),
undef,
'an empty command is not searched'
);
done_testing();
+8
View File
@@ -307,4 +307,12 @@ is(
'forced Kea succeeds when available'
);
{
local $ENV{PATH} = '';
ok(
xCAT::DHCP::Backend::_command_exists('sh'),
'backend availability retains the standard system-directory fallback'
);
}
done_testing();
+13 -6
View File
@@ -4,12 +4,14 @@ use warnings;
no warnings 'once';
use FindBin;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use File::Temp qw(tempdir);
use Socket ();
use Test::More;
use XCAT::Test::File qw(repo_path);
BEGIN {
package xCAT::Table;
@@ -81,12 +83,8 @@ require xCAT::Utils;
*xCAT::Utils::runcmd = sub { return; };
}
my $source_dhcp_plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/dhcp.pm";
if ( -f $source_dhcp_plugin ) {
require $source_dhcp_plugin;
} else {
require xCAT_plugin::dhcp;
}
my $source_dhcp_plugin = repo_path('xCAT-server/lib/xcat/plugins/dhcp.pm');
require $source_dhcp_plugin;
require xCAT::DHCP::Backend::Kea;
{
@@ -162,6 +160,15 @@ foreach my $case (@sysconfig_policy_cases) {
close($ip_fh);
chmod 0755, $fake_ip;
{
local $ENV{PATH} = $tmpdir;
is(
xCAT_plugin::dhcp::kea_command_path('ip'),
$fake_ip,
'DHCP route command lookup retains the first executable PATH match'
);
}
no warnings 'redefine';
local *xCAT_plugin::dhcp::kea_command_path = sub {
my ($command) = @_;
+9
View File
@@ -580,6 +580,15 @@ FAKE_KEA
close($fake_kea_fh) or die "Unable to close fake Kea command: $!";
chmod 0755, $fake_kea_dhcp4 or die "Unable to make fake Kea command executable: $!";
{
local $ENV{PATH} = $unit_dir;
is(
xCAT::DHCP::Backend::Kea::_command_path('kea-dhcp4-build-report'),
$fake_kea_dhcp4,
'Kea command lookup retains the first executable PATH match'
);
}
my $command_socket_backend = xCAT::DHCP::Backend::Kea->new(kea_dhcp4_command => $fake_kea_dhcp4);
is( $command_socket_backend->control_socket_path('kea4-ctrl-socket'), '/xcat-test-command-run/kea/kea4-ctrl-socket', 'socket path comes from the Kea build-report command' );
+1
View File
@@ -3,6 +3,7 @@ use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-probe/lib/perl";
use Test::More;
+1
View File
@@ -3,6 +3,7 @@ use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-probe/lib/perl";
use File::Slurper qw(write_text);
+1
View File
@@ -3,6 +3,7 @@ use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-probe/lib/perl";
use File::Temp qw(tempdir);
@@ -3,6 +3,7 @@ use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-probe/lib/perl";
use Test::More;
@@ -49,6 +50,14 @@ ok(probe_utils::_tcp_listener_output_has_port($netstat_output, 80, qr/httpd|apac
ok(!probe_utils::_tcp_listener_output_has_port($netstat_output, 0), 'invalid port is rejected');
ok(!probe_utils::_tcp_listener_output_has_port($netstat_output, 'http'), 'non-numeric port is rejected');
{
local $ENV{PATH} = '';
ok(
!probe_utils::_command_available('sh'),
'probe command discovery retains PATH-only lookup without system fallbacks'
);
}
{
no warnings 'redefine';
my @commands;
@@ -9,11 +9,14 @@ use File::Spec;
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/../../build-utils/lib";
use Test::More;
use XCAT::BuildUtils qw(XCAT_PROBE_HELPERS);
use XCAT::Test::File qw(repo_path slurp_repo_file);
my @helpers = qw(
CommandUtils.pm
GlobalDef.pm
NetworkUtils.pm
ServiceNodeUtils.pm
@@ -26,6 +29,9 @@ my @affected_subcommands = qw(
);
my $builder = slurp_repo_file('buildrpms.pl');
my $debian_builder = slurp_repo_file('build-ubunturepo');
my $installed_probe_test =
slurp_repo_file('xCAT-test/autotest/testcase/probe/xcatproble_list');
my $rpm_spec = slurp_repo_file('xCAT-probe/xCAT-probe.spec');
my $debian_control = slurp_repo_file('xCAT-probe/debian/control');
like($builder, qr/sub prepare_xcat_probe_source_tar\b/, 'RPM builder has dedicated xCAT-probe source preparation');
@@ -65,6 +71,20 @@ for my $helper (@helpers) {
my $source = repo_path(File::Spec->catfile('perl-xCAT', 'xCAT', $helper));
ok(-f $source, "$helper source exists");
like($builder, qr/^\s*\Q$helper\E\s*$/m, "RPM builder stages $helper");
ok(
scalar(grep { $_ eq $helper } XCAT_PROBE_HELPERS),
"the shared builder helper list carries $helper"
);
like(
$debian_builder,
qr{cp -f [^\n]*/perl-xCAT/xCAT/\Q$helper\E\s+[^\n]*/lib/perl/xCAT/},
"Debian builder stages $helper"
);
like(
$installed_probe_test,
qr/cmd:for module in [^;]*\b\Q$helper\E\b[^;]*; do test -r/,
"installed probe payload checks $helper"
);
}
my $tmpdir = tempdir(CLEANUP => 1);