diff --git a/crates/api-core/src/test_support/mac_address_pool.rs b/crates/api-core/src/test_support/mac_address_pool.rs index 290a7cb6e9..3cbfd09a3b 100644 --- a/crates/api-core/src/test_support/mac_address_pool.rs +++ b/crates/api-core/src/test_support/mac_address_pool.rs @@ -15,31 +15,62 @@ * limitations under the License. */ -use std::sync::atomic::{AtomicUsize, Ordering}; - +use carbide_utils::test_support::mac_address_pool as mock_mac_pool; use mac_address::MacAddress; +pub use mock_mac_pool::MacAddressPoolConfig; + +pub const DPU_OOB_MAC_ADDRESS_POOL_CONFIG: MacAddressPoolConfig = MacAddressPoolConfig { + start: [0x11, 0x11, 0x11, 0x11, 0x0, 0x0], + length: 65536, +}; + +pub const DPU_BMC_MAC_ADDRESS_POOL_CONFIG: MacAddressPoolConfig = MacAddressPoolConfig { + start: [0x11, 0x11, 0x22, 0x22, 0x0, 0x0], + length: 65536, +}; + +pub const HOST_MAC_ADDRESS_POOL_CONFIG: MacAddressPoolConfig = MacAddressPoolConfig { + start: [0x22, 0x22, 0x11, 0x11, 0x0, 0x0], + length: 65536, +}; + +pub const HOST_BMC_MAC_ADDRESS_POOL_CONFIG: MacAddressPoolConfig = MacAddressPoolConfig { + start: [0x22, 0x22, 0x22, 0x22, 0x0, 0x0], + length: 65536, +}; + +pub const HOST_NON_DPU_MAC_ADDRESS_POOL_CONFIG: MacAddressPoolConfig = MacAddressPoolConfig { + start: [0x33, 0x33, 0x11, 0x11, 0x0, 0x0], + length: 65536, +}; + +pub const EXPECTED_SWITCH_BMC_MAC_ADDRESS_POOL_CONFIG: MacAddressPoolConfig = + MacAddressPoolConfig { + start: [0x44, 0x44, 0x11, 0x11, 0x0, 0x0], + length: 65536, + }; -#[derive(Copy, Clone, Debug)] -pub struct MacAddressPoolConfig { - /// The first mac address in the pool as a byte array - pub start: [u8; 6], - /// The amount of addresses in the pool - pub length: usize, -} +pub const EXPECTED_POWER_SHELF_BMC_MAC_ADDRESS_POOL_CONFIG: MacAddressPoolConfig = + MacAddressPoolConfig { + start: [0x44, 0x44, 0x22, 0x22, 0x0, 0x0], + length: 65536, + }; + +pub const EXPECTED_SWITCH_NVOS_MAC_ADDRESS_POOL_CONFIG: MacAddressPoolConfig = + MacAddressPoolConfig { + start: [0x44, 0x44, 0x33, 0x33, 0x0, 0x0], + length: 65536, + }; #[derive(Debug)] pub struct MacAddressPool { - /// Defines which addresses are available in the pool - config: MacAddressPoolConfig, - /// How many addresses have already been allocated - used: AtomicUsize, + inner: mock_mac_pool::MacAddressPool, } impl MacAddressPool { pub fn new(config: MacAddressPoolConfig) -> Self { Self { - config, - used: AtomicUsize::new(0), + inner: mock_mac_pool::MacAddressPool::new(config), } } @@ -47,90 +78,47 @@ impl MacAddressPool { /// /// Will panic once the pool is depleted pub fn allocate(&self) -> MacAddress { - let offset = self.used.fetch_add(1, Ordering::SeqCst); - if offset >= self.config.length { - panic!("Mac address pool with config {:?} is depleted", self.config); - } - - let mut u64_address = to_u64_be(self.config.start); - u64_address += offset as u64; - - let mut bytes = [0u8; 6]; - // The MAC address is stored by `to_u64_be` stored in the last 6 bytes - bytes.copy_from_slice(&u64_address.to_be_bytes()[2..8]); - - MacAddress::new(bytes) + self.inner + .allocate() + .unwrap_or_else(|error| panic!("{error}")) } /// Returns whether an address is part of the pool pub fn contains(&self, address: MacAddress) -> bool { - let a = to_u64_be(address.bytes()); - let min = to_u64_be(self.config.start); - - (min..min + self.config.length as u64).contains(&a) + self.inner.contains(address) } } lazy_static::lazy_static! { /// Pool of DPU MAC addresses pub static ref DPU_OOB_MAC_ADDRESS_POOL: MacAddressPool = - MacAddressPool::new(MacAddressPoolConfig { - start: [0x11, 0x11, 0x11, 0x11, 0x0, 0x0], - length: 65536, - }); + MacAddressPool::new(DPU_OOB_MAC_ADDRESS_POOL_CONFIG); /// Pool of DPU BMC MAC addresses pub static ref DPU_BMC_MAC_ADDRESS_POOL: MacAddressPool = - MacAddressPool::new(MacAddressPoolConfig { - start: [0x11, 0x11, 0x22, 0x22, 0x0, 0x0], - length: 65536, - }); + MacAddressPool::new(DPU_BMC_MAC_ADDRESS_POOL_CONFIG); /// Pool of Host MAC addresses pub static ref HOST_MAC_ADDRESS_POOL: MacAddressPool = - MacAddressPool::new(MacAddressPoolConfig { - start: [0x22, 0x22, 0x11, 0x11, 0x0, 0x0], - length: 65536, - }); + MacAddressPool::new(HOST_MAC_ADDRESS_POOL_CONFIG); /// Pool of Host BMC MAC addresses pub static ref HOST_BMC_MAC_ADDRESS_POOL: MacAddressPool = - MacAddressPool::new(MacAddressPoolConfig { - start: [0x22, 0x22, 0x22, 0x22, 0x0, 0x0], - length: 65536, - }); + MacAddressPool::new(HOST_BMC_MAC_ADDRESS_POOL_CONFIG); /// Pool of Host non-DPU MAC addresses pub static ref HOST_NON_DPU_MAC_ADDRESS_POOL: MacAddressPool = - MacAddressPool::new(MacAddressPoolConfig { - start: [0x33, 0x33, 0x11, 0x11, 0x0, 0x0], - length: 65536, - }); + MacAddressPool::new(HOST_NON_DPU_MAC_ADDRESS_POOL_CONFIG); /// Pool of Expected Switch BMC MAC addresses pub static ref EXPECTED_SWITCH_BMC_MAC_ADDRESS_POOL: MacAddressPool = - MacAddressPool::new(MacAddressPoolConfig { - start: [0x44, 0x44, 0x11, 0x11, 0x0, 0x0], - length: 65536, - }); + MacAddressPool::new(EXPECTED_SWITCH_BMC_MAC_ADDRESS_POOL_CONFIG); /// Pool of Expected Power Shelf BMC MAC addresses pub static ref EXPECTED_POWER_SHELF_BMC_MAC_ADDRESS_POOL: MacAddressPool = - MacAddressPool::new(MacAddressPoolConfig { - start: [0x44, 0x44, 0x22, 0x22, 0x0, 0x0], - length: 65536, - }); + MacAddressPool::new(EXPECTED_POWER_SHELF_BMC_MAC_ADDRESS_POOL_CONFIG); /// Pool of Expected Switch NVOS MAC addresses pub static ref EXPECTED_SWITCH_NVOS_MAC_ADDRESS_POOL: MacAddressPool = - MacAddressPool::new(MacAddressPoolConfig { - start: [0x44, 0x44, 0x33, 0x33, 0x0, 0x0], - length: 65536, - }); -} - -fn to_u64_be(bytes: [u8; 6]) -> u64 { - u64::from_be_bytes([ - 0, 0, bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], - ]) + MacAddressPool::new(EXPECTED_SWITCH_NVOS_MAC_ADDRESS_POOL_CONFIG); } diff --git a/crates/api-core/src/tests/mac_address_pool.rs b/crates/api-core/src/tests/mac_address_pool.rs index b2df0acdca..34a00975c5 100644 --- a/crates/api-core/src/tests/mac_address_pool.rs +++ b/crates/api-core/src/tests/mac_address_pool.rs @@ -20,7 +20,13 @@ use mac_address::MacAddress; -use crate::test_support::mac_address_pool::{MacAddressPool, MacAddressPoolConfig}; +use crate::test_support::mac_address_pool::{ + DPU_BMC_MAC_ADDRESS_POOL_CONFIG, DPU_OOB_MAC_ADDRESS_POOL_CONFIG, + EXPECTED_POWER_SHELF_BMC_MAC_ADDRESS_POOL_CONFIG, EXPECTED_SWITCH_BMC_MAC_ADDRESS_POOL_CONFIG, + EXPECTED_SWITCH_NVOS_MAC_ADDRESS_POOL_CONFIG, HOST_BMC_MAC_ADDRESS_POOL_CONFIG, + HOST_MAC_ADDRESS_POOL_CONFIG, HOST_NON_DPU_MAC_ADDRESS_POOL_CONFIG, MacAddressPool, + MacAddressPoolConfig, +}; #[test] fn allocate_addresses() { @@ -64,3 +70,52 @@ fn depleted_pool_panics() { ); pool.allocate(); } + +#[test] +fn configured_ranges_do_not_overlap() { + for (left_index, (left_name, left_config)) in pool_configs().iter().enumerate() { + for (right_name, right_config) in pool_configs().iter().skip(left_index + 1) { + assert!( + !ranges_overlap(*left_config, *right_config), + "{left_name} overlaps {right_name}" + ); + } + } +} + +fn pool_configs() -> [(&'static str, MacAddressPoolConfig); 8] { + [ + ("dpu_oob", DPU_OOB_MAC_ADDRESS_POOL_CONFIG), + ("dpu_bmc", DPU_BMC_MAC_ADDRESS_POOL_CONFIG), + ("host", HOST_MAC_ADDRESS_POOL_CONFIG), + ("host_bmc", HOST_BMC_MAC_ADDRESS_POOL_CONFIG), + ("host_non_dpu", HOST_NON_DPU_MAC_ADDRESS_POOL_CONFIG), + ( + "expected_switch_bmc", + EXPECTED_SWITCH_BMC_MAC_ADDRESS_POOL_CONFIG, + ), + ( + "expected_power_shelf_bmc", + EXPECTED_POWER_SHELF_BMC_MAC_ADDRESS_POOL_CONFIG, + ), + ( + "expected_switch_nvos", + EXPECTED_SWITCH_NVOS_MAC_ADDRESS_POOL_CONFIG, + ), + ] +} + +fn ranges_overlap(left: MacAddressPoolConfig, right: MacAddressPoolConfig) -> bool { + let left_start = to_u64_be(left.start); + let left_end = left_start + left.length as u64; + let right_start = to_u64_be(right.start); + let right_end = right_start + right.length as u64; + + left_start < right_end && right_start < left_end +} + +fn to_u64_be(bytes: [u8; 6]) -> u64 { + u64::from_be_bytes([ + 0, 0, bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], + ]) +} diff --git a/crates/api-integration-tests/tests/lib.rs b/crates/api-integration-tests/tests/lib.rs index 6514d56b5c..4a6ac7738e 100644 --- a/crates/api-integration-tests/tests/lib.rs +++ b/crates/api-integration-tests/tests/lib.rs @@ -22,7 +22,9 @@ use std::sync::Arc; use std::time::{self, Duration}; use ::carbide_utils::HostPortPair; -use ::machine_a_tron::{BmcMockRegistry, HostMachineHandle, MachineATronConfig, MachineConfig}; +use ::machine_a_tron::{ + BmcMockRegistry, HostMachineHandle, MachineATronConfig, MachineConfig, MockMacAddressPoolConfig, +}; use api_test_helper::{ IntegrationTestEnvironment, domain, instance, machine, metrics, subnet, tenant, utils, vpc, vpc_prefix, @@ -914,6 +916,10 @@ where api_refresh_interval: Duration::from_millis(500), mock_bmc_ssh_server: false, mock_bmc_ssh_port: None, + mock_mac_address_pool: MockMacAddressPoolConfig { + base_mac_address: "02:ff:00:00:00:10".parse().unwrap(), + length: 65536, + }, }; let (machine_handles, _mat_handle) = api_test_helper::machine_a_tron::run_local( diff --git a/crates/api-test-helper/src/machine_a_tron.rs b/crates/api-test-helper/src/machine_a_tron.rs index 1cdc52bb8c..8e3ab36df0 100644 --- a/crates/api-test-helper/src/machine_a_tron.rs +++ b/crates/api-test-helper/src/machine_a_tron.rs @@ -75,6 +75,7 @@ pub async fn run_local( desired_firmware ); + let mock_mac_pool = Arc::new(app_config.mock_mac_pool()); let app_context = Arc::new(MachineATronContext { bmc_registration_mode: if let Some(bmc_address_registry) = bmc_address_registry.as_ref() { BmcRegistrationMode::BackingInstance(bmc_address_registry.clone()) @@ -87,6 +88,7 @@ pub async fn run_local( api_throttler, desired_firmware_versions: desired_firmware, forge_api_client, + mock_mac_pool, }); let mat = MachineATron::new(app_context.clone()); diff --git a/crates/bmc-mock/Cargo.toml b/crates/bmc-mock/Cargo.toml index 687da8c97f..720d42a0a9 100644 --- a/crates/bmc-mock/Cargo.toml +++ b/crates/bmc-mock/Cargo.toml @@ -25,7 +25,9 @@ authors.workspace = true [dependencies] bmc-vendor = { path = "../bmc-vendor" } carbide-rpc = { path = "../rpc", default-features = false } -carbide-utils = { path = "../utils", default-features = false } +carbide-utils = { path = "../utils", default-features = false, features = [ + "test-support", +] } arc-swap = { workspace = true } axum = { workspace = true } diff --git a/crates/bmc-mock/src/lib.rs b/crates/bmc-mock/src/lib.rs index 1b6bb73aa8..70db50c8f6 100644 --- a/crates/bmc-mock/src/lib.rs +++ b/crates/bmc-mock/src/lib.rs @@ -38,6 +38,9 @@ pub mod test_support; pub mod tls; pub use bmc_state::{BmcEvent, BmcState}; +pub use carbide_utils::test_support::mac_address_pool::{ + MacAddressPool, MacAddressPoolConfig as MockMacAddressPoolConfig, MacAddressPoolError, +}; pub use combined_server::{CombinedServer, ListenerOrAddress}; pub use machine_info::{ DpuFirmwareVersions, DpuMachineInfo, DpuSettings, HostMachineInfo, MachineInfo, diff --git a/crates/bmc-mock/src/machine_info.rs b/crates/bmc-mock/src/machine_info.rs index 144b312401..61c2e0e1e8 100644 --- a/crates/bmc-mock/src/machine_info.rs +++ b/crates/bmc-mock/src/machine_info.rs @@ -16,16 +16,15 @@ */ use std::borrow::Cow; use std::sync::Arc; -use std::sync::atomic::{AtomicU32, Ordering}; +use carbide_utils::test_support::mac_address_pool::{MacAddressPool, MacAddressPoolError}; use mac_address::MacAddress; use serde::{Deserialize, Serialize}; use crate::redfish::update_service::UpdateServiceConfig; -use crate::{hw, redfish}; -static NEXT_MAC_ADDRESS: AtomicU32 = AtomicU32::new(1); use crate::{ DUMMY_FACTORY_DPU_PASSWORD, DUMMY_FACTORY_PASSWORD, DUMMY_FACTORY_USERNAME, HostHardwareType, + hw, redfish, }; /// Represents static information we know ahead of time about a host or DPU (independent of any @@ -48,6 +47,8 @@ pub struct HostMachineInfo { pub nvos_mac_addresses: Vec, #[serde(default)] pub switch_serial_number: Option, + #[serde(skip)] + pub discovery_info: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -59,6 +60,23 @@ pub struct DpuMachineInfo { pub serial: String, #[serde(flatten)] pub settings: DpuSettings, + #[serde(skip)] + pub discovery_info: Option, +} + +fn allocate_hardware_mac(mac_pool: &MacAddressPool) -> MacAddress { + mac_pool + .allocate() + .expect("mock MAC address pool must have host hardware addresses") +} + +fn allocate_hardware_mac_array(mac_pool: &MacAddressPool) -> [MacAddress; N] { + let addresses = (0..N) + .map(|_| allocate_hardware_mac(mac_pool)) + .collect::>(); + addresses + .try_into() + .expect("fixed-size MAC allocation must match requested length") } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -91,27 +109,47 @@ impl Default for DpuSettings { } } -impl Default for DpuMachineInfo { - fn default() -> Self { - Self::new(HostHardwareType::DellPowerEdgeR750, DpuSettings::default()) - } +pub(crate) struct MachineRedfishConfig { + pub manager_config: redfish::manager::Config, + pub system_config: redfish::computer_system::Config, + pub chassis_config: redfish::chassis::ChassisConfig, + pub update_service_config: UpdateServiceConfig, } impl DpuMachineInfo { - pub fn new(hw_type: HostHardwareType, settings: DpuSettings) -> Self { - let bmc_mac_address = next_mac(); - let host_mac_address = next_mac(); - let oob_mac_address = next_mac(); - Self { + pub fn allocate( + hw_type: HostHardwareType, + settings: DpuSettings, + mac_pool: &MacAddressPool, + ) -> Result { + let bmc_mac_address = mac_pool.allocate()?; + let host_mac_address = mac_pool.allocate()?; + let oob_mac_address = mac_pool.allocate()?; + let mut info = Self { hw_type, bmc_mac_address, host_mac_address, oob_mac_address, settings, serial: format!("MT{}", oob_mac_address.to_string().replace(':', "")), + discovery_info: None, + }; + info.cache_discovery_info(); + Ok(info) + } + + fn cache_discovery_info(&mut self) { + if self.discovery_info.is_none() { + self.discovery_info = Some(self.bluefield3().discovery_info()); } } + pub fn discovery_info(&self) -> rpc::machine_discovery::DiscoveryInfo { + self.discovery_info + .clone() + .unwrap_or_else(|| self.bluefield3().discovery_info()) + } + fn bluefield3(&self) -> hw::bluefield3::Bluefield3<'_> { let mode = match self.hw_type { HostHardwareType::DellPowerEdgeR750 @@ -148,32 +186,45 @@ impl DpuMachineInfo { } impl HostMachineInfo { - pub fn new(hw_type: HostHardwareType, dpus: Vec) -> Self { - let bmc_mac_address = next_mac(); - let nvos_mac_addresses = if matches!(hw_type, HostHardwareType::NvidiaSwitchNd5200Ld) { - vec![next_mac()] - } else { - vec![] - }; + pub fn allocate( + hw_type: HostHardwareType, + dpus: Vec, + mac_pool: &MacAddressPool, + ) -> Result { + let include_non_dpu_mac_address = dpus.is_empty() + && !matches!( + hw_type, + HostHardwareType::LiteOnPowerShelf | HostHardwareType::NvidiaSwitchNd5200Ld + ); + let nvos_mac_address_count = + usize::from(matches!(hw_type, HostHardwareType::NvidiaSwitchNd5200Ld)); + let bmc_mac_address = mac_pool.allocate()?; + let non_dpu_mac_address = include_non_dpu_mac_address + .then(|| mac_pool.allocate()) + .transpose()?; + let nvos_mac_addresses = (0..nvos_mac_address_count) + .map(|_| mac_pool.allocate()) + .collect::, _>>()?; let switch_serial_number = nvos_mac_addresses .first() .map(|mac| format!("MT{}", mac.to_string().replace(':', ""))); - Self { + let mut info = Self { hw_type, bmc_mac_address, serial: bmc_mac_address.to_string().replace(':', ""), - non_dpu_mac_address: if dpus.is_empty() - && !matches!( - hw_type, - HostHardwareType::LiteOnPowerShelf | HostHardwareType::NvidiaSwitchNd5200Ld - ) { - Some(next_mac()) - } else { - None - }, + non_dpu_mac_address, nvos_mac_addresses, switch_serial_number, dpus, + discovery_info: None, + }; + info.cache_discovery_info(mac_pool); + Ok(info) + } + + fn cache_discovery_info(&mut self, mac_pool: &MacAddressPool) { + if self.discovery_info.is_none() { + self.discovery_info = self.build_discovery_info(mac_pool); } } @@ -253,104 +304,126 @@ impl HostMachineInfo { } } - pub fn manager_config(&self) -> redfish::manager::Config { - match self.hw_type { - HostHardwareType::DellPowerEdgeR750 => self.dell_poweredge_r750().manager_config(), - HostHardwareType::WiwynnGB200Nvl => self.wiwynn_gb200_nvl().manager_config(), - HostHardwareType::LenovoGB300Nvl => self.lenovo_gb300_nvl().manager_config(), - HostHardwareType::NvidiaDgxGb300 => self.dgx_gb300_nvl().manager_config(), - HostHardwareType::SupermicroGb300Nvl => self.supermicro_gb300_nvl().manager_config(), - HostHardwareType::LiteOnPowerShelf => self.liteon_power_shelf().manager_config(), - HostHardwareType::NvidiaSwitchNd5200Ld => { - self.nvidia_switch_nd5200_ld().manager_config() - } - HostHardwareType::NvidiaDgxH100 => self.nvidia_dgx_h100().manager_config(), - HostHardwareType::GenericAmi | HostHardwareType::GenericSupermicro => { - self.generic_server().manager_config() - } - } - } - - pub fn system_config( + pub(crate) fn redfish_config( &self, callbacks: Arc, - ) -> redfish::computer_system::Config { + mac_pool: &MacAddressPool, + ) -> MachineRedfishConfig { match self.hw_type { HostHardwareType::DellPowerEdgeR750 => { - self.dell_poweredge_r750().system_config(callbacks) + let hardware = self.dell_poweredge_r750(mac_pool); + MachineRedfishConfig { + manager_config: hardware.manager_config(), + system_config: hardware.system_config(callbacks), + chassis_config: hardware.chassis_config(), + update_service_config: hardware.update_service_config(), + } } - HostHardwareType::WiwynnGB200Nvl => self.wiwynn_gb200_nvl().system_config(callbacks), - HostHardwareType::LenovoGB300Nvl => self.lenovo_gb300_nvl().system_config(callbacks), - HostHardwareType::NvidiaDgxGb300 => self.dgx_gb300_nvl().system_config(callbacks), - HostHardwareType::SupermicroGb300Nvl => { - self.supermicro_gb300_nvl().system_config(callbacks) + HostHardwareType::WiwynnGB200Nvl => { + let hardware = self.wiwynn_gb200_nvl(); + MachineRedfishConfig { + manager_config: hardware.manager_config(), + system_config: hardware.system_config(callbacks), + chassis_config: hardware.chassis_config(), + update_service_config: hardware.update_service_config(), + } } - HostHardwareType::LiteOnPowerShelf => self.liteon_power_shelf().system_config(), - HostHardwareType::NvidiaSwitchNd5200Ld => { - self.nvidia_switch_nd5200_ld().system_config() + HostHardwareType::LenovoGB300Nvl => { + let hardware = self.lenovo_gb300_nvl(mac_pool); + MachineRedfishConfig { + manager_config: hardware.manager_config(), + system_config: hardware.system_config(callbacks), + chassis_config: hardware.chassis_config(), + update_service_config: hardware.update_service_config(), + } } - HostHardwareType::NvidiaDgxH100 => self.nvidia_dgx_h100().system_config(callbacks), - HostHardwareType::GenericAmi | HostHardwareType::GenericSupermicro => { - self.generic_server().system_config(callbacks) + HostHardwareType::NvidiaDgxGb300 => { + let hardware = self.dgx_gb300_nvl(mac_pool); + MachineRedfishConfig { + manager_config: hardware.manager_config(), + system_config: hardware.system_config(callbacks), + chassis_config: hardware.chassis_config(), + update_service_config: hardware.update_service_config(), + } + } + HostHardwareType::SupermicroGb300Nvl => { + let hardware = self.supermicro_gb300_nvl(mac_pool); + MachineRedfishConfig { + manager_config: hardware.manager_config(), + system_config: hardware.system_config(callbacks), + chassis_config: hardware.chassis_config(), + update_service_config: hardware.update_service_config(), + } + } + HostHardwareType::LiteOnPowerShelf => { + let hardware = self.liteon_power_shelf(); + MachineRedfishConfig { + manager_config: hardware.manager_config(), + system_config: hardware.system_config(), + chassis_config: hardware.chassis_config(), + update_service_config: hardware.update_service_config(), + } } - } - } - - pub fn chassis_config(&self) -> redfish::chassis::ChassisConfig { - match self.hw_type { - HostHardwareType::DellPowerEdgeR750 => self.dell_poweredge_r750().chassis_config(), - HostHardwareType::WiwynnGB200Nvl => self.wiwynn_gb200_nvl().chassis_config(), - HostHardwareType::LenovoGB300Nvl => self.lenovo_gb300_nvl().chassis_config(), - HostHardwareType::NvidiaDgxGb300 => self.dgx_gb300_nvl().chassis_config(), - HostHardwareType::SupermicroGb300Nvl => self.supermicro_gb300_nvl().chassis_config(), - HostHardwareType::LiteOnPowerShelf => self.liteon_power_shelf().chassis_config(), HostHardwareType::NvidiaSwitchNd5200Ld => { - self.nvidia_switch_nd5200_ld().chassis_config() + let hardware = self.nvidia_switch_nd5200_ld(mac_pool); + MachineRedfishConfig { + manager_config: hardware.manager_config(), + system_config: hardware.system_config(), + chassis_config: hardware.chassis_config(), + update_service_config: hardware.update_service_config(), + } + } + HostHardwareType::NvidiaDgxH100 => { + let hardware = self.nvidia_dgx_h100(mac_pool); + MachineRedfishConfig { + manager_config: hardware.manager_config(), + system_config: hardware.system_config(callbacks), + chassis_config: hardware.chassis_config(), + update_service_config: hardware.update_service_config(), + } } - HostHardwareType::NvidiaDgxH100 => self.nvidia_dgx_h100().chassis_config(), HostHardwareType::GenericAmi | HostHardwareType::GenericSupermicro => { - self.generic_server().chassis_config() + let hardware = self.generic_server(); + MachineRedfishConfig { + manager_config: hardware.manager_config(), + system_config: hardware.system_config(callbacks), + chassis_config: hardware.chassis_config(), + update_service_config: hardware.update_service_config(), + } } } } - pub fn update_service_config(&self) -> UpdateServiceConfig { + fn build_discovery_info( + &self, + mac_pool: &MacAddressPool, + ) -> Option { match self.hw_type { HostHardwareType::DellPowerEdgeR750 => { - self.dell_poweredge_r750().update_service_config() + Some(self.dell_poweredge_r750(mac_pool).discovery_info()) + } + HostHardwareType::WiwynnGB200Nvl => Some(self.wiwynn_gb200_nvl().discovery_info()), + HostHardwareType::LenovoGB300Nvl => { + Some(self.lenovo_gb300_nvl(mac_pool).discovery_info()) } - HostHardwareType::WiwynnGB200Nvl => self.wiwynn_gb200_nvl().update_service_config(), - HostHardwareType::LenovoGB300Nvl => self.lenovo_gb300_nvl().update_service_config(), - HostHardwareType::NvidiaDgxGb300 => self.dgx_gb300_nvl().update_service_config(), + HostHardwareType::NvidiaDgxGb300 => Some(self.dgx_gb300_nvl(mac_pool).discovery_info()), HostHardwareType::SupermicroGb300Nvl => { - self.supermicro_gb300_nvl().update_service_config() + Some(self.supermicro_gb300_nvl(mac_pool).discovery_info()) } - HostHardwareType::LiteOnPowerShelf => self.liteon_power_shelf().update_service_config(), - HostHardwareType::NvidiaSwitchNd5200Ld => { - self.nvidia_switch_nd5200_ld().update_service_config() + HostHardwareType::NvidiaDgxH100 => { + Some(self.nvidia_dgx_h100(mac_pool).discovery_info()) } - HostHardwareType::NvidiaDgxH100 => self.nvidia_dgx_h100().update_service_config(), HostHardwareType::GenericAmi | HostHardwareType::GenericSupermicro => { - self.generic_server().update_service_config() + Some(self.generic_server().discovery_info()) } + HostHardwareType::LiteOnPowerShelf | HostHardwareType::NvidiaSwitchNd5200Ld => None, } } pub fn discovery_info(&self) -> rpc::machine_discovery::DiscoveryInfo { - match self.hw_type { - HostHardwareType::DellPowerEdgeR750 => self.dell_poweredge_r750().discovery_info(), - HostHardwareType::WiwynnGB200Nvl => self.wiwynn_gb200_nvl().discovery_info(), - HostHardwareType::LenovoGB300Nvl => self.lenovo_gb300_nvl().discovery_info(), - HostHardwareType::NvidiaDgxGb300 => self.dgx_gb300_nvl().discovery_info(), - HostHardwareType::SupermicroGb300Nvl => self.supermicro_gb300_nvl().discovery_info(), - HostHardwareType::NvidiaDgxH100 => self.nvidia_dgx_h100().discovery_info(), - HostHardwareType::GenericAmi | HostHardwareType::GenericSupermicro => { - self.generic_server().discovery_info() - } - HostHardwareType::LiteOnPowerShelf | HostHardwareType::NvidiaSwitchNd5200Ld => { - panic!("discovery_info requested for {}", self.hw_type) - } - } + self.discovery_info + .clone() + .unwrap_or_else(|| panic!("discovery_info requested for {}", self.hw_type)) } pub fn factory_default_account(&self) -> redfish::account_service::Account { @@ -366,7 +439,10 @@ impl HostMachineInfo { ) } - fn dell_poweredge_r750(&self) -> hw::dell_poweredge_r750::DellPowerEdgeR750<'_> { + fn dell_poweredge_r750( + &self, + mac_pool: &MacAddressPool, + ) -> hw::dell_poweredge_r750::DellPowerEdgeR750<'_> { let nics = if self.dpus.is_empty() { self.non_dpu_mac_address .iter() @@ -385,8 +461,8 @@ impl HostMachineInfo { product_serial_number: Cow::Borrowed(&self.serial), nics, embedded_nic: hw::dell_poweredge_r750::EmbeddedNic { - port_1: next_mac(), - port_2: next_mac(), + port_1: allocate_hardware_mac(mac_pool), + port_2: allocate_hardware_mac(mac_pool), }, } } @@ -435,7 +511,7 @@ impl HostMachineInfo { } } - fn dgx_gb300_nvl(&self) -> hw::dgx_gb300_nvl::DgxGB300Nvl<'_> { + fn dgx_gb300_nvl(&self, mac_pool: &MacAddressPool) -> hw::dgx_gb300_nvl::DgxGB300Nvl<'_> { let mut dpus = self.dpus.iter(); // Serials are from the DGX GB300 scrape. // GPU_0/1 and GPU_2/3 share a superchip serial; the HGX baseboard @@ -459,12 +535,12 @@ impl HostMachineInfo { .expect("One DPU must present for DGX GB300 NVL") .bluefield3(), embedded_1g_nic: hw::nic_intel_i210::NicIntelI210 { - mac_address: next_mac(), + mac_address: allocate_hardware_mac(mac_pool), }, - bmc_mac_address_eth0: next_mac(), - bmc_mac_address_eth1: next_mac(), - bmc_mac_address_usb0: next_mac(), - hgx_bmc_mac_address_usb0: next_mac(), + bmc_mac_address_eth0: allocate_hardware_mac(mac_pool), + bmc_mac_address_eth1: allocate_hardware_mac(mac_pool), + bmc_mac_address_usb0: allocate_hardware_mac(mac_pool), + hgx_bmc_mac_address_usb0: allocate_hardware_mac(mac_pool), hgx_serial_number: superchip_a_sn.into(), topology: hw::nvidia_gbx00::Topology { chassis_physical_slot_number: 25, @@ -478,7 +554,10 @@ impl HostMachineInfo { } } - fn supermicro_gb300_nvl(&self) -> hw::supermicro_gb300_nvl::SupermicroGB300Nvl<'_> { + fn supermicro_gb300_nvl( + &self, + mac_pool: &MacAddressPool, + ) -> hw::supermicro_gb300_nvl::SupermicroGB300Nvl<'_> { let mut dpus = self.dpus.iter(); // Serials are from the SMC GB300 tray scrape. // GPU_0/1 and GPU_2/3 share a superchip serial; the HGX baseboard @@ -500,12 +579,12 @@ impl HostMachineInfo { .expect("One DPU must present for SMC GB300 NVL") .bluefield3(), embedded_1g_nic: hw::nic_intel_i210::NicIntelI210 { - mac_address: next_mac(), + mac_address: allocate_hardware_mac(mac_pool), }, - bmc_mac_address_eth0: next_mac(), - bmc_mac_address_eth1: next_mac(), - bmc_mac_address_usb0: next_mac(), - hgx_bmc_mac_address_usb0: next_mac(), + bmc_mac_address_eth0: allocate_hardware_mac(mac_pool), + bmc_mac_address_eth1: allocate_hardware_mac(mac_pool), + bmc_mac_address_usb0: allocate_hardware_mac(mac_pool), + hgx_bmc_mac_address_usb0: allocate_hardware_mac(mac_pool), hgx_serial_number: superchip_a_sn.into(), topology: hw::nvidia_gbx00::Topology { chassis_physical_slot_number: 25, @@ -519,7 +598,10 @@ impl HostMachineInfo { } } - fn lenovo_gb300_nvl(&self) -> hw::lenovo_gb300_nvl::LenovoGB300Nvl<'_> { + fn lenovo_gb300_nvl( + &self, + mac_pool: &MacAddressPool, + ) -> hw::lenovo_gb300_nvl::LenovoGB300Nvl<'_> { let mut dpus = self.dpus.iter(); let cpu0_sn = "0x000000017FFFFFFFFF00000000000001"; let cpu1_sn = "0x000000017FFFFFFFFF00000000000002"; @@ -535,12 +617,12 @@ impl HostMachineInfo { .expect("One DPU must present for GB300 NVL") .bluefield3(), embedded_1g_nic: hw::nic_intel_i210::NicIntelI210 { - mac_address: next_mac(), + mac_address: allocate_hardware_mac(mac_pool), }, - bmc_mac_address_eth0: next_mac(), - bmc_mac_address_eth1: next_mac(), - bmc_mac_address_usb0: next_mac(), - hgx_bmc_mac_address_usb0: next_mac(), + bmc_mac_address_eth0: allocate_hardware_mac(mac_pool), + bmc_mac_address_eth1: allocate_hardware_mac(mac_pool), + bmc_mac_address_usb0: allocate_hardware_mac(mac_pool), + hgx_bmc_mac_address_usb0: allocate_hardware_mac(mac_pool), hgx_serial_number: "012345678901234567890123".into(), topology: hw::nvidia_gbx00::Topology { chassis_physical_slot_number: 25, @@ -588,11 +670,14 @@ impl HostMachineInfo { } } - fn nvidia_switch_nd5200_ld(&self) -> hw::nvidia_switch_nd5200_ld::NvidiaSwitchNd5200Ld<'_> { + fn nvidia_switch_nd5200_ld( + &self, + mac_pool: &MacAddressPool, + ) -> hw::nvidia_switch_nd5200_ld::NvidiaSwitchNd5200Ld<'_> { hw::nvidia_switch_nd5200_ld::NvidiaSwitchNd5200Ld { bmc_mac_address_eth0: self.bmc_mac_address, - bmc_mac_address_eth1: next_mac(), - bmc_mac_address_usb0: next_mac(), + bmc_mac_address_eth1: allocate_hardware_mac(mac_pool), + bmc_mac_address_usb0: allocate_hardware_mac(mac_pool), bmc_serial_number: Cow::Borrowed(&self.serial), switch_serial_number: self .switch_serial_number @@ -602,31 +687,37 @@ impl HostMachineInfo { } } - fn nvidia_dgx_h100(&self) -> hw::nvidia_dgx_h100::NvidiaDgxH100<'_> { - let storage_nic0_p0_mac = next_mac(); - let storage_nic0_serial = format!("MT{}", storage_nic0_p0_mac.to_string().replace(":", "")); + fn nvidia_dgx_h100(&self, mac_pool: &MacAddressPool) -> hw::nvidia_dgx_h100::NvidiaDgxH100<'_> { + let ib_nics = [ + allocate_hardware_mac_array(mac_pool), + allocate_hardware_mac_array(mac_pool), + ]; + let mgmt_nic = allocate_hardware_mac(mac_pool); + let storage_nic0 = allocate_hardware_mac_array(mac_pool); + let storage_nic1 = allocate_hardware_mac_array(mac_pool); + let storage_nic0_serial = format!("MT{}", storage_nic0[0].to_string().replace(":", "")); hw::nvidia_dgx_h100::NvidiaDgxH100 { dgx_system_serial_number: Cow::Borrowed(&self.serial), dgx_chassis_serial_number: Cow::Borrowed("1663223000002"), ib_nics: [ hw::nic_nvidia_cx7::NicNvidiaCx7B { serial_number: "MT2307X00001".into(), - mac_addresses: [(); _].map(|_| next_mac()), + mac_addresses: ib_nics[0], }, hw::nic_nvidia_cx7::NicNvidiaCx7B { serial_number: "MT2307X00002".into(), - mac_addresses: [(); _].map(|_| next_mac()), + mac_addresses: ib_nics[1], }, ], mgmt_nic: hw::nic_intel_x550::NicIntelX550 { - mac_address: next_mac(), + mac_address: mgmt_nic, }, storage_nic0: hw::nic_nvidia_cx7::NicNvidiaCx7A { serial_number: storage_nic0_serial.into(), - mac_addresses: [(); _].map(|_| next_mac()), + mac_addresses: storage_nic0, }, storage_nic1: hw::nic_intel_e810::NicIntelE810 { - mac_addresses: [(); _].map(|_| next_mac()), + mac_addresses: storage_nic1, }, dpu: self .dpus @@ -643,9 +734,9 @@ impl HostMachineInfo { "1652900000007".into(), "1652900000008".into(), ], - bmc_mac_address_eth0: next_mac(), - bmc_mac_address_usb0: next_mac(), - hgx_bmc_mac_address_usb0: next_mac(), + bmc_mac_address_eth0: allocate_hardware_mac(mac_pool), + bmc_mac_address_usb0: allocate_hardware_mac(mac_pool), + hgx_bmc_mac_address_usb0: allocate_hardware_mac(mac_pool), } } @@ -665,6 +756,35 @@ impl HostMachineInfo { } impl MachineInfo { + pub fn host(mut info: HostMachineInfo, mac_pool: &MacAddressPool) -> Self { + info.cache_discovery_info(mac_pool); + Self::Host(info) + } + + pub fn dpu(mut info: DpuMachineInfo) -> Self { + info.cache_discovery_info(); + Self::Dpu(info) + } + + pub(crate) fn redfish_config( + &self, + callbacks: Arc, + mac_pool: &MacAddressPool, + ) -> MachineRedfishConfig { + match self { + MachineInfo::Host(host) => host.redfish_config(callbacks, mac_pool), + MachineInfo::Dpu(dpu) => { + let hardware = dpu.bluefield3(); + MachineRedfishConfig { + manager_config: hardware.manager_config(), + system_config: hardware.system_config(callbacks), + chassis_config: hardware.chassis_config(), + update_service_config: hardware.update_service_config(), + } + } + } + } + pub fn oem_state(&self) -> redfish::oem::State { match self { MachineInfo::Host(host) => host.oem_state(), @@ -677,13 +797,6 @@ impl MachineInfo { } } - pub fn manager_config(&self) -> redfish::manager::Config { - match self { - MachineInfo::Host(host) => host.manager_config(), - MachineInfo::Dpu(dpu) => dpu.bluefield3().manager_config(), - } - } - pub fn bmc_vendor(&self) -> redfish::oem::BmcVendor { match self { MachineInfo::Host(h) => h.bmc_vendor(), @@ -707,30 +820,6 @@ impl MachineInfo { } } - pub fn system_config( - &self, - callbacks: Arc, - ) -> redfish::computer_system::Config { - match self { - MachineInfo::Host(host) => host.system_config(callbacks), - MachineInfo::Dpu(dpu) => dpu.bluefield3().system_config(callbacks), - } - } - - pub fn chassis_config(&self) -> redfish::chassis::ChassisConfig { - match self { - Self::Host(h) => h.chassis_config(), - Self::Dpu(dpu) => dpu.bluefield3().chassis_config(), - } - } - - pub fn update_service_config(&self) -> UpdateServiceConfig { - match self { - Self::Host(h) => h.update_service_config(), - Self::Dpu(dpu) => dpu.bluefield3().update_service_config(), - } - } - pub fn product_serial(&self) -> &String { match self { Self::Host(h) => &h.serial, @@ -771,7 +860,7 @@ impl MachineInfo { pub fn discovery_info(&self) -> rpc::machine_discovery::DiscoveryInfo { match self { Self::Host(h) => h.discovery_info(), - Self::Dpu(dpu) => dpu.bluefield3().discovery_info(), + Self::Dpu(dpu) => dpu.discovery_info(), } } @@ -787,19 +876,6 @@ impl MachineInfo { } } -fn next_mac() -> MacAddress { - let next_mac_num = NEXT_MAC_ADDRESS.fetch_add(1, Ordering::Acquire); - - let bytes: Vec = [0x02u8, 0x01] - .into_iter() - .chain(next_mac_num.to_be_bytes()) - .collect(); - - let mac_bytes = <[u8; 6]>::try_from(bytes).unwrap(); - - MacAddress::from(mac_bytes) -} - /// CPU / GPU / IO-board chassis common to every GB300 tray: NVIDIA HGX reference /// silicon, identical in shape across ODMs (only the serials differ per scrape). /// GPU_0/1 and GPU_2/3 each share a superchip serial. @@ -850,3 +926,76 @@ fn gb300_boards<'a>( ], } } + +#[cfg(test)] +mod tests { + use carbide_utils::test_support::mac_address_pool::MacAddressPoolConfig; + + use super::*; + + #[test] + fn dpu_allocate_allocates_unique_macs_from_pool() { + let mac_pool = MacAddressPool::new(MacAddressPoolConfig { + start: [0x02, 0x01, 0x0, 0x0, 0x0, 0x1], + length: 8, + }); + let first = DpuMachineInfo::allocate( + HostHardwareType::DellPowerEdgeR750, + Default::default(), + &mac_pool, + ) + .unwrap(); + let second = DpuMachineInfo::allocate( + HostHardwareType::DellPowerEdgeR750, + Default::default(), + &mac_pool, + ) + .unwrap(); + + assert_ne!(first.bmc_mac_address, second.bmc_mac_address); + assert_ne!(first.host_mac_address, second.host_mac_address); + assert_ne!(first.oob_mac_address, second.oob_mac_address); + } + + #[test] + fn host_allocate_allocates_unique_macs_from_pool() { + let mac_pool = MacAddressPool::new(MacAddressPoolConfig { + start: [0x02, 0x01, 0x0, 0x0, 0x0, 0x1], + length: 8, + }); + let first = + HostMachineInfo::allocate(HostHardwareType::GenericAmi, vec![], &mac_pool).unwrap(); + let second = + HostMachineInfo::allocate(HostHardwareType::GenericAmi, vec![], &mac_pool).unwrap(); + + assert_ne!(first.bmc_mac_address, second.bmc_mac_address); + assert_ne!(first.non_dpu_mac_address, second.non_dpu_mac_address); + } + + #[test] + fn machine_info_discovery_info_is_cached() { + let mac_pool = MacAddressPool::new(MacAddressPoolConfig { + start: [0x02, 0x01, 0x0, 0x0, 0x0, 0x1], + length: 5, + }); + let host = + HostMachineInfo::allocate(HostHardwareType::DellPowerEdgeR750, vec![], &mac_pool) + .unwrap(); + let machine_info = MachineInfo::host(host, &mac_pool); + + let _ = machine_info.discovery_info(); + let _ = machine_info.discovery_info(); + + assert_eq!( + mac_pool.allocate().unwrap(), + MacAddress::new([0x02, 0x01, 0x0, 0x0, 0x0, 0x5]) + ); + assert!(matches!( + mac_pool.allocate(), + Err(MacAddressPoolError::Depleted { + attempted_offset: 5, + .. + }) + )); + } +} diff --git a/crates/bmc-mock/src/main.rs b/crates/bmc-mock/src/main.rs index 6b9f705ded..2a69976e4d 100644 --- a/crates/bmc-mock/src/main.rs +++ b/crates/bmc-mock/src/main.rs @@ -26,7 +26,8 @@ use std::sync::Arc; use axum::Router; use bmc_mock::{ BmcCommand, Callbacks, DpuMachineInfo, HostHardwareType, HostMachineInfo, ListenerOrAddress, - MachineInfo, MockPowerState, SetSystemPowerError, SystemPowerControl, + MacAddressPool, MachineInfo, MockMacAddressPoolConfig, MockPowerState, SetSystemPowerError, + SystemPowerControl, }; use tar_router::TarGzOption; use tokio::sync::{RwLock, mpsc}; @@ -155,11 +156,28 @@ fn spawn_qemu_reboot_handler() -> mpsc::UnboundedSender { fn default_host_mock() -> Router { let command_channel = spawn_qemu_reboot_handler(); let callbacks = Arc::new(ChannelCallbacks::new(command_channel)); - bmc_mock::machine_router( - MachineInfo::Host(HostMachineInfo::new( + let mac_pool = default_mock_mac_pool(); + let dpus = vec![ + DpuMachineInfo::allocate( + HostHardwareType::WiwynnGB200Nvl, + Default::default(), + &mac_pool, + ) + .expect("default mock MAC address pool must have DPU addresses"), + DpuMachineInfo::allocate( HostHardwareType::WiwynnGB200Nvl, - vec![DpuMachineInfo::default(), DpuMachineInfo::default()], - )), + Default::default(), + &mac_pool, + ) + .expect("default mock MAC address pool must have DPU addresses"), + ]; + bmc_mock::machine_router( + MachineInfo::host( + HostMachineInfo::allocate(HostHardwareType::WiwynnGB200Nvl, dpus, &mac_pool) + .expect("default mock MAC address pool must have host addresses"), + &mac_pool, + ), + &mac_pool, callbacks, String::default(), false, @@ -167,6 +185,15 @@ fn default_host_mock() -> Router { .0 } +fn default_mock_mac_pool() -> MacAddressPool { + MacAddressPool::new(DEFAULT_MOCK_MAC_POOL_CONFIG) +} + +const DEFAULT_MOCK_MAC_POOL_CONFIG: MockMacAddressPoolConfig = MockMacAddressPoolConfig { + start: [0x02, 0x01, 0x0, 0x0, 0x0, 0x1], + length: u32::MAX as usize, +}; + #[derive(Debug)] struct ChannelCallbacks { command_channel: mpsc::UnboundedSender, diff --git a/crates/bmc-mock/src/mock_machine_router.rs b/crates/bmc-mock/src/mock_machine_router.rs index b6d3613838..76c476f6e6 100644 --- a/crates/bmc-mock/src/mock_machine_router.rs +++ b/crates/bmc-mock/src/mock_machine_router.rs @@ -24,8 +24,8 @@ use crate::bmc_state::BmcState; use crate::injection::InjectionStore; use crate::redfish::manager::ManagerState; use crate::{ - Callbacks, HostHardwareType, MachineInfo, SystemPowerControl, auth_router, middleware_router, - redfish, + Callbacks, HostHardwareType, MacAddressPool, MachineInfo, SystemPowerControl, auth_router, + middleware_router, redfish, }; #[derive(Debug)] @@ -63,13 +63,12 @@ impl AddRoutes for Router { /// the provided MachineInfo. pub fn machine_router( machine_info: MachineInfo, + mac_pool: &MacAddressPool, callbacks: Arc, mat_host_id: String, redfish_auth: bool, ) -> (Router, BmcState) { - let system_config = machine_info.system_config(callbacks.clone()); - let chassis_config = machine_info.chassis_config(); - let update_service_config = machine_info.update_service_config(); + let redfish_config = machine_info.redfish_config(callbacks.clone(), mac_pool); let bmc_vendor = machine_info.bmc_vendor(); let bmc_product = machine_info.bmc_product(); let bmc_redfish_version = machine_info.bmc_redfish_version(); @@ -92,15 +91,17 @@ pub fn machine_router( } MachineInfo::Host(_) => router.add_routes(crate::redfish::oem::dell::idrac::add_routes), }; - let manager = Arc::new(ManagerState::new(&machine_info.manager_config())); + let manager = Arc::new(ManagerState::new(&redfish_config.manager_config)); let system_state = Arc::new(crate::redfish::computer_system::SystemState::from_config( - system_config, + redfish_config.system_config, )); let chassis_state = Arc::new(crate::redfish::chassis::ChassisState::from_config( - chassis_config, + redfish_config.chassis_config, )); let update_service_state = Arc::new( - crate::redfish::update_service::UpdateServiceState::from_config(update_service_config), + crate::redfish::update_service::UpdateServiceState::from_config( + redfish_config.update_service_config, + ), ); let account_service_state = Arc::new( crate::redfish::account_service::AccountServiceState::new(factory_default_account), diff --git a/crates/bmc-mock/src/redfish/expander_router.rs b/crates/bmc-mock/src/redfish/expander_router.rs index b079e9738a..553e194f14 100644 --- a/crates/bmc-mock/src/redfish/expander_router.rs +++ b/crates/bmc-mock/src/redfish/expander_router.rs @@ -256,11 +256,22 @@ mod tests { fn test_host_mock() -> Router { let callbacks = Arc::new(TestCallbacks {}); - crate::machine_router( - MachineInfo::Host(HostMachineInfo::new( + let mac_pool = default_mock_mac_pool(); + let dpus = vec![ + DpuMachineInfo::allocate( HostHardwareType::DellPowerEdgeR750, - vec![DpuMachineInfo::default()], - )), + Default::default(), + &mac_pool, + ) + .expect("default mock MAC address pool must have DPU addresses"), + ]; + crate::machine_router( + MachineInfo::host( + HostMachineInfo::allocate(HostHardwareType::DellPowerEdgeR750, dpus, &mac_pool) + .expect("default mock MAC address pool must have host addresses"), + &mac_pool, + ), + &mac_pool, callbacks, String::default(), false, @@ -268,6 +279,15 @@ mod tests { .0 } + fn default_mock_mac_pool() -> MacAddressPool { + MacAddressPool::new(DEFAULT_MOCK_MAC_POOL_CONFIG) + } + + const DEFAULT_MOCK_MAC_POOL_CONFIG: MockMacAddressPoolConfig = MockMacAddressPoolConfig { + start: [0x02, 0x01, 0x0, 0x0, 0x0, 0x1], + length: u32::MAX as usize, + }; + #[tokio::test] async fn test_expand() { let bmc_mock = test_host_mock(); diff --git a/crates/bmc-mock/src/test_support/mod.rs b/crates/bmc-mock/src/test_support/mod.rs index 5f07f97c41..449d10c2f6 100644 --- a/crates/bmc-mock/src/test_support/mod.rs +++ b/crates/bmc-mock/src/test_support/mod.rs @@ -22,8 +22,9 @@ use url::Url; use crate::machine_info::DpuSettings; use crate::{ - BmcState, Callbacks, DpuMachineInfo, HostHardwareType, HostMachineInfo, MachineInfo, - MockPowerState, SetSystemPowerError, SystemPowerControl, machine_router, + BmcState, Callbacks, DpuMachineInfo, HostHardwareType, HostMachineInfo, MacAddressPool, + MachineInfo, MockMacAddressPoolConfig, MockPowerState, SetSystemPowerError, SystemPowerControl, + machine_router, }; pub mod axum_http_client; @@ -71,128 +72,120 @@ async fn test_bmc((router, state): (axum::Router, BmcState)) -> TestBmcHandle { } } -pub async fn wiwynn_gb200_bmc() -> TestBmcHandle { - test_bmc(machine_router( - MachineInfo::Host(HostMachineInfo::new( - HostHardwareType::WiwynnGB200Nvl, - vec![ - DpuMachineInfo::new(HostHardwareType::WiwynnGB200Nvl, DpuSettings::default()), - DpuMachineInfo::new(HostHardwareType::WiwynnGB200Nvl, DpuSettings::default()), - ], - )), +fn mock_host_info( + hw_type: HostHardwareType, + dpu_settings: Vec, + mac_pool: &MacAddressPool, +) -> HostMachineInfo { + let dpus = dpu_settings + .into_iter() + .map(|settings| mock_dpu_info(hw_type, settings, mac_pool)) + .collect(); + HostMachineInfo::allocate(hw_type, dpus, mac_pool) + .expect("default mock MAC address pool must have host addresses") +} + +fn mock_host_router( + hw_type: HostHardwareType, + dpu_settings: Vec, +) -> (axum::Router, BmcState) { + let mac_pool = default_mock_mac_pool(); + machine_router( + MachineInfo::host(mock_host_info(hw_type, dpu_settings, &mac_pool), &mac_pool), + &mac_pool, Arc::new(NoopCallbacks), "test-host-id".to_string(), false, + ) +} + +fn default_mock_mac_pool() -> MacAddressPool { + MacAddressPool::new(DEFAULT_MOCK_MAC_POOL_CONFIG) +} + +const DEFAULT_MOCK_MAC_POOL_CONFIG: MockMacAddressPoolConfig = MockMacAddressPoolConfig { + start: [0x02, 0x01, 0x0, 0x0, 0x0, 0x1], + length: u32::MAX as usize, +}; + +fn mock_dpu_info( + hw_type: HostHardwareType, + settings: DpuSettings, + mac_pool: &MacAddressPool, +) -> DpuMachineInfo { + DpuMachineInfo::allocate(hw_type, settings, mac_pool) + .expect("default mock MAC address pool must have DPU addresses") +} + +pub async fn wiwynn_gb200_bmc() -> TestBmcHandle { + test_bmc(mock_host_router( + HostHardwareType::WiwynnGB200Nvl, + vec![DpuSettings::default(), DpuSettings::default()], )) .await } pub async fn lenovo_gb300_bmc() -> TestBmcHandle { - test_bmc(machine_router( - MachineInfo::Host(HostMachineInfo::new( - HostHardwareType::LenovoGB300Nvl, - vec![DpuMachineInfo::new( - HostHardwareType::LenovoGB300Nvl, - DpuSettings::default(), - )], - )), - Arc::new(NoopCallbacks), - "test-host-id".to_string(), - false, + test_bmc(mock_host_router( + HostHardwareType::LenovoGB300Nvl, + vec![DpuSettings::default()], )) .await } pub async fn dgx_gb300_bmc() -> TestBmcHandle { - test_bmc(machine_router( - MachineInfo::Host(HostMachineInfo::new( - HostHardwareType::NvidiaDgxGb300, - vec![DpuMachineInfo::new( - HostHardwareType::NvidiaDgxGb300, - DpuSettings::default(), - )], - )), - Arc::new(NoopCallbacks), - "test-host-id".to_string(), - false, + test_bmc(mock_host_router( + HostHardwareType::NvidiaDgxGb300, + vec![DpuSettings::default()], )) .await } pub async fn supermicro_gb300_bmc() -> TestBmcHandle { - test_bmc(machine_router( - MachineInfo::Host(HostMachineInfo::new( - HostHardwareType::SupermicroGb300Nvl, - vec![DpuMachineInfo::new( - HostHardwareType::SupermicroGb300Nvl, - DpuSettings::default(), - )], - )), - Arc::new(NoopCallbacks), - "test-host-id".to_string(), - false, + test_bmc(mock_host_router( + HostHardwareType::SupermicroGb300Nvl, + vec![DpuSettings::default()], )) .await } pub async fn generic_supermicro_bmc() -> TestBmcHandle { - test_bmc(machine_router( - MachineInfo::Host(HostMachineInfo::new( - HostHardwareType::GenericSupermicro, - vec![], - )), - Arc::new(NoopCallbacks), - "test-host-id".to_string(), - false, + test_bmc(mock_host_router( + HostHardwareType::GenericSupermicro, + vec![], )) .await } pub async fn liteon_powershelf_bmc() -> TestBmcHandle { - test_bmc(machine_router( - MachineInfo::Host(HostMachineInfo::new( - HostHardwareType::LiteOnPowerShelf, - vec![], - )), - Arc::new(NoopCallbacks), - "test-host-id".to_string(), - false, - )) - .await + test_bmc(mock_host_router(HostHardwareType::LiteOnPowerShelf, vec![])).await } pub async fn nvidia_switch_nd5200_ld_bmc() -> TestBmcHandle { - test_bmc(machine_router( - MachineInfo::Host(HostMachineInfo::new( - HostHardwareType::NvidiaSwitchNd5200Ld, - vec![], - )), - Arc::new(NoopCallbacks), - "test-host-id".to_string(), - false, + test_bmc(mock_host_router( + HostHardwareType::NvidiaSwitchNd5200Ld, + vec![], )) .await } pub async fn dell_poweredge_r750_bmc() -> TestBmcHandle { - test_bmc(machine_router( - MachineInfo::Host(HostMachineInfo::new( - HostHardwareType::DellPowerEdgeR750, - vec![], - )), - Arc::new(NoopCallbacks), - "test-host-id".to_string(), - false, + test_bmc(mock_host_router( + HostHardwareType::DellPowerEdgeR750, + vec![], )) .await } pub async fn dell_poweredge_r750_bluefield3_bmc(settings: DpuSettings) -> TestBmcHandle { + let mac_pool = default_mock_mac_pool(); test_bmc(machine_router( - MachineInfo::Dpu(DpuMachineInfo::new( + MachineInfo::dpu(mock_dpu_info( HostHardwareType::DellPowerEdgeR750, settings, + &mac_pool, )), + &mac_pool, Arc::new(NoopCallbacks), "test-dpu-id".to_string(), false, @@ -201,13 +194,7 @@ pub async fn dell_poweredge_r750_bluefield3_bmc(settings: DpuSettings) -> TestBm } pub async fn generic_ami_bmc() -> TestBmcHandle { - test_bmc(machine_router( - MachineInfo::Host(HostMachineInfo::new(HostHardwareType::GenericAmi, vec![])), - Arc::new(NoopCallbacks), - "test-host-id".to_string(), - false, - )) - .await + test_bmc(mock_host_router(HostHardwareType::GenericAmi, vec![])).await } #[cfg(test)] @@ -223,16 +210,7 @@ mod test { #[tokio::test] async fn transport_supports_expand_query_through_mock_expander() { let client = AxumRouterHttpClient::new( - machine_router( - MachineInfo::Host(HostMachineInfo::new( - HostHardwareType::DellPowerEdgeR750, - vec![], - )), - Arc::new(NoopCallbacks), - "test-host-id".to_string(), - false, - ) - .0, + mock_host_router(HostHardwareType::DellPowerEdgeR750, vec![]).0, ); let url = Url::parse("https://bmc-mock.local/redfish/v1/Chassis?$expand=.($levels=1)").unwrap(); diff --git a/crates/machine-a-tron/Cargo.toml b/crates/machine-a-tron/Cargo.toml index c51bc5f118..912837f695 100644 --- a/crates/machine-a-tron/Cargo.toml +++ b/crates/machine-a-tron/Cargo.toml @@ -46,7 +46,7 @@ thiserror = { workspace = true } axum = { workspace = true } crossterm = { features = ["event-stream"], workspace = true } ratatui = { workspace = true } -mac_address = { workspace = true } +mac_address = { features = ["serde"], workspace = true } futures = { workspace = true } figment = { features = ["toml"], workspace = true } reqwest = { default-features = false, features = [ @@ -63,8 +63,8 @@ rand = { workspace = true } russh = { workspace = true } # [local-dependencies] -carbide-rpc = { path = "../rpc" } bmc-mock = { path = "../bmc-mock" } +carbide-rpc = { path = "../rpc" } carbide-tls = { path = "../tls" } carbide-version = { path = "../version" } carbide-uuid = { path = "../uuid" } diff --git a/crates/machine-a-tron/src/bmc_mock_wrapper.rs b/crates/machine-a-tron/src/bmc_mock_wrapper.rs index 3be7a67b1c..8eb84f9bb5 100644 --- a/crates/machine-a-tron/src/bmc_mock_wrapper.rs +++ b/crates/machine-a-tron/src/bmc_mock_wrapper.rs @@ -51,8 +51,13 @@ impl BmcMockWrapper { hostname: Arc, host_id: Uuid, ) -> Self { - let (bmc_mock_router, bmc_mock_state) = - bmc_mock::machine_router(machine_info.clone(), callbacks, host_id.to_string(), true); + let (bmc_mock_router, bmc_mock_state) = bmc_mock::machine_router( + machine_info.clone(), + &app_context.mock_mac_pool, + callbacks, + host_id.to_string(), + true, + ); BmcMockWrapper { machine_info, diff --git a/crates/machine-a-tron/src/config.rs b/crates/machine-a-tron/src/config.rs index 86d3b4209e..88c6dbe220 100644 --- a/crates/machine-a-tron/src/config.rs +++ b/crates/machine-a-tron/src/config.rs @@ -20,7 +20,10 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use bmc_mock::{DpuMachineInfo, DpuSettings, HostHardwareType, HostMachineInfo}; +use bmc_mock::{ + DpuMachineInfo, DpuSettings, HostHardwareType, HostMachineInfo, MacAddressPool, + MockMacAddressPoolConfig as BmcMockMacAddressPoolConfig, +}; use carbide_uuid::machine::MachineId; use clap::Parser; use duration_str::deserialize_duration; @@ -191,6 +194,9 @@ pub struct MachineATronConfig { #[serde(default = "default_bmc_mock_port")] pub bmc_mock_port: u16, + #[serde(default = "default_mock_mac_address_pool_config")] + pub mock_mac_address_pool: MockMacAddressPoolConfig, + /// Set this to true if you want each mock machine to run a mock BMC ssh server. This is useful /// for testing things like ssh-console. #[serde(default = "default_false")] @@ -249,7 +255,26 @@ pub struct MachineATronConfig { pub api_refresh_interval: Duration, } +#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)] +pub struct MockMacAddressPoolConfig { + pub base_mac_address: MacAddress, + pub length: usize, +} + +impl From for BmcMockMacAddressPoolConfig { + fn from(value: MockMacAddressPoolConfig) -> Self { + Self { + start: value.base_mac_address.bytes(), + length: value.length, + } + } +} + impl MachineATronConfig { + pub fn mock_mac_pool(&self) -> MacAddressPool { + MacAddressPool::new(self.mock_mac_address_pool.into()) + } + pub fn read_persisted_machines( &self, ) -> eyre::Result>>> { @@ -347,6 +372,7 @@ impl From for HostMachineInfo { non_dpu_mac_address: value.non_dpu_mac_address, nvos_mac_addresses: value.nvos_mac_addresses, switch_serial_number: value.switch_serial_number, + discovery_info: None, } } } @@ -376,6 +402,7 @@ impl From for DpuMachineInfo { oob_mac_address: value.oob_mac_address, serial: value.serial, settings: value.settings, + discovery_info: None, } } } @@ -384,6 +411,13 @@ fn default_bmc_mock_port() -> u16 { 2000 } +fn default_mock_mac_address_pool_config() -> MockMacAddressPoolConfig { + MockMacAddressPoolConfig { + base_mac_address: MacAddress::new([0x02, 0xaa, 0x0, 0x0, 0x0, 0x1]), + length: u32::MAX as usize, + } +} + fn default_template_dir() -> String { String::from("dev/machine-a-tron/templates") } @@ -433,6 +467,7 @@ pub struct MachineATronContext { /// firmware, DPU's can mock that they already have this installed. pub desired_firmware_versions: Vec, pub forge_api_client: ForgeApiClient, + pub mock_mac_pool: Arc, } impl MachineATronContext { @@ -515,5 +550,44 @@ scout_run_interval = "5s" let round_tripped = toml::from_str::(&serialized) .expect("Could not deserialize serialized config"); assert_eq!(round_tripped, cfg); + assert_eq!( + cfg.mock_mac_address_pool, + default_mock_mac_address_pool_config() + ); + } + + #[test] + fn test_configures_mock_mac_address_pool() { + let cfg_str = r#" +carbide_api_url = "https://carbide-api.forge:443" +log_file = "mat.log" +interface = "br-77cbb29de011" +bmc_mock_port = 1266 +mock_mac_address_pool = { base_mac_address = "02:aa:00:00:00:10", length = 32 } +configure_carbide_bmc_proxy_host = "192.168.1.20" + +[machines.config] +host_count = 1 +dpu_per_host_count = 1 +dpu_reboot_delay = 1 +host_reboot_delay = 1 +vpc_count = 0 +admin_dhcp_relay_address = "192.168.176.1" +oob_dhcp_relay_address = "192.168.192.1" +subnets_per_vpc = 0 + "#; + + let cfg = toml::from_str::(cfg_str).expect("Could not parse config"); + assert_eq!( + cfg.mock_mac_address_pool, + MockMacAddressPoolConfig { + base_mac_address: "02:aa:00:00:00:10".parse().unwrap(), + length: 32, + } + ); + assert_eq!( + cfg.mock_mac_pool().allocate().unwrap(), + "02:aa:00:00:00:10".parse().unwrap() + ); } } diff --git a/crates/machine-a-tron/src/dpu_machine.rs b/crates/machine-a-tron/src/dpu_machine.rs index 1c7c4e4f50..e7933da050 100644 --- a/crates/machine-a-tron/src/dpu_machine.rs +++ b/crates/machine-a-tron/src/dpu_machine.rs @@ -77,6 +77,7 @@ impl DpuMachine { oob_mac_address: persisted_dpu_machine.oob_mac_address, serial: persisted_dpu_machine.serial.clone(), settings: persisted_dpu_machine.settings.clone(), + discovery_info: None, }; let state_machine = MachineStateMachine::from_persisted( PersistedMachine::Dpu(persisted_dpu_machine), @@ -127,16 +128,18 @@ impl DpuMachine { .unwrap_or_default() .fill_missing_from_desired_firmware(&app_context.desired_firmware_versions); - let dpu_info = DpuMachineInfo::new( + let dpu_info = DpuMachineInfo::allocate( hw_type, DpuSettings { nic_mode: config.dpus_in_nic_mode, firmware_versions: firmware_versions.into(), ..Default::default() }, - ); + &app_context.mock_mac_pool, + ) + .expect("machine-a-tron mock MAC address pool must have DPU addresses"); let state_machine = MachineStateMachine::new( - MachineInfo::Dpu(dpu_info.clone()), + MachineInfo::dpu(dpu_info.clone()), config, app_context.clone(), bmc_control_tx, diff --git a/crates/machine-a-tron/src/host_machine.rs b/crates/machine-a-tron/src/host_machine.rs index ae3c69dee4..91181a70d9 100644 --- a/crates/machine-a-tron/src/host_machine.rs +++ b/crates/machine-a-tron/src/host_machine.rs @@ -103,6 +103,7 @@ impl HostMachine { non_dpu_mac_address: persisted_host_machine.non_dpu_mac_address, nvos_mac_addresses: persisted_host_machine.nvos_mac_addresses.clone(), switch_serial_number: persisted_host_machine.switch_serial_number.clone(), + discovery_info: None, }; let dpus = dpu_machines .into_iter() @@ -175,17 +176,19 @@ impl HostMachine { ) }) .collect::>(); - let host_info = HostMachineInfo::new( + let host_info = HostMachineInfo::allocate( config.hw_type, dpu_machines.iter().map(|d| d.dpu_info().clone()).collect(), - ); + &app_context.mock_mac_pool, + ) + .expect("machine-a-tron mock MAC address pool must have host addresses"); let dpus = dpu_machines .into_iter() .map(|d| d.start(true)) .collect::>(); let state_machine = MachineStateMachine::new( - MachineInfo::Host(host_info.clone()), + MachineInfo::host(host_info.clone(), &app_context.mock_mac_pool), config, app_context.clone(), bmc_control_tx, diff --git a/crates/machine-a-tron/src/lib.rs b/crates/machine-a-tron/src/lib.rs index 9a6c509d0c..adb0150d1e 100644 --- a/crates/machine-a-tron/src/lib.rs +++ b/crates/machine-a-tron/src/lib.rs @@ -36,8 +36,8 @@ use std::time::{Duration, Instant}; pub use bmc_mock_wrapper::BmcMockRegistry; pub use config::{ - MachineATronArgs, MachineATronConfig, MachineATronContext, MachineConfig, PersistedDpuMachine, - PersistedHostMachine, + MachineATronArgs, MachineATronConfig, MachineATronContext, MachineConfig, + MockMacAddressPoolConfig, PersistedDpuMachine, PersistedHostMachine, }; pub use dpu_machine::DpuMachineHandle; pub use host_machine::HostMachineHandle; diff --git a/crates/machine-a-tron/src/machine_state_machine.rs b/crates/machine-a-tron/src/machine_state_machine.rs index c2207c40e5..9f0c00cbfd 100644 --- a/crates/machine-a-tron/src/machine_state_machine.rs +++ b/crates/machine-a-tron/src/machine_state_machine.rs @@ -219,27 +219,34 @@ impl MachineStateMachine { ) -> MachineStateMachine { let (initial_os_image, tpm_ek_certificate, bmc_dhcp_id, machine_dhcp_id, machine_info) = match persisted_machine { - PersistedMachine::Host(h) => ( - h.installed_os, - h.tpm_ek_certificate, - h.bmc_dhcp_id, - h.machine_dhcp_id, - MachineInfo::Host(HostMachineInfo { - hw_type: h.hw_type.unwrap_or_default(), - bmc_mac_address: h.bmc_mac_address, - serial: h.serial, - dpus: h.dpus.into_iter().map(Into::into).collect(), - non_dpu_mac_address: h.non_dpu_mac_address, - nvos_mac_addresses: h.nvos_mac_addresses, - switch_serial_number: h.switch_serial_number, - }), - ), + PersistedMachine::Host(h) => { + let hw_type = h.hw_type.unwrap_or_default(); + ( + h.installed_os, + h.tpm_ek_certificate, + h.bmc_dhcp_id, + h.machine_dhcp_id, + MachineInfo::host( + HostMachineInfo { + hw_type, + bmc_mac_address: h.bmc_mac_address, + serial: h.serial, + dpus: h.dpus.into_iter().map(Into::into).collect(), + non_dpu_mac_address: h.non_dpu_mac_address, + nvos_mac_addresses: h.nvos_mac_addresses, + switch_serial_number: h.switch_serial_number, + discovery_info: None, + }, + &app_context.mock_mac_pool, + ), + ) + } PersistedMachine::Dpu(d) => ( d.installed_os, None, d.bmc_dhcp_id, d.machine_dhcp_id, - MachineInfo::Dpu(d.into()), + MachineInfo::dpu(d.into()), ), }; let (fsm, actions) = MachineFsm::init(true, Self::is_bmc_only(&machine_info, &config)); diff --git a/crates/machine-a-tron/src/main.rs b/crates/machine-a-tron/src/main.rs index 73f0a12179..003eaf5413 100644 --- a/crates/machine-a-tron/src/main.rs +++ b/crates/machine-a-tron/src/main.rs @@ -136,6 +136,7 @@ async fn main() -> Result<(), Box> { let bmc_mock_port = app_config.bmc_mock_port; let tui_enabled = app_config.tui_enabled; + let mock_mac_pool = Arc::new(app_config.mock_mac_pool()); let app_context = Arc::new(MachineATronContext { app_config, @@ -145,6 +146,7 @@ async fn main() -> Result<(), Box> { api_throttler, desired_firmware_versions, forge_api_client, + mock_mac_pool, }); let info = app_context.forge_api_client.version(false).await?; diff --git a/crates/utils/src/test_support/mac_address_pool.rs b/crates/utils/src/test_support/mac_address_pool.rs new file mode 100644 index 0000000000..5c1c74603c --- /dev/null +++ b/crates/utils/src/test_support/mac_address_pool.rs @@ -0,0 +1,161 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::fmt; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use mac_address::MacAddress; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct MacAddressPoolConfig { + /// The first MAC address in the pool as a byte array. + pub start: [u8; 6], + /// The number of addresses in the pool. + pub length: usize, +} + +#[derive(Debug, Eq, PartialEq)] +pub enum MacAddressPoolError { + Depleted { + config: MacAddressPoolConfig, + attempted_offset: usize, + }, +} + +impl fmt::Display for MacAddressPoolError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MacAddressPoolError::Depleted { config, .. } => { + write!(f, "Mac address pool with config {config:?} is depleted") + } + } + } +} + +impl std::error::Error for MacAddressPoolError {} + +#[derive(Debug)] +pub struct MacAddressPool { + config: MacAddressPoolConfig, + used: AtomicUsize, +} + +impl MacAddressPool { + pub const fn new(config: MacAddressPoolConfig) -> Self { + Self { + config, + used: AtomicUsize::new(0), + } + } + + pub fn config(&self) -> MacAddressPoolConfig { + self.config + } + + pub fn allocate(&self) -> Result { + loop { + let offset = self.used.load(Ordering::SeqCst); + if offset >= self.config.length { + return Err(MacAddressPoolError::Depleted { + config: self.config, + attempted_offset: offset, + }); + } + + if self + .used + .compare_exchange(offset, offset + 1, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + { + return Ok(mac_address_at_offset(self.config.start, offset)); + } + } + } + + pub fn contains(&self, address: MacAddress) -> bool { + let address = to_u64_be(address.bytes()); + let min = to_u64_be(self.config.start); + let Some(max) = min.checked_add(self.config.length as u64) else { + return false; + }; + + (min..max).contains(&address) + } +} + +fn mac_address_at_offset(start: [u8; 6], offset: usize) -> MacAddress { + let u64_address = to_u64_be(start) + offset as u64; + let mut bytes = [0u8; 6]; + bytes.copy_from_slice(&u64_address.to_be_bytes()[2..8]); + MacAddress::new(bytes) +} + +fn to_u64_be(bytes: [u8; 6]) -> u64 { + u64::from_be_bytes([ + 0, 0, bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], + ]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn allocate_addresses() { + let pool = MacAddressPool::new(MacAddressPoolConfig { + start: [0x11, 0x12, 0x13, 0x14, 0x15, 0x1], + length: 256, + }); + assert!(!pool.contains(MacAddress::new([0x11, 0x12, 0x13, 0x14, 0x15, 0]))); + + for i in 1..=255 { + let expected = MacAddress::new([0x11, 0x12, 0x13, 0x14, 0x15, i as u8]); + assert_eq!(pool.allocate().unwrap(), expected); + assert!(pool.contains(expected)); + } + let expected = MacAddress::new([0x11, 0x12, 0x13, 0x14, 0x16, 0]); + assert_eq!(pool.allocate().unwrap(), expected); + assert!(pool.contains(expected)); + assert!(!pool.contains(MacAddress::new([0x11, 0x12, 0x13, 0x14, 0x16, 1]))); + } + + #[test] + fn depleted_pool_returns_error() { + let config = MacAddressPoolConfig { + start: [0x11, 0x12, 0x13, 0x14, 0x15, 0xFF], + length: 2, + }; + let pool = MacAddressPool::new(config); + + assert_eq!( + pool.allocate().unwrap(), + MacAddress::new([0x11, 0x12, 0x13, 0x14, 0x15, 0xFF]) + ); + assert_eq!( + pool.allocate().unwrap(), + MacAddress::new([0x11, 0x12, 0x13, 0x14, 0x16, 0x00]) + ); + assert_eq!( + pool.allocate(), + Err(MacAddressPoolError::Depleted { + config, + attempted_offset: 2, + }) + ); + } +} diff --git a/crates/utils/src/test_support/mod.rs b/crates/utils/src/test_support/mod.rs index a8709d61f3..78d262f1b1 100644 --- a/crates/utils/src/test_support/mod.rs +++ b/crates/utils/src/test_support/mod.rs @@ -16,4 +16,5 @@ */ pub mod certs; +pub mod mac_address_pool; pub mod test_meter;