382 lines
14 KiB
Rust
382 lines
14 KiB
Rust
use crate::{
|
|
config::secret::{RedhatSecret, SshKeyPair},
|
|
data::{FileContent, FilePath, Version},
|
|
hardware::PhysicalHost,
|
|
infra::inventory::InventoryRepositoryFactory,
|
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
|
inventory::{HostRole, Inventory},
|
|
modules::{
|
|
dhcp::DhcpHostBindingScore,
|
|
http::{IPxeMacBootFileScore, StaticFilesHttpScore},
|
|
okd::{
|
|
bootstrap_load_balancer::OKDBootstrapLoadBalancerScore,
|
|
templates::{BootstrapIpxeTpl, InstallConfigYaml},
|
|
},
|
|
},
|
|
score::Score,
|
|
topology::{HAClusterTopology, HostBinding},
|
|
};
|
|
use async_trait::async_trait;
|
|
use derive_new::new;
|
|
use harmony_secret::SecretManager;
|
|
use harmony_types::id::Id;
|
|
use log::{debug, info};
|
|
use serde::Serialize;
|
|
use std::path::PathBuf;
|
|
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
// 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 _ = 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::success("Bootstrap phase complete".into()))
|
|
}
|
|
}
|