networking VMs (WIP)
This commit is contained in:
		@@ -5,6 +5,8 @@ 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;
 | 
			
		||||
@@ -299,6 +301,32 @@ 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());
 | 
			
		||||
 | 
			
		||||
    // 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,
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    // 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);
 | 
			
		||||
 | 
			
		||||
    parts.push("--net".into());
 | 
			
		||||
    parts.push(format!("tap={},mac={}", tap_name, mac));
 | 
			
		||||
 | 
			
		||||
    if let Some(extra) = rec.spec.extra_args.clone() {
 | 
			
		||||
        for e in extra {
 | 
			
		||||
            parts.push(e);
 | 
			
		||||
@@ -480,6 +508,150 @@ 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 script = format!(
 | 
			
		||||
        "#!/bin/bash -e
 | 
			
		||||
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),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    match sal_process::run(&script).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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn ensure_host_net_prereq_dnsmasq_nftables(
 | 
			
		||||
    bridge_name: &str,
 | 
			
		||||
    bridge_addr_cidr: &str,
 | 
			
		||||
    subnet_cidr: &str,
 | 
			
		||||
    dhcp_start: &str,
 | 
			
		||||
    dhcp_end: &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
 | 
			
		||||
            )));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Build idempotent setup script
 | 
			
		||||
    let script = format!(
 | 
			
		||||
        "#!/bin/bash -e
 | 
			
		||||
set -e
 | 
			
		||||
 | 
			
		||||
BR={br}
 | 
			
		||||
BR_ADDR={br_addr}
 | 
			
		||||
SUBNET={subnet}
 | 
			
		||||
DHCP_START={dstart}
 | 
			
		||||
DHCP_END={dend}
 | 
			
		||||
 | 
			
		||||
# Determine default WAN interface
 | 
			
		||||
WAN_IF=$(ip -o route show default | awk '{{print $5}}' | head -n1)
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
 | 
			
		||||
# IPv4 forwarding
 | 
			
		||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null
 | 
			
		||||
 | 
			
		||||
# nftables NAT (idempotent)
 | 
			
		||||
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)
 | 
			
		||||
mkdir -p /etc/dnsmasq.d
 | 
			
		||||
CFG=/etc/dnsmasq.d/hero-$BR.conf
 | 
			
		||||
TMP=/etc/dnsmasq.d/.hero-$BR.conf.new
 | 
			
		||||
cat >\"$TMP\" <<EOF
 | 
			
		||||
interface=$BR
 | 
			
		||||
bind-interfaces
 | 
			
		||||
dhcp-range=$DHCP_START,$DHCP_END,12h
 | 
			
		||||
dhcp-option=option:dns-server,1.1.1.1,8.8.8.8
 | 
			
		||||
