From fd559276341d634b89f84671dc8b204cc5b0c1d4 Mon Sep 17 00:00:00 2001 From: Chet Nichols III Date: Thu, 25 Jun 2026 11:48:28 -0700 Subject: [PATCH] test(machine-a-tron): guard the empty-NDF-MAC Dell boot NIC through HostInit libredfish v0.44.16 makes a Dell host whose boot NIC reports an empty NetworkDeviceFunction MAC -- a NicMode-stripped or iDRAC partition, where the EthernetInterface MAC stays populated -- resolve the boot NIC by its interface id instead of by the absent MAC, so it clears HostInit/PollingBiosSetup instead of looping there. This bumps the pin to bring that in and adds an end-to-end machine-a-tron test so the path stays covered. The host still resolves a Pair (MAC + the interface id captured at exploration), so PollingBiosSetup's is_bios_setup MAC lookup fails and falls back to the id -- reaching Ready proves the host no longer hangs. - Bump libredfish v0.44.15 -> v0.44.16 (the Dell BIOS-verify by-interface-id fix). - Add a per-host bmc-mock empty_ndf_mac knob: serves an empty boot-NIC NetworkDeviceFunction MAC while the EthernetInterface MAC stays populated (honored by Dell, the one model with a NetworkDeviceFunction). - Add a machine-a-tron test driving such a Dell host to Ready; pre-fix it strands in PollingBiosSetup. Tests added! This supports https://github.com/NVIDIA/infra-controller/issues/2821 Signed-off-by: Chet Nichols III --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/api-integration-tests/tests/lib.rs | 45 +++++++++++++++++++ crates/bmc-mock/src/hw/dell_poweredge_r750.rs | 15 ++++++- crates/bmc-mock/src/machine_info.rs | 5 +++ crates/machine-a-tron/src/config.rs | 7 +++ crates/machine-a-tron/src/host_machine.rs | 4 +- 7 files changed, 76 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 101c7a40a7..68993566f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6561,7 +6561,7 @@ dependencies = [ [[package]] name = "libredfish" version = "0.0.0" -source = "git+https://github.com/NVIDIA/libredfish.git?tag=v0.44.15#539221bcc0b16603e16bc2ee3d9632ce8576e330" +source = "git+https://github.com/NVIDIA/libredfish.git?tag=v0.44.16#211f6e8dcdf4fa59500e3308246a6bc21a24276e" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index f40f827891..b59558f7e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ authors = ["NVIDIA Carbide Engineering "] [workspace.dependencies] clap = { version = "4", features = ["derive", "env"] } -libredfish = { git = "https://github.com/NVIDIA/libredfish.git", tag = "v0.44.15" } +libredfish = { git = "https://github.com/NVIDIA/libredfish.git", tag = "v0.44.16" } librms = { git = "https://github.com/NVIDIA/nv-rms-client.git", tag = "v0.9.0-rc1" } ansi-to-html = "0.2.2" diff --git a/crates/api-integration-tests/tests/lib.rs b/crates/api-integration-tests/tests/lib.rs index bda03396c6..fa49a1c559 100644 --- a/crates/api-integration-tests/tests/lib.rs +++ b/crates/api-integration-tests/tests/lib.rs @@ -193,6 +193,13 @@ async fn test_integration() -> eyre::Result<()> { Ipv4Addr::new(172, 20, 0, 2), ) .boxed(), + test_machine_a_tron_empty_ndf_mac( + &test_env, + &bmc_address_registry, + // Relay IP in host-inband net (zero-DPU host) + Ipv4Addr::new(10, 10, 11, 2), + ) + .boxed(), ]); tokio::select! { @@ -334,6 +341,7 @@ async fn test_metrics_integration() -> eyre::Result<()> { 1, 1, false, + false, // empty_ndf_mac &test_env, &bmc_address_registry, Ipv4Addr::new(172, 20, 0, 1), @@ -481,6 +489,7 @@ async fn test_machine_a_tron_multidpu( 1, 2, false, + false, // empty_ndf_mac test_env, bmc_mock_registry, admin_dhcp_relay_address, @@ -563,6 +572,7 @@ async fn test_machine_a_tron_zerodpu( 1, 0, false, + false, // empty_ndf_mac test_env, bmc_mock_registry, admin_dhcp_relay_address, @@ -612,6 +622,36 @@ async fn test_machine_a_tron_zerodpu( .await } +/// A Dell host whose boot NIC reports an empty `NetworkDeviceFunction` MAC while +/// its `EthernetInterface` MAC stays populated -- the NicMode-stripped / iDRAC +/// partition case. The boot interface still resolves as a `Pair` (MAC + the +/// interface id captured at exploration), so `PollingBiosSetup`'s `is_bios_setup` +/// MAC lookup fails and falls back to the interface id. Reaching `Ready` proves +/// the host no longer hangs in `HostInit` / `PollingBiosSetup`. +async fn test_machine_a_tron_empty_ndf_mac( + test_env: &IntegrationTestEnvironment, + bmc_mock_registry: &BmcMockRegistry, + admin_dhcp_relay_address: Ipv4Addr, +) -> eyre::Result<()> { + run_machine_a_tron_test( + HostHardwareType::DellPowerEdgeR750, + 1, + 0, + false, + true, // empty_ndf_mac + test_env, + bmc_mock_registry, + admin_dhcp_relay_address, + |machine_handle| async move { + machine_handle + .wait_until_machine_up_with_api_state("Ready", Duration::from_secs(90)) + .await?; + Ok::<(), eyre::Report>(()) + }, + ) + .await +} + async fn test_machine_a_tron_singledpu_nic_mode( hw_type: HostHardwareType, test_env: &IntegrationTestEnvironment, @@ -623,6 +663,7 @@ async fn test_machine_a_tron_singledpu_nic_mode( 1, 1, true, + false, // empty_ndf_mac test_env, bmc_mock_registry, admin_dhcp_relay_address, @@ -686,6 +727,7 @@ async fn test_machine_a_tron_dual_stack( 1, 1, false, + false, // empty_ndf_mac test_env, bmc_mock_registry, admin_dhcp_relay_address, @@ -793,6 +835,7 @@ async fn test_machine_a_tron_dual_stack_l2( 1, 1, false, + false, // empty_ndf_mac test_env, bmc_mock_registry, admin_dhcp_relay_address, @@ -853,6 +896,7 @@ async fn run_machine_a_tron_test( host_count: u32, dpu_per_host_count: u32, dpus_in_nic_mode: bool, + empty_ndf_mac: bool, test_env: &IntegrationTestEnvironment, bmc_mock_registry: &BmcMockRegistry, admin_dhcp_relay_address: Ipv4Addr, @@ -896,6 +940,7 @@ where scout_run_interval: Duration::from_secs(1), network_virtualization_type: None, dpus_in_nic_mode, + empty_ndf_mac, dpu_firmware_versions: None, dpu_agent_version: None, }), diff --git a/crates/bmc-mock/src/hw/dell_poweredge_r750.rs b/crates/bmc-mock/src/hw/dell_poweredge_r750.rs index 1c99fdc047..1a0c46ea20 100644 --- a/crates/bmc-mock/src/hw/dell_poweredge_r750.rs +++ b/crates/bmc-mock/src/hw/dell_poweredge_r750.rs @@ -31,6 +31,10 @@ pub struct DellPowerEdgeR750<'a> { pub product_serial_number: Cow<'a, str>, pub nics: Vec<(hw::nic::SlotNumber, hw::nic::Nic<'a>)>, pub embedded_nic: EmbeddedNic, + /// Serve an empty `NetworkDeviceFunction` MAC for the boot NIC (its + /// `EthernetInterface` MAC stays populated), reproducing a NicMode-stripped + /// iDRAC partition where the boot NIC must be resolved by interface id. + pub empty_ndf_mac: bool, } pub struct EmbeddedNic { @@ -194,8 +198,17 @@ impl DellPowerEdgeR750<'_> { &network_adapter_id, &function_id, ); + // The NetworkDeviceFunction MAC normally mirrors the NIC's MAC, but a + // NicMode-stripped boot NIC (or an iDRAC partition) can leave it empty + // while the EthernetInterface still carries the MAC -- serve that + // asymmetry on request so the boot NIC resolves by interface id. + let ndf_mac_address = if self.empty_ndf_mac { + String::new() + } else { + nic.mac_address.to_string() + }; let function = redfish::network_device_function::builder(func_resource) - .ethernet(json!({"MACAddress": &nic.mac_address})) + .ethernet(json!({"MACAddress": ndf_mac_address})) .oem(redfish::oem::dell::network_device_function::dell_nic_info( &function_id, *slot, diff --git a/crates/bmc-mock/src/machine_info.rs b/crates/bmc-mock/src/machine_info.rs index b7a223ca1c..03a3d75faa 100644 --- a/crates/bmc-mock/src/machine_info.rs +++ b/crates/bmc-mock/src/machine_info.rs @@ -46,6 +46,9 @@ pub struct HostMachineInfo { pub nvos_mac_addresses: Vec, pub switch_serial_number: Option, pub hw_mac_addr_pool: MacAddressPoolConfig, + /// Serve an empty boot-NIC `NetworkDeviceFunction` MAC (Dell hosts only), + /// exercising boot-NIC resolution by interface id. Defaults to false. + pub empty_ndf_mac: bool, } #[derive(Debug, Clone)] @@ -177,6 +180,7 @@ impl HostMachineInfo { switch_serial_number, dpus, hw_mac_addr_pool, + empty_ndf_mac: false, } } @@ -393,6 +397,7 @@ impl HostMachineInfo { port_1: next_mac(), port_2: next_mac(), }, + empty_ndf_mac: self.empty_ndf_mac, } } diff --git a/crates/machine-a-tron/src/config.rs b/crates/machine-a-tron/src/config.rs index 86296d78ea..881cdd73ad 100644 --- a/crates/machine-a-tron/src/config.rs +++ b/crates/machine-a-tron/src/config.rs @@ -123,6 +123,13 @@ pub struct MachineConfig { #[serde(default)] pub dpu_agent_version: Option, + + /// If true, this host's boot-NIC `NetworkDeviceFunction` reports an empty MAC + /// while its `EthernetInterface` MAC stays populated -- a Dell-only quirk for + /// exercising boot-NIC resolution by interface id. Honored only by hosts whose + /// hardware models a `NetworkDeviceFunction` (currently `DellPowerEdgeR750`). + #[serde(default)] + pub empty_ndf_mac: bool, } #[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)] diff --git a/crates/machine-a-tron/src/host_machine.rs b/crates/machine-a-tron/src/host_machine.rs index 5b7d3fbdf4..d2cab6eac5 100644 --- a/crates/machine-a-tron/src/host_machine.rs +++ b/crates/machine-a-tron/src/host_machine.rs @@ -106,6 +106,7 @@ impl HostMachine { nvos_mac_addresses: persisted_host_machine.nvos_mac_addresses.clone(), switch_serial_number: persisted_host_machine.switch_serial_number.clone(), hw_mac_addr_pool, + empty_ndf_mac: config.empty_ndf_mac, }; let dpus = dpu_machines .into_iter() @@ -182,12 +183,13 @@ impl HostMachine { ) }) .collect::>(); - let host_info = HostMachineInfo::new( + let mut host_info = HostMachineInfo::new( config.hw_type, dpu_machines.iter().map(|d| d.dpu_info().clone()).collect(), mac_pool, hw_pool_config, ); + host_info.empty_ndf_mac = config.empty_ndf_mac; let dpus = dpu_machines .into_iter() .map(|d| d.start(true))