sync: Multi-project updates - SolverBot, GuruRMM, Dataforth
SolverBot: - Inject active project path into agent system prompts so agents know which directory to scope file operations to GuruRMM: - Bump agent version to 0.6.0 - Add serde aliases for PowerShell/ClaudeTask command types - Add typed CommandType enum on server for proper serialization - Support claude_task command type in send_command API Dataforth: - Fix SCP space-escaping in Sync-FromNAS.ps1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
606
projects/msp-tools/guru-rmm/GURURMM_DOCUMENTATION.md
Normal file
606
projects/msp-tools/guru-rmm/GURURMM_DOCUMENTATION.md
Normal file
@@ -0,0 +1,606 @@
|
||||
# GuruRMM - Complete Reference Documentation
|
||||
|
||||
**Project:** GuruRMM - Remote Monitoring and Management Platform
|
||||
**Version:** Server 0.2.0 / Agent 0.3.5 (deployed as 0.5.1)
|
||||
**Last Updated:** 2026-02-17
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Project Overview](#project-overview)
|
||||
2. [Architecture](#architecture)
|
||||
3. [API Endpoints](#api-endpoints)
|
||||
4. [WebSocket Protocol](#websocket-protocol)
|
||||
5. [Command Execution](#command-execution)
|
||||
6. [Claude Code Integration](#claude-code-integration)
|
||||
7. [Agent Configuration](#agent-configuration)
|
||||
8. [Deployed Agents](#deployed-agents)
|
||||
9. [Database](#database)
|
||||
10. [Authentication](#authentication)
|
||||
11. [Auto-Update System](#auto-update-system)
|
||||
12. [Known Issues](#known-issues)
|
||||
13. [Development](#development)
|
||||
14. [File Structure](#file-structure)
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
GuruRMM is a Remote Monitoring and Management (RMM) platform built entirely in Rust. It provides real-time agent monitoring, remote command execution, system metrics collection, and service watchdog capabilities for managed IT environments.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
| Component | Technology | Version |
|
||||
|------------|-----------------------------------------|---------|
|
||||
| Server | Rust (Axum 0.7, SQLx 0.8, PostgreSQL) | 0.2.0 |
|
||||
| Agent | Rust (cross-platform, native service) | 0.3.5 (deployed as 0.5.1) |
|
||||
| Dashboard | React + TypeScript + Vite | -- |
|
||||
| Real-time | WebSocket (tokio-tungstenite) | -- |
|
||||
| Database | PostgreSQL | -- |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Server
|
||||
|
||||
- **Internal Address:** 172.16.3.30:3001
|
||||
- **Production URL:** https://rmm-api.azcomputerguru.com
|
||||
- **WebSocket Endpoint:** wss://rmm-api.azcomputerguru.com/ws
|
||||
- **Database:** PostgreSQL (same server)
|
||||
- **Service:** systemd unit `gururmm-server`
|
||||
- **Source:** `D:\ClaudeTools\projects\msp-tools\guru-rmm\server\`
|
||||
|
||||
### Agent
|
||||
|
||||
- **Windows Service Name:** GuruRMM (uses native-service feature)
|
||||
- **Legacy Mode:** NSSM wrapper for Windows 7 / Server 2008 R2
|
||||
- **Config Path:** `C:\ProgramData\GuruRMM\agent.toml`
|
||||
- **Binary Path:** `C:\Program Files\GuruRMM\gururmm-agent.exe`
|
||||
- **Source:** `D:\ClaudeTools\projects\msp-tools\guru-rmm\agent\`
|
||||
|
||||
### Communication Model
|
||||
|
||||
```
|
||||
+-------------------+ WebSocket (persistent, bidirectional) +-------------------+
|
||||
| GuruRMM Agent | <-----------------------------------------------> | GuruRMM Server |
|
||||
| (Windows/Linux) | | (Axum + Tokio) |
|
||||
+-------------------+ +-------------------+
|
||||
|
|
||||
| REST API (JWT)
|
||||
v
|
||||
+-------------------+
|
||||
| Dashboard |
|
||||
| (React + TS) |
|
||||
+-------------------+
|
||||
```
|
||||
|
||||
- **Primary:** WebSocket -- persistent bidirectional connection between agent and server
|
||||
- **Legacy Fallback:** REST heartbeat polling -- [WARNING] NOT FULLY IMPLEMENTED
|
||||
- **Auth:** API key sent in initial WebSocket authentication message
|
||||
- **Site-Based Auth:** WORD-WORD-NUMBER format site codes combined with device_id
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Authentication
|
||||
|
||||
| Method | Path | Description | Auth Required |
|
||||
|--------|--------------------|----------------------------|---------------|
|
||||
| POST | /api/auth/login | User login (email/password -> JWT) | No |
|
||||
| POST | /api/auth/register | User registration | No (disabled) |
|
||||
| GET | /api/auth/me | Get current user info | Yes |
|
||||
|
||||
### Clients
|
||||
|
||||
| Method | Path | Description | Auth Required |
|
||||
|--------|-------------------------|-------------------------|---------------|
|
||||
| GET | /api/clients | List all clients | Yes |
|
||||
| POST | /api/clients | Create client | Yes |
|
||||
| GET | /api/clients/:id | Get client by ID | Yes |
|
||||
| PUT | /api/clients/:id | Update client | Yes |
|
||||
| DELETE | /api/clients/:id | Delete client | Yes |
|
||||
| GET | /api/clients/:id/sites | List client's sites | Yes |
|
||||
|
||||
### Sites
|
||||
|
||||
| Method | Path | Description | Auth Required |
|
||||
|--------|--------------------------------|--------------------------|---------------|
|
||||
| GET | /api/sites | List all sites | Yes |
|
||||
| POST | /api/sites | Create site | Yes |
|
||||
| GET | /api/sites/:id | Get site by ID | Yes |
|
||||
| PUT | /api/sites/:id | Update site | Yes |
|
||||
| DELETE | /api/sites/:id | Delete site | Yes |
|
||||
| POST | /api/sites/:id/regenerate-key | Regenerate site API key | Yes |
|
||||
|
||||
### Agents
|
||||
|
||||
| Method | Path | Description | Auth Required |
|
||||
|--------|--------------------------|--------------------------------------|---------------|
|
||||
| GET | /api/agents | List all agents | Yes |
|
||||
| POST | /api/agents | Register agent (authenticated) | Yes |
|
||||
| GET | /api/agents/stats | Agent statistics | Yes |
|
||||
| GET | /api/agents/unassigned | List unassigned agents | Yes |
|
||||
| GET | /api/agents/:id | Get agent details | Yes |
|
||||
| DELETE | /api/agents/:id | Delete agent | Yes |
|
||||
| POST | /api/agents/:id/move | Move agent to different site | Yes |
|
||||
| GET | /api/agents/:id/state | Get agent state (network, metrics) | Yes |
|
||||
|
||||
### Commands
|
||||
|
||||
| Method | Path | Description | Auth Required |
|
||||
|--------|----------------------------|----------------------------|---------------|
|
||||
| POST | /api/agents/:id/command | Send command to agent | Yes |
|
||||
| GET | /api/commands | List recent commands | Yes |
|
||||
| GET | /api/commands/:id | Get command status/result | Yes |
|
||||
|
||||
### Metrics
|
||||
|
||||
| Method | Path | Description | Auth Required |
|
||||
|--------|----------------------------|---------------------------|---------------|
|
||||
| GET | /api/agents/:id/metrics | Get agent metrics history | Yes |
|
||||
| GET | /api/metrics/summary | Metrics summary | Yes |
|
||||
|
||||
### Legacy Agent Endpoints
|
||||
|
||||
These endpoints do **not** require JWT authentication. They are used by agents in legacy polling mode.
|
||||
|
||||
| Method | Path | Description | Auth Required |
|
||||
|--------|-------------------------------|------------------------------|---------------|
|
||||
| POST | /api/agent/register-legacy | Register with site code | No |
|
||||
| POST | /api/agent/heartbeat | Agent heartbeat | No |
|
||||
| POST | /api/agent/command-result | Submit command result | No |
|
||||
|
||||
[WARNING] Legacy heartbeat returns empty `pending_commands` -- not implemented (agents.rs line 334).
|
||||
[WARNING] Legacy command-result endpoint does not store results (agents.rs lines 354-360).
|
||||
|
||||
### WebSocket
|
||||
|
||||
| Method | Path | Description | Auth Required |
|
||||
|--------|------|------------------------|---------------------|
|
||||
| GET | /ws | WebSocket upgrade | API key in auth msg |
|
||||
|
||||
---
|
||||
|
||||
## WebSocket Protocol
|
||||
|
||||
### Connection Flow
|
||||
|
||||
1. Client initiates WebSocket upgrade to `wss://rmm-api.azcomputerguru.com/ws`
|
||||
2. Agent sends authentication message with API key and device info
|
||||
3. Server validates API key (SHA256 hash match or site code lookup)
|
||||
4. On success, server registers the WebSocket connection for the agent
|
||||
5. Bidirectional message exchange begins
|
||||
|
||||
### Message Types
|
||||
|
||||
**Agent -> Server:**
|
||||
|
||||
- `Auth` -- Initial authentication payload (api_key, hostname, os_info, version)
|
||||
- `Heartbeat` -- Periodic keepalive
|
||||
- `MetricsReport` -- System metrics (CPU, memory, disk, network)
|
||||
- `NetworkState` -- Network configuration snapshot (hash-based change detection)
|
||||
- `CommandResult` -- Result of executed command (exit_code, stdout, stderr, duration)
|
||||
- `WatchdogEvent` -- Service monitoring event
|
||||
|
||||
**Server -> Agent:**
|
||||
|
||||
- `AuthResponse` -- Success/failure of authentication
|
||||
- `Command` -- Command to execute (CommandPayload)
|
||||
- `Update` -- Auto-update instruction (download_url, checksum)
|
||||
- `Ping` -- Keepalive ping
|
||||
|
||||
---
|
||||
|
||||
## Command Execution
|
||||
|
||||
### Command Types
|
||||
|
||||
| Type | Description | Shell Used |
|
||||
|--------------|----------------------------------------------|---------------------------------------------|
|
||||
| shell | System shell command | cmd.exe (Windows), /bin/sh (Unix) |
|
||||
| powershell | PowerShell command | powershell -NoProfile -NonInteractive -Command |
|
||||
| python | Python inline code | python -c |
|
||||
| script | Custom interpreter | Configurable |
|
||||
| claude_task | Claude Code task execution (special handler) | Claude Code CLI |
|
||||
|
||||
### Command Flow
|
||||
|
||||
```
|
||||
1. Dashboard sends POST /api/agents/:id/command
|
||||
Body: { command_type, command, timeout_seconds, elevated }
|
||||
|
||||
2. Server creates command record in database (status = pending)
|
||||
|
||||
3. If agent is connected via WebSocket:
|
||||
-> Server sends command via WebSocket
|
||||
-> Status updated to "running"
|
||||
|
||||
4. If agent is offline:
|
||||
-> Command stays as "pending" (queued)
|
||||
|
||||
5. Agent receives command and executes it
|
||||
|
||||
6. Agent sends CommandResult back via WebSocket
|
||||
-> { id, exit_code, stdout, stderr, duration_ms }
|
||||
|
||||
7. Server updates database with result
|
||||
```
|
||||
|
||||
### Command States
|
||||
|
||||
| State | Description |
|
||||
|-----------|------------------------------------------------|
|
||||
| pending | Created, agent offline or not yet sent |
|
||||
| running | Sent to agent via WebSocket, awaiting result |
|
||||
| completed | Agent reported exit_code = 0 |
|
||||
| failed | Agent reported exit_code != 0 |
|
||||
|
||||
### [BUG] Server-Agent Command Type Mismatch
|
||||
|
||||
This is a **critical** known bug that prevents all remote command execution.
|
||||
|
||||
**Root Cause:**
|
||||
|
||||
The server's `CommandPayload` serializes `command_type` as a plain JSON string:
|
||||
|
||||
```json
|
||||
{"command_type": "powershell", "command": "Get-Process", ...}
|
||||
```
|
||||
|
||||
The agent's `CommandPayload` expects `command_type` as a Rust enum (`CommandType::PowerShell`), which serde deserializes from an object or tagged format, not a bare string.
|
||||
|
||||
**Result:** Serde deserialization fails silently on the agent side. Commands are never executed. All commands remain in "running" state permanently because no `CommandResult` is ever sent back.
|
||||
|
||||
**Fix Required:** Either:
|
||||
- Change the server to serialize `command_type` in the enum format the agent expects, OR
|
||||
- Change the agent to accept plain string values for `command_type`
|
||||
|
||||
---
|
||||
|
||||
## Claude Code Integration
|
||||
|
||||
### Architecture
|
||||
|
||||
The agent includes a built-in Claude Code executor for running AI-assisted tasks.
|
||||
|
||||
- **Singleton:** Global `ClaudeExecutor` via `once_cell::Lazy`
|
||||
- **Working Directory:** Restricted to `C:\Shares\test\` only
|
||||
- **Rate Limit:** 10 tasks per hour (sliding window)
|
||||
- **Max Concurrent:** 2 simultaneous tasks
|
||||
- **Default Timeout:** 300 seconds (max 600)
|
||||
- **Input Sanitization:** Blocks `& | ; $ ( ) < > \` \n \r`
|
||||
|
||||
### Claude Task Command Format
|
||||
|
||||
The server sends:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "command",
|
||||
"payload": {
|
||||
"id": "uuid",
|
||||
"command_type": "claude_task",
|
||||
"command": "task description",
|
||||
"timeout_seconds": 300,
|
||||
"elevated": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[WARNING] This also suffers from the command type mismatch bug. The agent expects `command_type` to be an object for ClaudeTask:
|
||||
|
||||
```json
|
||||
{
|
||||
"claude_task": {
|
||||
"task": "...",
|
||||
"working_directory": "...",
|
||||
"context_files": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|------------------------------------------|
|
||||
| 0 | Task completed successfully |
|
||||
| 1 | Task failed |
|
||||
| 124 | Task timed out |
|
||||
| -1 | Executor error (rate limit, validation) |
|
||||
|
||||
---
|
||||
|
||||
## Agent Configuration
|
||||
|
||||
### agent.toml Format
|
||||
|
||||
```toml
|
||||
[server]
|
||||
url = "wss://rmm-api.azcomputerguru.com/ws"
|
||||
api_key = "SITE-CODE-1234" # or grmm_xxxxx API key
|
||||
|
||||
[metrics]
|
||||
interval_seconds = 60 # Range: 10-3600, default: 60
|
||||
collect_cpu = true
|
||||
collect_memory = true
|
||||
collect_disk = true
|
||||
collect_network = true
|
||||
|
||||
[watchdog]
|
||||
enabled = true
|
||||
check_interval_seconds = 30
|
||||
|
||||
[[watchdog.services]]
|
||||
name = "ServiceName"
|
||||
action = "restart"
|
||||
max_restarts = 3
|
||||
restart_cooldown_seconds = 60
|
||||
```
|
||||
|
||||
### Hardcoded Intervals
|
||||
|
||||
These values are currently not configurable via `agent.toml`:
|
||||
|
||||
| Interval | Value | Notes |
|
||||
|----------------------------|-------------|--------------------------------|
|
||||
| Heartbeat | 30 seconds | |
|
||||
| Network state check | 30 seconds | Uses hash-based change detection |
|
||||
| Connection idle timeout | 90 seconds | |
|
||||
| Auth timeout | 10 seconds | |
|
||||
| Reconnect delay | 10 seconds | |
|
||||
| Command execution timeout | 300 seconds | Configurable per command |
|
||||
|
||||
---
|
||||
|
||||
## Deployed Agents
|
||||
|
||||
| Hostname | Agent ID (prefix) | Version | OS | Status |
|
||||
|-------------|--------------------|---------|-----------------------------|---------|
|
||||
| ACG-M-L5090 | 97f63c3b-... | 0.5.1 | Windows 11 (26200) | online |
|
||||
| AD2 | d28a1c90-... | 0.5.1 | Windows Server 2016 (14393) | online |
|
||||
| gururmm | 8cd0440f-... | 0.5.1 | Ubuntu 22.04 | offline |
|
||||
| SL-SERVER | 2585f6d5-... | 0.5.1 | unknown | offline |
|
||||
| SL-SERVER | dff818e6-... | 0.5.1 | unknown | online |
|
||||
|
||||
---
|
||||
|
||||
## Database
|
||||
|
||||
### Connection Details
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|------------------------------------|
|
||||
| Host | 172.16.3.30 |
|
||||
| Port | 5432 |
|
||||
| Database | gururmm |
|
||||
| User | gururmm |
|
||||
| Password | 43617ebf7eb242e814ca9988cc4df5ad |
|
||||
|
||||
### Key Tables
|
||||
|
||||
| Table | Description |
|
||||
|---------------------|------------------------------------------------|
|
||||
| users | User accounts (JWT auth, Argon2id hashing) |
|
||||
| clients | Client organizations |
|
||||
| sites | Physical locations with API keys |
|
||||
| agents | RMM agent instances |
|
||||
| agent_state | Latest agent state (network, metrics snapshot) |
|
||||
| agent_updates | Agent update tracking |
|
||||
| alerts | System alerts |
|
||||
| commands | Remote command execution log |
|
||||
| metrics | Performance metrics time series |
|
||||
| policies | Configuration policies |
|
||||
| registration_tokens | Agent registration tokens |
|
||||
| watchdog_events | Service monitoring events |
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
### API Authentication (JWT)
|
||||
|
||||
1. Send `POST /api/auth/login` with `{ email, password }`
|
||||
2. Server validates credentials (Argon2id password hash)
|
||||
3. Returns JWT token (24-hour expiry)
|
||||
4. Include token in subsequent requests: `Authorization: Bearer <token>`
|
||||
|
||||
**Admin Credentials:**
|
||||
|
||||
| Field | Value |
|
||||
|----------|------------------------------------|
|
||||
| Email | claude-api@azcomputerguru.com |
|
||||
| Password | ClaudeAPI2026!@# |
|
||||
|
||||
### Agent Authentication (API Key)
|
||||
|
||||
Two authentication modes:
|
||||
|
||||
1. **Direct API Key** -- Agent sends `grmm_xxxxx` format key, server matches against `api_key_hash` (SHA256) in agents table
|
||||
2. **Site-Based** -- Agent sends site code (WORD-WORD-NUMBER format, e.g., `DARK-GROVE-7839`) combined with `device_id`, server looks up site and registers/matches agent
|
||||
|
||||
### SSO (Optional)
|
||||
|
||||
- **Provider:** Microsoft Entra ID
|
||||
- **Client ID:** 18a15f5d-7ab8-46f4-8566-d7b5436b84b6
|
||||
|
||||
---
|
||||
|
||||
## Auto-Update System
|
||||
|
||||
### Update Flow
|
||||
|
||||
```
|
||||
1. Agent connects via WebSocket and sends its version in the auth payload
|
||||
|
||||
2. Server checks if a newer version is available for the agent's OS/architecture
|
||||
|
||||
3. If update needed:
|
||||
-> Server sends Update message with download_url and SHA256 checksum
|
||||
|
||||
4. Agent downloads the new binary from the download URL
|
||||
|
||||
5. Agent verifies the SHA256 checksum
|
||||
|
||||
6. Agent replaces its own binary and restarts
|
||||
|
||||
7. On reconnection, agent reports previous_version in auth payload
|
||||
|
||||
8. Server marks the update as completed
|
||||
```
|
||||
|
||||
### Download Location
|
||||
|
||||
- **Server Path:** `/var/www/gururmm/downloads/`
|
||||
- **Public URL:** `https://rmm-api.azcomputerguru.com/downloads/`
|
||||
|
||||
---
|
||||
|
||||
## Known Issues
|
||||
|
||||
### CRITICAL
|
||||
|
||||
| ID | Type | Description |
|
||||
|----|------------|--------------------------------------------------------------------------------------------|
|
||||
| 1 | [BUG] | Command type mismatch between server (String) and agent (Enum) -- commands never execute |
|
||||
| 2 | [TODO] | Legacy heartbeat returns empty pending_commands (agents.rs line 334) |
|
||||
| 3 | [TODO] | Legacy command-result endpoint does not store results (agents.rs lines 354-360) |
|
||||
| 4 | [SECURITY] | CORS configured with AllowOrigin::Any -- should be restricted to known origins |
|
||||
|
||||
### MAJOR
|
||||
|
||||
| ID | Description |
|
||||
|----|--------------------------------------------------------------------------------|
|
||||
| 1 | No command timeout enforcement on server side |
|
||||
| 2 | No retry logic for failed WebSocket sends |
|
||||
| 3 | Database inconsistency: agent shows "online" but command sends fail silently |
|
||||
| 4 | Missing database indexes on frequently queried columns |
|
||||
| 5 | No rate limiting on command submissions |
|
||||
|
||||
### MINOR
|
||||
|
||||
| ID | Description |
|
||||
|----|--------------------------------------------------------------------------|
|
||||
| 1 | Hardcoded intervals (heartbeat, network check) not configurable |
|
||||
| 2 | Watchdog events logged but not stored in database |
|
||||
| 3 | No log rotation configured |
|
||||
| 4 | Unicode characters in agent output (should use ASCII per coding guidelines) |
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Server
|
||||
cd server && cargo build --release
|
||||
|
||||
# Agent (Windows, native service mode)
|
||||
cd agent && cargo build --release
|
||||
|
||||
# Agent (Legacy mode for Windows 7 / Server 2008 R2)
|
||||
cd agent && cargo build --release --features legacy --no-default-features
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
cargo test # Run unit tests
|
||||
cargo clippy # Run linter
|
||||
cargo fmt --check # Check formatting
|
||||
```
|
||||
|
||||
### Deploying the Server
|
||||
|
||||
```bash
|
||||
# On gururmm server (172.16.3.30)
|
||||
systemctl stop gururmm-server
|
||||
cp target/release/gururmm-server /opt/gururmm/
|
||||
systemctl start gururmm-server
|
||||
journalctl -u gururmm-server -f
|
||||
```
|
||||
|
||||
### Deploying the Agent
|
||||
|
||||
```cmd
|
||||
REM On target Windows machine
|
||||
sc stop GuruRMM
|
||||
copy gururmm-agent.exe "C:\Program Files\GuruRMM\gururmm-agent.exe"
|
||||
sc start GuruRMM
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
D:\ClaudeTools\projects\msp-tools\guru-rmm\
|
||||
|
|
||||
+-- agent/
|
||||
| +-- src/
|
||||
| | +-- main.rs # Entry point, CLI parsing, service install
|
||||
| | +-- config.rs # TOML config loading and validation
|
||||
| | +-- claude.rs # Claude Code executor (rate-limited singleton)
|
||||
| | +-- service.rs # Windows service handler (native-service feature)
|
||||
| | +-- device_id.rs # Hardware-based device ID generation
|
||||
| | +-- transport/
|
||||
| | | +-- mod.rs # Message types (AgentMessage, ServerMessage, CommandType enum)
|
||||
| | | +-- websocket.rs # WebSocket client, reconnection, command execution
|
||||
| | +-- metrics/
|
||||
| | | +-- mod.rs # System metrics collection, network state hashing
|
||||
| | +-- updater/
|
||||
| | +-- mod.rs # Self-update logic (download, verify, replace)
|
||||
| +-- deploy/ # Deployment configs per site
|
||||
| +-- Cargo.toml # v0.3.5, features: native-service, legacy
|
||||
|
|
||||
+-- server/
|
||||
| +-- src/
|
||||
| | +-- main.rs # Axum server setup, router, middleware
|
||||
| | +-- api/
|
||||
| | | +-- mod.rs # Route definitions and grouping
|
||||
| | | +-- agents.rs # Agent management + legacy polling endpoints
|
||||
| | | +-- commands.rs # Command dispatch and status tracking
|
||||
| | | +-- auth.rs # JWT login, registration, user info
|
||||
| | | +-- clients.rs # Client CRUD operations
|
||||
| | | +-- sites.rs # Site management and API key regeneration
|
||||
| | | +-- metrics.rs # Metrics query endpoints
|
||||
| | +-- ws/
|
||||
| | | +-- mod.rs # WebSocket handler, ServerMessage types, CommandPayload (String type)
|
||||
| | +-- db/
|
||||
| | | +-- agents.rs # Agent database operations
|
||||
| | | +-- commands.rs # Command database operations
|
||||
| | +-- auth/
|
||||
| | +-- mod.rs # JWT middleware and token validation
|
||||
| +-- Cargo.toml # v0.2.0
|
||||
|
|
||||
+-- dashboard/ # React frontend (if present)
|
||||
|
|
||||
+-- docs/
|
||||
+-- FEATURE_ROADMAP.md # Complete feature plan (654 lines)
|
||||
+-- REMEDIATION_PLAN.md # Security and code review (1277 lines)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Item | Value |
|
||||
|--------------------|---------------------------------------------|
|
||||
| Server URL | https://rmm-api.azcomputerguru.com |
|
||||
| WebSocket URL | wss://rmm-api.azcomputerguru.com/ws |
|
||||
| Internal Address | 172.16.3.30:3001 |
|
||||
| Database | PostgreSQL @ 172.16.3.30:5432/gururmm |
|
||||
| Service Name | gururmm-server (systemd) |
|
||||
| Agent Service | GuruRMM (Windows SCM) |
|
||||
| Agent Config | C:\ProgramData\GuruRMM\agent.toml |
|
||||
| Agent Binary | C:\Program Files\GuruRMM\gururmm-agent.exe |
|
||||
| Downloads | https://rmm-api.azcomputerguru.com/downloads/ |
|
||||
| Admin Email | claude-api@azcomputerguru.com |
|
||||
| SSO Client ID | 18a15f5d-7ab8-46f4-8566-d7b5436b84b6 |
|
||||
|
||||
---
|
||||
|
||||
*Document generated 2026-02-17. Source of truth for GuruRMM project reference.*
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gururmm-agent"
|
||||
version = "0.3.5"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
description = "GuruRMM Agent - Cross-platform RMM agent"
|
||||
authors = ["GuruRMM"]
|
||||
|
||||
@@ -199,6 +199,9 @@ pub enum CommandType {
|
||||
Shell,
|
||||
|
||||
/// PowerShell command (Windows)
|
||||
/// Alias "powershell" for backwards compatibility with servers that send
|
||||
/// the command type as a plain string instead of snake_case enum format.
|
||||
#[serde(alias = "powershell")]
|
||||
PowerShell,
|
||||
|
||||
/// Python script
|
||||
@@ -208,6 +211,7 @@ pub enum CommandType {
|
||||
Script { interpreter: String },
|
||||
|
||||
/// Claude Code task execution
|
||||
#[serde(alias = "claude_task")]
|
||||
ClaudeTask {
|
||||
/// Task description for Claude Code
|
||||
task: String,
|
||||
|
||||
67
projects/msp-tools/guru-rmm/deploy_agent_chunks.py
Normal file
67
projects/msp-tools/guru-rmm/deploy_agent_chunks.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import requests, json, time, sys
|
||||
|
||||
with open("/tmp/agent_b64.txt") as f:
|
||||
b64 = f.read().strip()
|
||||
print("Base64 length:", len(b64))
|
||||
|
||||
token_r = requests.post("http://localhost:3001/api/auth/login", json={"email": "claude-api@azcomputerguru.com", "password": "ClaudeAPI2026!@#"})
|
||||
token = token_r.json()["token"]
|
||||
headers = {"Authorization": "Bearer " + token, "Content-Type": "application/json"}
|
||||
agent_id = "d28a1c90-47d7-448f-a287-197bc8892234"
|
||||
|
||||
chunk_size = 200000
|
||||
chunks = [b64[i:i+chunk_size] for i in range(0, len(b64), chunk_size)]
|
||||
print("Chunks:", len(chunks))
|
||||
|
||||
def send_cmd(cmd, timeout=60, wait=15):
|
||||
r = requests.post(
|
||||
"http://localhost:3001/api/agents/" + agent_id + "/command",
|
||||
headers=headers,
|
||||
json={"command_type": "powershell", "command": cmd, "timeout_seconds": timeout}
|
||||
)
|
||||
cmd_id = r.json()["command_id"]
|
||||
time.sleep(wait)
|
||||
r2 = requests.get("http://localhost:3001/api/commands/" + cmd_id, headers=headers)
|
||||
return r2.json()
|
||||
|
||||
# Chunk 1: create file
|
||||
cmd = "$b = [Convert]::FromBase64String('" + chunks[0] + "'); New-Item -ItemType Directory -Path C:/Temp -Force | Out-Null; [System.IO.File]::WriteAllBytes('C:/Temp/agent_chunk.bin', $b); Write-Output ('Chunk 1: ' + $b.Length.ToString() + ' bytes')"
|
||||
d = send_cmd(cmd)
|
||||
print("Chunk 1:", d["status"], d.get("stdout",""))
|
||||
if d["status"] != "completed":
|
||||
print("ERROR:", (d.get("stderr","") or "")[:300])
|
||||
sys.exit(1)
|
||||
|
||||
# Append remaining chunks
|
||||
for i, chunk in enumerate(chunks[1:], 2):
|
||||
cmd = "$b = [Convert]::FromBase64String('" + chunk + "'); $f = [System.IO.File]::Open('C:/Temp/agent_chunk.bin', [System.IO.FileMode]::Append); $f.Write($b, 0, $b.Length); $f.Close(); Write-Output ('Chunk " + str(i) + ": ' + $b.Length.ToString() + ' bytes')"
|
||||
d = send_cmd(cmd, wait=10)
|
||||
print("Chunk", i, ":", d["status"], d.get("stdout",""))
|
||||
if d["status"] != "completed":
|
||||
print("ERROR:", (d.get("stderr","") or "")[:300])
|
||||
sys.exit(1)
|
||||
|
||||
# Verify final size
|
||||
d = send_cmd("(Get-Item C:/Temp/agent_chunk.bin).Length", wait=5)
|
||||
print("Final size:", d.get("stdout","").strip(), "(expected 3577856)")
|
||||
|
||||
# Now create update script that stops service, replaces binary, starts service
|
||||
update_cmd = """
|
||||
$src = 'C:/Temp/agent_chunk.bin'
|
||||
$dst = 'C:/Program Files/GuruRMM/gururmm-agent.exe'
|
||||
$bak = 'C:/Program Files/GuruRMM/gururmm-agent.exe.bak'
|
||||
Copy-Item $dst $bak -Force
|
||||
Stop-Service GuruRMMAgent -Force
|
||||
Start-Sleep -Seconds 2
|
||||
Copy-Item $src $dst -Force
|
||||
Start-Service GuruRMMAgent
|
||||
Write-Output 'Agent updated and restarted'
|
||||
"""
|
||||
print("\nNow creating scheduled task to perform the update...")
|
||||
sched_cmd = 'schtasks /create /tn AgentUpdate /tr "powershell.exe -ExecutionPolicy Bypass -Command \\"' + update_cmd.replace('\n', '; ').strip() + '\\"" /sc ONCE /st 00:00 /sd 01/01/2030 /ru SYSTEM /f'
|
||||
d = send_cmd(sched_cmd, wait=5)
|
||||
print("Sched task create:", d["status"], d.get("stdout",""), d.get("stderr","")[:200] if d.get("stderr") else "")
|
||||
|
||||
d = send_cmd("schtasks /run /tn AgentUpdate", wait=5)
|
||||
print("Sched task run:", d["status"], d.get("stdout",""))
|
||||
print("\nAgent will restart momentarily. Wait 30s then check connection.")
|
||||
88
projects/msp-tools/guru-rmm/deploy_via_textchunks.py
Normal file
88
projects/msp-tools/guru-rmm/deploy_via_textchunks.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Deploy agent binary to AD2 by writing base64 text chunks to a file, then decoding."""
|
||||
import requests, json, time, sys, base64
|
||||
|
||||
# Read and encode the binary
|
||||
with open("agent/target/release/gururmm-agent.exe", "rb") as f:
|
||||
binary = f.read()
|
||||
b64 = base64.b64encode(binary).decode('ascii')
|
||||
print(f"Binary: {len(binary)} bytes, Base64: {len(b64)} chars")
|
||||
|
||||
# Auth
|
||||
token_r = requests.post('http://172.16.3.30:3001/api/auth/login', json={
|
||||
'email': 'claude-api@azcomputerguru.com',
|
||||
'password': 'ClaudeAPI2026!@#'
|
||||
})
|
||||
token = token_r.json()['token']
|
||||
headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}
|
||||
agent_id = 'd28a1c90-47d7-448f-a287-197bc8892234'
|
||||
|
||||
def send_cmd(cmd, timeout=60, wait=10):
|
||||
r = requests.post(
|
||||
'http://172.16.3.30:3001/api/agents/' + agent_id + '/command',
|
||||
headers=headers,
|
||||
json={'command_type': 'powershell', 'command': cmd, 'timeout_seconds': timeout}
|
||||
)
|
||||
data = r.json()
|
||||
cmd_id = data['command_id']
|
||||
time.sleep(wait)
|
||||
# Poll until complete
|
||||
for attempt in range(10):
|
||||
r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers)
|
||||
d = r2.json()
|
||||
if d['status'] != 'running':
|
||||
return d
|
||||
time.sleep(5)
|
||||
return d
|
||||
|
||||
# Step 1: Delete old file and create fresh one
|
||||
print("Step 1: Preparing temp file...")
|
||||
d = send_cmd("Remove-Item C:/Temp/agent.b64 -Force -ErrorAction SilentlyContinue; "
|
||||
"New-Item -ItemType Directory -Path C:/Temp -Force | Out-Null; "
|
||||
"'' | Set-Content C:/Temp/agent.b64 -NoNewline; "
|
||||
"Write-Output 'Ready'", wait=8)
|
||||
print(f" {d['status']}: {d.get('stdout','').strip()}")
|
||||
if d['status'] != 'completed':
|
||||
print(f" ERROR: {(d.get('stderr','') or '')[:300]}")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 2: Write base64 text in chunks
|
||||
# Windows command line limit is ~32KB, keep chunks under 20KB to be safe
|
||||
chunk_size = 20000
|
||||
chunks = [b64[i:i+chunk_size] for i in range(0, len(b64), chunk_size)]
|
||||
print(f"Step 2: Writing {len(chunks)} chunks of ~{chunk_size} chars each...")
|
||||
|
||||
for i, chunk in enumerate(chunks):
|
||||
# Use Add-Content to append text (no base64 decode here, just text)
|
||||
# Escape single quotes in base64 (shouldn't have any, but just in case)
|
||||
safe_chunk = chunk.replace("'", "''")
|
||||
cmd = f"Add-Content -Path C:/Temp/agent.b64 -Value '{safe_chunk}' -NoNewline; Write-Output 'chunk{i+1}ok'"
|
||||
d = send_cmd(cmd, wait=5)
|
||||
status = d['status']
|
||||
stdout = d.get('stdout', '').strip()
|
||||
if status != 'completed' or f'chunk{i+1}ok' not in stdout:
|
||||
print(f" Chunk {i+1}/{len(chunks)} FAILED: {status} - {stdout}")
|
||||
print(f" stderr: {(d.get('stderr','') or '')[:300]}")
|
||||
sys.exit(1)
|
||||
if (i+1) % 10 == 0 or i == 0 or i == len(chunks)-1:
|
||||
print(f" Chunk {i+1}/{len(chunks)}: OK")
|
||||
|
||||
# Step 3: Verify base64 file size
|
||||
print("Step 3: Verifying base64 file...")
|
||||
d = send_cmd(f"$f = Get-Item C:/Temp/agent.b64; Write-Output $f.Length", wait=5)
|
||||
remote_size = d.get('stdout', '').strip()
|
||||
print(f" Remote b64 size: {remote_size} (expected: {len(b64)})")
|
||||
|
||||
# Step 4: Decode base64 file to binary
|
||||
print("Step 4: Decoding base64 to binary...")
|
||||
cmd = ("$b64 = Get-Content C:/Temp/agent.b64 -Raw; "
|
||||
"$bytes = [Convert]::FromBase64String($b64); "
|
||||
"[System.IO.File]::WriteAllBytes('C:/Temp/gururmm-agent-new.exe', $bytes); "
|
||||
"$f = Get-Item C:/Temp/gururmm-agent-new.exe; "
|
||||
"Write-Output ('Decoded: ' + $f.Length.ToString() + ' bytes')")
|
||||
d = send_cmd(cmd, timeout=120, wait=15)
|
||||
print(f" {d['status']}: {d.get('stdout','').strip()}")
|
||||
if d.get('stderr'):
|
||||
print(f" stderr: {(d.get('stderr','') or '')[:300]}")
|
||||
|
||||
print(f"\nExpected binary size: {len(binary)} bytes")
|
||||
print("Done!")
|
||||
35
projects/msp-tools/guru-rmm/download_agent.py
Normal file
35
projects/msp-tools/guru-rmm/download_agent.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import requests, json, time, sys
|
||||
|
||||
# Auth
|
||||
token_r = requests.post('http://172.16.3.30:3001/api/auth/login', json={
|
||||
'email': 'claude-api@azcomputerguru.com',
|
||||
'password': 'ClaudeAPI2026!@#'
|
||||
})
|
||||
token = token_r.json()['token']
|
||||
headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}
|
||||
agent_id = 'd28a1c90-47d7-448f-a287-197bc8892234'
|
||||
|
||||
# Send download command via PowerShell
|
||||
cmd = (
|
||||
"New-Item -ItemType Directory -Path C:/Temp -Force | Out-Null; "
|
||||
"Invoke-WebRequest -Uri 'http://172.16.3.30/gururmm-agent-new.exe' "
|
||||
"-OutFile 'C:/Temp/gururmm-agent-new.exe' -UseBasicParsing; "
|
||||
"$f = Get-Item 'C:/Temp/gururmm-agent-new.exe'; "
|
||||
"Write-Output ('Downloaded: ' + $f.Length.ToString() + ' bytes')"
|
||||
)
|
||||
|
||||
r = requests.post(
|
||||
'http://172.16.3.30:3001/api/agents/' + agent_id + '/command',
|
||||
headers=headers,
|
||||
json={'command_type': 'powershell', 'command': cmd, 'timeout_seconds': 120}
|
||||
)
|
||||
print('Send:', r.json())
|
||||
cmd_id = r.json()['command_id']
|
||||
|
||||
# Wait and check
|
||||
time.sleep(20)
|
||||
r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers)
|
||||
d = r2.json()
|
||||
print('Status:', d['status'])
|
||||
print('stdout:', d.get('stdout', ''))
|
||||
print('stderr:', (d.get('stderr', '') or '')[:500])
|
||||
46
projects/msp-tools/guru-rmm/scp_agent.py
Normal file
46
projects/msp-tools/guru-rmm/scp_agent.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import requests, json, time, sys
|
||||
|
||||
# Auth
|
||||
token_r = requests.post('http://172.16.3.30:3001/api/auth/login', json={
|
||||
'email': 'claude-api@azcomputerguru.com',
|
||||
'password': 'ClaudeAPI2026!@#'
|
||||
})
|
||||
token = token_r.json()['token']
|
||||
headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}
|
||||
agent_id = 'd28a1c90-47d7-448f-a287-197bc8892234'
|
||||
|
||||
def send_cmd(cmd, timeout=120, wait=20):
|
||||
r = requests.post(
|
||||
'http://172.16.3.30:3001/api/agents/' + agent_id + '/command',
|
||||
headers=headers,
|
||||
json={'command_type': 'powershell', 'command': cmd, 'timeout_seconds': timeout}
|
||||
)
|
||||
data = r.json()
|
||||
print('Sent:', data.get('status', 'error'), data.get('message', ''))
|
||||
cmd_id = data['command_id']
|
||||
time.sleep(wait)
|
||||
r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers)
|
||||
d = r2.json()
|
||||
print('Status:', d['status'])
|
||||
print('stdout:', d.get('stdout', ''))
|
||||
stderr = d.get('stderr', '') or ''
|
||||
if stderr:
|
||||
print('stderr:', stderr[:500])
|
||||
print('exit_code:', d.get('exit_code'))
|
||||
return d
|
||||
|
||||
# First, check what transfer tools are available on AD2
|
||||
print("=== Checking available tools ===")
|
||||
d = send_cmd("Get-Command scp,ssh,curl -ErrorAction SilentlyContinue | Select-Object Name,Source | Format-Table -AutoSize", wait=10)
|
||||
|
||||
print("\n=== Trying SCP from AD2 to RMM server ===")
|
||||
# Use scp with StrictHostKeyChecking=no for automated transfer
|
||||
cmd = (
|
||||
"scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL "
|
||||
"guru@172.16.3.30:/tmp/gururmm-agent-new.exe C:/Temp/gururmm-agent-new.exe 2>&1; "
|
||||
"if (Test-Path C:/Temp/gururmm-agent-new.exe) { "
|
||||
" $f = Get-Item C:/Temp/gururmm-agent-new.exe; "
|
||||
" Write-Output ('File size: ' + $f.Length.ToString() + ' bytes') "
|
||||
"} else { Write-Output 'File not found after SCP' }"
|
||||
)
|
||||
d = send_cmd(cmd, wait=30)
|
||||
@@ -10,16 +10,16 @@ use uuid::Uuid;
|
||||
|
||||
use crate::auth::AuthUser;
|
||||
use crate::db::{self, Command};
|
||||
use crate::ws::{CommandPayload, ServerMessage};
|
||||
use crate::ws::{CommandPayload, CommandType, ServerMessage};
|
||||
use crate::AppState;
|
||||
|
||||
/// Request to send a command to an agent
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SendCommandRequest {
|
||||
/// Command type (shell, powershell, python, script)
|
||||
/// Command type (shell, powershell, python, script, claude_task)
|
||||
pub command_type: String,
|
||||
|
||||
/// Command text to execute
|
||||
/// Command text to execute (also used as task description for claude_task)
|
||||
pub command: String,
|
||||
|
||||
/// Timeout in seconds (optional, default 300)
|
||||
@@ -27,6 +27,12 @@ pub struct SendCommandRequest {
|
||||
|
||||
/// Run as elevated/admin (optional, default false)
|
||||
pub elevated: Option<bool>,
|
||||
|
||||
/// Working directory for claude_task (optional, default C:\Shares\test)
|
||||
pub working_directory: Option<String>,
|
||||
|
||||
/// Context files for claude_task (optional)
|
||||
pub context_files: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Response after sending a command
|
||||
@@ -59,6 +65,18 @@ pub async fn send_command(
|
||||
"Command sent by user"
|
||||
);
|
||||
|
||||
// Validate and build command type
|
||||
let command_type = if CommandType::is_claude_task(&req.command_type) {
|
||||
CommandType::new_claude_task(
|
||||
req.command.clone(),
|
||||
req.working_directory.clone(),
|
||||
req.context_files.clone(),
|
||||
)
|
||||
} else {
|
||||
CommandType::from_api_string(&req.command_type)
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, e))?
|
||||
};
|
||||
|
||||
// Verify agent exists
|
||||
let _agent = db::get_agent_by_id(&state.db, agent_id)
|
||||
.await
|
||||
@@ -66,9 +84,10 @@ pub async fn send_command(
|
||||
.ok_or((StatusCode::NOT_FOUND, "Agent not found".to_string()))?;
|
||||
|
||||
// Create command record with user ID for audit trail
|
||||
// Store the canonical db string format for consistency
|
||||
let create = db::CreateCommand {
|
||||
agent_id,
|
||||
command_type: req.command_type.clone(),
|
||||
command_type: command_type.as_db_string().to_string(),
|
||||
command_text: req.command.clone(),
|
||||
created_by: Some(user.user_id),
|
||||
};
|
||||
@@ -80,10 +99,11 @@ pub async fn send_command(
|
||||
// Check if agent is connected
|
||||
let agents = state.agents.read().await;
|
||||
if agents.is_connected(&agent_id) {
|
||||
// Send command via WebSocket
|
||||
// Send command via WebSocket using the proper enum type
|
||||
// This serializes as snake_case to match the agent's expected format
|
||||
let cmd_msg = ServerMessage::Command(CommandPayload {
|
||||
id: command.id,
|
||||
command_type: req.command_type,
|
||||
command_type,
|
||||
command: req.command,
|
||||
timeout_seconds: req.timeout_seconds,
|
||||
elevated: req.elevated.unwrap_or(false),
|
||||
|
||||
@@ -193,10 +193,74 @@ pub struct NetworkStatePayload {
|
||||
pub state_hash: String,
|
||||
}
|
||||
|
||||
/// Types of commands that can be sent to agents.
|
||||
/// Must match the agent's CommandType enum serialization format.
|
||||
/// Uses snake_case to match the agent's #[serde(rename_all = "snake_case")].
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommandType {
|
||||
/// Shell command (cmd on Windows, sh on Unix)
|
||||
Shell,
|
||||
/// PowerShell command (Windows)
|
||||
PowerShell,
|
||||
/// Python script
|
||||
Python,
|
||||
/// Raw script execution
|
||||
Script,
|
||||
/// Claude Code task execution
|
||||
ClaudeTask {
|
||||
/// Task description for Claude Code
|
||||
task: String,
|
||||
/// Optional working directory
|
||||
working_directory: Option<String>,
|
||||
/// Optional context files
|
||||
context_files: Option<Vec<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl CommandType {
|
||||
/// Parse a command type string from the API into the enum.
|
||||
/// Accepts both snake_case ("power_shell") and common formats ("powershell").
|
||||
/// Note: ClaudeTask requires additional fields - use `new_claude_task()` instead.
|
||||
pub fn from_api_string(s: &str) -> Result<Self, String> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"shell" => Ok(Self::Shell),
|
||||
"powershell" | "power_shell" => Ok(Self::PowerShell),
|
||||
"python" => Ok(Self::Python),
|
||||
"script" => Ok(Self::Script),
|
||||
"claude_task" | "claudetask" => Err(
|
||||
"claude_task type requires task field - use the claude_task-specific API fields".to_string()
|
||||
),
|
||||
_ => Err(format!("Unknown command type: '{}'. Valid types: shell, powershell, python, script, claude_task", s)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a command type string represents a claude_task.
|
||||
pub fn is_claude_task(s: &str) -> bool {
|
||||
matches!(s.to_lowercase().as_str(), "claude_task" | "claudetask")
|
||||
}
|
||||
|
||||
/// Create a ClaudeTask command type with the required fields.
|
||||
pub fn new_claude_task(task: String, working_directory: Option<String>, context_files: Option<Vec<String>>) -> Self {
|
||||
Self::ClaudeTask { task, working_directory, context_files }
|
||||
}
|
||||
|
||||
/// Convert back to the string format stored in the database.
|
||||
pub fn as_db_string(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Shell => "shell",
|
||||
Self::PowerShell => "powershell",
|
||||
Self::Python => "python",
|
||||
Self::Script => "script",
|
||||
Self::ClaudeTask { .. } => "claude_task",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommandPayload {
|
||||
pub id: Uuid,
|
||||
pub command_type: String,
|
||||
pub command_type: CommandType,
|
||||
pub command: String,
|
||||
pub timeout_seconds: Option<u64>,
|
||||
pub elevated: bool,
|
||||
|
||||
80
projects/msp-tools/guru-rmm/swap_agent.py
Normal file
80
projects/msp-tools/guru-rmm/swap_agent.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Create scheduled task on AD2 to swap agent binary and restart service."""
|
||||
import requests, time
|
||||
|
||||
# Auth
|
||||
token_r = requests.post('http://172.16.3.30:3001/api/auth/login', json={
|
||||
'email': 'claude-api@azcomputerguru.com',
|
||||
'password': 'ClaudeAPI2026!@#'
|
||||
})
|
||||
token = token_r.json()['token']
|
||||
headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}
|
||||
agent_id = 'd28a1c90-47d7-448f-a287-197bc8892234'
|
||||
|
||||
# Create a scheduled task to swap the binary and restart
|
||||
# Uses schtasks for simplicity - runs a PowerShell script that:
|
||||
# 1. Stops the agent service
|
||||
# 2. Backs up old binary
|
||||
# 3. Copies new binary in place
|
||||
# 4. Starts the service
|
||||
# 5. Writes result to file
|
||||
cmd = (
|
||||
"$script = @'\n"
|
||||
"Stop-Service GuruRMMAgent -Force\n"
|
||||
"Start-Sleep -Seconds 3\n"
|
||||
"Copy-Item \"C:\\Program Files\\GuruRMM\\gururmm-agent.exe\" \"C:\\Program Files\\GuruRMM\\gururmm-agent.exe.bak\" -Force\n"
|
||||
"Copy-Item \"C:\\Temp\\gururmm-agent-new.exe\" \"C:\\Program Files\\GuruRMM\\gururmm-agent.exe\" -Force\n"
|
||||
"Start-Sleep -Seconds 1\n"
|
||||
"Start-Service GuruRMMAgent\n"
|
||||
"\"Agent updated at $(Get-Date)\" | Out-File C:\\Temp\\update_result.txt\n"
|
||||
"'@\n"
|
||||
"$script | Out-File C:/Temp/update_agent.ps1 -Encoding UTF8\n"
|
||||
"Write-Output ('Script written: ' + (Get-Item C:/Temp/update_agent.ps1).Length.ToString() + ' bytes')"
|
||||
)
|
||||
|
||||
print("Step 1: Writing update script to AD2...")
|
||||
r = requests.post(
|
||||
'http://172.16.3.30:3001/api/agents/' + agent_id + '/command',
|
||||
headers=headers,
|
||||
json={'command_type': 'powershell', 'command': cmd, 'timeout_seconds': 30}
|
||||
)
|
||||
print('Send:', r.json())
|
||||
cmd_id = r.json()['command_id']
|
||||
time.sleep(10)
|
||||
r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers)
|
||||
d = r2.json()
|
||||
print('Status:', d['status'])
|
||||
print('stdout:', d.get('stdout', ''))
|
||||
if d.get('stderr'):
|
||||
print('stderr:', (d.get('stderr', '') or '')[:300])
|
||||
|
||||
if d['status'] != 'completed':
|
||||
print("FAILED to write script")
|
||||
exit(1)
|
||||
|
||||
# Step 2: Create and run scheduled task
|
||||
print("\nStep 2: Creating scheduled task to run update...")
|
||||
cmd2 = (
|
||||
"schtasks /create /tn AgentBinaryUpdate /tr "
|
||||
"\"powershell.exe -ExecutionPolicy Bypass -File C:\\Temp\\update_agent.ps1\" "
|
||||
"/sc ONCE /st 00:00 /sd 01/01/2030 /ru SYSTEM /rl HIGHEST /f; "
|
||||
"schtasks /run /tn AgentBinaryUpdate; "
|
||||
"Write-Output 'Update task started - agent will restart momentarily'"
|
||||
)
|
||||
|
||||
r = requests.post(
|
||||
'http://172.16.3.30:3001/api/agents/' + agent_id + '/command',
|
||||
headers=headers,
|
||||
json={'command_type': 'powershell', 'command': cmd2, 'timeout_seconds': 30}
|
||||
)
|
||||
print('Send:', r.json())
|
||||
cmd_id = r.json()['command_id']
|
||||
time.sleep(10)
|
||||
r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers)
|
||||
d = r2.json()
|
||||
print('Status:', d['status'])
|
||||
print('stdout:', d.get('stdout', ''))
|
||||
if d.get('stderr'):
|
||||
print('stderr:', (d.get('stderr', '') or '')[:300])
|
||||
|
||||
print("\nAgent will stop, binary will be swapped, then service restarts.")
|
||||
print("Wait ~30 seconds then check if agent reconnects.")
|
||||
Reference in New Issue
Block a user