From fe0501b78480b13383175357d4f06387096dd974 Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Mon, 15 Sep 2025 22:05:09 -0400 Subject: [PATCH] split host & switch config --- harmony/src/domain/topology/ha_cluster.rs | 9 + harmony/src/domain/topology/network.rs | 20 +- harmony/src/modules/okd/host_network.rs | 290 ++++++++++++++++++---- 3 files changed, 269 insertions(+), 50 deletions(-) diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index ac53d01..b9a3b4e 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -28,6 +28,7 @@ use super::PreparationOutcome; use super::Router; use super::Switch; use super::SwitchError; +use super::SwitchNetworkConfig; use super::TftpServer; use super::Topology; @@ -280,6 +281,14 @@ impl Switch for HAClusterTopology { ) -> Result<(), SwitchError> { todo!() } + + async fn configure_switch_network( + &self, + _host: &PhysicalHost, + _config: SwitchNetworkConfig, + ) -> Result<(), SwitchError> { + todo!() + } } #[derive(Debug)] diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index a6127c8..4492667 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -179,8 +179,14 @@ pub trait Switch: Send + Sync { async fn configure_host_network( &self, - _host: &PhysicalHost, - _config: HostNetworkConfig, + host: &PhysicalHost, + config: HostNetworkConfig, + ) -> Result<(), SwitchError>; + + async fn configure_switch_network( + &self, + host: &PhysicalHost, + config: SwitchNetworkConfig, ) -> Result<(), SwitchError>; } @@ -200,6 +206,16 @@ pub struct SlaveInterface { // FIXME: Should we add speed as well? And other params } +#[derive(Clone, Debug, PartialEq)] +pub struct SwitchNetworkConfig { + pub port_channel: PortChannel, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PortChannel { + pub ports: Vec, +} + #[derive(Debug, Clone, new)] pub struct SwitchError { msg: String, diff --git a/harmony/src/modules/okd/host_network.rs b/harmony/src/modules/okd/host_network.rs index 60a89e7..514a9fd 100644 --- a/harmony/src/modules/okd/host_network.rs +++ b/harmony/src/modules/okd/host_network.rs @@ -8,7 +8,10 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{self, Bond, HostNetworkConfig, SlaveInterface, Switch, Topology}, + topology::{ + self, Bond, HostNetworkConfig, PortChannel, SlaveInterface, Switch, SwitchNetworkConfig, + Topology, + }, }; #[derive(Debug, Clone, Serialize)] @@ -56,20 +59,34 @@ impl Interpret for HostNetworkConfigurationInterpret { _inventory: &Inventory, topology: &T, ) -> Result { - let host = self.score.hosts.first().unwrap(); - let mac_addresses = host.get_mac_address(); - let mac_address = mac_addresses.first().unwrap(); - let host_network_config = HostNetworkConfig { - bond: Bond { - interfaces: vec![SlaveInterface { - mac_address: *mac_address, - }], - }, - }; + for host in &self.score.hosts { + let mut interfaces = vec![]; + let mut ports = vec![]; - let _ = topology - .configure_host_network(host, host_network_config) - .await; + for mac_address in host.get_mac_address() { + if let Some(port) = topology.get_port_for_mac_address(&mac_address).await { + interfaces.push(SlaveInterface { mac_address }); + ports.push(port); + } + } + + let _ = topology + .configure_host_network( + host, + HostNetworkConfig { + bond: Bond { interfaces }, + }, + ) + .await; + let _ = topology + .configure_switch_network( + host, + SwitchNetworkConfig { + port_channel: PortChannel { ports }, + }, + ) + .await; + } // foreach hosts // foreach mac addresses @@ -78,17 +95,12 @@ impl Interpret for HostNetworkConfigurationInterpret { // create bond for all valid addresses (port found) // apply network to host first, then switch (to avoid losing hosts that are already connected) // topology.configure_host_network(host, config) <--- will create bonds - // topology.configure_switch_network(port, config) <--- will create port channels + // topology.configure_switch_network(host, config) <--- will create port channels Ok(Outcome::success("".into())) } } -struct PortMapping { - port: String, - mac_address: MacAddress, -} - #[cfg(test)] mod tests { use assertor::*; @@ -98,8 +110,8 @@ mod tests { use crate::{ hardware::HostCategory, topology::{ - Bond, HostNetworkConfig, PreparationError, PreparationOutcome, SlaveInterface, - SwitchError, + Bond, HostNetworkConfig, PortChannel, PreparationError, PreparationOutcome, + SlaveInterface, SwitchError, SwitchNetworkConfig, }, }; use std::{ @@ -111,15 +123,22 @@ mod tests { lazy_static! { pub static ref HOST_ID: Id = Id::from_str("host-1").unwrap(); - pub static ref INTERFACE: MacAddress = - MacAddress::try_from("00:11:22:33:44:55".to_string()).unwrap(); + pub static ref ANOTHER_HOST_ID: Id = Id::from_str("host-2").unwrap(); + pub static ref EXISTING_INTERFACE: MacAddress = + MacAddress::try_from("00:00:00:00:00:00".to_string()).unwrap(); + pub static ref ANOTHER_EXISTING_INTERFACE: MacAddress = + MacAddress::try_from("42:42:42:42:42:42".to_string()).unwrap(); + pub static ref UNKNOWN_INTERFACE: MacAddress = + MacAddress::try_from("99:99:99:99:99:99".to_string()).unwrap(); + pub static ref PORT: String = "1/0/42".into(); + pub static ref ANOTHER_PORT: String = "2/0/42".into(); } #[tokio::test] - async fn one_host_one_mac_address_should_create_bond_with_one_interface() { - let host = given_host(&HOST_ID, *INTERFACE); + async fn host_with_one_mac_address_should_create_bond_with_one_interface() { + let host = given_host(&HOST_ID, vec![*EXISTING_INTERFACE]); let score = given_score(vec![host]); - let topology = SwitchWithPortTopology::new(); + let topology = TopologyWithSwitch::new(); let _ = score.interpret(&Inventory::empty(), &topology).await; @@ -129,28 +148,161 @@ mod tests { HostNetworkConfig { bond: Bond { interfaces: vec![SlaveInterface { - mac_address: *INTERFACE, + mac_address: *EXISTING_INTERFACE, }], }, }, )]); } - fn given_host(id: &Id, mac_address: MacAddress) -> PhysicalHost { + #[tokio::test] + async fn host_with_one_mac_address_should_create_port_channel_with_one_port() { + let host = given_host(&HOST_ID, vec![*EXISTING_INTERFACE]); + let score = given_score(vec![host]); + let topology = TopologyWithSwitch::new(); + + let _ = score.interpret(&Inventory::empty(), &topology).await; + + let configured_switch_networks = topology.configured_switch_networks.lock().unwrap(); + assert_that!(*configured_switch_networks).contains_exactly(vec![( + HOST_ID.clone(), + SwitchNetworkConfig { + port_channel: PortChannel { + ports: vec![PORT.clone()], + }, + }, + )]); + } + + #[tokio::test] + async fn host_with_multiple_mac_addresses_should_create_one_bond_with_all_interfaces() { + let score = given_score(vec![given_host( + &HOST_ID, + vec![*EXISTING_INTERFACE, *ANOTHER_EXISTING_INTERFACE], + )]); + let topology = TopologyWithSwitch::new(); + + let _ = score.interpret(&Inventory::empty(), &topology).await; + + let configured_host_networks = topology.configured_host_networks.lock().unwrap(); + assert_that!(*configured_host_networks).contains_exactly(vec![( + HOST_ID.clone(), + HostNetworkConfig { + bond: Bond { + interfaces: vec![ + SlaveInterface { + mac_address: *EXISTING_INTERFACE, + }, + SlaveInterface { + mac_address: *ANOTHER_EXISTING_INTERFACE, + }, + ], + }, + }, + )]); + } + + #[tokio::test] + async fn multiple_hosts_should_create_one_bond_per_host() { + let score = given_score(vec![ + given_host(&HOST_ID, vec![*EXISTING_INTERFACE]), + given_host(&ANOTHER_HOST_ID, vec![*ANOTHER_EXISTING_INTERFACE]), + ]); + let topology = TopologyWithSwitch::new(); + + let _ = score.interpret(&Inventory::empty(), &topology).await; + + let configured_host_networks = topology.configured_host_networks.lock().unwrap(); + assert_that!(*configured_host_networks).contains_exactly(vec![ + ( + HOST_ID.clone(), + HostNetworkConfig { + bond: Bond { + interfaces: vec![SlaveInterface { + mac_address: *EXISTING_INTERFACE, + }], + }, + }, + ), + ( + ANOTHER_HOST_ID.clone(), + HostNetworkConfig { + bond: Bond { + interfaces: vec![SlaveInterface { + mac_address: *ANOTHER_EXISTING_INTERFACE, + }], + }, + }, + ), + ]); + } + + #[tokio::test] + async fn multiple_hosts_should_create_one_port_channel_per_host() { + let score = given_score(vec![ + given_host(&HOST_ID, vec![*EXISTING_INTERFACE]), + given_host(&ANOTHER_HOST_ID, vec![*ANOTHER_EXISTING_INTERFACE]), + ]); + let topology = TopologyWithSwitch::new(); + + let _ = score.interpret(&Inventory::empty(), &topology).await; + + let configured_switch_networks = topology.configured_switch_networks.lock().unwrap(); + assert_that!(*configured_switch_networks).contains_exactly(vec![ + ( + HOST_ID.clone(), + SwitchNetworkConfig { + port_channel: PortChannel { + ports: vec![PORT.clone()], + }, + }, + ), + ( + ANOTHER_HOST_ID.clone(), + SwitchNetworkConfig { + port_channel: PortChannel { + ports: vec![ANOTHER_PORT.clone()], + }, + }, + ), + ]); + } + + #[tokio::test] + async fn port_not_found_for_mac_address_should_not_configure_interface() { + // FIXME: Should it still configure an empty bond/port channel? + let score = given_score(vec![given_host(&HOST_ID, vec![*UNKNOWN_INTERFACE])]); + let topology = TopologyWithSwitch::new_port_not_found(); + + let _ = score.interpret(&Inventory::empty(), &topology).await; + + let configured_host_networks = topology.configured_host_networks.lock().unwrap(); + assert_that!(*configured_host_networks).contains_exactly(vec![( + HOST_ID.clone(), + HostNetworkConfig { + bond: Bond { interfaces: vec![] }, + }, + )]); + let configured_switch_networks = topology.configured_switch_networks.lock().unwrap(); + assert_that!(*configured_switch_networks).contains_exactly(vec![( + HOST_ID.clone(), + SwitchNetworkConfig { + port_channel: PortChannel { ports: vec![] }, + }, + )]); + } + + fn given_score(hosts: Vec) -> HostNetworkConfigurationScore { + HostNetworkConfigurationScore { hosts } + } + + fn given_host(id: &Id, mac_addresses: Vec) -> PhysicalHost { + let network = mac_addresses.iter().map(|m| given_interface(*m)).collect(); + PhysicalHost { id: id.clone(), category: HostCategory::Server, - network: vec![NetworkInterface { - name: "interface-1".into(), - mac_address, - speed_mbps: None, - is_up: true, - mtu: 1, - ipv4_addresses: vec![], - ipv6_addresses: vec![], - driver: "driver".into(), - firmware_version: None, - }], + network, storage: vec![], labels: vec![], memory_modules: vec![], @@ -158,24 +310,46 @@ mod tests { } } - fn given_score(hosts: Vec) -> HostNetworkConfigurationScore { - HostNetworkConfigurationScore { hosts } + fn given_interface(mac_address: MacAddress) -> NetworkInterface { + NetworkInterface { + name: format!("{mac_address}"), + mac_address, + speed_mbps: None, + is_up: true, + mtu: 1, + ipv4_addresses: vec![], + ipv6_addresses: vec![], + driver: "driver".into(), + firmware_version: None, + } } - struct SwitchWithPortTopology { + struct TopologyWithSwitch { + available_ports: Vec, configured_host_networks: Arc>>, + configured_switch_networks: Arc>>, } - impl SwitchWithPortTopology { + impl TopologyWithSwitch { fn new() -> Self { Self { + available_ports: vec![PORT.clone(), ANOTHER_PORT.clone()], configured_host_networks: Arc::new(Mutex::new(vec![])), + configured_switch_networks: Arc::new(Mutex::new(vec![])), + } + } + + fn new_port_not_found() -> Self { + Self { + available_ports: vec![], + configured_host_networks: Arc::new(Mutex::new(vec![])), + configured_switch_networks: Arc::new(Mutex::new(vec![])), } } } #[async_trait] - impl Topology for SwitchWithPortTopology { + impl Topology for TopologyWithSwitch { fn name(&self) -> &str { "SwitchWithPortTopology" } @@ -186,9 +360,18 @@ mod tests { } #[async_trait] - impl Switch for SwitchWithPortTopology { - async fn get_port_for_mac_address(&self, mac_address: &MacAddress) -> Option { - Some("1/0/42".into()) + impl Switch for TopologyWithSwitch { + async fn get_port_for_mac_address(&self, _mac_address: &MacAddress) -> Option { + if self.available_ports.is_empty() { + return None; + } + + Some( + self.available_ports + .get(self.configured_host_networks.lock().unwrap().len() % 2) + .unwrap() + .clone(), + ) } async fn configure_host_network( @@ -201,5 +384,16 @@ mod tests { Ok(()) } + + async fn configure_switch_network( + &self, + host: &PhysicalHost, + config: SwitchNetworkConfig, + ) -> Result<(), SwitchError> { + let mut configured_switch_networks = self.configured_switch_networks.lock().unwrap(); + configured_switch_networks.push((host.id.clone(), config.clone())); + + Ok(()) + } } }