use std::thread;
use std::time::Duration;
use crate::config::{GuestOs, Result, VmProfile};
use crate::ssh::VmSession;
use crate::winrm::elevated::ProgressCallback;
use crate::winrm::WinRM;
pub mod boot_wait;
pub mod linux;
pub mod logger;
pub mod macos;
pub mod windows;
pub use logger::BootstrapLogger;
const WINDOWS_MARKER: &str = "C:\\Users\\vagrant\\.testbed-bootstrapped";
const LINUX_MARKER: &str = "/home/vagrant/.testbed-bootstrapped";
const MACOS_MARKER: &str = "/Users/vagrant/.testbed-bootstrapped";
pub fn is_bootstrapped(profile: &VmProfile) -> bool {
match profile.os {
GuestOs::Windows => {
if let Ok(winrm) = WinRM::from_profile(profile) {
let result = winrm.run_ps_quiet(&format!(
"if (Test-Path '{WINDOWS_MARKER}') {{ 'YES' }} else {{ 'NO' }}"
));
if let Ok(cmd) = result {
return cmd.stdout.trim() == "YES";
}
}
if let Ok(mut session) = crate::ssh::connect(profile) {
let result = crate::ssh::exec_ps_windows(&mut session,
&format!("if (Test-Path '{WINDOWS_MARKER}') {{ 'YES' }} else {{ 'NO' }}")
);
if let Ok((output, _)) = result {
return output.trim() == "YES";
}
}
false
}
GuestOs::Linux => {
if let Ok(mut session) = crate::ssh::connect(profile) {
let result = crate::ssh::exec(&mut session, &format!("[ -f {LINUX_MARKER} ] && echo YES || echo NO"));
if let Ok(output) = result {
return output.trim() == "YES";
}
}
false
}
GuestOs::MacOS => {
if let Ok(mut session) = crate::ssh::connect(profile) {
let result = crate::ssh::exec(&mut session, &format!("[ -f {MACOS_MARKER} ] && echo YES || echo NO"));
if let Ok(output) = result {
return output.trim() == "YES";
}
}
false
}
}
}
pub fn bootstrap(profile: &VmProfile, session: &mut VmSession, logger: &BootstrapLogger, progress: ProgressCallback<'_>) -> Result<()> {
if is_bootstrapped(profile) {
logger.message("already bootstrapped, skipping");
return Ok(());
}
match profile.os {
GuestOs::Windows => {
let winrm = WinRM::from_profile(profile)?;
if !is_bootstrapped(profile) {
windows::bootstrap_windows_winrm_phase(profile, &winrm, logger, progress)?;
}
thread::sleep(Duration::from_secs(10));
let deadline = std::time::Instant::now() + Duration::from_secs(120);
loop {
if std::time::Instant::now() > deadline {
return Err(crate::config::TestbedError::BootstrapFailed {
step: "wait for SSH".to_string(),
message: "timed out waiting for SSH after WinRM bootstrap".to_string(),
});
}
if crate::ssh::connect(profile).is_ok() {
break;
}
thread::sleep(Duration::from_secs(5));
}
*session = crate::ssh::connect(profile)?;
windows::bootstrap_windows_ssh_phase(profile, &winrm, session, logger, progress, false)?;
}
GuestOs::Linux => {
linux::bootstrap_linux(profile, session, logger)?;
}
GuestOs::MacOS => {
bootstrap_macos(profile, session, logger)?;
}
}
if !is_bootstrapped(profile) {
return Err(crate::config::TestbedError::BootstrapFailed {
step: "verification".to_string(),
message: "bootstrap marker not found after completion".to_string(),
});
}
Ok(())
}
fn bootstrap_macos(profile: &crate::config::VmProfile, session: &mut VmSession, logger: &BootstrapLogger) -> Result<()> {
macos::bootstrap_macos(profile, session, logger)
}
pub const BOOTSTRAP_MISE_TOML: &str = include_str!("../../scripts/bootstrap_mise.toml");