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:
605
projects/msp-tools/guru-rmm/agent/src/metrics/mod.rs
Normal file
605
projects/msp-tools/guru-rmm/agent/src/metrics/mod.rs
Normal file
@@ -0,0 +1,605 @@
|
||||
//! System metrics collection module
|
||||
//!
|
||||
//! Uses the `sysinfo` crate for cross-platform system metrics collection.
|
||||
//! Collects CPU, memory, disk, and network statistics.
|
||||
//! Uses `local-ip-address` for network interface enumeration.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use local_ip_address::list_afinet_netifas;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Mutex;
|
||||
use sysinfo::{CpuRefreshKind, Disks, MemoryRefreshKind, Networks, RefreshKind, System, Users};
|
||||
|
||||
/// System metrics data structure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemMetrics {
|
||||
/// Timestamp when metrics were collected
|
||||
pub timestamp: DateTime<Utc>,
|
||||
|
||||
/// CPU usage percentage (0-100)
|
||||
pub cpu_percent: f32,
|
||||
|
||||
/// Memory usage percentage (0-100)
|
||||
pub memory_percent: f32,
|
||||
|
||||
/// Memory used in bytes
|
||||
pub memory_used_bytes: u64,
|
||||
|
||||
/// Total memory in bytes
|
||||
pub memory_total_bytes: u64,
|
||||
|
||||
/// Disk usage percentage (0-100) - primary disk
|
||||
pub disk_percent: f32,
|
||||
|
||||
/// Disk used in bytes - primary disk
|
||||
pub disk_used_bytes: u64,
|
||||
|
||||
/// Total disk space in bytes - primary disk
|
||||
pub disk_total_bytes: u64,
|
||||
|
||||
/// Network bytes received since last collection
|
||||
pub network_rx_bytes: u64,
|
||||
|
||||
/// Network bytes transmitted since last collection
|
||||
pub network_tx_bytes: u64,
|
||||
|
||||
/// Operating system type
|
||||
pub os_type: String,
|
||||
|
||||
/// Operating system version
|
||||
pub os_version: String,
|
||||
|
||||
/// System hostname
|
||||
pub hostname: String,
|
||||
|
||||
/// System uptime in seconds
|
||||
#[serde(default)]
|
||||
pub uptime_seconds: u64,
|
||||
|
||||
/// Boot time as Unix timestamp
|
||||
#[serde(default)]
|
||||
pub boot_time: i64,
|
||||
|
||||
/// Logged in username (if available)
|
||||
#[serde(default)]
|
||||
pub logged_in_user: Option<String>,
|
||||
|
||||
/// User idle time in seconds (time since last input)
|
||||
#[serde(default)]
|
||||
pub user_idle_seconds: Option<u64>,
|
||||
|
||||
/// Public/WAN IP address (fetched periodically)
|
||||
#[serde(default)]
|
||||
pub public_ip: Option<String>,
|
||||
}
|
||||
|
||||
/// Metrics collector using sysinfo
|
||||
pub struct MetricsCollector {
|
||||
/// System info instance (needs to be refreshed for each collection)
|
||||
system: Mutex<System>,
|
||||
|
||||
/// Previous network stats for delta calculation
|
||||
prev_network_rx: Mutex<u64>,
|
||||
prev_network_tx: Mutex<u64>,
|
||||
|
||||
/// Cached public IP (refreshed less frequently)
|
||||
cached_public_ip: Mutex<Option<String>>,
|
||||
|
||||
/// Last time public IP was fetched
|
||||
last_public_ip_fetch: Mutex<Option<std::time::Instant>>,
|
||||
}
|
||||
|
||||
impl MetricsCollector {
|
||||
/// Create a new metrics collector
|
||||
pub fn new() -> Self {
|
||||
// Create system with minimal initial refresh
|
||||
let system = System::new_with_specifics(
|
||||
RefreshKind::new()
|
||||
.with_cpu(CpuRefreshKind::everything())
|
||||
.with_memory(MemoryRefreshKind::everything()),
|
||||
);
|
||||
|
||||
Self {
|
||||
system: Mutex::new(system),
|
||||
prev_network_rx: Mutex::new(0),
|
||||
prev_network_tx: Mutex::new(0),
|
||||
cached_public_ip: Mutex::new(None),
|
||||
last_public_ip_fetch: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect current system metrics
|
||||
pub async fn collect(&self) -> SystemMetrics {
|
||||
// Collect CPU - need to do two refreshes with delay for accurate reading
|
||||
// We release the lock between operations to avoid holding MutexGuard across await
|
||||
{
|
||||
let mut system = self.system.lock().unwrap();
|
||||
system.refresh_cpu_all();
|
||||
}
|
||||
|
||||
// Small delay for CPU measurement accuracy
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||
|
||||
// Collect all synchronous metrics first, in a block that releases all locks
|
||||
let (
|
||||
cpu_percent,
|
||||
memory_percent,
|
||||
memory_used,
|
||||
memory_total,
|
||||
disk_percent,
|
||||
disk_used,
|
||||
disk_total,
|
||||
delta_rx,
|
||||
delta_tx,
|
||||
os_type,
|
||||
os_version,
|
||||
hostname,
|
||||
uptime_seconds,
|
||||
boot_time,
|
||||
logged_in_user,
|
||||
user_idle_seconds,
|
||||
) = {
|
||||
// Acquire system lock
|
||||
let mut system = self.system.lock().unwrap();
|
||||
system.refresh_cpu_all();
|
||||
system.refresh_memory();
|
||||
|
||||
// Calculate CPU usage (average across all cores)
|
||||
let cpu_percent = system.global_cpu_usage();
|
||||
|
||||
// Memory metrics
|
||||
let memory_used = system.used_memory();
|
||||
let memory_total = system.total_memory();
|
||||
let memory_percent = if memory_total > 0 {
|
||||
(memory_used as f32 / memory_total as f32) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Disk metrics (use first/primary disk)
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
let (disk_used, disk_total, disk_percent) = disks
|
||||
.iter()
|
||||
.next()
|
||||
.map(|d| {
|
||||
let total = d.total_space();
|
||||
let available = d.available_space();
|
||||
let used = total.saturating_sub(available);
|
||||
let percent = if total > 0 {
|
||||
(used as f32 / total as f32) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
(used, total, percent)
|
||||
})
|
||||
.unwrap_or((0, 0, 0.0));
|
||||
|
||||
// Network metrics (sum all interfaces)
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
let (total_rx, total_tx): (u64, u64) = networks
|
||||
.iter()
|
||||
.map(|(_, data)| (data.total_received(), data.total_transmitted()))
|
||||
.fold((0, 0), |(acc_rx, acc_tx), (rx, tx)| {
|
||||
(acc_rx + rx, acc_tx + tx)
|
||||
});
|
||||
|
||||
// Calculate delta from previous collection
|
||||
let (delta_rx, delta_tx) = {
|
||||
let mut prev_rx = self.prev_network_rx.lock().unwrap();
|
||||
let mut prev_tx = self.prev_network_tx.lock().unwrap();
|
||||
|
||||
let delta_rx = total_rx.saturating_sub(*prev_rx);
|
||||
let delta_tx = total_tx.saturating_sub(*prev_tx);
|
||||
|
||||
*prev_rx = total_rx;
|
||||
*prev_tx = total_tx;
|
||||
|
||||
(delta_rx, delta_tx)
|
||||
};
|
||||
|
||||
// Get OS info
|
||||
let os_type = std::env::consts::OS.to_string();
|
||||
let os_version = System::os_version().unwrap_or_else(|| "unknown".to_string());
|
||||
let hostname = System::host_name().unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
// Get uptime and boot time
|
||||
let uptime_seconds = System::uptime();
|
||||
let boot_time = System::boot_time() as i64;
|
||||
|
||||
// Get logged in user
|
||||
let logged_in_user = self.get_logged_in_user();
|
||||
|
||||
// Get user idle time (platform-specific)
|
||||
let user_idle_seconds = self.get_user_idle_time();
|
||||
|
||||
// Return all values - locks are dropped at end of this block
|
||||
(
|
||||
cpu_percent,
|
||||
memory_percent,
|
||||
memory_used,
|
||||
memory_total,
|
||||
disk_percent,
|
||||
disk_used,
|
||||
disk_total,
|
||||
delta_rx,
|
||||
delta_tx,
|
||||
os_type,
|
||||
os_version,
|
||||
hostname,
|
||||
uptime_seconds,
|
||||
boot_time,
|
||||
logged_in_user,
|
||||
user_idle_seconds,
|
||||
)
|
||||
};
|
||||
|
||||
// All locks are now released - safe to do async work
|
||||
// Get public IP (cached, refreshed every 5 minutes)
|
||||
let public_ip = self.get_public_ip().await;
|
||||
|
||||
SystemMetrics {
|
||||
timestamp: Utc::now(),
|
||||
cpu_percent,
|
||||
memory_percent,
|
||||
memory_used_bytes: memory_used,
|
||||
memory_total_bytes: memory_total,
|
||||
disk_percent,
|
||||
disk_used_bytes: disk_used,
|
||||
disk_total_bytes: disk_total,
|
||||
network_rx_bytes: delta_rx,
|
||||
network_tx_bytes: delta_tx,
|
||||
os_type,
|
||||
os_version,
|
||||
hostname,
|
||||
uptime_seconds,
|
||||
boot_time,
|
||||
logged_in_user,
|
||||
user_idle_seconds,
|
||||
public_ip,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the currently logged in user
|
||||
fn get_logged_in_user(&self) -> Option<String> {
|
||||
let users = Users::new_with_refreshed_list();
|
||||
// Return the first user found (typically the console user)
|
||||
users.iter().next().map(|u| u.name().to_string())
|
||||
}
|
||||
|
||||
/// Get user idle time in seconds (time since last keyboard/mouse input)
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_user_idle_time(&self) -> Option<u64> {
|
||||
// Windows: Use GetLastInputInfo API
|
||||
use std::mem;
|
||||
|
||||
#[repr(C)]
|
||||
struct LASTINPUTINFO {
|
||||
cb_size: u32,
|
||||
dw_time: u32,
|
||||
}
|
||||
|
||||
extern "system" {
|
||||
fn GetLastInputInfo(plii: *mut LASTINPUTINFO) -> i32;
|
||||
fn GetTickCount() -> u32;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let mut lii = LASTINPUTINFO {
|
||||
cb_size: mem::size_of::<LASTINPUTINFO>() as u32,
|
||||
dw_time: 0,
|
||||
};
|
||||
|
||||
if GetLastInputInfo(&mut lii) != 0 {
|
||||
let idle_ms = GetTickCount().wrapping_sub(lii.dw_time);
|
||||
Some((idle_ms / 1000) as u64)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get user idle time in seconds (Unix/macOS)
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn get_user_idle_time(&self) -> Option<u64> {
|
||||
// Unix: Check /dev/tty* or use platform-specific APIs
|
||||
// For now, return None - can be enhanced with X11/Wayland idle detection
|
||||
None
|
||||
}
|
||||
|
||||
/// Get public IP address (cached for 5 minutes)
|
||||
async fn get_public_ip(&self) -> Option<String> {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const REFRESH_INTERVAL: Duration = Duration::from_secs(300); // 5 minutes
|
||||
|
||||
// Check if we have a cached value that's still fresh
|
||||
{
|
||||
let last_fetch = self.last_public_ip_fetch.lock().unwrap();
|
||||
let cached_ip = self.cached_public_ip.lock().unwrap();
|
||||
|
||||
if let Some(last) = *last_fetch {
|
||||
if last.elapsed() < REFRESH_INTERVAL {
|
||||
return cached_ip.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch new public IP
|
||||
let new_ip = self.fetch_public_ip().await;
|
||||
|
||||
// Update cache
|
||||
{
|
||||
let mut last_fetch = self.last_public_ip_fetch.lock().unwrap();
|
||||
let mut cached_ip = self.cached_public_ip.lock().unwrap();
|
||||
*last_fetch = Some(Instant::now());
|
||||
*cached_ip = new_ip.clone();
|
||||
}
|
||||
|
||||
new_ip
|
||||
}
|
||||
|
||||
/// Fetch public IP from external service
|
||||
async fn fetch_public_ip(&self) -> Option<String> {
|
||||
// Try multiple services for reliability
|
||||
let services = [
|
||||
"https://api.ipify.org",
|
||||
"https://ifconfig.me/ip",
|
||||
"https://icanhazip.com",
|
||||
];
|
||||
|
||||
for service in &services {
|
||||
match reqwest::get(*service).await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
if let Ok(ip) = resp.text().await {
|
||||
let ip = ip.trim().to_string();
|
||||
// Basic validation: should look like an IP
|
||||
if ip.parse::<std::net::IpAddr>().is_ok() {
|
||||
return Some(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Get basic system info (for registration)
|
||||
pub fn get_system_info(&self) -> SystemInfo {
|
||||
let system = self.system.lock().unwrap();
|
||||
|
||||
SystemInfo {
|
||||
os_type: std::env::consts::OS.to_string(),
|
||||
os_version: System::os_version().unwrap_or_else(|| "unknown".to_string()),
|
||||
hostname: System::host_name().unwrap_or_else(|| "unknown".to_string()),
|
||||
cpu_count: system.cpus().len() as u32,
|
||||
total_memory_bytes: system.total_memory(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MetricsCollector {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Basic system information (for agent registration)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemInfo {
|
||||
/// Operating system type (windows, linux, macos)
|
||||
pub os_type: String,
|
||||
|
||||
/// Operating system version
|
||||
pub os_version: String,
|
||||
|
||||
/// System hostname
|
||||
pub hostname: String,
|
||||
|
||||
/// Number of CPU cores
|
||||
pub cpu_count: u32,
|
||||
|
||||
/// Total memory in bytes
|
||||
pub total_memory_bytes: u64,
|
||||
}
|
||||
|
||||
/// Network interface information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct NetworkInterface {
|
||||
/// Interface name (e.g., "eth0", "Wi-Fi", "Ethernet")
|
||||
pub name: String,
|
||||
|
||||
/// MAC address (if available from sysinfo)
|
||||
pub mac_address: Option<String>,
|
||||
|
||||
/// IPv4 addresses assigned to this interface
|
||||
pub ipv4_addresses: Vec<String>,
|
||||
|
||||
/// IPv6 addresses assigned to this interface
|
||||
pub ipv6_addresses: Vec<String>,
|
||||
}
|
||||
|
||||
/// Complete network state (sent on connect and on change)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct NetworkState {
|
||||
/// Timestamp when network state was collected
|
||||
pub timestamp: DateTime<Utc>,
|
||||
|
||||
/// All network interfaces with their addresses
|
||||
pub interfaces: Vec<NetworkInterface>,
|
||||
|
||||
/// Hash of the network state for quick change detection
|
||||
pub state_hash: String,
|
||||
}
|
||||
|
||||
impl NetworkState {
|
||||
/// Collect current network state from the system
|
||||
pub fn collect() -> Self {
|
||||
let mut interface_map: HashMap<String, NetworkInterface> = HashMap::new();
|
||||
|
||||
// Get IP addresses from local-ip-address crate
|
||||
if let Ok(netifas) = list_afinet_netifas() {
|
||||
for (name, ip) in netifas {
|
||||
let entry = interface_map.entry(name.clone()).or_insert_with(|| {
|
||||
NetworkInterface {
|
||||
name: name.clone(),
|
||||
mac_address: None,
|
||||
ipv4_addresses: Vec::new(),
|
||||
ipv6_addresses: Vec::new(),
|
||||
}
|
||||
});
|
||||
|
||||
match ip {
|
||||
IpAddr::V4(addr) => {
|
||||
let addr_str = addr.to_string();
|
||||
if !entry.ipv4_addresses.contains(&addr_str) {
|
||||
entry.ipv4_addresses.push(addr_str);
|
||||
}
|
||||
}
|
||||
IpAddr::V6(addr) => {
|
||||
let addr_str = addr.to_string();
|
||||
if !entry.ipv6_addresses.contains(&addr_str) {
|
||||
entry.ipv6_addresses.push(addr_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get MAC addresses from sysinfo
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
for (name, data) in &networks {
|
||||
if let Some(entry) = interface_map.get_mut(name) {
|
||||
let mac = data.mac_address();
|
||||
let mac_str = format!(
|
||||
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
||||
mac.0[0], mac.0[1], mac.0[2], mac.0[3], mac.0[4], mac.0[5]
|
||||
);
|
||||
// Don't store empty/null MACs
|
||||
if mac_str != "00:00:00:00:00:00" {
|
||||
entry.mac_address = Some(mac_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to sorted vec for consistent ordering
|
||||
let mut interfaces: Vec<NetworkInterface> = interface_map.into_values().collect();
|
||||
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
// Filter out loopback and link-local only interfaces
|
||||
interfaces.retain(|iface| {
|
||||
// Keep if has any non-loopback IPv4
|
||||
let has_real_ipv4 = iface.ipv4_addresses.iter().any(|ip| {
|
||||
!ip.starts_with("127.") && !ip.starts_with("169.254.")
|
||||
});
|
||||
// Keep if has any non-link-local IPv6
|
||||
let has_real_ipv6 = iface.ipv6_addresses.iter().any(|ip| {
|
||||
!ip.starts_with("fe80:") && !ip.starts_with("::1")
|
||||
});
|
||||
has_real_ipv4 || has_real_ipv6
|
||||
});
|
||||
|
||||
// Generate hash for change detection
|
||||
let state_hash = Self::compute_hash(&interfaces);
|
||||
|
||||
NetworkState {
|
||||
timestamp: Utc::now(),
|
||||
interfaces,
|
||||
state_hash,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute a simple hash of the network state for change detection
|
||||
fn compute_hash(interfaces: &[NetworkInterface]) -> String {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
for iface in interfaces {
|
||||
iface.name.hash(&mut hasher);
|
||||
iface.mac_address.hash(&mut hasher);
|
||||
for ip in &iface.ipv4_addresses {
|
||||
ip.hash(&mut hasher);
|
||||
}
|
||||
for ip in &iface.ipv6_addresses {
|
||||
ip.hash(&mut hasher);
|
||||
}
|
||||
}
|
||||
format!("{:016x}", hasher.finish())
|
||||
}
|
||||
|
||||
/// Check if network state has changed compared to another state
|
||||
pub fn has_changed(&self, other: &NetworkState) -> bool {
|
||||
self.state_hash != other.state_hash
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_metrics_collection() {
|
||||
let collector = MetricsCollector::new();
|
||||
let metrics = collector.collect().await;
|
||||
|
||||
// Basic sanity checks
|
||||
assert!(metrics.cpu_percent >= 0.0 && metrics.cpu_percent <= 100.0);
|
||||
assert!(metrics.memory_percent >= 0.0 && metrics.memory_percent <= 100.0);
|
||||
assert!(metrics.memory_total_bytes > 0);
|
||||
assert!(!metrics.os_type.is_empty());
|
||||
assert!(!metrics.hostname.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_info() {
|
||||
let collector = MetricsCollector::new();
|
||||
let info = collector.get_system_info();
|
||||
|
||||
assert!(!info.os_type.is_empty());
|
||||
assert!(!info.hostname.is_empty());
|
||||
assert!(info.cpu_count > 0);
|
||||
assert!(info.total_memory_bytes > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_state_collection() {
|
||||
let state = NetworkState::collect();
|
||||
|
||||
// Should have a valid timestamp
|
||||
assert!(state.timestamp <= Utc::now());
|
||||
|
||||
// Should have a hash
|
||||
assert!(!state.state_hash.is_empty());
|
||||
assert_eq!(state.state_hash.len(), 16); // 64-bit hash as hex
|
||||
|
||||
// Print for debugging
|
||||
println!("Network state collected:");
|
||||
for iface in &state.interfaces {
|
||||
println!(" {}: IPv4={:?}, IPv6={:?}, MAC={:?}",
|
||||
iface.name, iface.ipv4_addresses, iface.ipv6_addresses, iface.mac_address);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_state_change_detection() {
|
||||
let state1 = NetworkState::collect();
|
||||
let state2 = NetworkState::collect();
|
||||
|
||||
// Same state should have same hash
|
||||
assert!(!state1.has_changed(&state2));
|
||||
|
||||
// Create a modified state
|
||||
let mut modified = state1.clone();
|
||||
if let Some(iface) = modified.interfaces.first_mut() {
|
||||
iface.ipv4_addresses.push("10.99.99.99".to_string());
|
||||
}
|
||||
modified.state_hash = NetworkState::compute_hash(&modified.interfaces);
|
||||
|
||||
// Modified state should be detected as changed
|
||||
assert!(state1.has_changed(&modified));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user