use serde::{Deserialize, Serialize}; use std::fs; use std::path::Path; use sal_os; use sal_process; /// Host dependency check error #[derive(Debug)] pub enum HostCheckError { Io(String), } impl std::fmt::Display for HostCheckError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { HostCheckError::Io(e) => write!(f, "IO error: {}", e), } } } impl std::error::Error for HostCheckError {} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HostCheckReport { pub ok: bool, pub critical: Vec, pub optional: Vec, pub notes: Vec, } fn hero_vm_root() -> String { let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into()); format!("{}/hero/virt/vms", home.trim_end_matches('/')) } fn bin_missing(name: &str) -> bool { sal_process::which(name).is_none() } /// Perform host dependency checks required for image preparation and Cloud Hypervisor run. /// Returns a structured report that Rhai can consume easily. pub fn host_check_deps() -> Result { let mut critical: Vec = Vec::new(); let mut optional: Vec = Vec::new(); let mut notes: Vec = Vec::new(); // Must run as root let uid_res = sal_process::run("id -u").silent(true).die(false).execute(); match uid_res { Ok(r) if r.success => { let uid_s = r.stdout.trim(); if uid_s != "0" { critical.push("not running as root (required for nbd/mount/network)".into()); } } _ => { notes.push("failed to determine uid via `id -u`".into()); } } // Core binaries required for CH and image manipulation let core_bins = [ "cloud-hypervisor", // CH binary (dynamic) "cloud-hypervisor-static", // CH static (if present) "ch-remote", "ch-remote-static", // hypervisor-fw is expected at /images/hypervisor-fw (not on PATH) "qemu-img", "qemu-nbd", "blkid", "tune2fs", "partprobe", "mount", "umount", "sed", "awk", "modprobe", ]; // Networking helpers (for default bridge + NAT path) let net_bins = ["ip", "nft", "dnsmasq", "systemctl"]; // Evaluate presence let mut have_any_ch = false; if !bin_missing("cloud-hypervisor") || !bin_missing("cloud-hypervisor-static") { have_any_ch = true; } if !have_any_ch { critical.push("cloud-hypervisor or cloud-hypervisor-static not found on PATH".into()); } if bin_missing("ch-remote") && bin_missing("ch-remote-static") { critical.push("ch-remote or ch-remote-static not found on PATH".into()); } for b in [&core_bins[4..], &net_bins[..]].concat() { if bin_missing(b) { // treat qemu/img/nbd stack and filesystem tools as critical // treat networking tools as critical too since default path provisions bridge/DHCP critical.push(format!("missing binary: {}", b)); } } // Filesystem/path checks // Ensure /images exists and expected image files are present (ubuntu, alpine, hypervisor-fw) let images_root = "/images"; if !Path::new(images_root).exists() { critical.push(format!("{} not found (expected base images directory)", images_root)); } else { let ubuntu_path = format!("{}/noble-server-cloudimg-amd64.img", images_root); let alpine_path = format!("{}/alpine-virt-cloudimg-amd64.qcow2", images_root); let fw_path = format!("{}/hypervisor-fw", images_root); if !Path::new(&ubuntu_path).exists() { critical.push(format!("missing base image: {}", ubuntu_path)); } if !Path::new(&alpine_path).exists() { critical.push(format!("missing base image: {}", alpine_path)); } if !Path::new(&fw_path).exists() { critical.push(format!("missing firmware: {}", fw_path)); } } // Ensure VM root directory is writable/creatable let vm_root = hero_vm_root(); if let Err(e) = sal_os::mkdir(&vm_root) { critical.push(format!( "cannot create/access VM root directory {}: {}", vm_root, e )); } else { // also try writing a small file let probe_path = format!("{}/.__hero_probe", vm_root); if let Err(e) = fs::write(&probe_path, b"ok") { critical.push(format!( "VM root not writable {}: {}", vm_root, e )); } else { let _ = fs::remove_file(&probe_path); } } // Optional Mycelium IPv6 checks when enabled via env let ipv6_env = std::env::var("HERO_VIRT_IPV6_ENABLE").unwrap_or_else(|_| "".into()); let ipv6_enabled = ipv6_env.eq_ignore_ascii_case("1") || ipv6_env.eq_ignore_ascii_case("true"); if ipv6_enabled { // Require mycelium CLI if bin_missing("mycelium") { critical.push("mycelium CLI not found on PATH (required when HERO_VIRT_IPV6_ENABLE=true)".into()); } // Validate interface presence and global IPv6 let ifname = std::env::var("HERO_VIRT_MYCELIUM_IF").unwrap_or_else(|_| "mycelium".into()); let check_if = sal_process::run(&format!("ip -6 addr show dev {}", ifname)) .silent(true) .die(false) .execute(); match check_if { Ok(r) if r.success => { let out = r.stdout; if !(out.contains("inet6") && out.contains("scope global")) { notes.push(format!( "iface '{}' present but no global IPv6 detected; Mycelium may not be up yet", ifname )); } } _ => { critical.push(format!( "iface '{}' not found or no IPv6; ensure Mycelium is running", ifname )); } } // Best-effort: parse `mycelium inspect` for Address let insp = sal_process::run("mycelium inspect").silent(true).die(false).execute(); match insp { Ok(res) if res.success && res.stdout.contains("Address:") => { // good enough } _ => { notes.push("`mycelium inspect` did not return an Address; IPv6 overlay may be unavailable".into()); } } } // Summarize ok flag let ok = critical.is_empty(); Ok(HostCheckReport { ok, critical, optional, notes, }) }