feat(switch): configure host network and switch network

This commit is contained in:
Ian Letourneau 2025-09-15 17:07:50 -04:00 committed by Ian Letourneau
parent c84b2413ed
commit 61b02e7a28
8 changed files with 313 additions and 10 deletions

10
Cargo.lock generated
View File

@ -429,6 +429,15 @@ dependencies = [
"wait-timeout", "wait-timeout",
] ]
[[package]]
name = "assertor"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ff24d87260733dc86d38a11c60d9400ce4a74a05d0dafa2a6f5ab249cd857cb"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.7.2" version = "0.7.2"
@ -2305,6 +2314,7 @@ name = "harmony"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"askama", "askama",
"assertor",
"async-trait", "async-trait",
"base64 0.22.1", "base64 0.22.1",
"bollard", "bollard",

View File

@ -14,7 +14,8 @@ members = [
"harmony_composer", "harmony_composer",
"harmony_inventory_agent", "harmony_inventory_agent",
"harmony_secret_derive", "harmony_secret_derive",
"harmony_secret", "adr/agent_discovery/mdns", "harmony_secret",
"adr/agent_discovery/mdns",
] ]
[workspace.package] [workspace.package]
@ -67,4 +68,11 @@ serde = { version = "1.0.209", features = ["derive", "rc"] }
serde_json = "1.0.127" serde_json = "1.0.127"
askama = "0.14" askama = "0.14"
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
reqwest = { version = "0.12", features = ["blocking", "stream", "rustls-tls", "http2", "json"], default-features = false } reqwest = { version = "0.12", features = [
"blocking",
"stream",
"rustls-tls",
"http2",
"json",
], default-features = false }
assertor = "0.0.4"

View File

@ -80,3 +80,4 @@ inquire.workspace = true
[dev-dependencies] [dev-dependencies]
pretty_assertions.workspace = true pretty_assertions.workspace = true
assertor.workspace = true

View File

@ -7,6 +7,7 @@ use log::info;
use crate::data::FileContent; use crate::data::FileContent;
use crate::executors::ExecutorError; use crate::executors::ExecutorError;
use crate::hardware::PhysicalHost;
use crate::topology::PxeOptions; use crate::topology::PxeOptions;
use super::DHCPStaticEntry; use super::DHCPStaticEntry;
@ -15,6 +16,7 @@ use super::DnsRecord;
use super::DnsRecordType; use super::DnsRecordType;
use super::DnsServer; use super::DnsServer;
use super::Firewall; use super::Firewall;
use super::HostNetworkConfig;
use super::HttpServer; use super::HttpServer;
use super::IpAddress; use super::IpAddress;
use super::K8sclient; use super::K8sclient;
@ -24,6 +26,8 @@ use super::LogicalHost;
use super::PreparationError; use super::PreparationError;
use super::PreparationOutcome; use super::PreparationOutcome;
use super::Router; use super::Router;
use super::Switch;
use super::SwitchError;
use super::TftpServer; use super::TftpServer;
use super::Topology; use super::Topology;
@ -263,6 +267,21 @@ impl HttpServer for HAClusterTopology {
} }
} }
#[async_trait]
impl Switch for HAClusterTopology {
async fn get_port_for_mac_address(&self, mac_address: &MacAddress) -> Option<String> {
todo!()
}
async fn configure_host_network(
&self,
_host: &PhysicalHost,
_config: HostNetworkConfig,
) -> Result<(), SwitchError> {
todo!()
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct DummyInfra; pub struct DummyInfra;

View File

@ -1,10 +1,11 @@
use std::{net::Ipv4Addr, str::FromStr, sync::Arc}; use std::{error::Error, net::Ipv4Addr, str::FromStr, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new;
use harmony_types::net::{IpAddress, MacAddress}; use harmony_types::net::{IpAddress, MacAddress};
use serde::Serialize; use serde::Serialize;
use crate::executors::ExecutorError; use crate::{executors::ExecutorError, hardware::PhysicalHost};
use super::{LogicalHost, k8s::K8sClient}; use super::{LogicalHost, k8s::K8sClient};
@ -172,6 +173,46 @@ impl FromStr for DnsRecordType {
} }
} }
#[async_trait]
pub trait Switch: Send + Sync {
async fn get_port_for_mac_address(&self, mac_address: &MacAddress) -> Option<String>;
async fn configure_host_network(
&self,
_host: &PhysicalHost,
_config: HostNetworkConfig,
) -> Result<(), SwitchError>;
}
#[derive(Clone, Debug, PartialEq)]
pub struct HostNetworkConfig {
pub bond: Bond,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Bond {
pub interfaces: Vec<SlaveInterface>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SlaveInterface {
pub mac_address: MacAddress,
// FIXME: Should we add speed as well? And other params
}
#[derive(Debug, Clone, new)]
pub struct SwitchError {
msg: String,
}
impl std::fmt::Display for SwitchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.msg)
}
}
impl Error for SwitchError {}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::sync::Arc; use std::sync::Arc;

View File

