mirror of
https://github.com/house-of-vanity/yggman.git
synced 2025-10-23 20:59:08 +00:00
Compare commits
4 Commits
a2eabe2c69
...
4bf349bfe3
Author | SHA1 | Date | |
---|---|---|---|
|
4bf349bfe3 | ||
|
4a30b441ca | ||
|
16e90ccb4d | ||
|
35ec6995a0 |
283
src/agent.rs
283
src/agent.rs
@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use tokio::time::sleep;
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
use tracing::{error, info, warn, debug};
|
||||
@@ -31,6 +32,14 @@ struct Args {
|
||||
/// Reconnect interval in seconds
|
||||
#[arg(long, default_value = "5")]
|
||||
reconnect_interval: u64,
|
||||
|
||||
/// Skip automatic Yggdrasil service restart after config changes
|
||||
#[arg(long)]
|
||||
no_restart: bool,
|
||||
|
||||
/// Custom command to restart Yggdrasil service (overrides platform detection)
|
||||
#[arg(long)]
|
||||
restart_command: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -57,6 +66,7 @@ enum ServerMessage {
|
||||
allowed_public_keys: Vec<String>,
|
||||
},
|
||||
Update {
|
||||
listen: Vec<String>,
|
||||
peers: Vec<String>,
|
||||
allowed_public_keys: Vec<String>,
|
||||
},
|
||||
@@ -181,7 +191,7 @@ async fn run_agent(args: &Args, ygg_config_path: &str) -> Result<()> {
|
||||
match msg {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
match serde_json::from_str::<ServerMessage>(&text) {
|
||||
Ok(server_msg) => handle_server_message(server_msg, ygg_config_path).await?,
|
||||
Ok(server_msg) => handle_server_message(server_msg, ygg_config_path, args.no_restart, &args.restart_command).await?,
|
||||
Err(e) => warn!("Failed to parse server message: {}", e),
|
||||
}
|
||||
}
|
||||
@@ -225,7 +235,7 @@ async fn run_agent(args: &Args, ygg_config_path: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_server_message(msg: ServerMessage, ygg_config_path: &str) -> Result<()> {
|
||||
async fn handle_server_message(msg: ServerMessage, ygg_config_path: &str, no_restart: bool, restart_command: &Option<String>) -> Result<()> {
|
||||
match msg {
|
||||
ServerMessage::Config {
|
||||
node_id,
|
||||
@@ -246,25 +256,49 @@ async fn handle_server_message(msg: ServerMessage, ygg_config_path: &str) -> Res
|
||||
|
||||
// Apply configuration to Yggdrasil
|
||||
match write_yggdrasil_config(ygg_config_path, &private_key, &listen, &peers, &allowed_public_keys).await {
|
||||
Ok(_) => info!("Configuration successfully applied to {}", ygg_config_path),
|
||||
Ok(_) => {
|
||||
info!("Configuration successfully written to {}", ygg_config_path);
|
||||
// Restart Yggdrasil service to apply new configuration
|
||||
if !no_restart {
|
||||
if let Err(e) = restart_yggdrasil_service(restart_command) {
|
||||
error!("Failed to restart Yggdrasil service: {}", e);
|
||||
}
|
||||
} else {
|
||||
info!("Skipping service restart (--no-restart flag set)");
|
||||
}
|
||||
},
|
||||
Err(e) => error!("Failed to write Yggdrasil config: {}", e),
|
||||
}
|
||||
}
|
||||
ServerMessage::Update {
|
||||
listen,
|
||||
peers,
|
||||
allowed_public_keys,
|
||||
} => {
|
||||
info!("Received configuration update:");
|
||||
info!(" Updated listen endpoints: {:?}", listen);
|
||||
info!(" Updated peers: {} configured", peers.len());
|
||||
for peer in &peers {
|
||||
debug!(" - {}", peer);
|
||||
}
|
||||
info!(" Updated allowed keys: {} configured", allowed_public_keys.len());
|
||||
|
||||
// Apply configuration update to Yggdrasil
|
||||
// For updates we need to read current config and update only peers/allowed keys
|
||||
match update_yggdrasil_config(ygg_config_path, &peers, &allowed_public_keys).await {
|
||||
Ok(_) => info!("Configuration update successfully applied to {}", ygg_config_path),
|
||||
// Apply full configuration update to Yggdrasil
|
||||
match update_yggdrasil_config_full(ygg_config_path, &listen, &peers, &allowed_public_keys).await {
|
||||
Ok(true) => {
|
||||
info!("Configuration update successfully applied to {}", ygg_config_path);
|
||||
// Restart Yggdrasil service to apply updated configuration
|
||||
if !no_restart {
|
||||
if let Err(e) = restart_yggdrasil_service(restart_command) {
|
||||
error!("Failed to restart Yggdrasil service: {}", e);
|
||||
}
|
||||
} else {
|
||||
info!("Skipping service restart (--no-restart flag set)");
|
||||
}
|
||||
},
|
||||
Ok(false) => {
|
||||
info!("Configuration unchanged, skipping restart");
|
||||
},
|
||||
Err(e) => error!("Failed to update Yggdrasil config: {}", e),
|
||||
}
|
||||
}
|
||||
@@ -346,10 +380,45 @@ async fn write_yggdrasil_config(
|
||||
});
|
||||
|
||||
let config_json = serde_json::to_string_pretty(&config)?;
|
||||
tokio::fs::write(config_path, config_json).await?;
|
||||
|
||||
// Try to write directly first
|
||||
match tokio::fs::write(config_path, &config_json).await {
|
||||
Ok(_) => {
|
||||
info!("Yggdrasil configuration written to {}", config_path);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||
// Try with sudo if permission denied
|
||||
warn!("Permission denied writing to {}, trying with sudo...", config_path);
|
||||
|
||||
use std::process::Stdio;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
let mut child = tokio::process::Command::new("sudo")
|
||||
.args(&["-n", "tee", config_path])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
if let Some(stdin) = child.stdin.as_mut() {
|
||||
stdin.write_all(config_json.as_bytes()).await?;
|
||||
}
|
||||
|
||||
let output = child.wait_with_output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
error!("Failed to write config with sudo. Make sure the agent has sudo privileges.");
|
||||
error!("You may need to add this to sudoers: 'username ALL=(ALL) NOPASSWD: /usr/bin/tee {}'", config_path);
|
||||
return Err(anyhow!("Failed to write config with sudo: {}", stderr));
|
||||
}
|
||||
|
||||
info!("Yggdrasil configuration written to {} with sudo", config_path);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(anyhow!("Failed to write configuration: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_yggdrasil_config(
|
||||
@@ -372,3 +441,201 @@ async fn update_yggdrasil_config(
|
||||
info!("Yggdrasil configuration updated in {}", config_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_yggdrasil_config_full(
|
||||
config_path: &str,
|
||||
listen: &[String],
|
||||
peers: &[String],
|
||||
allowed_public_keys: &[String]
|
||||
) -> Result<bool> { // Returns true if config was updated
|
||||
// Read current config
|
||||
let current_config = tokio::fs::read_to_string(config_path).await?;
|
||||
let mut config: serde_json::Value = serde_json::from_str(¤t_config)?;
|
||||
|
||||
// Check if config actually changed
|
||||
let old_listen = config["Listen"].clone();
|
||||
let old_peers = config["Peers"].clone();
|
||||
let old_keys = config["AllowedPublicKeys"].clone();
|
||||
|
||||
let new_listen = serde_json::json!(listen);
|
||||
let new_peers = serde_json::json!(peers);
|
||||
let new_keys = serde_json::json!(allowed_public_keys);
|
||||
|
||||
if old_listen == new_listen && old_peers == new_peers && old_keys == new_keys {
|
||||
debug!("Configuration unchanged, skipping update");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Update listen, peers and allowed public keys
|
||||
config["Listen"] = new_listen;
|
||||
config["Peers"] = new_peers;
|
||||
config["AllowedPublicKeys"] = new_keys;
|
||||
|
||||
// Write updated config back
|
||||
let updated_config = serde_json::to_string_pretty(&config)?;
|
||||
|
||||
// Try to write directly first
|
||||
match tokio::fs::write(config_path, &updated_config).await {
|
||||
Ok(_) => {
|
||||
info!("Yggdrasil configuration fully updated in {}", config_path);
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||
// Try with sudo if permission denied
|
||||
warn!("Permission denied writing to {}, trying with sudo...", config_path);
|
||||
|
||||
use std::process::Stdio;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
let mut child = tokio::process::Command::new("sudo")
|
||||
.args(&["-n", "tee", config_path])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
if let Some(stdin) = child.stdin.as_mut() {
|
||||
stdin.write_all(updated_config.as_bytes()).await?;
|
||||
}
|
||||
|
||||
let output = child.wait_with_output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
error!("Failed to write config with sudo. Make sure the agent has sudo privileges.");
|
||||
error!("You may need to add this to sudoers: 'username ALL=(ALL) NOPASSWD: /usr/bin/tee {}'", config_path);
|
||||
return Err(anyhow!("Failed to write config with sudo: {}", stderr));
|
||||
}
|
||||
|
||||
info!("Yggdrasil configuration fully updated in {} with sudo", config_path);
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => Err(anyhow!("Failed to write configuration: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
fn restart_yggdrasil_service(custom_command: &Option<String>) -> Result<()> {
|
||||
// If custom command is provided, use it
|
||||
if let Some(cmd) = custom_command {
|
||||
info!("Using custom restart command: {}", cmd);
|
||||
let parts: Vec<&str> = cmd.split_whitespace().collect();
|
||||
if parts.is_empty() {
|
||||
return Err(anyhow!("Invalid custom restart command"));
|
||||
}
|
||||
|
||||
let output = Command::new(parts[0])
|
||||
.args(&parts[1..])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
error!("Custom restart command failed. Stderr: {}", stderr);
|
||||
error!("Stdout: {}", stdout);
|
||||
return Err(anyhow!("Failed to restart Yggdrasil with custom command: {}", stderr));
|
||||
}
|
||||
|
||||
info!("Yggdrasil service restarted successfully with custom command");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Detect platform and restart accordingly
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
info!("Restarting Yggdrasil service on Linux...");
|
||||
|
||||
// First try with systemctl directly (in case we're running as root)
|
||||
let output = Command::new("systemctl")
|
||||
.args(&["restart", "yggdrasil"])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) if out.status.success() => {
|
||||
info!("Yggdrasil service restarted successfully");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(out) => {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
debug!("Direct systemctl failed: {}", stderr);
|
||||
|
||||
// Try with sudo if direct systemctl failed
|
||||
info!("Attempting restart with sudo...");
|
||||
let sudo_output = Command::new("sudo")
|
||||
.args(&["-n", "systemctl", "restart", "yggdrasil"])
|
||||
.output()?;
|
||||
|
||||
if !sudo_output.status.success() {
|
||||
let sudo_stderr = String::from_utf8_lossy(&sudo_output.stderr);
|
||||
error!("Failed to restart Yggdrasil service. Make sure the agent is running as root or has sudo privileges.");
|
||||
error!("You may need to add this to sudoers: 'username ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart yggdrasil'");
|
||||
return Err(anyhow!("Failed to restart Yggdrasil service: {}", sudo_stderr));
|
||||
}
|
||||
info!("Yggdrasil service restarted successfully with sudo");
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(anyhow!("Failed to execute systemctl: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
info!("Restarting Yggdrasil service on macOS...");
|
||||
|
||||
// First unload the service
|
||||
let unload = Command::new("launchctl")
|
||||
.args(&["unload", "/Library/LaunchDaemons/yggdrasil.plist"])
|
||||
.output()?;
|
||||
|
||||
if !unload.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&unload.stderr);
|
||||
warn!("Failed to unload Yggdrasil service: {} (continuing anyway)", stderr);
|
||||
}
|
||||
|
||||
// Then load it again
|
||||
let load = Command::new("launchctl")
|
||||
.args(&["load", "/Library/LaunchDaemons/yggdrasil.plist"])
|
||||
.output()?;
|
||||
|
||||
if !load.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&load.stderr);
|
||||
return Err(anyhow!("Failed to load Yggdrasil service: {}", stderr));
|
||||
}
|
||||
info!("Yggdrasil service restarted successfully");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "freebsd")]
|
||||
{
|
||||
info!("Restarting Yggdrasil service on FreeBSD...");
|
||||
let output = Command::new("service")
|
||||
.args(&["yggdrasil", "restart"])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow!("Failed to restart Yggdrasil service: {}", stderr));
|
||||
}
|
||||
info!("Yggdrasil service restarted successfully");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "openbsd")]
|
||||
{
|
||||
info!("Restarting Yggdrasil service on OpenBSD...");
|
||||
let output = Command::new("rcctl")
|
||||
.args(&["restart", "yggdrasil"])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow!("Failed to restart Yggdrasil service: {}", stderr));
|
||||
}
|
||||
info!("Yggdrasil service restarted successfully");
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "openbsd")))]
|
||||
{
|
||||
warn!("Platform not supported for automatic service restart. Please restart Yggdrasil manually.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -31,6 +31,7 @@ pub enum ServerMessage {
|
||||
allowed_public_keys: Vec<String>,
|
||||
},
|
||||
Update {
|
||||
listen: Vec<String>,
|
||||
peers: Vec<String>,
|
||||
allowed_public_keys: Vec<String>,
|
||||
},
|
||||
@@ -149,6 +150,14 @@ pub async fn handle_agent_socket(
|
||||
|
||||
// Get current node information
|
||||
if let Some(current_node) = node_manager.get_node_by_id(id).await {
|
||||
// Sort addresses for comparison to avoid false positives
|
||||
let mut new_addresses = addresses.clone();
|
||||
new_addresses.sort();
|
||||
let mut current_addresses = current_node.addresses.clone();
|
||||
current_addresses.sort();
|
||||
|
||||
// Only update if addresses actually changed
|
||||
if new_addresses != current_addresses {
|
||||
// Update node with new addresses
|
||||
match node_manager.update_node(
|
||||
id,
|
||||
@@ -165,6 +174,9 @@ pub async fn handle_agent_socket(
|
||||
error!("Failed to update addresses for node {}: {}", id, e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Address list unchanged for node {}, skipping update", id);
|
||||
}
|
||||
} else {
|
||||
warn!("Cannot update addresses for unknown node: {}", id);
|
||||
}
|
||||
|
@@ -35,6 +35,7 @@ pub async fn broadcast_configuration_update(node_manager: &Arc<NodeManager>) {
|
||||
for (node_id, tx) in connections.iter() {
|
||||
if let Some(config) = configs.get(node_id) {
|
||||
let update = ServerMessage::Update {
|
||||
listen: config.listen.clone(),
|
||||
peers: config.peers.clone(),
|
||||
allowed_public_keys: config.allowed_public_keys.clone(),
|
||||
};
|
||||
@@ -46,6 +47,7 @@ pub async fn broadcast_configuration_update(node_manager: &Arc<NodeManager>) {
|
||||
} else {
|
||||
// Node was deleted, send empty configuration to disconnect agent gracefully
|
||||
let update = ServerMessage::Update {
|
||||
listen: vec![],
|
||||
peers: vec![],
|
||||
allowed_public_keys: vec![],
|
||||
};
|
||||
|
Reference in New Issue
Block a user