Files
herolib_rust/packages/system/virt/src/hostcheck/mod.rs
Maxime Van Hees 784f87db97 WIP2
2025-08-27 16:03:32 +02:00

196 lines
6.6 KiB
Rust

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<String>,
pub optional: Vec<String>,
pub notes: Vec<String>,
}
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<HostCheckReport, HostCheckError> {
let mut critical: Vec<String> = Vec::new();
let mut optional: Vec<String> = Vec::new();
let mut notes: Vec<String> = 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,
})
}