diff --git a/Cargo.lock b/Cargo.lock index ffbd2c9..7e74492 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -680,6 +680,7 @@ version = "0.1.0" dependencies = [ "async-trait", "harmony_types", + "log", "russh", "russh-keys", "tokio", diff --git a/brocade/Cargo.toml b/brocade/Cargo.toml index f80d1ac..3112918 100644 --- a/brocade/Cargo.toml +++ b/brocade/Cargo.toml @@ -7,7 +7,8 @@ license.workspace = true [dependencies] async-trait.workspace = true -harmony_types = { version = "0.1.0", path = "../harmony_types" } +harmony_types = { path = "../harmony_types" } russh.workspace = true russh-keys.workspace = true tokio.workspace = true +log.workspace = true diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index 86b75b8..49cfaae 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -5,6 +5,7 @@ use std::{ use async_trait::async_trait; use harmony_types::net::{IpAddress, MacAddress}; +use log::{debug, info}; use russh::client::{Handle, Handler}; use russh_keys::key; use std::str::FromStr; @@ -21,7 +22,12 @@ pub struct BrocadeClient { } impl BrocadeClient { - pub async fn init(ip: IpAddress, username: &str, password: &str) -> Result { + pub async fn init( + ip_addresses: &[IpAddress], + username: &str, + password: &str, + ) -> Result { + let ip = ip_addresses[0]; let config = russh::client::Config::default(); let mut client = russh::client::connect(Arc::new(config), (ip, 22), Client {}).await?; @@ -64,6 +70,8 @@ impl BrocadeClient { } pub async fn configure_port_channel(&self, ports: &[String]) -> Result { + info!("[Brocade] Configuring port-channel with ports: {ports:?}"); + let channel_id = self.find_available_channel_id().await?; let mut commands = Vec::new(); @@ -93,7 +101,8 @@ impl BrocadeClient { } pub async fn find_available_channel_id(&self) -> Result { - // FIXME: The command might vary slightly by Brocade OS version. + debug!("[Brocade] Finding next available channel id..."); + let output = self.run_command("show port-channel summary").await?; let mut used_ids = Vec::new(); @@ -121,10 +130,13 @@ impl BrocadeClient { } } + debug!("[Brocade] Found channel id '{next_id}'"); Ok(next_id) } async fn run_command(&self, command: &str) -> Result { + debug!("[Brocade] Running command: '{command}'..."); + let mut channel = self.client.channel_open_session().await?; let mut output = Vec::new(); @@ -142,9 +154,9 @@ impl BrocadeClient { } russh::ChannelMsg::ExitStatus { exit_status } => { if exit_status != 0 { + let output_str = String::from_utf8(output).unwrap_or_default(); return Err(Error::CommandError(format!( - "Command failed with exit status {exit_status}, output {}", - String::from_utf8(output).unwrap_or_default() + "Command failed with exit status {exit_status}, output {output_str}", ))); } } @@ -170,6 +182,8 @@ impl BrocadeClient { // Execute commands sequentially and check for errors immediately. for command in commands { + debug!("[Brocade] Running command: '{command}'..."); + let mut output = Vec::new(); channel.exec(true, command.as_str()).await?; @@ -187,7 +201,7 @@ impl BrocadeClient { if exit_status != 0 { let output_str = String::from_utf8(output).unwrap_or_default(); return Err(Error::CommandError(format!( - "Command '{command}' failed with exit status {exit_status}: {output_str}", + "Command failed with exit status {exit_status}: {output_str}", ))); } } diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index f49210a..3633983 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -77,7 +77,7 @@ harmony_secret = { path = "../harmony_secret" } askama.workspace = true sqlx.workspace = true inquire.workspace = true -brocade = { version = "0.1.0", path = "../brocade" } +brocade = { path = "../brocade" } [dev-dependencies] pretty_assertions.workspace = true diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 42cd7c5..cc07b8b 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -5,16 +5,25 @@ use harmony_secret::Secret; use harmony_secret::SecretManager; use harmony_types::net::MacAddress; use harmony_types::net::Url; +use k8s_openapi::api::core::v1::Namespace; +use kube::api::ObjectMeta; use log::debug; use log::info; -use russh::client; -use russh::client::Handler; use serde::Deserialize; use serde::Serialize; use crate::data::FileContent; use crate::executors::ExecutorError; use crate::hardware::PhysicalHost; +use crate::modules::okd::crd::InstallPlanApproval; +use crate::modules::okd::crd::OperatorGroup; +use crate::modules::okd::crd::OperatorGroupSpec; +use crate::modules::okd::crd::Subscription; +use crate::modules::okd::crd::SubscriptionSpec; +use crate::modules::okd::crd::nmstate; +use crate::modules::okd::crd::nmstate::NMState; +use crate::modules::okd::crd::nmstate::NodeNetworkConfigurationPolicy; +use crate::modules::okd::crd::nmstate::NodeNetworkConfigurationPolicySpec; use crate::topology::PxeOptions; use super::DHCPStaticEntry; @@ -35,12 +44,12 @@ use super::PreparationOutcome; use super::Router; use super::Switch; use super::SwitchError; -use super::SwitchPort; use super::TftpServer; use super::Topology; use super::k8s::K8sClient; -use std::error::Error; +use std::collections::BTreeMap; +use std::net::IpAddr; use std::sync::Arc; #[derive(Debug, Clone)] @@ -102,37 +111,224 @@ impl HAClusterTopology { .to_string() } - fn find_master_switch(&self) -> Option { - self.switch.first().cloned() // FIXME: Should we be smarter to find the master switch? + async fn ensure_nmstate_operator_installed(&self) -> Result<(), String> { + // FIXME: Find a way to check nmstate is already available (get pod -n openshift-nmstate) + debug!("Installing NMState operator..."); + let k8s_client = self.k8s_client().await?; + + let nmstate_namespace = Namespace { + metadata: ObjectMeta { + name: Some("openshift-nmstate".to_string()), + finalizers: Some(vec!["kubernetes".to_string()]), + ..Default::default() + }, + ..Default::default() + }; + debug!("Creating NMState namespace: {nmstate_namespace:#?}"); + k8s_client + .apply(&nmstate_namespace, None) + .await + .map_err(|e| e.to_string())?; + + let nmstate_operator_group = OperatorGroup { + metadata: ObjectMeta { + name: Some("openshift-nmstate".to_string()), + namespace: Some("openshift-nmstate".to_string()), + ..Default::default() + }, + spec: OperatorGroupSpec { + target_namespaces: vec!["openshift-nmstate".to_string()], + }, + }; + debug!("Creating NMState operator group: {nmstate_operator_group:#?}"); + k8s_client + .apply(&nmstate_operator_group, None) + .await + .map_err(|e| e.to_string())?; + + let nmstate_subscription = Subscription { + metadata: ObjectMeta { + name: Some("kubernetes-nmstate-operator".to_string()), + namespace: Some("openshift-nmstate".to_string()), + ..Default::default() + }, + spec: SubscriptionSpec { + channel: Some("stable".to_string()), + install_plan_approval: Some(InstallPlanApproval::Automatic), + name: "kubernetes-nmstate-operator".to_string(), + source: "redhat-operators".to_string(), + source_namespace: "openshift-marketplace".to_string(), + }, + }; + debug!("Subscribing to NMState Operator: {nmstate_subscription:#?}"); + k8s_client + .apply(&nmstate_subscription, None) + .await + .map_err(|e| e.to_string())?; + + let nmstate = NMState { + metadata: ObjectMeta { + name: Some("nmstate".to_string()), + ..Default::default() + }, + ..Default::default() + }; + debug!("Creating NMState: {nmstate:#?}"); + k8s_client + .apply(&nmstate, None) + .await + .map_err(|e| e.to_string())?; + + Ok(()) } - async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> { + fn get_next_bond_id(&self) -> u8 { + 42 // FIXME: Find a better way to declare the bond id + } + + async fn configure_bond( + &self, + host: &PhysicalHost, + config: &HostNetworkConfig, + ) -> Result<(), SwitchError> { + self.ensure_nmstate_operator_installed() + .await + .map_err(|e| { + SwitchError::new(format!( + "Can't configure bond, NMState operator not available: {e}" + )) + })?; + + let bond_config = self.create_bond_configuration(host, config); + debug!("Configuring bond for host {host:?}: {bond_config:#?}"); + self.k8s_client() + .await + .unwrap() + .apply(&bond_config, None) + .await + .unwrap(); + todo!() } - async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result { + fn create_bond_configuration( + &self, + host: &PhysicalHost, + config: &HostNetworkConfig, + ) -> NodeNetworkConfigurationPolicy { + let host_name = host.id.clone(); + + let bond_id = self.get_next_bond_id(); + let bond_name = format!("bond{bond_id}"); + let mut bond_mtu: Option = None; + let mut bond_mac_address: Option = None; + let mut bond_ports = Vec::new(); + let mut interfaces: Vec = Vec::new(); + + for switch_port in &config.switch_ports { + let interface_name = switch_port.interface.name.clone(); + + interfaces.push(nmstate::InterfaceSpec { + name: interface_name.clone(), + description: Some(format!("Member of bond {bond_name}")), + r#type: "ethernet".to_string(), + state: "up".to_string(), + mtu: Some(switch_port.interface.mtu), + mac_address: Some(switch_port.interface.mac_address.to_string()), + ipv4: Some(nmstate::IpStackSpec { + enabled: Some(false), + ..Default::default() + }), + ipv6: Some(nmstate::IpStackSpec { + enabled: Some(false), + ..Default::default() + }), + link_aggregation: None, + ..Default::default() + }); + + bond_ports.push(interface_name); + + // Use the first port's details for the bond mtu and mac address + if bond_mtu.is_none() { + bond_mtu = Some(switch_port.interface.mtu); + } + if bond_mac_address.is_none() { + bond_mac_address = Some(switch_port.interface.mac_address.to_string()); + } + } + + interfaces.push(nmstate::InterfaceSpec { + name: bond_name.clone(), + description: Some(format!("Network bond for host {host_name}")), + r#type: "bond".to_string(), + state: "up".to_string(), + mtu: bond_mtu, + mac_address: bond_mac_address, + ipv4: Some(nmstate::IpStackSpec { + dhcp: Some(true), + enabled: Some(true), + ..Default::default() + }), + ipv6: Some(nmstate::IpStackSpec { + dhcp: Some(true), + autoconf: Some(true), + enabled: Some(true), + ..Default::default() + }), + link_aggregation: Some(nmstate::BondSpec { + mode: "802.3ad".to_string(), + ports: bond_ports, + ..Default::default() + }), + ..Default::default() + }); + + NodeNetworkConfigurationPolicy { + metadata: ObjectMeta { + name: Some(format!("{host_name}-bond-config")), + ..Default::default() + }, + spec: NodeNetworkConfigurationPolicySpec { + node_selector: Some(BTreeMap::from([( + "kubernetes.io/hostname".to_string(), + host_name.to_string(), + )])), + desired_state: nmstate::DesiredStateSpec { interfaces }, + }, + } + } + + async fn get_switch_client(&self) -> Result, SwitchError> { let auth = SecretManager::get_or_prompt::() .await .map_err(|e| SwitchError::new(format!("Failed to get credentials: {e}")))?; - let switch = self - .find_master_switch() - .ok_or(SwitchError::new("No switch found in topology".to_string()))?; - let client = BrocadeSwitchClient::init(switch.ip, &auth.username, &auth.password) + // FIXME: We assume Brocade switches + let switches: Vec = self.switch.iter().map(|s| s.ip).collect(); + let client = BrocadeSwitchClient::init(&switches, &auth.username, &auth.password) .await .map_err(|e| SwitchError::new(format!("Failed to connect to switch: {e}")))?; + Ok(Box::new(client)) + } + + async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> { + debug!("Configuring port channel: {config:#?}"); + let client = self.get_switch_client().await?; + let switch_ports: Vec = config .switch_ports .iter() .map(|s| s.port_name.clone()) .collect(); - let channel_id = client + + client .configure_port_channel(switch_ports) .await .map_err(|e| SwitchError::new(format!("Failed to configure switch: {e}")))?; - Ok(channel_id) + Ok(()) } pub fn autoload() -> Self { @@ -312,12 +508,7 @@ impl HttpServer for HAClusterTopology { #[async_trait] impl Switch for HAClusterTopology { async fn get_port_for_mac_address(&self, mac_address: &MacAddress) -> Option { - let auth = SecretManager::get_or_prompt::() - .await - .unwrap(); - - let switch = self.find_master_switch()?; - let client = BrocadeSwitchClient::init(switch.ip, &auth.username, &auth.password).await; + let client = self.get_switch_client().await; let Ok(client) = client else { return None; @@ -328,18 +519,16 @@ impl Switch for HAClusterTopology { async fn configure_host_network( &self, - _host: &PhysicalHost, + host: &PhysicalHost, config: HostNetworkConfig, ) -> Result<(), SwitchError> { - let _ = self.configure_bond(&config).await; - let channel_id = self.configure_port_channel(&config).await; - - todo!() + self.configure_bond(host, &config).await?; + self.configure_port_channel(&config).await } } #[async_trait] -trait SwitchClient { +trait SwitchClient: Send + Sync { async fn find_port(&self, mac_address: &MacAddress) -> Option; async fn configure_port_channel(&self, switch_ports: Vec) -> Result; } @@ -349,8 +538,12 @@ struct BrocadeSwitchClient { } impl BrocadeSwitchClient { - async fn init(ip: IpAddress, username: &str, password: &str) -> Result { - let brocade = BrocadeClient::init(ip, username, password).await?; + async fn init( + ip_addresses: &[IpAddress], + username: &str, + password: &str, + ) -> Result { + let brocade = BrocadeClient::init(ip_addresses, username, password).await?; Ok(Self { brocade }) } } @@ -451,8 +644,8 @@ impl DhcpServer for DummyInfra { } async fn set_dhcp_range( &self, - start: &IpAddress, - end: &IpAddress, + _start: &IpAddress, + _end: &IpAddress, ) -> Result<(), ExecutorError> { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 9b42a22..a1da8dd 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -191,9 +191,16 @@ pub struct HostNetworkConfig { #[derive(Clone, Debug, PartialEq)] pub struct SwitchPort { - pub mac_address: MacAddress, + pub interface: NetworkInterface, pub port_name: String, - // FIXME: Should we add speed as well? And other params +} + +#[derive(Clone, Debug, PartialEq)] +pub struct NetworkInterface { + pub name: String, + pub mac_address: MacAddress, + pub speed_mbps: Option, + pub mtu: u32, } #[derive(Debug, Clone, new)] diff --git a/harmony/src/modules/okd/bootstrap_03_control_plane.rs b/harmony/src/modules/okd/bootstrap_03_control_plane.rs index dd0fdee..a3fe4c2 100644 --- a/harmony/src/modules/okd/bootstrap_03_control_plane.rs +++ b/harmony/src/modules/okd/bootstrap_03_control_plane.rs @@ -11,7 +11,7 @@ use crate::{ okd::{host_network::HostNetworkConfigurationScore, templates::BootstrapIpxeTpl}, }, score::Score, - topology::{self, HAClusterTopology, HostBinding}, + topology::{HAClusterTopology, HostBinding}, }; use async_trait::async_trait; use derive_new::new; @@ -30,7 +30,7 @@ pub struct OKDSetup03ControlPlaneScore {} impl Score for OKDSetup03ControlPlaneScore { fn create_interpret(&self) -> Box> { - Box::new(OKDSetup03ControlPlaneInterpret::new(self.clone())) + Box::new(OKDSetup03ControlPlaneInterpret::new()) } fn name(&self) -> String { @@ -40,17 +40,15 @@ impl Score for OKDSetup03ControlPlaneScore { #[derive(Debug, Clone)] pub struct OKDSetup03ControlPlaneInterpret { - score: OKDSetup03ControlPlaneScore, version: Version, status: InterpretStatus, } impl OKDSetup03ControlPlaneInterpret { - pub fn new(score: OKDSetup03ControlPlaneScore) -> Self { + pub fn new() -> Self { let version = Version::from("1.0.0").unwrap(); Self { version, - score, status: InterpretStatus::QUEUED, } } @@ -161,7 +159,7 @@ impl OKDSetup03ControlPlaneInterpret { } .to_string(); - debug!("[ControlPlane] iPXE content template:\n{}", content); + debug!("[ControlPlane] iPXE content template:\n{content}"); // Create and apply an iPXE boot file for each node. for node in nodes { @@ -191,16 +189,13 @@ impl OKDSetup03ControlPlaneInterpret { /// Prompts the user to reboot the target control plane nodes. async fn reboot_targets(&self, nodes: &Vec) -> Result<(), InterpretError> { let node_ids: Vec = nodes.iter().map(|n| n.id.to_string()).collect(); - info!( - "[ControlPlane] Requesting reboot for control plane nodes: {:?}", - node_ids - ); + info!("[ControlPlane] Requesting reboot for control plane nodes: {node_ids:?}",); let confirmation = inquire::Confirm::new( &format!("Please reboot the {} control plane nodes ({}) to apply their PXE configuration. Press enter when ready.", nodes.len(), node_ids.join(", ")), ) .prompt() - .map_err(|e| InterpretError::new(format!("User prompt failed: {}", e)))?; + .map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?; if !confirmation { return Err(InterpretError::new( @@ -222,19 +217,18 @@ impl OKDSetup03ControlPlaneInterpret { topology: &HAClusterTopology, hosts: &Vec, ) -> Result<(), InterpretError> { + // Generate MC or NNCP from inventory NIC data; apply via ignition or post-join. + info!("[ControlPlane] Ensuring persistent bonding via MachineConfig/NNCP"); let score = HostNetworkConfigurationScore { hosts: hosts.clone(), // FIXME: Avoid clone if possible }; + score.interpret(inventory, topology).await?; - 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( "Network configuration for control plane nodes is not automated yet. Configure it manually if needed.", ) .prompt() - .map_err(|e| InterpretError::new(format!("User prompt failed: {}", e)))?; + .map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?; Ok(()) } diff --git a/harmony/src/modules/okd/crd/machineconfig.rs b/harmony/src/modules/okd/crd/machineconfig.rs new file mode 100644 index 0000000..df710e2 --- /dev/null +++ b/harmony/src/modules/okd/crd/machineconfig.rs @@ -0,0 +1,43 @@ +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[kube( + group = "machineconfiguration.openshift.io", + version = "v1", + kind = "MachineConfig", + namespaced +)] +#[serde(rename_all = "camelCase")] +pub struct MachineConfigSpec { + pub config: Config, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct Config { + pub ignition: Ignition, + pub storage: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct Ignition { + pub version: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct Storage { + pub files: Vec, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct File { + pub path: String, + pub contents: FileContents, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct FileContents { + pub source: String, +} diff --git a/harmony/src/modules/okd/crd/mod.rs b/harmony/src/modules/okd/crd/mod.rs new file mode 100644 index 0000000..aa35190 --- /dev/null +++ b/harmony/src/modules/okd/crd/mod.rs @@ -0,0 +1,42 @@ +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +pub mod machineconfig; +pub mod nmstate; + +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[kube( + group = "operators.coreos.com", + version = "v1", + kind = "OperatorGroup", + namespaced +)] +#[serde(rename_all = "camelCase")] +pub struct OperatorGroupSpec { + pub target_namespaces: Vec, +} + +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[kube( + group = "operators.coreos.com", + version = "v1alpha1", + kind = "Subscription", + namespaced +)] +#[serde(rename_all = "camelCase")] +pub struct SubscriptionSpec { + pub name: String, + pub source: String, + pub source_namespace: String, + pub channel: Option, + pub install_plan_approval: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub enum InstallPlanApproval { + #[serde(rename = "Automatic")] + Automatic, + #[serde(rename = "Manual")] + Manual, +} diff --git a/harmony/src/modules/okd/crd/nmstate.rs b/harmony/src/modules/okd/crd/nmstate.rs new file mode 100644 index 0000000..b6117fd --- /dev/null +++ b/harmony/src/modules/okd/crd/nmstate.rs @@ -0,0 +1,251 @@ +use std::collections::BTreeMap; + +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[kube(group = "nmstate.io", version = "v1", kind = "NMState", namespaced)] +#[serde(rename_all = "camelCase")] +pub struct NMStateSpec { + pub probe_configuration: Option, +} + +impl Default for NMState { + fn default() -> Self { + Self { + metadata: Default::default(), + spec: NMStateSpec { + probe_configuration: None, + }, + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ProbeConfig { + pub dns: ProbeDns, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ProbeDns { + pub host: String, +} + +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[kube( + group = "nmstate.io", + version = "v1", + kind = "NodeNetworkConfigurationPolicy", + namespaced +)] +#[serde(rename_all = "camelCase")] +pub struct NodeNetworkConfigurationPolicySpec { + pub node_selector: Option>, + pub desired_state: DesiredStateSpec, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct DesiredStateSpec { + pub interfaces: Vec, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct InterfaceSpec { + pub name: String, + pub description: Option, + pub r#type: String, + pub state: String, + pub mac_address: Option, + pub mtu: Option, + pub controller: Option, + pub ipv4: Option, + pub ipv6: Option, + pub ethernet: Option, + pub link_aggregation: Option, + pub vlan: Option, + pub vxlan: Option, + pub mac_vtap: Option, + pub mac_vlan: Option, + pub infiniband: Option, + pub linux_bridge: Option, + pub ovs_bridge: Option, + pub ethtool: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct IpStackSpec { + pub enabled: Option, + pub dhcp: Option, + pub autoconf: Option, + pub address: Option>, + pub auto_dns: Option, + pub auto_gateway: Option, + pub auto_routes: Option, + pub dhcp_client_id: Option, + pub dhcp_duid: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct IpAddressSpec { + pub ip: String, + pub prefix_length: u8, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct EthernetSpec { + pub speed: Option, + pub duplex: Option, + pub auto_negotiation: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct BondSpec { + pub mode: String, + pub ports: Vec, + pub options: Option>, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct VlanSpec { + pub base_iface: String, + pub id: u16, + pub protocol: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct VxlanSpec { + pub base_iface: String, + pub id: u32, + pub remote: String, + pub local: Option, + pub learning: Option, + pub destination_port: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct MacVtapSpec { + pub base_iface: String, + pub mode: String, + pub promiscuous: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct MacVlanSpec { + pub base_iface: String, + pub mode: String, + pub promiscuous: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct InfinibandSpec { + pub base_iface: String, + pub pkey: String, + pub mode: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct LinuxBridgeSpec { + pub options: Option, + pub ports: Option>, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct LinuxBridgeOptions { + pub mac_ageing_time: Option, + pub multicast_snooping: Option, + pub stp: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct StpOptions { + pub enabled: Option, + pub forward_delay: Option, + pub hello_time: Option, + pub max_age: Option, + pub priority: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct LinuxBridgePort { + pub name: String, + pub vlan: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct LinuxBridgePortVlan { + pub mode: Option, + pub trunk_tags: Option>, + pub tag: Option, + pub enable_native: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct VlanTag { + pub id: u16, + pub id_range: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct VlanIdRange { + pub min: u16, + pub max: u16, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct OvsBridgeSpec { + pub options: Option, + pub ports: Option>, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct OvsBridgeOptions { + pub stp: Option, + pub rstp: Option, + pub mcast_snooping_enable: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct OvsPortSpec { + pub name: String, + pub link_aggregation: Option, + pub vlan: Option, + pub r#type: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct EthtoolSpec { + // FIXME: Properly describe this spec +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub struct EthtoolFecSpec { + pub auto: Option, + pub mode: Option, +} diff --git a/harmony/src/modules/okd/host_network.rs b/harmony/src/modules/okd/host_network.rs index 7a68115..f6318c0 100644 --- a/harmony/src/modules/okd/host_network.rs +++ b/harmony/src/modules/okd/host_network.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use harmony_types::{id::Id, net::MacAddress}; +use harmony_types::id::Id; use serde::Serialize; use crate::{ @@ -8,7 +8,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{HostNetworkConfig, Switch, SwitchPort, Topology}, + topology::{HostNetworkConfig, NetworkInterface, Switch, SwitchPort, Topology}, }; #[derive(Debug, Clone, Serialize)] @@ -59,10 +59,17 @@ impl Interpret for HostNetworkConfigurationInterpret { for host in &self.score.hosts { let mut switch_ports = vec![]; - for mac_address in host.get_mac_address() { + for network_interface in &host.network { + let mac_address = network_interface.mac_address; + if let Some(port_name) = topology.get_port_for_mac_address(&mac_address).await { switch_ports.push(SwitchPort { - mac_address, + interface: NetworkInterface { + name: network_interface.name.clone(), + mac_address, + speed_mbps: network_interface.speed_mbps, + mtu: network_interface.mtu, + }, port_name, }); } @@ -73,14 +80,6 @@ impl Interpret for HostNetworkConfigurationInterpret { .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 config - // topology.configure_host_network(host, config) <--- will create both bonds & port channels - Ok(Outcome::success("".into())) } } @@ -88,7 +87,7 @@ impl Interpret for HostNetworkConfigurationInterpret { #[cfg(test)] mod tests { use assertor::*; - use harmony_inventory_agent::hwinfo::NetworkInterface; + use harmony_types::net::MacAddress; use lazy_static::lazy_static; use crate::{ @@ -107,19 +106,31 @@ mod tests { 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: MacAddress = - MacAddress::try_from("AA:BB:CC:DD:EE:F1".to_string()).unwrap(); - pub static ref ANOTHER_EXISTING_INTERFACE: MacAddress = - MacAddress::try_from("AA:BB:CC:DD:EE:F2".to_string()).unwrap(); - pub static ref UNKNOWN_INTERFACE: MacAddress = - MacAddress::try_from("11:22:33:44:55:61".to_string()).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: String = "1/0/42".into(); pub static ref ANOTHER_PORT: String = "2/0/42".into(); } #[tokio::test] async fn host_with_one_mac_address_should_create_bond_with_one_interface() { - let host = given_host(&HOST_ID, vec![*EXISTING_INTERFACE]); + let host = given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]); let score = given_score(vec![host]); let topology = TopologyWithSwitch::new(); @@ -130,7 +141,7 @@ mod tests { HOST_ID.clone(), HostNetworkConfig { switch_ports: vec![SwitchPort { - mac_address: *EXISTING_INTERFACE, + interface: EXISTING_INTERFACE.clone(), port_name: PORT.clone(), }], }, @@ -141,7 +152,10 @@ mod tests { 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, *ANOTHER_EXISTING_INTERFACE], + vec![ + EXISTING_INTERFACE.clone(), + ANOTHER_EXISTING_INTERFACE.clone(), + ], )]); let topology = TopologyWithSwitch::new(); @@ -153,11 +167,11 @@ mod tests { HostNetworkConfig { switch_ports: vec![ SwitchPort { - mac_address: *EXISTING_INTERFACE, + interface: EXISTING_INTERFACE.clone(), port_name: PORT.clone(), }, SwitchPort { - mac_address: *ANOTHER_EXISTING_INTERFACE, + interface: ANOTHER_EXISTING_INTERFACE.clone(), port_name: ANOTHER_PORT.clone(), }, ], @@ -168,8 +182,8 @@ mod tests { #[tokio::test] async fn multiple_hosts_should_create_one_bond_per_host() { let score = given_score(vec![ - given_host(&HOST_ID, vec![*EXISTING_INTERFACE]), - given_host(&ANOTHER_HOST_ID, vec![*ANOTHER_EXISTING_INTERFACE]), + given_host(&HOST_ID, vec![EXISTING_INTERFACE.clone()]), + given_host(&ANOTHER_HOST_ID, vec![ANOTHER_EXISTING_INTERFACE.clone()]), ]); let topology = TopologyWithSwitch::new(); @@ -181,7 +195,7 @@ mod tests { HOST_ID.clone(), HostNetworkConfig { switch_ports: vec![SwitchPort { - mac_address: *EXISTING_INTERFACE, + interface: EXISTING_INTERFACE.clone(), port_name: PORT.clone(), }], }, @@ -190,7 +204,7 @@ mod tests { ANOTHER_HOST_ID.clone(), HostNetworkConfig { switch_ports: vec![SwitchPort { - mac_address: *ANOTHER_EXISTING_INTERFACE, + interface: ANOTHER_EXISTING_INTERFACE.clone(), port_name: ANOTHER_PORT.clone(), }], }, @@ -201,7 +215,7 @@ mod tests { #[tokio::test] async fn port_not_found_for_mac_address_should_not_configure_interface() { // FIXME: Should it still configure an empty bond/port channel? - let score = given_score(vec![given_host(&HOST_ID, vec![*UNKNOWN_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; @@ -219,8 +233,8 @@ mod tests { HostNetworkConfigurationScore { hosts } } - fn given_host(id: &Id, mac_addresses: Vec) -> PhysicalHost { - let network = mac_addresses.iter().map(|m| given_interface(*m)).collect(); + fn given_host(id: &Id, network_interfaces: Vec) -> PhysicalHost { + let network = network_interfaces.iter().map(given_interface).collect(); PhysicalHost { id: id.clone(), @@ -233,13 +247,15 @@ mod tests { } } - fn given_interface(mac_address: MacAddress) -> NetworkInterface { - NetworkInterface { - name: format!("{mac_address}"), - mac_address, - speed_mbps: None, + 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: 1, + mtu: interface.mtu, ipv4_addresses: vec![], ipv6_addresses: vec![], driver: "driver".into(), diff --git a/harmony/src/modules/okd/mod.rs b/harmony/src/modules/okd/mod.rs index 1d052d5..a12f132 100644 --- a/harmony/src/modules/okd/mod.rs +++ b/harmony/src/modules/okd/mod.rs @@ -19,4 +19,5 @@ 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 crd; pub mod host_network; diff --git a/harmony_types/src/net.rs b/harmony_types/src/net.rs index 5b7449a..51de86e 100644 --- a/harmony_types/src/net.rs +++ b/harmony_types/src/net.rs @@ -41,7 +41,7 @@ impl TryFrom for MacAddress { bytes[i] = u8::from_str_radix(part, 16).map_err(|_| { std::io::Error::new( std::io::ErrorKind::InvalidInput, - format!("Invalid hex value in part {}: '{}'", i, part), + format!("Invalid hex value in part {i}: '{part}'"), ) })?; } @@ -106,8 +106,8 @@ impl Serialize for Url { impl std::fmt::Display for Url { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Url::LocalFolder(path) => write!(f, "{}", path), - Url::Url(url) => write!(f, "{}", url), + Url::LocalFolder(path) => write!(f, "{path}"), + Url::Url(url) => write!(f, "{url}"), } } }