cleanup
This commit is contained in:
		| @@ -1,6 +1,5 @@ | ||||
| use crate::cloudhv::{vm_create, vm_start, CloudHvError, VmSpec}; | ||||
| use crate::image_prep::{image_prepare, Flavor as ImgFlavor, ImagePrepOptions, NetPlanOpts}; | ||||
| use sal_process; | ||||
| use crate::cloudhv::net::{NetworkingProfileSpec, DefaultNatOptions, BridgeOptions}; | ||||
|  | ||||
| /// Cloud Hypervisor VM Builder focused on Rhai ergonomics. | ||||
|   | ||||
| @@ -5,8 +5,6 @@ use std::fs; | ||||
| use std::path::{Path, PathBuf}; | ||||
| use std::thread; | ||||
| use std::time::Duration; | ||||
| use std::collections::hash_map::DefaultHasher; | ||||
| use std::hash::{Hash, Hasher}; | ||||
|  | ||||
| use sal_os; | ||||
| use sal_process; | ||||
| @@ -416,7 +414,7 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> { | ||||
|  | ||||
|     if let Some(profile) = profile_effective { | ||||
|         match profile { | ||||
|             NetworkingProfileSpec::DefaultNat(mut nat) => { | ||||
|             NetworkingProfileSpec::DefaultNat(nat) => { | ||||
|                 // IPv6 handling (auto via Mycelium unless disabled) | ||||
|                 let mut ipv6_bridge_cidr: Option<String> = None; | ||||
|                 if nat.ipv6_enable { | ||||
| @@ -579,8 +577,8 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> { | ||||
|             std::env::var("HERO_VIRT_DHCP_LEASE_FILE") | ||||
|                 .unwrap_or_else(|_| format!("/var/lib/misc/dnsmasq-hero-{}.leases", bridge_name)) | ||||
|         }); | ||||
|         let ipv4 = net::discover_ipv4_from_leases(&lease_path, &mac_lower, 12); | ||||
|         let ipv6 = net::discover_ipv6_on_bridge(&bridge_name, &mac_lower); | ||||
|         let _ipv4 = net::discover_ipv4_from_leases(&lease_path, &mac_lower, 12); | ||||
|         let _ipv6 = net::discover_ipv6_on_bridge(&bridge_name, &mac_lower); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| @@ -713,253 +711,14 @@ pub fn vm_list() -> Result<Vec<VmRecord>, CloudHvError> { | ||||
|     Ok(out) | ||||
| } | ||||
|  | ||||
| fn tap_name_for_id(id: &str) -> String { | ||||
|     // Linux IFNAMSIZ is typically 15; keep "tap-" + 10 hex = 14 chars | ||||
|     let mut h = DefaultHasher::new(); | ||||
|     id.hash(&mut h); | ||||
|     let v = h.finish(); | ||||
|     let hex = format!("{:016x}", v); | ||||
|     format!("tap-{}", &hex[..10]) | ||||
| } | ||||
|  | ||||
| fn ensure_tap_for_vm(bridge_name: &str, id: &str) -> Result<String, CloudHvError> { | ||||
|     let tap = tap_name_for_id(id); | ||||
|  | ||||
|     let body = format!( | ||||
|         "BR={br} | ||||
| TAP={tap} | ||||
| UIDX=$(id -u) | ||||
| GIDX=$(id -g) | ||||
|  | ||||
| # Create TAP if missing and assign to current user/group | ||||
| ip link show \"$TAP\" >/dev/null 2>&1 || ip tuntap add dev \"$TAP\" mode tap user \"$UIDX\" group \"$GIDX\" | ||||
|  | ||||
| # Enslave to bridge and bring up (idempotent) | ||||
| ip link set \"$TAP\" master \"$BR\" 2>/dev/null || true | ||||
| ip link set \"$TAP\" up | ||||
| ", | ||||
|         br = shell_escape(bridge_name), | ||||
|         tap = shell_escape(&tap), | ||||
|     ); | ||||
|     let heredoc_tap = format!("bash -e -s <<'EOF'\n{}\nEOF\n", body); | ||||
|   | ||||
|     match sal_process::run(&heredoc_tap).silent(true).execute() { | ||||
|         Ok(res) if res.success => Ok(tap), | ||||
|         Ok(res) => Err(CloudHvError::CommandFailed(format!( | ||||
|             "Failed to ensure TAP '{}': {}", | ||||
|             tap, res.stderr | ||||
|         ))), | ||||
|         Err(e) => Err(CloudHvError::CommandFailed(format!( | ||||
|             "Failed to ensure TAP '{}': {}", | ||||
|             tap, e | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn stable_mac_from_id(id: &str) -> String { | ||||
|     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 | ||||
|     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) | ||||
| } | ||||
|  | ||||
| /// Discover the mycelium IPv6 address by inspecting the interface itself (no CLI dependency). | ||||
| /// Returns (interface_name, first global IPv6 address found on the interface). | ||||
| fn get_mycelium_ipv6_addr(iface_hint: &str) -> Result<(String, String), CloudHvError> { | ||||
|     let iface = std::env::var("HERO_VIRT_MYCELIUM_IF").unwrap_or_else(|_| iface_hint.to_string()); | ||||
|  | ||||
|     // Query IPv6 addresses on the interface | ||||
|     let cmd = format!("ip -6 addr show dev {}", shell_escape(&iface)); | ||||
|     let res = sal_process::run(&cmd).silent(true).die(false).execute(); | ||||
|     let out = match res { | ||||
|         Ok(r) if r.success => r.stdout, | ||||
|         _ => { | ||||
|             return Err(CloudHvError::DependencyMissing(format!( | ||||
|                 "mycelium interface '{}' not found or no IPv6 configured", | ||||
|                 iface | ||||
|             ))) | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Extract the first global IPv6 address present on the interface. | ||||
|     for line in out.lines() { | ||||
|         let lt = line.trim(); | ||||
|         // Example line: "inet6 578:9fcf:.../7 scope global" | ||||
|         if lt.starts_with("inet6 ") && lt.contains("scope global") { | ||||
|             let parts: Vec<&str> = lt.split_whitespace().collect(); | ||||
|             if let Some(addr_cidr) = parts.get(1) { | ||||
|                 let addr_only = addr_cidr.split('/').next().unwrap_or("").trim(); | ||||
|                 if !addr_only.is_empty() && addr_only.parse::<std::net::Ipv6Addr>().is_ok() { | ||||
|                     return Ok((iface, addr_only.to_string())); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Err(CloudHvError::DependencyMissing(format!( | ||||
|         "no global IPv6 found on interface '{}'", | ||||
|         iface | ||||
|     ))) | ||||
| } | ||||
|  | ||||
| /// Derive a /64 prefix P from the mycelium IPv6 and return (P/64, P::2/64). | ||||
| fn derive_ipv6_prefix_from_mycelium(m: &str) -> Result<(String, String), CloudHvError> { | ||||
|     let ip = m | ||||
|         .parse::<std::net::Ipv6Addr>() | ||||
|         .map_err(|e| CloudHvError::InvalidSpec(format!("invalid mycelium IPv6 address '{}': {}", m, e)))?; | ||||
|     let seg = ip.segments(); // [u16; 8] | ||||
|     // Take the top /64 from the mycelium address; zero the host half | ||||
|     let pfx = std::net::Ipv6Addr::new(seg[0], seg[1], seg[2], seg[3], 0, 0, 0, 0); | ||||
|     // Router address for the bridge = P::2 | ||||
|     let router = std::net::Ipv6Addr::new(seg[0], seg[1], seg[2], seg[3], 0, 0, 0, 2); | ||||
|     let pfx_str = format!("{}/64", pfx); | ||||
|     let router_cidr = format!("{}/64", router); | ||||
|     Ok((pfx_str, router_cidr)) | ||||
| } | ||||
|  | ||||
| fn ensure_host_net_prereq_dnsmasq_nftables( | ||||
|     bridge_name: &str, | ||||
|     bridge_addr_cidr: &str, | ||||
|     subnet_cidr: &str, | ||||
|     dhcp_start: &str, | ||||
|     dhcp_end: &str, | ||||
|     ipv6_bridge_cidr: Option<&str>, | ||||
|     mycelium_if: Option<&str>, | ||||
| ) -> Result<(), CloudHvError> { | ||||
|     // Dependencies | ||||
|     for bin in ["ip", "nft", "dnsmasq", "systemctl"] { | ||||
|         if sal_process::which(bin).is_none() { | ||||
|             return Err(CloudHvError::DependencyMissing(format!( | ||||
|                 "{} not found on PATH; required for VM networking", | ||||
|                 bin | ||||
|             ))); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Prepare optional IPv6 value (empty string when disabled) | ||||
|     let ipv6_cidr = ipv6_bridge_cidr.unwrap_or(""); | ||||
|  | ||||
|     // Build idempotent setup script | ||||
|     let body = format!( | ||||
|         "set -e | ||||
| BR={br} | ||||
| BR_ADDR={br_addr} | ||||
| SUBNET={subnet} | ||||
| DHCP_START={dstart} | ||||
| DHCP_END={dend} | ||||
| IPV6_CIDR={v6cidr} | ||||
| LEASE_FILE=/var/lib/misc/dnsmasq-hero-$BR.leases | ||||
|  | ||||
| # Determine default WAN interface | ||||
| WAN_IF=$(ip -o route show default | awk '{{print $5}}' | head -n1) | ||||
| if [ -z \"$WAN_IF\" ]; then | ||||
|   echo \"No default WAN interface detected (required for IPv4 NAT)\" >&2 | ||||
|   exit 2 | ||||
| fi | ||||
|   | ||||
| # Bridge creation (idempotent) | ||||
| ip link show \"$BR\" >/dev/null 2>&1 || ip link add name \"$BR\" type bridge | ||||
| ip addr replace \"$BR_ADDR\" dev \"$BR\" | ||||
| ip link set \"$BR\" up | ||||
|  | ||||
| # IPv6 bridge address and forwarding (optional) | ||||
| if [ -n \"$IPV6_CIDR\" ]; then | ||||
|   ip -6 addr replace \"$IPV6_CIDR\" dev \"$BR\" | ||||
|   sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null || true | ||||
| fi | ||||
|  | ||||
| # IPv4 forwarding | ||||
| sysctl -w net.ipv4.ip_forward=1 >/dev/null | ||||
|  | ||||
| # nftables NAT (idempotent) for IPv4 | ||||
| nft list table ip hero >/dev/null 2>&1 || nft add table ip hero | ||||
| nft list chain ip hero postrouting >/dev/null 2>&1 || nft add chain ip hero postrouting {{ type nat hook postrouting priority 100 \\; }} | ||||
| nft list chain ip hero postrouting | grep -q \"ip saddr $SUBNET oifname \\\"$WAN_IF\\\" masquerade\" \ | ||||
|   || nft add rule ip hero postrouting ip saddr $SUBNET oifname \"$WAN_IF\" masquerade | ||||
|  | ||||
| # dnsmasq DHCPv4 + RA/DHCPv6 config (idempotent) | ||||
| mkdir -p /etc/dnsmasq.d | ||||
| mkdir -p /var/lib/misc | ||||
| CFG=/etc/dnsmasq.d/hero-$BR.conf | ||||
| TMP=/etc/dnsmasq.d/.hero-$BR.conf.new | ||||
|  | ||||
| RELOAD=0 | ||||
| CONF=/etc/dnsmasq.conf | ||||
| # Ensure conf-dir includes /etc/dnsmasq.d (simple fixed-string check to avoid regex escapes in Rust) | ||||
| if ! grep -qF \"conf-dir=/etc/dnsmasq.d\" \"$CONF\"; then | ||||
|   printf '%s\n' 'conf-dir=/etc/dnsmasq.d,*.conf' >> \"$CONF\" | ||||
|   RELOAD=1 | ||||
| fi | ||||
|  | ||||
|  | ||||
| # Ensure lease file exists and is writable by dnsmasq user | ||||
| touch \"$LEASE_FILE\" || true | ||||
| chown dnsmasq:dnsmasq \"$LEASE_FILE\" 2>/dev/null || true | ||||
|  | ||||
| # Always include IPv4 section | ||||
| printf '%s\n' \ | ||||
|   \"interface=$BR\" \ | ||||
|   \"bind-interfaces\" \ | ||||
|   \"dhcp-authoritative\" \ | ||||
|   \"dhcp-range=$DHCP_START,$DHCP_END,12h\" \ | ||||
|   \"dhcp-option=option:dns-server,1.1.1.1,8.8.8.8\" \ | ||||
|   \"dhcp-leasefile=$LEASE_FILE\" >\"$TMP\" | ||||
|   | ||||
| # Optionally append IPv6 RA/DHCPv6 | ||||
| if [ -n \"$IPV6_CIDR\" ]; then | ||||
|   printf '%s\n' \ | ||||
|     \"enable-ra\" \ | ||||
|     \"dhcp-range=::,constructor:BR_PLACEHOLDER,ra-names,64,12h\" \ | ||||
|     \"dhcp-option=option6:dns-server,[2001:4860:4860::8888],[2606:4700:4700::1111]\" >>\"$TMP\" | ||||
|   sed -i \"s/BR_PLACEHOLDER/$BR/g\" \"$TMP\" | ||||
| fi | ||||
|  | ||||
| if [ ! -f \"$CFG\" ] || ! cmp -s \"$CFG\" \"$TMP\"; then | ||||
|   mv \"$TMP\" \"$CFG\" | ||||
|   if systemctl is-active --quiet dnsmasq; then | ||||
|     systemctl reload dnsmasq || systemctl restart dnsmasq || true | ||||
|   else | ||||
|     systemctl enable --now dnsmasq || true | ||||
|   fi | ||||
| else | ||||
|   rm -f \"$TMP\" | ||||
|   systemctl enable --now dnsmasq || true | ||||
| fi | ||||
|  | ||||
| # Reload if main conf was updated to include conf-dir | ||||
| if [ \"$RELOAD\" = \"1\" ]; then | ||||
|   systemctl reload dnsmasq || systemctl restart dnsmasq || true | ||||
| fi | ||||
| ", | ||||
|         br = shell_escape(bridge_name), | ||||
|         br_addr = shell_escape(bridge_addr_cidr), | ||||
|         subnet = shell_escape(subnet_cidr), | ||||
|         dstart = shell_escape(dhcp_start), | ||||
|         dend = shell_escape(dhcp_end), | ||||
|         v6cidr = shell_escape(ipv6_cidr), | ||||
|     ); | ||||
|  | ||||
|     // Use a unique heredoc delimiter to avoid clashing with inner <<EOF blocks | ||||
|     let heredoc_net = format!("bash -e -s <<'HERONET'\n{}\nHERONET\n", body); | ||||
|  | ||||
|     match sal_process::run(&heredoc_net).silent(true).execute() { | ||||
|         Ok(res) if res.success => Ok(()), | ||||
|         Ok(res) => Err(CloudHvError::CommandFailed(format!( | ||||
|             "Host networking setup failed: {}", | ||||
|             res.stderr | ||||
|         ))), | ||||
|         Err(e) => Err(CloudHvError::CommandFailed(format!( | ||||
|             "Host networking setup failed: {}", | ||||
|             e | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Render a shell-safe command string from vector of tokens | ||||
| fn shell_join(parts: &Vec<String>) -> String { | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use sal_process; | ||||
|  | ||||
| use crate::cloudhv::CloudHvError; | ||||
|   | ||||
| @@ -42,7 +42,7 @@ fn bin_missing(name: &str) -> bool { | ||||
| /// 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 optional: Vec<String> = Vec::new(); | ||||
|     let mut notes: Vec<String> = Vec::new(); | ||||
|  | ||||
|     // Must run as root | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::fs; | ||||
| use std::path::Path; | ||||
|  | ||||
| use sal_os; | ||||
| @@ -443,7 +442,7 @@ if [ -f "$MNT_ROOT/etc/ssh/sshd_config" ]; then | ||||
|   sed -i -E 's/^[[:space:]]*AuthenticationMethods[[:space:]].*$/# hero: removed AuthenticationMethods/' "$MNT_ROOT/etc/ssh/sshd_config" 2>/dev/null || true | ||||
| fi | ||||
| if [ -d "$MNT_ROOT/etc/ssh/sshd_config.d" ]; then | ||||
|   find "$MNT_ROOT/etc/ssh/sshd_config.d" -type f -name '*.conf' -exec sed -i -E 's/^[[:space:]]*AuthenticationMethods[[:space:]].*$/# hero: removed AuthenticationMethods/' {} + 2>/dev/null \; || true | ||||
|   find "$MNT_ROOT/etc/ssh/sshd_config.d" -type f -name '*.conf' -exec sed -i -E 's/^[[:space:]]*AuthenticationMethods[[:space:]].*$/# hero: removed AuthenticationMethods/' {{}} + 2>/dev/null \; || true | ||||
| fi | ||||
|  | ||||
| # Set password for default user 'ubuntu' | ||||
| @@ -653,7 +652,7 @@ if [ -f /etc/ssh/sshd_config ]; then | ||||
|   sed -i -E 's/^[[:space:]]*AuthenticationMethods[[:space:]].*$/# hero: removed AuthenticationMethods/' /etc/ssh/sshd_config 2>/dev/null || true | ||||
| fi | ||||
| if [ -d /etc/ssh/sshd_config.d ]; then | ||||
|   find /etc/ssh/sshd_config.d -type f -name '*.conf' -exec sed -i -E 's/^[[:space:]]*AuthenticationMethods[[:space:]].*$/# hero: removed AuthenticationMethods/' {} + 2>/dev/null \; || true | ||||
|   find /etc/ssh/sshd_config.d -type f -name '*.conf' -exec sed -i -E 's/^[[:space:]]*AuthenticationMethods[[:space:]].*$/# hero: removed AuthenticationMethods/' {{}} + 2>/dev/null \; || true | ||||
| fi | ||||
|  | ||||
| # Ensure Include covers drop-ins | ||||
|   | ||||
| @@ -171,6 +171,13 @@ pub fn cloudhv_vm_info(id: &str) -> Result<Map, Box<EvalAltResult>> { | ||||
| } | ||||
|  | ||||
| pub fn cloudhv_discover_ipv4_from_leases(lease_path: &str, mac_lower: &str, timeout_secs: i64) -> Dynamic { | ||||
|     // Check verbosity from environment variable, default to verbose | ||||
|     let verbose = std::env::var("VIRT_VERBOSE").unwrap_or_else(|_| "1".to_string()) == "1"; | ||||
|  | ||||
|     if verbose { | ||||
|         println!("🔍 Discovering VM network addresses..."); | ||||
|     } | ||||
|  | ||||
|     match crate::cloudhv::net::discover_ipv4_from_leases(lease_path, mac_lower, timeout_secs as u64) { | ||||
|         Some(ip) => ip.into(), | ||||
|         None => Dynamic::UNIT, | ||||
| @@ -184,6 +191,45 @@ pub fn cloudhv_discover_ipv6_on_bridge(bridge_name: &str, mac_lower: &str) -> Dy | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn cloudhv_display_network_info(vm_id: &str, ipv4: Dynamic, ipv6: Dynamic) { | ||||
|     // Check verbosity from environment variable, default to verbose | ||||
|     let verbose = std::env::var("VIRT_VERBOSE").unwrap_or_else(|_| "1".to_string()) == "1"; | ||||
|  | ||||
|     if !verbose { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     println!("✅ VM {} is ready!", vm_id); | ||||
|     println!(""); | ||||
|     println!("🌐 Network Information:"); | ||||
|  | ||||
|     if ipv4.is_string() && !ipv4.clone().cast::<String>().is_empty() { | ||||
|         println!("   IPv4: {}", ipv4.clone().cast::<String>()); | ||||
|     } else { | ||||
|         println!("   IPv4: Not assigned yet (VM may still be configuring)"); | ||||
|     } | ||||
|  | ||||
|     if ipv6.is_string() && !ipv6.clone().cast::<String>().is_empty() { | ||||
|         println!("   IPv6: {}", ipv6.clone().cast::<String>()); | ||||
|     } else { | ||||
|         println!("   IPv6: Not available"); | ||||
|     } | ||||
|  | ||||
|     println!(""); | ||||
|     println!("💡 VM is running in the background. To connect:"); | ||||
|  | ||||
|     let ssh_addr = if ipv4.is_string() && !ipv4.clone().cast::<String>().is_empty() { | ||||
|         ipv4.cast::<String>() | ||||
|     } else { | ||||
|         "<IPv4>".to_string() | ||||
|     }; | ||||
|     println!("   SSH: ssh ubuntu@{}", ssh_addr); | ||||
|     println!(""); | ||||
|     println!("🛑 To stop the VM later:"); | ||||
|     println!("   cloudhv_vm_stop(\"{}\", false);", vm_id); | ||||
|     println!("   cloudhv_vm_delete(\"{}\", true);", vm_id); | ||||
| } | ||||
|  | ||||
| // Module registration | ||||
|  | ||||
| pub fn register_cloudhv_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | ||||
| @@ -195,5 +241,6 @@ pub fn register_cloudhv_module(engine: &mut Engine) -> Result<(), Box<EvalAltRes | ||||
|     engine.register_fn("cloudhv_vm_info", cloudhv_vm_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); | ||||
|     Ok(()) | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| use crate::cloudhv::builder::CloudHvBuilder; | ||||
| use crate::hostcheck::host_check_deps; | ||||
| use crate::image_prep::{image_prepare, Flavor as ImgFlavor, ImagePrepOptions, NetPlanOpts}; | ||||
| use rhai::{Engine, EvalAltResult, Map, Array}; | ||||
| use rhai::{Engine, EvalAltResult, Array}; | ||||
|  | ||||
| // Improved functional-style builder with better method names for fluent feel | ||||
| fn cloudhv_builder(id: &str) -> CloudHvBuilder { | ||||
| @@ -85,14 +85,37 @@ fn network_custom(b: CloudHvBuilder, args: Array) -> CloudHvBuilder { | ||||
| } | ||||
|  | ||||
| fn launch(mut b: CloudHvBuilder) -> Result<String, Box<EvalAltResult>> { | ||||
|     // Check verbosity from environment variable, default to verbose | ||||
|     let verbose = std::env::var("VIRT_VERBOSE").unwrap_or_else(|_| "1".to_string()) == "1"; | ||||
|  | ||||
|     if verbose { | ||||
|         println!("Preparing Ubuntu image and configuring VM..."); | ||||
|     } | ||||
|  | ||||
|     b.launch().map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("cloudhv builder launch failed: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         )) | ||||
|     }).map(|vm_id| { | ||||
|         if verbose { | ||||
|             println!("✅ VM launched successfully"); | ||||
|         } | ||||
|         vm_id | ||||
|     }) | ||||
| } | ||||
|  | ||||
| fn wait_for_vm_boot(seconds: i64) { | ||||
|     // Check verbosity from environment variable, default to verbose | ||||
|     let verbose = std::env::var("VIRT_VERBOSE").unwrap_or_else(|_| "1".to_string()) == "1"; | ||||
|  | ||||
|     if verbose { | ||||
|         println!("⏳ Waiting {} seconds for VM to boot and configure network...", seconds); | ||||
|     } | ||||
|  | ||||
|     std::thread::sleep(std::time::Duration::from_secs(seconds as u64)); | ||||
| } | ||||
|  | ||||
| // Noob-friendly one-shot wrapper | ||||
| fn vm_easy_launch(flavor: &str, id: &str, memory_mb: i64, vcpus: i64) -> Result<String, Box<EvalAltResult>> { | ||||
|     // Preflight | ||||
| @@ -172,6 +195,7 @@ pub fn register_cloudhv_builder_module(engine: &mut Engine) -> Result<(), Box<Ev | ||||
|  | ||||
|     // Action | ||||
|     engine.register_fn("launch", launch); | ||||
|     engine.register_fn("wait_for_vm_boot", wait_for_vm_boot); | ||||
|  | ||||
|     // One-shot wrapper | ||||
|     engine.register_fn("vm_easy_launch", vm_easy_launch); | ||||
|   | ||||
| @@ -27,9 +27,42 @@ fn report_to_map(r: &HostCheckReport) -> Map { | ||||
| } | ||||
|  | ||||
| fn host_check() -> Result<Map, Box<EvalAltResult>> { | ||||
|     // Check verbosity from environment variable, default to verbose | ||||
|     let verbose = std::env::var("VIRT_VERBOSE").unwrap_or_else(|_| "1".to_string()) == "1"; | ||||
|  | ||||
|     if verbose { | ||||
|         println!("Checking system requirements..."); | ||||
|     } | ||||
|  | ||||
|     match host_check_deps() { | ||||
|         Ok(rep) => Ok(report_to_map(&rep)), | ||||
|         Ok(rep) => { | ||||
|             if verbose { | ||||
|                 if rep.ok { | ||||
|                     println!("✅ System requirements met"); | ||||
|                 } else { | ||||
|                     println!("❌ System check failed - missing dependencies:"); | ||||
|                     if !rep.critical.is_empty() { | ||||
|                         println!("Critical:"); | ||||
|                         for dep in &rep.critical { | ||||
|                             println!("  - {}", dep); | ||||
|                         } | ||||
|                     } | ||||
|                     if !rep.optional.is_empty() { | ||||
|                         println!("Optional:"); | ||||
|                         for dep in &rep.optional { | ||||
|                             println!("  - {}", dep); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Ok(report_to_map(&rep)) | ||||
|         }, | ||||
|         Err(e) => { | ||||
|             if verbose { | ||||
|                 println!("❌ System check failed - missing dependencies:"); | ||||
|                 println!("Critical:"); | ||||
|                 println!("  - host_check failed: {}", e); | ||||
|             } | ||||
|             let mut m = Map::new(); | ||||
|             m.insert("ok".into(), Dynamic::FALSE); | ||||
|             let mut crit = Array::new(); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| use crate::image_prep::{image_prepare, Flavor, ImagePrepOptions, NetPlanOpts}; | ||||
| use rhai::{Array, Dynamic, Engine, EvalAltResult, Map}; | ||||
| use rhai::{Engine, EvalAltResult, Map}; | ||||
|  | ||||
| fn parse_flavor(s: &str) -> Result<Flavor, Box<EvalAltResult>> { | ||||
|     match s { | ||||
|   | ||||
| @@ -4,28 +4,12 @@ | ||||
| let vm_id = "vm-clean-test"; | ||||
|  | ||||
| // Phase 1: Host check | ||||
| print("Checking system requirements..."); | ||||
| let hc = host_check(); | ||||
| if !(hc.ok == true) { | ||||
|     print("❌ System check failed - missing dependencies:"); | ||||
|     if hc.critical != () && hc.critical.len() > 0 { | ||||
|         print("Critical:"); | ||||
|         for dep in hc.critical { | ||||
|             print("  - " + dep); | ||||
|         } | ||||
|     } | ||||
|     if hc.optional != () && hc.optional.len() > 0 { | ||||
|         print("Optional:"); | ||||
|         for dep in hc.optional { | ||||
|             print("  - " + dep); | ||||
|         } | ||||
|     } | ||||
|     throw "Host check failed: missing dependencies"; | ||||
| } | ||||
| print("✅ System requirements met"); | ||||
|  | ||||
| // Phase 2: Create VM using fluent builder pattern | ||||
| print("Preparing Ubuntu image and configuring VM..."); | ||||
| let vm_id_actual = ""; | ||||
| try { | ||||
|     vm_id_actual = cloudhv_builder(vm_id) | ||||
| @@ -39,37 +23,15 @@ try { | ||||
| } | ||||
|  | ||||
| // Phase 3: Wait for VM to boot and get network configuration | ||||
| print("✅ VM launched successfully"); | ||||
| print("⏳ Waiting 10 seconds for VM to boot and configure network..."); | ||||
| sleep(10);  | ||||
| wait_for_vm_boot(10); | ||||
|  | ||||
| // Phase 4: Discover VM IP addresses | ||||
| print("🔍 Discovering VM network 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 5: Display connection info | ||||
| print("✅ VM " + vm_id_actual + " is ready!"); | ||||
| print(""); | ||||
| print("🌐 Network Information:"); | ||||
| if ipv4 != () && ipv4 != "" { | ||||
|     print("   IPv4: " + ipv4); | ||||
| } else { | ||||
|     print("   IPv4: Not assigned yet (VM may still be configuring)"); | ||||
| } | ||||
| if ipv6 != () && ipv6 != "" { | ||||
|     print("   IPv6: " + ipv6); | ||||
| } else { | ||||
|     print("   IPv6: Not available"); | ||||
| } | ||||
| print(""); | ||||
| print("💡 VM is running in the background. To connect:"); | ||||
| print("   SSH: ssh ubuntu@" + (if ipv4 != () && ipv4 != "" { ipv4 } else { "<IPv4>" })); | ||||
| print(""); | ||||
| print("🛑 To stop the VM later:"); | ||||
| print("   cloudhv_vm_stop(\"" + vm_id_actual + "\", false);"); | ||||
| print("   cloudhv_vm_delete(\"" + vm_id_actual + "\", true);"); | ||||
| cloudhv_display_network_info(vm_id_actual, ipv4, ipv6); | ||||
|  | ||||
| /* | ||||
| try { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user