diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 88045ae..f7cab57 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -13,7 +13,7 @@ use kube::{ use log::debug; use log::info; -use crate::modules::okd::crd::nmstate::{self, NodeNetworkConfigurationPolicy, NodeNetworkState}; +use crate::modules::okd::crd::nmstate::{self, NodeNetworkConfigurationPolicy}; use crate::topology::PxeOptions; use crate::{data::FileContent, modules::okd::crd::nmstate::NMState}; use crate::{ @@ -27,7 +27,7 @@ use super::{ Topology, k8s::K8sClient, }; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; use std::sync::Arc; #[derive(Debug, Clone)] @@ -157,21 +157,6 @@ impl HAClusterTopology { Ok(()) } - async fn get_next_bond_id(&self, hostname: &str) -> Result { - let network_state: Option = self - .k8s_client() - .await - .unwrap() - .get_resource(hostname, None) - .await - .map_err(|e| format!("Failed to list nodes: {e}"))?; - - println!("HELLLOOOO NETWORK STATE: {network_state:#?}"); - - let bond_id = 42; // FIXME: Find a better way to declare the bond id - Ok(format!("bond{bond_id}")) - } - async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> { self.ensure_nmstate_operator_installed() .await @@ -228,7 +213,7 @@ impl HAClusterTopology { interfaces.push(nmstate::Interface { name: interface_name.clone(), description: Some(format!("Member of bond {bond_name}")), - r#type: "ethernet".to_string(), + r#type: nmstate::InterfaceType::Ethernet, state: "up".to_string(), mtu: Some(switch_port.interface.mtu), mac_address: Some(switch_port.interface.mac_address.to_string()), @@ -258,7 +243,7 @@ impl HAClusterTopology { interfaces.push(nmstate::Interface { name: bond_name.to_string(), description: Some(format!("Network bond for host {host}")), - r#type: "bond".to_string(), + r#type: nmstate::InterfaceType::Bond, state: "up".to_string(), copy_mac_from, ipv4: Some(nmstate::IpStackSpec { @@ -325,6 +310,37 @@ impl HAClusterTopology { .cloned() } + async fn get_next_bond_id(&self, hostname: &str) -> Result { + let network_state: Option = self + .k8s_client() + .await + .unwrap() + .get_resource(hostname, None) + .await + .map_err(|e| format!("Failed to list nodes: {e}"))?; + + let interfaces = vec![]; + let existing_bonds: Vec<&nmstate::Interface> = network_state + .as_ref() + .and_then(|network_state| network_state.status.current_state.as_ref()) + .map_or(&interfaces, |current_state| ¤t_state.interfaces) + .iter() + .filter(|i| i.r#type == nmstate::InterfaceType::Bond && i.link_aggregation.is_some()) + .collect(); + + let used_ids: HashSet = existing_bonds + .iter() + .filter_map(|i| { + i.name + .strip_prefix("bond") + .and_then(|id| id.parse::().ok()) + }) + .collect(); + + let next_id = (0..).find(|id| !used_ids.contains(id)).unwrap(); + Ok(format!("bond{next_id}")) + } + async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> { debug!("Configuring port channel: {config:#?}"); let switch_ports = config.switch_ports.iter().map(|s| s.port.clone()).collect(); diff --git a/harmony/src/modules/okd/crd/nmstate.rs b/harmony/src/modules/okd/crd/nmstate.rs index 3c4955e..f0eb4ae 100644 --- a/harmony/src/modules/okd/crd/nmstate.rs +++ b/harmony/src/modules/okd/crd/nmstate.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; -use kube::CustomResource; +use k8s_openapi::{ClusterResourceScope, Resource}; +use kube::{CustomResource, api::ObjectMeta}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -47,7 +48,7 @@ pub struct ProbeDns { group = "nmstate.io", version = "v1", kind = "NodeNetworkConfigurationPolicy", - namespaced + namespaced = false )] #[serde(rename_all = "camelCase")] pub struct NodeNetworkConfigurationPolicySpec { @@ -56,18 +57,38 @@ pub struct NodeNetworkConfigurationPolicySpec { pub desired_state: NetworkState, } -#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] -#[kube( - group = "nmstate.io", - version = "v1beta1", - kind = "NodeNetworkState", - plural = "nodenetworkstates", - namespaced = false, - status = "NodeNetworkStateStatus" -)] +// Currently, kube-rs derive doesn't support resources without a `spec` field, so we have +// to implement it ourselves. +// +// Ref: +// - https://github.com/kube-rs/kube/issues/1763 +// - https://github.com/kube-rs/kube/discussions/1762 +#[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] -pub struct NodeNetworkStateSpec { - // This resource is read-only and has no spec. +pub struct NodeNetworkState { + metadata: ObjectMeta, + pub status: NodeNetworkStateStatus, +} + +impl Resource for NodeNetworkState { + const API_VERSION: &'static str = "nmstate.io/v1beta1"; + const GROUP: &'static str = "nmstate.io"; + const VERSION: &'static str = "v1beta1"; + const KIND: &'static str = "NodeNetworkState"; + const URL_PATH_SEGMENT: &'static str = "nodenetworkstates"; + type Scope = ClusterResourceScope; +} + +impl k8s_openapi::Metadata for NodeNetworkState { + type Ty = ObjectMeta; + + fn metadata(&self) -> &Self::Ty { + &self.metadata + } + + fn metadata_mut(&mut self) -> &mut Self::Ty { + &mut self.metadata + } } #[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] @@ -243,7 +264,7 @@ pub struct Interface { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, - pub r#type: String, + pub r#type: InterfaceType, pub state: String, #[serde(skip_serializing_if = "Option::is_none")] pub mac_address: Option, @@ -273,14 +294,11 @@ pub struct Interface { pub infiniband: Option, #[serde(skip_serializing_if = "Option::is_none")] pub linux_bridge: Option, - #[serde(skip_serializing_if = "Option::is_none")] #[serde(alias = "bridge")] pub ovs_bridge: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub ethtool: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub accept_all_mac_addresses: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -307,6 +325,53 @@ pub struct Interface { pub patch: Option, } +#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum InterfaceType { + #[serde(rename = "unknown")] + Unknown, + #[serde(rename = "dummy")] + Dummy, + #[serde(rename = "loopback")] + Loopback, + #[serde(rename = "linux-bridge")] + LinuxBridge, + #[serde(rename = "ovs-bridge")] + OvsBridge, + #[serde(rename = "ovs-interface")] + OvsInterface, + #[serde(rename = "bond")] + Bond, + #[serde(rename = "ipvlan")] + IpVlan, + #[serde(rename = "vlan")] + Vlan, + #[serde(rename = "vxlan")] + Vxlan, + #[serde(rename = "mac-vlan")] + Macvlan, + #[serde(rename = "mac-vtap")] + Macvtap, + #[serde(rename = "ethernet")] + Ethernet, + #[serde(rename = "infiniband")] + Infiniband, + #[serde(rename = "vrf")] + Vrf, + #[serde(rename = "veth")] + Veth, + #[serde(rename = "ipsec")] + Ipsec, + #[serde(rename = "hsr")] + Hrs, +} + +impl Default for InterfaceType { + fn default() -> Self { + Self::Loopback + } +} + #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] #[serde(rename_all = "kebab-case")] pub struct IpStackSpec {