From 1d09f2542845420e14d34330704006f002ac0afb Mon Sep 17 00:00:00 2001 From: Krish Dandiwala Date: Tue, 23 Jun 2026 19:12:58 +0000 Subject: [PATCH 1/2] fix: more changes to support lenovo GB300s Signed-off-by: Krish Dandiwala --- Cargo.lock | 21 +++++----- Cargo.toml | 2 +- crates/api-model/src/site_explorer/mod.rs | 7 +++- .../src/test_support/axum_http_client.rs | 14 ++++++- crates/site-explorer/src/redfish.rs | 39 ++++++++++++------- 5 files changed, 56 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51665299d4..071ca74a39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7496,9 +7496,9 @@ dependencies = [ [[package]] name = "nv-redfish" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06c0a56718c0605b550a9957d90bcf824edd52093f09e32e6c6b5087dd86de7f" +checksum = "13237aec9be47b56223e52ed62be45cf58e9c9ab40a00b9ba825a3632786fcda" dependencies = [ "futures-core", "futures-util", @@ -7513,9 +7513,9 @@ dependencies = [ [[package]] name = "nv-redfish-bmc-http" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6dd788740d6111263b57d44dd3b4f59f5201f81e5383cd6784a977cdefe2196" +checksum = "738a573625be85553f64e75c5a83099f5f17dd0a16755b2ddd000325c324c8eb" dependencies = [ "futures-core", "futures-util", @@ -7529,6 +7529,7 @@ dependencies = [ "serde_path_to_error", "sse-stream", "time", + "tokio", "tokio-util", "url", "uuid", @@ -7536,9 +7537,9 @@ dependencies = [ [[package]] name = "nv-redfish-core" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f897d4e715aac6857673a344d05aea380d8d87cbf5eba876c9768664ecc8cba1" +checksum = "835391f6b5ec1c4886a8b4e37b3162a0a4a632d02a7d466c653088227de10205" dependencies = [ "futures-core", "futures-io", @@ -7551,9 +7552,9 @@ dependencies = [ [[package]] name = "nv-redfish-csdl-compiler" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "264eb8b256664d1d2b953d2bfc26648f8c3986d0540dc4eaa2e666f2f16de567" +checksum = "00553364bafff3d15ba96fa6fcbe4891419d96f6fc6d400ad7bd87b9e8b745ea" dependencies = [ "clap", "clap_derive", @@ -7569,9 +7570,9 @@ dependencies = [ [[package]] name = "nv-redfish-schema" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7049c7e09b59afe558b46d6c35323a3b29e9cc32daeede4eb5a7a2ca47ff4a3" +checksum = "30bd3d46c21adcdf729aeed28d272e929d27e32fcd7b0016ea3dd3516bc55d4d" dependencies = [ "glob", ] diff --git a/Cargo.toml b/Cargo.toml index 17045b8d17..913ed0dfc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ tracing-appender = "0.2.4" tracing-opentelemetry = "0.32.1" # NV-Redfish -nv-redfish = { version = "0.10.0" } +nv-redfish = { version = "0.10.3" } ######## # MARK: - Pinned packages that we can't upgrade due to conflicts or just bugs diff --git a/crates/api-model/src/site_explorer/mod.rs b/crates/api-model/src/site_explorer/mod.rs index f40daf3a20..4ee7613b81 100644 --- a/crates/api-model/src/site_explorer/mod.rs +++ b/crates/api-model/src/site_explorer/mod.rs @@ -1332,8 +1332,13 @@ pub struct EthernetInterface { pub struct UefiDevicePath(String); lazy_static! { + // Not anchored at start: GB300/Grace UEFI device paths prefix the PciRoot + // node with vendor/MMIO nodes, e.g. + // VenHw()/MemoryMapped(0xB,...)/PciRoot(0x16)/Pci(0x0,0x0)/Pci(0x0,0x0) + // An `^PciRoot` anchor never matches those and aborts the whole exploration + // (`Could not match regex in PCI Device Path`). Match PciRoot wherever it appears. static ref PCI_ROOT_REGEX: Regex = - Regex::new(r"^PciRoot\(([^)]*)\)").expect("must always compile"); + Regex::new(r"PciRoot\(([^)]*)\)").expect("must always compile"); static ref PCI_NODE_REGEX: Regex = Regex::new(r"/Pci\(([^)]*)\)").expect("must always compile"); } diff --git a/crates/bmc-mock/src/test_support/axum_http_client.rs b/crates/bmc-mock/src/test_support/axum_http_client.rs index 79bec0bbc3..a6e9429d95 100644 --- a/crates/bmc-mock/src/test_support/axum_http_client.rs +++ b/crates/bmc-mock/src/test_support/axum_http_client.rs @@ -21,7 +21,9 @@ use axum::Router; use axum::body::Body; use axum::http::{HeaderMap, Method, Request, StatusCode}; use http_body_util::BodyExt; -use nv_redfish::bmc_http::{BmcCredentials, CacheableError, HttpClient}; +use nv_redfish::bmc_http::{ + BmcCredentials, CacheableError, HttpClient, RejectedUriReferenceError, RequestError, +}; use nv_redfish::core::upload::{MultipartUpdateRequest, UploadReader}; use nv_redfish::core::{BoxTryStream, ModificationResponse, ODataETag, SessionCreateResponse}; use serde::Serialize; @@ -39,6 +41,7 @@ pub enum Error { Json(serde_json::Error), Http(axum::http::Error), Cache(String), + RejectedUriReference(String), NotSupported(&'static str), } @@ -51,6 +54,9 @@ impl fmt::Display for Error { Self::Json(err) => write!(f, "json error: {err}"), Self::Http(err) => write!(f, "http build error: {err}"), Self::Cache(reason) => write!(f, "cache error: {reason}"), + Self::RejectedUriReference(reason) => { + write!(f, "rejected URI reference: {reason}") + } Self::NotSupported(what) => write!(f, "not supported in test client: {what}"), } } @@ -58,6 +64,12 @@ impl fmt::Display for Error { impl std::error::Error for Error {} +impl RequestError for Error { + fn rejected_uri_reference(error: RejectedUriReferenceError) -> Self { + Self::RejectedUriReference(error.reason) + } +} + impl CacheableError for Error { fn is_cached(&self) -> bool { matches!( diff --git a/crates/site-explorer/src/redfish.rs b/crates/site-explorer/src/redfish.rs index 2cfdaaf188..957f553b0d 100644 --- a/crates/site-explorer/src/redfish.rs +++ b/crates/site-explorer/src/redfish.rs @@ -232,22 +232,33 @@ impl RedfishClient { .map_err(|err| redact_password(err, curr_password.as_str())) .map_err(map_redfish_error)?; } - // Vikings and Lenovo GB300s. GB300s are detected as AMI at this - // point (vendor isn't refined to LenovoGB300 until later), but both - // rotate via the same admin account, so handle them together. + // Vikings and Lenovo GB300s (both still detected as AMI here). + // Resolve the admin account by username, and fall back to the conventional + // id "2" only when reads are blocked by `PasswordChangeRequired` (Viking factory state). + // Any other error propagates. + // + // https://docs.nvidia.com/dgx/dgxh100-user-guide/redfish-api-supp.html RedfishVendor::AMI | RedfishVendor::LenovoGB300 => { - /* - https://docs.nvidia.com/dgx/dgxh100-user-guide/redfish-api-supp.html - - You should set the password after the first boot. The following curl command changes the password for the admin user. - curl -k -u : --request PATCH 'https:///redfish/v1/AccountService/Accounts/2' --header 'If-Match: *' --header 'Content-Type: application/json' --data-raw '{ "Password" : "" }' - */ - client - .change_password_by_id("2", new_password.as_str()) + match client + .change_password(curr_user.as_str(), new_password.as_str()) .await - .map_err(|err| redact_password(err, new_password.as_str())) - .map_err(|err| redact_password(err, curr_password.as_str())) - .map_err(map_redfish_error)?; + { + Ok(()) => {} + Err(libredfish::RedfishError::PasswordChangeRequired) => { + client + .change_password_by_id("2", new_password.as_str()) + .await + .map_err(|err| redact_password(err, new_password.as_str())) + .map_err(|err| redact_password(err, curr_password.as_str())) + .map_err(map_redfish_error)?; + } + Err(err) => { + return Err(map_redfish_error(redact_password( + redact_password(err, new_password.as_str()), + curr_password.as_str(), + ))); + } + } } RedfishVendor::LenovoAMI | RedfishVendor::Supermicro From 4c7373ba9b280a689c64558a15185a6e9c56bd28 Mon Sep 17 00:00:00 2001 From: Krish Dandiwala Date: Tue, 23 Jun 2026 21:36:59 +0000 Subject: [PATCH 2/2] fix: look at outer-chassis for dpu serial number as backup Signed-off-by: Krish Dandiwala --- crates/site-explorer/src/lib.rs | 42 ++++++- crates/site-explorer/tests/site_explorer.rs | 116 +++++++++++++++++++- 2 files changed, 151 insertions(+), 7 deletions(-) diff --git a/crates/site-explorer/src/lib.rs b/crates/site-explorer/src/lib.rs index d2b5da2401..77e66cae34 100644 --- a/crates/site-explorer/src/lib.rs +++ b/crates/site-explorer/src/lib.rs @@ -1188,14 +1188,33 @@ impl SiteExplorer { } } - // A DPU can show up as a chassis network adapter instead of a PCIe - // device on some BMCs; fall back to those only if the PCIe scan found none. + // A DPU can show up as a chassis instead of a PCIe device on some + // BMCs; fall back to the chassis inventory only if the PCIe scan + // found none. if dpu_exploration.expected_managed_total() == 0 { for chassis in ep.report.chassis.iter() { - for network_adapter in chassis.network_adapters.iter() { + // Some BMCs (e.g. the AMI/Lenovo GB300 host BMC) report the + // BlueField as the chassis object itself -- model, part_number + // and serial_number live on the chassis, while its nested + // network adapter carries an empty serial. Match on the + // chassis identity in that case; otherwise fall back to the + // chassis's network adapters (other vendors put the DPU + // serial there). Matching only one of the two per chassis + // keeps a single DPU from being counted twice. + let chassis_is_bluefield = chassis + .part_number + .as_deref() + .map(str::trim) + .is_some_and(is_bluefield_model); + let chassis_has_serial = chassis + .serial_number + .as_deref() + .map(str::trim) + .is_some_and(|serial| !serial.is_empty()); + if chassis_is_bluefield && chassis_has_serial { self.record_host_dpu_device( - network_adapter.part_number.as_deref(), - network_adapter.serial_number.as_deref(), + chassis.part_number.as_deref(), + chassis.serial_number.as_deref(), &dpu_sn_to_endpoint, host_dpu_mode, &ep, @@ -1203,6 +1222,19 @@ impl SiteExplorer { metrics, ) .await; + } else { + for network_adapter in chassis.network_adapters.iter() { + self.record_host_dpu_device( + network_adapter.part_number.as_deref(), + network_adapter.serial_number.as_deref(), + &dpu_sn_to_endpoint, + host_dpu_mode, + &ep, + &mut dpu_exploration, + metrics, + ) + .await; + } } } } diff --git a/crates/site-explorer/tests/site_explorer.rs b/crates/site-explorer/tests/site_explorer.rs index e249d7a557..c3eef3dee0 100644 --- a/crates/site-explorer/tests/site_explorer.rs +++ b/crates/site-explorer/tests/site_explorer.rs @@ -35,8 +35,8 @@ use model::machine::machine_search_config::MachineSearchConfig; use model::machine::{LoadSnapshotOptions, Machine}; use model::metadata::Metadata; use model::site_explorer::{ - ComputerSystem, EndpointExplorationError, EndpointExplorationReport, EndpointType, ExploredDpu, - ExploredManagedHost, NicMode, PreingestionState, UefiDevicePath, + Chassis, ComputerSystem, EndpointExplorationError, EndpointExplorationReport, EndpointType, + ExploredDpu, ExploredManagedHost, NetworkAdapter, NicMode, PreingestionState, UefiDevicePath, }; use model::test_support::{DpuConfig, ManagedHostConfig}; use rpc::forge::GetSiteExplorationRequest; @@ -2891,6 +2891,118 @@ async fn test_site_explorer_enforces_nic_mode_on_fallback_serial_match( Ok(()) } +/// Some host BMCs (e.g. the AMI/Lenovo GB300 `HG635N_V2`) report the BlueField +/// as the chassis object itself -- model, part_number and serial_number live on +/// the chassis, while its nested network adapter carries an empty serial -- and +/// don't enumerate the DPU over PCIe at all. The host<->DPU serial match must +/// therefore consider the chassis identity, not just `chassis.network_adapters[]`. +/// +/// Here the operator declares NO `fallback_dpu_serial_numbers` and the default +/// `DpuMode`, so the only thing that can pair the host with its DPU is the +/// chassis-reported serial. The host must pair (and not fall through to the +/// zero-DPU path). +#[sqlx_test] +async fn test_site_explorer_pairs_dpu_from_chassis_serial( + pool: PgPool, +) -> Result<(), Box> { + use model::expected_machine::{ExpectedMachine, ExpectedMachineData}; + use model::site_explorer::NicMode; + + let env = Env::new(pool).await; + + const CHASSIS_DPU_SERIAL: &str = "chassis-reported-dpu-serial"; + // The DPU's BMC reports DPU mode; the host BMC carries no DPU PCIe device, + // only the BlueField chassis below, so the chassis serial is the only link. + let dpu_config = DpuConfig { + nic_mode: Some(NicMode::Dpu), + serial: CHASSIS_DPU_SERIAL.to_string(), + ..DpuConfig::default() + }; + // A host with no DPU configs: its report carries no BlueField PCIe device or + // network adapter, so the only BlueField is the chassis we push below. (A + // configured DPU would inject a phantom BlueField into the host's PCIe scan, + // making `expected_managed_total() != 0` and skipping the chassis fallback.) + let mock_host = ManagedHostConfig::zero_dpu(); + let host_bmc_mac = mock_host.bmc_mac_address; + + let mut txn = env.pool.begin().await?; + db::expected_machine::create( + &mut txn, + ExpectedMachine { + id: None, + bmc_mac_address: host_bmc_mac, + data: ExpectedMachineData { + bmc_username: "ADMIN".to_string(), + bmc_password: "PASS".to_string(), + serial_number: "EM-GB300-CHASSIS-SERIAL".to_string(), + metadata: model::metadata::Metadata::new_with_default_name(), + ..Default::default() + }, + }, + ) + .await?; + txn.commit().await?; + + let mut host_report: EndpointExplorationReport = mock_host.into(); + host_report.chassis.push(Chassis { + id: "Riser_Slot1_BlueField_3_SmartNIC_Main_Card".to_string(), + manufacturer: Some("Nvidia".to_string()), + model: Some("BlueField-3 SmartNIC Main Card".to_string()), + part_number: Some("900-9D3B6-00CN-PA0".to_string()), + serial_number: Some(CHASSIS_DPU_SERIAL.to_string()), + network_adapters: vec![NetworkAdapter { + id: "Riser_Slot1_BlueField_3_SmartNIC_Main_Card".to_string(), + serial_number: Some(String::new()), + ..Default::default() + }], + ..Default::default() + }); + + let mut host_bmc = env.new_machine(&host_bmc_mac.to_string(), "SomeVendor"); + let mut dpu_bmc = env.new_machine(&dpu_config.bmc_mac_address.to_string(), "NVIDIA/BF/BMC"); + host_bmc.discover_dhcp(env.api()).await?; + dpu_bmc.discover_dhcp(env.api()).await?; + + let explorer_config = SiteExplorerConfig { + enabled: Arc::new(true.into()), + retained_boot_interface_window: None, + explorations_per_run: 10, + concurrent_explorations: 1, + run_interval: std::time::Duration::from_secs(1), + create_machines: Arc::new(true.into()), + ..Default::default() + }; + let explorer = env.test_site_explorer(explorer_config); + explorer.insert_endpoint_results(vec![ + (dpu_bmc.ip.parse().unwrap(), Ok(dpu_config.clone().into())), + (host_bmc.ip.parse().unwrap(), Ok(host_report)), + ]); + + // First iteration: initial endpoint exploration. + explorer.run_single_iteration().await.unwrap(); + let mut txn = env.pool.begin().await?; + for ip in [host_bmc.ip.parse()?, dpu_bmc.ip.parse()?] { + db::explored_endpoints::set_preingestion_complete(ip, &mut txn).await?; + } + txn.commit().await?; + // Second iteration: per-host matching pairs the DPU off the chassis serial. + explorer.run_single_iteration().await.unwrap(); + + let explored_managed_hosts = db::explored_managed_host::find_all(&env.pool).await?; + assert_eq!( + explored_managed_hosts.len(), + 1, + "GB300 host should pair via its chassis-reported BlueField serial" + ); + assert_eq!( + explored_managed_hosts[0].dpus.len(), + 1, + "the chassis-matched DPU should be attached to the host" + ); + + Ok(()) +} + /// A managed host's DPU-facing `machine_interface` is created (via DHCP) with /// just a MAC and no `boot_interface_id`. The exploration that ingests the host /// then backfills the vendor-specific Redfish interface id onto that row, matched