wip: OKD Installation automation layed out. Next step : review this after some sleep and fill in the (many) blanks with actual implementations.
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Run Check Script / check (pull_request) Successful in 1m7s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Run Check Script / check (pull_request) Successful in 1m7s
				
			This commit is contained in:
		
							parent
							
								
									9c5d1bd27f
								
							
						
					
					
						commit
						730b913bd1
					
				| @ -32,6 +32,7 @@ pub enum InterpretName { | |||||||
|     Lamp, |     Lamp, | ||||||
|     ApplicationMonitoring, |     ApplicationMonitoring, | ||||||
|     K8sPrometheusCrdAlerting, |     K8sPrometheusCrdAlerting, | ||||||
|  |     Custom(&'static str), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl std::fmt::Display for InterpretName { | impl std::fmt::Display for InterpretName { | ||||||
| @ -58,6 +59,7 @@ impl std::fmt::Display for InterpretName { | |||||||
|             InterpretName::Lamp => f.write_str("LAMP"), |             InterpretName::Lamp => f.write_str("LAMP"), | ||||||
|             InterpretName::ApplicationMonitoring => f.write_str("ApplicationMonitoring"), |             InterpretName::ApplicationMonitoring => f.write_str("ApplicationMonitoring"), | ||||||
|             InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"), |             InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"), | ||||||
|  |             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(), | ||||||
|  |         )) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -2,5 +2,6 @@ pub mod bootstrap_dhcp; | |||||||
| pub mod bootstrap_load_balancer; | pub mod bootstrap_load_balancer; | ||||||
| pub mod dhcp; | pub mod dhcp; | ||||||
| pub mod dns; | pub mod dns; | ||||||
|  | pub mod installation; | ||||||
| pub mod load_balancer; | pub mod load_balancer; | ||||||
| pub mod upgrade; | pub mod upgrade; | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user