From a1f0a3e5e81bfb612f224e6618b688c1fbc6578f Mon Sep 17 00:00:00 2001 From: Howard Enos Date: Sat, 4 Jul 2026 09:25:14 -0700 Subject: [PATCH] sync: auto-sync from HOWARD-HOME at 2026-07-04 09:24:45 Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-07-04 09:24:45 --- ...026-07-03-howard-gps-rmm-coverage-audit.md | 13 +++++ .../gps-rmm-audit/tools/reassign-staging.py | 50 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 projects/gps-rmm-audit/tools/reassign-staging.py diff --git a/projects/gps-rmm-audit/session-logs/2026-07/2026-07-03-howard-gps-rmm-coverage-audit.md b/projects/gps-rmm-audit/session-logs/2026-07/2026-07-03-howard-gps-rmm-coverage-audit.md index 275257bd..a2eb6947 100644 --- a/projects/gps-rmm-audit/session-logs/2026-07/2026-07-03-howard-gps-rmm-coverage-audit.md +++ b/projects/gps-rmm-audit/session-logs/2026-07/2026-07-03-howard-gps-rmm-coverage-audit.md @@ -181,3 +181,16 @@ Russo Law Firm (3 machines, single site): Department set on all (RUSSO-SRV->IT, Instrumental Music Center (music retail/repair, single site, ~12 in RMM): fixed IMC1 type->Server, Department set on 8 (IMC1->IT; IMC-STATION1/2 + IMC-L1-STATION9 + DESKTOP-44L80C0/MR3ALTK POS workstations->Sales; IMC-Lessons->Lessons; IMC-SvcStr->Repair). Skipped generics (C2B, DESKTOP-GHG12G3, IMC-Mini, LAPTOP-DCHQ3F92). Fleet pass: added `servers->IT` fallback to sc-cleanup.py (device type Server + no role token -> IT). Re-ran fleet-wide (safe - the >1-session skip prevents re-contamination). Set +20 departments (servers->IT + role tokens) across ~16 clients (AMT, BirthBiologic, Cutting Edge, Glaztech, Horseshoe, Len's, Lonestar, Patriot, Peaceful Spirit, QWM, Safesite, Sif-oidak, Sombra, Prairie Schooner, Golden Corral, Universal Cryogenics), fixed 6 device types. Company converged to 0 (contamination fully resolved). Remaining departments = person-named/generic machines, need per-client wiki passes (the long tail). + +## Update: Staging auto-enroll system (generic installer + reassignment) + +Problem: onboarded Bucket-C orgs show 0 agents - machines are offline/briefly-online, so per-client site-specific SC pushes don't land before they disconnect. Enrollment stalled ~90/189. + +Solution (Howard's idea): a SINGLE generic installer pushed by a Syncro policy to ALL managed machines; they enroll into a catch-all "Staging" site whenever they come online; then auto-reassign to the real client by hostname->Syncro customer->GuruRMM client. + +Built: +- GuruRMM client "Staging - Auto Enroll" (04b24e18-5dee-4eb4-b7a4-3f967b997a27) / site "Staging" (7c980f78-075a-4c09-915c-ba961936bc95) / code DARK-STORM-3150. Key vaulted at infrastructure/gururmm-staging-site.sops.yaml. +- Syncro one-liner to add to a policy (runs on all assets when online): irm 'https://rmm.azcomputerguru.com/install/DARK-STORM-3150/windows' | iex +- projects/gps-rmm-audit/tools/reassign-staging.py: moves Staging agents to their real client via POST /api/agents/:id/move, matching hostname->Syncro customer business_name->GuruRMM client (main site). Idempotent; --dry supported. Verified runs clean (0 staging agents currently). + +Next: Howard adds the one-liner to the Syncro all-machines policy; schedule reassign-staging.py (or fold into the daily GPS-RMM-Progress task). Unmatched agents stay in Staging + flagged. diff --git a/projects/gps-rmm-audit/tools/reassign-staging.py b/projects/gps-rmm-audit/tools/reassign-staging.py new file mode 100644 index 00000000..a087ba20 --- /dev/null +++ b/projects/gps-rmm-audit/tools/reassign-staging.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# reassign-staging.py — move GuruRMM agents out of the "Staging - Auto Enroll" catch-all +# into their real client, by matching hostname -> Syncro customer -> GuruRMM client. +# +# Flow: a generic installer (site DARK-STORM-3150) is pushed via a Syncro policy to every +# managed machine; they enroll into Staging. This tool then reassigns each to the right +# client automatically. Idempotent — run it on a schedule or on demand. +# +# Env: RMM, TOK (GuruRMM), SK (Syncro api key). --dry = report only. +import json,urllib.request,ssl,os,sys +RMM=os.environ["RMM"]; TOK=os.environ["TOK"]; SK=os.environ["SK"] +ctx=ssl.create_default_context(); DRY="--dry" in sys.argv +STAGING_CLIENT="Staging - Auto Enroll" +def rget(path): + return json.loads("".join(c for c in urllib.request.urlopen(urllib.request.Request(f"{RMM}{path}",headers={"Authorization":f"Bearer {TOK}"}),context=ctx,timeout=30).read().decode("utf-8","replace") if ord(c)>=32 or c in "\t\n\r")) +def move(aid,site_id): + req=urllib.request.Request(f"{RMM}/api/agents/{aid}/move",data=json.dumps({"site_id":site_id}).encode(),method="POST",headers={"Authorization":f"Bearer {TOK}","Content-Type":"application/json"}) + urllib.request.urlopen(req,context=ctx,timeout=20) +def syncro_customer(hostname): + raw=urllib.request.urlopen(urllib.request.Request(f"https://computerguru.syncromsp.com/api/v1/customer_assets?query={urllib.parse.quote(hostname)}&api_key={SK}"),context=ctx,timeout=25).read() + raw="".join(chr(b) for b in raw if b>=32 or b in (9,10,13)) + for a in json.loads(raw).get("assets",[]): + if (a.get("name") or "").lower()==hostname.lower(): + c=a.get("customer") or {} + return c.get("business_name") or c.get("fullname") + return None +import urllib.parse +agents=rget("/api/agents") +staging=[a for a in agents if a.get("client_name")==STAGING_CLIENT] +print(f"{len(staging)} agents in Staging") +# GuruRMM client -> a target site id (prefer 'Main'/'Main Office', else first) +clients=rget("/api/clients") +name2site={} +for c in clients: + if c["name"]==STAGING_CLIENT: continue + sites=rget(f"/api/clients/{c['id']}/sites") + if not sites: continue + main=next((s for s in sites if s["name"].lower() in ("main","main office","office")), sites[0]) + name2site[c["name"].lower()]=main["id"] +moved=0; unmatched=[] +for a in staging: + cust=syncro_customer(a["hostname"]) + site=name2site.get((cust or "").lower()) + if cust and site: + if not DRY: move(a["id"],site) + moved+=1; print(f" {a['hostname']} -> '{cust}'") + else: + unmatched.append(f"{a['hostname']} (syncro='{cust}')") +print(f"\n{'WOULD move' if DRY else 'Moved'} {moved}; unmatched {len(unmatched)} (stay in Staging):") +for u in unmatched: print(" "+u)