WIP2
This commit is contained in:
		| @@ -266,12 +266,12 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> { | ||||
|  | ||||
|             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", | ||||
|                 let heredoc2 = format!( | ||||
|                     "bash -e -s <<'EOF'\nset -euo pipefail\nqemu-img convert -O raw {} - | dd of={} bs=4M status=none\nEOF\n", | ||||
|                     shell_escape(&disk_to_use), | ||||
|                     shell_escape(&dest) | ||||
|                 ); | ||||
|                 match sal_process::run(&cmd2).silent(true).die(false).execute() { | ||||
|                 match sal_process::run(&heredoc2).silent(true).die(false).execute() { | ||||
|                     Ok(res) if res.success => { | ||||
|                         converted_ok = true; | ||||
|                     } | ||||
| @@ -407,7 +407,31 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> { | ||||
|             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()); | ||||
|  | ||||
|   | ||||
|         // Optional IPv6 over Mycelium provisioning (bridge P::2/64 + RA) guarded by env and detection. | ||||
|         let ipv6_env = std::env::var("HERO_VIRT_IPV6_ENABLE").unwrap_or_else(|_| "".into()); | ||||
|         let mut ipv6_enabled = ipv6_env.eq_ignore_ascii_case("1") || ipv6_env.eq_ignore_ascii_case("true"); | ||||
|         let mycelium_if_cfg = std::env::var("HERO_VIRT_MYCELIUM_IF").unwrap_or_else(|_| "mycelium".into()); | ||||
|         let mut ipv6_bridge_cidr: Option<String> = None; | ||||
|         let mut mycelium_if_opt: Option<String> = None; | ||||
|         let enable_auto = ipv6_env.is_empty(); // auto-enable if mycelium is detected and not explicitly disabled | ||||
|   | ||||
|         if ipv6_enabled || enable_auto { | ||||
|             if let Ok(cidr) = std::env::var("HERO_VIRT_IPV6_BRIDGE_CIDR") { | ||||
|                 // Explicit override for bridge IPv6 (e.g., "400:...::2/64") | ||||
|                 ipv6_bridge_cidr = Some(cidr); | ||||
|                 mycelium_if_opt = Some(mycelium_if_cfg.clone()); | ||||
|                 ipv6_enabled = true; | ||||
|             } else if let Ok((ifname, myc_addr)) = get_mycelium_ipv6_addr(&mycelium_if_cfg) { | ||||
|                 // Derive P::2/64 from the mycelium node address | ||||
|                 if let Ok((_pfx, router_cidr)) = derive_ipv6_prefix_from_mycelium(&myc_addr) { | ||||
|                     ipv6_bridge_cidr = Some(router_cidr); | ||||
|                     mycelium_if_opt = Some(ifname); | ||||
|                     ipv6_enabled = true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
|         // Ensure host-side networking (requires root privileges / CAP_NET_ADMIN) | ||||
|         ensure_host_net_prereq_dnsmasq_nftables( | ||||
|             &bridge_name, | ||||
| @@ -415,6 +439,8 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> { | ||||
|             &subnet_cidr, | ||||
|             &dhcp_start, | ||||
|             &dhcp_end, | ||||
|             ipv6_bridge_cidr.as_deref(), | ||||
|             mycelium_if_opt.as_deref(), | ||||
|         )?; | ||||
|  | ||||
|         // Ensure a TAP device for this VM and attach to the bridge | ||||
| @@ -432,18 +458,15 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> { | ||||
|     } | ||||
|  | ||||
|     let args_str = shell_join(&parts); | ||||
|     let script = format!( | ||||
|         "#!/bin/bash -e | ||||
| nohup {} > '{}' 2>&1 & | ||||
| echo $! > '{}' | ||||
| ", | ||||
|     // Execute via a bash heredoc to avoid any quoting pitfalls | ||||
|     let heredoc = format!( | ||||
|         "bash -e -s <<'EOF'\nnohup {} > '{}' 2>&1 &\necho $! > '{}'\nEOF\n", | ||||
|         args_str, | ||||
|         log_file, | ||||
|         vm_pid_path(id).to_string_lossy() | ||||
|     ); | ||||
|  | ||||
|     // Execute script; this will background cloud-hypervisor and return | ||||
|     let result = sal_process::run(&script).execute(); | ||||
|     // Execute command; this will background cloud-hypervisor and return | ||||
|     let result = sal_process::run(&heredoc).execute(); | ||||
|     match result { | ||||
|         Ok(res) => { | ||||
|             if !res.success { | ||||
| @@ -644,9 +667,8 @@ fn tap_name_for_id(id: &str) -> String { | ||||
| fn ensure_tap_for_vm(bridge_name: &str, id: &str) -> Result<String, CloudHvError> { | ||||
|     let tap = tap_name_for_id(id); | ||||
|  | ||||
|     let script = format!( | ||||
|         "#!/bin/bash -e | ||||
| BR={br} | ||||
|     let body = format!( | ||||
|         "BR={br} | ||||
| TAP={tap} | ||||
| UIDX=$(id -u) | ||||
| GIDX=$(id -g) | ||||
| @@ -661,8 +683,9 @@ ip link set \"$TAP\" up | ||||
|         br = shell_escape(bridge_name), | ||||
|         tap = shell_escape(&tap), | ||||
|     ); | ||||
|  | ||||
|     match sal_process::run(&script).silent(true).execute() { | ||||
|     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 '{}': {}", | ||||
| @@ -688,12 +711,75 @@ fn stable_mac_from_id(id: &str) -> String { | ||||
|     format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", b0, b1, b2, b3, b4, b5) | ||||
| } | ||||
|  | ||||
| /// Discover the mycelium IPv6 address and validate the interface. | ||||
| /// | ||||
| /// Returns (interface_name, address_string). | ||||
| fn get_mycelium_ipv6_addr(iface_hint: &str) -> Result<(String, String), CloudHvError> { | ||||
|     // Parse `mycelium inspect` for "Address: ..." | ||||
|     let insp = sal_process::run("mycelium inspect").silent(true).die(false).execute(); | ||||
|     let mut addr: Option<String> = None; | ||||
|     if let Ok(res) = insp { | ||||
|         if res.success { | ||||
|             for l in res.stdout.lines() { | ||||
|                 let lt = l.trim(); | ||||
|                 if let Some(rest) = lt.strip_prefix("Address:") { | ||||
|                     let val = rest.trim().trim_matches('"').to_string(); | ||||
|                     if !val.is_empty() { | ||||
|                         addr = Some(val); | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     let iface = std::env::var("HERO_VIRT_MYCELIUM_IF").unwrap_or_else(|_| iface_hint.to_string()); | ||||
|  | ||||
|     // Validate interface exists and has IPv6 configured (best-effort) | ||||
|     let cmd = format!("ip -6 addr show dev {}", shell_escape(&iface)); | ||||
|     match sal_process::run(&cmd).silent(true).die(false).execute() { | ||||
|         Ok(r) if r.success => { | ||||
|             // proceed | ||||
|         } | ||||
|         _ => { | ||||
|             return Err(CloudHvError::DependencyMissing(format!( | ||||
|                 "mycelium interface '{}' not found or no IPv6 configured", | ||||
|                 iface | ||||
|             ))); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if let Some(a) = addr { | ||||
|         Ok((iface, a)) | ||||
|     } else { | ||||
|         Err(CloudHvError::DependencyMissing( | ||||
|             "failed to read mycelium IPv6 address via `mycelium inspect`".into(), | ||||
|         )) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// 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"] { | ||||
| @@ -705,16 +791,18 @@ fn ensure_host_net_prereq_dnsmasq_nftables( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Build idempotent setup script | ||||
|     let script = format!( | ||||
|         "#!/bin/bash -e | ||||
| set -e | ||||
|     // 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} | ||||
|  | ||||
| # Determine default WAN interface | ||||
| WAN_IF=$(ip -o route show default | awk '{{print $5}}' | head -n1) | ||||
| @@ -724,23 +812,27 @@ 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 placeholder address + forward (temporary) | ||||
| ip -6 addr add 400::1/64 dev \"$BR\" 2>/dev/null || true | ||||
| sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null || true | ||||
| # 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) | ||||
| # 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 DHCP config (idempotent) | ||||
| # dnsmasq DHCPv4 + RA/DHCPv6 config (idempotent) | ||||
| mkdir -p /etc/dnsmasq.d | ||||
| CFG=/etc/dnsmasq.d/hero-$BR.conf | ||||
| TMP=/etc/dnsmasq.d/.hero-$BR.conf.new | ||||
|  | ||||
| # Always include IPv4 section | ||||
| cat >\"$TMP\" <<EOF | ||||
| interface=$BR | ||||
| bind-interfaces | ||||
| @@ -748,6 +840,16 @@ dhcp-range=$DHCP_START,$DHCP_END,12h | ||||
| dhcp-option=option:dns-server,1.1.1.1,8.8.8.8 | ||||
| EOF | ||||
|  | ||||
| # Optionally append IPv6 RA/DHCPv6 | ||||
| if [ -n \"$IPV6_CIDR\" ]; then | ||||
|   cat >>\"$TMP\" <<'EOFV6' | ||||
| enable-ra | ||||
| dhcp-range=::,constructor:BR_PLACEHOLDER,ra-names,64,12h | ||||
| dhcp-option=option6:dns-server,[2001:4860:4860::8888],[2606:4700:4700::1111] | ||||
| EOFV6 | ||||
|   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 | ||||
| @@ -765,9 +867,13 @@ fi | ||||
|         subnet = shell_escape(subnet_cidr), | ||||
|         dstart = shell_escape(dhcp_start), | ||||
|         dend = shell_escape(dhcp_end), | ||||
|         v6cidr = shell_escape(ipv6_cidr), | ||||
|     ); | ||||
|  | ||||
|     match sal_process::run(&script).silent(true).execute() { | ||||
|     // 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: {}", | ||||
|   | ||||
| @@ -140,10 +140,53 @@ pub fn host_check_deps() -> Result<HostCheckReport, HostCheckError> { | ||||
|             let _ = fs::remove_file(&probe_path); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
|     // Optional Mycelium IPv6 checks when enabled via env | ||||
|     let ipv6_env = std::env::var("HERO_VIRT_IPV6_ENABLE").unwrap_or_else(|_| "".into()); | ||||
|     let ipv6_enabled = ipv6_env.eq_ignore_ascii_case("1") || ipv6_env.eq_ignore_ascii_case("true"); | ||||
|     if ipv6_enabled { | ||||
|         // Require mycelium CLI | ||||
|         if bin_missing("mycelium") { | ||||
|             critical.push("mycelium CLI not found on PATH (required when HERO_VIRT_IPV6_ENABLE=true)".into()); | ||||
|         } | ||||
|         // Validate interface presence and global IPv6 | ||||
|         let ifname = std::env::var("HERO_VIRT_MYCELIUM_IF").unwrap_or_else(|_| "mycelium".into()); | ||||
|         let check_if = sal_process::run(&format!("ip -6 addr show dev {}", ifname)) | ||||
|             .silent(true) | ||||
|             .die(false) | ||||
|             .execute(); | ||||
|         match check_if { | ||||
|             Ok(r) if r.success => { | ||||
|                 let out = r.stdout; | ||||
|                 if !(out.contains("inet6") && out.contains("scope global")) { | ||||
|                     notes.push(format!( | ||||
|                         "iface '{}' present but no global IPv6 detected; Mycelium may not be up yet", | ||||
|                         ifname | ||||
|                     )); | ||||
|                 } | ||||
|             } | ||||
|             _ => { | ||||
|                 critical.push(format!( | ||||
|                     "iface '{}' not found or no IPv6; ensure Mycelium is running", | ||||
|                     ifname | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
|         // Best-effort: parse `mycelium inspect` for Address | ||||
|         let insp = sal_process::run("mycelium inspect").silent(true).die(false).execute(); | ||||
|         match insp { | ||||
|             Ok(res) if res.success && res.stdout.contains("Address:") => { | ||||
|                 // good enough | ||||
|             } | ||||
|             _ => { | ||||
|                 notes.push("`mycelium inspect` did not return an Address; IPv6 overlay may be unavailable".into()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
|     // Summarize ok flag | ||||
|     let ok = critical.is_empty(); | ||||
|  | ||||
|   | ||||
|     Ok(HostCheckReport { | ||||
|         ok, | ||||
|         critical, | ||||
|   | ||||
| @@ -52,9 +52,9 @@ impl Default for NetPlanOpts { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             dhcp4: true, | ||||
|             dhcp6: false, | ||||
|             ipv6_addr: Some("400::10/64".into()), | ||||
|             gw6: Some("400::1".into()), | ||||
|             dhcp6: true, | ||||
|             ipv6_addr: None, | ||||
|             gw6: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -154,8 +154,6 @@ pub fn image_prepare(opts: &ImagePrepOptions) -> Result<ImagePrepResult, ImagePr | ||||
|     match opts.flavor { | ||||
|         Flavor::Ubuntu => { | ||||
|             // Build bash script that performs all steps and echos "RAW|ROOT_UUID|BOOT_UUID" at end | ||||
|             let net_ipv6 = opts.net.ipv6_addr.clone().unwrap_or_else(|| "400::10/64".into()); | ||||
|             let gw6 = opts.net.gw6.clone().unwrap_or_else(|| "400::1".into()); | ||||
|             let disable_ci_net = opts.disable_cloud_init_net; | ||||
|  | ||||
|             // Keep script small and robust; avoid brace-heavy awk to simplify escaping. | ||||
| @@ -332,11 +330,6 @@ network: | ||||
|     eth0: | ||||
|       dhcp4: {dhcp4} | ||||
|       dhcp6: {dhcp6} | ||||
|       addresses: | ||||
|         - {ipv6} | ||||
|       routes: | ||||
|         - to: \"::/0\" | ||||
|           via: {gw6} | ||||
|       nameservers: | ||||
|         addresses: [8.8.8.8, 1.1.1.1, 2001:4860:4860::8888] | ||||
| EOF | ||||
| @@ -370,8 +363,6 @@ exit 0 | ||||
|                 raw = shell_escape(&raw_path), | ||||
|                 dhcp4 = if opts.net.dhcp4 { "true" } else { "false" }, | ||||
|                 dhcp6 = if opts.net.dhcp6 { "true" } else { "false" }, | ||||
|                 ipv6 = shell_escape(&net_ipv6), | ||||
|                 gw6 = shell_escape(&gw6), | ||||
|                 disable_ci_net = if disable_ci_net { "true" } else { "false" }, | ||||
|             ); | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
| //      /images/noble-server-cloudimg-amd64.img | ||||
| //      /images/alpine-virt-cloudimg-amd64.qcow2 (Alpine prepare not implemented yet) | ||||
| //      /images/hypervisor-fw (firmware binary used via --kernel) | ||||
| //  - Network defaults: IPv4 NAT + dnsmasq DHCP; placeholder IPv6 on bridge + guest netplan. | ||||
|  //  - Network defaults: IPv4 NAT (dnsmasq DHCP) + IPv6 routed over Mycelium (RA/DHCPv6). No static IPv6 is written into the guest; it autoconfigures via RA. | ||||
| // | ||||
| // Conventions: | ||||
| //  - Functional builder chaining: b = memory_mb(b, 4096), etc. | ||||
| @@ -81,12 +81,6 @@ let prep_opts = #{ | ||||
|     flavor: "ubuntu", | ||||
|     // source: optional override, default uses /images/noble-server-cloudimg-amd64.img | ||||
|     // target_dir: optional override, default $HOME/hero/virt/vms/<id> | ||||
|     net: #{ | ||||
|         dhcp4: true, | ||||
|         dhcp6: false, | ||||
|         ipv6_addr: "400::10/64", | ||||
|         gw6: "400::1", | ||||
|     }, | ||||
|     disable_cloud_init_net: true, | ||||
| }; | ||||
|  | ||||
| @@ -150,6 +144,7 @@ try { | ||||
|     fail("cloudhv_vm_list failed: " + e.to_string()); | ||||
| } | ||||
|  | ||||
| sleep(1000000); | ||||
| // ------------------------------------------------------------------------------------ | ||||
| // Phase 5: Stop & delete VM A | ||||
| // ------------------------------------------------------------------------------------ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user