Files
claudetools/projects/msp-tools/guru-rmm/server/src/db/updates.rs
Mike Swanson a602118f6d 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>
2026-01-18 11:51:47 -07:00

218 lines
5.4 KiB
Rust

//! Database operations for agent updates
use anyhow::Result;
use sqlx::{PgPool, Row};
use uuid::Uuid;
/// Agent update record
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct AgentUpdateRecord {
pub id: Uuid,
pub agent_id: Uuid,
pub update_id: Uuid,
pub old_version: String,
pub target_version: String,
pub download_url: Option<String>,
pub checksum_sha256: Option<String>,
pub status: Option<String>,
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
pub error_message: Option<String>,
}
/// Create a new agent update record
pub async fn create_agent_update(
pool: &PgPool,
agent_id: Uuid,
update_id: Uuid,
old_version: &str,
target_version: &str,
download_url: &str,
checksum_sha256: &str,
) -> Result<()> {
sqlx::query(
r#"
INSERT INTO agent_updates (agent_id, update_id, old_version, target_version, download_url, checksum_sha256, status)
VALUES ($1, $2, $3, $4, $5, $6, 'pending')
"#,
)
.bind(agent_id)
.bind(update_id)
.bind(old_version)
.bind(target_version)
.bind(download_url)
.bind(checksum_sha256)
.execute(pool)
.await?;
Ok(())
}
/// Mark an agent update as completed
pub async fn complete_agent_update(
pool: &PgPool,
update_id: Uuid,
new_version: Option<&str>,
) -> Result<()> {
sqlx::query(
r#"
UPDATE agent_updates
SET status = 'completed', completed_at = NOW()
WHERE update_id = $1
"#,
)
.bind(update_id)
.execute(pool)
.await?;
// If new_version provided, update the agent's version
if let Some(version) = new_version {
sqlx::query(
r#"
UPDATE agents
SET agent_version = $2, updated_at = NOW()
WHERE id = (SELECT agent_id FROM agent_updates WHERE update_id = $1)
"#,
)
.bind(update_id)
.bind(version)
.execute(pool)
.await?;
}
Ok(())
}
/// Mark an agent update as failed
pub async fn fail_agent_update(
pool: &PgPool,
update_id: Uuid,
error_message: Option<&str>,
) -> Result<()> {
sqlx::query(
r#"
UPDATE agent_updates
SET status = 'failed', completed_at = NOW(), error_message = $2
WHERE update_id = $1
"#,
)
.bind(update_id)
.bind(error_message)
.execute(pool)
.await?;
Ok(())
}
/// Update the status of an agent update (for progress tracking)
pub async fn update_agent_update_status(
pool: &PgPool,
update_id: Uuid,
status: &str,
) -> Result<()> {
sqlx::query(
r#"
UPDATE agent_updates
SET status = $2
WHERE update_id = $1
"#,
)
.bind(update_id)
.bind(status)
.execute(pool)
.await?;
Ok(())
}
/// Get pending update for an agent (if any)
pub async fn get_pending_update(
pool: &PgPool,
agent_id: Uuid,
) -> Result<Option<AgentUpdateRecord>> {
let record = sqlx::query_as::<_, AgentUpdateRecord>(
r#"
SELECT id, agent_id, update_id, old_version, target_version,
download_url, checksum_sha256, status, started_at, completed_at, error_message
FROM agent_updates
WHERE agent_id = $1 AND status IN ('pending', 'downloading', 'installing')
ORDER BY started_at DESC
LIMIT 1
"#,
)
.bind(agent_id)
.fetch_optional(pool)
.await?;
Ok(record)
}
/// Get stale updates (started but not completed within timeout)
pub async fn get_stale_updates(
pool: &PgPool,
timeout_secs: i64,
) -> Result<Vec<AgentUpdateRecord>> {
let records = sqlx::query_as::<_, AgentUpdateRecord>(
r#"
SELECT id, agent_id, update_id, old_version, target_version,
download_url, checksum_sha256, status, started_at, completed_at, error_message
FROM agent_updates
WHERE status IN ('pending', 'downloading', 'installing')
AND started_at < NOW() - INTERVAL '1 second' * $1
"#,
)
.bind(timeout_secs as f64)
.fetch_all(pool)
.await?;
Ok(records)
}
/// Complete a pending update by matching agent reconnection
/// Called when an agent reconnects with a previous_version different from agent_version
pub async fn complete_update_by_agent(
pool: &PgPool,
agent_id: Uuid,
pending_update_id: Option<Uuid>,
old_version: &str,
new_version: &str,
) -> Result<bool> {
// First try by update_id if provided
if let Some(update_id) = pending_update_id {
let result = sqlx::query(
r#"
UPDATE agent_updates
SET status = 'completed', completed_at = NOW()
WHERE update_id = $1 AND agent_id = $2 AND status IN ('pending', 'downloading', 'installing')
"#,
)
.bind(update_id)
.bind(agent_id)
.execute(pool)
.await?;
if result.rows_affected() > 0 {
return Ok(true);
}
}
// Fall back to finding by old_version match
let result = sqlx::query(
r#"
UPDATE agent_updates
SET status = 'completed', completed_at = NOW()
WHERE agent_id = $1
AND old_version = $2
AND target_version = $3
AND status IN ('pending', 'downloading', 'installing')
"#,
)
.bind(agent_id)
.bind(old_version)
.bind(new_version)
.execute(pool)
.await?;
Ok(result.rows_affected() > 0)
}