From 0dabdf2bb2d2d273d0696eba944b01db43c119f46504da8f41892385b104ea7e Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Thu, 6 Nov 2025 21:56:57 +0100 Subject: [PATCH] Auto-detect orchestrator contact address and enhance platform-specific configurations This commit introduces: - Automatic detection of the orchestrator contact address when not explicitly provided. - Platform-specific logic for determining reachable IPs, including libvirt network parsing (Linux) and external IP detection. - Updates to GRPC address processing to handle both specific and unspecified hosts. - Additional utility functions for parsing and detecting IPs in libvirt configurations. --- TODO.txt | 3 - crates/orchestrator/src/main.rs | 126 +++++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 6 deletions(-) diff --git a/TODO.txt b/TODO.txt index 8eaa794..14fa076 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,6 +1,3 @@ - - -- Make orchestrator detect the address it will be reachable by checking the libvirt config or on illumos use it's external IP - Make VM reachable IP of the orchestrator configurable in case the setup on illumos gets more complicated (via config file) - Make the forge-integration task use fnox secrets diff --git a/crates/orchestrator/src/main.rs b/crates/orchestrator/src/main.rs index e4a5b94..410ff8b 100644 --- a/crates/orchestrator/src/main.rs +++ b/crates/orchestrator/src/main.rs @@ -145,9 +145,12 @@ async fn main() -> Result<()> { .await; }); - // Orchestrator contact address for runner to dial back (can override via ORCH_CONTACT_ADDR) - let orch_contact = - std::env::var("ORCH_CONTACT_ADDR").unwrap_or_else(|_| opts.grpc_addr.clone()); + // Orchestrator contact address for runner to dial back (auto-detect if not provided) + let orch_contact = match std::env::var("ORCH_CONTACT_ADDR") { + Ok(v) => v, + Err(_) => detect_contact_addr(&opts), + }; + info!(contact = %orch_contact, "orchestrator contact address determined"); // Compose default runner URLs served by this orchestrator (if runner_dir configured) let (runner_url_default, runner_urls_default) = if opts.runner_dir.is_some() { @@ -306,6 +309,123 @@ fn parse_capacity_map(s: Option<&str>) -> HashMap { m } +fn detect_contact_addr(opts: &Opts) -> String { + // Extract host and port from GRPC_ADDR (format host:port). + let (host_part, port_part) = match opts.grpc_addr.rsplit_once(':') { + Some((h, p)) => (h.to_string(), p.to_string()), + None => (opts.grpc_addr.clone(), String::from("")), + }; + + // If host is already a specific address (not any/unspecified), keep it. + let host_trim = host_part.trim(); + let is_unspecified = host_trim == "0.0.0.0" || host_trim == "::" || host_trim == "[::]" || host_trim.is_empty(); + if !is_unspecified { + return opts.grpc_addr.clone(); + } + + // Try platform-specific detection + #[cfg(all(target_os = "linux"))] + { + // Attempt to read libvirt network XML to obtain the NAT gateway IP (reachable from guests). + if let Some(ip) = detect_libvirt_network_ip(&opts.libvirt_network) { + let port = if port_part.is_empty() { String::from("50051") } else { port_part.clone() }; + return format!("{}:{}", ip, port); + } + // Fallback to external IP detection + if let Some(ip) = detect_external_ip() { + let port = if port_part.is_empty() { String::from("50051") } else { port_part.clone() }; + return format!("{}:{}", ip, port); + } + // Last resort + return format!("127.0.0.1:{}", if port_part.is_empty() { "50051" } else { &port_part }); + } + + #[cfg(target_os = "illumos")] + { + if let Some(ip) = detect_external_ip() { + let port = if port_part.is_empty() { String::from("50051") } else { port_part.clone() }; + return format!("{}:{}", ip, port); + } + return format!("127.0.0.1:{}", if port_part.is_empty() { "50051" } else { &port_part }); + } + + // Other platforms: best-effort external IP + if let Some(ip) = detect_external_ip() { + let port = if port_part.is_empty() { String::from("50051") } else { port_part }; + return format!("{}:{}", ip, port); + } + opts.grpc_addr.clone() +} + +#[cfg(any(target_os = "linux", target_os = "illumos"))] +fn detect_external_ip() -> Option { + use std::net::{SocketAddr, UdpSocket}; + // UDP connect trick: no packets are actually sent, but OS picks a route and local addr. + let target: SocketAddr = "1.1.1.1:80".parse().ok()?; + let sock = UdpSocket::bind("0.0.0.0:0").ok()?; + sock.connect(target).ok()?; + let local = sock.local_addr().ok()?; + Some(local.ip().to_string()) +} + +#[cfg(target_os = "linux")] +fn detect_libvirt_network_ip(name: &str) -> Option { + use std::fs; + let candidates = vec![ + format!("/etc/libvirt/qemu/networks/{}.xml", name), + format!("/etc/libvirt/qemu/networks/autostart/{}.xml", name), + format!("/var/lib/libvirt/network/{}.xml", name), + format!("/var/lib/libvirt/qemu/networks/{}.xml", name), + ]; + for p in candidates { + if let Ok(xml) = fs::read_to_string(&p) { + // Look for or with double quotes + if let Some(ip) = extract_ip_from_libvirt_network_xml(&xml) { + return Some(ip); + } + } + } + None +} + +#[cfg(target_os = "linux")] +fn extract_ip_from_libvirt_network_xml(xml: &str) -> Option { + // Very small string-based parser to avoid extra dependencies + // Find "').map(|e| s + e).unwrap_or(xml.len()); + let segment = &xml[s..end]; + if let Some(val) = extract_attr_value(segment, "address") { + return Some(val.to_string()); + } + idx = end + 1; + if idx >= xml.len() { break; } + } + None +} + +#[cfg(target_os = "linux")] +fn extract_attr_value<'a>(tag: &'a str, key: &'a str) -> Option<&'a str> { + // Search for key='value' or key="value" + if let Some(pos) = tag.find(key) { + let rest = &tag[pos + key.len()..]; + let rest = rest.trim_start(); + if rest.starts_with('=') { + let rest = &rest[1..].trim_start(); + let quote = rest.chars().next()?; + if quote == '\'' || quote == '"' { + let rest2 = &rest[1..]; + if let Some(end) = rest2.find(quote) { + return Some(&rest2[..end]); + } + } + } + } + None +} + fn make_cloud_init_userdata( repo_url: &str, commit_sha: &str,