#!/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: idparenttypeactivenametarget 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", " ") # hostname first: ping monitors carry a junk default url ("https://") target = m.get("hostname") or m.get("url") 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()