EOF
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
",
 | 
			
		||||
        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),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    match sal_process::run(&script).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 {
 | 
			
		||||
    let mut s = String::new();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										148
									
								
								packages/system/virt/tests/rhai/05_cloudhv_diag.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								packages/system/virt/tests/rhai/05_cloudhv_diag.rhai
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,148 @@
 | 
			
		||||
// Cloud Hypervisor diagnostic script
 | 
			
		||||
// Creates a VM, starts CH, verifies PID, API socket, ch-remote info, and tails logs.
 | 
			
		||||
 | 
			
		||||
print("=== CloudHV Diagnostic ===");
 | 
			
		||||
 | 
			
		||||
// Dependency check
 | 
			
		||||
let chs = which("cloud-hypervisor-static");
 | 
			
		||||
let chrs = which("ch-remote-static");
 | 
			
		||||
let ch_missing = (chs == () || chs == "");
 | 
			
		||||
let chr_missing = (chrs == () || chrs == "");
 | 
			
		||||
if ch_missing || chr_missing {
 | 
			
		||||
    print("cloud-hypervisor-static and/or ch-remote-static not available - aborting.");
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Inputs
 | 
			
		||||
let firmware_path = "/tmp/virt_images/hypervisor-fw";
 | 
			
		||||
let disk_path = "/tmp/virt_images/noble-server-cloudimg-amd64.img";
 | 
			
		||||
 | 
			
		||||
if !exist(firmware_path) {
 | 
			
		||||
    print(`Firmware not found: ${firmware_path}`);
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
if !exist(disk_path) {
 | 
			
		||||
    print(`Disk image not found: ${disk_path}`);
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Unique ID
 | 
			
		||||
let rid = run_silent("date +%s%N");
 | 
			
		||||
let suffix = if rid.success && rid.stdout != "" { rid.stdout.trim() } else { "100000" };
 | 
			
		||||
let vm_id = `diagvm_${suffix}`;
 | 
			
		||||
 | 
			
		||||
// Socket path will be obtained from VM info (SAL populates spec.api_socket after start)
 | 
			
		||||
 | 
			
		||||
// Build minimal spec; let SAL decide the api_socket under the VM dir
 | 
			
		||||
let spec = #{
 | 
			
		||||
    "id": vm_id,
 | 
			
		||||
    "disk_path": disk_path,
 | 
			
		||||
    "vcpus": 1,
 | 
			
		||||
    "memory_mb": 512
 | 
			
		||||
};
 | 
			
		||||
spec.firmware_path = firmware_path;
 | 
			
		||||
 | 
			
		||||
fn pid_alive(p) {
 | 
			
		||||
    if p == () { return false; }
 | 
			
		||||
    // Use /proc to avoid noisy "kill: No such process" messages from kill -0
 | 
			
		||||
    return exist(`/proc/${p}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn tail_log(p, n) {
 | 
			
		||||
    if exist(p) {
 | 
			
		||||
        let r = run_silent(`tail -n ${n} ${p}`);
 | 
			
		||||
        if r.success { print(r.stdout); } else { print(r.stderr); }
 | 
			
		||||
    } else {
 | 
			
		||||
        print(`Log file not found: ${p}`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
    print("--- Create VM spec ---");
 | 
			
		||||
    let created = cloudhv_vm_create(spec);
 | 
			
		||||
    print(`created: ${created}`);
 | 
			
		||||
} catch (err) {
 | 
			
		||||
    print(`create failed: ${err}`);
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Read back info to get SAL-resolved log_file path
 | 
			
		||||
let info0 = cloudhv_vm_info(vm_id);
 | 
			
		||||
let log_file = info0.runtime.log_file;
 | 
			
		||||
 | 
			
		||||
// Rely on SAL to handle socket directory creation and stale-socket cleanup
 | 
			
		||||
 | 
			
		||||
print("--- Start VM ---");
 | 
			
		||||
try {
 | 
			
		||||
    cloudhv_vm_start(vm_id);
 | 
			
		||||
    print("start invoked");
 | 
			
		||||
} catch (err) {
 | 
			
		||||
    print(`start failed: ${err}`);
 | 
			
		||||
    tail_log(log_file, 200);
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Fetch PID and discover API socket path from updated spec
 | 
			
		||||
let info1 = cloudhv_vm_info(vm_id);
 | 
			
		||||
let pid = info1.runtime.pid;
 | 
			
		||||
let api_sock = info1.spec.api_socket;
 | 
			
		||||
print(`pid=${pid}`);
 | 
			
		||||
print(`api_sock_from_sal=${api_sock}`);
 | 
			
		||||
 | 
			
		||||
// Wait for socket file
 | 
			
		||||
let sock_ok = false;
 | 
			
		||||
for x in 0..50 {
 | 
			
		||||
    if exist(api_sock) { sock_ok = true; break; }
 | 
			
		||||
    sleep(1);
 | 
			
		||||
}
 | 
			
		||||
print(`api_sock_exists=${sock_ok} path=${api_sock}`);
 | 
			
		||||
 | 
			
		||||
// Probe ch-remote info
 | 
			
		||||
let info_ok = false;
 | 
			
		||||
let last_err = "";
 | 
			
		||||
if sock_ok {
 | 
			
		||||
    for x in 0..20 {
 | 
			
		||||
        let r = run_silent(`ch-remote-static --api-socket ${api_sock} info`);
 | 
			
		||||
        if r.success {
 | 
			
		||||
            info_ok = true;
 | 
			
		||||
            print("ch-remote info OK");
 | 
			
		||||
            break;
 | 
			
		||||
        } else {
 | 
			
		||||
            last_err = if r.stderr != "" { r.stderr } else { r.stdout };
 | 
			
		||||
            sleep(1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
if !info_ok {
 | 
			
		||||
    print("ch-remote info FAILED");
 | 
			
		||||
    if last_err != "" { print(last_err); }
 | 
			
		||||
    let alive = pid_alive(pid);
 | 
			
		||||
    print(`pid_alive=${alive}`);
 | 
			
		||||
    print("--- Last 200 lines of CH log ---");
 | 
			
		||||
    tail_log(log_file, 200);
 | 
			
		||||
    print("--- End of log ---");
 | 
			
		||||
} else {
 | 
			
		||||
    print("--- Stop via SAL (force) ---");
 | 
			
		||||
    try {
 | 
			
		||||
        cloudhv_vm_stop(vm_id, true);
 | 
			
		||||
        print("SAL stop invoked (force)");
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        print(`stop failed: ${err}`);
 | 
			
		||||
    }
 | 
			
		||||
    // wait for exit (check original PID)
 | 
			
		||||
    for x in 0..30 {
 | 
			
		||||
        if !pid_alive(pid) { break; }
 | 
			
		||||
        sleep(1);
 | 
			
		||||
    }
 | 
			
		||||
    print(`pid_alive_after_stop=${pid_alive(pid)}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
print("--- Cleanup ---");
 | 
			
		||||
try {
 | 
			
		||||
    cloudhv_vm_delete(vm_id, false);
 | 
			
		||||
    print("vm deleted");
 | 
			
		||||
} catch (err) {
 | 
			
		||||
    print(`delete failed: ${err}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
print("=== Diagnostic done ===");
 | 
			
		||||
							
								
								
									
										309
									
								
								packages/system/virt/tests/rhai/06_cloudhv_cloudinit_dhcpd.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										309
									
								
								packages/system/virt/tests/rhai/06_cloudhv_cloudinit_dhcpd.rhai
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,309 @@
 | 
			
		||||
// Cloud-init NoCloud + host DHCP (dnsmasq) provisioning for Cloud Hypervisor
 | 
			
		||||
// - Accepts a user-supplied SSH public key
 | 
			
		||||
// - Ensures Ubuntu cloud image via SAL qcow2 builder
 | 
			
		||||
// - Sets up host bridge br0 and tap0, and runs an ephemeral dnsmasq bound to br0
 | 
			
		||||
// - Builds NoCloud seed ISO (cloud-localds preferred; genisoimage fallback)
 | 
			
		||||
// - Creates/starts a VM and prints SSH connection instructions
 | 
			
		||||
//
 | 
			
		||||
// Requirements (run this script with privileges that allow sudo commands):
 | 
			
		||||
//   - cloud-hypervisor-static, ch-remote-static
 | 
			
		||||
//   - cloud-image-utils (for cloud-localds) or genisoimage/xorriso
 | 
			
		||||
//   - dnsmasq, iproute2
 | 
			
		||||
//   - qemu tools already used by qcow2 builder
 | 
			
		||||
//
 | 
			
		||||
// Note: This script uses sudo for network and dnsmasq operations.
 | 
			
		||||
 | 
			
		||||
print("=== CloudHV + cloud-init + host DHCP (dnsmasq) ===");
 | 
			
		||||
 | 
			
		||||
// ----------- User input -----------
 | 
			
		||||
let user_pubkey = "ssh-ed25519 REPLACE_WITH_YOUR_PUBLIC_KEY user@host";
 | 
			
		||||
 | 
			
		||||
// Optional: choose boot method. If firmware is present in common locations, it will be used.
 | 
			
		||||
// Otherwise, if kernel_path exists, direct kernel boot will be used.
 | 
			
		||||
// If neither is found, the script will abort before starting the VM.
 | 
			
		||||
let firmware_path_override = "";      // e.g., "/usr/share/cloud-hypervisor/hypervisor-fw"
 | 
			
		||||
let kernel_path_override   = "";      // e.g., "/path/to/vmlinux"
 | 
			
		||||
let kernel_cmdline_override = "console=ttyS0 reboot=k panic=1";
 | 
			
		||||
 | 
			
		||||
// Network parameters (local-only setup)
 | 
			
		||||
let bridge   = "br0";
 | 
			
		||||
let br_cidr  = "192.168.127.1/24";
 | 
			
		||||
let br_ip    = "192.168.127.1";
 | 
			
		||||
let tap      = "tap0";
 | 
			
		||||
let mac      = "02:00:00:00:00:10"; // locally administered MAC
 | 
			
		||||
 | 
			
		||||
// Paths
 | 
			
		||||
let base_dir     = "/tmp/virt_images";
 | 
			
		||||
let seed_iso     = `${base_dir}/seed.iso`;
 | 
			
		||||
let user_data    = `${base_dir}/user-data`;
 | 
			
		||||
let meta_data    = `${base_dir}/meta-data`;
 | 
			
		||||
let dnsmasq_pid  = `${base_dir}/dnsmasq.pid`;
 | 
			
		||||
let dnsmasq_lease= `${base_dir}/dnsmasq.leases`;
 | 
			
		||||
let dnsmasq_log  = `${base_dir}/dnsmasq.log`;
 | 
			
		||||
 | 
			
		||||
// ----------- Dependency checks -----------
 | 
			
		||||
print("\n--- Checking dependencies ---");
 | 
			
		||||
let chs = which("cloud-hypervisor-static");
 | 
			
		||||
let chrs = which("ch-remote-static");
 | 
			
		||||
let clds = which("cloud-localds");
 | 
			
		||||
let geniso = which("genisoimage");
 | 
			
		||||
let dns = which("dnsmasq");
 | 
			
		||||
let ipt = which("ip");
 | 
			
		||||
 | 
			
		||||
let missing = false;
 | 
			
		||||
if chs == () || chs == "" {
 | 
			
		||||
    print("❌ cloud-hypervisor-static not found on PATH");
 | 
			
		||||
    missing = true;
 | 
			
		||||
}
 | 
			
		||||
if chrs == () || chrs == "" {
 | 
			
		||||
    print("❌ ch-remote-static not found on PATH");
 | 
			
		||||
    missing = true;
 | 
			
		||||
}
 | 
			
		||||
if (clds == () || clds == "") && (geniso == () || geniso == "") {
 | 
			
		||||
    print("❌ Neither cloud-localds nor genisoimage is available. Install cloud-image-utils or genisoimage.");
 | 
			
		||||
    missing = true;
 | 
			
		||||
}
 | 
			
		||||
if dns == () || dns == "" {
 | 
			
		||||
    print("❌ dnsmasq not found on PATH");
 | 
			
		||||
    missing = true;
 | 
			
		||||
}
 | 
			
		||||
if ipt == () || ipt == "" {
 | 
			
		||||
    print("❌ ip (iproute2) not found on PATH");
 | 
			
		||||
    missing = true;
 | 
			
		||||
}
 | 
			
		||||
if missing {
 | 
			
		||||
    print("=== Aborting due to missing dependencies ===");
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
print("✓ Dependencies look OK");
 | 
			
		||||
 | 
			
		||||
// ----------- Ensure base image -----------
 | 
			
		||||
print("\n--- Ensuring Ubuntu 24.04 cloud image ---");
 | 
			
		||||
let base;
 | 
			
		||||
try {
 | 
			
		||||
    // Adjust the size_gb as desired; this resizes the cloud image sparsely.
 | 
			
		||||
    base = qcow2_build_ubuntu_24_04_base(base_dir, 10);
 | 
			
		||||
} catch (err) {
 | 
			
		||||
    print(`❌ Failed to build/ensure base image: ${err}`);
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
let disk_path = base.base_image_path;
 | 
			
		||||
print(`✓ Using base image: ${disk_path}`);
 | 
			
		||||
 | 
			
		||||
// ----------- Host networking (bridge + tap) -----------
 | 
			
		||||
print("\n--- Configuring host networking (bridge + tap) ---");
 | 
			
		||||
// Idempotent: create br0 if missing; assign IP if not present; set up
 | 
			
		||||
run_silent(`sudo sh -lc 'ip link show ${bridge} >/dev/null 2>&1 || ip link add ${bridge} type bridge'`);
 | 
			
		||||
run_silent(`sudo sh -lc 'ip addr show dev ${bridge} | grep -q "${br_cidr}" || ip addr add ${br_cidr} dev ${bridge}'`);
 | 
			
		||||
run_silent(`sudo sh -lc 'ip link set ${bridge} up'`);
 | 
			
		||||
 | 
			
		||||
// Idempotent: create tap and attach to bridge
 | 
			
		||||
run_silent(`sudo sh -lc 'ip link show ${tap} >/dev/null 2>&1 || ip tuntap add dev ${tap} mode tap'`);
 | 
			
		||||
run_silent(`sudo sh -lc 'bridge link | grep -q "${tap}" || ip link set ${tap} master ${bridge}'`);
 | 
			
		||||
run_silent(`sudo sh -lc 'ip link set ${tap} up'`);
 | 
			
		||||
print(`✓ Bridge ${bridge} and tap ${tap} configured`);
 | 
			
		||||
 | 
			
		||||
// ----------- Start/ensure dnsmasq on br0 -----------
 | 
			
		||||
print("\n--- Ensuring dnsmasq serving DHCP on the bridge ---");
 | 
			
		||||
// If an instance with our pid-file is running, keep it; otherwise start a new one bound to br0.
 | 
			
		||||
// Use --port=0 to avoid DNS port conflicts; we only need DHCP here.
 | 
			
		||||
let dns_state = run_silent(`bash -lc 'if [ -f ${dnsmasq_pid} ] && ps -p $(cat ${dnsmasq_pid}) >/dev/null 2>&1; then echo RUNNING; else echo STOPPED; fi'`);
 | 
			
		||||
let need_start = true;
 | 
			
		||||
if dns_state.success && dns_state.stdout.trim() == "RUNNING" {
 | 
			
		||||
    print("✓ dnsmasq already running (pid file present and alive)");
 | 
			
		||||
    need_start = false;
 | 
			
		||||
} else {
 | 
			
		||||
    // Clean stale files
 | 
			
		||||
    run_silent(`bash -lc 'rm -f ${dnsmasq_pid} ${dnsmasq_lease}'`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if need_start {
 | 
			
		||||
    let cmd = `
 | 
			
		||||
        nohup sudo dnsmasq \
 | 
			
		||||
            --port=0 \
 | 
			
		||||
            --bind-interfaces \
 | 
			
		||||
            --except-interface=lo \
 | 
			
		||||
            --interface=${bridge} \
 | 
			
		||||
            --dhcp-range=192.168.127.100,192.168.127.200,12h \
 | 
			
		||||
            --dhcp-option=option:router,${br_ip} \
 | 
			
		||||
            --dhcp-option=option:dns-server,1.1.1.1 \
 | 
			
		||||
            --pid-file=${dnsmasq_pid} \
 | 
			
		||||
            --dhcp-leasefile=${dnsmasq_lease} \
 | 
			
		||||
            > ${dnsmasq_log} 2>&1 &
 | 
			
		||||
        echo $! >/dev/null
 | 
			
		||||
    `;
 | 
			
		||||
    let r = run_silent(`bash -lc ${cmd.stringify()}`);
 | 
			
		||||
    if !r.success {
 | 
			
		||||
        print(`❌ Failed to start dnsmasq. Check log: ${dnsmasq_log}`);
 | 
			
		||||
        exit();
 | 
			
		||||
    }
 | 
			
		||||
    // Wait briefly for pid file
 | 
			
		||||
    sleep(1);
 | 
			
		||||
    let chk = run_silent(`bash -lc 'test -f ${dnsmasq_pid} && ps -p $(cat ${dnsmasq_pid}) >/dev/null 2>&1 && echo OK || echo FAIL'`);
 | 
			
		||||
    if !(chk.success && chk.stdout.trim() == "OK") {
 | 
			
		||||
        print(`❌ dnsmasq did not come up. See ${dnsmasq_log}`);
 | 
			
		||||
        exit();
 | 
			
		||||
    }
 | 
			
		||||
    print("✓ dnsmasq started (DHCP on br0)");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ----------- Build cloud-init NoCloud seed (user-data/meta-data) -----------
 | 
			
		||||
print("\n--- Building NoCloud seed (user-data, meta-data) ---");
 | 
			
		||||
run_silent(`bash -lc 'mkdir -p ${base_dir}'`);
 | 
			
		||||
 | 
			
		||||
// Compose user-data and meta-data content
 | 
			
		||||
let ud = `#cloud-config
 | 
			
		||||
users:
 | 
			
		||||
  - name: ubuntu
 | 
			
		||||
    groups: [adm, cdrom, dialout, lxd, plugdev, sudo]
 | 
			
		||||
    sudo: ALL=(ALL) NOPASSWD:ALL
 | 
			
		||||
    shell: /bin/bash
 | 
			
		||||
    lock_passwd: true
 | 
			
		||||
    ssh_authorized_keys:
 | 
			
		||||
      - ${user_pubkey}
 | 
			
		||||
ssh_pwauth: false
 | 
			
		||||
package_update: true
 | 
			
		||||
`;
 | 
			
		||||
let md = `instance-id: iid-ubuntu-noble-001
 | 
			
		||||
local-hostname: noblevm
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
// Write files via heredoc
 | 
			
		||||
let wr1 = run_silent(`bash -lc "cat > ${user_data} <<'EOF'\n${ud}\nEOF"`);
 | 
			
		||||
if !wr1.success { print(`❌ Failed to write ${user_data}`); exit(); }
 | 
			
		||||
let wr2 = run_silent(`bash -lc "cat > ${meta_data} <<'EOF'\n${md}\nEOF"`);
 | 
			
		||||
if !wr2.success { print(`❌ Failed to write ${meta_data}`); exit(); }
 | 
			
		||||
 | 
			
		||||
// Build seed ISO (prefer cloud-localds)
 | 
			
		||||
let built = false;
 | 
			
		||||
if !(clds == () || clds == "") {
 | 
			
		||||
    let r = run_silent(`bash -lc "sudo cloud-localds ${seed_iso} ${user_data} ${meta_data}"`);
 | 
			
		||||
    if r.success {
 | 
			
		||||
        built = true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
if !built {
 | 
			
		||||
    if geniso == () || geniso == "" {
 | 
			
		||||
        print("❌ Neither cloud-localds nor genisoimage succeeded/available to build seed.iso");
 | 
			
		||||
        exit();
 | 
			
		||||
    }
 | 
			
		||||
    let r2 = run_silent(`bash -lc "sudo genisoimage -output ${seed_iso} -volid cidata -joliet -rock ${user_data} ${meta_data}"`);
 | 
			
		||||
    if !r2.success {
 | 
			
		||||
        print("❌ genisoimage failed to create seed.iso");
 | 
			
		||||
        exit();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
print(`✓ Seed ISO: ${seed_iso}`);
 | 
			
		||||
 | 
			
		||||
// ----------- Determine boot method (firmware or kernel) -----------
 | 
			
		||||
print("\n--- Determining boot method ---");
 | 
			
		||||
let firmware_path = "";
 | 
			
		||||
if firmware_path_override != "" && exist(firmware_path_override) {
 | 
			
		||||
    firmware_path = firmware_path_override;
 | 
			
		||||
} else {
 | 
			
		||||
    let candidates = [
 | 
			
		||||
        "/usr/local/share/cloud-hypervisor/hypervisor-fw",
 | 
			
		||||
        "/usr/share/cloud-hypervisor/hypervisor-fw",
 | 
			
		||||
        "/usr/lib/cloud-hypervisor/hypervisor-fw",
 | 
			
		||||
        "/tmp/virt_images/hypervisor-fw"
 | 
			
		||||
    ];
 | 
			
		||||
    for p in candidates {
 | 
			
		||||
        if exist(p) { firmware_path = p; break; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
let kernel_path = "";
 | 
			
		||||
if kernel_path_override != "" && exist(kernel_path_override) {
 | 
			
		||||
    kernel_path = kernel_path_override;
 | 
			
		||||
}
 | 
			
		||||
if firmware_path == "" && kernel_path == "" {
 | 
			
		||||
    print("❌ No firmware_path or kernel_path found. Set firmware_path_override or kernel_path_override at top and re-run.");
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
if firmware_path != "" {
 | 
			
		||||
    print(`✓ Using firmware boot: ${firmware_path}`);
 | 
			
		||||
} else {
 | 
			
		||||
    print(`✓ Using direct kernel boot: ${kernel_path}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ----------- Create and start VM -----------
 | 
			
		||||
print("\n--- Creating and starting VM ---");
 | 
			
		||||
let rid = run_silent("date +%s%N");
 | 
			
		||||
let suffix = if rid.success && rid.stdout != "" { rid.stdout.trim() } else { "100000" };
 | 
			
		||||
let vm_id = `noble_vm_${suffix}`;
 | 
			
		||||
 | 
			
		||||
let spec = #{
 | 
			
		||||
    "id": vm_id,
 | 
			
		||||
    "disk_path": disk_path,
 | 
			
		||||
    "api_socket": "",
 | 
			
		||||
    "vcpus": 2,
 | 
			
		||||
    "memory_mb": 2048
 | 
			
		||||
};
 | 
			
		||||
if firmware_path != "" {
 | 
			
		||||
    spec.firmware_path = firmware_path;
 | 
			
		||||
} else {
 | 
			
		||||
    spec.kernel_path = kernel_path;
 | 
			
		||||
    spec.cmdline = kernel_cmdline_override;
 | 
			
		||||
}
 | 
			
		||||
spec.extra_args = [
 | 
			
		||||
    "--disk", `path=${seed_iso},readonly=true`,
 | 
			
		||||
    "--net",  `tap=${tap},mac=${mac}`
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
    let created = cloudhv_vm_create(spec);
 | 
			
		||||
    print(`✓ VM created: ${created}`);
 | 
			
		||||
} catch (err) {
 | 
			
		||||
    print(`❌ VM create failed: ${err}`);
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
    cloudhv_vm_start(vm_id);
 | 
			
		||||
    print("✓ VM start invoked");
 | 
			
		||||
} catch (err) {
 | 
			
		||||
    print(`❌ VM start failed: ${err}`);
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ----------- Wait for DHCP lease and print access info -----------
 | 
			
		||||
print("\n--- Waiting for DHCP lease from dnsmasq ---");
 | 
			
		||||
let vm_ip = "";
 | 
			
		||||
for i in 0..60 {
 | 
			
		||||
    sleep(1);
 | 
			
		||||
    let lr = run_silent(`bash -lc "if [ -f ${dnsmasq_lease} ]; then awk '\\$2 ~ /${mac}/ {print \\$3}' ${dnsmasq_lease} | tail -n1; fi"`);
 | 
			
		||||
    if lr.success {
 | 
			
		||||
        let ip = lr.stdout.trim();
 | 
			
		||||
        if ip != "" {
 | 
			
		||||
            vm_ip = ip;
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
if vm_ip == "" {
 | 
			
		||||
    print("⚠️ Could not discover VM IP from leases yet. You can check leases and retry:");
 | 
			
		||||
    print(`   cat ${dnsmasq_lease}`);
 | 
			
		||||
} else {
 | 
			
		||||
    print(`✓ Lease acquired: ${vm_ip}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
print("\n--- VM access details ---");
 | 
			
		||||
print(`VM ID: ${vm_id}`);
 | 
			
		||||
let info = cloudhv_vm_info(vm_id);
 | 
			
		||||
print(`API socket: ${info.spec.api_socket}`);
 | 
			
		||||
print(`Console log: ${info.runtime.log_file}`);
 | 
			
		||||
print(`Bridge: ${bridge} at ${br_ip}, TAP: ${tap}, MAC: ${mac}`);
 | 
			
		||||
print(`Seed: ${seed_iso}`);
 | 
			
		||||
if vm_ip != "" {
 | 
			
		||||
    print("\nSSH command (key-only; default user 'ubuntu'):");
 | 
			
		||||
    print(`ssh -o StrictHostKeyChecking=no ubuntu@${vm_ip}`);
 | 
			
		||||
} else {
 | 
			
		||||
    print("\nSSH command (replace <IP> after you see a lease):");
 | 
			
		||||
    print(`ssh -o StrictHostKeyChecking=no ubuntu@<IP>`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
print("\nCleanup hints (manual):");
 | 
			
		||||
print(`- Stop dnsmasq: sudo kill \$(cat ${dnsmasq_pid})`);
 | 
			
		||||
print(`- Remove TAP:   sudo ip link set ${tap} down; sudo ip link del ${tap}`);
 | 
			
		||||
print("  (Keep the bridge if you will reuse it.)");
 | 
			
		||||
 | 
			
		||||
print("\n=== Completed ===");
 | 
			
		||||
							
								
								
									
										311
									
								
								packages/system/virt/tests/rhai/07_cloudhv_ubuntu_ssh.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								packages/system/virt/tests/rhai/07_cloudhv_ubuntu_ssh.rhai
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,311 @@
 | 
			
		||||
// Create and boot an Ubuntu 24.04 VM with cloud-init SSH key injection on Cloud Hypervisor
 | 
			
		||||
// - Uses qcow2 base image builder from SAL
 | 
			
		||||
// - Builds a NoCloud seed ISO embedding your SSH public key
 | 
			
		||||
// - Starts the VM; host networking prerequisites (bridge/dnsmasq/nftables) are ensured by CloudHV SAL
 | 
			
		||||
// - Attempts to discover the VM IP from dnsmasq leases and prints SSH instructions
 | 
			
		||||
//
 | 
			
		||||
// Requirements on host:
 | 
			
		||||
//   - cloud-hypervisor-static, ch-remote-static
 | 
			
		||||
//   - cloud-localds (preferred) OR genisoimage
 | 
			
		||||
//   - qemu-img (already used by qcow2 SAL)
 | 
			
		||||
//   - dnsmasq + nftables (will be handled by SAL during vm_start)
 | 
			
		||||
//
 | 
			
		||||
// Note:
 | 
			
		||||
//   - SAL CloudHV networking will create a bridge br-hero, enable dnsmasq, and add a NAT rule via nftables
 | 
			
		||||
//   - This script does NOT manage host networking; it relies on SAL to do so during vm_start()
 | 
			
		||||
 | 
			
		||||
print("=== CloudHV Ubuntu 24.04 with SSH key (cloud-init) ===");
 | 
			
		||||
 | 
			
		||||
// ---------- Inputs ----------
 | 
			
		||||
let user_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFyZJCEsvRc0eitsOoq+ywC5Lmqejvk3hXMVbO0AxPrd maxime@maxime-arch";
 | 
			
		||||
 | 
			
		||||
// Optional overrides for boot method (if firmware is present, it will be preferred)
 | 
			
		||||
let firmware_path_override = "";   // e.g., "/usr/share/cloud-hypervisor/hypervisor-fw"
 | 
			
		||||
let kernel_path_override   = "";   // e.g., "/path/to/vmlinux"
 | 
			
		||||
let kernel_cmdline         = "console=ttyS0 reboot=k panic=1";
 | 
			
		||||
 | 
			
		||||
// Cloud-init hostname and instance id (used to identify leases reliably)
 | 
			
		||||
let cloudinit_hostname = "noblevm";
 | 
			
		||||
let cloudinit_instance_id = "iid-ubuntu-noble-ssh";
 | 
			
		||||
 | 
			
		||||
// Paths
 | 
			
		||||
let base_dir  = "/tmp/virt_images";
 | 
			
		||||
let seed_iso  = `${base_dir}/seed-ssh.iso`;
 | 
			
		||||
let user_data = `${base_dir}/user-data`;
 | 
			
		||||
let meta_data = `${base_dir}/meta-data`;
 | 
			
		||||
 | 
			
		||||
// ---------- Dependency checks ----------
 | 
			
		||||
print("\n--- Checking dependencies ---");
 | 
			
		||||
let chs = which("cloud-hypervisor-static");
 | 
			
		||||
let chrs = which("ch-remote-static");
 | 
			
		||||
let clds = which("cloud-localds");
 | 
			
		||||
let geniso = which("genisoimage");
 | 
			
		||||
let qemu = which("qemu-img");
 | 
			
		||||
 | 
			
		||||
let missing = false;
 | 
			
		||||
if chs == () || chs == "" {
 | 
			
		||||
    print("❌ cloud-hypervisor-static not found on PATH");
 | 
			
		||||
    missing = true;
 | 
			
		||||
}
 | 
			
		||||
if chrs == () || chrs == "" {
 | 
			
		||||
    print("❌ ch-remote-static not found on PATH");
 | 
			
		||||
    missing = true;
 | 
			
		||||
}
 | 
			
		||||
if (clds == () || clds == "") && (geniso == () || geniso == "") {
 | 
			
		||||
    print("❌ Neither cloud-localds nor genisoimage is available. Install cloud-image-utils or genisoimage.");
 | 
			
		||||
    missing = true;
 | 
			
		||||
}
 | 
			
		||||
if qemu == () || qemu == "" {
 | 
			
		||||
    print("❌ qemu-img not found (required by base image builder)");
 | 
			
		||||
    missing = true;
 | 
			
		||||
}
 | 
			
		||||
if missing {
 | 
			
		||||
    print("=== Aborting due to missing dependencies ===");
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
print("✓ Dependencies look OK");
 | 
			
		||||
 | 
			
		||||
// ---------- Ensure base image ----------
 | 
			
		||||
print("\n--- Ensuring Ubuntu 24.04 cloud image ---");
 | 
			
		||||
let base;
 | 
			
		||||
try {
 | 
			
		||||
    // Resize to e.g. 10 GiB sparse (adjust as needed)
 | 
			
		||||
    base = qcow2_build_ubuntu_24_04_base(base_dir, 10);
 | 
			
		||||
} catch (err) {
 | 
			
		||||
    print(`❌ Failed to build/ensure base image: ${err}`);
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
let disk_path = base.base_image_path;
 | 
			
		||||
print(`✓ Using base image: ${disk_path}`);
 | 
			
		||||
 | 
			
		||||
// ---------- Build cloud-init NoCloud seed (user-data/meta-data) ----------
 | 
			
		||||
print("\n--- Building NoCloud seed (SSH key) ---");
 | 
			
		||||
run_silent(`mkdir -p ${base_dir}`);
 | 
			
		||||
 | 
			
		||||
// Compose user-data and meta-data
 | 
			
		||||
let ud = `#cloud-config
 | 
			
		||||
users:
 | 
			
		||||
  - name: ubuntu
 | 
			
		||||
    groups: [adm, cdrom, dialout, lxd, plugdev, sudo]
 | 
			
		||||
    sudo: ALL=(ALL) NOPASSWD:ALL
 | 
			
		||||
    shell: /bin/bash
 | 
			
		||||
    lock_passwd: true
 | 
			
		||||
    ssh_authorized_keys:
 | 
			
		||||
      - ${user_pubkey}
 | 
			
		||||
ssh_pwauth: false
 | 
			
		||||
package_update: true
 | 
			
		||||
`;
 | 
			
		||||
let md = `instance-id: ${cloudinit_instance_id}
 | 
			
		||||
local-hostname: ${cloudinit_hostname}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
// Write files
 | 
			
		||||
let wr1 = run_silent(`/bin/bash -lc "cat > ${user_data} <<'EOF'
 | 
			
		||||
${ud}
 | 
			
		||||
EOF"`);
 | 
			
		||||
if !wr1.success { print(`❌ Failed to write ${user_data}`); exit(); }
 | 
			
		||||
let wr2 = run_silent(`/bin/bash -lc "cat > ${meta_data} <<'EOF'
 | 
			
		||||
${md}
 | 
			
		||||
EOF"`);
 | 
			
		||||
if !wr2.success { print(`❌ Failed to write ${meta_data}`); exit(); }
 | 
			
		||||
 | 
			
		||||
// Build seed ISO (prefer cloud-localds)
 | 
			
		||||
let built = false;
 | 
			
		||||
if !(clds == () || clds == "") {
 | 
			
		||||
    let r = run_silent(`cloud-localds ${seed_iso} ${user_data} ${meta_data}`);
 | 
			
		||||
    if r.success { built = true; }
 | 
			
		||||
}
 | 
			
		||||
if !built {
 | 
			
		||||
    if geniso == () || geniso == "" {
 | 
			
		||||
        print("❌ Neither cloud-localds nor genisoimage available to build seed.iso");
 | 
			
		||||
        exit();
 | 
			
		||||
    }
 | 
			
		||||
    let r2 = run_silent(`genisoimage -output ${seed_iso} -volid cidata -joliet -rock ${user_data} ${meta_data}`);
 | 
			
		||||
    if !r2.success {
 | 
			
		||||
        print("❌ genisoimage failed to create seed.iso");
 | 
			
		||||
        exit();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
print(`✓ Seed ISO: ${seed_iso}`);
 | 
			
		||||
 | 
			
		||||
// ---------- Determine boot method (firmware or kernel) ----------
 | 
			
		||||
print("\n--- Determining boot method ---");
 | 
			
		||||
let firmware_path = "";
 | 
			
		||||
if firmware_path_override != "" && exist(firmware_path_override) {
 | 
			
		||||
    firmware_path = firmware_path_override;
 | 
			
		||||
} else {
 | 
			
		||||
    let candidates = [
 | 
			
		||||
        "/usr/local/share/cloud-hypervisor/hypervisor-fw",
 | 
			
		||||
        "/usr/share/cloud-hypervisor/hypervisor-fw",
 | 
			
		||||
        "/usr/lib/cloud-hypervisor/hypervisor-fw",
 | 
			
		||||
        "/tmp/virt_images/hypervisor-fw"
 | 
			
		||||
    ];
 | 
			
		||||
    for p in candidates {
 | 
			
		||||
        if exist(p) { firmware_path = p; break; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
let kernel_path = "";
 | 
			
		||||
if kernel_path_override != "" && exist(kernel_path_override) {
 | 
			
		||||
    kernel_path = kernel_path_override;
 | 
			
		||||
}
 | 
			
		||||
if firmware_path == "" && kernel_path == "" {
 | 
			
		||||
    print("❌ No firmware_path or kernel_path found. Set firmware_path_override or kernel_path_override and re-run.");
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
if firmware_path != "" {
 | 
			
		||||
    print(`✓ Using firmware boot: ${firmware_path}`);
 | 
			
		||||
} else {
 | 
			
		||||
    print(`✓ Using direct kernel boot: ${kernel_path}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------- Create and start VM ----------
 | 
			
		||||
print("\n--- Creating and starting VM ---");
 | 
			
		||||
let rid = run_silent("date +%s%N");
 | 
			
		||||
// Make suffix robust even if date outputs nothing
 | 
			
		||||
let suffix = "100000";
 | 
			
		||||
if rid.success {
 | 
			
		||||
    let t = rid.stdout.trim();
 | 
			
		||||
    if t != "" { suffix = t; }
 | 
			
		||||
}
 | 
			
		||||
let vm_id = `noble_ssh_${suffix}`;
 | 
			
		||||
 | 
			
		||||
let spec = #{
 | 
			
		||||
    "id": vm_id,
 | 
			
		||||
    "disk_path": disk_path,
 | 
			
		||||
    "api_socket": "",
 | 
			
		||||
    "vcpus": 2,
 | 
			
		||||
    "memory_mb": 2048
 | 
			
		||||
};
 | 
			
		||||
if firmware_path != "" {
 | 
			
		||||
    spec.firmware_path = firmware_path;
 | 
			
		||||
} else {
 | 
			
		||||
    spec.kernel_path = kernel_path;
 | 
			
		||||
    spec.cmdline = kernel_cmdline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Attach the NoCloud seed ISO as a read-only disk
 | 
			
		||||
spec.extra_args = [
 | 
			
		||||
    "--disk", `path=${seed_iso},readonly=true`
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
    let created = cloudhv_vm_create(spec);
 | 
			
		||||
    print(`✓ VM created: ${created}`);
 | 
			
		||||
} catch (err) {
 | 
			
		||||
    print(`❌ VM create failed: ${err}`);
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
    cloudhv_vm_start(vm_id);
 | 
			
		||||
    print("✓ VM start invoked");
 | 
			
		||||
} catch (err) {
 | 
			
		||||
    print(`❌ VM start failed: ${err}`);
 | 
			
		||||
    exit();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------- Wait for VM API socket and probe readiness ----------
 | 
			
		||||
print("\n--- Waiting for VM API socket ---");
 | 
			
		||||
let api_sock = "";
 | 
			
		||||
// Discover socket path (from SAL or common defaults)
 | 
			
		||||
let fallback_candidates = [
 | 
			
		||||
    `/root/hero/virt/vms/${vm_id}/api.sock`,
 | 
			
		||||
    `/home/maxime/hero/virt/vms/${vm_id}/api.sock`
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// First, try to detect the socket on disk with a longer timeout
 | 
			
		||||
let sock_exists = false;
 | 
			
		||||
for i in 0..180 {
 | 
			
		||||
    sleep(1);
 | 
			
		||||
    let info = cloudhv_vm_info(vm_id);
 | 
			
		||||
    api_sock = info.spec.api_socket;
 | 
			
		||||
    if api_sock == () || api_sock == "" {
 | 
			
		||||
        for cand in fallback_candidates {
 | 
			
		||||
            if exist(cand) { api_sock = cand; break; }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if api_sock != () && api_sock != "" && exist(api_sock) {
 | 
			
		||||
        sock_exists = true;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Regardless of filesystem existence, also try probing the API directly
 | 
			
		||||
let api_ok = false;
 | 
			
		||||
if api_sock != () && api_sock != "" {
 | 
			
		||||
    for i in 0..60 {
 | 
			
		||||
        let r = run_silent(`ch-remote-static --api-socket ${api_sock} info`);
 | 
			
		||||
        if r.success { api_ok = true; break; }
 | 
			
		||||
        sleep(1);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if api_ok {
 | 
			
		||||
    print("✓ VM API reachable");
 | 
			
		||||
} else if sock_exists {
 | 
			
		||||
    print("⚠️ VM API socket exists but API not reachable yet");
 | 
			
		||||
} else {
 | 
			
		||||
    print("⚠️ VM API socket not found yet; proceeding");
 | 
			
		||||
    let info_dbg = cloudhv_vm_info(vm_id);
 | 
			
		||||
    let log_path = info_dbg.runtime.log_file;
 | 
			
		||||
    if exist(log_path) {
 | 
			
		||||
        let t = run_silent(`tail -n 120 ${log_path}`);
 | 
			
		||||
        if t.success && t.stdout.trim() != "" {
 | 
			
		||||
            print("\n--- Last 120 lines of console log (diagnostics) ---");
 | 
			
		||||
            print(t.stdout);
 | 
			
		||||
            print("--- End of console log ---");
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        print(`(console log not found at ${log_path})`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------- Discover VM IP from dnsmasq leases ----------
 | 
			
		||||
print("\n--- Discovering VM IP (dnsmasq leases) ---");
 | 
			
		||||
// SAL enables system dnsmasq for br-hero by default; leases usually at /var/lib/misc/dnsmasq.leases
 | 
			
		||||
let leases_paths = [
 | 
			
		||||
    "/var/lib/misc/dnsmasq.leases",
 | 
			
		||||
    "/var/lib/dnsmasq/dnsmasq.leases"
 | 
			
		||||
];
 | 
			
		||||
let vm_ip = "";
 | 
			
		||||
for path in leases_paths {
 | 
			
		||||
    if !exist(path) { continue; }
 | 
			
		||||
    for i in 0..120 {
 | 
			
		||||
        sleep(1);
 | 
			
		||||
        // Pure awk (no nested shells/pipes). Keep last IP matching hostname.
 | 
			
		||||
        let lr = run_silent(`awk -v host="${cloudinit_hostname}" '($4 ~ host){ip=$3} END{if(ip!=\"\") print ip}' ${path}`);
 | 
			
		||||
        if lr.success {
 | 
			
		||||
            let ip = lr.stdout.trim();
 | 
			
		||||
            if ip != "" {
 | 
			
		||||
                vm_ip = ip;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if vm_ip != "" { break; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------- Output connection details ----------
 | 
			
		||||
print("\n--- VM access details ---");
 | 
			
		||||
let info = cloudhv_vm_info(vm_id);
 | 
			
		||||
print(`VM ID: ${vm_id}`);
 | 
			
		||||
if info.runtime.pid != () {
 | 
			
		||||
    print(`PID: ${info.runtime.pid}`);
 | 
			
		||||
}
 | 
			
		||||
print(`Status: ${info.runtime.status}`);
 | 
			
		||||
print(`API socket: ${info.spec.api_socket}`);
 | 
			
		||||
print(`Console log: ${info.runtime.log_file}`);
 | 
			
		||||
print(`Seed ISO: ${seed_iso}`);
 | 
			
		||||
print(`Hostname: ${cloudinit_hostname}`);
 | 
			
		||||
 | 
			
		||||
if vm_ip != "" {
 | 
			
		||||
    print("\nSSH command (default user 'ubuntu'):");
 | 
			
		||||
    print(`ssh -o StrictHostKeyChecking=no ubuntu@${vm_ip}`);
 | 
			
		||||
} else {
 | 
			
		||||
    print("\n⚠️ Could not resolve VM IP yet from leases. Try later:");
 | 
			
		||||
    print("  - Check leases: sudo cat /var/lib/misc/dnsmasq.leases | grep noblevm");
 | 
			
		||||
    print("  - Or find on bridge (example): ip -4 neigh show dev br-hero");
 | 
			
		||||
    print("  - Then SSH: ssh -o StrictHostKeyChecking=no ubuntu@<IP>");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
print("\n=== Completed: Ubuntu VM launched with SSH key via cloud-init ===");
 | 
			
		||||
		Reference in New Issue
	
	Block a user