forked from NationTech/harmony
		
	wip: OKD Installation automation layed out. Next step : review this after some sleep and fill in the (many) blanks with actual implementations.
This commit is contained in:
		
							parent
							
								
									55bfe306ad
								
							
						
					
					
						commit
						56c181fc3d
					
				| @ -32,6 +32,7 @@ pub enum InterpretName { | ||||
|     K8sPrometheusCrdAlerting, | ||||
|     DiscoverInventoryAgent, | ||||
|     CephClusterHealth, | ||||
|     Custom(&'static str), | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for InterpretName { | ||||
| @ -60,6 +61,7 @@ impl std::fmt::Display for InterpretName { | ||||
|             InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"), | ||||
|             InterpretName::DiscoverInventoryAgent => f.write_str("DiscoverInventoryAgent"), | ||||
|             InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"), | ||||
|             InterpretName::Custom(name) => f.write_str(name), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										868
									
								
								harmony/src/modules/okd/installation.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										868
									
								
								harmony/src/modules/okd/installation.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,868 @@ | ||||
| //! OKDInstallationScore
 | ||||
| //!
 | ||||
| //! Overview
 | ||||
| //! --------
 | ||||
| //! OKDInstallationScore orchestrates an end-to-end, bare-metal OKD (OpenShift/OKD 4.19)
 | ||||
| //! installation using Harmony’s strongly-typed Scores and Interprets. It encodes the
 | ||||
| //! “discovery-first, then provision” strategy with strict ordering, observable progress,
 | ||||
| //! and minimal assumptions about the underlying network.
 | ||||
| //!
 | ||||
| //! Design goals
 | ||||
| //! - Deterministic, observable pipeline from unknown hardware to a healthy OKD cluster.
 | ||||
| //! - Do NOT require LACP bonding during PXE/inventory. Bonding is configured only
 | ||||
| //!   after the host has a stable OS on disk (SCOS/RHCOS) and OKD MachineConfigs/NNCP
 | ||||
| //!   can enforce persistence safely.
 | ||||
| //! - Support per-MAC iPXE rendering without requiring multiple DHCP reservations for
 | ||||
| //!   the same host. Discovery runs with generic DHCP (access/unbonded). Role-specific
 | ||||
| //!   per-MAC PXE entries are activated just-in-time before install.
 | ||||
| //! - Emit HarmonyEvent instrumentation at each step via the Score::interpret path.
 | ||||
| //!
 | ||||
| //! High-level flow
 | ||||
| //! 1) OKDSetup01Inventory
 | ||||
| //!    - Serve default iPXE + Kickstart (in-RAM CentOS Stream 9) for discovery only.
 | ||||
| //!    - Enable SSH with the cluster’s ephemeral pubkey, start a Rust inventory agent.
 | ||||
| //!    - Harmony discovers nodes by scraping the agent endpoint and collects MACs/NICs.
 | ||||
| //!    - DNS: optionally register temporary hostnames and enable DHCP lease registration.
 | ||||
| //!
 | ||||
| //! 2) OKDSetup02Bootstrap
 | ||||
| //!    - User selects which discovered node becomes bootstrap.
 | ||||
| //!    - Render per-MAC iPXE for bootstrap with OKD 4.19 SCOS live assets + ignition.
 | ||||
| //!    - Reboot node via SSH; install bootstrap; wait for bootstrap-complete.
 | ||||
| //!
 | ||||
| //! 3) OKDSetup03ControlPlane
 | ||||
| //!    - Render per-MAC iPXE for cp0/cp1/cp2 with ignition (includes persistent bond via
 | ||||
| //!      MachineConfig or NNCP if required). Reboot via SSH, join masters.
 | ||||
| //!
 | ||||
| //! 4) OKDSetup04Workers
 | ||||
| //!    - Render per-MAC iPXE for worker set; join workers.
 | ||||
| //!
 | ||||
| //! 5) OKDSetup05SanityCheck
 | ||||
| //!    - Validate API/ingress/clusteroperators; ensure healthy control plane and SDN.
 | ||||
| //!
 | ||||
| //! 6) OKDSetup06InstallationReport
 | ||||
| //!    - Produce a concise, machine-readable report (JSON) and a human summary.
 | ||||
| //!
 | ||||
| //! Network notes
 | ||||
| //! - During Inventory: ports must be simple access (no LACP). DHCP succeeds; iPXE
 | ||||
| //!   loads CentOS Stream live with Kickstart and starts the inventory endpoint.
 | ||||
| //! - During Provisioning: only after SCOS is on disk and Ignition/MC can be applied
 | ||||
| //!   do we set the bond persistently. If early bonding is truly required on a host,
 | ||||
| //!   use kernel args selectively in the per-MAC PXE for that host, but never for the
 | ||||
| //!   generic discovery path.
 | ||||
| //!
 | ||||
| //! DNS and hostname
 | ||||
| //! - Because a single host may present multiple MACs, but DHCP/ISC on OPNsense may not
 | ||||
| //!   easily support “one hostname across multiple MACs” in a single lease entry, we avoid
 | ||||
| //!   strict hostname binding during discovery. We rely on dynamic leases and record the
 | ||||
| //!   mapping (IP/MAC) at scrape time.
 | ||||
| //! - Once a role is assigned, we render a per-MAC PXE entry and ensure the role-specific
 | ||||
| //!   DNS A/AAAA/CNAME entries are present (e.g., api, api-int, apps wildcard). This keeps
 | ||||
| //!   DHCP simple and DNS consistent for OKD.
 | ||||
| //!
 | ||||
| //! Instrumentation
 | ||||
| //! - All child Scores are executed via Score::interpret, which emits HarmonyEvent
 | ||||
| //!   InterpretExecutionStarted/Finished. The orchestrator also emits HarmonyStarted/
 | ||||
| //!   HarmonyFinished around the full pipeline execution.
 | ||||
| //!
 | ||||
| //! Configuration knobs
 | ||||
| //! - lan_cidr: CIDR to scan/allow for discovery endpoints.
 | ||||
| //! - public_domain: External wildcard/apps domain (e.g., apps.example.com).
 | ||||
| //! - internal_domain: Internal cluster domain (e.g., cluster.local or harmony.mcd).
 | ||||
| //!
 | ||||
| //! Notes
 | ||||
| //! - This file co-locates step Scores for ease of review. In follow-up changes, refactor
 | ||||
| //!   step Scores (OKDSetupXX*) into separate modules.
 | ||||
| 
 | ||||
| use async_trait::async_trait; | ||||
| use derive_new::new; | ||||
| use harmony_macros::{ip, ipv4}; | ||||
| use log::info; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     instrumentation::{HarmonyEvent, instrument}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     score::Score, | ||||
|     topology::{DnsRecord, DnsRecordType, DnsServer, Topology}, | ||||
| }; | ||||
| 
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| // Public Orchestrator Score
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, new)] | ||||
| pub struct OKDInstallationScore { | ||||
|     /// The LAN CIDR where discovery endpoints live (e.g., 192.168.10.0/24)
 | ||||
|     pub lan_cidr: String, | ||||
|     /// Public external domain (e.g., example.com). Used for api/apps wildcard, etc.
 | ||||
|     pub public_domain: String, | ||||
|     /// Internal cluster domain (e.g., harmony.mcd). Used for internal svc/ingress and DNS.
 | ||||
|     pub internal_domain: String, | ||||
| } | ||||
| 
 | ||||
