diff --git a/docs/source/guides/admin-guides/references/man1/rinv.1.rst b/docs/source/guides/admin-guides/references/man1/rinv.1.rst index a1e47f34f..c9764297c 100644 --- a/docs/source/guides/admin-guides/references/man1/rinv.1.rst +++ b/docs/source/guides/admin-guides/references/man1/rinv.1.rst @@ -152,7 +152,9 @@ Calling \ **rinv**\ for VMware will display the UUID/GUID, number of CPUs, amou \ **firm**\ - Retrieves firmware versions. + Retrieves firmware versions. For IBM and Lenovo System x systems managed through + IMM, ``UEFI Version`` reports the active firmware. If the IMM reports a pending + UEFI build ID, \ **rinv**\ reports it separately as ``Pending UEFI Build ID``. diff --git a/xCAT-client/pods/man1/rinv.1.pod b/xCAT-client/pods/man1/rinv.1.pod index 815c9a81a..112ec05e5 100644 --- a/xCAT-client/pods/man1/rinv.1.pod +++ b/xCAT-client/pods/man1/rinv.1.pod @@ -95,7 +95,9 @@ Retrieves serial number. =item B -Retrieves firmware versions. +Retrieves firmware versions. For IBM and Lenovo System x systems managed through +IMM, C reports the active firmware. If the IMM reports a pending +UEFI build ID, B reports it separately as C. =item B diff --git a/xCAT-server/lib/xcat/plugins/ipmi.pm b/xCAT-server/lib/xcat/plugins/ipmi.pm index 5b48d0fd0..11c86818f 100644 --- a/xCAT-server/lib/xcat/plugins/ipmi.pm +++ b/xCAT-server/lib/xcat/plugins/ipmi.pm @@ -3439,13 +3439,16 @@ sub got_bios_date { my $fru = FRU->new(); $fru->rec_type("bios,uefi,firmware"); $fru->desc("UEFI Version"); - my $pending = ""; - if ($sessdata->{biosbuildpending}) { - $pending = " [PENDING: $sessdata->{biosbuildpending}]"; - } - my $value = $sessdata->{biosbuildversion} . " (" . $sessdata->{biosbuildid} . " " . $sessdata->{biosbuilddate} . ")$pending"; + my $value = $sessdata->{biosbuildversion} . " (" . $sessdata->{biosbuildid} . " " . $sessdata->{biosbuilddate} . ")"; $fru->value($value); $sessdata->{fru_hash}->{uefi} = $fru; + if ($sessdata->{biosbuildpending}) { + $fru = FRU->new(); + $fru->rec_type("bios,uefi,firmware"); + $fru->desc("Pending UEFI Build ID"); + $fru->value($sessdata->{biosbuildpending}); + $sessdata->{fru_hash}->{uefi_pending} = $fru; + } get_imm_property(property => "/v2/fpga/build_id", callback => \&got_fpga_buildid, sessdata => $sessdata); } else { initfru_with_mprom($sessdata); diff --git a/xCAT-test/unit/ipmi_pending_uefi.t b/xCAT-test/unit/ipmi_pending_uefi.t new file mode 100644 index 000000000..7559575a2 --- /dev/null +++ b/xCAT-test/unit/ipmi_pending_uefi.t @@ -0,0 +1,234 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +## no critic (Modules::RequireFilenameMatchesPackage, TestingAndDebugging::ProhibitNoStrict, TestingAndDebugging::ProhibitNoWarnings) +no warnings qw(once redefine); + +use File::Spec; +use FindBin; +use Test::More; + +BEGIN { + for my $module (qw( + xCAT::GlobalDef xCAT_monitoring::monitorctrl xCAT::IPMI + xCAT::BMCUtils xCAT::PasswordUtils xCAT::Utils xCAT::TableUtils + xCAT::IMMUtils xCAT::ServiceNodeUtils xCAT::NetworkUtils xCAT::Usage + xCAT::data::ibmhwtypes xCAT::data::ibmleds + xCAT::data::ipmigenericevents xCAT::data::ipmisensorevents + IBM::EnergyManager + )) { + (my $file = $module) =~ s{::}{/}g; + $INC{"$file.pm"} = __FILE__; + no strict 'refs'; + *{"${module}::import"} = sub { }; + } + + package xCAT::SvrUtils; + our @messages; + sub import { } + sub sendmsg { push @messages, $_[0]; } + $INC{'xCAT/SvrUtils.pm'} = __FILE__; + + package xCAT::SPD; + sub import { + my $caller = caller; + no strict 'refs'; + *{"${caller}::decode_spd"} = sub { return; }; + } + $INC{'xCAT/SPD.pm'} = __FILE__; + + package File::Path; + sub import { + my $caller = caller; + no strict 'refs'; + *{"${caller}::mkpath"} = sub { return; }; + } + $INC{'File/Path.pm'} = __FILE__; + + package Thread; + sub import { + my $caller = caller; + no strict 'refs'; + *{"${caller}::yield"} = sub { return; }; + } + $INC{'Thread.pm'} = __FILE__; + + package LWP; + our $VERSION = 5.64; + sub import { } + $INC{'LWP.pm'} = __FILE__; + + package HTTP::Request::Common; + sub import { + my $caller = caller; + no strict 'refs'; + *{"${caller}::GET"} = sub { return; }; + *{"${caller}::POST"} = sub { return; }; + } + $INC{'HTTP/Request/Common.pm'} = __FILE__; + + package main; + *CORE::GLOBAL::chmod = sub (@) { return 1; }; +} + +package main; + +my $repo_root = File::Spec->catdir($FindBin::Bin, '..', '..'); +$ENV{XCATROOT} = File::Spec->catdir($repo_root, 'xCAT-server'); +my $plugin = File::Spec->catfile( + $repo_root, qw(xCAT-server lib xcat plugins ipmi.pm) +); +do $plugin or die $@ || "Unable to load $plugin: $!"; + +sub new_sessdata { + my ($pending) = @_; + return { + biosbuildid => 'TCE123A', + biosbuildpending => $pending, + biosbuildversion => '2.13', + bmcnum => 1, + extraargs => [], + fru_hash => {}, + invtypes => ['firmware'], + isite => 0, + node => 'node01', + }; +} + +sub run_got_bios_date { + my ($sessdata) = @_; + my @calls; + local *xCAT_plugin::ipmi::get_imm_property = sub { + push @calls, { @_ }; + }; + xCAT_plugin::ipmi::got_bios_date( + data => '2026-08-30', + sessdata => $sessdata, + ); + return \@calls; +} + +sub assert_next_property { + my ($calls, $sessdata, $case) = @_; + + is(scalar(@{$calls}), 1, "$case: one next property request"); + is( + $calls->[0]->{property}, + '/v2/fpga/build_id', + "$case: FPGA build ID follows", + ); + is( + $calls->[0]->{callback}, + \&xCAT_plugin::ipmi::got_fpga_buildid, + "$case: correct callback follows", + ); + is( + $calls->[0]->{sessdata}, + $sessdata, + "$case: session data is preserved", + ); +} + +subtest 'pending UEFI build ID is a separate inventory record' => sub { + my $sessdata = new_sessdata('TCE124A'); + my $calls = run_got_bios_date($sessdata); + + is( + $sessdata->{biosbuilddate}, + '2026-08-30', + 'BIOS build date is recorded', + ); + is_deeply( + [sort keys %{ $sessdata->{fru_hash} }], + [qw(uefi uefi_pending)], + 'active and pending keys are exact', + ); + + my $active = $sessdata->{fru_hash}->{uefi}; + isa_ok($active, 'FRU', 'active inventory record'); + is( + $active->rec_type, + 'bios,uefi,firmware', + 'active record type is unchanged', + ); + is($active->desc, 'UEFI Version', 'active record label is unchanged'); + is( + $active->value, + '2.13 (TCE123A 2026-08-30)', + 'active value does not associate the pending build with this bank', + ); + + my $pending = $sessdata->{fru_hash}->{uefi_pending}; + isa_ok($pending, 'FRU', 'pending inventory record'); + is( + $pending ? $pending->rec_type : undef, + 'bios,uefi,firmware', + 'pending record has firmware inventory types', + ); + is( + $pending ? $pending->desc : undef, + 'Pending UEFI Build ID', + 'pending record identifies the value as a build ID', + ); + is( + $pending ? $pending->value : undef, + 'TCE124A', + 'pending record contains the OEM pending build ID', + ); + assert_next_property($calls, $sessdata, 'pending'); + + local @xCAT::SvrUtils::messages = (); + { + local $SIG{__WARN__} = sub { + my ($warning) = @_; + return + if $warning =~ + /\AArgument "uefi(?:_pending)?" isn't numeric in numeric comparison \(<=>\)/; + die $warning; + }; + xCAT_plugin::ipmi::fru_initted($sessdata); + } + is_deeply( + \@xCAT::SvrUtils::messages, + [ + sprintf( + '%-20s %s', + 'UEFI Version:', + '2.13 (TCE123A 2026-08-30)', + ), + sprintf('%-20s %s', 'Pending UEFI Build ID:', 'TCE124A'), + ], + 'active UEFI output precedes pending UEFI output', + ); +}; + +subtest 'no pending UEFI build leaves inventory unchanged' => sub { + my $sessdata = new_sessdata(0); + my $calls = run_got_bios_date($sessdata); + + is_deeply( + [sort keys %{ $sessdata->{fru_hash} }], + ['uefi'], + 'only the active key is present', + ); + my $active = $sessdata->{fru_hash}->{uefi}; + is( + $active->rec_type, + 'bios,uefi,firmware', + 'active record type remains unchanged', + ); + is($active->desc, 'UEFI Version', 'active record label remains unchanged'); + is( + $active->value, + '2.13 (TCE123A 2026-08-30)', + 'active record value remains unchanged', + ); + ok( + !exists $sessdata->{fru_hash}->{uefi_pending}, + 'pending record is omitted', + ); + assert_next_property($calls, $sessdata, 'no pending'); +}; + +done_testing();