@ -5,11 +5,13 @@ use crate::{
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::{HostRole, Inventory}, inventory::{HostRole, Inventory},
modules::{ modules::{
dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore, dhcp::DhcpHostBindingScore,
inventory::DiscoverHostForRoleScore, okd::templates::BootstrapIpxeTpl, http::IPxeMacBootFileScore,
inventory::DiscoverHostForRoleScore,
okd::{host_network::HostNetworkConfigurationScore, templates::BootstrapIpxeTpl},
}, },
score::Score, score::Score,
topology::{HAClusterTopology, HostBinding}, topology::{self, HAClusterTopology, HostBinding},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
@ -209,8 +211,23 @@ impl OKDSetup03ControlPlaneInterpret {
Ok(()) Ok(())
} }
// TODO: Apply host network configuration.
// Delegate to a score: HostNetworkConfigurationScore { host: physical_host } qui manipule Switch dans Topology
// Use-case Affilium: remplacement carte reseau, pas juste installation clean
//
/// Placeholder for automating network bonding configuration. /// Placeholder for automating network bonding configuration.
async fn persist_network_bond(&self) -> Result<(), InterpretError> { async fn persist_network_bond(
&self,
inventory: &Inventory,
topology: &HAClusterTopology,
hosts: &Vec<PhysicalHost>,
) -> Result<(), InterpretError> {
let score = HostNetworkConfigurationScore {
hosts: hosts.clone(), // FIXME: Avoid clone if possible
};
score.interpret(inventory, topology);
// Generate MC or NNCP from inventory NIC data; apply via ignition or post-join. // Generate MC or NNCP from inventory NIC data; apply via ignition or post-join.
info!("[ControlPlane] Ensuring persistent bonding via MachineConfig/NNCP"); info!("[ControlPlane] Ensuring persistent bonding via MachineConfig/NNCP");
inquire::Confirm::new( inquire::Confirm::new(
@ -260,7 +277,8 @@ impl Interpret<HAClusterTopology> for OKDSetup03ControlPlaneInterpret {
self.reboot_targets(&nodes).await?; self.reboot_targets(&nodes).await?;
// 5. Placeholder for post-boot network configuration (e.g., bonding). // 5. Placeholder for post-boot network configuration (e.g., bonding).
self.persist_network_bond().await?; self.persist_network_bond(inventory, topology, &nodes)
.await?;
// TODO: Implement a step to wait for the control plane nodes to join the cluster // TODO: Implement a step to wait for the control plane nodes to join the cluster
// and for the cluster operators to become available. This would be similar to // and for the cluster operators to become available. This would be similar to

View File

@ -0,0 +1,205 @@
use async_trait::async_trait;
use harmony_types::{id::Id, net::MacAddress};
use serde::Serialize;
use crate::{
data::Version,
hardware::PhysicalHost,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
score::Score,
topology::{self, Bond, HostNetworkConfig, SlaveInterface, Switch, 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,
}
#[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> {
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,
}],
},
};
let _ = topology
.configure_host_network(host, host_network_config)
.await;
// foreach hosts
// foreach mac addresses
// let port = topology.get_port_for_mac_address(); // si pas de port -> mac address pas connectee
// create port channel for all ports found
// 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
Ok(Outcome::success("".into()))
}
}
struct PortMapping {
port: String,
mac_address: MacAddress,
}
#[cfg(test)]
mod tests {
use assertor::*;
use harmony_inventory_agent::hwinfo::NetworkInterface;
use lazy_static::lazy_static;
use crate::{
hardware::HostCategory,
topology::{
Bond, HostNetworkConfig, PreparationError, PreparationOutcome, SlaveInterface,
SwitchError,
},
};
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 INTERFACE: MacAddress =
MacAddress::try_from("00:11:22:33:44:55".to_string()).unwrap();
}
#[tokio::test]
async fn one_host_one_mac_address_should_create_bond_with_one_interface() {
let host = given_host(&HOST_ID, *INTERFACE);
let score = given_score(vec![host]);
let topology = SwitchWithPortTopology::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: *INTERFACE,
}],
},
},
)]);
}
fn given_host(id: &Id, mac_address: MacAddress) -> PhysicalHost {
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,
}],
storage: vec![],
labels: vec![],
memory_modules: vec![],
cpus: vec![],
}
}
fn given_score(hosts: Vec<PhysicalHost>) -> HostNetworkConfigurationScore {
HostNetworkConfigurationScore { hosts }
}
struct SwitchWithPortTopology {
configured_host_networks: Arc<Mutex<Vec<(Id, HostNetworkConfig)>>>,
}
impl SwitchWithPortTopology {
fn new() -> Self {
Self {
configured_host_networks: Arc::new(Mutex::new(vec![])),
}
}
}
#[async_trait]
impl Topology for SwitchWithPortTopology {
fn name(&self) -> &str {
"SwitchWithPortTopology"
}
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
Ok(PreparationOutcome::Success { details: "".into() })
}
}
#[async_trait]
impl Switch for SwitchWithPortTopology {
async fn get_port_for_mac_address(&self, mac_address: &MacAddress) -> Option<String> {
Some("1/0/42".into())
}
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(())
}
}
}

View File

@ -19,3 +19,4 @@ pub use bootstrap_03_control_plane::*;
pub use bootstrap_04_workers::*; pub use bootstrap_04_workers::*;
pub use bootstrap_05_sanity_check::*; pub use bootstrap_05_sanity_check::*;
pub use bootstrap_06_installation_report::*; pub use bootstrap_06_installation_report::*;
pub mod host_network;