From 8bcade27a14df490e3bab2e3ea62043e6eb2db3f Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 4 Sep 2025 17:23:17 -0400 Subject: [PATCH] refact: Split OKD installation score into a simple struct that returns the list of scores. Also refactored discovery logic so it can be used by bootstrap and control plane steps --- Cargo.lock | 1 + examples/nanodc/Cargo.toml | 1 + examples/nanodc/src/main.rs | 17 +- examples/okd_installation/src/main.rs | 4 +- examples/okd_pxe/src/main.rs | 14 +- harmony/src/domain/inventory/mod.rs | 2 +- harmony/src/infra/inventory/sqlite.rs | 1 + harmony/src/modules/inventory/discovery.rs | 122 +++ harmony/src/modules/inventory/mod.rs | 3 + .../src/modules/okd/bootstrap_01_prepare.rs | 120 +++ .../src/modules/okd/bootstrap_02_bootstrap.rs | 387 +++++++ .../modules/okd/bootstrap_03_control_plane.rs | 195 ++++ .../src/modules/okd/bootstrap_04_workers.rs | 102 ++ .../modules/okd/bootstrap_05_sanity_check.rs | 101 ++ .../okd/bootstrap_06_installation_report.rs | 101 ++ harmony/src/modules/okd/installation.rs | 986 +----------------- harmony/src/modules/okd/mod.rs | 12 + harmony/src/modules/okd/templates.rs | 1 + harmony/templates/okd/bootstrap.ipxe.j2 | 2 +- 19 files changed, 1187 insertions(+), 985 deletions(-) create mode 100644 harmony/src/modules/inventory/discovery.rs create mode 100644 harmony/src/modules/okd/bootstrap_01_prepare.rs create mode 100644 harmony/src/modules/okd/bootstrap_02_bootstrap.rs create mode 100644 harmony/src/modules/okd/bootstrap_03_control_plane.rs create mode 100644 harmony/src/modules/okd/bootstrap_04_workers.rs create mode 100644 harmony/src/modules/okd/bootstrap_05_sanity_check.rs create mode 100644 harmony/src/modules/okd/bootstrap_06_installation_report.rs diff --git a/Cargo.lock b/Cargo.lock index afc3f2e..e87eede 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1759,6 +1759,7 @@ dependencies = [ "env_logger", "harmony", "harmony_macros", + "harmony_secret", "harmony_tui", "harmony_types", "log", diff --git a/examples/nanodc/Cargo.toml b/examples/nanodc/Cargo.toml index ccd3a3a..889c24d 100644 --- a/examples/nanodc/Cargo.toml +++ b/examples/nanodc/Cargo.toml @@ -13,6 +13,7 @@ harmony_types = { path = "../../harmony_types" } cidr = { workspace = true } tokio = { workspace = true } harmony_macros = { path = "../../harmony_macros" } +harmony_secret = { path = "../../harmony_secret" } log = { workspace = true } env_logger = { workspace = true } url = { workspace = true } diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index 5b04bf3..c89de73 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -5,10 +5,7 @@ use std::{ use cidr::Ipv4Cidr; use harmony::{ - hardware::{HostCategory, Location, PhysicalHost, SwitchGroup}, - infra::opnsense::OPNSenseManagementInterface, - inventory::Inventory, - modules::{ + config::secret::SshKeyPair, data::{FileContent, FilePath}, hardware::{HostCategory, Location, PhysicalHost, SwitchGroup}, infra::opnsense::OPNSenseManagementInterface, inventory::Inventory, modules::{ http::StaticFilesHttpScore, okd::{ bootstrap_dhcp::OKDBootstrapDhcpScore, @@ -16,10 +13,10 @@ use harmony::{ dns::OKDDnsScore, ipxe::OKDIpxeScore, }, tftp::TftpScore, - }, - topology::{LogicalHost, UnmanagedRouter}, + }, topology::{LogicalHost, UnmanagedRouter} }; use harmony_macros::{ip, mac_address}; +use harmony_secret::SecretManager; use harmony_types::net::Url; #[tokio::main] @@ -123,6 +120,8 @@ async fn main() { let load_balancer_score = harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology); + let ssh_key = SecretManager::get_or_prompt::().await.unwrap(); + let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); let http_score = StaticFilesHttpScore { folder_to_serve: Some(Url::LocalFolder( @@ -133,13 +132,15 @@ async fn main() { }; let kickstart_filename = "inventory.kickstart".to_string(); - let cluster_pubkey_filename = "cluster_ssh_key.pub".to_string(); let harmony_inventory_agent = "harmony_inventory_agent".to_string(); let ipxe_score = OKDIpxeScore { kickstart_filename, harmony_inventory_agent, - cluster_pubkey, + cluster_pubkey: FileContent { + path: FilePath::Relative("cluster_ssh_key.pub".to_string()), + content: ssh_key.public, + }, }; harmony_tui::run( diff --git a/examples/okd_installation/src/main.rs b/examples/okd_installation/src/main.rs index 04bf853..e474e0a 100644 --- a/examples/okd_installation/src/main.rs +++ b/examples/okd_installation/src/main.rs @@ -2,7 +2,7 @@ mod topology; use crate::topology::{get_inventory, get_topology}; use harmony::{ - config::secret::SshKeyPair, data::{FileContent, FilePath}, modules::okd::{installation::OKDInstallationScore, ipxe::OKDIpxeScore}, score::Score, topology::HAClusterTopology + config::secret::SshKeyPair, data::{FileContent, FilePath}, modules::okd::{installation::OKDInstallationPipeline, ipxe::OKDIpxeScore}, score::Score, topology::HAClusterTopology }; use harmony_secret::SecretManager; @@ -22,7 +22,7 @@ async fn main() { content: ssh_key.public, }, }), - Box::new(OKDInstallationScore {}), + Box::new(OKDInstallationPipeline {}), ]; harmony_cli::run(inventory, topology, scores, None) .await diff --git a/examples/okd_pxe/src/main.rs b/examples/okd_pxe/src/main.rs index 97e6f74..bd638dd 100644 --- a/examples/okd_pxe/src/main.rs +++ b/examples/okd_pxe/src/main.rs @@ -1,7 +1,12 @@ mod topology; use crate::topology::{get_inventory, get_topology}; -use harmony::modules::okd::ipxe::OKDIpxeScore; +use harmony::{ + config::secret::SshKeyPair, + data::{FileContent, FilePath}, + modules::okd::ipxe::OKDIpxeScore, +}; +use harmony_secret::SecretManager; #[tokio::main] async fn main() { @@ -9,13 +14,16 @@ async fn main() { let topology = get_topology().await; let kickstart_filename = "inventory.kickstart".to_string(); - let cluster_pubkey_filename = "cluster_ssh_key.pub".to_string(); let harmony_inventory_agent = "harmony_inventory_agent".to_string(); + let ssh_key = SecretManager::get_or_prompt::().await.unwrap(); let ipxe_score = OKDIpxeScore { kickstart_filename, harmony_inventory_agent, - cluster_pubkey, + cluster_pubkey: FileContent { + path: FilePath::Relative("cluster_ssh_key.pub".to_string()), + content: ssh_key.public, + }, }; harmony_cli::run(inventory, topology, vec![Box::new(ipxe_score)], None) diff --git a/harmony/src/domain/inventory/mod.rs b/harmony/src/domain/inventory/mod.rs index aa75365..072ab79 100644 --- a/harmony/src/domain/inventory/mod.rs +++ b/harmony/src/domain/inventory/mod.rs @@ -63,7 +63,7 @@ impl Inventory { } } -#[derive(Debug, Serialize, Deserialize, sqlx::Type)] +#[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone)] pub enum HostRole { Bootstrap, ControlPlane, diff --git a/harmony/src/infra/inventory/sqlite.rs b/harmony/src/infra/inventory/sqlite.rs index d626640..cd83df7 100644 --- a/harmony/src/infra/inventory/sqlite.rs +++ b/harmony/src/infra/inventory/sqlite.rs @@ -108,6 +108,7 @@ impl InventoryRepository for SqliteInventoryRepository { Ok(()) } + async fn get_host_for_role(&self, role: HostRole) -> Result, RepoError> { struct HostIdRow { host_id: String, diff --git a/harmony/src/modules/inventory/discovery.rs b/harmony/src/modules/inventory/discovery.rs new file mode 100644 index 0000000..1cbb23d --- /dev/null +++ b/harmony/src/modules/inventory/discovery.rs @@ -0,0 +1,122 @@ +use async_trait::async_trait; +use harmony_types::id::Id; +use log::{error, info}; +use serde::{Deserialize, Serialize}; + +use crate::{ + data::Version, + hardware::PhysicalHost, + infra::inventory::InventoryRepositoryFactory, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::{HostRole, Inventory}, + modules::inventory::LaunchDiscoverInventoryAgentScore, + score::Score, + topology::Topology, +}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiscoverHostForRoleScore { + pub role: HostRole, +} + +impl Score for DiscoverHostForRoleScore { + fn name(&self) -> String { + "DiscoverInventoryAgentScore".to_string() + } + + fn create_interpret(&self) -> Box> { + Box::new(DiscoverHostForRoleInterpret { + score: self.clone(), + }) + } +} + +#[derive(Debug)] +pub struct DiscoverHostForRoleInterpret { + score: DiscoverHostForRoleScore, +} + +#[async_trait] +impl Interpret for DiscoverHostForRoleInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { + info!( + "Launching discovery agent, make sure that your nodes are successfully PXE booted and running inventory agent. They should answer on `http://:8080/inventory`" + ); + LaunchDiscoverInventoryAgentScore { + discovery_timeout: None, + } + .interpret(inventory, topology) + .await?; + + let host: PhysicalHost; + let host_repo = InventoryRepositoryFactory::build().await?; + + loop { + let all_hosts = host_repo.get_all_hosts().await?; + + if all_hosts.is_empty() { + info!("No discovered hosts found yet. Waiting for hosts to appear..."); + // Sleep to avoid spamming the user and logs while waiting for nodes. + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + continue; + } + + let ans = inquire::Select::new( + &format!("Select the node to be used for role {:?}:", self.score.role), + all_hosts, + ) + .with_help_message("Press Esc to refresh the list of discovered hosts") + .prompt(); + + match ans { + Ok(choice) => { + info!("Selected {} as the bootstrap node.", choice.summary()); + host_repo + .save_role_mapping(&HostRole::Bootstrap, &choice) + .await?; + host = choice; + break; + } + Err(inquire::InquireError::OperationCanceled) => { + info!("Refresh requested. Fetching list of discovered hosts again..."); + continue; + } + Err(e) => { + error!( + "Failed to select node for role {:?} : {}", + self.score.role, e + ); + return Err(InterpretError::new(format!( + "Could not select host : {}", + e.to_string() + ))); + } + } + } + + Ok(Outcome::success(format!( + "Successfully discovered host {} for role {:?}", + host.summary(), + self.score.role + ))) + } + fn get_name(&self) -> InterpretName { + InterpretName::Custom("DiscoverHostForRoleScore") + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} diff --git a/harmony/src/modules/inventory/mod.rs b/harmony/src/modules/inventory/mod.rs index 85e0853..a0f6443 100644 --- a/harmony/src/modules/inventory/mod.rs +++ b/harmony/src/modules/inventory/mod.rs @@ -1,3 +1,6 @@ +mod discovery; +pub use discovery::*; + use async_trait::async_trait; use harmony_inventory_agent::local_presence::DiscoveryEvent; use log::{debug, info, trace}; diff --git a/harmony/src/modules/okd/bootstrap_01_prepare.rs b/harmony/src/modules/okd/bootstrap_01_prepare.rs new file mode 100644 index 0000000..70a0b1a --- /dev/null +++ b/harmony/src/modules/okd/bootstrap_01_prepare.rs @@ -0,0 +1,120 @@ +use async_trait::async_trait; +use derive_new::new; +use harmony_types::id::Id; +use log::{error, info, warn}; +use serde::Serialize; + +use crate::{ + data::Version, + hardware::PhysicalHost, + infra::inventory::InventoryRepositoryFactory, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::{HostRole, Inventory}, + modules::inventory::{DiscoverHostForRoleScore, LaunchDiscoverInventoryAgentScore}, + score::Score, + topology::HAClusterTopology, +}; +// ------------------------------------------------------------------------------------------------- +// 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)] +pub struct OKDSetup01InventoryScore {} + +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)] +pub 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_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: &HAClusterTopology, + ) -> Result { + info!("Setting up base DNS config for OKD"); + let cluster_domain = &topology.domain_name; + let load_balancer_ip = &topology.load_balancer.get_ip(); + inquire::Confirm::new(&format!( + "Set hostnames manually in your opnsense dnsmasq config : +*.apps.{cluster_domain} -> {load_balancer_ip} +api.{cluster_domain} -> {load_balancer_ip} +api-int.{cluster_domain} -> {load_balancer_ip} + +When you can dig them, confirm to continue. +" + )) + .prompt() + .expect("Prompt error"); + // TODO reactivate automatic dns config when migration from unbound to dnsmasq is done + // OKDDnsScore::new(topology) + // .interpret(inventory, topology) + // .await?; + + // TODO refactor this section into a function discover_hosts_for_role(...) that can be used + // from anywhere in the project, not a member of this struct + + let mut bootstrap_host: Option = None; + let repo = InventoryRepositoryFactory::build().await?; + + while bootstrap_host.is_none() { + let hosts = repo.get_host_for_role(HostRole::Bootstrap).await?; + bootstrap_host = hosts.into_iter().next().to_owned(); + DiscoverHostForRoleScore { + role: HostRole::Bootstrap, + } + .interpret(inventory, topology) + .await?; + } + + Ok(Outcome::new( + InterpretStatus::SUCCESS, + format!( + "Found and assigned bootstrap node: {}", + bootstrap_host.unwrap().summary() + ), + )) + } +} diff --git a/harmony/src/modules/okd/bootstrap_02_bootstrap.rs b/harmony/src/modules/okd/bootstrap_02_bootstrap.rs new file mode 100644 index 0000000..b2bde35 --- /dev/null +++ b/harmony/src/modules/okd/bootstrap_02_bootstrap.rs @@ -0,0 +1,387 @@ +use std::{fmt::Write, path::PathBuf}; + +use async_trait::async_trait; +use derive_new::new; +use harmony_secret::SecretManager; +use harmony_types::id::Id; +use log::{debug, error, info, warn}; +use serde::{Deserialize, Serialize}; +use tokio::{fs::File, io::AsyncWriteExt, process::Command}; + +use crate::{ + config::secret::{RedhatSecret, SshKeyPair}, + data::{FileContent, FilePath, Version}, + hardware::PhysicalHost, + infra::inventory::InventoryRepositoryFactory, + instrumentation::{HarmonyEvent, instrument}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::{HostRole, Inventory}, + modules::{ + dhcp::DhcpHostBindingScore, + http::{IPxeMacBootFileScore, StaticFilesHttpScore}, + inventory::LaunchDiscoverInventoryAgentScore, + okd::{ + bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, + templates::{BootstrapIpxeTpl, InstallConfigYaml}, + }, + }, + score::Score, + topology::{HAClusterTopology, HostBinding}, +}; +// ------------------------------------------------------------------------------------------------- +// 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)] +pub struct OKDSetup02BootstrapScore {} + +impl Score for OKDSetup02BootstrapScore { + fn create_interpret(&self) -> Box> { + Box::new(OKDSetup02BootstrapInterpret::new()) + } + + fn name(&self) -> String { + "OKDSetup02BootstrapScore".to_string() + } +} + +#[derive(Debug, Clone)] +pub struct OKDSetup02BootstrapInterpret { + version: Version, + status: InterpretStatus, +} + +impl OKDSetup02BootstrapInterpret { + pub fn new() -> Self { + let version = Version::from("1.0.0").unwrap(); + Self { + version, + status: InterpretStatus::QUEUED, + } + } + + async fn get_bootstrap_node(&self) -> Result { + let repo = InventoryRepositoryFactory::build().await?; + match repo + .get_host_for_role(HostRole::Bootstrap) + .await? + .into_iter() + .next() + { + Some(host) => Ok(host), + None => Err(InterpretError::new( + "No bootstrap node available".to_string(), + )), + } + } + + async fn prepare_ignition_files( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result<(), InterpretError> { + let okd_bin_path = PathBuf::from("./data/okd/bin"); + let okd_installation_path_str = + format!("./data/okd/installation_files_{}", inventory.location.name); + let okd_images_path = &PathBuf::from("./data/okd/installer_image/"); + let okd_installation_path = &PathBuf::from(okd_installation_path_str); + + let exit_status = Command::new("mkdir") + .arg("-p") + .arg(okd_installation_path) + .spawn() + .expect("Command failed to start") + .wait() + .await + .map_err(|e| { + InterpretError::new(format!("Failed to create okd installation directory : {e}")) + })?; + if !exit_status.success() { + return Err(InterpretError::new(format!( + "Failed to create okd installation directory" + ))); + } else { + info!( + "Created OKD installation directory {}", + okd_installation_path.to_string_lossy() + ); + } + + let redhat_secret = SecretManager::get_or_prompt::().await?; + let ssh_key = SecretManager::get_or_prompt::().await?; + + let install_config_yaml = InstallConfigYaml { + cluster_name: &topology.get_cluster_name(), + cluster_domain: &topology.get_cluster_base_domain(), + pull_secret: &redhat_secret.pull_secret, + ssh_public_key: &ssh_key.public, + } + .to_string(); + + let install_config_file_path = &okd_installation_path.join("install-config.yaml"); + + self.create_file(install_config_file_path, install_config_yaml.as_bytes()) + .await?; + + let install_config_backup_extension = install_config_file_path + .extension() + .map(|e| format!("{}.bak", e.to_string_lossy())) + .unwrap_or("bak".to_string()); + + let mut install_config_backup = install_config_file_path.clone(); + install_config_backup.set_extension(install_config_backup_extension); + + self.create_file(&install_config_backup, install_config_yaml.as_bytes()) + .await?; + + info!("Creating manifest files with openshift-install"); + let output = Command::new(okd_bin_path.join("openshift-install")) + .args([ + "create", + "manifests", + "--dir", + okd_installation_path.to_str().unwrap(), + ]) + .output() + .await + .map_err(|e| InterpretError::new(format!("Failed to create okd manifest : {e}")))?; + let stdout = String::from_utf8(output.stdout).unwrap(); + info!("openshift-install stdout :\n\n{}", stdout); + let stderr = String::from_utf8(output.stderr).unwrap(); + info!("openshift-install stderr :\n\n{}", stderr); + info!("openshift-install exit status : {}", output.status); + if !output.status.success() { + return Err(InterpretError::new(format!( + "Failed to create okd manifest, exit code {} : {}", + output.status, stderr + ))); + } + + info!("Creating ignition files with openshift-install"); + let output = Command::new(okd_bin_path.join("openshift-install")) + .args([ + "create", + "ignition-configs", + "--dir", + okd_installation_path.to_str().unwrap(), + ]) + .output() + .await + .map_err(|e| { + InterpretError::new(format!("Failed to create okd ignition config : {e}")) + })?; + let stdout = String::from_utf8(output.stdout).unwrap(); + info!("openshift-install stdout :\n\n{}", stdout); + let stderr = String::from_utf8(output.stderr).unwrap(); + info!("openshift-install stderr :\n\n{}", stderr); + info!("openshift-install exit status : {}", output.status); + if !output.status.success() { + return Err(InterpretError::new(format!( + "Failed to create okd manifest, exit code {} : {}", + output.status, stderr + ))); + } + + let ignition_files_http_path = PathBuf::from("okd_ignition_files"); + let prepare_file_content = async |filename: &str| -> Result { + let local_path = okd_installation_path.join(filename); + let remote_path = ignition_files_http_path.join(filename); + + info!( + "Preparing file content for local file : {} to remote : {}", + local_path.to_string_lossy(), + remote_path.to_string_lossy() + ); + + let content = tokio::fs::read_to_string(&local_path).await.map_err(|e| { + InterpretError::new(format!( + "Could not read file content {} : {e}", + local_path.to_string_lossy() + )) + })?; + + Ok(FileContent { + path: FilePath::Relative(remote_path.to_string_lossy().to_string()), + content, + }) + }; + + StaticFilesHttpScore { + remote_path: None, + folder_to_serve: None, + files: vec![ + prepare_file_content("bootstrap.ign").await?, + prepare_file_content("master.ign").await?, + prepare_file_content("worker.ign").await?, + prepare_file_content("metadata.json").await?, + ], + } + .interpret(inventory, topology) + .await?; + + info!("Successfully prepared ignition files for OKD installation"); + // ignition_files_http_path // = PathBuf::from("okd_ignition_files"); + info!( + r#"Uploading images, they can be refreshed with a command similar to this one: openshift-install coreos print-stream-json | grep -Eo '"https.*(kernel.|initramfs.|rootfs.)\w+(\.img)?"' | grep x86_64 | xargs -n 1 curl -LO"# + ); + + inquire::Confirm::new( + &format!("push installer image files with `scp -r {}/* root@{}:/usr/local/http/scos/` until performance issue is resolved", okd_images_path.to_string_lossy(), topology.http_server.get_ip())).prompt().expect("Prompt error"); + + // let scos_http_path = PathBuf::from("scos"); + // StaticFilesHttpScore { + // folder_to_serve: Some(Url::LocalFolder( + // okd_images_path.to_string_lossy().to_string(), + // )), + // remote_path: Some(scos_http_path.to_string_lossy().to_string()), + // files: vec![], + // } + // .interpret(inventory, topology) + // .await?; + + Ok(()) + } + + async fn configure_host_binding( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result<(), InterpretError> { + let binding = HostBinding { + logical_host: topology.bootstrap_host.clone(), + physical_host: self.get_bootstrap_node().await?, + }; + info!("Configuring host binding for bootstrap node {binding:?}"); + + DhcpHostBindingScore { + host_binding: vec![binding], + domain: Some(topology.domain_name.clone()), + } + .interpret(inventory, topology) + .await?; + Ok(()) + } + + async fn render_per_mac_pxe( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result<(), InterpretError> { + let content = BootstrapIpxeTpl { + http_ip: &topology.http_server.get_ip().to_string(), + scos_path: "scos", // TODO use some constant + ignition_http_path: "okd_ignition_files", // TODO use proper variable + installation_device: "/dev/sda", + ignition_file_name: "bootstrap.ign", + } + .to_string(); + + let bootstrap_node = self.get_bootstrap_node().await?; + let mac_address = bootstrap_node.get_mac_address(); + + info!("[Bootstrap] Rendering per-MAC PXE for bootstrap node"); + debug!("bootstrap ipxe content : {content}"); + debug!("bootstrap mac addresses : {mac_address:?}"); + + IPxeMacBootFileScore { + mac_address, + content, + } + .interpret(inventory, topology) + .await?; + Ok(()) + } + + async fn setup_bootstrap_load_balancer( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result<(), InterpretError> { + let outcome = OKDBootstrapLoadBalancerScore::new(topology) + .interpret(inventory, topology) + .await?; + info!("Successfully executed OKDBootstrapLoadBalancerScore : {outcome:?}"); + Ok(()) + } + + async fn reboot_target(&self) -> Result<(), InterpretError> { + // Placeholder: ssh reboot using the inventory ephemeral key + info!("[Bootstrap] Rebooting bootstrap node via SSH"); + // TODO reboot programatically, there are some logical checks and refactoring to do such as + // accessing the bootstrap node config (ip address) from the inventory + let confirmation = inquire::Confirm::new( + "Now reboot the bootstrap node so it picks up its pxe boot file. Press enter when ready.", + ) + .prompt() + .expect("Unexpected prompt error"); + Ok(()) + } + + async fn wait_for_bootstrap_complete(&self) -> Result<(), InterpretError> { + // Placeholder: wait-for bootstrap-complete + info!("[Bootstrap] Waiting for bootstrap-complete …"); + todo!("[Bootstrap] Waiting for bootstrap-complete …") + } + + async fn create_file(&self, path: &PathBuf, content: &[u8]) -> Result<(), InterpretError> { + let mut install_config_file = File::create(path).await.map_err(|e| { + InterpretError::new(format!( + "Could not create file {} : {e}", + path.to_string_lossy() + )) + })?; + install_config_file.write(content).await.map_err(|e| { + InterpretError::new(format!( + "Could not write file {} : {e}", + path.to_string_lossy() + )) + })?; + 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: &HAClusterTopology, + ) -> Result { + self.configure_host_binding(inventory, topology).await?; + self.prepare_ignition_files(inventory, topology).await?; + self.render_per_mac_pxe(inventory, topology).await?; + self.setup_bootstrap_load_balancer(inventory, topology) + .await?; + + // TODO https://docs.okd.io/latest/installing/installing_bare_metal/upi/installing-bare-metal.html#installation-user-provisioned-validating-dns_installing-bare-metal + // self.validate_dns_config(inventory, topology).await?; + + self.reboot_target().await?; + self.wait_for_bootstrap_complete().await?; + + Ok(Outcome::new( + InterpretStatus::SUCCESS, + "Bootstrap phase complete".into(), + )) + } +} diff --git a/harmony/src/modules/okd/bootstrap_03_control_plane.rs b/harmony/src/modules/okd/bootstrap_03_control_plane.rs new file mode 100644 index 0000000..1739aea --- /dev/null +++ b/harmony/src/modules/okd/bootstrap_03_control_plane.rs @@ -0,0 +1,195 @@ +use std::{fmt::Write, path::PathBuf}; + +use async_trait::async_trait; +use derive_new::new; +use harmony_secret::SecretManager; +use harmony_types::id::Id; +use log::{debug, error, info, warn}; +use serde::{Deserialize, Serialize}; +use tokio::{fs::File, io::AsyncWriteExt, process::Command}; + +use crate::{ + config::secret::{RedhatSecret, SshKeyPair}, + data::{FileContent, FilePath, Version}, + hardware::PhysicalHost, + infra::inventory::InventoryRepositoryFactory, + instrumentation::{HarmonyEvent, instrument}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::{HostRole, Inventory}, + modules::{ + dhcp::DhcpHostBindingScore, + http::{IPxeMacBootFileScore, StaticFilesHttpScore}, + inventory::{DiscoverHostForRoleScore, LaunchDiscoverInventoryAgentScore}, + okd::{ + bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, + templates::{BootstrapIpxeTpl, InstallConfigYaml}, + }, + }, + score::Score, + topology::{HAClusterTopology, HostBinding}, +}; +// ------------------------------------------------------------------------------------------------- +// 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)] +pub 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)] +pub 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 configure_host_binding( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + nodes: &Vec, + ) -> Result<(), InterpretError> { + let binding = HostBinding { + logical_host: topology.bootstrap_host.clone(), + physical_host: self.get_bootstrap_node().await?, + }; + info!("Configuring host binding for bootstrap node {binding:?}"); + + DhcpHostBindingScore { + host_binding: vec![binding], + domain: Some(topology.domain_name.clone()), + } + .interpret(inventory, topology) + .await?; + Ok(()) + } + + async fn configure_ipxe( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + nodes: &Vec, + ) -> Result<(), InterpretError> { + info!("[ControlPlane] Rendering per-MAC PXE"); + let content = BootstrapIpxeTpl { + http_ip: &topology.http_server.get_ip().to_string(), + scos_path: "scos", // TODO use some constant + ignition_http_path: "okd_ignition_files", // TODO use proper variable + installation_device: "/dev/sda", + ignition_file_name: "bootstrap.ign", + } + .to_string(); + + let bootstrap_node = self.get_nodes().await?; + let mac_address = bootstrap_node.get_mac_address(); + + info!("[Bootstrap] Rendering per-MAC PXE for bootstrap node"); + debug!("bootstrap ipxe content : {content}"); + debug!("bootstrap mac addresses : {mac_address:?}"); + + IPxeMacBootFileScore { + mac_address, + content, + } + .interpret(inventory, topology) + .await?; + 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"); + inquire::Confirm::new( + "Network configuration for control plane nodes is not automated yet, configure it manually now.", + ) + .prompt() + .expect("Unexpected prompt error"); + + Ok(()) + } + + async fn get_nodes( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result, InterpretError> { + let repo = InventoryRepositoryFactory::build().await?; + let mut control_plane_hosts = repo.get_host_for_role(HostRole::ControlPlane).await?; + + while control_plane_hosts.len() < 3 { + info!( + "Discovery of 3 control plane hosts in progress, current number {}", + control_plane_hosts.len() + ); + DiscoverHostForRoleScore { + role: HostRole::ControlPlane, + } + .interpret(inventory, topology) + .await?; + control_plane_hosts = repo.get_host_for_role(HostRole::ControlPlane).await?; + } + + if control_plane_hosts.len() < 3 { + Err(InterpretError::new(format!( + "OKD Requires at least 3 hosts, got {}, cannot proceed", + control_plane_hosts.len() + ))) + } else { + Ok(control_plane_hosts) + } + } +} + +#[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: &HAClusterTopology, + ) -> Result { + let nodes = self.get_nodes(inventory, topology).await?; + // TODO add relevant methods here + self.persist_network_bond().await?; + Ok(Outcome::new( + InterpretStatus::SUCCESS, + "Control plane provisioned".into(), + )) + } +} diff --git a/harmony/src/modules/okd/bootstrap_04_workers.rs b/harmony/src/modules/okd/bootstrap_04_workers.rs new file mode 100644 index 0000000..d5ed87c --- /dev/null +++ b/harmony/src/modules/okd/bootstrap_04_workers.rs @@ -0,0 +1,102 @@ +use std::{fmt::Write, path::PathBuf}; + +use async_trait::async_trait; +use derive_new::new; +use harmony_secret::SecretManager; +use harmony_types::id::Id; +use log::{debug, error, info, warn}; +use serde::{Deserialize, Serialize}; +use tokio::{fs::File, io::AsyncWriteExt, process::Command}; + +use crate::{ + config::secret::{RedhatSecret, SshKeyPair}, + data::{FileContent, FilePath, Version}, + hardware::PhysicalHost, + infra::inventory::InventoryRepositoryFactory, + instrumentation::{HarmonyEvent, instrument}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::{HostRole, Inventory}, + modules::{ + dhcp::DhcpHostBindingScore, + http::{IPxeMacBootFileScore, StaticFilesHttpScore}, + inventory::LaunchDiscoverInventoryAgentScore, + okd::{ + bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, + templates::{BootstrapIpxeTpl, InstallConfigYaml}, + }, + }, + score::Score, + topology::{HAClusterTopology, HostBinding}, +}; +// ------------------------------------------------------------------------------------------------- +// 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)] +pub 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)] +pub 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: &HAClusterTopology, + ) -> Result { + self.render_and_reboot().await?; + Ok(Outcome::new( + InterpretStatus::SUCCESS, + "Workers provisioned".into(), + )) + } +} diff --git a/harmony/src/modules/okd/bootstrap_05_sanity_check.rs b/harmony/src/modules/okd/bootstrap_05_sanity_check.rs new file mode 100644 index 0000000..f1a4c2a --- /dev/null +++ b/harmony/src/modules/okd/bootstrap_05_sanity_check.rs @@ -0,0 +1,101 @@ +use std::{fmt::Write, path::PathBuf}; + +use async_trait::async_trait; +use derive_new::new; +use harmony_secret::SecretManager; +use harmony_types::id::Id; +use log::{debug, error, info, warn}; +use serde::{Deserialize, Serialize}; +use tokio::{fs::File, io::AsyncWriteExt, process::Command}; + +use crate::{ + config::secret::{RedhatSecret, SshKeyPair}, + data::{FileContent, FilePath, Version}, + hardware::PhysicalHost, + infra::inventory::InventoryRepositoryFactory, + instrumentation::{HarmonyEvent, instrument}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::{HostRole, Inventory}, + modules::{ + dhcp::DhcpHostBindingScore, + http::{IPxeMacBootFileScore, StaticFilesHttpScore}, + inventory::LaunchDiscoverInventoryAgentScore, + okd::{ + bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, + templates::{BootstrapIpxeTpl, InstallConfigYaml}, + }, + }, + score::Score, + topology::{HAClusterTopology, HostBinding}, +}; +// ------------------------------------------------------------------------------------------------- +// Step 05: Sanity Check +// - Validate API reachability, ClusterOperators, ingress, and SDN status. +// ------------------------------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, new)] +pub 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)] +pub 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: &HAClusterTopology, + ) -> Result { + self.run_checks().await?; + Ok(Outcome::new( + InterpretStatus::SUCCESS, + "Sanity checks passed".into(), + )) + } +} diff --git a/harmony/src/modules/okd/bootstrap_06_installation_report.rs b/harmony/src/modules/okd/bootstrap_06_installation_report.rs new file mode 100644 index 0000000..792d567 --- /dev/null +++ b/harmony/src/modules/okd/bootstrap_06_installation_report.rs @@ -0,0 +1,101 @@ +// ------------------------------------------------------------------------------------------------- +use std::{fmt::Write, path::PathBuf}; +use async_trait::async_trait; +use derive_new::new; +use harmony_secret::SecretManager; +use harmony_types::id::Id; +use log::{debug, error, info, warn}; +use serde::{Deserialize, Serialize}; +use tokio::{fs::File, io::AsyncWriteExt, process::Command}; + +use crate::{ + config::secret::{RedhatSecret, SshKeyPair}, + data::{FileContent, FilePath, Version}, + hardware::PhysicalHost, + infra::inventory::InventoryRepositoryFactory, + instrumentation::{HarmonyEvent, instrument}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::{HostRole, Inventory}, + modules::{ + dhcp::DhcpHostBindingScore, + http::{IPxeMacBootFileScore, StaticFilesHttpScore}, + inventory::LaunchDiscoverInventoryAgentScore, + okd::{ + bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, + templates::{BootstrapIpxeTpl, InstallConfigYaml}, + }, + }, + score::Score, + topology::{HAClusterTopology, HostBinding}, +}; + +// Step 06: Installation Report +// - Emit JSON and concise human summary of nodes, roles, versions, and health. +// ------------------------------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, new)] +pub struct OKDSetup06InstallationReportScore {} + +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)] +pub 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 OKD installation report",); + 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: &HAClusterTopology, + ) -> Result { + self.generate().await?; + Ok(Outcome::new( + InterpretStatus::SUCCESS, + "Installation report generated".into(), + )) + } +} diff --git a/harmony/src/modules/okd/installation.rs b/harmony/src/modules/okd/installation.rs index a169f62..72603c8 100644 --- a/harmony/src/modules/okd/installation.rs +++ b/harmony/src/modules/okd/installation.rs @@ -47,981 +47,27 @@ //! - public_domain: External wildcard/apps domain (e.g., apps.example.com). //! - internal_domain: Internal cluster domain (e.g., cluster.local or harmony.mcd). -use std::{fmt::Write, path::PathBuf}; - -use async_trait::async_trait; -use derive_new::new; -use harmony_secret::SecretManager; -use harmony_types::id::Id; -use log::{debug, error, info, warn}; -use serde::{Deserialize, Serialize}; -use tokio::{fs::File, io::AsyncWriteExt, process::Command}; - use crate::{ - config::secret::{RedhatSecret, SshKeyPair}, - data::{FileContent, FilePath, Version}, - hardware::PhysicalHost, - infra::inventory::InventoryRepositoryFactory, - instrumentation::{HarmonyEvent, instrument}, - interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, - inventory::{HostRole, Inventory}, - modules::{ - dhcp::DhcpHostBindingScore, - http::{IPxeMacBootFileScore, StaticFilesHttpScore}, - inventory::LaunchDiscoverInventoryAgentScore, - okd::{ - bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, - templates::{BootstrapIpxeTpl, InstallConfigYaml}, - }, + modules::okd::{ + OKDSetup01InventoryScore, OKDSetup02BootstrapScore, OKDSetup03ControlPlaneScore, + OKDSetup04WorkersScore, OKDSetup05SanityCheckScore, + bootstrap_06_installation_report::OKDSetup06InstallationReportScore, }, score::Score, - topology::{HAClusterTopology, HostBinding}, + topology::HAClusterTopology, }; -// ------------------------------------------------------------------------------------------------- -// Public Orchestrator Score -// ------------------------------------------------------------------------------------------------- +pub struct OKDInstallationPipeline; -#[derive(Debug, Clone, Serialize, Deserialize, new)] -pub struct OKDInstallationScore {} - -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: &HAClusterTopology, - ) -> Result<(), InterpretError> { - OKDSetup01InventoryScore::new() - .interpret(inventory, topology) - .await?; - Ok(()) - } - - async fn run_bootstrap_phase( - &self, - inventory: &Inventory, - topology: &HAClusterTopology, - ) -> Result<(), InterpretError> { - OKDSetup02BootstrapScore::new() - .interpret(inventory, topology) - .await?; - Ok(()) - } - - async fn run_control_plane_phase( - &self, - inventory: &Inventory, - topology: &HAClusterTopology, - ) -> 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: &HAClusterTopology, - ) -> Result<(), InterpretError> { - let workers_score = OKDSetup04WorkersScore::new(); - workers_score.interpret(inventory, topology).await?; - Ok(()) - } - - async fn run_sanity_phase( - &self, - inventory: &Inventory, - topology: &HAClusterTopology, - ) -> Result<(), InterpretError> { - let sanity_score = OKDSetup05SanityCheckScore::new(); - sanity_score.interpret(inventory, topology).await?; - Ok(()) - } - - async fn run_report_phase( - &self, - inventory: &Inventory, - topology: &HAClusterTopology, - ) -> Result<(), InterpretError> { - let report_score = OKDSetup06InstallationReportScore::new(); - 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: &HAClusterTopology, - ) -> Result { - instrument(HarmonyEvent::HarmonyStarted).ok(); - - info!("Starting OKD installation pipeline",); - - self.run_inventory_phase(inventory, topology).await?; - - self.run_bootstrap_phase(inventory, topology).await?; - - self.run_control_plane_phase(inventory, topology).await?; - - self.run_workers_phase(inventory, topology).await?; - - self.run_sanity_phase(inventory, topology).await?; - - self.run_report_phase(inventory, topology).await?; - - instrument(HarmonyEvent::HarmonyFinished).ok(); - - Ok(Outcome::new( - InterpretStatus::SUCCESS, - "OKD installation pipeline completed".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 {} - -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_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: &HAClusterTopology, - ) -> Result { - info!("Setting up base DNS config for OKD"); - let cluster_domain = &topology.domain_name; - let load_balancer_ip = &topology.load_balancer.get_ip(); - inquire::Confirm::new(&format!( - "Set hostnames manually in your opnsense dnsmasq config : -*.apps.{cluster_domain} -> {load_balancer_ip} -api.{cluster_domain} -> {load_balancer_ip} -api-int.{cluster_domain} -> {load_balancer_ip} - -When you can dig them, confirm to continue. -" - )) - .prompt() - .expect("Prompt error"); - // TODO reactivate automatic dns config when migration from unbound to dnsmasq is done - // OKDDnsScore::new(topology) - // .interpret(inventory, topology) - // .await?; - - info!( - "Launching discovery agent, make sure that your nodes are successfully PXE booted and running inventory agent. They should answer on `http://:8080/inventory`" - ); - LaunchDiscoverInventoryAgentScore { - discovery_timeout: None, - } - .interpret(inventory, topology) - .await?; - - let bootstrap_host: PhysicalHost; - let host_repo = InventoryRepositoryFactory::build().await?; - - loop { - let all_hosts = host_repo.get_all_hosts().await?; - - if all_hosts.is_empty() { - warn!("No discovered hosts found yet. Waiting for hosts to appear..."); - // Sleep to avoid spamming the user and logs while waiting for nodes. - tokio::time::sleep(std::time::Duration::from_secs(3)).await; - continue; - } - - let ans = inquire::Select::new( - "Select the node to be used as the bootstrap node:", - all_hosts, - ) - .with_help_message("Press Esc to refresh the list of discovered hosts") - .prompt(); - - match ans { - Ok(choice) => { - info!("Selected {} as the bootstrap node.", choice.summary()); - host_repo - .save_role_mapping(&HostRole::Bootstrap, &choice) - .await?; - bootstrap_host = choice; - break; - } - Err(inquire::InquireError::OperationCanceled) => { - info!("Refresh requested. Fetching list of discovered hosts again..."); - continue; - } - Err(e) => { - error!("Failed to select bootstrap node: {}", e); - return Err(InterpretError::new(format!( - "Could not select host : {}", - e.to_string() - ))); - } - } - } - - Ok(Outcome::new( - InterpretStatus::SUCCESS, - format!( - "Found and assigned bootstrap node: {}", - bootstrap_host.summary() - ), - )) - } -} - -// ------------------------------------------------------------------------------------------------- -// 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 {} - -impl Score for OKDSetup02BootstrapScore { - fn create_interpret(&self) -> Box> { - Box::new(OKDSetup02BootstrapInterpret::new()) - } - - fn name(&self) -> String { - "OKDSetup02BootstrapScore".to_string() - } -} - -#[derive(Debug, Clone)] -struct OKDSetup02BootstrapInterpret { - version: Version, - status: InterpretStatus, -} - -impl OKDSetup02BootstrapInterpret { - pub fn new() -> Self { - let version = Version::from("1.0.0").unwrap(); - Self { - version, - status: InterpretStatus::QUEUED, - } - } - - async fn get_bootstrap_node(&self) -> Result { - let repo = InventoryRepositoryFactory::build().await?; - match repo - .get_host_for_role(HostRole::Bootstrap) - .await? - .into_iter() - .next() - { - Some(host) => Ok(host), - None => Err(InterpretError::new( - "No bootstrap node available".to_string(), - )), - } - } - - async fn prepare_ignition_files( - &self, - inventory: &Inventory, - topology: &HAClusterTopology, - ) -> Result<(), InterpretError> { - let okd_bin_path = PathBuf::from("./data/okd/bin"); - let okd_installation_path_str = - format!("./data/okd/installation_files_{}", inventory.location.name); - let okd_images_path = &PathBuf::from("./data/okd/installer_image/"); - let okd_installation_path = &PathBuf::from(okd_installation_path_str); - - let exit_status = Command::new("mkdir") - .arg("-p") - .arg(okd_installation_path) - .spawn() - .expect("Command failed to start") - .wait() - .await - .map_err(|e| { - InterpretError::new(format!("Failed to create okd installation directory : {e}")) - })?; - if !exit_status.success() { - return Err(InterpretError::new(format!( - "Failed to create okd installation directory" - ))); - } else { - info!( - "Created OKD installation directory {}", - okd_installation_path.to_string_lossy() - ); - } - - let redhat_secret = SecretManager::get_or_prompt::().await?; - let ssh_key = SecretManager::get_or_prompt::().await?; - - let install_config_yaml = InstallConfigYaml { - cluster_name: &topology.get_cluster_name(), - cluster_domain: &topology.get_cluster_base_domain(), - pull_secret: &redhat_secret.pull_secret, - ssh_public_key: &ssh_key.public, - } - .to_string(); - - let install_config_file_path = &okd_installation_path.join("install-config.yaml"); - - self.create_file(install_config_file_path, install_config_yaml.as_bytes()) - .await?; - - let install_config_backup_extension = install_config_file_path - .extension() - .map(|e| format!("{}.bak", e.to_string_lossy())) - .unwrap_or("bak".to_string()); - - let mut install_config_backup = install_config_file_path.clone(); - install_config_backup.set_extension(install_config_backup_extension); - - self.create_file(&install_config_backup, install_config_yaml.as_bytes()) - .await?; - - info!("Creating manifest files with openshift-install"); - let output = Command::new(okd_bin_path.join("openshift-install")) - .args([ - "create", - "manifests", - "--dir", - okd_installation_path.to_str().unwrap(), - ]) - .output() - .await - .map_err(|e| InterpretError::new(format!("Failed to create okd manifest : {e}")))?; - let stdout = String::from_utf8(output.stdout).unwrap(); - info!("openshift-install stdout :\n\n{}", stdout); - let stderr = String::from_utf8(output.stderr).unwrap(); - info!("openshift-install stderr :\n\n{}", stderr); - info!("openshift-install exit status : {}", output.status); - if !output.status.success() { - return Err(InterpretError::new(format!( - "Failed to create okd manifest, exit code {} : {}", - output.status, stderr - ))); - } - - info!("Creating ignition files with openshift-install"); - let output = Command::new(okd_bin_path.join("openshift-install")) - .args([ - "create", - "ignition-configs", - "--dir", - okd_installation_path.to_str().unwrap(), - ]) - .output() - .await - .map_err(|e| { - InterpretError::new(format!("Failed to create okd ignition config : {e}")) - })?; - let stdout = String::from_utf8(output.stdout).unwrap(); - info!("openshift-install stdout :\n\n{}", stdout); - let stderr = String::from_utf8(output.stderr).unwrap(); - info!("openshift-install stderr :\n\n{}", stderr); - info!("openshift-install exit status : {}", output.status); - if !output.status.success() { - return Err(InterpretError::new(format!( - "Failed to create okd manifest, exit code {} : {}", - output.status, stderr - ))); - } - - let ignition_files_http_path = PathBuf::from("okd_ignition_files"); - let prepare_file_content = async |filename: &str| -> Result { - let local_path = okd_installation_path.join(filename); - let remote_path = ignition_files_http_path.join(filename); - - info!( - "Preparing file content for local file : {} to remote : {}", - local_path.to_string_lossy(), - remote_path.to_string_lossy() - ); - - let content = tokio::fs::read_to_string(&local_path).await.map_err(|e| { - InterpretError::new(format!( - "Could not read file content {} : {e}", - local_path.to_string_lossy() - )) - })?; - - Ok(FileContent { - path: FilePath::Relative(remote_path.to_string_lossy().to_string()), - content, - }) - }; - - StaticFilesHttpScore { - remote_path: None, - folder_to_serve: None, - files: vec![ - prepare_file_content("bootstrap.ign").await?, - prepare_file_content("master.ign").await?, - prepare_file_content("worker.ign").await?, - prepare_file_content("metadata.json").await?, - ], - } - .interpret(inventory, topology) - .await?; - - info!("Successfully prepared ignition files for OKD installation"); - // ignition_files_http_path // = PathBuf::from("okd_ignition_files"); - info!( - r#"Uploading images, they can be refreshed with a command similar to this one: openshift-install coreos print-stream-json | grep -Eo '"https.*(kernel.|initramfs.|rootfs.)\w+(\.img)?"' | grep x86_64 | xargs -n 1 curl -LO"# - ); - - inquire::Confirm::new( - &format!("push installer image files with `scp -r {}/* root@{}:/usr/local/http/scos/` until performance issue is resolved", okd_images_path.to_string_lossy(), topology.http_server.get_ip())).prompt().expect("Prompt error"); - - // let scos_http_path = PathBuf::from("scos"); - // StaticFilesHttpScore { - // folder_to_serve: Some(Url::LocalFolder( - // okd_images_path.to_string_lossy().to_string(), - // )), - // remote_path: Some(scos_http_path.to_string_lossy().to_string()), - // files: vec![], - // } - // .interpret(inventory, topology) - // .await?; - - Ok(()) - } - - async fn configure_host_binding( - &self, - inventory: &Inventory, - topology: &HAClusterTopology, - ) -> Result<(), InterpretError> { - let binding = HostBinding { - logical_host: topology.bootstrap_host.clone(), - physical_host: self.get_bootstrap_node().await?, - }; - info!("Configuring host binding for bootstrap node {binding:?}"); - - DhcpHostBindingScore { - host_binding: vec![binding], - domain: Some(topology.domain_name.clone()), - } - .interpret(inventory, topology) - .await?; - Ok(()) - } - - async fn render_per_mac_pxe( - &self, - inventory: &Inventory, - topology: &HAClusterTopology, - ) -> Result<(), InterpretError> { - let content = BootstrapIpxeTpl { - http_ip: &topology.http_server.get_ip().to_string(), - scos_path: "scos", // TODO use some constant - ignition_http_path: "okd_ignition_files", // TODO use proper variable - installation_device: "/dev/sda", - // TODO do something smart based on the host drives - // topology. Something like use the smallest device - // above 200G that is an ssd - } - .to_string(); - - let bootstrap_node = self.get_bootstrap_node().await?; - let mac_address = bootstrap_node.get_mac_address(); - - info!("[Bootstrap] Rendering per-MAC PXE for bootstrap node"); - debug!("bootstrap ipxe content : {content}"); - debug!("bootstrap mac addresses : {mac_address:?}"); - - IPxeMacBootFileScore { - mac_address, - content, - } - .interpret(inventory, topology) - .await?; - Ok(()) - } - - async fn setup_bootstrap_load_balancer( - &self, - inventory: &Inventory, - topology: &HAClusterTopology, - ) -> Result<(), InterpretError> { - let outcome = OKDBootstrapLoadBalancerScore::new(topology) - .interpret(inventory, topology) - .await?; - info!("Successfully executed OKDBootstrapLoadBalancerScore : {outcome:?}"); - Ok(()) - } - - async fn reboot_target(&self) -> Result<(), InterpretError> { - // Placeholder: ssh reboot using the inventory ephemeral key - info!("[Bootstrap] Rebooting bootstrap node via SSH"); - // TODO reboot programatically, there are some logical checks and refactoring to do such as - // accessing the bootstrap node config (ip address) from the inventory - let confirmation = inquire::Confirm::new( - "Now reboot the bootstrap node so it picks up its pxe boot file. Press enter when ready.", - ) - .with_default(true) - .prompt() - .expect("Unexpected prompt error"); - Ok(()) - } - - async fn wait_for_bootstrap_complete(&self) -> Result<(), InterpretError> { - // Placeholder: wait-for bootstrap-complete - info!("[Bootstrap] Waiting for bootstrap-complete …"); - todo!("[Bootstrap] Waiting for bootstrap-complete …") - } - - async fn create_file(&self, path: &PathBuf, content: &[u8]) -> Result<(), InterpretError> { - let mut install_config_file = File::create(path).await.map_err(|e| { - InterpretError::new(format!( - "Could not create file {} : {e}", - path.to_string_lossy() - )) - })?; - install_config_file.write(content).await.map_err(|e| { - InterpretError::new(format!( - "Could not write file {} : {e}", - path.to_string_lossy() - )) - })?; - 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: &HAClusterTopology, - ) -> Result { - self.configure_host_binding(inventory, topology).await?; - self.prepare_ignition_files(inventory, topology).await?; - self.render_per_mac_pxe(inventory, topology).await?; - self.setup_bootstrap_load_balancer(inventory, topology) - .await?; - - // TODO https://docs.okd.io/latest/installing/installing_bare_metal/upi/installing-bare-metal.html#installation-user-provisioned-validating-dns_installing-bare-metal - // self.validate_dns_config(inventory, topology).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: &HAClusterTopology, - ) -> 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: &HAClusterTopology, - ) -> 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: &HAClusterTopology, - ) -> 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 {} - -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 OKD installation report",); - 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: &HAClusterTopology, - ) -> Result { - self.generate().await?; - Ok(Outcome::new( - InterpretStatus::SUCCESS, - "Installation report generated".into(), - )) +impl OKDInstallationPipeline { + pub async fn get_all_scores() -> Vec>> { + vec![ + Box::new(OKDSetup01InventoryScore::new()), + Box::new(OKDSetup02BootstrapScore::new()), + Box::new(OKDSetup03ControlPlaneScore::new()), + Box::new(OKDSetup04WorkersScore::new()), + Box::new(OKDSetup05SanityCheckScore::new()), + Box::new(OKDSetup06InstallationReportScore::new()), + ] } } diff --git a/harmony/src/modules/okd/mod.rs b/harmony/src/modules/okd/mod.rs index f255959..1bd4514 100644 --- a/harmony/src/modules/okd/mod.rs +++ b/harmony/src/modules/okd/mod.rs @@ -1,3 +1,9 @@ +mod bootstrap_01_prepare; +mod bootstrap_02_bootstrap; +mod bootstrap_03_control_plane; +mod bootstrap_04_workers; +mod bootstrap_05_sanity_check; +mod bootstrap_06_installation_report; pub mod bootstrap_dhcp; pub mod bootstrap_load_balancer; pub mod dhcp; @@ -7,3 +13,9 @@ pub mod ipxe; pub mod load_balancer; pub mod templates; pub mod upgrade; +pub use bootstrap_01_prepare::*; +pub use bootstrap_02_bootstrap::*; +pub use bootstrap_03_control_plane::*; +pub use bootstrap_04_workers::*; +pub use bootstrap_05_sanity_check::*; +pub use bootstrap_06_installation_report::*; diff --git a/harmony/src/modules/okd/templates.rs b/harmony/src/modules/okd/templates.rs index 9aa3035..2e1494e 100644 --- a/harmony/src/modules/okd/templates.rs +++ b/harmony/src/modules/okd/templates.rs @@ -16,4 +16,5 @@ pub struct BootstrapIpxeTpl<'a> { pub scos_path: &'a str, pub installation_device: &'a str, pub ignition_http_path: &'a str, + pub ignition_file_name: &'static str, } diff --git a/harmony/templates/okd/bootstrap.ipxe.j2 b/harmony/templates/okd/bootstrap.ipxe.j2 index 7f1539c..79b6fa6 100644 --- a/harmony/templates/okd/bootstrap.ipxe.j2 +++ b/harmony/templates/okd/bootstrap.ipxe.j2 @@ -9,7 +9,7 @@ set http_ip {{ http_ip }} set scos_path {{ scos_path }} set inst_dev {{ installation_device }} set ign_path {{ ignition_http_path }} -set ign_file bootstrap.ign +set ign_file {{ ignition_file_name }} # --- Derived Variables --- set base-url http://${http_ip}:8080