From 4aa1db0dce921a3b95a83b0790165a50b84497af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Mon, 31 Aug 2026 19:53:01 -0300 Subject: [PATCH 1/2] refactor(getadapter): share PCI adapter scan --- xCAT-genesis-scripts/usr/bin/getadapter | 51 +++++++++++-------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/xCAT-genesis-scripts/usr/bin/getadapter b/xCAT-genesis-scripts/usr/bin/getadapter index b090ba86e..717b70540 100755 --- a/xCAT-genesis-scripts/usr/bin/getadapter +++ b/xCAT-genesis-scripts/usr/bin/getadapter @@ -18,6 +18,24 @@ transmit_adapter_info() { fi } +append_missing_pci_adapters() { + local adapter_pattern=$1 + local adapter_file=$2 + local model_separator=$3 + local pci + local tmp + + for pci in $(lspci | awk -v adapter_pattern="$adapter_pattern" '$0 ~ adapter_pattern {print $1}'); do + if ! grep -q "$pci" "$adapter_file" 2>/dev/null; then + tmp="$(lspci | grep "$pci")" + echo " +$pci +${tmp##*"$model_separator"} +" >> "$adapter_file" + fi + done +} + rm -f "$SCANNICLOG" >/dev/null 2>&1 if [ -f "$ADAPTERFILE" ]; then @@ -66,35 +84,10 @@ for n in /sys/class/net/*; do fi done -for pci in $(lspci |awk '/Ethernet/ {print $1}'); do - if ! grep -q "$pci" "$ADAPTERFILE" 2>/dev/null; then - tmp="$(lspci |grep "$pci")" - echo " -$pci -${tmp##*:} -" >> "$ADAPTERFILE" - fi -done - -for pci in $(lspci |awk '/Network/ {print $1}'); do - if ! grep -q "$pci" "$ADAPTERFILE" 2>/dev/null; then - tmp="$(lspci |grep "$pci")" - echo " -$pci -${tmp##*: } -" >> "$ADAPTERFILE" - fi -done - -for pci in $(lspci |awk '/Mellanox/ {print $1}'); do - if ! grep -q "$pci" "$ADAPTERFILE" 2>/dev/null; then - tmp="$(lspci |grep "$pci")" - echo " -$pci -${tmp##*: } -" >> "$ADAPTERFILE" - fi -done +# Keep the legacy Ethernet model spacing while sharing the scan. +append_missing_pci_adapters "Ethernet" "$ADAPTERFILE" ":" +append_missing_pci_adapters "Network" "$ADAPTERFILE" ": " +append_missing_pci_adapters "Mellanox" "$ADAPTERFILE" ": " echo "" >> "$ADAPTERFILE" From ac77bec6fbbcb10c3baf8309b1a0cc06efb48c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Mon, 31 Aug 2026 19:55:08 -0300 Subject: [PATCH 2/2] test(getadapter): cover shared PCI adapter scan --- xCAT-test/unit/getadapter_pci_scan.t | 222 +++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 xCAT-test/unit/getadapter_pci_scan.t diff --git a/xCAT-test/unit/getadapter_pci_scan.t b/xCAT-test/unit/getadapter_pci_scan.t new file mode 100644 index 000000000..4d384e170 --- /dev/null +++ b/xCAT-test/unit/getadapter_pci_scan.t @@ -0,0 +1,222 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Path qw(make_path); +use File::Spec; +use File::Temp qw(tempdir); +use FindBin; +use lib "$FindBin::Bin/../lib"; +use POSIX qw(_exit); +use Test::More; + +use XCAT::Test::File qw(repo_path); + +my $source_getadapter; +if ( defined $ENV{XCAT_TEST_GETADAPTER} ) { + $source_getadapter = $ENV{XCAT_TEST_GETADAPTER}; + die "XCAT_TEST_GETADAPTER=$source_getadapter is not a readable file" + unless length($source_getadapter) + && -f $source_getadapter + && -r _; +} +else { + $source_getadapter = + repo_path('xCAT-genesis-scripts/usr/bin/getadapter'); +} +plan skip_all => "$source_getadapter is required" + unless -f $source_getadapter && -r _; + +my $tmpdir = tempdir( CLEANUP => 1 ); +my $test_bin = File::Spec->catdir( $tmpdir, 'bin' ); +my $sys_class_net = File::Spec->catdir( $tmpdir, 'sys', 'class', 'net' ); +my $adapter_file = File::Spec->catfile( $tmpdir, 'adapterinfo' ); +my $scan_log = File::Spec->catfile( $tmpdir, 'adapterscan.log' ); +my $getadapter = File::Spec->catfile( $tmpdir, 'getadapter' ); +my $stdout_file = File::Spec->catfile( $tmpdir, 'stdout' ); +my $stderr_file = File::Spec->catfile( $tmpdir, 'stderr' ); +my $test_interface = File::Spec->catdir( $sys_class_net, 'eth0' ); +make_path( $test_bin, $test_interface ); + +my $getadapter_body = read_file($source_getadapter); +my $adapter_file_rewrites = + $getadapter_body =~ s{/tmp/adapterinfo}{$adapter_file}g; +my $scan_log_rewrites = + $getadapter_body =~ s{/tmp/adapterscan\.log}{$scan_log}g; +my $sysfs_rewrites = + $getadapter_body =~ s{/sys/class/net}{$sys_class_net}g; +die 'Unable to sandbox getadapter adapter file' + unless $adapter_file_rewrites; +die 'Unable to sandbox getadapter scan log' + unless $scan_log_rewrites; +die 'Unable to sandbox getadapter sysfs paths' + unless $sysfs_rewrites; +write_executable( $getadapter, $getadapter_body ); +write_file( File::Spec->catfile( $test_interface, 'address' ), + "aa:bb:cc:dd:ee:ff\n" ); + +is( system( 'bash', '-n', $getadapter ), 0, + 'getadapter has valid Bash syntax' ); + +my $lspci_fixture = File::Spec->catfile( $tmpdir, 'lspci.txt' ); +my $lspci_log = File::Spec->catfile( $tmpdir, 'lspci.log' ); +my $request_copy = File::Spec->catfile( $tmpdir, 'request.xml' ); + +write_executable( + File::Spec->catfile( $test_bin, 'lspci' ), + <<'SH' +#!/bin/sh +printf 'lspci\n' >>"$XCAT_TEST_LSPCI_LOG" +/bin/cat "$XCAT_TEST_LSPCI_FIXTURE" +SH +); + +write_executable( + File::Spec->catfile( $test_bin, 'udevadm' ), + <<'SH' +#!/bin/sh +interface=${2##*/} +printf 'E: INTERFACE=%s\n' "$interface" +printf 'E: DEVPATH=/devices/pci0000:00/0000:01:00.0/net/%s\n' "$interface" +SH +); + +write_executable( + File::Spec->catfile( $test_bin, 'ip' ), + "#!/bin/sh\nexit 0\n" +); + +write_executable( + File::Spec->catfile( $test_bin, 'openssl' ), + <<'SH' +#!/bin/sh +/bin/cat >"$XCAT_TEST_REQUEST_COPY" +SH +); + +write_file( + $lspci_fixture, + <<'LSPCI' +01:00.0 Ethernet controller: Existing Ethernet Adapter +02:00.0 Ethernet controller: Mellanox Ethernet Adapter +03:00.0 Network controller: Mellanox Network Adapter +04:00.0 Network controller: Wireless Adapter +05:00.0 Audio device: Example Audio Device +06:00.0 Infiniband controller: Mellanox Technologies MT27800 +LSPCI +); + +my ( $status, $stdout, $stderr ) = run_getadapter(); +is( $status, 0, 'getadapter completes with overlapping PCI classes' ); +is( $stdout, '', 'getadapter keeps stdout empty' ); +is( $stderr, '', 'getadapter keeps stderr empty' ); + +my $request = read_file($adapter_file); +is( read_file($request_copy), $request, + 'getadapter transmits the generated request unchanged' ); +like( + $request, + qr{\A\ngetadapter\nupdate}, + 'getadapter preserves the request header' +); +like( $request, qr{\n\z}, + 'getadapter preserves the request terminator' ); + +my @missing_pci = $request =~ m{([0-9][^<]+)}g; +is_deeply( + \@missing_pci, + [ qw(02:00.0 03:00.0 04:00.0 06:00.0) ], + 'missing adapters retain Ethernet, Network, and Mellanox scan order' +); + +my @missing_models = $request =~ m{([^<]*)}g; +is_deeply( + \@missing_models, + [ + ' Mellanox Ethernet Adapter', + 'Mellanox Network Adapter', + 'Wireless Adapter', + 'Mellanox Technologies MT27800' + ], + 'missing adapters retain their legacy model spacing' +); +unlike( $request, qr{.*Existing Ethernet Adapter}, + 'an adapter already recorded by sysfs is not appended again' ); +is( line_count($lspci_log), 7, + 'overlapping Mellanox adapters do not trigger duplicate detail lookups' ); + +write_file( $lspci_fixture, + "05:00.0 Audio device: Example Audio Device\n" ); +write_file( $lspci_log, '' ); + +( $status, $stdout, $stderr ) = run_getadapter(); +is( $status, 0, 'getadapter completes when no PCI class matches' ); +is( $stdout, '', 'an empty PCI scan keeps stdout empty' ); +is( $stderr, '', 'an empty PCI scan keeps stderr empty' ); +$request = read_file($adapter_file); +my @empty_models = $request =~ m{([^<]*)}g; +is_deeply( \@empty_models, [], + 'an empty PCI scan does not append missing adapter models' ); +is( line_count($lspci_log), 3, + 'an empty PCI scan queries each adapter class once' ); + +done_testing(); + +sub run_getadapter +{ + local %ENV = ( + %ENV, + PATH => "$test_bin:$ENV{PATH}", + XCATMASTER => '192.0.2.1', + XCAT_TEST_LSPCI_FIXTURE => $lspci_fixture, + XCAT_TEST_LSPCI_LOG => $lspci_log, + XCAT_TEST_REQUEST_COPY => $request_copy, + ); + + my $pid = fork(); + die "Unable to fork getadapter: $!" unless defined $pid; + if ( $pid == 0 ) { + open( STDOUT, '>:raw', $stdout_file ) or _exit(126); + open( STDERR, '>:raw', $stderr_file ) or _exit(126); + exec 'bash', $getadapter or _exit(127); + } + my $reaped = waitpid( $pid, 0 ); + my $raw_status = $?; + my $status = $reaped == $pid && !( $raw_status & 127 ) + ? $raw_status >> 8 + : 255; + my $stdout = read_file($stdout_file) // ''; + my $stderr = read_file($stderr_file) // ''; + return ( $status, $stdout, $stderr ); +} + +sub write_executable +{ + my ( $path, $contents ) = @_; + write_file( $path, $contents ); + chmod 0755, $path or die "Unable to make $path executable: $!"; +} + +sub write_file +{ + my ( $path, $contents ) = @_; + open( my $fh, '>:raw', $path ) or die "Unable to write $path: $!"; + print {$fh} $contents; + close($fh) or die "Unable to close $path: $!"; +} + +sub read_file +{ + my ($path) = @_; + open( my $fh, '<:raw', $path ) or die "Unable to read $path: $!"; + my $contents = do { local $/; <$fh> }; + close($fh) or die "Unable to close $path: $!"; + return $contents; +} + +sub line_count +{ + my ($path) = @_; + my $contents = read_file($path); + return scalar grep { length($_) } split /\n/, $contents; +}