feat(kuma): Cloudron HTTP-200 coverage + group discipline [#435]
55 http monitors added under "Cloud Systems" (blue/green: canary first, verified green); 9 root-level strays re-homed into founder's groups (root now zero). New tools: inventory (read-only dump), cloudron-sync (idempotent diff/add from committed app list), regroup (lib-based moves; raw editMonitor times out on this build). fleet-sync now requires --group-id so it can never place monitors at root again. Results: https://projects.knownelement.com/issues/435#note-4
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""kuma-inventory.py — read-only Uptime Kuma monitor inventory dump [#435]
|
||||
|
||||
Connects via the socket.io API (same pattern as kuma-fleet-sync.py) and
|
||||
prints every monitor with id, type, parent (group), name, target, active
|
||||
state. Read-only: emits nothing back to the server. Used to plan group
|
||||
placement and coverage gaps; never mutates.
|
||||
|
||||
Env (from ~/.creds/uptime-kuma.env):
|
||||
UPTIME_KUMA_URL, UPTIME_KUMA_TOKEN
|
||||
|
||||
Usage:
|
||||
python3 kuma-inventory.py # TSV dump: id<TAB>parent<TAB>type<TAB>active<TAB>name<TAB>target
|
||||
python3 kuma-inventory.py --summary # group counts + root-level offenders only
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
|
||||
import socketio
|
||||
|
||||
|
||||
def kuma_connect():
|
||||
sio = socketio.Client(reconnection=False)
|
||||
state = {}
|
||||
|
||||
@sio.on("monitorList")
|
||||
def _ml(d):
|
||||
state["ml"] = d
|
||||
|
||||
sio.connect(
|
||||
os.environ["UPTIME_KUMA_URL"], transports=["websocket"], wait_timeout=30
|
||||
)
|
||||
r = sio.call("loginByToken", os.environ["UPTIME_KUMA_TOKEN"], timeout=30)
|
||||
if not (isinstance(r, dict) and r.get("ok")):
|
||||
raise SystemExit(f"login failed: {r}")
|
||||
sio.emit("monitorList")
|
||||
for _ in range(20):
|
||||
if "ml" in state:
|
||||
break
|
||||
time.sleep(1)
|
||||
return sio, state.get("ml", {})
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--summary", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
sio, mons = kuma_connect()
|
||||
try:
|
||||
by_id = {str(m.get("id")): m for m in mons.values()}
|
||||
|
||||
def name_of(mid):
|
||||
m = by_id.get(str(mid))
|
||||
return (m.get("name") or "?") if m else f"id{mid}"
|
||||
|
||||
rows = []
|
||||
for m in mons.values():
|
||||
mid = m.get("id")
|
||||
parent = m.get("parent") or m.get("parent_id") or ""
|
||||
parent = name_of(parent) if parent not in ("", None) else "-"
|
||||
mtype = m.get("type") or "?"
|
||||
active = "on" if m.get("active", True) else "PAUSED"
|
||||
name = (m.get("name") or "?").replace("\t", " ")
|
||||
target = (m.get("hostname") if m.get("type") == "dns" else None) or m.get("url") or m.get("hostname") or ""
|
||||
target = str(target).replace("\t", " ")
|
||||
rows.append((mid, parent, mtype, active, name, target))
|
||||
rows.sort(key=lambda r: (r[1] == "-", r[1], r[4].lower()))
|
||||
|
||||
if args.summary:
|
||||
groups = [r for r in rows if r[2] == "group"]
|
||||
root = [r for r in rows if r[1] == "-" and r[2] != "group"]
|
||||
print(f"total monitors: {len(rows)} groups: {len(groups)} root-level: {len(root)}")
|
||||
print("\ngroups:")
|
||||
for r in groups:
|
||||
kids = sum(1 for x in rows if x[1] == r[4])
|
||||
print(f" {r[4]} ({kids} children, id={r[0]})")
|
||||
print("\nroot-level monitors (must move into groups):")
|
||||
for r in root:
|
||||
print(f" id={r[0]} [{r[2]}] {r[3]} {r[4]} {r[5]}")
|
||||
else:
|
||||
for r in rows:
|
||||
print("\t".join(str(c) for c in r))
|
||||
finally:
|
||||
sio.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user