fix(host_network): skip configuration for host with only 1 interface/port
All checks were successful
Run Check Script / check (pull_request) Successful in 1m11s

This commit is contained in:
Ian Letourneau 2025-11-05 12:47:41 -05:00
parent 148504439e
commit c89c30e8f2

View File

@ -1,6 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use harmony_types::id::Id; use harmony_types::id::Id;
use log::{debug, info}; use log::{debug, info, warn};
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
@ -49,6 +49,13 @@ impl HostNetworkConfigurationInterpret {
switch_ports: vec![], switch_ports: vec![],
}); });
} }
if host.network.len() == 1 {
info!("[Host {current_host}/{total_hosts}] Only one interface to configure, skipping");
return Ok(HostNetworkConfig {
host_id: host.id.clone(),
switch_ports: vec![],
});
}
let switch_ports = self let switch_ports = self
.collect_switch_ports_for_host(topology, host, current_host, total_hosts) .collect_switch_ports_for_host(topology, host, current_host, total_hosts)
@ -59,7 +66,7 @@ impl HostNetworkConfigurationInterpret {
switch_ports, switch_ports,
}; };
if !config.switch_ports.is_empty() { if config.switch_ports.len() > 1 {
info!( info!(
"[Host {current_host}/{total_hosts}] Found {} ports for {} interfaces", "[Host {current_host}/{total_hosts}] Found {} ports for {} interfaces",
config.switch_ports.len(), config.switch_ports.len(),
@ -76,11 +83,16 @@ impl HostNetworkConfigurationInterpret {
.map_err(|e| { .map_err(|e| {
InterpretError::new(format!("Failed to configure host network: {e}")) InterpretError::new(format!("Failed to configure host network: {e}"))
})?; })?;
} else { } else if config.switch_ports.is_empty() {
info!( info!(
"[Host {current_host}/{total_hosts}] No ports found for {} interfaces, skipping", "[Host {current_host}/{total_hosts}] No ports found for {} interfaces, skipping",
host.network.len() host.network.len()
); );
} else {
warn!(
"[Host {current_host}/{total_hosts}] Found a single port for {} interfaces, skipping",
host.network.len()
);
} }
Ok(config) Ok(config)
@ -227,6 +239,7 @@ impl<T: Topology + NetworkManager + Switch> Interpret<T> for HostNetworkConfigur
host_configurations.push(host_configuration); host_configurations.push(host_configuration);
current_host += 1; current_host += 1;
} }
if current_host > 1 { if current_host > 1 {
let details = self.format_host_configuration(host_configurations); let details = self.format_host_configuration(host_configurations);
@ -279,6 +292,18 @@ mod tests {
speed_mbps: None, speed_mbps: None,
mtu: 1, mtu: 1,
}; };
pub static ref YET_ANOTHER_EXISTING_INTERFACE: NetworkInterface = NetworkInterface {
mac_address: MacAddress::try_from("AA:BB:CC:DD:EE:F3".to_string()).unwrap(),
name: "interface-3".into(),
speed_mbps: None,
mtu: 1,
};
pub static ref LAST_EXISTING_INTERFACE: NetworkInterface = NetworkInterface {
mac_address: MacAddress::try_from("AA:BB:CC:DD:EE:F4".to_string()).unwrap(),
name: "interface-4".into(),
speed_mbps: None,
mtu: 1,
};
pub static ref UNKNOWN_INTERFACE: NetworkInterface = NetworkInterface { pub static ref UNKNOWN_INTERFACE: NetworkInterface = NetworkInterface {
mac_address: MacAddress::try_from("11:22:33:44:55:61".to_string()).unwrap(), mac_address: MacAddress::try_from("11:22:33:44:55:61".to_string()).unwrap(),
name: "unknown-interface".into(), name: "unknown-interface".into(),
@ -287,6 +312,8 @@ mod tests {
}; };
pub static ref PORT: PortLocation = PortLocation(1, 0, 42); pub static ref PORT: PortLocation = PortLocation(1, 0, 42);
pub static ref ANOTHER_PORT: PortLocation = PortLocation(2, 0, 42); pub static ref ANOTHER_PORT: PortLocation = PortLocation(2, 0, 42);
pub static ref YET_ANOTHER_PORT: PortLocation = PortLocation(1, 0, 45);
pub static ref LAST_PORT: PortLocation = PortLocation(2, 0, 45);
} }
#[tokio::test] #[tokio::test]
@ -314,7 +341,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn host_with_one_mac_address_should_configure_bond_with_one_interface() { async fn host_with_one_mac_address_should_skip_host_configuration() {
let host = given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]); let host = given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]);
let score = given_score(vec![host]); let score = given_score(vec![host]);
let topology = TopologyWithSwitch::new(); let topology = TopologyWithSwitch::new();
@ -322,37 +349,9 @@ mod tests {
let _ = score.interpret(&Inventory::empty(), &topology).await; let _ = score.interpret(&Inventory::empty(), &topology).await;
let config = topology.configured_bonds.lock().unwrap(); let config = topology.configured_bonds.lock().unwrap();
assert_that!(*config).contains_exactly(vec![( assert_that!(*config).is_empty();
HOST_ID.clone(),
HostNetworkConfig {
host_id: HOST_ID.clone(),
switch_ports: vec![SwitchPort {
interface: EXISTING_INTERFACE.clone(),
port: PORT.clone(),
}],
},
)]);
}
#[tokio::test]
async fn host_with_one_mac_address_should_configure_port_channel_with_one_interface() {
let host = given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]);
let score = given_score(vec![host]);
let topology = TopologyWithSwitch::new();
let _ = score.interpret(&Inventory::empty(), &topology).await;
let config = topology.configured_port_channels.lock().unwrap(); let config = topology.configured_port_channels.lock().unwrap();
assert_that!(*config).contains_exactly(vec![( assert_that!(*config).is_empty();
HOST_ID.clone(),
HostNetworkConfig {
host_id: HOST_ID.clone(),
switch_ports: vec![SwitchPort {
interface: EXISTING_INTERFACE.clone(),
port: PORT.clone(),
}],
},
)]);
} }
#[tokio::test] #[tokio::test]
@ -423,8 +422,20 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn multiple_hosts_should_configure_one_bond_per_host() { async fn multiple_hosts_should_configure_one_bond_per_host() {
let score = given_score(vec![ let score = given_score(vec![
given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]), given_host(
given_host(&ANOTHER_HOST_ID, vec![ANOTHER_EXISTING_INTERFACE.clone()]), &HOST_ID,
vec![
EXISTING_INTERFACE.clone(),
ANOTHER_EXISTING_INTERFACE.clone(),
],
),
given_host(
&ANOTHER_HOST_ID,
vec![
YET_ANOTHER_EXISTING_INTERFACE.clone(),
LAST_EXISTING_INTERFACE.clone(),
],
),
]); ]);
let topology = TopologyWithSwitch::new(); let topology = TopologyWithSwitch::new();
@ -436,20 +447,32 @@ mod tests {
HOST_ID.clone(), HOST_ID.clone(),
HostNetworkConfig { HostNetworkConfig {
host_id: HOST_ID.clone(), host_id: HOST_ID.clone(),
switch_ports: vec![SwitchPort { switch_ports: vec![
interface: EXISTING_INTERFACE.clone(), SwitchPort {
port: PORT.clone(), interface: EXISTING_INTERFACE.clone(),
}], port: PORT.clone(),
},
SwitchPort {
interface: ANOTHER_EXISTING_INTERFACE.clone(),
port: ANOTHER_PORT.clone(),
},
],
}, },
), ),
( (
ANOTHER_HOST_ID.clone(), ANOTHER_HOST_ID.clone(),
HostNetworkConfig { HostNetworkConfig {
host_id: ANOTHER_HOST_ID.clone(), host_id: ANOTHER_HOST_ID.clone(),
switch_ports: vec![SwitchPort { switch_ports: vec![
interface: ANOTHER_EXISTING_INTERFACE.clone(), SwitchPort {
port: ANOTHER_PORT.clone(), interface: YET_ANOTHER_EXISTING_INTERFACE.clone(),
}], port: YET_ANOTHER_PORT.clone(),
},
SwitchPort {
interface: LAST_EXISTING_INTERFACE.clone(),
port: LAST_PORT.clone(),
},
],
}, },
), ),
]); ]);
@ -458,8 +481,20 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn multiple_hosts_should_configure_one_port_channel_per_host() { async fn multiple_hosts_should_configure_one_port_channel_per_host() {
let score = given_score(vec![ let score = given_score(vec![
given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]), given_host(
given_host(&ANOTHER_HOST_ID, vec![ANOTHER_EXISTING_INTERFACE.clone()]), &HOST_ID,
vec![
EXISTING_INTERFACE.clone(),
ANOTHER_EXISTING_INTERFACE.clone(),
],
),
given_host(
&ANOTHER_HOST_ID,
vec![
YET_ANOTHER_EXISTING_INTERFACE.clone(),
LAST_EXISTING_INTERFACE.clone(),
],
),
]); ]);
let topology = TopologyWithSwitch::new(); let topology = TopologyWithSwitch::new();
@ -471,27 +506,39 @@ mod tests {
HOST_ID.clone(), HOST_ID.clone(),
HostNetworkConfig { HostNetworkConfig {
host_id: HOST_ID.clone(), host_id: HOST_ID.clone(),
switch_ports: vec![SwitchPort { switch_ports: vec![
interface: EXISTING_INTERFACE.clone(), SwitchPort {
port: PORT.clone(), interface: EXISTING_INTERFACE.clone(),
}], port: PORT.clone(),
},
SwitchPort {
interface: ANOTHER_EXISTING_INTERFACE.clone(),
port: ANOTHER_PORT.clone(),
},
],
}, },
), ),
( (
ANOTHER_HOST_ID.clone(), ANOTHER_HOST_ID.clone(),
HostNetworkConfig { HostNetworkConfig {
host_id: ANOTHER_HOST_ID.clone(), host_id: ANOTHER_HOST_ID.clone(),
switch_ports: vec![SwitchPort { switch_ports: vec![
interface: ANOTHER_EXISTING_INTERFACE.clone(), SwitchPort {
port: ANOTHER_PORT.clone(), interface: YET_ANOTHER_EXISTING_INTERFACE.clone(),
}], port: YET_ANOTHER_PORT.clone(),
},
SwitchPort {
interface: LAST_EXISTING_INTERFACE.clone(),
port: LAST_PORT.clone(),
},
],
}, },
), ),
]); ]);
} }
#[tokio::test] #[tokio::test]
async fn port_not_found_for_mac_address_should_not_configure_interface() { async fn port_not_found_for_mac_address_should_not_configure_host() {
let score = given_score(vec![given_host(&HOST_ID, vec![UNKNOWN_INTERFACE.clone()])]); let score = given_score(vec![given_host(&HOST_ID, vec![UNKNOWN_INTERFACE.clone()])]);
let topology = TopologyWithSwitch::new_port_not_found(); let topology = TopologyWithSwitch::new_port_not_found();
@ -503,6 +550,22 @@ mod tests {
assert_that!(*config).is_empty(); assert_that!(*config).is_empty();
} }
#[tokio::test]
async fn only_one_port_found_for_multiple_mac_addresses_should_not_configure_host() {
let score = given_score(vec![given_host(
&HOST_ID,
vec![EXISTING_INTERFACE.clone(), UNKNOWN_INTERFACE.clone()],
)]);
let topology = TopologyWithSwitch::new_single_port_found();
let _ = score.interpret(&Inventory::empty(), &topology).await;
let config = topology.configured_port_channels.lock().unwrap();
assert_that!(*config).is_empty();
let config = topology.configured_bonds.lock().unwrap();
assert_that!(*config).is_empty();
}
fn given_score(hosts: Vec<PhysicalHost>) -> HostNetworkConfigurationScore { fn given_score(hosts: Vec<PhysicalHost>) -> HostNetworkConfigurationScore {
HostNetworkConfigurationScore { hosts } HostNetworkConfigurationScore { hosts }
} }
@ -549,7 +612,12 @@ mod tests {
impl TopologyWithSwitch { impl TopologyWithSwitch {
fn new() -> Self { fn new() -> Self {
Self { Self {
available_ports: Arc::new(Mutex::new(vec![PORT.clone(), ANOTHER_PORT.clone()])), available_ports: Arc::new(Mutex::new(vec![
PORT.clone(),
ANOTHER_PORT.clone(),
YET_ANOTHER_PORT.clone(),
LAST_PORT.clone(),
])),
configured_port_channels: Arc::new(Mutex::new(vec![])), configured_port_channels: Arc::new(Mutex::new(vec![])),
switch_setup: Arc::new(Mutex::new(false)), switch_setup: Arc::new(Mutex::new(false)),
network_manager_setup: Arc::new(Mutex::new(false)), network_manager_setup: Arc::new(Mutex::new(false)),
@ -566,6 +634,16 @@ mod tests {
configured_bonds: Arc::new(Mutex::new(vec![])), configured_bonds: Arc::new(Mutex::new(vec![])),
} }
} }
fn new_single_port_found() -> Self {
Self {
available_ports: Arc::new(Mutex::new(vec![PORT.clone()])),
configured_port_channels: Arc::new(Mutex::new(vec![])),
switch_setup: Arc::new(Mutex::new(false)),
network_manager_setup: Arc::new(Mutex::new(false)),
configured_bonds: Arc::new(Mutex::new(vec![])),
}
}
} }
#[async_trait] #[async_trait]