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,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""kuma-cloudron-sync.py — Uptime Kuma Cloudron app coverage sync [#435]
|
||||
|
||||
Compares the committed Cloudron app list (oam/kuma/cloudron-apps.txt) against
|
||||
live Kuma http monitors, reports coverage gaps, and (with --add) creates the
|
||||
missing monitors INSIDE the "Cloud Systems" group (never root — founder
|
||||
ruling 2026-09-02: monitors go in groups).
|
||||
|
||||
App list provenance: CT-log enumeration + DNS resolve + HTTPS probe across
|
||||
all 23 Kuma-monaged domains (2026-09-02, see #435 note). Re-derive with
|
||||
/tmp-style probe pipeline or a read-only Cloudron API diff when a token
|
||||
lands. Two apps (forms/learn) return 500 today — monitored red on purpose
|
||||
(#681).
|
||||
|
||||
Env (from ~/.creds/uptime-kuma.env):
|
||||
UPTIME_KUMA_URL, UPTIME_KUMA_TOKEN
|
||||
|
||||
Usage:
|
||||
python3 kuma-cloudron-sync.py [--add] [--limit N] [--group-id ID]
|
||||
--add create missing monitors
|
||||
--limit N add at most N (canary mode)
|
||||
--group-id ID parent group (default: 48 = "Cloud Systems")
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import socketio
|
||||
|
||||
HERE = Path(__file__).resolve().parent.parent / "oam" / "kuma" / "cloudron-apps.txt"
|
||||
DEFAULT_GROUP = 48
|
||||
|
||||
|
||||
def load_apps():
|
||||
apps = []
|
||||
for line in HERE.read_text().splitlines():
|
||||
line = line.split("#", 1)[0].strip()
|
||||
if line:
|
||||
apps.append(line)
|
||||
return apps
|
||||
|
||||
|
||||
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("--add", action="store_true")
|
||||
ap.add_argument("--limit", type=int, default=0)
|
||||
ap.add_argument("--group-id", type=int, default=DEFAULT_GROUP)
|
||||
args = ap.parse_args()
|
||||
|
||||
apps = load_apps()
|
||||
sio, mons = kuma_connect()
|
||||
try:
|
||||
covered = set()
|
||||
for m in mons.values():
|
||||
name = (m.get("name") or "").strip().lower()
|
||||
url = (m.get("url") or "").strip().lower()
|
||||
for fqdn in apps:
|
||||
if fqdn == name or url.rstrip("/") in (
|
||||
f"https://{fqdn}".rstrip("/"),
|
||||
f"https://{fqdn}/".rstrip("/"),
|
||||
):
|
||||
covered.add(fqdn)
|
||||
missing = [a for a in apps if a not in covered]
|
||||
|
||||
print(f"apps in list: {len(apps)} covered: {len(covered)} missing: {len(missing)}")
|
||||
for fqdn in missing:
|
||||
print(f" MISSING {fqdn}")
|
||||
|
||||
if args.add and missing:
|
||||
batch = missing if not args.limit else missing[: args.limit]
|
||||
for fqdn in batch:
|
||||
payload = {
|
||||
"type": "http",
|
||||
"name": fqdn,
|
||||
"url": f"https://{fqdn}/",
|
||||
"interval": 60,
|
||||
"retryInterval": 60,
|
||||
"resendInterval": 0,
|
||||
"maxretries": 2,
|
||||
"notificationIDList": {"1": True, "2": True},
|
||||
"upsideDown": False,
|
||||
"description": "Cloudron app HTTP 200 (#435)",
|
||||
"httpBodyEncoding": "json",
|
||||
"accepted_statuscodes": ["200-299"],
|
||||
"conditions": [],
|
||||
"active": True,
|
||||
"parent": args.group_id,
|
||||
"maxredirects": 10,
|
||||
}
|
||||
r = sio.call("add", payload, timeout=30)
|
||||
ok = isinstance(r, dict) and r.get("ok")
|
||||
print(f" ADD {fqdn}: {'id=' + str(r.get('monitorID')) if ok else r}")
|
||||
finally:
|
||||
sio.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user