WIP: automating VM deployment
This commit is contained in:
153
packages/system/virt/src/hostcheck/mod.rs
Normal file
153
packages/system/virt/src/hostcheck/mod.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Summarize ok flag
|
||||
let ok = critical.is_empty();
|
||||
|
||||
Ok(HostCheckReport {
|
||||
ok,
|
||||
critical,
|
||||
optional,
|
||||
notes,
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user