fix(host_network): adjust bond & port-channel configuration (partial) #175

Merged
letian merged 12 commits from nmstate into master 2025-10-29 17:09:16 +00:00
7 changed files with 76 additions and 16 deletions
Showing only changes of commit 981529751a - Show all commits

View File

@ -31,6 +31,7 @@ pub struct BrocadeOptions {
pub struct TimeoutConfig { pub struct TimeoutConfig {
pub shell_ready: Duration, pub shell_ready: Duration,
pub command_execution: Duration, pub command_execution: Duration,
pub command_output: Duration,
pub cleanup: Duration, pub cleanup: Duration,
pub message_wait: Duration, pub message_wait: Duration,
} }
@ -40,6 +41,7 @@ impl Default for TimeoutConfig {
Self { Self {
shell_ready: Duration::from_secs(10), shell_ready: Duration::from_secs(10),
command_execution: Duration::from_secs(60), // Commands like `deploy` (for a LAG) can take a while command_execution: Duration::from_secs(60), // Commands like `deploy` (for a LAG) can take a while
command_output: Duration::from_secs(5), // Delay to start logging "waiting for command output"
cleanup: Duration::from_secs(10), cleanup: Duration::from_secs(10),
message_wait: Duration::from_millis(500), message_wait: Duration::from_millis(500),
} }

View File

@ -3,6 +3,7 @@ use std::str::FromStr;
use async_trait::async_trait; use async_trait::async_trait;
use harmony_types::switch::{PortDeclaration, PortLocation}; use harmony_types::switch::{PortDeclaration, PortLocation};
use log::{debug, info}; use log::{debug, info};
use regex::Regex;
use crate::{ use crate::{
BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo, BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo,
@ -110,6 +111,30 @@ impl NetworkOperatingSystemClient {
status, status,
})) }))
} }
fn map_configure_interfaces_error(&self, err: Error) -> Error {
debug!("[Brocade] {err}");
if let Error::CommandError(message) = &err {
if message.contains("switchport")
&& message.contains("Cannot configure aggregator member")
{
let re = Regex::new(r"\(conf-if-([a-zA-Z]+)-([\d/]+)\)#").unwrap();
if let Some(caps) = re.captures(message) {
let interface_type = &caps[1];
let port_location = &caps[2];
let interface = format!("{interface_type} {port_location}");
return Error::CommandError(format!(
"Cannot configure interface '{interface}', it is a member of a port-channel (LAG)"
));
}
}
}
err
}
} }
#[async_trait] #[async_trait]
@ -199,7 +224,8 @@ impl BrocadeClient for NetworkOperatingSystemClient {
self.shell self.shell
.run_commands(commands, ExecutionMode::Regular) .run_commands(commands, ExecutionMode::Regular)
.await?; .await
.map_err(|err| self.map_configure_interfaces_error(err))?;
info!("[Brocade] Interfaces configured."); info!("[Brocade] Interfaces configured.");

View File

@ -211,7 +211,7 @@ impl BrocadeSession {
let mut output = Vec::new(); let mut output = Vec::new();
let start = Instant::now(); let start = Instant::now();
let read_timeout = Duration::from_millis(500); let read_timeout = Duration::from_millis(500);
let log_interval = Duration::from_secs(3); let log_interval = Duration::from_secs(5);
let mut last_log = Instant::now(); let mut last_log = Instant::now();
loop { loop {
@ -221,7 +221,9 @@ impl BrocadeSession {
)); ));
} }
if start.elapsed() > Duration::from_secs(5) && last_log.elapsed() > log_interval { if start.elapsed() > self.options.timeouts.command_output
&& last_log.elapsed() > log_interval
{
info!("[Brocade] Waiting for command output..."); info!("[Brocade] Waiting for command output...");
last_log = Instant::now(); last_log = Instant::now();
} }
@ -276,7 +278,7 @@ impl BrocadeSession {
let output_lower = output.to_lowercase(); let output_lower = output.to_lowercase();
if ERROR_PATTERNS.iter().any(|&p| output_lower.contains(p)) { if ERROR_PATTERNS.iter().any(|&p| output_lower.contains(p)) {
return Err(Error::CommandError(format!( return Err(Error::CommandError(format!(
"Command '{command}' failed: {}", "Command error: {}",
output.trim() output.trim()
))); )));
} }

View File

@ -167,7 +167,7 @@ impl HAClusterTopology {
let bond_config = self.create_bond_configuration(config); let bond_config = self.create_bond_configuration(config);
debug!( debug!(
"Configuring bond for host {}: {bond_config:#?}", "Applying NMState bond config for host {}: {bond_config:#?}",
config.host_id config.host_id
); );
self.k8s_client() self.k8s_client()
@ -185,9 +185,11 @@ impl HAClusterTopology {
config: &HostNetworkConfig, config: &HostNetworkConfig,
) -> NodeNetworkConfigurationPolicy { ) -> NodeNetworkConfigurationPolicy {
let host_name = &config.host_id; let host_name = &config.host_id;
let bond_id = self.get_next_bond_id(); let bond_id = self.get_next_bond_id();
let bond_name = format!("bond{bond_id}"); let bond_name = format!("bond{bond_id}");
info!("Configuring bond '{bond_name}' for host '{host_name}'...");
let mut bond_mtu: Option<u32> = None; let mut bond_mtu: Option<u32> = None;
let mut bond_mac_address: Option<String> = None; let mut bond_mac_address: Option<String> = None;
let mut bond_ports = Vec::new(); let mut bond_ports = Vec::new();

View File

@ -116,11 +116,15 @@ impl Interpret<HAClusterTopology> for OKDSetupPersistNetworkBondInterpet {
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
let nodes = self.get_nodes(inventory, topology).await?; let nodes = self.get_nodes(inventory, topology).await?;
self.persist_network_bond(inventory, topology, &nodes) let res = self.persist_network_bond(inventory, topology, &nodes).await;
.await?;
Ok(Outcome::success( match res {
"Network bond successfully persisted".into(), Ok(_) => Ok(Outcome::success(
)) "Network bond successfully persisted".into(),
)),
Err(_) => Err(InterpretError::new(
"Failed to persist network bond".to_string(),
)),
}
} }
} }

