Author: Mike Swanson Machine: Mikes-MacBook-Air.local Timestamp: 2026-07-08 06:49:28
197 lines
6.3 KiB
Python
Executable File
197 lines
6.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Deploy GuruRMM agents to Glaztech machines via ScreenConnect
|
|
Maps SC site tags to RMM enrollment codes
|
|
|
|
CRITICAL: Use /download/msi suffix for direct installer downloads
|
|
Correct: https://rmm.azcomputerguru.com/install/{CODE}/download/msi
|
|
Incorrect: https://rmm.azcomputerguru.com/install/{CODE} (returns HTML page)
|
|
|
|
Attribution: When posting bot alerts about deployments, load identity first:
|
|
source .claude/scripts/get-identity.sh
|
|
bash .claude/scripts/post-bot-alert.sh "[RMM] $USER_SHORT deployed to X machines"
|
|
"""
|
|
import json
|
|
import subprocess
|
|
import os
|
|
import sys
|
|
|
|
# ScreenConnect Site Tag → RMM Site Code mapping
|
|
SITE_MAP = {
|
|
'TUC': 'UPPER-CLOUD-1975',
|
|
'SAN': 'GOLD-RIVER-5359',
|
|
'PHX': 'SOUTH-HAWK-1396',
|
|
'DEN': 'SWIFT-BEAR-1220',
|
|
'BOI': 'CALM-STAR-7234',
|
|
'SLC': 'BOLD-HAWK-9935',
|
|
'BRL': 'LOWER-PEAK-5448',
|
|
'INV': 'SWIFT-HAWK-5568',
|
|
'SHV': 'WEST-OCEAN-1630',
|
|
'ABQ': 'WEST-PHOENIX-4895',
|
|
'REMOTE': 'UPPER-FALCON-3720',
|
|
}
|
|
|
|
def main():
|
|
preview_only = '--preview' in sys.argv
|
|
limit = None
|
|
|
|
# Check for --limit N
|
|
if '--limit' in sys.argv:
|
|
idx = sys.argv.index('--limit')
|
|
if idx + 1 < len(sys.argv):
|
|
limit = int(sys.argv[idx + 1])
|
|
|
|
# Load SC sessions
|
|
with open('/Users/azcomputerguru/ClaudeTools/.glaztech-sessions.json') as f:
|
|
sessions = json.load(f)
|
|
|
|
print(f'=== Glaztech RMM Deployment ===')
|
|
print(f'Total SC machines: {len(sessions)}')
|
|
|
|
# Filter to Windows machines only (RMM agent is Windows-only currently)
|
|
deployments = []
|
|
skipped = []
|
|
|
|
for session in sessions:
|
|
session_id = session.get('SessionID')
|
|
name = session.get('Name', 'Unknown')
|
|
os_name = session.get('GuestInfo', {}).get('OperatingSystemName', '')
|
|
sc_site = session.get('CustomProperties', {}).get('CustomProperty2', '')
|
|
|
|
# Skip non-Windows
|
|
if 'Windows' not in os_name:
|
|
skipped.append({'name': name, 'reason': f'Non-Windows OS: {os_name}'})
|
|
continue
|
|
|
|
# Get RMM enrollment code
|
|
rmm_code = SITE_MAP.get(sc_site)
|
|
if not rmm_code:
|
|
skipped.append({'name': name, 'reason': f'Unknown site tag: {sc_site}'})
|
|
continue
|
|
|
|
deployments.append({
|
|
'session_id': session_id,
|
|
'name': name,
|
|
'os': os_name,
|
|
'sc_site': sc_site,
|
|
'rmm_code': rmm_code,
|
|
})
|
|
|
|
print(f'Windows machines: {len(deployments)}')
|
|
print(f'Skipped: {len(skipped)}')
|
|
|
|
if skipped:
|
|
print(f'\n[INFO] Skipped machines:')
|
|
for s in skipped[:10]:
|
|
print(f' {s["name"]:30s} - {s["reason"]}')
|
|
if len(skipped) > 10:
|
|
print(f' ... and {len(skipped) - 10} more')
|
|
|
|
if not deployments:
|
|
print('\n[INFO] No machines to deploy.')
|
|
return 0
|
|
|
|
# Apply limit if specified
|
|
if limit:
|
|
print(f'\n[INFO] Limiting to first {limit} machines')
|
|
deployments = deployments[:limit]
|
|
|
|
print(f'\n=== Deployment Plan ({len(deployments)} machines) ===')
|
|
print(f'{"Name":30s} {"SC Site":10s} {"RMM Code":20s}')
|
|
print('-' * 65)
|
|
for d in deployments[:20]:
|
|
print(f'{d["name"]:30s} {d["sc_site"]:10s} {d["rmm_code"]:20s}')
|
|
if len(deployments) > 20:
|
|
print(f'... and {len(deployments) - 20} more')
|
|
|
|
if preview_only:
|
|
print('\n[PREVIEW] Use --execute to deploy')
|
|
return 0
|
|
|
|
if '--execute' not in sys.argv:
|
|
print('\n[INFO] Add --execute to deploy')
|
|
return 0
|
|
|
|
# Deploy
|
|
print(f'\n=== Deploying {len(deployments)} RMM agents ===')
|
|
success_count = 0
|
|
fail_count = 0
|
|
|
|
env = {
|
|
'CLAUDETOOLS_ROOT': '/Users/azcomputerguru/ClaudeTools',
|
|
'PATH': os.environ.get('PATH', '/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin'),
|
|
'HOME': os.environ.get('HOME', '/Users/azcomputerguru'),
|
|
'SCREENCONNECT_API_SECRET': os.environ.get('SCREENCONNECT_API_SECRET', '')
|
|
}
|
|
|
|
for i, d in enumerate(deployments, 1):
|
|
# Build PowerShell install command (ScreenConnect requires #!ps prefix)
|
|
install_cmd = f'''#!ps
|
|
$ErrorActionPreference = "Stop"
|
|
$url = "https://rmm.azcomputerguru.com/install/{d['rmm_code']}/download/msi"
|
|
$installer = "$env:TEMP\\gururmm-install.msi"
|
|
$log = "$env:TEMP\\gururmm-install-{d['sc_site']}.log"
|
|
|
|
try {{
|
|
Write-Host "Downloading from: $url"
|
|
Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing
|
|
|
|
if (-not (Test-Path $installer)) {{
|
|
throw "Download failed - file not created"
|
|
}}
|
|
|
|
$size = (Get-Item $installer).Length
|
|
Write-Host "Downloaded: $size bytes"
|
|
|
|
if ($size -lt 100000) {{
|
|
throw "Downloaded file too small ($size bytes) - likely not an MSI"
|
|
}}
|
|
|
|
Write-Host "Installing GuruRMM agent..."
|
|
$proc = Start-Process msiexec.exe -ArgumentList "/i `"$installer`" /qn /norestart /l*v `"$log`"" -Wait -PassThru
|
|
|
|
if ($proc.ExitCode -eq 0) {{
|
|
Write-Host "SUCCESS: GuruRMM agent installed for site {d['sc_site']}"
|
|
}} else {{
|
|
Write-Host "ERROR: msiexec exited with code $($proc.ExitCode)"
|
|
Write-Host "Check log: $log"
|
|
}}
|
|
}} catch {{
|
|
Write-Host "ERROR: $_"
|
|
}} finally {{
|
|
if (Test-Path $installer) {{ Remove-Item $installer -Force -ErrorAction SilentlyContinue }}
|
|
}}
|
|
'''
|
|
|
|
# Send via ScreenConnect
|
|
cmd = [
|
|
'bash',
|
|
'/Users/azcomputerguru/ClaudeTools/.claude/scripts/py.sh',
|
|
'/Users/azcomputerguru/.claude/skills/screenconnect/scripts/sc.py',
|
|
'send-command',
|
|
'--session', d['session_id'],
|
|
'--command', install_cmd,
|
|
'--confirm'
|
|
]
|
|
|
|
try:
|
|
result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=30)
|
|
if result.returncode == 0:
|
|
print(f'[{i}/{len(deployments)}] OK: {d["name"]:30s} -> {d["sc_site"]}')
|
|
success_count += 1
|
|
else:
|
|
print(f'[{i}/{len(deployments)}] FAIL: {d["name"]:30s} - {result.stderr.strip()[:80]}')
|
|
fail_count += 1
|
|
except Exception as e:
|
|
print(f'[{i}/{len(deployments)}] ERROR: {d["name"]:30s} - {e}')
|
|
fail_count += 1
|
|
|
|
print(f'\n=== Summary ===')
|
|
print(f'Success: {success_count}')
|
|
print(f'Failed: {fail_count}')
|
|
|
|
return 0 if fail_count == 0 else 1
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|