Some checks failed
Run Check Script / check (pull_request) Has been cancelled
395 lines
13 KiB
Rust
395 lines
13 KiB
Rust
use async_trait::async_trait;
|
|
use harmony_types::id::Id;
|
|
use log::{debug, info};
|
|
use serde::Serialize;
|
|
|
|
use crate::{
|
|
data::Version,
|
|
hardware::PhysicalHost,
|
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
|
inventory::Inventory,
|
|
score::Score,
|
|
topology::{HostNetworkConfig, NetworkInterface, Switch, SwitchPort, Topology},
|
|
};
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct HostNetworkConfigurationScore {
|
|
pub hosts: Vec<PhysicalHost>,
|
|
}
|
|
|
|
impl<T: Topology + Switch> Score<T> for HostNetworkConfigurationScore {
|
|
fn name(&self) -> String {
|
|
"HostNetworkConfigurationScore".into()
|
|
}
|
|
|
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
|
Box::new(HostNetworkConfigurationInterpret {
|
|
score: self.clone(),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct HostNetworkConfigurationInterpret {
|
|
score: HostNetworkConfigurationScore,
|
|
}
|
|
|
|
impl HostNetworkConfigurationInterpret {
|
|
async fn configure_network_for_host<T: Topology + Switch>(
|
|
&self,
|
|
topology: &T,
|
|
host: &PhysicalHost,
|
|
) -> Result<(), InterpretError> {
|
|
let switch_ports = self.collect_switch_ports_for_host(topology, host).await?;
|
|
if !switch_ports.is_empty() {
|
|
topology
|
|
.configure_host_network(host, HostNetworkConfig { switch_ports })
|
|
.await
|
|
.map_err(|e| InterpretError::new(format!("Failed to configure host: {e}")))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn collect_switch_ports_for_host<T: Topology + Switch>(
|
|
&self,
|
|
topology: &T,
|
|
host: &PhysicalHost,
|
|
) -> Result<Vec<SwitchPort>, InterpretError> {
|
|
let mut switch_ports = vec![];
|
|
|
|
for network_interface in &host.network {
|
|
let mac_address = network_interface.mac_address;
|
|
|
|
match topology.get_port_for_mac_address(&mac_address).await {
|
|
Ok(Some(port)) => {
|
|
switch_ports.push(SwitchPort {
|
|
interface: NetworkInterface {
|
|
name: network_interface.name.clone(),
|
|
mac_address,
|
|
speed_mbps: network_interface.speed_mbps,
|
|
mtu: network_interface.mtu,
|
|
},
|
|
port,
|
|
});
|
|
}
|
|
Ok(None) => debug!("No port found for host '{}', skipping", host.id),
|
|
Err(e) => {
|
|
return Err(InterpretError::new(format!(
|
|
"Failed to get port for host '{}': {}",
|
|
host.id, e
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(switch_ports)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
|
|
fn get_name(&self) -> InterpretName {
|
|
InterpretName::Custom("HostNetworkConfigurationInterpret")
|
|
}
|
|
|
|
fn get_version(&self) -> Version {
|
|
todo!()
|
|
}
|
|
|
|
fn get_status(&self) -> InterpretStatus {
|
|
todo!()
|
|
}
|
|
|
|
fn get_children(&self) -> Vec<Id> {
|
|
vec![]
|
|
}
|
|
|
|
async fn execute(
|
|
&self,
|
|
_inventory: &Inventory,
|
|
topology: &T,
|
|
) -> Result<Outcome, InterpretError> {
|
|
if self.score.hosts.is_empty() {
|
|
return Ok(Outcome::noop("No hosts to configure".into()));
|
|
}
|
|
|
|
info!(
|
|
"Started network configuration for {} host(s)...",
|
|
self.score.hosts.len()
|
|
);
|
|
|
|
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;
|
|
}
|
|
|
|
if configured_host_count > 0 {
|
|
Ok(Outcome::success(format!(
|
|
"Configured {configured_host_count}/{} host(s)",
|
|
self.score.hosts.len()
|
|
)))
|
|
} else {
|
|
Ok(Outcome::noop("No hosts configured".into()))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use assertor::*;
|
|
use harmony_types::{net::MacAddress, switch::PortLocation};
|
|
use lazy_static::lazy_static;
|
|
|
|
use crate::{
|
|
hardware::HostCategory,
|
|
topology::{
|
|
HostNetworkConfig, PreparationError, PreparationOutcome, SwitchError, SwitchPort,
|
|
},
|
|
};
|
|
use std::{
|
|
str::FromStr,
|
|
sync::{Arc, Mutex},
|
|
};
|
|
|
|
use super::*;
|
|
|
|
lazy_static! {
|
|
pub static ref HOST_ID: Id = Id::from_str("host-1").unwrap();
|
|
pub static ref ANOTHER_HOST_ID: Id = Id::from_str("host-2").unwrap();
|
|
pub static ref EXISTING_INTERFACE: NetworkInterface = NetworkInterface {
|
|
mac_address: MacAddress::try_from("AA:BB:CC:DD:EE:F1".to_string()).unwrap(),
|
|
name: "interface-1".into(),
|
|
speed_mbps: None,
|
|
mtu: 1,
|
|
};
|
|
pub static ref ANOTHER_EXISTING_INTERFACE: NetworkInterface = NetworkInterface {
|
|
mac_address: MacAddress::try_from("AA:BB:CC:DD:EE:F2".to_string()).unwrap(),
|
|
name: "interface-2".into(),
|
|
speed_mbps: None,
|
|
mtu: 1,
|
|
};
|
|
pub static ref UNKNOWN_INTERFACE: NetworkInterface = NetworkInterface {
|
|
mac_address: MacAddress::try_from("11:22:33:44:55:61".to_string()).unwrap(),
|
|
name: "unknown-interface".into(),
|
|
speed_mbps: None,
|
|
mtu: 1,
|
|
};
|
|
pub static ref PORT: PortLocation = PortLocation(1, 0, 42);
|
|
pub static ref ANOTHER_PORT: PortLocation = PortLocation(2, 0, 42);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn should_setup_switch() {
|
|
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 switch_setup = topology.switch_setup.lock().unwrap();
|
|
assert_that!(*switch_setup).is_true();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn host_with_one_mac_address_should_create_bond_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 configured_host_networks = topology.configured_host_networks.lock().unwrap();
|
|
assert_that!(*configured_host_networks).contains_exactly(vec![(
|
|
HOST_ID.clone(),
|
|
HostNetworkConfig {
|
|
switch_ports: vec![SwitchPort {
|
|
interface: EXISTING_INTERFACE.clone(),
|
|
port: 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.clone(),
|
|
ANOTHER_EXISTING_INTERFACE.clone(),
|
|
],
|
|
)]);
|
|
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 {
|
|
switch_ports: vec![
|
|
SwitchPort {
|
|
interface: EXISTING_INTERFACE.clone(),
|
|
port: PORT.clone(),
|
|
},
|
|
SwitchPort {
|
|
interface: ANOTHER_EXISTING_INTERFACE.clone(),
|
|
port: ANOTHER_PORT.clone(),
|
|
},
|
|
],
|
|
},
|
|
)]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn multiple_hosts_should_create_one_bond_per_host() {
|
|
let score = given_score(vec![
|
|
given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]),
|
|
given_host(&ANOTHER_HOST_ID, vec![ANOTHER_EXISTING_INTERFACE.clone()]),
|
|
]);
|
|
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 {
|
|
switch_ports: vec![SwitchPort {
|
|
interface: EXISTING_INTERFACE.clone(),
|
|
port: PORT.clone(),
|
|
}],
|
|
},
|
|
),
|
|
(
|
|
ANOTHER_HOST_ID.clone(),
|
|
HostNetworkConfig {
|
|
switch_ports: vec![SwitchPort {
|
|
interface: ANOTHER_EXISTING_INTERFACE.clone(),
|
|
port: ANOTHER_PORT.clone(),
|
|
}],
|
|
},
|
|
),
|
|
]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn port_not_found_for_mac_address_should_not_configure_interface() {
|
|
let score = given_score(vec![given_host(&HOST_ID, vec![UNKNOWN_INTERFACE.clone()])]);
|
|
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).is_empty();
|
|
}
|
|
|
|
fn given_score(hosts: Vec<PhysicalHost>) -> HostNetworkConfigurationScore {
|
|
HostNetworkConfigurationScore { hosts }
|
|
}
|
|
|
|
fn given_host(id: &Id, network_interfaces: Vec<NetworkInterface>) -> PhysicalHost {
|
|
let network = network_interfaces.iter().map(given_interface).collect();
|
|
|
|
PhysicalHost {
|
|
id: id.clone(),
|
|
category: HostCategory::Server,
|
|
network,
|
|
storage: vec![],
|
|
labels: vec![],
|
|
memory_modules: vec![],
|
|
cpus: vec![],
|
|
}
|
|
}
|
|
|
|
fn given_interface(
|
|
interface: &NetworkInterface,
|
|
) -> harmony_inventory_agent::hwinfo::NetworkInterface {
|
|
harmony_inventory_agent::hwinfo::NetworkInterface {
|
|
name: interface.name.clone(),
|
|
mac_address: interface.mac_address,
|
|
speed_mbps: interface.speed_mbps,
|
|
is_up: true,
|
|
mtu: interface.mtu,
|
|
ipv4_addresses: vec![],
|
|
ipv6_addresses: vec![],
|
|
driver: "driver".into(),
|
|
firmware_version: None,
|
|
}
|
|
}
|
|
|
|
struct TopologyWithSwitch {
|
|
available_ports: Arc<Mutex<Vec<PortLocation>>>,
|
|
configured_host_networks: Arc<Mutex<Vec<(Id, HostNetworkConfig)>>>,
|
|
switch_setup: Arc<Mutex<bool>>,
|
|
}
|
|
|
|
impl TopologyWithSwitch {
|
|
fn new() -> Self {
|
|
Self {
|
|
available_ports: Arc::new(Mutex::new(vec![PORT.clone(), ANOTHER_PORT.clone()])),
|
|
configured_host_networks: Arc::new(Mutex::new(vec![])),
|
|
switch_setup: Arc::new(Mutex::new(false)),
|
|
}
|
|
}
|
|
|
|
fn new_port_not_found() -> Self {
|
|
Self {
|
|
available_ports: Arc::new(Mutex::new(vec![])),
|
|
configured_host_networks: Arc::new(Mutex::new(vec![])),
|
|
switch_setup: Arc::new(Mutex::new(false)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Topology for TopologyWithSwitch {
|
|
fn name(&self) -> &str {
|
|
"SwitchWithPortTopology"
|
|
}
|
|
|
|
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
|
|
Ok(PreparationOutcome::Success { details: "".into() })
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Switch for TopologyWithSwitch {
|
|
async fn setup_switch(&self) -> Result<(), SwitchError> {
|
|
let mut switch_configured = self.switch_setup.lock().unwrap();
|
|
*switch_configured = true;
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_port_for_mac_address(
|
|
&self,
|
|
_mac_address: &MacAddress,
|
|
) -> Result<Option<PortLocation>, SwitchError> {
|
|
let mut ports = self.available_ports.lock().unwrap();
|
|
if ports.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
Ok(Some(ports.remove(0)))
|
|
}
|
|
|
|
async fn configure_host_network(
|
|
&self,
|
|
host: &PhysicalHost,
|
|
config: HostNetworkConfig,
|
|
) -> Result<(), SwitchError> {
|
|
let mut configured_host_networks = self.configured_host_networks.lock().unwrap();
|
|
configured_host_networks.push((host.id.clone(), config.clone()));
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|