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.
This commit is contained in:
Till Wegmueller 2025-11-06 21:56:57 +01:00
parent 97599eb48d
commit 0dabdf2bb2
No known key found for this signature in database
2 changed files with 123 additions and 6 deletions

View file

@ -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

View file

@ -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<String, usize> {
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<String> {
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<String> {
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 <ip address='x.x.x.x' ...> 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<String> {
// Very small string-based parser to avoid extra dependencies
// Find "<ip" then search for "address='...'" or "address=\"...\""
let mut idx = 0;
while let Some(start) = xml[idx..].find("<ip") {
let s = idx + start;
let end = xml[s..].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,