From 56c181fc3df56ceae15ef839f9e9a5fdfb683549 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 18 Aug 2025 22:29:46 -0400 Subject: [PATCH] wip: OKD Installation automation layed out. Next step : review this after some sleep and fill in the (many) blanks with actual implementations. --- harmony/src/domain/interpret/mod.rs | 2 + harmony/src/modules/okd/installation.rs | 868 ++++++++++++++++++++++++ harmony/src/modules/okd/mod.rs | 1 + 3 files changed, 871 insertions(+) create mode 100644 harmony/src/modules/okd/installation.rs diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index 71d2f61..737bf28 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -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), } } } diff --git a/harmony/src/modules/okd/installation.rs b/harmony/src/modules/okd/installation.rs new file mode 100644 index 0000000..f9f59e2 --- /dev/null +++ b/harmony/src/modules/okd/installation.rs @@ -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 Score for OKDInstallationScore { + fn create_interpret(&self) -> Box> { + 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( + &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( + &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( + &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( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result<(), InterpretError> { + let workers_score = OKDSetup04WorkersScore::new(); + workers_score.interpret(inventory, topology).await?; + Ok(()) + } + + async fn run_sanity_phase( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result<(), InterpretError> { + let sanity_score = OKDSetup05SanityCheckScore::new(); + sanity_score.interpret(inventory, topology).await?; + Ok(()) + } + + async fn run_report_phase( + &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 Interpret 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 { + vec![] + } + + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { + 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, +} + +impl Score for OKDSetup01InventoryDnsScore { + fn create_interpret(&self) -> Box> { + 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(&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 = 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 Interpret 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 { + vec![] + } + + async fn execute( + &self, + _inventory: &Inventory, + topology: &T, + ) -> Result { + 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 Score for OKDSetup01InventoryScore { + fn create_interpret(&self) -> Box> { + 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( + &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 { + // 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 Interpret 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 { + vec![] + } + + async fn execute( + &self, + _inventory: &Inventory, + topology: &T, + ) -> Result { + 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 Score for OKDSetup02BootstrapScore { + fn create_interpret(&self) -> Box> { + 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 Interpret 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 { + vec![] + } + + async fn execute( + &self, + _inventory: &Inventory, + _topology: &T, + ) -> Result { + 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 Score for OKDSetup03ControlPlaneScore { + fn create_interpret(&self) -> Box> { + 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 Interpret 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 { + vec![] + } + + async fn execute( + &self, + _inventory: &Inventory, + _topology: &T, + ) -> Result { + 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 Score for OKDSetup04WorkersScore { + fn create_interpret(&self) -> Box> { + 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 Interpret 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 { + vec![] + } + + async fn execute( + &self, + _inventory: &Inventory, + _topology: &T, + ) -> Result { + 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 Score for OKDSetup05SanityCheckScore { + fn create_interpret(&self) -> Box> { + 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 Interpret 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 { + vec![] + } + + async fn execute( + &self, + _inventory: &Inventory, + _topology: &T, + ) -> Result { + 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 Score for OKDSetup06InstallationReportScore { + fn create_interpret(&self) -> Box> { + 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 Interpret 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 { + vec![] + } + + async fn execute( + &self, + _inventory: &Inventory, + _topology: &T, + ) -> Result { + self.generate().await?; + Ok(Outcome::new( + InterpretStatus::SUCCESS, + "Installation report generated".into(), + )) + } +} diff --git a/harmony/src/modules/okd/mod.rs b/harmony/src/modules/okd/mod.rs index fe61b1e..2cab1ad 100644 --- a/harmony/src/modules/okd/mod.rs +++ b/harmony/src/modules/okd/mod.rs @@ -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;