| impl<T: Topology + DnsServer + 'static> Score<T> for OKDInstallationScore { | ||||
|     fn create_interpret(&self) -> Box<dyn Interpret<T>> { | ||||
|         Box::new(OKDInstallationInterpret::new(self.clone())) | ||||
|     } | ||||
| 
 | ||||
|     fn name(&self) -> String { | ||||
|         "OKDInstallationScore".to_string() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| // Orchestrator Interpret
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct OKDInstallationInterpret { | ||||
|     score: OKDInstallationScore, | ||||
|     version: Version, | ||||
|     status: InterpretStatus, | ||||
| } | ||||
| 
 | ||||
| impl OKDInstallationInterpret { | ||||
|     pub fn new(score: OKDInstallationScore) -> Self { | ||||
|         let version = Version::from("0.1.0").expect("valid version"); | ||||
|         Self { | ||||
|             score, | ||||
|             version, | ||||
|             status: InterpretStatus::QUEUED, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async fn run_inventory_phase<T: Topology + DnsServer>( | ||||
|         &self, | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<(), InterpretError> { | ||||
|         // 1) Prepare DNS and DHCP lease registration (optional)
 | ||||
|         let dns_score = OKDSetup01InventoryDnsScore::new( | ||||
|             self.score.internal_domain.clone(), | ||||
|             self.score.public_domain.clone(), | ||||
|             Some(true), // register_dhcp_leases
 | ||||
|         ); | ||||
|         dns_score.interpret(inventory, topology).await?; | ||||
| 
 | ||||
|         // 2) Serve default iPXE + Kickstart and poll discovery
 | ||||
|         let discovery_score = OKDSetup01InventoryScore::new(self.score.lan_cidr.clone()); | ||||
|         discovery_score.interpret(inventory, topology).await?; | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn run_bootstrap_phase<T: Topology + DnsServer>( | ||||
|         &self, | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<(), InterpretError> { | ||||
|         // Select and provision bootstrap
 | ||||
|         let bootstrap_score = OKDSetup02BootstrapScore::new( | ||||
|             self.score.public_domain.clone(), | ||||
|             self.score.internal_domain.clone(), | ||||
|         ); | ||||
|         bootstrap_score.interpret(inventory, topology).await?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn run_control_plane_phase<T: Topology + DnsServer>( | ||||
|         &self, | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<(), InterpretError> { | ||||
|         let control_plane_score = OKDSetup03ControlPlaneScore::new(); | ||||
|         control_plane_score.interpret(inventory, topology).await?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn run_workers_phase<T: Topology + DnsServer>( | ||||
|         &self, | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<(), InterpretError> { | ||||
|         let workers_score = OKDSetup04WorkersScore::new(); | ||||
|         workers_score.interpret(inventory, topology).await?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn run_sanity_phase<T: Topology + DnsServer>( | ||||
|         &self, | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<(), InterpretError> { | ||||
|         let sanity_score = OKDSetup05SanityCheckScore::new(); | ||||
|         sanity_score.interpret(inventory, topology).await?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn run_report_phase<T: Topology + DnsServer>( | ||||
|         &self, | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<(), InterpretError> { | ||||
|         let report_score = OKDSetup06InstallationReportScore::new( | ||||
|             self.score.public_domain.clone(), | ||||
|             self.score.internal_domain.clone(), | ||||
|         ); | ||||
|         report_score.interpret(inventory, topology).await?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology + DnsServer> Interpret<T> for OKDInstallationInterpret { | ||||
|     fn get_name(&self) -> InterpretName { | ||||
|         InterpretName::Custom("OKDInstallationInterpret") | ||||
|     } | ||||
| 
 | ||||
|     fn get_version(&self) -> Version { | ||||
|         self.version.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_status(&self) -> InterpretStatus { | ||||
|         self.status.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<crate::domain::data::Id> { | ||||
|         vec![] | ||||
|     } | ||||
| 
 | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         instrument(HarmonyEvent::HarmonyStarted).ok(); | ||||
| 
 | ||||
|         info!( | ||||
|             "Starting OKD installation pipeline for public_domain={} internal_domain={} lan_cidr={}", | ||||
|             self.score.public_domain, self.score.internal_domain, self.score.lan_cidr | ||||
|         ); | ||||
| 
 | ||||
|         // 1) Inventory (default PXE, in-RAM kickstart, Rust inventory agent)
 | ||||
|         self.run_inventory_phase(inventory, topology).await?; | ||||
| 
 | ||||
|         // 2) Bootstrap (render per-MAC iPXE + ignition; reboot node; wait for bootstrap complete)
 | ||||
|         self.run_bootstrap_phase(inventory, topology).await?; | ||||
| 
 | ||||
|         // 3) Control plane
 | ||||
|         self.run_control_plane_phase(inventory, topology).await?; | ||||
| 
 | ||||
|         // 4) Workers
 | ||||
|         self.run_workers_phase(inventory, topology).await?; | ||||
| 
 | ||||
|         // 5) Sanity checks
 | ||||
|         self.run_sanity_phase(inventory, topology).await?; | ||||
| 
 | ||||
|         // 6) Installation report
 | ||||
|         self.run_report_phase(inventory, topology).await?; | ||||
| 
 | ||||
|         instrument(HarmonyEvent::HarmonyFinished).ok(); | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "OKD installation pipeline completed".into(), | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| // Step 01: Inventory DNS setup
 | ||||
| // - Keep DHCP simple; optionally register dynamic leases into DNS.
 | ||||
| // - Ensure base records for internal/public domains (api/api-int/apps wildcard).
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize, new)] | ||||
| struct OKDSetup01InventoryDnsScore { | ||||
|     internal_domain: String, | ||||
|     public_domain: String, | ||||
|     register_dhcp_leases: Option<bool>, | ||||
| } | ||||
| 
 | ||||
| impl<T: Topology + DnsServer> Score<T> for OKDSetup01InventoryDnsScore { | ||||
|     fn create_interpret(&self) -> Box<dyn Interpret<T>> { | ||||
|         Box::new(OKDSetup01InventoryDnsInterpret::new(self.clone())) | ||||
|     } | ||||
| 
 | ||||
|     fn name(&self) -> String { | ||||
|         "OKDSetup01InventoryDnsScore".to_string() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| struct OKDSetup01InventoryDnsInterpret { | ||||
|     score: OKDSetup01InventoryDnsScore, | ||||
|     version: Version, | ||||
|     status: InterpretStatus, | ||||
| } | ||||
| 
 | ||||
| impl OKDSetup01InventoryDnsInterpret { | ||||
|     pub fn new(score: OKDSetup01InventoryDnsScore) -> Self { | ||||
|         let version = Version::from("1.0.0").unwrap(); | ||||
|         Self { | ||||
|             version, | ||||
|             score, | ||||
|             status: InterpretStatus::QUEUED, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async fn ensure_dns<T: DnsServer>(&self, dns: &T) -> Result<(), InterpretError> { | ||||
|         // Minimal records placeholders; real IPs are set elsewhere in the flow.
 | ||||
|         // We register the names early to ensure resolvability for clients relying on DNS.
 | ||||
|         let mut records: Vec<DnsRecord> = vec![ | ||||
|             DnsRecord { | ||||
|                 value: ip!("0.0.0.0"), | ||||
|                 host: "api".to_string(), | ||||
|                 domain: self.score.internal_domain.clone(), | ||||
|                 record_type: DnsRecordType::A, | ||||
|             }, | ||||
|             DnsRecord { | ||||
|                 value: ip!("0.0.0.0"), | ||||
|                 host: "api-int".to_string(), | ||||
|                 domain: self.score.internal_domain.clone(), | ||||
|                 record_type: DnsRecordType::A, | ||||
|             }, | ||||
|             DnsRecord { | ||||
|                 value: ip!("0.0.0.0"), | ||||
|                 host: "*.apps.".to_string(), | ||||
|                 domain: self.score.internal_domain.clone(), | ||||
|                 record_type: DnsRecordType::A, | ||||
|             }, | ||||
|         ]; | ||||
|         dns.ensure_hosts_registered(records.drain(..).collect()) | ||||
|             .await?; | ||||
|         if let Some(register) = self.score.register_dhcp_leases { | ||||
|             dns.register_dhcp_leases(register).await?; | ||||
|         } | ||||
|         dns.commit_config().await?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology + DnsServer> Interpret<T> for OKDSetup01InventoryDnsInterpret { | ||||
|     fn get_name(&self) -> InterpretName { | ||||
|         InterpretName::Custom("OKDSetup01InventoryDns") | ||||
|     } | ||||
| 
 | ||||
|     fn get_version(&self) -> Version { | ||||
|         self.version.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_status(&self) -> InterpretStatus { | ||||
|         self.status.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<crate::domain::data::Id> { | ||||
|         vec![] | ||||
|     } | ||||
| 
 | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         _inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         info!("Ensuring base DNS and DHCP lease registration for discovery phase"); | ||||
|         self.ensure_dns(topology).await?; | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Inventory DNS prepared".into(), | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| // Step 01: Inventory (default PXE + Kickstart in RAM + Rust agent)
 | ||||
| // - This score exposes/ensures the default inventory assets and waits for discoveries.
 | ||||
| // - No early bonding. Simple access DHCP.
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize, new)] | ||||
| struct OKDSetup01InventoryScore { | ||||
|     lan_cidr: String, | ||||
| } | ||||
| 
 | ||||
| impl<T: Topology> Score<T> for OKDSetup01InventoryScore { | ||||
|     fn create_interpret(&self) -> Box<dyn Interpret<T>> { | ||||
|         Box::new(OKDSetup01InventoryInterpret::new(self.clone())) | ||||
|     } | ||||
| 
 | ||||
|     fn name(&self) -> String { | ||||
|         "OKDSetup01InventoryScore".to_string() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| struct OKDSetup01InventoryInterpret { | ||||
|     score: OKDSetup01InventoryScore, | ||||
|     version: Version, | ||||
|     status: InterpretStatus, | ||||
| } | ||||
| 
 | ||||
| impl OKDSetup01InventoryInterpret { | ||||
|     pub fn new(score: OKDSetup01InventoryScore) -> Self { | ||||
|         let version = Version::from("1.0.0").unwrap(); | ||||
|         Self { | ||||
|             version, | ||||
|             score, | ||||
|             status: InterpretStatus::QUEUED, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async fn ensure_inventory_assets<T: Topology>( | ||||
|         &self, | ||||
|         topology: &T, | ||||
|     ) -> Result<(), InterpretError> { | ||||
|         // Placeholder: push or verify iPXE default, Kickstart, and Rust inventory agent are hosted.
 | ||||
|         // Real implementation: publish to the PXE/HTTP server via the topology.
 | ||||
|         info!( | ||||
|             "[Inventory] Ensuring default iPXE, Kickstart, and inventory agent are available for LAN {}", | ||||
|             self.score.lan_cidr | ||||
|         ); | ||||
|         // topology.publish_http_asset(…) ?
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn discover_nodes(&self) -> Result<usize, InterpretError> { | ||||
|         // Placeholder: implement Harmony discovery logic (scan/pull/push mode).
 | ||||
|         // Returns number of newly discovered nodes.
 | ||||
|         info!( | ||||
|             "[Inventory] Scanning for inventory agents in {}", | ||||
|             self.score.lan_cidr | ||||
|         ); | ||||
|         // In practice, this would query harmony_composer or a local registry store.
 | ||||
|         Ok(3) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology> Interpret<T> for OKDSetup01InventoryInterpret { | ||||
|     fn get_name(&self) -> InterpretName { | ||||
|         InterpretName::Custom("OKDSetup01Inventory") | ||||
|     } | ||||
| 
 | ||||
|     fn get_version(&self) -> Version { | ||||
|         self.version.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_status(&self) -> InterpretStatus { | ||||
|         self.status.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<crate::domain::data::Id> { | ||||
|         vec![] | ||||
|     } | ||||
| 
 | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         _inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         self.ensure_inventory_assets(topology).await?; | ||||
|         let count = self.discover_nodes().await?; | ||||
|         info!("[Inventory] Discovered {count} nodes"); | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             format!("Inventory phase complete. Nodes discovered: {count}"), | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| // Step 02: Bootstrap
 | ||||
| // - Select bootstrap node (from discovered set).
 | ||||
| // - Render per-MAC iPXE pointing to OKD 4.19 SCOS live assets + bootstrap ignition.
 | ||||
| // - Reboot the host via SSH and wait for bootstrap-complete.
 | ||||
| // - No bonding at this stage unless absolutely required; prefer persistence via MC later.
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize, new)] | ||||
| struct OKDSetup02BootstrapScore { | ||||
|     public_domain: String, | ||||
|     internal_domain: String, | ||||
| } | ||||
| 
 | ||||
| impl<T: Topology> Score<T> for OKDSetup02BootstrapScore { | ||||
|     fn create_interpret(&self) -> Box<dyn Interpret<T>> { | ||||
|         Box::new(OKDSetup02BootstrapInterpret::new(self.clone())) | ||||
|     } | ||||
| 
 | ||||
|     fn name(&self) -> String { | ||||
|         "OKDSetup02BootstrapScore".to_string() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| struct OKDSetup02BootstrapInterpret { | ||||
|     score: OKDSetup02BootstrapScore, | ||||
|     version: Version, | ||||
|     status: InterpretStatus, | ||||
| } | ||||
| 
 | ||||
| impl OKDSetup02BootstrapInterpret { | ||||
|     pub fn new(score: OKDSetup02BootstrapScore) -> Self { | ||||
|         let version = Version::from("1.0.0").unwrap(); | ||||
|         Self { | ||||
|             version, | ||||
|             score, | ||||
|             status: InterpretStatus::QUEUED, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async fn render_per_mac_pxe(&self) -> Result<(), InterpretError> { | ||||
|         // Placeholder: use Harmony templates to emit {MAC}.ipxe selecting SCOS live + bootstrap ignition.
 | ||||
|         info!("[Bootstrap] Rendering per-MAC PXE for bootstrap node"); | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn reboot_target(&self) -> Result<(), InterpretError> { | ||||
|         // Placeholder: ssh reboot using the inventory ephemeral key
 | ||||
|         info!("[Bootstrap] Rebooting bootstrap node via SSH"); | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn wait_for_bootstrap_complete(&self) -> Result<(), InterpretError> { | ||||
|         // Placeholder: wait-for bootstrap-complete
 | ||||
|         info!("[Bootstrap] Waiting for bootstrap-complete …"); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology> Interpret<T> for OKDSetup02BootstrapInterpret { | ||||
|     fn get_name(&self) -> InterpretName { | ||||
|         InterpretName::Custom("OKDSetup02Bootstrap") | ||||
|     } | ||||
| 
 | ||||
|     fn get_version(&self) -> Version { | ||||
|         self.version.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_status(&self) -> InterpretStatus { | ||||
|         self.status.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<crate::domain::data::Id> { | ||||
|         vec![] | ||||
|     } | ||||
| 
 | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         _inventory: &Inventory, | ||||
|         _topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         self.render_per_mac_pxe().await?; | ||||
|         self.reboot_target().await?; | ||||
|         self.wait_for_bootstrap_complete().await?; | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Bootstrap phase complete".into(), | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| // Step 03: Control Plane
 | ||||
| // - Render per-MAC PXE & ignition for cp0/cp1/cp2.
 | ||||
| // - Persist bonding via MachineConfigs (or NNCP) once SCOS is active.
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize, new)] | ||||
| struct OKDSetup03ControlPlaneScore {} | ||||
| 
 | ||||
| impl<T: Topology> Score<T> for OKDSetup03ControlPlaneScore { | ||||
|     fn create_interpret(&self) -> Box<dyn Interpret<T>> { | ||||
|         Box::new(OKDSetup03ControlPlaneInterpret::new(self.clone())) | ||||
|     } | ||||
| 
 | ||||
|     fn name(&self) -> String { | ||||
|         "OKDSetup03ControlPlaneScore".to_string() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| struct OKDSetup03ControlPlaneInterpret { | ||||
|     score: OKDSetup03ControlPlaneScore, | ||||
|     version: Version, | ||||
|     status: InterpretStatus, | ||||
| } | ||||
| 
 | ||||
| impl OKDSetup03ControlPlaneInterpret { | ||||
|     pub fn new(score: OKDSetup03ControlPlaneScore) -> Self { | ||||
|         let version = Version::from("1.0.0").unwrap(); | ||||
|         Self { | ||||
|             version, | ||||
|             score, | ||||
|             status: InterpretStatus::QUEUED, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async fn render_and_reboot(&self) -> Result<(), InterpretError> { | ||||
|         info!("[ControlPlane] Rendering per-MAC PXE for masters and rebooting"); | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     async fn persist_network_bond(&self) -> Result<(), InterpretError> { | ||||
|         // Generate MC or NNCP from inventory NIC data; apply via ignition or post-join.
 | ||||
|         info!("[ControlPlane] Ensuring persistent bonding via MachineConfig/NNCP"); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology> Interpret<T> for OKDSetup03ControlPlaneInterpret { | ||||
|     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<crate::domain::data::Id> { | ||||
|         vec![] | ||||
|     } | ||||
| 
 | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         _inventory: &Inventory, | ||||
|         _topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         self.render_and_reboot().await?; | ||||
|         self.persist_network_bond().await?; | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Control plane provisioned".into(), | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| // Step 04: Workers
 | ||||
| // - Render per-MAC PXE & ignition for workers; join nodes.
 | ||||
| // - Persist bonding via MC/NNCP as required (same approach as masters).
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize, new)] | ||||
| struct OKDSetup04WorkersScore {} | ||||
| 
 | ||||
| impl<T: Topology> Score<T> for OKDSetup04WorkersScore { | ||||
|     fn create_interpret(&self) -> Box<dyn Interpret<T>> { | ||||
|         Box::new(OKDSetup04WorkersInterpret::new(self.clone())) | ||||
|     } | ||||
| 
 | ||||
|     fn name(&self) -> String { | ||||
|         "OKDSetup04WorkersScore".to_string() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| struct OKDSetup04WorkersInterpret { | ||||
|     score: OKDSetup04WorkersScore, | ||||
|     version: Version, | ||||
|     status: InterpretStatus, | ||||
| } | ||||
| 
 | ||||
| impl OKDSetup04WorkersInterpret { | ||||
|     pub fn new(score: OKDSetup04WorkersScore) -> Self { | ||||
|         let version = Version::from("1.0.0").unwrap(); | ||||
|         Self { | ||||
|             version, | ||||
|             score, | ||||
|             status: InterpretStatus::QUEUED, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async fn render_and_reboot(&self) -> Result<(), InterpretError> { | ||||
|         info!("[Workers] Rendering per-MAC PXE for workers and rebooting"); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology> Interpret<T> for OKDSetup04WorkersInterpret { | ||||
|     fn get_name(&self) -> InterpretName { | ||||
|         InterpretName::Custom("OKDSetup04Workers") | ||||
|     } | ||||
| 
 | ||||
|     fn get_version(&self) -> Version { | ||||
|         self.version.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_status(&self) -> InterpretStatus { | ||||
|         self.status.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<crate::domain::data::Id> { | ||||
|         vec![] | ||||
|     } | ||||
| 
 | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         _inventory: &Inventory, | ||||
|         _topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         self.render_and_reboot().await?; | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Workers provisioned".into(), | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| // Step 05: Sanity Check
 | ||||
| // - Validate API reachability, ClusterOperators, ingress, and SDN status.
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize, new)] | ||||
| struct OKDSetup05SanityCheckScore {} | ||||
| 
 | ||||
| impl<T: Topology> Score<T> for OKDSetup05SanityCheckScore { | ||||
|     fn create_interpret(&self) -> Box<dyn Interpret<T>> { | ||||
|         Box::new(OKDSetup05SanityCheckInterpret::new(self.clone())) | ||||
|     } | ||||
| 
 | ||||
|     fn name(&self) -> String { | ||||
|         "OKDSetup05SanityCheckScore".to_string() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| struct OKDSetup05SanityCheckInterpret { | ||||
|     score: OKDSetup05SanityCheckScore, | ||||
|     version: Version, | ||||
|     status: InterpretStatus, | ||||
| } | ||||
| 
 | ||||
| impl OKDSetup05SanityCheckInterpret { | ||||
|     pub fn new(score: OKDSetup05SanityCheckScore) -> Self { | ||||
|         let version = Version::from("1.0.0").unwrap(); | ||||
|         Self { | ||||
|             version, | ||||
|             score, | ||||
|             status: InterpretStatus::QUEUED, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async fn run_checks(&self) -> Result<(), InterpretError> { | ||||
|         info!("[Sanity] Checking API, COs, Ingress, and SDN health …"); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology> Interpret<T> for OKDSetup05SanityCheckInterpret { | ||||
|     fn get_name(&self) -> InterpretName { | ||||
|         InterpretName::Custom("OKDSetup05SanityCheck") | ||||
|     } | ||||
| 
 | ||||
|     fn get_version(&self) -> Version { | ||||
|         self.version.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_status(&self) -> InterpretStatus { | ||||
|         self.status.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<crate::domain::data::Id> { | ||||
|         vec![] | ||||
|     } | ||||
| 
 | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         _inventory: &Inventory, | ||||
|         _topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         self.run_checks().await?; | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Sanity checks passed".into(), | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| // Step 06: Installation Report
 | ||||
| // - Emit JSON and concise human summary of nodes, roles, versions, and health.
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize, new)] | ||||
| struct OKDSetup06InstallationReportScore { | ||||
|     public_domain: String, | ||||
|     internal_domain: String, | ||||
| } | ||||
| 
 | ||||
| impl<T: Topology> Score<T> for OKDSetup06InstallationReportScore { | ||||
|     fn create_interpret(&self) -> Box<dyn Interpret<T>> { | ||||
|         Box::new(OKDSetup06InstallationReportInterpret::new(self.clone())) | ||||
|     } | ||||
| 
 | ||||
|     fn name(&self) -> String { | ||||
|         "OKDSetup06InstallationReportScore".to_string() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| struct OKDSetup06InstallationReportInterpret { | ||||
|     score: OKDSetup06InstallationReportScore, | ||||
|     version: Version, | ||||
|     status: InterpretStatus, | ||||
| } | ||||
| 
 | ||||
| impl OKDSetup06InstallationReportInterpret { | ||||
|     pub fn new(score: OKDSetup06InstallationReportScore) -> Self { | ||||
|         let version = Version::from("1.0.0").unwrap(); | ||||
|         Self { | ||||
|             version, | ||||
|             score, | ||||
|             status: InterpretStatus::QUEUED, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async fn generate(&self) -> Result<(), InterpretError> { | ||||
|         info!( | ||||
|             "[Report] Generating installation report for {} / {}", | ||||
|             self.score.public_domain, self.score.internal_domain | ||||
|         ); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology> Interpret<T> for OKDSetup06InstallationReportInterpret { | ||||
|     fn get_name(&self) -> InterpretName { | ||||
|         InterpretName::Custom("OKDSetup06InstallationReport") | ||||
|     } | ||||
| 
 | ||||
|     fn get_version(&self) -> Version { | ||||
|         self.version.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_status(&self) -> InterpretStatus { | ||||
|         self.status.clone() | ||||
|     } | ||||
| 
 | ||||
|     fn get_children(&self) -> Vec<crate::domain::data::Id> { | ||||
|         vec![] | ||||
|     } | ||||
| 
 | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         _inventory: &Inventory, | ||||
|         _topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         self.generate().await?; | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Installation report generated".into(), | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| @ -3,5 +3,6 @@ pub mod bootstrap_load_balancer; | ||||
| pub mod dhcp; | ||||
| pub mod dns; | ||||
| pub mod ipxe; | ||||
| pub mod installation; | ||||
| pub mod load_balancer; | ||||
| pub mod upgrade; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user