diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 7be2725..5e9a567 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -47,6 +47,7 @@ pub struct HAClusterTopology { pub control_plane: Vec, pub workers: Vec, pub switch: Vec, + pub kubeconfig: Option, } #[async_trait] @@ -65,9 +66,17 @@ impl Topology for HAClusterTopology { #[async_trait] impl K8sclient for HAClusterTopology { async fn k8s_client(&self) -> Result, String> { - Ok(Arc::new( - K8sClient::try_default().await.map_err(|e| e.to_string())?, - )) + match &self.kubeconfig { + None => Ok(Arc::new( + K8sClient::try_default().await.map_err(|e| e.to_string())?, + )), + Some(kubeconfig) => { + let Some(client) = K8sClient::from_kubeconfig(&kubeconfig).await else { + return Err("Failed to create k8s client".to_string()); + }; + Ok(Arc::new(client)) + } + } } } @@ -123,7 +132,7 @@ impl HAClusterTopology { }; debug!("Creating NMState operator group: {nmstate_operator_group:#?}"); k8s_client - .apply(&nmstate_operator_group, None) + .apply(&nmstate_operator_group, Some("openshift-nmstate")) .await .map_err(|e| e.to_string())?; @@ -134,29 +143,29 @@ impl HAClusterTopology { ..Default::default() }, spec: SubscriptionSpec { - channel: Some("stable".to_string()), - install_plan_approval: Some(InstallPlanApproval::Automatic), + channel: Some("alpha".to_string()), name: "kubernetes-nmstate-operator".to_string(), - source: "redhat-operators".to_string(), + source: "operatorhubio-catalog".to_string(), source_namespace: "openshift-marketplace".to_string(), }, }; debug!("Subscribing to NMState Operator: {nmstate_subscription:#?}"); k8s_client - .apply(&nmstate_subscription, None) + .apply(&nmstate_subscription, Some("openshift-nmstate")) .await .map_err(|e| e.to_string())?; let nmstate = NMState { metadata: ObjectMeta { name: Some("nmstate".to_string()), + namespace: Some("openshift-nmstate".to_string()), ..Default::default() }, ..Default::default() }; debug!("Creating NMState: {nmstate:#?}"); k8s_client - .apply(&nmstate, None) + .apply(&nmstate, Some("openshift-nmstate")) .await .map_err(|e| e.to_string())?; @@ -187,9 +196,9 @@ impl HAClusterTopology { .unwrap() .apply(&bond_config, None) .await - .unwrap(); + .map_err(|e| SwitchError::new(format!("Failed to configure bond: {e}")))?; - todo!() + Ok(()) } fn create_bond_configuration( @@ -325,6 +334,7 @@ impl HAClusterTopology { }; Self { + kubeconfig: None, domain_name: "DummyTopology".to_string(), router: dummy_infra.clone(), load_balancer: dummy_infra.clone(), diff --git a/harmony/src/infra/kubers/mod.rs b/harmony/src/infra/kubers/mod.rs new file mode 100644 index 0000000..cd40856 --- /dev/null +++ b/harmony/src/infra/kubers/mod.rs @@ -0,0 +1 @@ +pub mod types; diff --git a/harmony/src/infra/kubers/types.rs b/harmony/src/infra/kubers/types.rs new file mode 100644 index 0000000..4e7db49 --- /dev/null +++ b/harmony/src/infra/kubers/types.rs @@ -0,0 +1,27 @@ +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(CustomResource, Default, Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[kube( + group = "operators.coreos.com", + version = "v1alpha1", + kind = "CatalogSource", + namespaced +)] +#[serde(rename_all = "camelCase")] +pub struct CatalogSourceSpec { + pub source_type: String, + pub image: String, + pub display_name: String, + pub publisher: String, +} + +impl Default for CatalogSource { + fn default() -> Self { + Self { + metadata: Default::default(), + spec: Default::default(), + } + } +} diff --git a/harmony/src/infra/mod.rs b/harmony/src/infra/mod.rs index 203cf90..5b38eab 100644 --- a/harmony/src/infra/mod.rs +++ b/harmony/src/infra/mod.rs @@ -3,5 +3,6 @@ pub mod executors; pub mod hp_ilo; pub mod intel_amt; pub mod inventory; +pub mod kubers; pub mod opnsense; mod sqlx; diff --git a/harmony/src/modules/k8s/resource.rs b/harmony/src/modules/k8s/resource.rs index d679326..57f9731 100644 --- a/harmony/src/modules/k8s/resource.rs +++ b/harmony/src/modules/k8s/resource.rs @@ -38,13 +38,15 @@ impl< + 'static + Send + Clone, - T: Topology, + T: Topology + K8sclient, > Score for K8sResourceScore where ::DynamicType: Default, { fn create_interpret(&self) -> Box> { - todo!() + Box::new(K8sResourceInterpret { + score: self.clone(), + }) } fn name(&self) -> String { diff --git a/harmony/src/modules/okd/bootstrap_03_control_plane.rs b/harmony/src/modules/okd/bootstrap_03_control_plane.rs index af8e71f..5abe848 100644 --- a/harmony/src/modules/okd/bootstrap_03_control_plane.rs +++ b/harmony/src/modules/okd/bootstrap_03_control_plane.rs @@ -5,10 +5,8 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::{HostRole, Inventory}, modules::{ - dhcp::DhcpHostBindingScore, - http::IPxeMacBootFileScore, - inventory::DiscoverHostForRoleScore, - okd::{host_network::HostNetworkConfigurationScore, templates::BootstrapIpxeTpl}, + dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore, + inventory::DiscoverHostForRoleScore, okd::templates::BootstrapIpxeTpl, }, score::Score, topology::{HAClusterTopology, HostBinding}, @@ -205,28 +203,6 @@ impl OKDSetup03ControlPlaneInterpret { Ok(()) } - - /// Placeholder for automating network bonding configuration. - async fn persist_network_bond( - &self, - inventory: &Inventory, - topology: &HAClusterTopology, - hosts: &Vec, - ) -> Result<(), InterpretError> { - info!("[ControlPlane] Ensuring persistent bonding"); - let score = HostNetworkConfigurationScore { - hosts: hosts.clone(), - }; - score.interpret(inventory, topology).await?; - - 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}")))?; - - Ok(()) - } } #[async_trait] @@ -265,10 +241,6 @@ impl Interpret for OKDSetup03ControlPlaneInterpret { // 4. Reboot the nodes to start the OS installation. self.reboot_targets(&nodes).await?; - // 5. Placeholder for post-boot network configuration (e.g., bonding). - 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 // the `wait-for bootstrap-complete` command. diff --git a/harmony/src/modules/okd/bootstrap_persist_network_bond.rs b/harmony/src/modules/okd/bootstrap_persist_network_bond.rs new file mode 100644 index 0000000..6c486ea --- /dev/null +++ b/harmony/src/modules/okd/bootstrap_persist_network_bond.rs @@ -0,0 +1,131 @@ +use crate::{ + data::Version, + hardware::PhysicalHost, + infra::inventory::InventoryRepositoryFactory, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::{HostRole, Inventory}, + modules::okd::host_network::HostNetworkConfigurationScore, + score::Score, + topology::HAClusterTopology, +}; +use async_trait::async_trait; +use derive_new::new; +use harmony_types::id::Id; +use log::info; +use serde::Serialize; + +// ------------------------------------------------------------------------------------------------- +// Step XX: Persist Network Bond +// - Persist bonding via NMState +// - Persist port channels on the Switch +// ------------------------------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, new)] +pub struct OKDSetupPersistNetworkBondScore {} + +impl Score for OKDSetupPersistNetworkBondScore { + fn create_interpret(&self) -> Box> { + Box::new(OKDSetupPersistNetworkBondInterpet::new()) + } + + fn name(&self) -> String { + "OKDSetupPersistNetworkBondScore".to_string() + } +} + +#[derive(Debug, Clone)] +pub struct OKDSetupPersistNetworkBondInterpet { + version: Version, + status: InterpretStatus, +} + +impl OKDSetupPersistNetworkBondInterpet { + pub fn new() -> Self { + let version = Version::from("1.0.0").unwrap(); + Self { + version, + status: InterpretStatus::QUEUED, + } + } + + /// Ensures that three physical hosts are discovered and available for the ControlPlane role. + /// It will trigger discovery if not enough hosts are found. + async fn get_nodes( + &self, + _inventory: &Inventory, + _topology: &HAClusterTopology, + ) -> Result, InterpretError> { + const REQUIRED_HOSTS: usize = 3; + let repo = InventoryRepositoryFactory::build().await?; + let control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?; + + if control_plane_hosts.len() < REQUIRED_HOSTS { + Err(InterpretError::new(format!( + "OKD Requires at least {} control plane hosts, but only found {}. Cannot proceed.", + REQUIRED_HOSTS, + control_plane_hosts.len() + ))) + } else { + // Take exactly the number of required hosts to ensure consistency. + Ok(control_plane_hosts + .into_iter() + .take(REQUIRED_HOSTS) + .collect()) + } + } + + async fn persist_network_bond( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + hosts: &Vec, + ) -> Result<(), InterpretError> { + info!("[ControlPlane] Ensuring persistent bonding"); + let score = HostNetworkConfigurationScore { + hosts: hosts.clone(), + }; + score.interpret(inventory, topology).await?; + + 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}")))?; + + Ok(()) + } +} + +#[async_trait] +impl Interpret for OKDSetupPersistNetworkBondInterpet { + fn get_name(&self) -> InterpretName { + InterpretName::Custom("OKDSetup03ControlPlane") + } + + fn get_version(&self) -> Version { + self.version.clone() + } + + fn get_status(&self) -> InterpretStatus { + self.status.clone() + } + + fn get_children(&self) -> Vec { + vec![] + } + + async fn execute( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result { + let nodes = self.get_nodes(inventory, topology).await?; + + self.persist_network_bond(inventory, topology, &nodes) + .await?; + + Ok(Outcome::success( + "Network bond successfully persisted".into(), + )) + } +} diff --git a/harmony/src/modules/okd/crd/mod.rs b/harmony/src/modules/okd/crd/mod.rs index c1a68ce..c6b3416 100644 --- a/harmony/src/modules/okd/crd/mod.rs +++ b/harmony/src/modules/okd/crd/mod.rs @@ -29,7 +29,6 @@ pub struct SubscriptionSpec { pub source: String, pub source_namespace: String, pub channel: Option, - pub install_plan_approval: Option, } #[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] diff --git a/harmony/src/modules/okd/installation.rs b/harmony/src/modules/okd/installation.rs index 72603c8..3deb59a 100644 --- a/harmony/src/modules/okd/installation.rs +++ b/harmony/src/modules/okd/installation.rs @@ -50,7 +50,7 @@ use crate::{ modules::okd::{ OKDSetup01InventoryScore, OKDSetup02BootstrapScore, OKDSetup03ControlPlaneScore, - OKDSetup04WorkersScore, OKDSetup05SanityCheckScore, + OKDSetup04WorkersScore, OKDSetup05SanityCheckScore, OKDSetupPersistNetworkBondScore, bootstrap_06_installation_report::OKDSetup06InstallationReportScore, }, score::Score, @@ -65,6 +65,7 @@ impl OKDInstallationPipeline { Box::new(OKDSetup01InventoryScore::new()), Box::new(OKDSetup02BootstrapScore::new()), Box::new(OKDSetup03ControlPlaneScore::new()), + Box::new(OKDSetupPersistNetworkBondScore::new()), Box::new(OKDSetup04WorkersScore::new()), Box::new(OKDSetup05SanityCheckScore::new()), Box::new(OKDSetup06InstallationReportScore::new()), diff --git a/harmony/src/modules/okd/mod.rs b/harmony/src/modules/okd/mod.rs index a12f132..8bb85ef 100644 --- a/harmony/src/modules/okd/mod.rs +++ b/harmony/src/modules/okd/mod.rs @@ -6,6 +6,7 @@ mod bootstrap_05_sanity_check; mod bootstrap_06_installation_report; pub mod bootstrap_dhcp; pub mod bootstrap_load_balancer; +mod bootstrap_persist_network_bond; pub mod dhcp; pub mod dns; pub mod installation; @@ -19,5 +20,6 @@ pub use bootstrap_03_control_plane::*; pub use bootstrap_04_workers::*; pub use bootstrap_05_sanity_check::*; pub use bootstrap_06_installation_report::*; +pub use bootstrap_persist_network_bond::*; pub mod crd; pub mod host_network; diff --git a/opnsense-config-xml/src/data/interfaces.rs b/opnsense-config-xml/src/data/interfaces.rs index b06f392..fb49a4d 100644 --- a/opnsense-config-xml/src/data/interfaces.rs +++ b/opnsense-config-xml/src/data/interfaces.rs @@ -9,7 +9,7 @@ pub struct Interface { pub physical_interface_name: String, pub descr: Option, pub mtu: Option, - pub enable: MaybeString, + pub enable: Option, pub lock: Option, #[yaserde(rename = "spoofmac")] pub spoof_mac: Option,