(unstable) pushing WIP

This commit is contained in:
Maxime Van Hees
2025-08-25 15:25:00 +02:00
parent af89ef0149
commit 1bb731711b
4 changed files with 456 additions and 107 deletions

View File

@@ -43,6 +43,8 @@ pub struct VmSpec {
pub id: String,
/// Optional for firmware boot; required for direct kernel boot
pub kernel_path: Option<String>,
/// Optional initramfs when using direct kernel boot
pub initramfs_path: Option<String>,
/// Optional for direct kernel boot; required for firmware boot
pub firmware_path: Option<String>,
/// Disk image path (qcow2 or raw)
@@ -228,38 +230,104 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
let _ = fs::remove_file(&api_path);
// Preflight disk: if source is qcow2, convert to raw to avoid CH "Compressed blocks not supported"
// This is best-effort: if qemu-img is unavailable or info fails, we skip conversion.
// Robust conversion:
// - Remove any stale destination
// - Try direct convert to destination file
// - On failure (e.g., byte-range lock issues), fallback to piping stdout into dd
let mut disk_to_use = rec.spec.disk_path.clone();
if let Ok(info) = qcow2::info(&disk_to_use) {
if info.get("format").and_then(|v| v.as_str()) == Some("qcow2") {
let dest = vm_dir(id).join("disk.raw").to_string_lossy().into_owned();
let cmd = format!(
// Best-effort remove stale target file to avoid locking errors
let _ = fs::remove_file(&dest);
// Attempt 1: normal qemu-img convert to dest file
let cmd1 = format!(
"qemu-img convert -O raw {} {}",
shell_escape(&disk_to_use),
shell_escape(&dest)
);
match sal_process::run(&cmd).silent(true).execute() {
Ok(res) if res.success => {
disk_to_use = dest;
let attempt1 = sal_process::run(&cmd1).silent(true).die(false).execute();
let mut converted_ok = false;
let mut err1: Option<String> = None;
if let Ok(res) = attempt1 {
if res.success {
converted_ok = true;
} else {
err1 = Some(format!("{}{}", res.stdout, res.stderr));
}
Ok(res) => {
return Err(CloudHvError::CommandFailed(format!(
"Failed converting qcow2 to raw: {}",
res.stderr
)));
}
Err(e) => {
return Err(CloudHvError::CommandFailed(format!(
"Failed converting qcow2 to raw: {}",
e
)));
} else if let Err(e) = attempt1 {
err1 = Some(e.to_string());
}
if !converted_ok {
// Attempt 2: pipe via stdout into dd (avoids qemu-img destination locking semantics on some FS)
let cmd2 = format!(
"#!/bin/bash -euo pipefail\nqemu-img convert -O raw {} - | dd of={} bs=4M status=none",
shell_escape(&disk_to_use),
shell_escape(&dest)
);
match sal_process::run(&cmd2).silent(true).die(false).execute() {
Ok(res) if res.success => {
converted_ok = true;
}
Ok(res) => {
let mut msg = String::from("Failed converting qcow2 to raw.");
if let Some(e1) = err1 {
msg.push_str(&format!("\nFirst attempt error:\n{}", e1));
}
msg.push_str(&format!("\nSecond attempt error:\n{}{}", res.stdout, res.stderr));
return Err(CloudHvError::CommandFailed(msg));
}
Err(e) => {
let mut msg = String::from("Failed converting qcow2 to raw.");
if let Some(e1) = err1 {
msg.push_str(&format!("\nFirst attempt error:\n{}", e1));
}
msg.push_str(&format!("\nSecond attempt error:\n{}", e));
return Err(CloudHvError::CommandFailed(msg));
}
}
}
if converted_ok {
disk_to_use = dest;
}
}
}
// Build command (minimal args for Phase 2)
// We redirect all output to log_file via shell and keep process in background with nohup
// Consolidate extra --disk occurrences from spec.extra_args into a single --disk (CH version requires variadic form)
// Collect disk value tokens provided by the user and strip them from extra args so we can render one '--disk' followed by multiple values.
let mut extra_disk_vals: Vec<String> = Vec::new();
let mut extra_args_sans_disks: Vec<String> = Vec::new();
if let Some(extra) = rec.spec.extra_args.clone() {
let mut i = 0usize;
while i < extra.len() {
let tok = extra[i].clone();
if tok == "--disk" {
if i + 1 < extra.len() {
extra_disk_vals.push(extra[i + 1].clone());
i += 2;
continue;
} else {
// dangling --disk without value; drop it
i += 1;
continue;
}
} else if let Some(rest) = tok.strip_prefix("--disk=") {
if !rest.is_empty() {
extra_disk_vals.push(rest.to_string());
}
i += 1;
continue;
}
// keep token
extra_args_sans_disks.push(tok);
i += 1;
}
}
// CH CLI flags (very common subset)
// --disk path=... uses virtio-blk by default
@@ -282,6 +350,12 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
.unwrap_or_else(|| "console=ttyS0 reboot=k panic=1".to_string());
parts.push("--kernel".into());
parts.push(kpath);
if let Some(initrd) = rec.spec.initramfs_path.clone() {
if Path::new(&initrd).exists() {
parts.push("--initramfs".into());
parts.push(initrd);
}
}
parts.push("--cmdline".into());
parts.push(cmdline);
} else {
@@ -292,6 +366,10 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
parts.push("--disk".into());
parts.push(format!("path={}", disk_to_use));
// Append any additional disk value tokens (from sanitized extra args) so CH sees a single '--disk' with multiple values
for dv in &extra_disk_vals {
parts.push(dv.clone());
}
parts.push("--cpus".into());
parts.push(format!("boot={}", rec.spec.vcpus));
parts.push("--memory".into());
@@ -301,36 +379,50 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
parts.push("--console".into());
parts.push("off".into());
// Networking prerequisites (bridge + NAT via nftables + dnsmasq DHCP)
// Defaults can be overridden via env:
// HERO_VIRT_BRIDGE_NAME, HERO_VIRT_BRIDGE_ADDR_CIDR, HERO_VIRT_SUBNET_CIDR, HERO_VIRT_DHCP_START, HERO_VIRT_DHCP_END
let bridge_name = std::env::var("HERO_VIRT_BRIDGE_NAME").unwrap_or_else(|_| "br-hero".into());
let bridge_addr_cidr = std::env::var("HERO_VIRT_BRIDGE_ADDR_CIDR").unwrap_or_else(|_| "172.30.0.1/24".into());
let subnet_cidr = std::env::var("HERO_VIRT_SUBNET_CIDR").unwrap_or_else(|_| "172.30.0.0/24".into());
let dhcp_start = std::env::var("HERO_VIRT_DHCP_START").unwrap_or_else(|_| "172.30.0.50".into());
let dhcp_end = std::env::var("HERO_VIRT_DHCP_END").unwrap_or_else(|_| "172.30.0.250".into());
// Determine if the user provided explicit network arguments (e.g. "--net", "tap=...,mac=...")
// If so, do NOT provision the default host networking or add a default NIC.
let has_user_net = rec
.spec
.extra_args
.as_ref()
.map(|v| v.iter().any(|tok| tok == "--net"))
.unwrap_or(false);
// Ensure host-side networking (requires root privileges / CAP_NET_ADMIN)
ensure_host_net_prereq_dnsmasq_nftables(
&bridge_name,
&bridge_addr_cidr,
&subnet_cidr,
&dhcp_start,
&dhcp_end,
)?;
if !has_user_net {
// Networking prerequisites (bridge + NAT via nftables + dnsmasq DHCP)
// Defaults can be overridden via env:
// HERO_VIRT_BRIDGE_NAME, HERO_VIRT_BRIDGE_ADDR_CIDR, HERO_VIRT_SUBNET_CIDR, HERO_VIRT_DHCP_START, HERO_VIRT_DHCP_END
let bridge_name = std::env::var("HERO_VIRT_BRIDGE_NAME").unwrap_or_else(|_| "br-hero".into());
let bridge_addr_cidr =
std::env::var("HERO_VIRT_BRIDGE_ADDR_CIDR").unwrap_or_else(|_| "172.30.0.1/24".into());
let subnet_cidr =
std::env::var("HERO_VIRT_SUBNET_CIDR").unwrap_or_else(|_| "172.30.0.0/24".into());
let dhcp_start =
std::env::var("HERO_VIRT_DHCP_START").unwrap_or_else(|_| "172.30.0.50".into());
let dhcp_end =
std::env::var("HERO_VIRT_DHCP_END").unwrap_or_else(|_| "172.30.0.250".into());
// Ensure a TAP device for this VM and attach to the bridge
let tap_name = ensure_tap_for_vm(&bridge_name, id)?;
// Stable locally-administered MAC derived from VM id
let mac = stable_mac_from_id(id);
// Ensure host-side networking (requires root privileges / CAP_NET_ADMIN)
ensure_host_net_prereq_dnsmasq_nftables(
&bridge_name,
&bridge_addr_cidr,
&subnet_cidr,
&dhcp_start,
&dhcp_end,
)?;
parts.push("--net".into());
parts.push(format!("tap={},mac={}", tap_name, mac));
// Ensure a TAP device for this VM and attach to the bridge
let tap_name = ensure_tap_for_vm(&bridge_name, id)?;
// Stable locally-administered MAC derived from VM id
let mac = stable_mac_from_id(id);
if let Some(extra) = rec.spec.extra_args.clone() {
for e in extra {
parts.push(e);
}
parts.push("--net".into());
parts.push(format!("tap={},mac={}", tap_name, mac));
}
// Append any user-provided extra args, sans any '--disk' we already consolidated
for e in extra_args_sans_disks {
parts.push(e);
}
let args_str = shell_join(&parts);
@@ -369,6 +461,32 @@ echo $! > '{}'
Err(_) => None,
};
// Quick health check: ensure process did not exit immediately due to CLI errors (e.g., duplicate flags)
if let Some(pid_num) = pid {
thread::sleep(Duration::from_millis(300));
if !proc_exists(pid_num) {
// Tail log to surface the error cause
let tail_cmd = format!("tail -n 200 {}", shell_escape(&log_file));
let tail = sal_process::run(&tail_cmd).die(false).silent(true).execute();
let mut log_snip = String::new();
if let Ok(res) = tail {
if res.success {
log_snip = res.stdout;
} else {
log_snip = format!("{}{}", res.stdout, res.stderr);
}
}
return Err(CloudHvError::CommandFailed(format!(
"cloud-hypervisor exited immediately after start. Log tail:\n{}",
log_snip
)));
}
} else {
return Err(CloudHvError::CommandFailed(
"failed to obtain cloud-hypervisor PID (start script did not write pid)".into(),
));
}
// Update state
rec.runtime.pid = pid;
rec.runtime.status = if pid.is_some() { "running".into() } else { "stopped".into() };