networking VMs (WIP)
This commit is contained in:
@@ -5,6 +5,8 @@ use std::fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
use sal_os;
|
use sal_os;
|
||||||
use sal_process;
|
use sal_process;
|
||||||
@@ -299,6 +301,32 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
|||||||
parts.push("--console".into());
|
parts.push("--console".into());
|
||||||
parts.push("off".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() {
|
if let Some(extra) = rec.spec.extra_args.clone() {
|
||||||
for e in extra {
|
for e in extra {
|
||||||
parts.push(e);
|
parts.push(e);
|
||||||
@@ -480,6 +508,150 @@ pub fn vm_list() -> Result<Vec<VmRecord>, CloudHvError> {
|
|||||||
Ok(out)
|
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
|
/// Render a shell-safe command string from vector of tokens
|
||||||
fn shell_join(parts: &Vec<String>) -> String {
|
fn shell_join(parts: &Vec<String>) -> String {
|
||||||
let mut s = String::new();
|
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