(unstable) pushing WIP
This commit is contained in:
@@ -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() };
|
||||
|
Reference in New Issue
Block a user