cleanup 2
This commit is contained in:
@@ -74,6 +74,15 @@ pub struct VmRuntime {
|
||||
pub status: String,
|
||||
/// Console log file path
|
||||
pub log_file: String,
|
||||
/// Bridge name used for networking discovery (if applicable)
|
||||
#[serde(default)]
|
||||
pub bridge_name: Option<String>,
|
||||
/// dnsmasq lease file used (if applicable)
|
||||
#[serde(default)]
|
||||
pub lease_file: Option<String>,
|
||||
/// Stable MAC used for NIC injection (derived from VM id)
|
||||
#[serde(default)]
|
||||
pub mac: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -108,11 +117,88 @@ fn vm_dir(id: &str) -> PathBuf {
|
||||
fn vm_json_path(id: &str) -> PathBuf {
|
||||
vm_dir(id).join("vm.json")
|
||||
}
|
||||
// Attempt to resolve a VM record across both the current user's HOME and root HOME.
|
||||
// This handles cases where the VM was created/launched under sudo (HOME=/root).
|
||||
fn resolve_vm_json_path(id: &str) -> Option<PathBuf> {
|
||||
let candidates = vec![
|
||||
hero_vm_root(), // $HOME/hero/virt/vms
|
||||
Path::new("/root/hero/virt/vms").to_path_buf(),
|
||||
];
|
||||
for base in candidates {
|
||||
let p = base.join(id).join("vm.json");
|
||||
if p.exists() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn vm_log_path(id: &str) -> PathBuf {
|
||||
vm_dir(id).join("logs/console.log")
|
||||
}
|
||||
|
||||
/// Attempt to resolve an API socket across both the current user's HOME and root HOME.
|
||||
/// This handles cases where the VM was launched under sudo (HOME=/root).
|
||||
fn resolve_vm_api_socket_path(id: &str) -> Option<PathBuf> {
|
||||
let candidates = vec![
|
||||
hero_vm_root(), // $HOME/hero/virt/vms
|
||||
Path::new("/root/hero/virt/vms").to_path_buf(),
|
||||
];
|
||||
for base in candidates {
|
||||
let p = base.join(id).join("api.sock");
|
||||
if p.exists() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Query cloud-hypervisor for the first NIC's tap and mac via ch-remote-static.
|
||||
/// Returns (tap_name, mac_lower) if successful.
|
||||
fn ch_query_tap_mac(api_sock: &Path) -> Option<(String, String)> {
|
||||
let cmd = format!(
|
||||
"ch-remote-static --api-socket {} info",
|
||||
shell_escape(&api_sock.to_string_lossy())
|
||||
);
|
||||
if let Ok(res) = sal_process::run(&cmd).silent(true).die(false).execute() {
|
||||
if res.success {
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&res.stdout) {
|
||||
if let Some(net0) = v
|
||||
.get("config")
|
||||
.and_then(|c| c.get("net"))
|
||||
.and_then(|n| n.get(0))
|
||||
{
|
||||
let tap = net0.get("tap").and_then(|t| t.as_str()).unwrap_or("").to_string();
|
||||
let mac = net0.get("mac").and_then(|m| m.as_str()).unwrap_or("").to_string();
|
||||
if !tap.is_empty() && !mac.is_empty() {
|
||||
return Some((tap, mac.to_lowercase()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Infer the bridge name a tap device is attached to by parsing `ip -o link show <tap>` output.
|
||||
fn bridge_name_for_tap(tap: &str) -> Option<String> {
|
||||
let cmd = format!("ip -o link show {}", shell_escape(tap));
|
||||
if let Ok(res) = sal_process::run(&cmd).silent(true).die(false).execute() {
|
||||
if res.success {
|
||||
for line in res.stdout.lines() {
|
||||
if let Some(idx) = line.find(" master ") {
|
||||
let rest = &line[idx + " master ".len()..];
|
||||
let name = rest.split_whitespace().next().unwrap_or("");
|
||||
if !name.is_empty() {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn vm_pid_path(id: &str) -> PathBuf {
|
||||
vm_dir(id).join("pid")
|
||||
}
|
||||
@@ -180,6 +266,23 @@ pub fn vm_create(spec: &VmSpec) -> Result<String, CloudHvError> {
|
||||
return Err(CloudHvError::InvalidSpec("memory_mb must be >= 128".into()));
|
||||
}
|
||||
|
||||
// If a VM with this id already exists, ensure it's not running to avoid clobber + resource conflicts
|
||||
let json_path = vm_json_path(&spec.id);
|
||||
if json_path.exists() {
|
||||
if let Ok(value) = read_json(&json_path) {
|
||||
if let Ok(existing) = serde_json::from_value::<VmRecord>(value.clone()) {
|
||||
if let Some(pid) = existing.runtime.pid {
|
||||
if proc_exists(pid) {
|
||||
return Err(CloudHvError::CommandFailed(format!(
|
||||
"VM '{}' already exists and is running with pid {}. Stop or delete it first, or choose a different id.",
|
||||
spec.id, pid
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare directory layout
|
||||
let dir = vm_dir(&spec.id);
|
||||
sal_os::mkdir(
|
||||
@@ -190,17 +293,35 @@ pub fn vm_create(spec: &VmSpec) -> Result<String, CloudHvError> {
|
||||
let log_dir = dir.join("logs");
|
||||
sal_os::mkdir(log_dir.to_str().unwrap()).map_err(|e| CloudHvError::IoError(e.to_string()))?;
|
||||
|
||||
// Persist initial record
|
||||
// Build runtime (preserve prior metadata if present; will be refreshed on start)
|
||||
let mut runtime = VmRuntime {
|
||||
pid: None,
|
||||
status: "stopped".into(),
|
||||
log_file: vm_log_path(&spec.id).to_string_lossy().into_owned(),
|
||||
bridge_name: None,
|
||||
lease_file: None,
|
||||
mac: None,
|
||||
};
|
||||
if json_path.exists() {
|
||||
if let Ok(value) = read_json(&json_path) {
|
||||
if let Ok(existing) = serde_json::from_value::<VmRecord>(value) {
|
||||
if !existing.runtime.log_file.is_empty() {
|
||||
runtime.log_file = existing.runtime.log_file;
|
||||
}
|
||||
runtime.bridge_name = existing.runtime.bridge_name;
|
||||
runtime.lease_file = existing.runtime.lease_file;
|
||||
runtime.mac = existing.runtime.mac;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Persist record (spec updated, runtime preserved/reset to stopped)
|
||||
let rec = VmRecord {
|
||||
spec: spec.clone(),
|
||||
runtime: VmRuntime {
|
||||
pid: None,
|
||||
status: "stopped".into(),
|
||||
log_file: vm_log_path(&spec.id).to_string_lossy().into_owned(),
|
||||
},
|
||||
runtime,
|
||||
};
|
||||
let value = serde_json::to_value(&rec).map_err(|e| CloudHvError::JsonError(e.to_string()))?;
|
||||
write_json(&vm_json_path(&spec.id), &value)?;
|
||||
write_json(&json_path, &value)?;
|
||||
|
||||
Ok(spec.id.clone())
|
||||
}
|
||||
@@ -563,6 +684,9 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
rec.runtime.pid = pid;
|
||||
rec.runtime.status = if pid.is_some() { "running".into() } else { "stopped".into() };
|
||||
rec.runtime.log_file = log_file;
|
||||
rec.runtime.bridge_name = bridge_for_disc.clone();
|
||||
rec.runtime.lease_file = lease_for_disc.clone();
|
||||
rec.runtime.mac = Some(net::stable_mac_from_id(id));
|
||||
rec.spec.api_socket = api_socket.clone();
|
||||
|
||||
let value = serde_json::to_value(&rec).map_err(|e| CloudHvError::JsonError(e.to_string()))?;
|
||||
@@ -584,17 +708,89 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return VM record info (spec + runtime) by id
|
||||
//// Return VM record info (spec + runtime) by id
|
||||
pub fn vm_info(id: &str) -> Result<VmRecord, CloudHvError> {
|
||||
let p = vm_json_path(id);
|
||||
if !p.exists() {
|
||||
// Try current user's VM root first, then fall back to /root (common when VM was launched under sudo)
|
||||
let p_user = vm_json_path(id);
|
||||
let p = if p_user.exists() {
|
||||
p_user
|
||||
} else if let Some(p2) = resolve_vm_json_path(id) {
|
||||
p2
|
||||
} else {
|
||||
return Err(CloudHvError::NotFound(format!("VM '{}' not found", id)));
|
||||
}
|
||||
};
|
||||
let value = read_json(&p)?;
|
||||
let rec: VmRecord = serde_json::from_value(value).map_err(|e| CloudHvError::JsonError(e.to_string()))?;
|
||||
Ok(rec)
|
||||
}
|
||||
|
||||
//// Discover VM network info using persisted metadata (bridge/lease/mac) with sensible fallbacks.
|
||||
/// Returns (IPv4, IPv6, MAC, BridgeName, LeaseFile), each optional.
|
||||
pub fn vm_network_info(
|
||||
id: &str,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(Option<String>, Option<String>, Option<String>, Option<String>, Option<String>), CloudHvError> {
|
||||
let rec = vm_info(id)?;
|
||||
|
||||
// Start with persisted/env/default values
|
||||
let mut bridge_name = rec
|
||||
.runtime
|
||||
.bridge_name
|
||||
.clone()
|
||||
.or_else(|| std::env::var("HERO_VIRT_BRIDGE_NAME").ok())
|
||||
.unwrap_or_else(|| "br-hero".into());
|
||||
|
||||
// MAC: persisted or deterministically derived (lowercased for matching)
|
||||
let mut mac_lower = rec
|
||||
.runtime
|
||||
.mac
|
||||
.clone()
|
||||
.unwrap_or_else(|| net::stable_mac_from_id(id))
|
||||
.to_lowercase();
|
||||
|
||||
// Attempt to query CH for ground-truth (tap, mac) if API socket is available
|
||||
if let Some(api_sock) = resolve_vm_api_socket_path(id) {
|
||||
if let Some((tap, mac_from_ch)) = ch_query_tap_mac(&api_sock) {
|
||||
mac_lower = mac_from_ch;
|
||||
if let Some(br) = bridge_name_for_tap(&tap) {
|
||||
bridge_name = br;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lease file: persisted -> env -> derived from (possibly overridden) bridge
|
||||
let lease_path = rec
|
||||
.runtime
|
||||
.lease_file
|
||||
.clone()
|
||||
.or_else(|| std::env::var("HERO_VIRT_DHCP_LEASE_FILE").ok())
|
||||
.unwrap_or_else(|| format!("/var/lib/misc/dnsmasq-hero-{}.leases", bridge_name));
|
||||
|
||||
// Discover addresses
|
||||
let ipv4 = net::discover_ipv4_from_leases(&lease_path, &mac_lower, timeout_secs);
|
||||
let ipv6 = {
|
||||
use std::time::{Duration, Instant};
|
||||
let deadline = Instant::now() + Duration::from_secs(timeout_secs);
|
||||
let mut v6: Option<String> = None;
|
||||
while Instant::now() < deadline {
|
||||
if let Some(ip) = net::discover_ipv6_on_bridge(&bridge_name, &mac_lower) {
|
||||
v6 = Some(ip);
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(800));
|
||||
}
|
||||
v6
|
||||
};
|
||||
|
||||
Ok((
|
||||
ipv4,
|
||||
ipv6,
|
||||
Some(mac_lower),
|
||||
Some(bridge_name),
|
||||
Some(lease_path),
|
||||
))
|
||||
}
|
||||
|
||||
/// Stop a VM via ch-remote (graceful), optionally force kill
|
||||
pub fn vm_stop(id: &str, force: bool) -> Result<(), CloudHvError> {
|
||||
ensure_deps().ok(); // best-effort; we might still force-kill
|
||||
|
@@ -189,8 +189,7 @@ if [ -n \"$IPV6_CIDR\" ]; then
|
||||
printf '%s\\n' \
|
||||
\"enable-ra\" \
|
||||
\"dhcp-range=$BRIDGE_PREFIX,ra-names,12h\" \
|
||||
\"dhcp-option=option6:dns-server,[2001:4860:4860::8888]\" \
|
||||
\"dhcp-option=option6:route,3600,400::/7,[$BRIDGE_ADDR]\" >>\"$TMP\"
|
||||
\"dhcp-option=option6:dns-server,[2001:4860:4860::8888]\" >>\"$TMP\"
|
||||
fi
|
||||
|
||||
if [ ! -f \"$CFG\" ] || ! cmp -s \"$CFG\" \"$TMP\"; then
|
||||
@@ -246,7 +245,15 @@ TAP={tap}
|
||||
UIDX=$(id -u)
|
||||
GIDX=$(id -g)
|
||||
|
||||
ip link show \"$TAP\" >/dev/null 2>&1 || ip tuntap add dev \"$TAP\" mode tap user \"$UIDX\" group \"$GIDX\"
|
||||
# Ensure a clean TAP state to avoid Resource busy if a previous VM run left it lingering
|
||||
if ip link show \"$TAP\" >/dev/null 2>&1; then
|
||||
ip link set \"$TAP\" down || true
|
||||
ip link set \"$TAP\" nomaster 2>/dev/null || true
|
||||
ip tuntap del dev \"$TAP\" mode tap 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Recreate with correct ownership and attach to bridge
|
||||
ip tuntap add dev \"$TAP\" mode tap user \"$UIDX\" group \"$GIDX\"
|
||||
ip link set \"$TAP\" master \"$BR\" 2>/dev/null || true
|
||||
ip link set \"$TAP\" up
|
||||
",
|
||||
@@ -258,13 +265,18 @@ ip link set \"$TAP\" up
|
||||
}
|
||||
|
||||
/// Stable locally-administered unicast MAC derived from VM id.
|
||||
/// IMPORTANT: Use a deterministic hash (FNV-1a) rather than DefaultHasher (which is randomized).
|
||||
pub fn stable_mac_from_id(id: &str) -> String {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = DefaultHasher::new();
|
||||
id.hash(&mut h);
|
||||
let v = h.finish();
|
||||
let b0 = (((v >> 40) & 0xff) as u8 & 0xfe) | 0x02; // locally administered, unicast
|
||||
// 64-bit FNV-1a
|
||||
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
|
||||
const FNV_PRIME: u64 = 0x00000100000001B3;
|
||||
let mut v: u64 = FNV_OFFSET;
|
||||
for b in id.as_bytes() {
|
||||
v ^= *b as u64;
|
||||
v = v.wrapping_mul(FNV_PRIME);
|
||||
}
|
||||
// Locally administered, unicast
|
||||
let b0 = (((v >> 40) & 0xff) as u8 & 0xfe) | 0x02;
|
||||
let b1 = ((v >> 32) & 0xff) as u8;
|
||||
let b2 = ((v >> 24) & 0xff) as u8;
|
||||
let b3 = ((v >> 16) & 0xff) as u8;
|
||||
|
@@ -83,19 +83,21 @@ fn default_disable_cloud_init_net() -> bool {
|
||||
}
|
||||
|
||||
fn stable_mac_from_id(id: &str) -> String {
|
||||
let mut h = DefaultHasher::new();
|
||||
id.hash(&mut h);
|
||||
let v = h.finish();
|
||||
// Use deterministic FNV-1a (matches host-side MAC derivation used by CH builder)
|
||||
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
|
||||
const FNV_PRIME: u64 = 0x00000100000001B3;
|
||||
let mut v: u64 = FNV_OFFSET;
|
||||
for b in id.as_bytes() {
|
||||
v ^= *b as u64;
|
||||
v = v.wrapping_mul(FNV_PRIME);
|
||||
}
|
||||
let b0 = (((v >> 40) & 0xff) as u8 & 0xfe) | 0x02; // locally administered, unicast
|
||||
let b1 = ((v >> 32) & 0xff) as u8;
|
||||
let b2 = ((v >> 24) & 0xff) as u8;
|
||||
let b3 = ((v >> 16) & 0xff) as u8;
|
||||
let b4 = ((v >> 8) & 0xff) as u8;
|
||||
let b5 = (v & 0xff) as u8;
|
||||
format!(
|
||||
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
|
||||
b0, b1, b2, b3, b4, b5
|
||||
)
|
||||
format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", b0, b1, b2, b3, b4, b5)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
@@ -230,6 +230,21 @@ pub fn cloudhv_display_network_info(vm_id: &str, ipv4: Dynamic, ipv6: Dynamic) {
|
||||
println!(" cloudhv_vm_delete(\"{}\", true);", vm_id);
|
||||
}
|
||||
|
||||
/// High-level network discovery that avoids hardcoded MAC/paths.
|
||||
/// Returns a Rhai map with fields: ipv4, ipv6, mac, bridge, lease.
|
||||
pub fn cloudhv_vm_network_info(id: &str, timeout_secs: i64) -> Result<Map, Box<EvalAltResult>> {
|
||||
let (ipv4, ipv6, mac, bridge, lease) =
|
||||
hv_to_rhai(cloudhv::vm_network_info(id, timeout_secs as u64))?;
|
||||
let mut m = Map::new();
|
||||
m.insert("vm_id".into(), id.to_string().into());
|
||||
m.insert("ipv4".into(), ipv4.map(Into::into).unwrap_or(Dynamic::UNIT));
|
||||
m.insert("ipv6".into(), ipv6.map(Into::into).unwrap_or(Dynamic::UNIT));
|
||||
m.insert("mac".into(), mac.map(Into::into).unwrap_or(Dynamic::UNIT));
|
||||
m.insert("bridge".into(), bridge.map(Into::into).unwrap_or(Dynamic::UNIT));
|
||||
m.insert("lease".into(), lease.map(Into::into).unwrap_or(Dynamic::UNIT));
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
// Module registration
|
||||
|
||||
pub fn register_cloudhv_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
@@ -239,6 +254,7 @@ pub fn register_cloudhv_module(engine: &mut Engine) -> Result<(), Box<EvalAltRes
|
||||
engine.register_fn("cloudhv_vm_delete", cloudhv_vm_delete);
|
||||
engine.register_fn("cloudhv_vm_list", cloudhv_vm_list);
|
||||
engine.register_fn("cloudhv_vm_info", cloudhv_vm_info);
|
||||
engine.register_fn("cloudhv_vm_network_info", cloudhv_vm_network_info);
|
||||
engine.register_fn("cloudhv_discover_ipv4_from_leases", cloudhv_discover_ipv4_from_leases);
|
||||
engine.register_fn("cloudhv_discover_ipv6_on_bridge", cloudhv_discover_ipv6_on_bridge);
|
||||
engine.register_fn("cloudhv_display_network_info", cloudhv_display_network_info);
|
||||
|
@@ -3,6 +3,21 @@
|
||||
|
||||
let vm_id = "vm-clean-test";
|
||||
|
||||
// Phase 0: Pre-clean any existing VM with the same id (best-effort)
|
||||
// This avoids TAP "Resource busy" when a previous run is still active.
|
||||
try {
|
||||
cloudhv_vm_stop(vm_id, true);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
// brief wait to let processes exit and TAP release
|
||||
wait_for_vm_boot(1);
|
||||
try {
|
||||
cloudhv_vm_delete(vm_id, true);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Phase 1: Host check
|
||||
let hc = host_check();
|
||||
if !(hc.ok == true) {
|
||||
@@ -25,10 +40,11 @@ try {
|
||||
// Phase 3: Wait for VM to boot and get network configuration
|
||||
wait_for_vm_boot(10);
|
||||
|
||||
// Phase 4: Discover VM IP addresses
|
||||
let mac_addr = "a2:26:1e:ac:96:3a"; // This should be derived from vm_id_actual
|
||||
let ipv4 = cloudhv_discover_ipv4_from_leases("/var/lib/misc/dnsmasq-hero-br-hero.leases", mac_addr, 30);
|
||||
let ipv6 = cloudhv_discover_ipv6_on_bridge("br-hero", mac_addr);
|
||||
// Phase 4: Discover VM IP addresses (robust, no hardcoded MAC/paths)
|
||||
let net = cloudhv_vm_network_info(vm_id_actual, 30);
|
||||
let ipv4 = net["ipv4"]; // Dynamic UNIT if not found yet
|
||||
let ipv6 = net["ipv6"]; // Dynamic UNIT if not found
|
||||
// Optional: you could also inspect net["mac"], net["bridge"], net["lease"]
|
||||
|
||||
// Phase 5: Display connection info
|
||||
cloudhv_display_network_info(vm_id_actual, ipv4, ipv6);
|
||||
|
Reference in New Issue
Block a user