use clap::ArgMatches;
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::bootstrap;
use crate::bootstrap::BootstrapLogger;
use crate::config::{get_profile, GuestOs, DisplayMode};
use crate::providers::{default_provider, ResolvedPorts};
use crate::ssh;
use crate::state;
use crate::winrm::WinRM;
use crate::winrm::elevated::ProgressCallback;
type BoxedError = Box<dyn std::error::Error + Send + Sync>;
fn resolve_profile(name: &str) -> std::result::Result<crate::config::VmProfile, BoxedError> {
get_profile(name).map(|p| p.clone()).map_err(|e| Box::new(e) as BoxedError)
}
fn ensure_image(profile: &crate::config::VmProfile) -> Result<(), BoxedError> {
let cache_path = profile.image_cache_path();
if cache_path.exists() {
return Ok(());
}
eprintln!(" Image not cached, importing...");
let provider = default_provider()?;
let path = provider.ensure_image(profile)
.map_err(|e| format!("Failed to import image: {e}"))?;
eprintln!(" Image imported at {}", path.display());
Ok(())
}
fn ensure_vm_running(profile: &crate::config::VmProfile) -> Result<ResolvedPorts, BoxedError> {
if state::is_running(&profile.name) {
let vm_state = state::load(&profile.name)
.expect("is_running returned true but load failed");
eprintln!(" VM '{}' already running (PID {:?})", profile.name, vm_state.pid);
return Ok(ResolvedPorts {
ssh_port: vm_state.ssh_port,
winrm_port: vm_state.winrm_port,
rdp_port: vm_state.rdp_port,
vnc_port: vm_state.vnc_port,
});
}
eprintln!(" VM '{}' not running, launching...", profile.name);
let provider = default_provider()?;
let handle = provider.launch(profile, DisplayMode::Headless)
.map_err(|e| format!("Failed to launch VM: {e}"))?;
let ports = handle.resolved_ports.clone();
drop(handle);
eprintln!(" VM launched: SSH={}, VNC={}, WinRM={:?}",
ports.ssh_port, ports.vnc_port, ports.winrm_port);
Ok(ports)
}
pub fn cmd_bootstrap(args: &ArgMatches) -> std::result::Result<(), BoxedError> {
let name = args.get_one::<String>("profile").unwrap();
let force_vsbuild = args.get_flag("force-vsbuild");
let profile = resolve_profile(name)?;
let logger = Arc::new(
BootstrapLogger::new(name)
.map_err(|e| format!("Failed to create bootstrap logger: {e}"))?,
);
if bootstrap::is_bootstrapped(&profile) {
logger.message("already bootstrapped, skipping.");
return Ok(());
}
ensure_image(&profile)?;
let ports = ensure_vm_running(&profile)?;
let logger_for_progress = Arc::clone(&logger);
let progress_cb: ProgressCallback<'_> = Some(&|msg: &str| {
logger_for_progress.message(msg);
});
match profile.os {
GuestOs::Linux | GuestOs::MacOS => bootstrap_ssh(&profile, name, &logger, &ports),
GuestOs::Windows => bootstrap_windows(&profile, name, &logger, progress_cb, force_vsbuild, &ports),
}
}
fn bootstrap_ssh(profile: &crate::config::VmProfile, name: &str, logger: &BootstrapLogger, ports: &ResolvedPorts) -> Result<(), BoxedError> {
let ssh_port = ports.ssh_port;
logger.message("→ Connecting via SSH...");
let mut session = ssh::connect_from_port(ssh_port, &profile.user, profile.os)
.map_err(|e| format!("SSH connection failed: {e}"))?;
logger.message("✓ Connected, starting bootstrap...");
bootstrap::bootstrap(profile, &mut session, logger, None)?;
logger.message(&format!("✓ {name} bootstrap complete"));
Ok(())
}
fn bootstrap_windows(profile: &crate::config::VmProfile, name: &str, logger: &BootstrapLogger, progress: ProgressCallback<'_>, force_vsbuild: bool, ports: &ResolvedPorts) -> Result<(), BoxedError> {
let winrm_port = ports.winrm_port
.ok_or_else(|| format!("No WinRM port configured for '{}'", profile.name))?;
logger.message(&format!("→ Waiting for WinRM on port {winrm_port}..."));
let start = Instant::now();
let timeout = Duration::from_secs(300);
loop {
if start.elapsed() > timeout {
return Err(format!("Timed out waiting for WinRM after {:?}", timeout).into());
}
let winrm = WinRM::new("127.0.0.1", winrm_port, profile.user, profile.pass);
if winrm.ping() {
break;
}
std::thread::sleep(Duration::from_secs(5));
}
logger.message("✓ WinRM ready");
let winrm = WinRM::new("127.0.0.1", winrm_port, profile.user, profile.pass);
bootstrap::windows::bootstrap_windows_winrm_phase(profile, &winrm, logger, progress)?;
logger.message(&format!("→ Waiting for SSH on port {}...", ports.ssh_port));
let start = Instant::now();
let timeout = Duration::from_secs(120);
loop {
if start.elapsed() > timeout {
return Err(crate::config::TestbedError::BootstrapFailed {
step: "wait for SSH".to_string(),
message: format!("timed out waiting for SSH after {:?}", timeout),
}.into());
}
if ssh::connect_from_port(ports.ssh_port, &profile.user, profile.os).is_ok() {
break;
}
std::thread::sleep(Duration::from_secs(5));
}
logger.message("✓ SSH ready");
let mut session = ssh::connect_from_port(ports.ssh_port, &profile.user, profile.os)
.map_err(|e| format!("SSH connection failed: {e}"))?;
bootstrap::windows::bootstrap_windows_ssh_phase(profile, &winrm, &mut session, logger, progress, force_vsbuild)?;
let mut verified = false;
for attempt in 0..5 {
if bootstrap::is_bootstrapped(profile) {
verified = true;
break;
}
if attempt < 4 {
std::thread::sleep(std::time::Duration::from_secs(5));
}
}
if !verified {
return Err(crate::config::TestbedError::BootstrapFailed {
step: "verification".to_string(),
message: "bootstrap marker not found after completion".to_string(),
}.into());
}
logger.message(&format!("✓ {name} bootstrap complete"));
Ok(())
}