diff --git a/Cargo.lock b/Cargo.lock index 2af94a0..1ec5d60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -429,6 +429,15 @@ dependencies = [ "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]] name = "async-broadcast" version = "0.7.2" @@ -2305,6 +2314,7 @@ name = "harmony" version = "0.1.0" dependencies = [ "askama", + "assertor", "async-trait", "base64 0.22.1", "bollard", diff --git a/Cargo.toml b/Cargo.toml index d92c0e7..32231d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,8 @@ members = [ "harmony_composer", "harmony_inventory_agent", "harmony_secret_derive", - "harmony_secret", "adr/agent_discovery/mdns", + "harmony_secret", + "adr/agent_discovery/mdns", ] [workspace.package] @@ -66,5 +67,12 @@ thiserror = "2.0.14" serde = { version = "1.0.209", features = ["derive", "rc"] } serde_json = "1.0.127" askama = "0.14" -sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite" ] } -reqwest = { version = "0.12", features = ["blocking", "stream", "rustls-tls", "http2", "json"], default-features = false } +sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } +reqwest = { version = "0.12", features = [ + "blocking", + "stream", + "rustls-tls", + "http2", + "json", +], default-features = false } +assertor = "0.0.4" diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index ad57db1..f9e671a 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -80,3 +80,4 @@ inquire.workspace = true [dev-dependencies] pretty_assertions.workspace = true +assertor.workspace = true diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index c9f565e..ac53d01 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -7,6 +7,7 @@ use log::info; use crate::data::FileContent; use crate::executors::ExecutorError; +use crate::hardware::PhysicalHost; use crate::topology::PxeOptions; use super::DHCPStaticEntry; @@ -15,6 +16,7 @@ use super::DnsRecord; use super::DnsRecordType; use super::DnsServer; use super::Firewall; +use super::HostNetworkConfig; use super::HttpServer; use super::IpAddress; use super::K8sclient; @@ -24,6 +26,8 @@ use super::LogicalHost; use super::PreparationError; use super::PreparationOutcome; use super::Router; +use super::Switch; +use super::SwitchError; use super::TftpServer; 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 { + todo!() + } + + async fn configure_host_network( + &self, + _host: &PhysicalHost, + _config: HostNetworkConfig, + ) -> Result<(), SwitchError> { + todo!() + } +} + #[derive(Debug)] pub struct DummyInfra; diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index c7ab5cc..a6127c8 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -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 derive_new::new; use harmony_types::net::{IpAddress, MacAddress}; use serde::Serialize; -use crate::executors::ExecutorError; +use crate::{executors::ExecutorError, hardware::PhysicalHost}; 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; + + 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, +} + +#[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)] mod test { use std::sync::Arc; diff --git a/harmony/src/modules/okd/bootstrap_03_control_plane.rs b/harmony/src/modules/okd/bootstrap_03_control_plane.rs index ba9e12d..dd0fdee 100644 --- a/harmony/src/modules/okd/bootstrap_03_control_plane.rs +++ b/harmony/src/modules/okd/bootstrap_03_control_plane.rs @@ -5,11 +5,13 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::{HostRole, Inventory}, modules::{ - dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore, - inventory::DiscoverHostForRoleScore, okd::templates::BootstrapIpxeTpl, + dhcp::DhcpHostBindingScore, + http::IPxeMacBootFileScore, + inventory::DiscoverHostForRoleScore, + okd::{host_network::HostNetworkConfigurationScore, templates::BootstrapIpxeTpl}, }, score::Score, - topology::{HAClusterTopology, HostBinding}, + topology::{self, HAClusterTopology, HostBinding}, }; use async_trait::async_trait; use derive_new::new; @@ -209,8 +211,23 @@ impl OKDSetup03ControlPlaneInterpret { 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. - async fn persist_network_bond(&self) -> Result<(), InterpretError> { + async fn persist_network_bond( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + hosts: &Vec, + ) -> 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. info!("[ControlPlane] Ensuring persistent bonding via MachineConfig/NNCP"); inquire::Confirm::new( @@ -260,7 +277,8 @@ impl Interpret for OKDSetup03ControlPlaneInterpret { self.reboot_targets(&nodes).await?; // 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 // and for the cluster operators to become available. This would be similar to diff --git a/harmony/src/modules/okd/host_network.rs b/harmony/src/modules/okd/host_network.rs new file mode 100644 index 0000000..60a89e7 --- /dev/null +++ b/harmony/src/modules/okd/host_network.rs @@ -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, +} + +impl Score for HostNetworkConfigurationScore { + fn name(&self) -> String { + "HostNetworkConfigurationScore".into() + } + + fn create_interpret(&self) -> Box> { + Box::new(HostNetworkConfigurationInterpret { + score: self.clone(), + }) + } +} + +#[derive(Debug)] +pub struct HostNetworkConfigurationInterpret { + score: HostNetworkConfigurationScore, +} + +#[async_trait] +impl Interpret 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 { + vec![] + } + + async fn execute( + &self, + _inventory: &Inventory, + topology: &T, + ) -> Result { + 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) -> HostNetworkConfigurationScore { + HostNetworkConfigurationScore { hosts } + } + + struct SwitchWithPortTopology { + configured_host_networks: Arc>>, + } + + 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 { + Ok(PreparationOutcome::Success { details: "".into() }) + } + } + + #[async_trait] + impl Switch for SwitchWithPortTopology { + async fn get_port_for_mac_address(&self, mac_address: &MacAddress) -> Option { + 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(()) + } + } +} diff --git a/harmony/src/modules/okd/mod.rs b/harmony/src/modules/okd/mod.rs index 1bd4514..1d052d5 100644 --- a/harmony/src/modules/okd/mod.rs +++ b/harmony/src/modules/okd/mod.rs @@ -19,3 +19,4 @@ pub use bootstrap_03_control_plane::*; pub use bootstrap_04_workers::*; pub use bootstrap_05_sanity_check::*; pub use bootstrap_06_installation_report::*; +pub mod host_network;