diff --git a/.gitignore b/.gitignore index 8defeab4..9d70d4bc 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ botserver-stack docs/book *.rdb botserver-installers -.git-rewrite \ No newline at end of file +.git-rewrite +vault-unseal-keys \ No newline at end of file diff --git a/src/core/package_manager/cli.rs b/src/core/package_manager/cli.rs index 542669f7..ed1d4283 100644 --- a/src/core/package_manager/cli.rs +++ b/src/core/package_manager/cli.rs @@ -252,8 +252,9 @@ pub async fn run() -> Result<()> { vault_migrate(env_file).await?; } "put" => { - if args.len() < 5 { - eprintln!("Usage: botserver vault put [key=value...]"); + if args.len() < 4 { + eprintln!("Usage: botserver vault put [key=value] [key=value...]"); + eprintln!(" botserver vault put (interactive mode - prompts for keys)"); return Ok(()); } let path = &args[3]; @@ -610,20 +611,59 @@ async fn vault_put(path: &str, kvs: &[&str]) -> Result<()> { } let mut data: HashMap = HashMap::new(); - for kv in kvs { - if let Some((k, v)) = kv.split_once('=') { - data.insert(k.to_string(), v.to_string()); - } else { - eprintln!("Invalid key=value pair: {}", kv); + + // If no key=value provided, enter interactive mode + if kvs.is_empty() { + println!("\n=== Interactive Vault Store ==="); + println!("Path: {}", path); + println!("Enter values (press Enter with empty key to finish):\n"); + + loop { + print!("Key: "); + std::io::Write::flush(&mut std::io::stdout())?; + let mut key = String::new(); + std::io::stdin().read_line(&mut key)?; + let key = key.trim().to_string(); + + if key.is_empty() { + break; + } + + print!("Value: "); + std::io::Write::flush(&mut std::io::stdout())?; + let mut value = String::new(); + std::io::stdin().read_line(&mut value)?; + let value = value.trim().to_string(); + + if value.is_empty() { + eprintln!("Value cannot be empty, skipping '{}'", key); + continue; + } + + data.insert(key.clone(), value); + println!("* Saved '{}'\n", key); + } + + if data.is_empty() { + return Err(anyhow::anyhow!("No values provided")); + } + } else { + // Command line mode + for kv in kvs { + if let Some((k, v)) = kv.split_once('=') { + data.insert(k.to_string(), v.to_string()); + } else { + eprintln!("Invalid key=value pair: {}", kv); + } + } + + if data.is_empty() { + return Err(anyhow::anyhow!("No valid key=value pairs provided")); } } - if data.is_empty() { - return Err(anyhow::anyhow!("No valid key=value pairs provided")); - } - - manager.put_secret(path, data).await?; - println!("* Stored {} key(s) at {}", kvs.len(), path); + manager.put_secret(path, data.clone()).await?; + println!("\n✓ Stored {} key(s) at {}", data.len(), path); Ok(()) } diff --git a/src/core/package_manager/facade.rs b/src/core/package_manager/facade.rs index a875b87c..6dbe2a54 100644 --- a/src/core/package_manager/facade.rs +++ b/src/core/package_manager/facade.rs @@ -409,27 +409,52 @@ impl PackageManager { } fn assign_static_ip(container_name: &str) -> Result { - // Pick a deterministic IP from the last 2 bytes of the MAC address to avoid collisions. + // Discover the host bridge gateway and subnet dynamically from the container's default route. + // The container already has IPv6 + a link on eth0, so we can read the bridge from the host. + let bridge_info = std::process::Command::new("ip") + .args(["-4", "addr", "show", "lxdbr0"]) + .output() + .ok() + .and_then(|o| { + let out = String::from_utf8_lossy(&o.stdout).to_string(); + // Extract "10.x.x.x/prefix" from "inet 10.x.x.x/24 ..." + out.lines() + .find(|l| l.contains("inet ")) + .and_then(|l| l.split_whitespace().nth(1)) + .map(|s| s.to_string()) + }); + + let (gateway, prefix) = match bridge_info.as_deref().and_then(|cidr| { + let (ip, pfx_str) = cidr.split_once('/')?; + let parts: Vec<&str> = ip.split('.').collect(); + let base = format!("{}.{}.{}.", parts[0], parts[1], parts[2]); + let pfx = pfx_str.parse::().ok()?; + Some((ip.to_string(), format!("{base}{{octet}}/{pfx}"))) + }) { + Some(pair) => pair, + None => return Err(anyhow::anyhow!("Cannot determine lxdbr0 subnet")), + }; + + // Derive last octet from MAC to avoid collisions (range 100-249). let mac_out = safe_lxc(&["exec", container_name, "--", "cat", "/sys/class/net/eth0/address"]); - let last_octet = mac_out + let octet = mac_out .and_then(|o| o.status.success().then(|| String::from_utf8_lossy(&o.stdout).trim().to_string())) .and_then(|mac| { let parts: Vec<&str> = mac.split(':').collect(); let a = u8::from_str_radix(parts.get(4)?, 16).ok()?; let b = u8::from_str_radix(parts.get(5)?, 16).ok()?; - // Map into 100-250 range to avoid gateway (.1) and broadcast (.255) - Some(100u16 + ((u16::from(a) * 256 + u16::from(b)) % 150) as u16) + Some(100u16 + (u16::from(a) * 256 + u16::from(b)) % 150) }) .unwrap_or(100); - let ip = format!("10.43.228.{last_octet}"); - let cidr = format!("{ip}/24"); + let cidr = prefix.replace("{octet}", &octet.to_string()); + let ip = cidr.split('/').next().unwrap_or("").to_string(); safe_lxc(&["exec", container_name, "--", "ip", "addr", "add", &cidr, "dev", "eth0"]); - safe_lxc(&["exec", container_name, "--", "ip", "route", "add", "default", "via", "10.43.228.1"]); + safe_lxc(&["exec", container_name, "--", "ip", "route", "add", "default", "via", &gateway]); safe_lxc(&["exec", container_name, "--", "bash", "-c", &format!("printf 'nameserver 8.8.8.8\\nnameserver 8.8.4.4\\n' > /etc/resolv.conf && \ - printf '#!/bin/sh\\nip addr add {cidr} dev eth0 2>/dev/null||true\\nip route add default via 10.43.228.1 2>/dev/null||true\\nexit 0\\n' > /etc/rc.local && \ + printf '#!/bin/sh\\nip addr add {cidr} dev eth0 2>/dev/null||true\\nip route add default via {gateway} 2>/dev/null||true\\nexit 0\\n' > /etc/rc.local && \ chmod +x /etc/rc.local && systemctl enable rc-local 2>/dev/null||true")]); info!("Assigned static IP {} to container '{}'", ip, container_name); diff --git a/vault-unseal-keys b/vault-unseal-keys deleted file mode 100644 index 6419936b..00000000 --- a/vault-unseal-keys +++ /dev/null @@ -1,5 +0,0 @@ -Unseal Key 1: KNMpbHTx0zYKG+P8oKoFNe7KKK7gEFCJ/IeoyXmHJpcb -Unseal Key 2: XoTPcUpbE4j5u6AeIjVhzWd1ku4U5NCANlPLf8pJMA3y -Unseal Key 3: sGEbMnQxDmS5itqsdNacia+glPKBIzLk2/9jntEkAIwo -Unseal Key 4: 9jPawRkzt0nmuR4V+KeecMns2in3pj+fQFqLfsfyimN1 -Unseal Key 5: JO0Bi3UXibXdBMTcCPNmmghXhLNcV14035KkZhc3kU1j