Created comprehensive VPN setup tooling for Peaceful Spirit L2TP/IPsec connection and enhanced agent documentation framework. VPN Configuration (PST-NW-VPN): - Setup-PST-L2TP-VPN.ps1: Automated L2TP/IPsec setup with split-tunnel and DNS - Connect-PST-VPN.ps1: Connection helper with PPP adapter detection, DNS (192.168.0.2), and route config (192.168.0.0/24) - Connect-PST-VPN-Standalone.ps1: Self-contained connection script for remote deployment - Fix-PST-VPN-Auth.ps1: Authentication troubleshooting for CHAP/MSChapv2 - Diagnose-VPN-Interface.ps1: Comprehensive VPN interface and routing diagnostic - Quick-Test-VPN.ps1: Fast connectivity verification (DNS/router/routes) - Add-PST-VPN-Route-Manual.ps1: Manual route configuration helper - vpn-connect.bat, vpn-disconnect.bat: Simple batch file shortcuts - OpenVPN config files (Windows-compatible, abandoned for L2TP) Key VPN Implementation Details: - L2TP creates PPP adapter with connection name as interface description - UniFi auto-configures DNS (192.168.0.2) but requires manual route to 192.168.0.0/24 - Split-tunnel enabled (only remote traffic through VPN) - All-user connection for pre-login auto-connect via scheduled task - Authentication: CHAP + MSChapv2 for UniFi compatibility Agent Documentation: - AGENT_QUICK_REFERENCE.md: Quick reference for all specialized agents - documentation-squire.md: Documentation and task management specialist agent - Updated all agent markdown files with standardized formatting Project Organization: - Moved conversation logs to dedicated directories (guru-connect-conversation-logs, guru-rmm-conversation-logs) - Cleaned up old session JSONL files from projects/msp-tools/ - Added guru-connect infrastructure (agent, dashboard, proto, scripts, .gitea workflows) - Added guru-rmm server components and deployment configs Technical Notes: - VPN IP pool: 192.168.4.x (client gets 192.168.4.6) - Remote network: 192.168.0.0/24 (router at 192.168.0.10) - PSK: rrClvnmUeXEFo90Ol+z7tfsAZHeSK6w7 - Credentials: pst-admin / 24Hearts$ Files: 15 VPN scripts, 2 agent docs, conversation log reorganization, guru-connect/guru-rmm infrastructure additions Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
291 lines
8.2 KiB
Rust
291 lines
8.2 KiB
Rust
//! 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<String>,
|
|
}
|
|
|
|
/// 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<ServiceWatch>,
|
|
|
|
/// List of processes to monitor
|
|
#[serde(default)]
|
|
pub processes: Vec<ProcessWatch>,
|
|
}
|
|
|
|
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<String>,
|
|
}
|
|
|
|
/// 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<Self> {
|
|
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);
|
|
}
|
|
}
|