196 lines
6.6 KiB
Rust
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,
|
|
})
|
|
} |