network/gaiia/sync_all_radius_groups.py
2026-07-19 14:42:02 -05:00

160 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""
Run 'Update RADIUS Account Groups' for every account in Gaiia.
Status must be UPPERCASE (ACTIVE, SUSPENDED, etc.)
Usage:
source .envrc
uv run python3 sync_all_radius_groups.py --dry-run (preview)
uv run python3 sync_all_radius_groups.py (execute)
"""
from gaiia import GaiiaClient, GaiiaAPIError, GaiiaRateLimitedError
import json
import sys
import time
WF_ID = "workflow_fLXTNCvridPqwEEXxf3CK1"
def list_accounts(client):
"""Paginate through all accounts, return [{id, name, status, productIds}]."""
accounts = []
after = None
page = 0
while True:
page += 1
data = client.query("""
query Q($after: String) {
accounts(first: 250, after: $after) {
nodes {
id
name
status { name }
billingSubscriptions {
nodes {
productVersion {
product { id name slug }
}
}
}
}
pageInfo { hasNextPage endCursor }
totalCount
}
}
""", {"after": after} if after else {})
result = data['accounts']
for acct in result['nodes']:
status_name = acct['status']['name'] if acct['status'] else ''
products = []
for sub in (acct.get('billingSubscriptions') or {}).get('nodes', []):
pv = sub.get('productVersion') or {}
p = pv.get('product') if pv else None
if p and p['id'] not in products:
products.append(p['id'])
accounts.append({
'id': acct['id'],
'name': acct.get('name', '') or '',
'status': status_name,
'productIds': products,
})
sys.stderr.write(f"\r Page {page}: {len(accounts)} accounts so far...")
sys.stderr.flush()
page_info = result['pageInfo']
if not page_info.get('hasNextPage'):
break
after = page_info['endCursor']
sys.stderr.write("\n")
return accounts
def sync_account(client, acct, dry_run=False):
"""Run the Update RADIUS Account Groups workflow for one account."""
status_upper = acct['status'].upper()
wf_input = {
"accountId": acct['id'],
"status": status_upper,
"productIds": acct['productIds'],
}
if dry_run:
name = acct['name'] or '(no name)'
print(f" [DRY-RUN] {name} status={status_upper} products={len(acct['productIds'])}")
return True
try:
result = client.query("""
mutation Run($input: StartWorkflowExecutionInput!) {
startWorkflowExecution(input: $input) {
workflowExecution { id status }
}
}
""", {"input": {"workflowId": WF_ID, "input": wf_input}})
we = result['startWorkflowExecution']['workflowExecution']
return we is not None
except GaiiaAPIError as e:
print(f" FAILED {acct.get('name','?')}: API error - {e}")
return False
except GaiiaRateLimitedError as e:
print(f" RATE LIMITED - sleeping until {e.retry_at}")
if e.retry_at:
wait = max(0, (e.retry_at.timestamp() - time.time()))
time.sleep(wait + 1)
return False
def main():
dry_run = "--dry-run" in sys.argv
with GaiiaClient(timezone='America/Chicago') as g:
print("Listing all accounts...")
accounts = list_accounts(g)
print(f"\nFound {len(accounts)} accounts")
# Stats
statuses = {}
for a in accounts:
s = a['status'].upper()
statuses[s] = statuses.get(s, 0) + 1
print(f"Statuses: {statuses}")
if dry_run:
print("\n--dry-run mode: previewing --")
else:
print(f"\nSyncing via workflow {WF_ID}...")
succeeded = 0
failed = 0
for i, a in enumerate(accounts):
name = a['name'] or f"(id={a['id']})"
if not dry_run:
sys.stderr.write(f"\r {i+1}/{len(accounts)}: {name[:40]}...")
sys.stderr.flush()
if sync_account(g, a, dry_run):
succeeded += 1
else:
failed += 1
if not dry_run:
time.sleep(0.15) # avoid rate limiting (~7/sec)
if not dry_run:
sys.stderr.write("\n")
print(f"\nDone: {succeeded} started, {failed} failed")
if dry_run:
print("(nothing executed — remove --dry-run to run)")
if __name__ == '__main__':
main()