wip
This commit is contained in:
@@ -11,8 +11,10 @@ use std::hash::{Hash, Hasher};
|
||||
use sal_os;
|
||||
use sal_process;
|
||||
use crate::qcow2;
|
||||
use crate::cloudhv::net::{NetworkingProfileSpec, DefaultNatOptions};
|
||||
|
||||
pub mod builder;
|
||||
pub mod net;
|
||||
|
||||
/// Error type for Cloud Hypervisor operations
|
||||
#[derive(Debug)]
|
||||
@@ -61,6 +63,9 @@ pub struct VmSpec {
|
||||
pub cmdline: Option<String>,
|
||||
/// Extra args (raw) if you need to extend; keep minimal for Phase 2
|
||||
pub extra_args: Option<Vec<String>>,
|
||||
/// Optional networking profile; when None, behavior follows explicit --net/--no-default-net or defaults
|
||||
#[serde(default)]
|
||||
pub net_profile: Option<NetworkingProfileSpec>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -394,72 +399,92 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
.map(|v| v.iter().any(|tok| tok == "--net" || tok == "--no-default-net"))
|
||||
.unwrap_or(false);
|
||||
|
||||
if !has_user_net {
|
||||
// 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());
|
||||
|
||||
// IPv6 over Mycelium: enabled by default.
|
||||
// If explicitly disabled via HERO_VIRT_IPV6_ENABLE=false|0, we skip.
|
||||
// If enabled but Mycelium is not detected, return an error.
|
||||
let ipv6_env = std::env::var("HERO_VIRT_IPV6_ENABLE").unwrap_or_else(|_| "".into());
|
||||
let ipv6_requested = match ipv6_env.to_lowercase().as_str() {
|
||||
"" | "1" | "true" | "yes" => true,
|
||||
"0" | "false" | "no" => false,
|
||||
_ => true,
|
||||
};
|
||||
let mycelium_if_cfg = std::env::var("HERO_VIRT_MYCELIUM_IF").unwrap_or_else(|_| "mycelium".into());
|
||||
let mut ipv6_bridge_cidr: Option<String> = None;
|
||||
let mut mycelium_if_opt: Option<String> = None;
|
||||
|
||||
if ipv6_requested {
|
||||
if let Ok(cidr) = std::env::var("HERO_VIRT_IPV6_BRIDGE_CIDR") {
|
||||
// Explicit override for bridge IPv6 (e.g., "400:...::2/64") but still require mycelium iface presence.
|
||||
// Validate mycelium interface and that it has IPv6 configured.
|
||||
let _ = get_mycelium_ipv6_addr(&mycelium_if_cfg)?; // returns DependencyMissing on failure
|
||||
ipv6_bridge_cidr = Some(cidr);
|
||||
mycelium_if_opt = Some(mycelium_if_cfg.clone());
|
||||
} else {
|
||||
// Auto-derive from mycelium node address; error out if not detected.
|
||||
println!("auto-deriving mycelium address...");
|
||||
let (ifname, myc_addr) = get_mycelium_ipv6_addr(&mycelium_if_cfg)?;
|
||||
println!("on if {ifname}, got myc addr: {myc_addr}");
|
||||
let (_pfx, router_cidr) = derive_ipv6_prefix_from_mycelium(&myc_addr)?;
|
||||
println!("derived pfx: {_pfx} and router cidr: {router_cidr}");
|
||||
ipv6_bridge_cidr = Some(router_cidr);
|
||||
mycelium_if_opt = Some(ifname);
|
||||
// Track chosen bridge/lease for later discovery
|
||||
let mut bridge_for_disc: Option<String> = None;
|
||||
let mut lease_for_disc: Option<String> = None;
|
||||
|
||||
// Determine effective networking profile
|
||||
let profile_effective = if let Some(p) = rec.spec.net_profile.clone() {
|
||||
Some(p)
|
||||
} else if has_user_net {
|
||||
// User provided explicit --net or --no-default-net; do not provision
|
||||
None
|
||||
} else {
|
||||
// Default behavior: NAT profile
|
||||
Some(NetworkingProfileSpec::DefaultNat(DefaultNatOptions::default()))
|
||||
};
|
||||
|
||||
if let Some(profile) = profile_effective {
|
||||
match profile {
|
||||
NetworkingProfileSpec::DefaultNat(mut nat) => {
|
||||
// IPv6 handling (auto via Mycelium unless disabled)
|
||||
let mut ipv6_bridge_cidr: Option<String> = None;
|
||||
if nat.ipv6_enable {
|
||||
if let Ok(cidr) = std::env::var("HERO_VIRT_IPV6_BRIDGE_CIDR") {
|
||||
// Validate mycelium iface presence if specified or default
|
||||
let if_hint = nat.mycelium_if.clone().unwrap_or_else(|| "mycelium".into());
|
||||
let _ = net::mycelium_ipv6_addr(&if_hint)?;
|
||||
ipv6_bridge_cidr = Some(cidr);
|
||||
} else {
|
||||
let if_hint = nat.mycelium_if.clone().unwrap_or_else(|| "mycelium".into());
|
||||
println!("auto-deriving mycelium address...");
|
||||
let (_ifname, myc_addr) = net::mycelium_ipv6_addr(&if_hint)?;
|
||||
let (_pfx, router_cidr) = net::derive_ipv6_prefix_from_mycelium(&myc_addr)?;
|
||||
println!("derived router cidr for bridge: {}", router_cidr);
|
||||
ipv6_bridge_cidr = Some(router_cidr);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure bridge, NAT, and DHCP
|
||||
net::ensure_bridge(&nat.bridge_name, &nat.bridge_addr_cidr, ipv6_bridge_cidr.as_deref())?;
|
||||
net::ensure_nat(&nat.subnet_cidr)?;
|
||||
let lease_used = net::ensure_dnsmasq(
|
||||
&nat.bridge_name,
|
||||
&nat.dhcp_start,
|
||||
&nat.dhcp_end,
|
||||
ipv6_bridge_cidr.as_deref(),
|
||||
nat.lease_file.as_deref(),
|
||||
)?;
|
||||
|
||||
bridge_for_disc = Some(nat.bridge_name.clone());
|
||||
lease_for_disc = Some(lease_used.clone());
|
||||
|
||||
// TAP + NIC args
|
||||
let tap_name = net::ensure_tap_for_vm(&nat.bridge_name, id)?;
|
||||
println!("TAP device for vm called: {tap_name}");
|
||||
let mac = net::stable_mac_from_id(id);
|
||||
println!("MAC for vm: {mac}");
|
||||
parts.push("--net".into());
|
||||
parts.push(format!("tap={},mac={}", tap_name, mac));
|
||||
}
|
||||
NetworkingProfileSpec::BridgeOnly(opts) => {
|
||||
let bridge_name = opts.bridge_name.clone();
|
||||
// Use provided IPv4 if any, else env default
|
||||
let bridge_addr_cidr = opts
|
||||
.bridge_addr_cidr
|
||||
.clone()
|
||||
.unwrap_or_else(|| std::env::var("HERO_VIRT_BRIDGE_ADDR_CIDR").unwrap_or_else(|_| "172.30.0.1/24".into()));
|
||||
// Ensure bridge (optional IPv6 from opts)
|
||||
net::ensure_bridge(&bridge_name, &bridge_addr_cidr, opts.bridge_ipv6_cidr.as_deref())?;
|
||||
// TAP + NIC only, no NAT/DHCP
|
||||
let tap_name = net::ensure_tap_for_vm(&bridge_name, id)?;
|
||||
println!("TAP device for vm called: {tap_name}");
|
||||
let mac = net::stable_mac_from_id(id);
|
||||
println!("MAC for vm: {mac}");
|
||||
parts.push("--net".into());
|
||||
parts.push(format!("tap={},mac={}", tap_name, mac));
|
||||
|
||||
// For discovery: we can attempt IPv6 neighbor; IPv4 lease not present
|
||||
bridge_for_disc = Some(bridge_name);
|
||||
lease_for_disc = None;
|
||||
}
|
||||
NetworkingProfileSpec::NoNet => {
|
||||
// Do nothing
|
||||
}
|
||||
NetworkingProfileSpec::CustomCli(_args) => {
|
||||
// Do not provision; user must add --net via extra_args
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
ipv6_bridge_cidr.as_deref(),
|
||||
mycelium_if_opt.as_deref(),
|
||||
)?;
|
||||
|
||||
// Ensure a TAP device for this VM and attach to the bridge
|
||||
let tap_name = ensure_tap_for_vm(&bridge_name, id)?;
|
||||
println!("TAP device for vm called: {tap_name}");
|
||||
// Stable locally-administered MAC derived from VM id
|
||||
let mac = stable_mac_from_id(id);
|
||||
println!("MAC for vm: {mac}");
|
||||
|
||||
parts.push("--net".into());
|
||||
parts.push(format!("tap={},mac={}", tap_name, mac));
|
||||
}
|
||||
|
||||
// Append any user-provided extra args, sans any '--disk' we already consolidated
|
||||
@@ -540,73 +565,40 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
println!("wrote JSON for VM");
|
||||
|
||||
// Best-effort: discover and print guest IPv4/IPv6 addresses (default-net path)
|
||||
// Give DHCP/ND a moment
|
||||
println!("waiting 5 secs for DHCP/ND");
|
||||
thread::sleep(Duration::from_millis(5000));
|
||||
let bridge_name = std::env::var("HERO_VIRT_BRIDGE_NAME").unwrap_or_else(|_| "br-hero".into());
|
||||
let mac_lower = stable_mac_from_id(id).to_lowercase();
|
||||
let mac_lower = net::stable_mac_from_id(id).to_lowercase();
|
||||
|
||||
// IPv4 from dnsmasq leases (pinned per-bridge leasefile)
|
||||
// Path set in ensure_host_net_prereq_dnsmasq_nftables: /var/lib/misc/dnsmasq-hero-$BR.leases
|
||||
let lease_path = std::env::var("HERO_VIRT_DHCP_LEASE_FILE")
|
||||
.unwrap_or_else(|_| format!("/var/lib/misc/dnsmasq-hero-{}.leases", bridge_name));
|
||||
// Parse dnsmasq leases directly to avoid shell quoting/pipelines
|
||||
let ipv4 = (|| {
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(12);
|
||||
loop {
|
||||
if let Ok(content) = fs::read_to_string(&lease_path) {
|
||||
let mut last_ip: Option<String> = None;
|
||||
for line in content.lines() {
|
||||
let cols: Vec<&str> = line.split_whitespace().collect();
|
||||
if cols.len() >= 3 && cols[1].eq_ignore_ascii_case(&mac_lower) {
|
||||
last_ip = Some(cols[2].to_string());
|
||||
}
|
||||
}
|
||||
if last_ip.is_some() {
|
||||
return last_ip;
|
||||
}
|
||||
}
|
||||
if std::time::Instant::now() >= deadline {
|
||||
return None;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(800));
|
||||
}
|
||||
})();
|
||||
println!(
|
||||
"Got IPv4 from dnsmasq lease ({}): {}",
|
||||
lease_path,
|
||||
ipv4.clone().unwrap_or("not found".to_string())
|
||||
);
|
||||
if let Some(bridge_name) = bridge_for_disc.clone() {
|
||||
let lease_path = lease_for_disc.unwrap_or_else(|| {
|
||||
std::env::var("HERO_VIRT_DHCP_LEASE_FILE")
|
||||
.unwrap_or_else(|_| format!("/var/lib/misc/dnsmasq-hero-{}.leases", bridge_name))
|
||||
});
|
||||
let ipv4 = net::discover_ipv4_from_leases(&lease_path, &mac_lower, 12);
|
||||
println!(
|
||||
"Got IPv4 from dnsmasq lease ({}): {}",
|
||||
lease_path,
|
||||
ipv4.clone().unwrap_or("not found".to_string())
|
||||
);
|
||||
|
||||
// IPv6 from neighbor table on the bridge (exclude link-local), parsed in Rust
|
||||
let ipv6 = (|| {
|
||||
let cmd = format!("ip -6 neigh show dev {}", bridge_name);
|
||||
if let Ok(res) = sal_process::run(&cmd).silent(true).die(false).execute() {
|
||||
if res.success {
|
||||
let mac_pat = format!("lladdr {}", mac_lower);
|
||||
for line in res.stdout.lines() {
|
||||
let lt = line.trim();
|
||||
if lt.to_lowercase().contains(&mac_pat) {
|
||||
if let Some(addr) = lt.split_whitespace().next() {
|
||||
if !addr.starts_with("fe80") && !addr.is_empty() {
|
||||
return Some(addr.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})();
|
||||
let ipv6 = net::discover_ipv6_on_bridge(&bridge_name, &mac_lower);
|
||||
println!(
|
||||
"Got IPv6 from neighbor table on bridge: {}",
|
||||
ipv6.clone().unwrap_or("not found".to_string())
|
||||
);
|
||||
|
||||
println!("Got IPv6 from neighbor table on bridge: {}", ipv6.clone().unwrap_or("not found".to_string()));
|
||||
|
||||
println!(
|
||||
"[cloudhv] VM '{}' guest addresses: IPv4={}, IPv6={}",
|
||||
id,
|
||||
ipv4.as_deref().unwrap_or(""),
|
||||
ipv6.as_deref().unwrap_or("")
|
||||
);
|
||||
println!(
|
||||
"[cloudhv] VM '{}' guest addresses: IPv4={}, IPv6={}",
|
||||
id,
|
||||
ipv4.as_deref().unwrap_or(""),
|
||||
ipv6.as_deref().unwrap_or("")
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"[cloudhv] VM '{}' guest addresses discovery skipped (no default bridge in use)",
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
Reference in New Issue
Block a user