diff --git a/brocade/src/network_operating_system.rs b/brocade/src/network_operating_system.rs index b14bc08..b1100b4 100644 --- a/brocade/src/network_operating_system.rs +++ b/brocade/src/network_operating_system.rs @@ -247,7 +247,12 @@ impl BrocadeClient for NetworkOperatingSystemClient { ports: &[PortLocation], ) -> Result<(), Error> { info!( - "[Brocade] Configuring port-channel '{channel_name} {channel_id}' with ports: {ports:?}" + "[Brocade] Configuring port-channel '{channel_id} {channel_name}' with ports: {}", + ports + .iter() + .map(|p| format!("{p}")) + .collect::>() + .join(", ") ); let interfaces = self.get_interfaces().await?; diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 54b36d8..1892cef 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -12,7 +12,6 @@ use log::info; use crate::data::FileContent; use crate::executors::ExecutorError; -use crate::hardware::PhysicalHost; use crate::infra::brocade::BrocadeSwitchAuth; use crate::infra::brocade::BrocadeSwitchClient; use crate::modules::okd::crd::nmstate::{ @@ -162,11 +161,7 @@ impl HAClusterTopology { 42 // FIXME: Find a better way to declare the bond id } - async fn configure_bond( - &self, - host: &PhysicalHost, - config: &HostNetworkConfig, - ) -> Result<(), SwitchError> { + async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> { self.ensure_nmstate_operator_installed() .await .map_err(|e| { @@ -175,8 +170,11 @@ impl HAClusterTopology { )) })?; - let bond_config = self.create_bond_configuration(host, config); - debug!("Configuring bond for host {host:?}: {bond_config:#?}"); + let bond_config = self.create_bond_configuration(config); + debug!( + "Configuring bond for host {}: {bond_config:#?}", + config.host_id + ); self.k8s_client() .await .unwrap() @@ -189,10 +187,9 @@ impl HAClusterTopology { fn create_bond_configuration( &self, - host: &PhysicalHost, config: &HostNetworkConfig, ) -> NodeNetworkConfigurationPolicy { - let host_name = host.id.clone(); + let host_name = &config.host_id; let bond_id = self.get_next_bond_id(); let bond_name = format!("bond{bond_id}"); @@ -294,18 +291,14 @@ impl HAClusterTopology { Ok(Box::new(client)) } - async fn configure_port_channel( - &self, - host: &PhysicalHost, - config: &HostNetworkConfig, - ) -> Result<(), SwitchError> { + async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> { debug!("Configuring port channel: {config:#?}"); let client = self.get_switch_client().await?; let switch_ports = config.switch_ports.iter().map(|s| s.port.clone()).collect(); client - .configure_port_channel(&format!("Harmony_{}", host.id), switch_ports) + .configure_port_channel(&format!("Harmony_{}", config.host_id), switch_ports) .await .map_err(|e| SwitchError::new(format!("Failed to configure switch: {e}")))?; @@ -504,13 +497,9 @@ impl Switch for HAClusterTopology { Ok(port) } - async fn configure_host_network( - &self, - host: &PhysicalHost, - config: HostNetworkConfig, - ) -> Result<(), SwitchError> { - self.configure_bond(host, &config).await?; - self.configure_port_channel(host, &config).await + async fn configure_host_network(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> { + self.configure_bond(config).await?; + self.configure_port_channel(config).await } } diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 99db03a..8a939a3 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -3,6 +3,7 @@ use std::{error::Error, net::Ipv4Addr, str::FromStr, sync::Arc}; use async_trait::async_trait; use derive_new::new; use harmony_types::{ + id::Id, net::{IpAddress, MacAddress}, switch::PortLocation, }; @@ -185,15 +186,12 @@ pub trait Switch: Send + Sync { mac_address: &MacAddress, ) -> Result, SwitchError>; - async fn configure_host_network( - &self, - host: &PhysicalHost, - config: HostNetworkConfig, - ) -> Result<(), SwitchError>; + async fn configure_host_network(&self, config: &HostNetworkConfig) -> Result<(), SwitchError>; } #[derive(Clone, Debug, PartialEq)] pub struct HostNetworkConfig { + pub host_id: Id, pub switch_ports: Vec, } diff --git a/harmony/src/modules/okd/bootstrap_persist_network_bond.rs b/harmony/src/modules/okd/bootstrap_persist_network_bond.rs index 6c486ea..fd901f1 100644 --- a/harmony/src/modules/okd/bootstrap_persist_network_bond.rs +++ b/harmony/src/modules/okd/bootstrap_persist_network_bond.rs @@ -15,7 +15,7 @@ use log::info; use serde::Serialize; // ------------------------------------------------------------------------------------------------- -// Step XX: Persist Network Bond +// Persist Network Bond // - Persist bonding via NMState // - Persist port channels on the Switch // ------------------------------------------------------------------------------------------------- @@ -80,18 +80,13 @@ impl OKDSetupPersistNetworkBondInterpet { topology: &HAClusterTopology, hosts: &Vec, ) -> Result<(), InterpretError> { - info!("[ControlPlane] Ensuring persistent bonding"); + info!("Ensuring persistent bonding"); + let score = HostNetworkConfigurationScore { hosts: hosts.clone(), }; score.interpret(inventory, topology).await?; - inquire::Confirm::new( - "Network configuration for control plane nodes is not automated yet. Configure it manually if needed.", - ) - .prompt() - .map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?; - Ok(()) } } diff --git a/harmony/src/modules/okd/host_network.rs b/harmony/src/modules/okd/host_network.rs index 3bc8c3c..81ce4b5 100644 --- a/harmony/src/modules/okd/host_network.rs +++ b/harmony/src/modules/okd/host_network.rs @@ -39,16 +39,34 @@ impl HostNetworkConfigurationInterpret { &self, topology: &T, host: &PhysicalHost, - ) -> Result<(), InterpretError> { + current_host: &usize, + total_hosts: &usize, + ) -> Result { + info!("[Host {current_host}/{total_hosts}] Collecting ports on switch..."); let switch_ports = self.collect_switch_ports_for_host(topology, host).await?; - if !switch_ports.is_empty() { + + let config = HostNetworkConfig { + host_id: host.id.clone(), + switch_ports, + }; + + if !config.switch_ports.is_empty() { + info!( + "[Host {current_host}/{total_hosts}] Found {} ports for {} interfaces", + config.switch_ports.len(), + host.network.len() + ); + + info!("[Host {current_host}/{total_hosts}] Configuring host network..."); topology - .configure_host_network(host, HostNetworkConfig { switch_ports }) + .configure_host_network(&config) .await .map_err(|e| InterpretError::new(format!("Failed to configure host: {e}")))?; + } else { + info!("[Host {current_host}/{total_hosts}] No ports found"); } - Ok(()) + Ok(config) } async fn collect_switch_ports_for_host( @@ -85,6 +103,47 @@ impl HostNetworkConfigurationInterpret { Ok(switch_ports) } + + fn format_host_configuration(&self, configs: Vec) -> Vec { + let mut report = vec![ + "Network Configuration Report".to_string(), + "------------------------------------------------------------------".to_string(), + ]; + + for config in configs { + let host = self + .score + .hosts + .iter() + .find(|h| h.id == config.host_id) + .unwrap(); + + println!("[Host] {host}"); + + if config.switch_ports.is_empty() { + report.push(format!( + "⏭️ Host {}: SKIPPED (No matching switch ports found)", + config.host_id + )); + } else { + let mappings: Vec = config + .switch_ports + .iter() + .map(|p| format!("[{} -> {}]", p.interface.name, p.port)) + .collect(); + + report.push(format!( + "✅ Host {}: Bonded {} port(s) {}", + config.host_id, + config.switch_ports.len(), + mappings.join(", ") + )); + } + } + report + .push("------------------------------------------------------------------".to_string()); + report + } } #[async_trait] @@ -114,27 +173,36 @@ impl Interpret for HostNetworkConfigurationInterpret { return Ok(Outcome::noop("No hosts to configure".into())); } - info!( - "Started network configuration for {} host(s)...", - self.score.hosts.len() - ); + let host_count = self.score.hosts.len(); + info!("Started network configuration for {host_count} host(s)...",); topology .setup_switch() .await .map_err(|e| InterpretError::new(format!("Switch setup failed: {e}")))?; - let mut configured_host_count = 0; - for host in &self.score.hosts { - self.configure_network_for_host(topology, host).await?; - configured_host_count += 1; - } + let mut current_host = 1; + let mut host_configurations = vec![]; - if configured_host_count > 0 { - Ok(Outcome::success(format!( - "Configured {configured_host_count}/{} host(s)", - self.score.hosts.len() - ))) + for host in &self.score.hosts { + let host_configuration = self + .configure_network_for_host(topology, host, ¤t_host, &host_count) + .await?; + + host_configurations.push(host_configuration); + current_host += 1; + } + if current_host > 1 { + let details = self.format_host_configuration(host_configurations); + + Ok(Outcome::success_with_details( + format!( + "Configured {}/{} host(s)", + current_host - 1, + self.score.hosts.len() + ), + details, + )) } else { Ok(Outcome::noop("No hosts configured".into())) } @@ -209,6 +277,7 @@ mod tests { assert_that!(*configured_host_networks).contains_exactly(vec![( HOST_ID.clone(), HostNetworkConfig { + host_id: HOST_ID.clone(), switch_ports: vec![SwitchPort { interface: EXISTING_INTERFACE.clone(), port: PORT.clone(), @@ -234,6 +303,7 @@ mod tests { assert_that!(*configured_host_networks).contains_exactly(vec![( HOST_ID.clone(), HostNetworkConfig { + host_id: HOST_ID.clone(), switch_ports: vec![ SwitchPort { interface: EXISTING_INTERFACE.clone(), @@ -263,6 +333,7 @@ mod tests { ( HOST_ID.clone(), HostNetworkConfig { + host_id: HOST_ID.clone(), switch_ports: vec![SwitchPort { interface: EXISTING_INTERFACE.clone(), port: PORT.clone(), @@ -272,6 +343,7 @@ mod tests { ( ANOTHER_HOST_ID.clone(), HostNetworkConfig { + host_id: ANOTHER_HOST_ID.clone(), switch_ports: vec![SwitchPort { interface: ANOTHER_EXISTING_INTERFACE.clone(), port: ANOTHER_PORT.clone(), @@ -382,11 +454,10 @@ mod tests { async fn configure_host_network( &self, - host: &PhysicalHost, - config: HostNetworkConfig, + config: &HostNetworkConfig, ) -> Result<(), SwitchError> { let mut configured_host_networks = self.configured_host_networks.lock().unwrap(); - configured_host_networks.push((host.id.clone(), config.clone())); + configured_host_networks.push((config.host_id.clone(), config.clone())); Ok(()) }