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
Some checks failed
Run Check Script / check (pull_request) Failing after 27s
Some checks failed
Run Check Script / check (pull_request) Failing after 27s
This commit is contained in:
parent
e7ccfe6969
commit
8bcade27a1
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1759,6 +1759,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"harmony",
|
"harmony",
|
||||||
"harmony_macros",
|
"harmony_macros",
|
||||||
|
"harmony_secret",
|
||||||
"harmony_tui",
|
"harmony_tui",
|
||||||
"harmony_types",
|
"harmony_types",
|
||||||
"log",
|
"log",
|
||||||
|
@ -13,6 +13,7 @@ harmony_types = { path = "../../harmony_types" }
|
|||||||
cidr = { workspace = true }
|
cidr = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
harmony_macros = { path = "../../harmony_macros" }
|
||||||
|
harmony_secret = { path = "../../harmony_secret" }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
env_logger = { workspace = true }
|
env_logger = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
@ -5,10 +5,7 @@ use std::{
|
|||||||
|
|
||||||
use cidr::Ipv4Cidr;
|
use cidr::Ipv4Cidr;
|
||||||
use harmony::{
|
use harmony::{
|
||||||
hardware::{HostCategory, Location, PhysicalHost, SwitchGroup},
|
config::secret::SshKeyPair, data::{FileContent, FilePath}, hardware::{HostCategory, Location, PhysicalHost, SwitchGroup}, infra::opnsense::OPNSenseManagementInterface, inventory::Inventory, modules::{
|
||||||
infra::opnsense::OPNSenseManagementInterface,
|
|
||||||
inventory::Inventory,
|
|
||||||
modules::{
|
|
||||||
http::StaticFilesHttpScore,
|
http::StaticFilesHttpScore,
|
||||||
okd::{
|
okd::{
|
||||||
bootstrap_dhcp::OKDBootstrapDhcpScore,
|
bootstrap_dhcp::OKDBootstrapDhcpScore,
|
||||||
@ -16,10 +13,10 @@ use harmony::{
|
|||||||
dns::OKDDnsScore, ipxe::OKDIpxeScore,
|
dns::OKDDnsScore, ipxe::OKDIpxeScore,
|
||||||
},
|
},
|
||||||
tftp::TftpScore,
|
tftp::TftpScore,
|
||||||
},
|
}, topology::{LogicalHost, UnmanagedRouter}
|
||||||
topology::{LogicalHost, UnmanagedRouter},
|
|
||||||
};
|
};
|
||||||
use harmony_macros::{ip, mac_address};
|
use harmony_macros::{ip, mac_address};
|
||||||
|
use harmony_secret::SecretManager;
|
||||||
use harmony_types::net::Url;
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@ -123,6 +120,8 @@ async fn main() {
|
|||||||
let load_balancer_score =
|
let load_balancer_score =
|
||||||
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
|
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
|
||||||
|
|
||||||
|
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap();
|
||||||
|
|
||||||
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
|
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
|
||||||
let http_score = StaticFilesHttpScore {
|
let http_score = StaticFilesHttpScore {
|
||||||
folder_to_serve: Some(Url::LocalFolder(
|
folder_to_serve: Some(Url::LocalFolder(
|
||||||
@ -133,13 +132,15 @@ async fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let kickstart_filename = "inventory.kickstart".to_string();
|
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 harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
||||||
|
|
||||||
let ipxe_score = OKDIpxeScore {
|
let ipxe_score = OKDIpxeScore {
|
||||||
kickstart_filename,
|
kickstart_filename,
|
||||||
harmony_inventory_agent,
|
harmony_inventory_agent,
|
||||||
cluster_pubkey,
|
cluster_pubkey: FileContent {
|
||||||
|
path: FilePath::Relative("cluster_ssh_key.pub".to_string()),
|
||||||
|
content: ssh_key.public,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
harmony_tui::run(
|
harmony_tui::run(
|
||||||
|
@ -2,7 +2,7 @@ mod topology;
|
|||||||
|
|
||||||
use crate::topology::{get_inventory, get_topology};
|
use crate::topology::{get_inventory, get_topology};
|
||||||
use harmony::{
|
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;
|
use harmony_secret::SecretManager;
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ async fn main() {
|
|||||||
content: ssh_key.public,
|
content: ssh_key.public,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
Box::new(OKDInstallationScore {}),
|
Box::new(OKDInstallationPipeline {}),
|
||||||
];
|
];
|
||||||
harmony_cli::run(inventory, topology, scores, None)
|
harmony_cli::run(inventory, topology, scores, None)
|
||||||
.await
|
.await
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
mod topology;
|
mod topology;
|
||||||
|
|
||||||
use crate::topology::{get_inventory, get_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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@ -9,13 +14,16 @@ async fn main() {
|
|||||||
let topology = get_topology().await;
|
let topology = get_topology().await;
|
||||||
|
|
||||||
let kickstart_filename = "inventory.kickstart".to_string();
|
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 harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
||||||
|
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap();
|
||||||
|
|
||||||
let ipxe_score = OKDIpxeScore {
|
let ipxe_score = OKDIpxeScore {
|
||||||
kickstart_filename,
|
kickstart_filename,
|
||||||
harmony_inventory_agent,
|
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)
|
harmony_cli::run(inventory, topology, vec![Box::new(ipxe_score)], None)
|
||||||
|
@ -63,7 +63,7 @@ impl Inventory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, sqlx::Type)]
|
#[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone)]
|
||||||
pub enum HostRole {
|
pub enum HostRole {
|
||||||
Bootstrap,
|
Bootstrap,
|
||||||
ControlPlane,
|
ControlPlane,
|
||||||
|
@ -108,6 +108,7 @@ impl InventoryRepository for SqliteInventoryRepository {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_host_for_role(&self, role: HostRole) -> Result<Vec<PhysicalHost>, RepoError> {
|
async fn get_host_for_role(&self, role: HostRole) -> Result<Vec<PhysicalHost>, RepoError> {
|
||||||
struct HostIdRow {
|
struct HostIdRow {
|
||||||
host_id: String,
|
host_id: String,
|
||||||
|
122
harmony/src/modules/inventory/discovery.rs
Normal file
122
harmony/src/modules/inventory/discovery.rs
Normal file
@ -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<T: Topology> Score<T> for DiscoverHostForRoleScore {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"DiscoverInventoryAgentScore".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(DiscoverHostForRoleInterpret {
|
||||||
|
score: self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DiscoverHostForRoleInterpret {
|
||||||
|
score: DiscoverHostForRoleScore,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology> Interpret<T> for DiscoverHostForRoleInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
info!(
|
||||||
|
"Launching discovery agent, make sure that your nodes are successfully PXE booted and running inventory agent. They should answer on `http://<node_ip>: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<Id> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
mod discovery;
|
||||||
|
pub use discovery::*;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_inventory_agent::local_presence::DiscoveryEvent;
|
use harmony_inventory_agent::local_presence::DiscoveryEvent;
|
||||||
use log::{debug, info, trace};
|
use log::{debug, info, trace};
|
||||||
|
120
harmony/src/modules/okd/bootstrap_01_prepare.rs
Normal file
120
harmony/src/modules/okd/bootstrap_01_prepare.rs
Normal file
@ -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<HAClusterTopology> for OKDSetup01InventoryScore {
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||||
|
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<HAClusterTopology> 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<Id> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &HAClusterTopology,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
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<PhysicalHost> = 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()
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
387
harmony/src/modules/okd/bootstrap_02_bootstrap.rs
Normal file
387
harmony/src/modules/okd/bootstrap_02_bootstrap.rs
Normal file
@ -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<HAClusterTopology> for OKDSetup02BootstrapScore {
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||||
|
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<PhysicalHost, InterpretError> {
|
||||||
|
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::<RedhatSecret>().await?;
|
||||||
|
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().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<FileContent, InterpretError> {
|
||||||
|
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<HAClusterTopology> 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<Id> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &HAClusterTopology,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
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(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
195
harmony/src/modules/okd/bootstrap_03_control_plane.rs
Normal file
195
harmony/src/modules/okd/bootstrap_03_control_plane.rs
Normal file
@ -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<HAClusterTopology> for OKDSetup03ControlPlaneScore {
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||||
|
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<PhysicalHost>,
|
||||||
|
) -> 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<PhysicalHost>,
|
||||||
|
) -> 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<Vec<PhysicalHost>, 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<HAClusterTopology> 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<Id> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &HAClusterTopology,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
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(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
102
harmony/src/modules/okd/bootstrap_04_workers.rs
Normal file
102
harmony/src/modules/okd/bootstrap_04_workers.rs
Normal file
@ -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<HAClusterTopology> for OKDSetup04WorkersScore {
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||||
|
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<HAClusterTopology> 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<Id> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &HAClusterTopology,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
self.render_and_reboot().await?;
|
||||||
|
Ok(Outcome::new(
|
||||||
|
InterpretStatus::SUCCESS,
|
||||||
|
"Workers provisioned".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
101
harmony/src/modules/okd/bootstrap_05_sanity_check.rs
Normal file
101
harmony/src/modules/okd/bootstrap_05_sanity_check.rs
Normal file
@ -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<HAClusterTopology> for OKDSetup05SanityCheckScore {
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||||
|
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<HAClusterTopology> 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<Id> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &HAClusterTopology,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
self.run_checks().await?;
|
||||||
|
Ok(Outcome::new(
|
||||||
|
InterpretStatus::SUCCESS,
|
||||||
|
"Sanity checks passed".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
101
harmony/src/modules/okd/bootstrap_06_installation_report.rs
Normal file
101
harmony/src/modules/okd/bootstrap_06_installation_report.rs
Normal file
@ -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<HAClusterTopology> for OKDSetup06InstallationReportScore {
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
||||||
|
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<HAClusterTopology> 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<Id> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &HAClusterTopology,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
self.generate().await?;
|
||||||
|
Ok(Outcome::new(
|
||||||
|
InterpretStatus::SUCCESS,
|
||||||
|
"Installation report generated".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
@ -47,981 +47,27 @@
|
|||||||
//! - public_domain: External wildcard/apps domain (e.g., apps.example.com).
|
//! - public_domain: External wildcard/apps domain (e.g., apps.example.com).
|
||||||
//! - internal_domain: Internal cluster domain (e.g., cluster.local or harmony.mcd).
|
//! - 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::{
|
use crate::{
|
||||||
config::secret::{RedhatSecret, SshKeyPair},
|
modules::okd::{
|
||||||
data::{FileContent, FilePath, Version},
|
OKDSetup01InventoryScore, OKDSetup02BootstrapScore, OKDSetup03ControlPlaneScore,
|
||||||
hardware::PhysicalHost,
|
OKDSetup04WorkersScore, OKDSetup05SanityCheckScore,
|
||||||
infra::inventory::InventoryRepositoryFactory,
|
bootstrap_06_installation_report::OKDSetup06InstallationReportScore,
|
||||||
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,
|
score::Score,
|
||||||
topology::{HAClusterTopology, HostBinding},
|
topology::HAClusterTopology,
|
||||||
};
|
};
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
pub struct OKDInstallationPipeline;
|
||||||
// Public Orchestrator Score
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
impl OKDInstallationPipeline {
|
||||||
|
pub async fn get_all_scores() -> Vec<Box<dyn Score<HAClusterTopology>>> {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, new)]
|
vec![
|
||||||
pub struct OKDInstallationScore {}
|
Box::new(OKDSetup01InventoryScore::new()),
|
||||||
|
Box::new(OKDSetup02BootstrapScore::new()),
|
||||||
impl Score<HAClusterTopology> for OKDInstallationScore {
|
Box::new(OKDSetup03ControlPlaneScore::new()),
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
Box::new(OKDSetup04WorkersScore::new()),
|
||||||
Box::new(OKDInstallationInterpret::new(self.clone()))
|
Box::new(OKDSetup05SanityCheckScore::new()),
|
||||||
}
|
Box::new(OKDSetup06InstallationReportScore::new()),
|
||||||
|
]
|
||||||
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<HAClusterTopology> 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<Id> {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
inventory: &Inventory,
|
|
||||||
topology: &HAClusterTopology,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
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<HAClusterTopology> for OKDSetup01InventoryScore {
|
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
|
||||||
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<HAClusterTopology> 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<Id> {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
inventory: &Inventory,
|
|
||||||
topology: &HAClusterTopology,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
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://<node_ip>: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<HAClusterTopology> for OKDSetup02BootstrapScore {
|
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
|
||||||
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<PhysicalHost, InterpretError> {
|
|
||||||
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::<RedhatSecret>().await?;
|
|
||||||
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().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<FileContent, InterpretError> {
|
|
||||||
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<HAClusterTopology> 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<Id> {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
inventory: &Inventory,
|
|
||||||
topology: &HAClusterTopology,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
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<HAClusterTopology> for OKDSetup03ControlPlaneScore {
|
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
|
||||||
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<HAClusterTopology> 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<Id> {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
_inventory: &Inventory,
|
|
||||||
_topology: &HAClusterTopology,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
self.render_and_reboot().await?;
|
|
||||||
self.persist_network_bond().await?;
|
|
||||||
Ok(Outcome::new(
|
|
||||||
InterpretStatus::SUCCESS,
|
|
||||||
"Control plane provisioned".into(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
|
||||||
// Step 04: Workers
|
|
||||||
// - Render per-MAC PXE & ignition for workers; join nodes.
|
|
||||||
// - Persist bonding via MC/NNCP as required (same approach as masters).
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, new)]
|
|
||||||
struct OKDSetup04WorkersScore {}
|
|
||||||
|
|
||||||
impl Score<HAClusterTopology> for OKDSetup04WorkersScore {
|
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
|
||||||
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<HAClusterTopology> 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<Id> {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
_inventory: &Inventory,
|
|
||||||
_topology: &HAClusterTopology,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
self.render_and_reboot().await?;
|
|
||||||
Ok(Outcome::new(
|
|
||||||
InterpretStatus::SUCCESS,
|
|
||||||
"Workers provisioned".into(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
|
||||||
// Step 05: Sanity Check
|
|
||||||
// - Validate API reachability, ClusterOperators, ingress, and SDN status.
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, new)]
|
|
||||||
struct OKDSetup05SanityCheckScore {}
|
|
||||||
|
|
||||||
impl Score<HAClusterTopology> for OKDSetup05SanityCheckScore {
|
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
|
||||||
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<HAClusterTopology> 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<Id> {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
_inventory: &Inventory,
|
|
||||||
_topology: &HAClusterTopology,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
self.run_checks().await?;
|
|
||||||
Ok(Outcome::new(
|
|
||||||
InterpretStatus::SUCCESS,
|
|
||||||
"Sanity checks passed".into(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
|
||||||
// Step 06: Installation Report
|
|
||||||
// - Emit JSON and concise human summary of nodes, roles, versions, and health.
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, new)]
|
|
||||||
struct OKDSetup06InstallationReportScore {}
|
|
||||||
|
|
||||||
impl Score<HAClusterTopology> for OKDSetup06InstallationReportScore {
|
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
|
|
||||||
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<HAClusterTopology> 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<Id> {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
_inventory: &Inventory,
|
|
||||||
_topology: &HAClusterTopology,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
self.generate().await?;
|
|
||||||
Ok(Outcome::new(
|
|
||||||
InterpretStatus::SUCCESS,
|
|
||||||
"Installation report generated".into(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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_dhcp;
|
||||||
pub mod bootstrap_load_balancer;
|
pub mod bootstrap_load_balancer;
|
||||||
pub mod dhcp;
|
pub mod dhcp;
|
||||||
@ -7,3 +13,9 @@ pub mod ipxe;
|
|||||||
pub mod load_balancer;
|
pub mod load_balancer;
|
||||||
pub mod templates;
|
pub mod templates;
|
||||||
pub mod upgrade;
|
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::*;
|
||||||
|
@ -16,4 +16,5 @@ pub struct BootstrapIpxeTpl<'a> {
|
|||||||
pub scos_path: &'a str,
|
pub scos_path: &'a str,
|
||||||
pub installation_device: &'a str,
|
pub installation_device: &'a str,
|
||||||
pub ignition_http_path: &'a str,
|
pub ignition_http_path: &'a str,
|
||||||
|
pub ignition_file_name: &'static str,
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ set http_ip {{ http_ip }}
|
|||||||
set scos_path {{ scos_path }}
|
set scos_path {{ scos_path }}
|
||||||
set inst_dev {{ installation_device }}
|
set inst_dev {{ installation_device }}
|
||||||
set ign_path {{ ignition_http_path }}
|
set ign_path {{ ignition_http_path }}
|
||||||
set ign_file bootstrap.ign
|
set ign_file {{ ignition_file_name }}
|
||||||
|
|
||||||
# --- Derived Variables ---
|
# --- Derived Variables ---
|
||||||
set base-url http://${http_ip}:8080
|
set base-url http://${http_ip}:8080
|
||||||
|
Loading…
Reference in New Issue
Block a user