WIP: configure-switch #159

Closed
johnride wants to merge 18 commits from configure-switch into master
8 changed files with 313 additions and 10 deletions
Showing only changes of commit 61b02e7a28 - Show all commits

10
Cargo.lock generated
View File

@ -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",

View File

@ -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]
@ -67,4 +68,11 @@ 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 }
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
letian marked this conversation as resolved Outdated

Je ne met pas la version dans les dependances locales habituellement

Je ne met pas la version dans les dependances locales habituellement
[dev-dependencies]
pretty_assertions.workspace = true
assertor.workspace = true

View File

@ -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<String> {
todo!()
}
async fn configure_host_network(
&self,
_host: &PhysicalHost,
_config: HostNetworkConfig,
) -> Result<(), SwitchError> {
todo!()
}
}
#[derive(Debug)]
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 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<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>,
}
letian marked this conversation as resolved
Review

speed yes, maybe we will need something else but nothing comes to mind right now.

speed yes, maybe we will need something else but nothing comes to mind right now.
#[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;

View File

@ -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.
Review

The clone has been bugging me too. We could use an Rc or Arc too as a balance between usability and performance.

The clone has been bugging me too. We could use an Rc or Arc too as a balance between usability and performance.
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.
info!("[ControlPlane] Ensuring persistent bonding via MachineConfig/NNCP");
inquire::Confirm::new(
@ -260,7 +277,8 @@ impl Interpret<HAClusterTopology> 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

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_05_sanity_check::*;
pub use bootstrap_06_installation_report::*;
pub mod host_network;