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:
10
projects/msp-tools/guru-rmm/server/src/updates/mod.rs
Normal file
10
projects/msp-tools/guru-rmm/server/src/updates/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! Agent update management
|
||||
//!
|
||||
//! Handles:
|
||||
//! - Scanning downloads directory for available agent versions
|
||||
//! - Version comparison to determine if agents need updates
|
||||
//! - Update tracking and timeout monitoring
|
||||
|
||||
mod scanner;
|
||||
|
||||
pub use scanner::{UpdateManager, AvailableVersion};
|
||||
311
projects/msp-tools/guru-rmm/server/src/updates/scanner.rs
Normal file
311
projects/msp-tools/guru-rmm/server/src/updates/scanner.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
//! Version scanner for available agent binaries
|
||||
//!
|
||||
//! Scans a downloads directory for agent binaries and parses version info
|
||||
//! from filenames in the format: gururmm-agent-{os}-{arch}-{version}[.exe]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use semver::Version;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
/// Information about an available agent version
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AvailableVersion {
|
||||
/// Semantic version
|
||||
pub version: Version,
|
||||
/// Operating system (linux, windows)
|
||||
pub os: String,
|
||||
/// Architecture (amd64, arm64)
|
||||
pub arch: String,
|
||||
/// Filename on disk
|
||||
pub filename: String,
|
||||
/// Full download URL
|
||||
pub download_url: String,
|
||||
/// SHA256 checksum
|
||||
pub checksum_sha256: String,
|
||||
/// File size in bytes
|
||||
pub file_size: u64,
|
||||
}
|
||||
|
||||
/// Manages available agent versions
|
||||
pub struct UpdateManager {
|
||||
/// Directory containing agent binaries
|
||||
downloads_dir: PathBuf,
|
||||
/// Base URL for downloads
|
||||
base_url: String,
|
||||
/// Cached available versions, keyed by "os-arch"
|
||||
versions: Arc<RwLock<HashMap<String, Vec<AvailableVersion>>>>,
|
||||
/// Whether auto-updates are enabled
|
||||
pub auto_update_enabled: bool,
|
||||
/// Update timeout in seconds
|
||||
pub update_timeout_secs: u64,
|
||||
}
|
||||
|
||||
impl UpdateManager {
|
||||
/// Create a new UpdateManager
|
||||
pub fn new(
|
||||
downloads_dir: PathBuf,
|
||||
base_url: String,
|
||||
auto_update_enabled: bool,
|
||||
update_timeout_secs: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
downloads_dir,
|
||||
base_url,
|
||||
versions: Arc::new(RwLock::new(HashMap::new())),
|
||||
auto_update_enabled,
|
||||
update_timeout_secs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan the downloads directory for available agent binaries
|
||||
pub async fn scan_versions(&self) -> Result<()> {
|
||||
let mut versions: HashMap<String, Vec<AvailableVersion>> = HashMap::new();
|
||||
|
||||
if !self.downloads_dir.exists() {
|
||||
warn!("Downloads directory does not exist: {:?}", self.downloads_dir);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let entries = std::fs::read_dir(&self.downloads_dir)?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let filename = match path.file_name().and_then(|n| n.to_str()) {
|
||||
Some(name) => name.to_string(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Skip checksum files
|
||||
if filename.ends_with(".sha256") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to parse as agent binary
|
||||
if let Some((os, arch, version)) = Self::parse_filename(&filename) {
|
||||
// Read checksum from companion file
|
||||
let checksum = self.read_checksum(&path).await.unwrap_or_default();
|
||||
|
||||
if checksum.is_empty() {
|
||||
warn!("No checksum found for {}, skipping", filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get file size
|
||||
let file_size = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
let download_url = format!("{}/{}", self.base_url.trim_end_matches('/'), filename);
|
||||
|
||||
let available = AvailableVersion {
|
||||
version,
|
||||
os: os.clone(),
|
||||
arch: arch.clone(),
|
||||
filename: filename.clone(),
|
||||
download_url,
|
||||
checksum_sha256: checksum,
|
||||
file_size,
|
||||
};
|
||||
|
||||
let key = format!("{}-{}", os, arch);
|
||||
debug!("Found agent binary: {} (v{})", filename, available.version);
|
||||
versions.entry(key).or_default().push(available);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort each list by version descending (newest first)
|
||||
for list in versions.values_mut() {
|
||||
list.sort_by(|a, b| b.version.cmp(&a.version));
|
||||
}
|
||||
|
||||
let total: usize = versions.values().map(|v| v.len()).sum();
|
||||
info!("Scanned {} agent binaries across {} platform/arch combinations", total, versions.len());
|
||||
|
||||
*self.versions.write().await = versions;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a filename to extract OS, architecture, and version
|
||||
///
|
||||
/// Expected format: gururmm-agent-{os}-{arch}-{version}[.exe]
|
||||
/// Examples:
|
||||
/// - gururmm-agent-linux-amd64-0.2.0
|
||||
/// - gururmm-agent-windows-amd64-0.2.0.exe
|
||||
fn parse_filename(filename: &str) -> Option<(String, String, Version)> {
|
||||
// Remove .exe extension if present
|
||||
let name = filename.strip_suffix(".exe").unwrap_or(filename);
|
||||
|
||||
// Split by dashes
|
||||
let parts: Vec<&str> = name.split('-').collect();
|
||||
|
||||
// Expected: ["gururmm", "agent", "linux", "amd64", "0", "2", "0"]
|
||||
// or: ["gururmm", "agent", "linux", "amd64", "0.2.0"]
|
||||
if parts.len() < 5 || parts[0] != "gururmm" || parts[1] != "agent" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let os = parts[2].to_string();
|
||||
let arch = parts[3].to_string();
|
||||
|
||||
// Version could be either:
|
||||
// - A single part with dots: "0.2.0"
|
||||
// - Multiple parts joined: "0", "2", "0"
|
||||
let version_str = if parts.len() == 5 {
|
||||
// Single part with dots
|
||||
parts[4].to_string()
|
||||
} else {
|
||||
// Multiple parts, join with dots
|
||||
parts[4..].join(".")
|
||||
};
|
||||
|
||||
let version = Version::parse(&version_str).ok()?;
|
||||
Some((os, arch, version))
|
||||
}
|
||||
|
||||
/// Read checksum from companion .sha256 file
|
||||
async fn read_checksum(&self, binary_path: &Path) -> Result<String> {
|
||||
let checksum_path = PathBuf::from(format!("{}.sha256", binary_path.display()));
|
||||
|
||||
if !checksum_path.exists() {
|
||||
return Err(anyhow::anyhow!("Checksum file not found"));
|
||||
}
|
||||
|
||||
let content = tokio::fs::read_to_string(&checksum_path).await?;
|
||||
|
||||
// Checksum file format: "<hash> <filename>" or just "<hash>"
|
||||
let checksum = content
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Empty checksum file"))?
|
||||
.to_lowercase();
|
||||
|
||||
// Validate it looks like a SHA256 hash (64 hex chars)
|
||||
if checksum.len() != 64 || !checksum.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Err(anyhow::anyhow!("Invalid checksum format"));
|
||||
}
|
||||
|
||||
Ok(checksum)
|
||||
}
|
||||
|
||||
/// Get the latest version available for a given OS/arch
|
||||
pub async fn get_latest_version(&self, os: &str, arch: &str) -> Option<AvailableVersion> {
|
||||
let versions = self.versions.read().await;
|
||||
let key = format!("{}-{}", os, arch);
|
||||
versions.get(&key).and_then(|list| list.first().cloned())
|
||||
}
|
||||
|
||||
/// Check if an agent with the given version needs an update
|
||||
/// Returns the available update if one exists
|
||||
pub async fn needs_update(
|
||||
&self,
|
||||
current_version: &str,
|
||||
os: &str,
|
||||
arch: &str,
|
||||
) -> Option<AvailableVersion> {
|
||||
if !self.auto_update_enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
let current = match Version::parse(current_version) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
warn!("Failed to parse current version '{}': {}", current_version, e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let latest = self.get_latest_version(os, arch).await?;
|
||||
|
||||
if latest.version > current {
|
||||
info!(
|
||||
"Agent needs update: {} -> {} ({}-{})",
|
||||
current, latest.version, os, arch
|
||||
);
|
||||
Some(latest)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all available versions (for dashboard display)
|
||||
pub async fn get_all_versions(&self) -> HashMap<String, Vec<AvailableVersion>> {
|
||||
self.versions.read().await.clone()
|
||||
}
|
||||
|
||||
/// Spawn a background task to periodically rescan versions
|
||||
pub fn spawn_scanner(&self, interval_secs: u64) -> tokio::task::JoinHandle<()> {
|
||||
let downloads_dir = self.downloads_dir.clone();
|
||||
let base_url = self.base_url.clone();
|
||||
let versions = self.versions.clone();
|
||||
let auto_update_enabled = self.auto_update_enabled;
|
||||
let update_timeout_secs = self.update_timeout_secs;
|
||||
|
||||
tokio::spawn(async move {
|
||||
let manager = UpdateManager {
|
||||
downloads_dir,
|
||||
base_url,
|
||||
versions,
|
||||
auto_update_enabled,
|
||||
update_timeout_secs,
|
||||
};
|
||||
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(interval_secs));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if let Err(e) = manager.scan_versions().await {
|
||||
error!("Failed to scan versions: {}", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_filename_linux() {
|
||||
let result = UpdateManager::parse_filename("gururmm-agent-linux-amd64-0.2.0");
|
||||
assert!(result.is_some());
|
||||
let (os, arch, version) = result.unwrap();
|
||||
assert_eq!(os, "linux");
|
||||
assert_eq!(arch, "amd64");
|
||||
assert_eq!(version, Version::new(0, 2, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_filename_windows() {
|
||||
let result = UpdateManager::parse_filename("gururmm-agent-windows-amd64-0.2.0.exe");
|
||||
assert!(result.is_some());
|
||||
let (os, arch, version) = result.unwrap();
|
||||
assert_eq!(os, "windows");
|
||||
assert_eq!(arch, "amd64");
|
||||
assert_eq!(version, Version::new(0, 2, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_filename_arm64() {
|
||||
let result = UpdateManager::parse_filename("gururmm-agent-linux-arm64-1.0.0");
|
||||
assert!(result.is_some());
|
||||
let (os, arch, version) = result.unwrap();
|
||||
assert_eq!(os, "linux");
|
||||
assert_eq!(arch, "arm64");
|
||||
assert_eq!(version, Version::new(1, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_filename_invalid() {
|
||||
assert!(UpdateManager::parse_filename("random-file.txt").is_none());
|
||||
assert!(UpdateManager::parse_filename("gururmm-server-linux-amd64-0.1.0").is_none());
|
||||
assert!(UpdateManager::parse_filename("gururmm-agent-linux").is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user