View File

@ -42,8 +42,17 @@ impl HostNetworkConfigurationInterpret {
current_host: &usize, current_host: &usize,
total_hosts: &usize, total_hosts: &usize,
) -> Result<HostNetworkConfig, InterpretError> { ) -> Result<HostNetworkConfig, InterpretError> {
info!("[Host {current_host}/{total_hosts}] Collecting ports on switch..."); if host.network.is_empty() {
let switch_ports = self.collect_switch_ports_for_host(topology, host).await?; info!("[Host {current_host}/{total_hosts}] No interfaces to configure, skipping");
return Ok(HostNetworkConfig {
host_id: host.id.clone(),
switch_ports: vec![],
});
}
let switch_ports = self
.collect_switch_ports_for_host(topology, host, current_host, total_hosts)
.await?;
let config = HostNetworkConfig { let config = HostNetworkConfig {
host_id: host.id.clone(), host_id: host.id.clone(),
@ -63,7 +72,10 @@ impl HostNetworkConfigurationInterpret {
.await .await
.map_err(|e| InterpretError::new(format!("Failed to configure host: {e}")))?; .map_err(|e| InterpretError::new(format!("Failed to configure host: {e}")))?;
} else { } else {
info!("[Host {current_host}/{total_hosts}] No ports found"); info!(
"[Host {current_host}/{total_hosts}] No ports found for {} interfaces, skipping",
host.network.len()
);
} }
Ok(config) Ok(config)
@ -73,14 +85,24 @@ impl HostNetworkConfigurationInterpret {
&self, &self,
topology: &T, topology: &T,
host: &PhysicalHost, host: &PhysicalHost,
current_host: &usize,
total_hosts: &usize,
) -> Result<Vec<SwitchPort>, InterpretError> { ) -> Result<Vec<SwitchPort>, InterpretError> {
let mut switch_ports = vec![]; let mut switch_ports = vec![];
if host.network.is_empty() {
return Ok(switch_ports);
}
info!("[Host {current_host}/{total_hosts}] Collecting ports on switch...");
for network_interface in &host.network { for network_interface in &host.network {
let mac_address = network_interface.mac_address; let mac_address = network_interface.mac_address;
match topology.get_port_for_mac_address(&mac_address).await { match topology.get_port_for_mac_address(&mac_address).await {
Ok(Some(port)) => { Ok(Some(port)) => {
info!(
"[Host {current_host}/{total_hosts}] Found port '{port}' for '{mac_address}'"
);
switch_ports.push(SwitchPort { switch_ports.push(SwitchPort {
interface: NetworkInterface { interface: NetworkInterface {
name: network_interface.name.clone(), name: network_interface.name.clone(),
@ -91,7 +113,7 @@ impl HostNetworkConfigurationInterpret {
port, port,
}); });
} }
Ok(None) => debug!("No port found for host '{}', skipping", host.id), Ok(None) => debug!("No port found for '{mac_address}', skipping"),
Err(e) => { Err(e) => {
return Err(InterpretError::new(format!( return Err(InterpretError::new(format!(
"Failed to get port for host '{}': {}", "Failed to get port for host '{}': {}",
@ -176,10 +198,12 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
let host_count = self.score.hosts.len(); let host_count = self.score.hosts.len();
info!("Started network configuration for {host_count} host(s)...",); info!("Started network configuration for {host_count} host(s)...",);
info!("Setting up switch with sane defaults...");
topology topology
.setup_switch() .setup_switch()
.await .await
.map_err(|e| InterpretError::new(format!("Switch setup failed: {e}")))?; .map_err(|e| InterpretError::new(format!("Switch setup failed: {e}")))?;
info!("Switch ready");
let mut current_host = 1; let mut current_host = 1;
let mut host_configurations = vec![]; let mut host_configurations = vec![];

View File

@ -40,7 +40,7 @@ pub fn init() {
HarmonyEvent::HarmonyFinished => { HarmonyEvent::HarmonyFinished => {
if !details.is_empty() { if !details.is_empty() {
println!( println!(
"\n{} All done! Here's what's next for you:", "\n{} All done! Here's a few info for you:",
theme::EMOJI_SUMMARY theme::EMOJI_SUMMARY
); );
for detail in details.iter() { for detail in details.iter() {