KeyHunter parser v10 + 18 scanners + tests (synthetic fixtures only, no keys/db/tokens)
This commit is contained in:
106
fleet_dispatch.py
Executable file
106
fleet_dispatch.py
Executable file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dispatch imba.yml workflow on selected fleet nodes (with SSL retries).
|
||||
SAFEGATE: requires explicit --node, --limit, or --all. No args = dry-run + exit.
|
||||
|
||||
Usage examples:
|
||||
python fleet_dispatch.py # dry-run: print node count, exit
|
||||
python fleet_dispatch.py --node imba-node-0002
|
||||
python fleet_dispatch.py --limit 10
|
||||
python fleet_dispatch.py --all
|
||||
"""
|
||||
import os
|
||||
KF_BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
os.makedirs(os.path.join(KF_BASE, "data"), exist_ok=True)
|
||||
import argparse
|
||||
import concurrent.futures as cf
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
DEPLOY = os.path.join(KF_BASE, "data/fleet_deploy.json")
|
||||
H = {"Accept": "application/vnd.github+json"}
|
||||
|
||||
s = requests.Session()
|
||||
s.trust_env = False
|
||||
s.mount("https://", HTTPAdapter(max_retries=Retry(total=5, backoff_factor=1)))
|
||||
|
||||
|
||||
def dispatch(rec):
|
||||
full = f"{rec['login']}/{rec['repo']}"
|
||||
h = {**H, "Authorization": f"Bearer {rec['pat']}"}
|
||||
for attempt in range(3):
|
||||
try:
|
||||
r = s.post(
|
||||
f"https://api.github.com/repos/{full}/actions/workflows/imba.yml/dispatches",
|
||||
headers=h, json={"ref": "main"}, timeout=30)
|
||||
if r.status_code in (201, 204):
|
||||
return full, True, r.status_code
|
||||
return full, False, f"{r.status_code} {r.text[:120]}"
|
||||
except Exception as e:
|
||||
if attempt == 2:
|
||||
return full, False, str(e)[:120]
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Dispatch imba.yml workflow on fleet nodes.",
|
||||
epilog="Without --node/--limit/--all: dry-run + exit (no dispatch).")
|
||||
target = ap.add_mutually_exclusive_group()
|
||||
target.add_argument("--node", metavar="REPO",
|
||||
help="exact repo name or login/repo")
|
||||
target.add_argument("--limit", type=int, metavar="N",
|
||||
help="first N ok nodes")
|
||||
target.add_argument("--all", action="store_true",
|
||||
help="ALL ok nodes (1391)")
|
||||
ap.add_argument("--offset", type=int, default=0,
|
||||
help="starting index")
|
||||
ap.add_argument("--dry-run", action="store_true",
|
||||
help="print target count and exit")
|
||||
args, _ = ap.parse_known_args()
|
||||
|
||||
recs = [r for r in json.load(open(DEPLOY, encoding="utf-8")) if r.get("ok")]
|
||||
if args.node:
|
||||
recs = [r for r in recs
|
||||
if r.get("repo") == args.node
|
||||
or f"{r.get('login')}/{r.get('repo')}" == args.node]
|
||||
elif args.limit:
|
||||
recs = recs[args.offset:args.offset + args.limit]
|
||||
elif args.all:
|
||||
recs = recs[args.offset:] # support offset even with --all
|
||||
else:
|
||||
# dry-run default when no explicit target
|
||||
print(f"fleet_dispatch.py SAFEGATE: no target specified.\n"
|
||||
f" --node REPO | --limit N | --all required for live dispatch.\n"
|
||||
f" ok nodes available: {len(recs)}\n"
|
||||
f" (dry-run; use --dry-run to see target count)")
|
||||
sys.exit(0)
|
||||
|
||||
if args.dry_run or not (args.node or args.limit or args.all):
|
||||
print(f"DRY-RUN: would dispatch {len(recs)} nodes")
|
||||
return
|
||||
|
||||
if not recs:
|
||||
sys.exit("no nodes selected (--node/--limit matched nothing)")
|
||||
print(f"dispatching {len(recs)} nodes", flush=True)
|
||||
ok = 0
|
||||
t0 = time.time()
|
||||
with cf.ThreadPoolExecutor(max_workers=12) as ex:
|
||||
for i, (full, good, info) in enumerate(ex.map(dispatch, recs), 1):
|
||||
ok += good
|
||||
if not good:
|
||||
print(f" FAIL {full}: {info}", flush=True)
|
||||
if i % 50 == 0:
|
||||
# ":.0s" is not a format code: every dispatch died here on the
|
||||
# final summary (and on the per-50 progress line), after the
|
||||
# ex.map loop had already run, so runs were dispatched but the
|
||||
# ok/total was never printed and the exit code was a traceback.
|
||||
print(f" [{i}/{len(recs)}] ok={ok} {time.time()-t0:.0f}s", flush=True)
|
||||
print(f"\nDONE {time.time()-t0:.0f}s: {ok}/{len(recs)} dispatched", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user