//! Agent configuration handling //! //! Configuration is loaded from a TOML file (default: agent.toml). //! The config file defines server connection, metrics collection, //! and watchdog settings. use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::path::Path; /// Root configuration structure #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentConfig { /// Server connection settings pub server: ServerConfig, /// Metrics collection settings #[serde(default)] pub metrics: MetricsConfig, /// Watchdog settings for monitoring services/processes #[serde(default)] pub watchdog: WatchdogConfig, } /// Server connection configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerConfig { /// WebSocket URL for the GuruRMM server (e.g., wss://rmm.example.com/ws) pub url: String, /// API key for authentication (obtained from server during registration) pub api_key: String, /// Optional custom hostname to report (defaults to system hostname) pub hostname_override: Option, } /// Metrics collection configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MetricsConfig { /// Interval in seconds between metrics collection (default: 60) #[serde(default = "default_metrics_interval")] pub interval_seconds: u64, /// Whether to collect CPU metrics #[serde(default = "default_true")] pub collect_cpu: bool, /// Whether to collect memory metrics #[serde(default = "default_true")] pub collect_memory: bool, /// Whether to collect disk metrics #[serde(default = "default_true")] pub collect_disk: bool, /// Whether to collect network metrics #[serde(default = "default_true")] pub collect_network: bool, } impl Default for MetricsConfig { fn default() -> Self { Self { interval_seconds: 60, collect_cpu: true, collect_memory: true, collect_disk: true, collect_network: true, } } } /// Watchdog configuration for service/process monitoring #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WatchdogConfig { /// Enable/disable watchdog functionality #[serde(default)] pub enabled: bool, /// Interval in seconds between watchdog checks (default: 30) #[serde(default = "default_watchdog_interval")] pub check_interval_seconds: u64, /// List of Windows/systemd services to monitor #[serde(default)] pub services: Vec, /// List of processes to monitor #[serde(default)] pub processes: Vec, } impl Default for WatchdogConfig { fn default() -> Self { Self { enabled: false, check_interval_seconds: 30, services: Vec::new(), processes: Vec::new(), } } } /// Configuration for monitoring a service #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServiceWatch { /// Service name (e.g., "CagService" for Datto RMM, "Syncro" for Syncro) pub name: String, /// Action to take when service is stopped #[serde(default)] pub action: WatchAction, /// Maximum number of restart attempts before alerting (default: 3) #[serde(default = "default_max_restarts")] pub max_restarts: u32, /// Cooldown period in seconds between restart attempts #[serde(default = "default_restart_cooldown")] pub restart_cooldown_seconds: u64, } /// Configuration for monitoring a process #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProcessWatch { /// Process name (e.g., "AEM.exe") pub name: String, /// Action to take when process is not found #[serde(default)] pub action: WatchAction, /// Optional path to executable to start if process is not running pub start_command: Option, } /// Action to take when a watched service/process is down #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum WatchAction { /// Only send an alert to the server #[default] Alert, /// Attempt to restart the service/process Restart, /// Ignore (for temporary disable without removing config) Ignore, } // Default value functions for serde fn default_metrics_interval() -> u64 { 60 } fn default_watchdog_interval() -> u64 { 30 } fn default_max_restarts() -> u32 { 3 } fn default_restart_cooldown() -> u64 { 60 } fn default_true() -> bool { true } impl AgentConfig { /// Load configuration from a TOML file pub fn load(path: &Path) -> Result { let content = std::fs::read_to_string(path) .with_context(|| format!("Failed to read config file: {:?}", path))?; let config: Self = toml::from_str(&content) .with_context(|| format!("Failed to parse config file: {:?}", path))?; config.validate()?; Ok(config) } /// Validate the configuration fn validate(&self) -> Result<()> { // Validate server URL if self.server.url.is_empty() { anyhow::bail!("Server URL cannot be empty"); } if !self.server.url.starts_with("ws://") && !self.server.url.starts_with("wss://") { anyhow::bail!("Server URL must start with ws:// or wss://"); } // Validate API key if self.server.api_key.is_empty() { anyhow::bail!("API key cannot be empty"); } // Validate intervals if self.metrics.interval_seconds < 10 { anyhow::bail!("Metrics interval must be at least 10 seconds"); } if self.watchdog.check_interval_seconds < 5 { anyhow::bail!("Watchdog check interval must be at least 5 seconds"); } Ok(()) } /// Generate a sample configuration pub fn sample() -> Self { Self { server: ServerConfig { url: "wss://rmm-api.azcomputerguru.com/ws".to_string(), api_key: "your-api-key-here".to_string(), hostname_override: None, }, metrics: MetricsConfig::default(), watchdog: WatchdogConfig { enabled: true, check_interval_seconds: 30, services: vec![ ServiceWatch { name: "CagService".to_string(), // Datto RMM action: WatchAction::Restart, max_restarts: 3, restart_cooldown_seconds: 60, }, ServiceWatch { name: "Syncro".to_string(), action: WatchAction::Restart, max_restarts: 3, restart_cooldown_seconds: 60, }, ], processes: vec![ProcessWatch { name: "AEM.exe".to_string(), // Datto AEM action: WatchAction::Alert, start_command: None, }], }, } } /// Get the hostname to report to the server pub fn get_hostname(&self) -> String { self.server .hostname_override .clone() .unwrap_or_else(|| hostname::get().map(|h| h.to_string_lossy().to_string()).unwrap_or_else(|_| "unknown".to_string())) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_sample_config_is_valid_structure() { let sample = AgentConfig::sample(); // Sample uses placeholder values, so it won't pass full validation // but the structure should be correct assert!(!sample.server.url.is_empty()); assert!(!sample.server.api_key.is_empty()); assert!(sample.watchdog.enabled); assert!(!sample.watchdog.services.is_empty()); } #[test] fn test_default_metrics_config() { let config = MetricsConfig::default(); assert_eq!(config.interval_seconds, 60); assert!(config.collect_cpu); assert!(config.collect_memory); assert!(config.collect_disk); assert!(config.collect_network); } #[test] fn test_watch_action_default() { let action = WatchAction::default(); assert_eq!(action, WatchAction::Alert); } }