diff --git a/.gitignore b/.gitignore index 149050f..3850d09 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ private_repos/ ### Harmony ### harmony.log +data/okd/installation_files* ### Helm ### # Chart dependencies diff --git a/Cargo.lock b/Cargo.lock index 83f5dfc..a49e99b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2427,6 +2427,7 @@ dependencies = [ "harmony_secret_derive", "http 1.3.1", "infisical", + "inquire", "lazy_static", "log", "pretty_assertions", diff --git a/examples/okd_installation/src/topology.rs b/examples/okd_installation/src/topology.rs index 79d5068..02553a5 100644 --- a/examples/okd_installation/src/topology.rs +++ b/examples/okd_installation/src/topology.rs @@ -22,7 +22,7 @@ pub async fn get_topology() -> HAClusterTopology { name: String::from("opnsense-1"), }; - let config = SecretManager::get::().await; + let config = SecretManager::get_or_prompt::().await; let config = config.unwrap(); let opnsense = Arc::new( diff --git a/examples/okd_pxe/src/topology.rs b/examples/okd_pxe/src/topology.rs index f6c4702..707969a 100644 --- a/examples/okd_pxe/src/topology.rs +++ b/examples/okd_pxe/src/topology.rs @@ -16,7 +16,7 @@ pub async fn get_topology() -> HAClusterTopology { name: String::from("opnsense-1"), }; - let config = SecretManager::get::().await; + let config = SecretManager::get_or_prompt::().await; let config = config.unwrap(); let opnsense = Arc::new( diff --git a/harmony/src/modules/okd/installation.rs b/harmony/src/modules/okd/installation.rs index 73fc49e..a169f62 100644 --- a/harmony/src/modules/okd/installation.rs +++ b/harmony/src/modules/okd/installation.rs @@ -425,7 +425,8 @@ impl OKDSetup02BootstrapInterpret { topology: &HAClusterTopology, ) -> Result<(), InterpretError> { let okd_bin_path = PathBuf::from("./data/okd/bin"); - let okd_installation_path_str = "./data/okd/installation_files"; + 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); @@ -450,8 +451,8 @@ impl OKDSetup02BootstrapInterpret { ); } - let redhat_secret = SecretManager::get::().await?; - let ssh_key = SecretManager::get::().await?; + let redhat_secret = SecretManager::get_or_prompt::().await?; + let ssh_key = SecretManager::get_or_prompt::().await?; let install_config_yaml = InstallConfigYaml { cluster_name: &topology.get_cluster_name(), @@ -562,38 +563,14 @@ impl OKDSetup02BootstrapInterpret { .interpret(inventory, topology) .await?; - let run_command = - async |cmd: &str, args: Vec<&str>| -> Result { - let output = Command::new(cmd).args(&args).output().await.map_err(|e| { - InterpretError::new(format!("Failed to launch command {cmd} : {e}")) - })?; - let stdout = String::from_utf8(output.stdout.clone()).unwrap(); - info!("{cmd} stdout :\n\n{}", stdout); - let stderr = String::from_utf8(output.stderr.clone()).unwrap(); - info!("{cmd} stderr :\n\n{}", stderr); - info!("{cmd} exit status : {}", output.status); - if !output.status.success() { - return Err(InterpretError::new(format!( - "Command execution failed, exit code {} : {} {}", - output.status, - cmd, - args.join(" ") - ))); - } - Ok(output) - }; - 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"# ); - warn!( - "TODO push installer image files with `scp -r data/okd/installer_image/* root@192.168.1.1:/usr/local/http/scos/` until performance issue is resolved" - ); inquire::Confirm::new( - "push installer image files with `scp -r data/okd/installer_image/* root@192.168.1.1:/usr/local/http/scos/` until performance issue is resolved").prompt().expect("Prompt error"); + &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 { @@ -636,10 +613,12 @@ impl OKDSetup02BootstrapInterpret { ) -> Result<(), InterpretError> { let content = BootstrapIpxeTpl { http_ip: &topology.http_server.get_ip().to_string(), - scos_path: "scos", // TODO use some constant - 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 + 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(); @@ -735,7 +714,7 @@ impl Interpret for OKDSetup02BootstrapInterpret { self.prepare_ignition_files(inventory, topology).await?; self.render_per_mac_pxe(inventory, topology).await?; self.setup_bootstrap_load_balancer(inventory, topology) - .await?; + .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?; diff --git a/harmony/src/modules/okd/templates.rs b/harmony/src/modules/okd/templates.rs index 26a1791..9aa3035 100644 --- a/harmony/src/modules/okd/templates.rs +++ b/harmony/src/modules/okd/templates.rs @@ -15,4 +15,5 @@ pub struct BootstrapIpxeTpl<'a> { pub http_ip: &'a str, pub scos_path: &'a str, pub installation_device: &'a str, + pub ignition_http_path: &'a str, } diff --git a/harmony/templates/boot.ipxe.j2 b/harmony/templates/boot.ipxe.j2 index 94ea07b..6b63ba2 100644 --- a/harmony/templates/boot.ipxe.j2 +++ b/harmony/templates/boot.ipxe.j2 @@ -1,6 +1,122 @@ #!ipxe +# Default chainloader with optional debug mode. +# - Press any key within 3 seconds at start to enable debug mode. +# - In debug mode: confirmations and extra sleeps are enabled. +# - In production (no key pressed): continues without prompts. +# Config set base-url http://{{ gateway_ip }}:8080 -set hostfile ${base-url}/byMAC/01-${mac:hexhyp}.ipxe +set macfile 01-${mac:hexhyp} +set hostfile ${base-url}/byMAC/${macfile}.ipxe +set fallback ${base-url}/fallback.ipxe -chain ${hostfile} || chain ${base-url}/fallback.ipxe +# Verbosity (1..4) +set debug 2 + +# State +set debugmode 0 + +echo +echo === iPXE chainload stage (default) === +echo MAC: ${mac} +echo Base URL: ${base-url} +echo Host file: ${hostfile} +echo Fallback : ${fallback} +echo ====================================== +echo +echo Press any key within 3 seconds to enter DEBUG MODE... +prompt --timeout 3 Entering debug mode... && set debugmode 1 || set debugmode 0 + +iseq ${debugmode} 1 && goto :debug_enabled || goto :debug_disabled + +:debug_enabled +echo DEBUG MODE: ON (confirmations and extra sleeps enabled) +sleep 1 +goto :start + +:debug_disabled +echo DEBUG MODE: OFF (no confirmations; production behavior) +sleep 1 +goto :start + +:start +# Show network status briefly in both modes +ifstat +iseq ${debugmode} 1 && sleep 2 || sleep 0 + +# Probe host-specific script via HTTP HEAD +echo +echo Probing host-specific script: ${hostfile} +http --head ${hostfile} +iseq ${rc} 0 && goto :has_hostfile || goto :no_hostfile + +:has_hostfile +echo Found host-specific script: ${hostfile} +iseq ${debugmode} 1 && goto :confirm_host || goto :chain_host + +:confirm_host +prompt --timeout 8 Press Enter to chain host script, Esc to abort... && goto :chain_host || goto :abort + +:chain_host +echo Chaining ${hostfile} ... +iseq ${debugmode} 1 && sleep 2 || sleep 0 +chain ${hostfile} || goto :host_chain_fail +# On success, control does not return. + +:host_chain_fail +echo ERROR: chain to ${hostfile} failed (rc=${rc}) +iseq ${debugmode} 1 && sleep 5 || sleep 1 +goto :try_fallback + +:no_hostfile +echo NOT FOUND or unreachable: ${hostfile} (rc=${rc}) +iseq ${debugmode} 1 && sleep 2 || sleep 0 + +:try_fallback +echo +echo Probing fallback script: ${fallback} +http --head ${fallback} +iseq ${rc} 0 && goto :has_fallback || goto :fallback_missing + +:has_fallback +iseq ${debugmode} 1 && goto :confirm_fallback || goto :chain_fallback + +:confirm_fallback +prompt --timeout 8 Press Enter to chain fallback, Esc to shell... && goto :chain_fallback || goto :shell + +:chain_fallback +echo Chaining ${fallback} ... +iseq ${debugmode} 1 && sleep 2 || sleep 0 +chain ${fallback} || goto :fallback_chain_fail +# On success, control does not return. + +:fallback_chain_fail +echo ERROR: chain to fallback failed (rc=${rc}) +iseq ${debugmode} 1 && sleep 5 || sleep 1 +goto :shell + +:fallback_missing +echo ERROR: Fallback script not reachable: ${fallback} (rc=${rc}) +iseq ${debugmode} 1 && sleep 5 || sleep 1 +goto :shell + +:abort +echo Aborted by user. +iseq ${debugmode} 1 && sleep 2 || sleep 1 +goto :shell + +:shell +echo +echo === iPXE debug shell === +echo Try: +echo dhcp +echo ifstat +echo ping {{ gateway_ip }} +echo http ${hostfile} +echo http ${fallback} +echo chain ${hostfile} +echo chain ${fallback} +sleep 1 +shell + +exit diff --git a/harmony/templates/okd/bootstrap.ipxe.j2 b/harmony/templates/okd/bootstrap.ipxe.j2 index 9a5a9b9..edc6534 100644 --- a/harmony/templates/okd/bootstrap.ipxe.j2 +++ b/harmony/templates/okd/bootstrap.ipxe.j2 @@ -2,6 +2,6 @@ set base-url http://{{ http_ip }}:8080 set scos-base-url = ${base-url}/{{ scos_path }} set installation-device = {{ installation_device }} -kernel ${scos-base-url}/scos-live-kernel.x86_64 initrd=main coreos.live.rootfs_url=${scos-base-url}/scos-live-rootfs.x86_64.img coreos.inst.install_dev=${installation-device} coreos.inst.ignition_url=${base-url}/bootstrap.ign +kernel ${scos-base-url}/scos-live-kernel.x86_64 initrd=main coreos.live.rootfs_url=${scos-base-url}/scos-live-rootfs.x86_64.img coreos.inst.install_dev=${installation-device} coreos.inst.ignition_url=${base-url}/{{ ignition_http_path }}/bootstrap.ign initrd --name main ${scos-base-url}/scos-live-initramfs.x86_64.img boot diff --git a/harmony_secret/Cargo.toml b/harmony_secret/Cargo.toml index 88f93ac..36bfb31 100644 --- a/harmony_secret/Cargo.toml +++ b/harmony_secret/Cargo.toml @@ -18,6 +18,7 @@ infisical = { git = "https://github.com/jggc/rust-sdk.git", branch = "patch-1" } tokio.workspace = true async-trait.workspace = true http.workspace = true +inquire.workspace = true [dev-dependencies] pretty_assertions.workspace = true diff --git a/harmony_secret/src/lib.rs b/harmony_secret/src/lib.rs index 4603e4c..a4f636f 100644 --- a/harmony_secret/src/lib.rs +++ b/harmony_secret/src/lib.rs @@ -110,6 +110,42 @@ impl SecretManager { }) } + pub async fn get_or_prompt() -> Result { + let secret = Self::get::().await; + let manager = get_secret_manager().await; + let prompted = secret.is_err(); + + let secret = secret.or_else(|e| -> Result { + debug!("Could not get secret : {e}"); + + let ns = &manager.namespace; + let key = T::KEY; + let secret_json = inquire::Text::new(&format!( + "Secret not found for {} {}, paste the JSON here :", + ns, key + )) + .prompt() + .map_err(|e| { + SecretStoreError::Store(format!("Failed to prompt secret {ns} {key} : {e}").into()) + })?; + + let secret: T = serde_json::from_str(&secret_json).map_err(|e| { + SecretStoreError::Deserialization { + key: T::KEY.to_string(), + source: e, + } + })?; + + Ok(secret) + })?; + + if prompted { + Self::set(&secret).await?; + } + + Ok(secret) + } + /// Serializes and stores a secret. pub async fn set(secret: &T) -> Result<(), SecretStoreError> { let manager = get_secret_manager().await;