cleanup 2
This commit is contained in:
		| @@ -74,6 +74,15 @@ pub struct VmRuntime { | |||||||
|     pub status: String, |     pub status: String, | ||||||
|     /// Console log file path |     /// Console log file path | ||||||
|     pub log_file: String, |     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)] | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
| @@ -108,11 +117,88 @@ fn vm_dir(id: &str) -> PathBuf { | |||||||
| fn vm_json_path(id: &str) -> PathBuf { | fn vm_json_path(id: &str) -> PathBuf { | ||||||
|     vm_dir(id).join("vm.json") |     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 { | fn vm_log_path(id: &str) -> PathBuf { | ||||||
|     vm_dir(id).join("logs/console.log") |     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 { | fn vm_pid_path(id: &str) -> PathBuf { | ||||||
|     vm_dir(id).join("pid") |     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())); |         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 |     // Prepare directory layout | ||||||
|     let dir = vm_dir(&spec.id); |     let dir = vm_dir(&spec.id); | ||||||
|     sal_os::mkdir( |     sal_os::mkdir( | ||||||
| @@ -190,17 +293,35 @@ pub fn vm_create(spec: &VmSpec) -> Result<String, CloudHvError> { | |||||||
|     let log_dir = dir.join("logs"); |     let log_dir = dir.join("logs"); | ||||||
|     sal_os::mkdir(log_dir.to_str().unwrap()).map_err(|e| CloudHvError::IoError(e.to_string()))?; |     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 { |     let rec = VmRecord { | ||||||
|         spec: spec.clone(), |         spec: spec.clone(), | ||||||
|         runtime: VmRuntime { |         runtime, | ||||||
|             pid: None, |  | ||||||
|             status: "stopped".into(), |  | ||||||
|             log_file: vm_log_path(&spec.id).to_string_lossy().into_owned(), |  | ||||||
|         }, |  | ||||||
|     }; |     }; | ||||||
|     let value = serde_json::to_value(&rec).map_err(|e| CloudHvError::JsonError(e.to_string()))?; |     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()) |     Ok(spec.id.clone()) | ||||||
| } | } | ||||||
| @@ -563,6 +684,9 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> { | |||||||
|     rec.runtime.pid = pid; |     rec.runtime.pid = pid; | ||||||
|     rec.runtime.status = if pid.is_some() { "running".into() } else { "stopped".into() }; |     rec.runtime.status = if pid.is_some() { "running".into() } else { "stopped".into() }; | ||||||
|     rec.runtime.log_file = log_file; |     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(); |     rec.spec.api_socket = api_socket.clone(); | ||||||
|  |  | ||||||
|     let value = serde_json::to_value(&rec).map_err(|e| CloudHvError::JsonError(e.to_string()))?; |     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(()) |     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> { | pub fn vm_info(id: &str) -> Result<VmRecord, CloudHvError> { | ||||||
|     let p = vm_json_path(id); |     // Try current user's VM root first, then fall back to /root (common when VM was launched under sudo) | ||||||
|     if !p.exists() { |     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))); |         return Err(CloudHvError::NotFound(format!("VM '{}' not found", id))); | ||||||
|     } |     }; | ||||||
|     let value = read_json(&p)?; |     let value = read_json(&p)?; | ||||||
|     let rec: VmRecord = serde_json::from_value(value).map_err(|e| CloudHvError::JsonError(e.to_string()))?; |     let rec: VmRecord = serde_json::from_value(value).map_err(|e| CloudHvError::JsonError(e.to_string()))?; | ||||||
|     Ok(rec) |     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 | /// Stop a VM via ch-remote (graceful), optionally force kill | ||||||
| pub fn vm_stop(id: &str, force: bool) -> Result<(), CloudHvError> { | pub fn vm_stop(id: &str, force: bool) -> Result<(), CloudHvError> { | ||||||
|     ensure_deps().ok(); // best-effort; we might still force-kill |     ensure_deps().ok(); // best-effort; we might still force-kill | ||||||
|   | |||||||
| @@ -189,8 +189,7 @@ if [ -n \"$IPV6_CIDR\" ]; then | |||||||
|   printf '%s\\n' \ |   printf '%s\\n' \ | ||||||
|     \"enable-ra\" \ |     \"enable-ra\" \ | ||||||
|     \"dhcp-range=$BRIDGE_PREFIX,ra-names,12h\" \ |     \"dhcp-range=$BRIDGE_PREFIX,ra-names,12h\" \ | ||||||
|     \"dhcp-option=option6:dns-server,[2001:4860:4860::8888]\" \ |     \"dhcp-option=option6:dns-server,[2001:4860:4860::8888]\" >>\"$TMP\" | ||||||
|     \"dhcp-option=option6:route,3600,400::/7,[$BRIDGE_ADDR]\" >>\"$TMP\" |  | ||||||
| fi | fi | ||||||
|  |  | ||||||
| if [ ! -f \"$CFG\" ] || ! cmp -s \"$CFG\" \"$TMP\"; then | if [ ! -f \"$CFG\" ] || ! cmp -s \"$CFG\" \"$TMP\"; then | ||||||
| @@ -246,7 +245,15 @@ TAP={tap} | |||||||
| UIDX=$(id -u) | UIDX=$(id -u) | ||||||
| GIDX=$(id -g) | 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\" master \"$BR\" 2>/dev/null || true | ||||||
| ip link set \"$TAP\" up | ip link set \"$TAP\" up | ||||||
| ", | ", | ||||||
| @@ -258,13 +265,18 @@ ip link set \"$TAP\" up | |||||||
| } | } | ||||||
|  |  | ||||||
| /// Stable locally-administered unicast MAC derived from VM id. | /// 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 { | pub fn stable_mac_from_id(id: &str) -> String { | ||||||
|     use std::collections::hash_map::DefaultHasher; |     // 64-bit FNV-1a | ||||||
|     use std::hash::{Hash, Hasher}; |     const FNV_OFFSET: u64 = 0xcbf29ce484222325; | ||||||
|     let mut h = DefaultHasher::new(); |     const FNV_PRIME: u64 = 0x00000100000001B3; | ||||||
|     id.hash(&mut h); |     let mut v: u64 = FNV_OFFSET; | ||||||
|     let v = h.finish(); |     for b in id.as_bytes() { | ||||||
|     let b0 = (((v >> 40) & 0xff) as u8 & 0xfe) | 0x02; // locally administered, unicast |         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 b1 = ((v >> 32) & 0xff) as u8; | ||||||
|     let b2 = ((v >> 24) & 0xff) as u8; |     let b2 = ((v >> 24) & 0xff) as u8; | ||||||
|     let b3 = ((v >> 16) & 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 { | fn stable_mac_from_id(id: &str) -> String { | ||||||
|     let mut h = DefaultHasher::new(); |     // Use deterministic FNV-1a (matches host-side MAC derivation used by CH builder) | ||||||
|     id.hash(&mut h); |     const FNV_OFFSET: u64 = 0xcbf29ce484222325; | ||||||
|     let v = h.finish(); |     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 b0 = (((v >> 40) & 0xff) as u8 & 0xfe) | 0x02; // locally administered, unicast | ||||||
|     let b1 = ((v >> 32) & 0xff) as u8; |     let b1 = ((v >> 32) & 0xff) as u8; | ||||||
|     let b2 = ((v >> 24) & 0xff) as u8; |     let b2 = ((v >> 24) & 0xff) as u8; | ||||||
|     let b3 = ((v >> 16) & 0xff) as u8; |     let b3 = ((v >> 16) & 0xff) as u8; | ||||||
|     let b4 = ((v >> 8) & 0xff) as u8; |     let b4 = ((v >> 8) & 0xff) as u8; | ||||||
|     let b5 = (v & 0xff) as u8; |     let b5 = (v & 0xff) as u8; | ||||||
|     format!( |     format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", b0, b1, b2, b3, b4, b5) | ||||||
|         "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", |  | ||||||
|         b0, b1, b2, b3, b4, b5 |  | ||||||
|     ) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | #[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); |     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 | // Module registration | ||||||
|  |  | ||||||
| pub fn register_cloudhv_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | 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_delete", cloudhv_vm_delete); | ||||||
|     engine.register_fn("cloudhv_vm_list", cloudhv_vm_list); |     engine.register_fn("cloudhv_vm_list", cloudhv_vm_list); | ||||||
|     engine.register_fn("cloudhv_vm_info", cloudhv_vm_info); |     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_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_discover_ipv6_on_bridge", cloudhv_discover_ipv6_on_bridge); | ||||||
|     engine.register_fn("cloudhv_display_network_info", cloudhv_display_network_info); |     engine.register_fn("cloudhv_display_network_info", cloudhv_display_network_info); | ||||||
|   | |||||||
| @@ -3,6 +3,21 @@ | |||||||
|  |  | ||||||
| let vm_id = "vm-clean-test"; | 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 | // Phase 1: Host check | ||||||
| let hc = host_check(); | let hc = host_check(); | ||||||
| if !(hc.ok == true) { | if !(hc.ok == true) { | ||||||
| @@ -25,10 +40,11 @@ try { | |||||||
| // Phase 3: Wait for VM to boot and get network configuration | // Phase 3: Wait for VM to boot and get network configuration | ||||||
| wait_for_vm_boot(10); | wait_for_vm_boot(10); | ||||||
|  |  | ||||||
| // Phase 4: Discover VM IP addresses | // Phase 4: Discover VM IP addresses (robust, no hardcoded MAC/paths) | ||||||
| let mac_addr = "a2:26:1e:ac:96:3a"; // This should be derived from vm_id_actual | let net = cloudhv_vm_network_info(vm_id_actual, 30); | ||||||
| let ipv4 = cloudhv_discover_ipv4_from_leases("/var/lib/misc/dnsmasq-hero-br-hero.leases", mac_addr, 30); | let ipv4 = net["ipv4"];    // Dynamic UNIT if not found yet | ||||||
| let ipv6 = cloudhv_discover_ipv6_on_bridge("br-hero", mac_addr); | 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 | // Phase 5: Display connection info | ||||||
| cloudhv_display_network_info(vm_id_actual, ipv4, ipv6); | cloudhv_display_network_info(vm_id_actual, ipv4, ipv6); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user