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>
285 lines
8.1 KiB
Rust
285 lines
8.1 KiB
Rust
//! Metrics database operations
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::PgPool;
|
|
use uuid::Uuid;
|
|
|
|
/// Metrics record from database
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct Metrics {
|
|
pub id: i64,
|
|
pub agent_id: Uuid,
|
|
pub timestamp: DateTime<Utc>,
|
|
pub cpu_percent: Option<f32>,
|
|
pub memory_percent: Option<f32>,
|
|
pub memory_used_bytes: Option<i64>,
|
|
pub disk_percent: Option<f32>,
|
|
pub disk_used_bytes: Option<i64>,
|
|
pub network_rx_bytes: Option<i64>,
|
|
pub network_tx_bytes: Option<i64>,
|
|
// Extended metrics
|
|
pub uptime_seconds: Option<i64>,
|
|
pub boot_time: Option<i64>,
|
|
pub logged_in_user: Option<String>,
|
|
pub user_idle_seconds: Option<i64>,
|
|
pub public_ip: Option<String>,
|
|
pub memory_total_bytes: Option<i64>,
|
|
pub disk_total_bytes: Option<i64>,
|
|
}
|
|
|
|
/// Create metrics data from agent
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct CreateMetrics {
|
|
pub agent_id: Uuid,
|
|
pub cpu_percent: Option<f32>,
|
|
pub memory_percent: Option<f32>,
|
|
pub memory_used_bytes: Option<i64>,
|
|
pub disk_percent: Option<f32>,
|
|
pub disk_used_bytes: Option<i64>,
|
|
pub network_rx_bytes: Option<i64>,
|
|
pub network_tx_bytes: Option<i64>,
|
|
// Extended metrics
|
|
pub uptime_seconds: Option<i64>,
|
|
pub boot_time: Option<i64>,
|
|
pub logged_in_user: Option<String>,
|
|
pub user_idle_seconds: Option<i64>,
|
|
pub public_ip: Option<String>,
|
|
pub memory_total_bytes: Option<i64>,
|
|
pub disk_total_bytes: Option<i64>,
|
|
}
|
|
|
|
/// Insert metrics into the database
|
|
pub async fn insert_metrics(pool: &PgPool, metrics: CreateMetrics) -> Result<i64, sqlx::Error> {
|
|
let result: (i64,) = sqlx::query_as(
|
|
r#"
|
|
INSERT INTO metrics (
|
|
agent_id, cpu_percent, memory_percent, memory_used_bytes,
|
|
disk_percent, disk_used_bytes, network_rx_bytes, network_tx_bytes,
|
|
uptime_seconds, boot_time, logged_in_user, user_idle_seconds,
|
|
public_ip, memory_total_bytes, disk_total_bytes
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
|
RETURNING id
|
|
"#,
|
|
)
|
|
.bind(metrics.agent_id)
|
|
.bind(metrics.cpu_percent)
|
|
.bind(metrics.memory_percent)
|
|
.bind(metrics.memory_used_bytes)
|
|
.bind(metrics.disk_percent)
|
|
.bind(metrics.disk_used_bytes)
|
|
.bind(metrics.network_rx_bytes)
|
|
.bind(metrics.network_tx_bytes)
|
|
.bind(metrics.uptime_seconds)
|
|
.bind(metrics.boot_time)
|
|
.bind(&metrics.logged_in_user)
|
|
.bind(metrics.user_idle_seconds)
|
|
.bind(&metrics.public_ip)
|
|
.bind(metrics.memory_total_bytes)
|
|
.bind(metrics.disk_total_bytes)
|
|
.fetch_one(pool)
|
|
.await?;
|
|
|
|
Ok(result.0)
|
|
}
|
|
|
|
/// Get recent metrics for an agent
|
|
pub async fn get_agent_metrics(
|
|
pool: &PgPool,
|
|
agent_id: Uuid,
|
|
limit: i64,
|
|
) -> Result<Vec<Metrics>, sqlx::Error> {
|
|
sqlx::query_as::<_, Metrics>(
|
|
r#"
|
|
SELECT * FROM metrics
|
|
WHERE agent_id = $1
|
|
ORDER BY timestamp DESC
|
|
LIMIT $2
|
|
"#,
|
|
)
|
|
.bind(agent_id)
|
|
.bind(limit)
|
|
.fetch_all(pool)
|
|
.await
|
|
}
|
|
|
|
/// Get metrics for an agent within a time range
|
|
pub async fn get_agent_metrics_range(
|
|
pool: &PgPool,
|
|
agent_id: Uuid,
|
|
start: DateTime<Utc>,
|
|
end: DateTime<Utc>,
|
|
) -> Result<Vec<Metrics>, sqlx::Error> {
|
|
sqlx::query_as::<_, Metrics>(
|
|
r#"
|
|
SELECT * FROM metrics
|
|
WHERE agent_id = $1 AND timestamp >= $2 AND timestamp <= $3
|
|
ORDER BY timestamp ASC
|
|
"#,
|
|
)
|
|
.bind(agent_id)
|
|
.bind(start)
|
|
.bind(end)
|
|
.fetch_all(pool)
|
|
.await
|
|
}
|
|
|
|
/// Get latest metrics for an agent
|
|
pub async fn get_latest_metrics(
|
|
pool: &PgPool,
|
|
agent_id: Uuid,
|
|
) -> Result<Option<Metrics>, sqlx::Error> {
|
|
sqlx::query_as::<_, Metrics>(
|
|
r#"
|
|
SELECT * FROM metrics
|
|
WHERE agent_id = $1
|
|
ORDER BY timestamp DESC
|
|
LIMIT 1
|
|
"#,
|
|
)
|
|
.bind(agent_id)
|
|
.fetch_optional(pool)
|
|
.await
|
|
}
|
|
|
|
/// Delete old metrics (for cleanup jobs)
|
|
pub async fn delete_old_metrics(
|
|
pool: &PgPool,
|
|
older_than: DateTime<Utc>,
|
|
) -> Result<u64, sqlx::Error> {
|
|
let result = sqlx::query("DELETE FROM metrics WHERE timestamp < $1")
|
|
.bind(older_than)
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
Ok(result.rows_affected())
|
|
}
|
|
|
|
/// Summary statistics for dashboard
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MetricsSummary {
|
|
pub avg_cpu: f32,
|
|
pub avg_memory: f32,
|
|
pub avg_disk: f32,
|
|
pub total_network_rx: i64,
|
|
pub total_network_tx: i64,
|
|
}
|
|
|
|
/// Get summary metrics across all agents (last hour)
|
|
pub async fn get_metrics_summary(pool: &PgPool) -> Result<MetricsSummary, sqlx::Error> {
|
|
let result: (Option<f64>, Option<f64>, Option<f64>, Option<i64>, Option<i64>) = sqlx::query_as(
|
|
r#"
|
|
SELECT
|
|
AVG(cpu_percent)::float8,
|
|
AVG(memory_percent)::float8,
|
|
AVG(disk_percent)::float8,
|
|
SUM(network_rx_bytes),
|
|
SUM(network_tx_bytes)
|
|
FROM metrics
|
|
WHERE timestamp > NOW() - INTERVAL '1 hour'
|
|
"#,
|
|
)
|
|
.fetch_one(pool)
|
|
.await?;
|
|
|
|
Ok(MetricsSummary {
|
|
avg_cpu: result.0.unwrap_or(0.0) as f32,
|
|
avg_memory: result.1.unwrap_or(0.0) as f32,
|
|
avg_disk: result.2.unwrap_or(0.0) as f32,
|
|
total_network_rx: result.3.unwrap_or(0),
|
|
total_network_tx: result.4.unwrap_or(0),
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Agent State (network interfaces, extended info snapshot)
|
|
// ============================================================================
|
|
|
|
/// Agent state record from database
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct AgentState {
|
|
pub agent_id: Uuid,
|
|
pub network_interfaces: Option<serde_json::Value>,
|
|
pub network_state_hash: Option<String>,
|
|
pub uptime_seconds: Option<i64>,
|
|
pub boot_time: Option<i64>,
|
|
pub logged_in_user: Option<String>,
|
|
pub user_idle_seconds: Option<i64>,
|
|
pub public_ip: Option<String>,
|
|
pub network_updated_at: Option<DateTime<Utc>>,
|
|
pub metrics_updated_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
/// Update or insert agent state (upsert)
|
|
pub async fn upsert_agent_state(
|
|
pool: &PgPool,
|
|
agent_id: Uuid,
|
|
uptime_seconds: Option<i64>,
|
|
boot_time: Option<i64>,
|
|
logged_in_user: Option<&str>,
|
|
user_idle_seconds: Option<i64>,
|
|
public_ip: Option<&str>,
|
|
) -> Result<(), sqlx::Error> {
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO agent_state (
|
|
agent_id, uptime_seconds, boot_time, logged_in_user,
|
|
user_idle_seconds, public_ip, metrics_updated_at
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, NOW())
|
|
ON CONFLICT (agent_id) DO UPDATE SET
|
|
uptime_seconds = EXCLUDED.uptime_seconds,
|
|
boot_time = EXCLUDED.boot_time,
|
|
logged_in_user = EXCLUDED.logged_in_user,
|
|
user_idle_seconds = EXCLUDED.user_idle_seconds,
|
|
public_ip = EXCLUDED.public_ip,
|
|
metrics_updated_at = NOW()
|
|
"#,
|
|
)
|
|
.bind(agent_id)
|
|
.bind(uptime_seconds)
|
|
.bind(boot_time)
|
|
.bind(logged_in_user)
|
|
.bind(user_idle_seconds)
|
|
.bind(public_ip)
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Update network state for an agent
|
|
pub async fn update_agent_network_state(
|
|
pool: &PgPool,
|
|
agent_id: Uuid,
|
|
interfaces: &serde_json::Value,
|
|
state_hash: &str,
|
|
) -> Result<(), sqlx::Error> {
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO agent_state (agent_id, network_interfaces, network_state_hash, network_updated_at)
|
|
VALUES ($1, $2, $3, NOW())
|
|
ON CONFLICT (agent_id) DO UPDATE SET
|
|
network_interfaces = EXCLUDED.network_interfaces,
|
|
network_state_hash = EXCLUDED.network_state_hash,
|
|
network_updated_at = NOW()
|
|
"#,
|
|
)
|
|
.bind(agent_id)
|
|
.bind(interfaces)
|
|
.bind(state_hash)
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get agent state by agent ID
|
|
pub async fn get_agent_state(pool: &PgPool, agent_id: Uuid) -> Result<Option<AgentState>, sqlx::Error> {
|
|
sqlx::query_as::<_, AgentState>("SELECT * FROM agent_state WHERE agent_id = $1")
|
|
.bind(agent_id)
|
|
.fetch_optional(pool)
|
|
.await
|
|
}
|