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