Add VPN configuration tools and agent documentation

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>
This commit is contained in:
2026-01-18 11:51:47 -07:00
parent b0a68d89bf
commit 6c316aa701
272 changed files with 37068 additions and 2 deletions

View File

@@ -0,0 +1,290 @@
//! 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);
}
}