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:
@@ -68,9 +68,9 @@ function Copy-FromNAS {
|
|||||||
New-Item -ItemType Directory -Path $localDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $localDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
# FIX: Use -i for key auth, remove embedded quotes around remote path
|
# Escape spaces in remote path for SCP (unescaped spaces cause "ambiguous target")
|
||||||
# DOS 8.3 filenames have no spaces - embedded quotes caused "ambiguous target"
|
$escapedRemote = $RemotePath -replace ' ', '\ '
|
||||||
$result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:${RemotePath}" "$LocalPath" 2>&1
|
$result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:${escapedRemote}" "$LocalPath" 2>&1
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
$errorMsg = $result | Out-String
|
$errorMsg = $result | Out-String
|
||||||
Write-Log " SCP PULL ERROR (exit $LASTEXITCODE): $errorMsg"
|
Write-Log " SCP PULL ERROR (exit $LASTEXITCODE): $errorMsg"
|
||||||
@@ -93,9 +93,9 @@ function Copy-ToNAS {
|
|||||||
$remoteDir = ($RemotePath -replace '[^/]+$', '').TrimEnd('/')
|
$remoteDir = ($RemotePath -replace '[^/]+$', '').TrimEnd('/')
|
||||||
Invoke-NASCommand "mkdir -p '$remoteDir'" | Out-Null
|
Invoke-NASCommand "mkdir -p '$remoteDir'" | Out-Null
|
||||||
|
|
||||||
# FIX: Use -i for key auth, remove embedded quotes around remote path
|
# Escape spaces in remote path for SCP (unescaped spaces cause "ambiguous target")
|
||||||
# DOS 8.3 filenames have no spaces - embedded quotes caused "ambiguous target"
|
$escapedRemote = $RemotePath -replace ' ', '\ '
|
||||||
$result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "$LocalPath" "${NAS_USER}@${NAS_IP}:${RemotePath}" 2>&1
|
$result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "$LocalPath" "${NAS_USER}@${NAS_IP}:${escapedRemote}" 2>&1
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
$errorMsg = $result | Out-String
|
$errorMsg = $result | Out-String
|
||||||
Write-Log " SCP PUSH ERROR (exit $LASTEXITCODE): $errorMsg"
|
Write-Log " SCP PUSH ERROR (exit $LASTEXITCODE): $errorMsg"
|
||||||
|
|||||||
0
projects/dataforth-dos/Sync-FromNAS.ps1.original
Normal file
0
projects/dataforth-dos/Sync-FromNAS.ps1.original
Normal file
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]
|
[package]
|
||||||
name = "gururmm-agent"
|
name = "gururmm-agent"
|
||||||
version = "0.3.5"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "GuruRMM Agent - Cross-platform RMM agent"
|
description = "GuruRMM Agent - Cross-platform RMM agent"
|
||||||
authors = ["GuruRMM"]
|
authors = ["GuruRMM"]
|
||||||
|
|||||||
@@ -199,6 +199,9 @@ pub enum CommandType {
|
|||||||
Shell,
|
Shell,
|
||||||
|
|
||||||
/// PowerShell command (Windows)
|
/// 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,
|
PowerShell,
|
||||||
|
|
||||||
/// Python script
|
/// Python script
|
||||||
@@ -208,6 +211,7 @@ pub enum CommandType {
|
|||||||
Script { interpreter: String },
|
Script { interpreter: String },
|
||||||
|
|
||||||
/// Claude Code task execution
|
/// Claude Code task execution
|
||||||
|
#[serde(alias = "claude_task")]
|
||||||
ClaudeTask {
|
ClaudeTask {
|
||||||
/// Task description for Claude Code
|
/// Task description for Claude Code
|
||||||
task: String,
|
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::auth::AuthUser;
|
||||||
use crate::db::{self, Command};
|
use crate::db::{self, Command};
|
||||||
use crate::ws::{CommandPayload, ServerMessage};
|
use crate::ws::{CommandPayload, CommandType, ServerMessage};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
/// Request to send a command to an agent
|
/// Request to send a command to an agent
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct SendCommandRequest {
|
pub struct SendCommandRequest {
|
||||||
/// Command type (shell, powershell, python, script)
|
/// Command type (shell, powershell, python, script, claude_task)
|
||||||
pub command_type: String,
|
pub command_type: String,
|
||||||
|
|
||||||
/// Command text to execute
|
/// Command text to execute (also used as task description for claude_task)
|
||||||
pub command: String,
|
pub command: String,
|
||||||
|
|
||||||
/// Timeout in seconds (optional, default 300)
|
/// Timeout in seconds (optional, default 300)
|
||||||
@@ -27,6 +27,12 @@ pub struct SendCommandRequest {
|
|||||||
|
|
||||||
/// Run as elevated/admin (optional, default false)
|
/// Run as elevated/admin (optional, default false)
|
||||||
pub elevated: Option<bool>,
|
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
|
/// Response after sending a command
|
||||||
@@ -59,6 +65,18 @@ pub async fn send_command(
|
|||||||
"Command sent by user"
|
"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
|
// Verify agent exists
|
||||||
let _agent = db::get_agent_by_id(&state.db, agent_id)
|
let _agent = db::get_agent_by_id(&state.db, agent_id)
|
||||||
.await
|
.await
|
||||||
@@ -66,9 +84,10 @@ pub async fn send_command(
|
|||||||
.ok_or((StatusCode::NOT_FOUND, "Agent not found".to_string()))?;
|
.ok_or((StatusCode::NOT_FOUND, "Agent not found".to_string()))?;
|
||||||
|
|
||||||
// Create command record with user ID for audit trail
|
// Create command record with user ID for audit trail
|
||||||
|
// Store the canonical db string format for consistency
|
||||||
let create = db::CreateCommand {
|
let create = db::CreateCommand {
|
||||||
agent_id,
|
agent_id,
|
||||||
command_type: req.command_type.clone(),
|
command_type: command_type.as_db_string().to_string(),
|
||||||
command_text: req.command.clone(),
|
command_text: req.command.clone(),
|
||||||
created_by: Some(user.user_id),
|
created_by: Some(user.user_id),
|
||||||
};
|
};
|
||||||
@@ -80,10 +99,11 @@ pub async fn send_command(
|
|||||||
// Check if agent is connected
|
// Check if agent is connected
|
||||||
let agents = state.agents.read().await;
|
let agents = state.agents.read().await;
|
||||||
if agents.is_connected(&agent_id) {
|
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 {
|
let cmd_msg = ServerMessage::Command(CommandPayload {
|
||||||
id: command.id,
|
id: command.id,
|
||||||
command_type: req.command_type,
|
command_type,
|
||||||
command: req.command,
|
command: req.command,
|
||||||
timeout_seconds: req.timeout_seconds,
|
timeout_seconds: req.timeout_seconds,
|
||||||
elevated: req.elevated.unwrap_or(false),
|
elevated: req.elevated.unwrap_or(false),
|
||||||
|
|||||||
@@ -193,10 +193,74 @@ pub struct NetworkStatePayload {
|
|||||||
pub state_hash: String,
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CommandPayload {
|
pub struct CommandPayload {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub command_type: String,
|
pub command_type: CommandType,
|
||||||
pub command: String,
|
pub command: String,
|
||||||
pub timeout_seconds: Option<u64>,
|
pub timeout_seconds: Option<u64>,
|
||||||
pub elevated: bool,
|
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.")
|
||||||
Submodule projects/solverbot updated: 342fe0fdf4...60b2617651
20
tmp_backup_ad2.ps1
Normal file
20
tmp_backup_ad2.ps1
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
|
||||||
|
$uncRoot = "\\192.168.0.6\C$"
|
||||||
|
|
||||||
|
try {
|
||||||
|
New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null
|
||||||
|
Write-Output "=== DRIVE MAPPED ==="
|
||||||
|
|
||||||
|
# Step 3: Back up the current script
|
||||||
|
$src = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1"
|
||||||
|
$bak = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1.bak.2026-02-17"
|
||||||
|
Copy-Item -Path $src -Destination $bak -Force
|
||||||
|
Write-Output "=== BACKUP CREATED: $bak ==="
|
||||||
|
|
||||||
|
Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue
|
||||||
|
} catch {
|
||||||
|
Write-Output "ERROR: $_"
|
||||||
|
}
|
||||||
56
tmp_bulksync.ps1
Normal file
56
tmp_bulksync.ps1
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
$uncRoot = "\\192.168.0.6\C$"
|
||||||
|
New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null
|
||||||
|
|
||||||
|
# Write a one-time bulk sync runner script
|
||||||
|
$bulkScript = @"
|
||||||
|
# One-time bulk sync with 86400 minute window (60 days)
|
||||||
|
# This catches all stranded files from the last ~35 days
|
||||||
|
# Auto-deletes itself after running
|
||||||
|
Set-Location "C:\Shares\test\scripts"
|
||||||
|
& powershell -NoProfile -ExecutionPolicy Bypass -File "C:\Shares\test\scripts\Sync-FromNAS.ps1" -MaxAgeMinutes 86400 -Verbose
|
||||||
|
# Log completion
|
||||||
|
Add-Content -Path "C:\Shares\test\scripts\sync-from-nas.log" -Value "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) : BULK SYNC COMPLETE - one-time catchup finished"
|
||||||
|
# Clean up this trigger script
|
||||||
|
Remove-Item -Path "C:\Shares\test\scripts\BulkSync-OneTime.ps1" -Force -ErrorAction SilentlyContinue
|
||||||
|
"@
|
||||||
|
|
||||||
|
Set-Content -Path "AD2:\Shares\test\scripts\BulkSync-OneTime.ps1" -Value $bulkScript
|
||||||
|
Write-Output "[OK] Bulk sync script written to AD2:\Shares\test\scripts\BulkSync-OneTime.ps1"
|
||||||
|
|
||||||
|
# Now create a scheduled task on AD2 to run it immediately
|
||||||
|
# We can do this by writing a schtasks command file
|
||||||
|
$taskScript = @"
|
||||||
|
schtasks /create /tn "BulkSync-OneTime" /tr "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\BulkSync-OneTime.ps1" /sc once /st 00:00 /ru INTRANET\sysadmin /rp "Paper123\!@#" /f
|
||||||
|
schtasks /run /tn "BulkSync-OneTime"
|
||||||
|
"@
|
||||||
|
|
||||||
|
Set-Content -Path "AD2:\Shares\test\scripts\run-bulk.cmd" -Value $taskScript
|
||||||
|
Write-Output "[OK] Task creation script written"
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "NOTE: The scheduled task needs to be triggered on AD2."
|
||||||
|
Write-Output "Attempting to use WMI to create process on AD2..."
|
||||||
|
|
||||||
|
# Try WMI process creation (may work even if WinRM doesnt)
|
||||||
|
try {
|
||||||
|
$proc = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\BulkSync-OneTime.ps1"
|
||||||
|
if ($proc.ReturnValue -eq 0) {
|
||||||
|
Write-Output "[OK] WMI process started on AD2\! PID: $($proc.ProcessId)"
|
||||||
|
} else {
|
||||||
|
Write-Output "[ERROR] WMI process creation failed with return value: $($proc.ReturnValue)"
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Output "[ERROR] WMI failed: $_"
|
||||||
|
Write-Output "Trying schtasks approach..."
|
||||||
|
try {
|
||||||
|
$schResult = schtasks /create /s 192.168.0.6 /u INTRANET\sysadmin /p "Paper123\!@#" /tn "BulkSync-OneTime" /tr "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\BulkSync-OneTime.ps1" /sc once /st 00:00 /ru INTRANET\sysadmin /rp "Paper123\!@#" /f 2>&1
|
||||||
|
Write-Output "schtasks create: $schResult"
|
||||||
|
$runResult = schtasks /run /s 192.168.0.6 /u INTRANET\sysadmin /p "Paper123\!@#" /tn "BulkSync-OneTime" 2>&1
|
||||||
|
Write-Output "schtasks run: $runResult"
|
||||||
|
} catch {
|
||||||
|
Write-Output "[ERROR] schtasks also failed: $_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue
|
||||||
29
tmp_bulksync2.ps1
Normal file
29
tmp_bulksync2.ps1
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
$uncPath = "\\192.168.0.6\C$\Shares\test\scripts"
|
||||||
|
|
||||||
|
# Map net use
|
||||||
|
net use "\\192.168.0.6\C$" /user:INTRANET\sysadmin ("Paper123" + [char]33 + "@#") 2>&1 | Out-Null
|
||||||
|
|
||||||
|
# Write the bulk sync script using direct UNC path
|
||||||
|
$bulkContent = @"
|
||||||
|
Set-Location "C:\Shares\test\scripts"
|
||||||
|
& powershell -NoProfile -ExecutionPolicy Bypass -File "C:\Shares\test\scripts\Sync-FromNAS.ps1" -MaxAgeMinutes 86400 -Verbose
|
||||||
|
"@
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText("\\192.168.0.6\C$\Shares\test\scripts\BulkSync-OneTime.ps1", $bulkContent)
|
||||||
|
Write-Output "[OK] BulkSync script written via System.IO"
|
||||||
|
|
||||||
|
# Verify it exists
|
||||||
|
$exists = Test-Path "\\192.168.0.6\C$\Shares\test\scripts\BulkSync-OneTime.ps1"
|
||||||
|
Write-Output "File exists: $exists"
|
||||||
|
|
||||||
|
# Now trigger it via WMI
|
||||||
|
$proc = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\BulkSync-OneTime.ps1"
|
||||||
|
if ($proc.ReturnValue -eq 0) {
|
||||||
|
Write-Output "[OK] Bulk sync started on AD2\! PID: $($proc.ProcessId)"
|
||||||
|
} else {
|
||||||
|
Write-Output "[ERROR] Failed to start: return value $($proc.ReturnValue)"
|
||||||
|
}
|
||||||
|
|
||||||
|
net use "\\192.168.0.6\C$" /delete 2>&1 | Out-Null
|
||||||
13
tmp_check2.ps1
Normal file
13
tmp_check2.ps1
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
|
||||||
|
# Check if still running
|
||||||
|
$procs = Get-WmiObject -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Filter "Name='powershell.exe'" 2>$null
|
||||||
|
$syncRunning = $false
|
||||||
|
foreach ($p in $procs) {
|
||||||
|
if ($p.CommandLine -match "BulkSync|86400") {
|
||||||
|
Write-Output "Still running: PID=$($p.ProcessId)"
|
||||||
|
$syncRunning = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $syncRunning) { Write-Output "Bulk sync has completed\!" }
|
||||||
18
tmp_check3.ps1
Normal file
18
tmp_check3.ps1
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
|
||||||
|
# Read the last 40 lines of the sync log
|
||||||
|
$logContent = [System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\sync-from-nas.log")
|
||||||
|
$lines = $logContent -split "`n"
|
||||||
|
$lastLines = $lines | Select-Object -Last 40
|
||||||
|
Write-Output "=== LAST 40 LOG LINES ==="
|
||||||
|
$lastLines | ForEach-Object { Write-Output $_ }
|
||||||
|
|
||||||
|
# Check process status
|
||||||
|
$procs = Get-WmiObject -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Filter "Name='powershell.exe'" 2>$null
|
||||||
|
$syncRunning = $false
|
||||||
|
foreach ($p in $procs) {
|
||||||
|
if ($p.CommandLine -match "BulkSync|86400") { $syncRunning = $true }
|
||||||
|
}
|
||||||
|
Write-Output ""
|
||||||
|
if ($syncRunning) { Write-Output "STATUS: Still running..." } else { Write-Output "STATUS: COMPLETED" }
|
||||||
18
tmp_check4.ps1
Normal file
18
tmp_check4.ps1
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
|
||||||
|
# Read the last 50 lines of the sync log
|
||||||
|
$logContent = [System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\sync-from-nas.log")
|
||||||
|
$lines = $logContent -split "`n"
|
||||||
|
$lastLines = $lines | Select-Object -Last 50
|
||||||
|
Write-Output "=== LAST 50 LOG LINES ==="
|
||||||
|
$lastLines | ForEach-Object { Write-Output $_ }
|
||||||
|
|
||||||
|
# Check process status
|
||||||
|
$procs = Get-WmiObject -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Filter "Name='powershell.exe'" 2>$null
|
||||||
|
$syncRunning = $false
|
||||||
|
foreach ($p in $procs) {
|
||||||
|
if ($p.CommandLine -match "BulkSync|86400") { $syncRunning = $true }
|
||||||
|
}
|
||||||
|
Write-Output ""
|
||||||
|
if ($syncRunning) { Write-Output "[...] STATUS: Still running..." } else { Write-Output "[OK] STATUS: COMPLETED" }
|
||||||
18
tmp_check_proc.ps1
Normal file
18
tmp_check_proc.ps1
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
|
||||||
|
# Check if the process is still running
|
||||||
|
$proc = Get-WmiObject -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Filter "ProcessId=15856" 2>$null
|
||||||
|
if ($proc) {
|
||||||
|
Write-Output "Process 15856 still running: $($proc.CommandLine)"
|
||||||
|
} else {
|
||||||
|
Write-Output "Process 15856 has completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for any powershell processes running our script
|
||||||
|
$procs = Get-WmiObject -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Filter "Name='powershell.exe'" 2>$null
|
||||||
|
foreach ($p in $procs) {
|
||||||
|
if ($p.CommandLine -match "Sync-FromNAS|BulkSync") {
|
||||||
|
Write-Output "Found sync process: PID=$($p.ProcessId) CMD=$($p.CommandLine)"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
tmp_connect_ad2.ps1
Normal file
21
tmp_connect_ad2.ps1
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
|
||||||
|
$uncRoot = "\\192.168.0.6\C$"
|
||||||
|
|
||||||
|
try {
|
||||||
|
New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop
|
||||||
|
Write-Output "=== DRIVE MAPPED ==="
|
||||||
|
$scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1"
|
||||||
|
if (Test-Path $scriptPath) {
|
||||||
|
Write-Output "=== SCRIPT CONTENT START ==="
|
||||||
|
Get-Content $scriptPath -Raw
|
||||||
|
Write-Output "=== SCRIPT CONTENT END ==="
|
||||||
|
} else {
|
||||||
|
Write-Output "Script not found"
|
||||||
|
}
|
||||||
|
Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue
|
||||||
|
} catch {
|
||||||
|
Write-Output "ERROR: $_"
|
||||||
|
}
|
||||||
37
tmp_fix2.ps1
Normal file
37
tmp_fix2.ps1
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
$uncRoot = "\\192.168.0.6\C$"
|
||||||
|
|
||||||
|
try {
|
||||||
|
New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null
|
||||||
|
$scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1"
|
||||||
|
$lines = Get-Content $scriptPath
|
||||||
|
|
||||||
|
# Fix line 70 (0-indexed: 69) - the Copy-FromNAS SCP line
|
||||||
|
$oldLine = $lines[69]
|
||||||
|
Write-Output "OLD LINE 70: $oldLine"
|
||||||
|
|
||||||
|
# The correct line should have backtick-quoted remote path and quoted local path
|
||||||
|
$lines[69] = ' $result = & $SCP -O -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:`"${RemotePath}`"" "$LocalPath" 2>&1'
|
||||||
|
|
||||||
|
Write-Output "NEW LINE 70: $($lines[69])"
|
||||||
|
|
||||||
|
# Write back
|
||||||
|
Set-Content -Path $scriptPath -Value $lines
|
||||||
|
Write-Output "=== LINE 70 FIXED ==="
|
||||||
|
|
||||||
|
# Verify both SCP lines are now correct
|
||||||
|
$verify = Get-Content $scriptPath
|
||||||
|
Write-Output "=== FINAL SCP LINES ==="
|
||||||
|
for ($i = 0; $i -lt $verify.Count; $i++) {
|
||||||
|
if ($verify[$i] -match "result.*SCP") {
|
||||||
|
Write-Output ("{0,3}: {1}" -f ($i+1), $verify[$i].TrimEnd())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue
|
||||||
|
} catch {
|
||||||
|
Write-Output "ERROR: $_"
|
||||||
|
Write-Output $_.ScriptStackTrace
|
||||||
|
}
|
||||||
58
tmp_fix_script.ps1
Normal file
58
tmp_fix_script.ps1
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
$uncRoot = "\\192.168.0.6\C$"
|
||||||
|
|
||||||
|
try {
|
||||||
|
New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null
|
||||||
|
Write-Output "=== DRIVE MAPPED ==="
|
||||||
|
|
||||||
|
$scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1"
|
||||||
|
$content = Get-Content $scriptPath -Raw
|
||||||
|
|
||||||
|
# Fix 1: Invoke-NASCommand - add user@host target
|
||||||
|
$old1 = '$result = & $SSH -i "C:\Users\sysadmin\.ssh\id_ed25519" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new $Command 2>&1'
|
||||||
|
$new1 = '$result = & $SSH -i "C:\Users\sysadmin\.ssh\id_ed25519" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${NAS_USER}@${NAS_IP}" $Command 2>&1'
|
||||||
|
$content = $content.Replace($old1, $new1)
|
||||||
|
|
||||||
|
# Fix 2: Copy-FromNAS - fix the SCP line (it has the if statement on same line + unquoted paths)
|
||||||
|
$old2 = '$result = & $SCP -O -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:$RemotePath" $LocalPath 2>&1 if ($LASTEXITCODE -ne 0) {'
|
||||||
|
$new2 = @"
|
||||||
|
`$result = & `$SCP -O -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "`${NAS_USER}@`${NAS_IP}:`"`${RemotePath}`"" "`$LocalPath" 2>&1
|
||||||
|
if (`$LASTEXITCODE -ne 0) {
|
||||||
|
"@
|
||||||
|
$content = $content.Replace($old2, $new2)
|
||||||
|
|
||||||
|
# Fix 3: Fix the error message in Copy-FromNAS (PUSH -> PULL)
|
||||||
|
$old3 = 'Write-Log " SCP PUSH ERROR (exit $LASTEXITCODE): $errorMsg"'
|
||||||
|
# Only fix the FIRST occurrence (in Copy-FromNAS). Use a targeted approach.
|
||||||
|
# Since we already fixed the structure, just do a simple replace of the first occurrence
|
||||||
|
$idx = $content.IndexOf($old3)
|
||||||
|
if ($idx -ge 0) {
|
||||||
|
$new3 = 'Write-Log " SCP PULL ERROR (exit $LASTEXITCODE): $errorMsg"'
|
||||||
|
$content = $content.Substring(0, $idx) + $new3 + $content.Substring($idx + $old3.Length)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fix 4: Copy-ToNAS - quote both paths
|
||||||
|
$old4 = '$result = & $SCP -O -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" $LocalPath "${NAS_USER}@${NAS_IP}:$RemotePath" 2>&1'
|
||||||
|
$new4 = '$result = & $SCP -O -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "$LocalPath" "${NAS_USER}@${NAS_IP}:`"${RemotePath}`"" 2>&1'
|
||||||
|
$content = $content.Replace($old4, $new4)
|
||||||
|
|
||||||
|
# Write the fixed script
|
||||||
|
Set-Content -Path $scriptPath -Value $content -NoNewline
|
||||||
|
Write-Output "=== SCRIPT UPDATED ==="
|
||||||
|
|
||||||
|
# Verify the fixes
|
||||||
|
$verify = Get-Content $scriptPath -Raw
|
||||||
|
Write-Output "=== VERIFY: SCP LINES ==="
|
||||||
|
$verify -split "`n" | Where-Object { $_ -match "result = .* SCP" } | ForEach-Object { Write-Output (" " + $_.Trim()) }
|
||||||
|
Write-Output "=== VERIFY: SSH LINE ==="
|
||||||
|
$verify -split "`n" | Where-Object { $_ -match "result = .* SSH" } | ForEach-Object { Write-Output (" " + $_.Trim()) }
|
||||||
|
Write-Output "=== VERIFY: ERROR MESSAGES ==="
|
||||||
|
$verify -split "`n" | Where-Object { $_ -match "SCP (PULL|PUSH) ERROR" } | ForEach-Object { Write-Output (" " + $_.Trim()) }
|
||||||
|
|
||||||
|
Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue
|
||||||
|
} catch {
|
||||||
|
Write-Output "ERROR: $_"
|
||||||
|
Write-Output $_.ScriptStackTrace
|
||||||
|
}
|
||||||
7
tmp_grep.ps1
Normal file
7
tmp_grep.ps1
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
$cmd = "cmd /c powershell -NoProfile -Command \"Get-Content C:\Shares\test\scripts\sync-from-nas.log | Select-String -Pattern 'Starting sync|Max age: 86400|Sync complete|No new DAT|Found.*DAT|PULL=|ambiguous' | Select-Object -Last 30 | ForEach-Object { $_.Line } > C:\Shares\test\scripts\tail-grep.txt 2>&1\""
|
||||||
|
$r = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList $cmd
|
||||||
|
Write-Output "PID: $($r.ProcessId)"
|
||||||
|
Start-Sleep -Seconds 15
|
||||||
|
Write-Output ([System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\tail-grep.txt"))
|
||||||
14
tmp_grep2.ps1
Normal file
14
tmp_grep2.ps1
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
|
||||||
|
$filterScript = 'Get-Content C:\Shares\test\scripts\sync-from-nas.log | Select-String -Pattern "Starting sync|Max age: 86400|Sync complete|No new DAT|Found.*DAT|PULL=|ambiguous|Copied to station|Copied report" | Select-Object -Last 40 | ForEach-Object { $_.Line } | Out-File C:\Shares\test\scripts\tail-grep.txt -Encoding utf8'
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText("\\192.168.0.6\C$\Shares\test\scripts\filter-log.ps1", $filterScript)
|
||||||
|
$r = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\filter-log.ps1"
|
||||||
|
Write-Output "PID: $($r.ProcessId)"
|
||||||
|
Start-Sleep -Seconds 20
|
||||||
|
try {
|
||||||
|
Write-Output ([System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\tail-grep.txt"))
|
||||||
|
} catch {
|
||||||
|
Write-Output "File not ready: $_"
|
||||||
|
}
|
||||||
24
tmp_inspect.ps1
Normal file
24
tmp_inspect.ps1
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
|
||||||
|
$uncRoot = "\\192.168.0.6\C$"
|
||||||
|
|
||||||
|
try {
|
||||||
|
New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null
|
||||||
|
Write-Output "=== DRIVE MAPPED ==="
|
||||||
|
|
||||||
|
$scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1"
|
||||||
|
$content = Get-Content $scriptPath -Raw
|
||||||
|
|
||||||
|
# Show the original problematic lines
|
||||||
|
Write-Output "=== ORIGINAL Copy-FromNAS SCP LINE ==="
|
||||||
|
$content -split "`n" | Where-Object { $_ -match "SCP.*RemotePath.*LocalPath|SCP.*LocalPath.*RemotePath" } | ForEach-Object { Write-Output $_.Trim() }
|
||||||
|
|
||||||
|
Write-Output "=== ORIGINAL Invoke-NASCommand LINE ==="
|
||||||
|
$content -split "`n" | Where-Object { $_ -match "result = .* \" } | ForEach-Object { Write-Output $_.Trim() }
|
||||||
|
|
||||||
|
Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue
|
||||||
|
} catch {
|
||||||
|
Write-Output "ERROR: $_"
|
||||||
|
}
|
||||||
20
tmp_lines.ps1
Normal file
20
tmp_lines.ps1
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
$uncRoot = "\\192.168.0.6\C$"
|
||||||
|
|
||||||
|
try {
|
||||||
|
New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null
|
||||||
|
$scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1"
|
||||||
|
$lines = Get-Content $scriptPath
|
||||||
|
|
||||||
|
# Show lines 70-95 (around Copy-FromNAS SCP area)
|
||||||
|
Write-Output "=== Lines 60-95 ==="
|
||||||
|
for ($i = 59; $i -lt 95 -and $i -lt $lines.Count; $i++) {
|
||||||
|
Write-Output ("{0,3}: {1}" -f ($i+1), $lines[$i])
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue
|
||||||
|
} catch {
|
||||||
|
Write-Output "ERROR: $_"
|
||||||
|
}
|
||||||
9
tmp_readlog.ps1
Normal file
9
tmp_readlog.ps1
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Read sync log - last 60 lines
|
||||||
|
try {
|
||||||
|
$log = [System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\sync-from-nas.log")
|
||||||
|
$logLines = $log -split "`n"
|
||||||
|
Write-Output "=== SYNC LOG (last 60 lines) ==="
|
||||||
|
$logLines | Select-Object -Last 60 | ForEach-Object { Write-Output $_ }
|
||||||
|
} catch {
|
||||||
|
Write-Output "ERROR reading log: $_"
|
||||||
|
}
|
||||||
0
tmp_readlog2.ps1
Normal file
0
tmp_readlog2.ps1
Normal file
18
tmp_readtail.ps1
Normal file
18
tmp_readtail.ps1
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
|
||||||
|
# Write a small script on AD2 to create the tail file
|
||||||
|
$tailScript = "Get-Content C:\Shares\test\scripts\sync-from-nas.log -Tail 80 | Out-File C:\Shares\test\scripts\sync-tail.txt -Encoding utf8; Get-Content C:\Shares\test\_SYNC_STATUS.txt | Out-File C:\Shares\test\scripts\sync-tail.txt -Append -Encoding utf8"
|
||||||
|
[System.IO.File]::WriteAllText("\\192.168.0.6\C$\Shares\test\scripts\read-tail.ps1", $tailScript)
|
||||||
|
|
||||||
|
# Run it via WMI
|
||||||
|
$cmd = "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\read-tail.ps1"
|
||||||
|
$result = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList $cmd
|
||||||
|
Write-Output "WMI result: $($result.ReturnValue) PID: $($result.ProcessId)"
|
||||||
|
|
||||||
|
# Wait for it to finish
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
|
||||||
|
# Read the tail file
|
||||||
|
$tailContent = [System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\sync-tail.txt")
|
||||||
|
Write-Output $tailContent
|
||||||
10
tmp_summary.ps1
Normal file
10
tmp_summary.ps1
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
|
||||||
|
$script = '$log = Get-Content C:\Shares\test\scripts\sync-from-nas.log; $bulk = $log | Where-Object { $_ -match "2026-02-17 16:(4|5)" }; $out = @(); $out += "=== FIRST 30 LINES ==="; $out += ($bulk | Select-Object -First 30); $out += ""; $out += "=== LAST 20 LINES ==="; $out += ($bulk | Select-Object -Last 20); $out += ""; $out += "=== SUMMARY ==="; $out += "Total lines: $($bulk.Count)"; $out += "Pushed: $(($bulk | Where-Object { $_ -match "Pushed:" }).Count)"; $out += "Errors: $(($bulk | Where-Object { $_ -match "ERROR" }).Count)"; $out += "Ambiguous: $(($bulk | Where-Object { $_ -match "ambiguous" }).Count)"; $out += "No such file: $(($bulk | Where-Object { $_ -match "No such file" }).Count)"; $out += "Copied station: $(($bulk | Where-Object { $_ -match "Copied to station" }).Count)"; $out += "Copied report: $(($bulk | Where-Object { $_ -match "Copied report" }).Count)"; $out | Out-File C:\Shares\test\scripts\sync-summary.txt -Encoding utf8'
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText("\\192.168.0.6\C$\Shares\test\scripts\make-summary.ps1", $script)
|
||||||
|
$r = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\make-summary.ps1"
|
||||||
|
Write-Output "PID: $($r.ProcessId)"
|
||||||
|
Start-Sleep -Seconds 10
|
||||||
|
Write-Output ([System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\sync-summary.txt"))
|
||||||
29
tmp_syntax.ps1
Normal file
29
tmp_syntax.ps1
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
$uncRoot = "\\192.168.0.6\C$"
|
||||||
|
|
||||||
|
try {
|
||||||
|
New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null
|
||||||
|
$scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1"
|
||||||
|
$content = Get-Content $scriptPath -Raw
|
||||||
|
|
||||||
|
# Syntax check via parsing the content string
|
||||||
|
$parseErrors = $null
|
||||||
|
[System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$null, [ref]$parseErrors)
|
||||||
|
if ($parseErrors.Count -eq 0) {
|
||||||
|
Write-Output "[OK] Script parses OK - no syntax errors"
|
||||||
|
} else {
|
||||||
|
foreach ($err in $parseErrors) {
|
||||||
|
Write-Output "[ERROR] PARSE ERROR: $($err.Message) at line $($err.Extent.StartLineNumber)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Count total lines
|
||||||
|
$lineCount = ($content -split "`n").Count
|
||||||
|
Write-Output "Total lines: $lineCount"
|
||||||
|
|
||||||
|
Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue
|
||||||
|
} catch {
|
||||||
|
Write-Output "ERROR: $_"
|
||||||
|
}
|
||||||
11
tmp_syntax2.ps1
Normal file
11
tmp_syntax2.ps1
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
$uncRoot = "\\192.168.0.6\C$"
|
||||||
|
New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null
|
||||||
|
$content = Get-Content "AD2:\Shares\test\scripts\Sync-FromNAS.ps1" -Raw
|
||||||
|
$tokens = $null; $parseErrors = $null
|
||||||
|
[void][System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$tokens, [ref]$parseErrors)
|
||||||
|
Write-Output "Parse errors: $($parseErrors.Count)"
|
||||||
|
if ($parseErrors.Count -gt 0) { $parseErrors | ForEach-Object { Write-Output " $_" } }
|
||||||
|
else { Write-Output "[OK] No syntax errors detected" }
|
||||||
|
Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue
|
||||||
6
tmp_tail.ps1
Normal file
6
tmp_tail.ps1
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
$r = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList "cmd /c powershell -NoProfile -Command Get-Content C:\Shares\test\scripts\sync-from-nas.log -Tail 40 > C:\Shares\test\scripts\tail40.txt 2>&1"
|
||||||
|
Write-Output "PID: $($r.ProcessId) Return: $($r.ReturnValue)"
|
||||||
|
Start-Sleep -Seconds 8
|
||||||
|
Write-Output ([System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\tail40.txt"))
|
||||||
18
tmp_test.ps1
Normal file
18
tmp_test.ps1
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
$uncRoot = "\\192.168.0.6\C$"
|
||||||
|
New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null
|
||||||
|
|
||||||
|
# First, do a dry run to see what files are stranded on the NAS
|
||||||
|
Write-Output "=== DRY RUN: Bulk sync with 86400 minute window (60 days) ==="
|
||||||
|
$dryRunOutput = & powershell -NoProfile -ExecutionPolicy Bypass -Command {
|
||||||
|
# We cannot run the script directly on AD2 from here, so we will invoke it remotely
|
||||||
|
}
|
||||||
|
|
||||||
|
# Actually we need to run this ON AD2. Lets check if we can invoke it via the mapped drive.
|
||||||
|
# The script needs to run locally on AD2 (it calls SCP/SSH). We need WinRM or SSH for that.
|
||||||
|
# Since WinRM is blocked from this machine, lets try another approach.
|
||||||
|
|
||||||
|
# Check: can we SSH to AD2 with the key from this machine?
|
||||||
|
Write-Output "=== Testing SSH to NAS directly from this machine ==="
|
||||||
|
Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue
|
||||||
33
tmp_verify.ps1
Normal file
33
tmp_verify.ps1
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
$uncRoot = "\\192.168.0.6\C$"
|
||||||
|
|
||||||
|
try {
|
||||||
|
New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null
|
||||||
|
$scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1"
|
||||||
|
$content = Get-Content $scriptPath -Raw
|
||||||
|
|
||||||
|
# Show the Copy-FromNAS function
|
||||||
|
$lines = $content -split "`n"
|
||||||
|
$inFunc = $false
|
||||||
|
$funcName = ""
|
||||||
|
foreach ($line in $lines) {
|
||||||
|
if ($line -match "^function (Copy-FromNAS|Copy-ToNAS|Invoke-NASCommand)") {
|
||||||
|
$inFunc = $true
|
||||||
|
$funcName = $Matches[1]
|
||||||
|
Write-Output "=== $funcName ==="
|
||||||
|
}
|
||||||
|
if ($inFunc) {
|
||||||
|
Write-Output $line.TrimEnd()
|
||||||
|
if ($line.Trim() -eq "}" -and $funcName) {
|
||||||
|
$inFunc = $false
|
||||||
|
Write-Output ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue
|
||||||
|
} catch {
|
||||||
|
Write-Output "ERROR: $_"
|
||||||
|
}
|
||||||
32
tmp_verify2.ps1
Normal file
32
tmp_verify2.ps1
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force
|
||||||
|
$cred = New-Object PSCredential("INTRANET\sysadmin", $pass)
|
||||||
|
$uncRoot = "\\192.168.0.6\C$"
|
||||||
|
|
||||||
|
try {
|
||||||
|
New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null
|
||||||
|
$scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1"
|
||||||
|
$lines = Get-Content $scriptPath
|
||||||
|
|
||||||
|
Write-Output "=== FULL VERIFICATION: Lines 48-100 ==="
|
||||||
|
for ($i = 47; $i -lt 100 -and $i -lt $lines.Count; $i++) {
|
||||||
|
Write-Output ("{0,3}: {1}" -f ($i+1), $lines[$i])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Also check the script parses without errors
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "=== SYNTAX CHECK ==="
|
||||||
|
$parseErrors = $null
|
||||||
|
[System.Management.Automation.Language.Parser]::ParseFile("AD2:\Shares\test\scripts\Sync-FromNAS.ps1", [ref]$null, [ref]$parseErrors)
|
||||||
|
if ($parseErrors.Count -eq 0) {
|
||||||
|
Write-Output "Script parses OK - no syntax errors"
|
||||||
|
} else {
|
||||||
|
foreach ($err in $parseErrors) {
|
||||||
|
Write-Output "PARSE ERROR: $($err.Message) at line $($err.Extent.StartLineNumber)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue
|
||||||
|
} catch {
|
||||||
|
Write-Output "ERROR: $_"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user