"""JSON API used by the patch editor, services and stats pages.""" from flask import Blueprint, jsonify, request from . import daemon, db, validate from .auth import login_required bp = Blueprint("api", __name__, url_prefix="/api") RANGES = { # range -> (seconds, tier, bucket seconds) "1h": (3600, "m", 60), "24h": (86400, "m", 60), "7d": (7 * 86400, "h", 3600), "30d": (30 * 86400, "h", 3600), "1y": (365 * 86400, "d", 86400), "all": (None, "d", 86400), } def _clients(): rows = db.get().execute("SELECT id, name, hostname, last_seen, last_addr FROM clients ORDER BY name") return [dict(r) for r in rows] @bp.get("/graph") @login_required def graph_get(): conn = db.get() nodes = [dict(r) for r in conn.execute( "SELECT id, type, client_id, host, port, proto, label, iface, origin, x, y FROM nodes ORDER BY id")] links = [{"from": r["from_node"], "to": r["to_node"]} for r in conn.execute( "SELECT from_node, to_node FROM links")] return jsonify(nodes=nodes, links=links, clients=_clients(), interfaces=_interfaces()) def _interfaces(): """{host id: [{name, addr}]}, host id 0 = target.""" out = {} for r in db.get().execute("SELECT client_id, name, addr FROM interfaces ORDER BY client_id, name"): out.setdefault(str(r["client_id"]), []).append({"name": r["name"], "addr": r["addr"]}) return out @bp.put("/graph") @login_required def graph_put(): conn = db.get() client_ids = {r["id"] for r in conn.execute("SELECT id FROM clients")} try: nodes, links = validate.graph(request.get_json(silent=True), client_ids) except (validate.Invalid, TypeError, ValueError) as e: return jsonify(ok=False, error=str(e)), 400 idmap = {} conn.execute("BEGIN IMMEDIATE") try: existing = {r["id"] for r in conn.execute("SELECT id FROM nodes")} keep = {n["id"] for n in nodes if n["id"] > 0} for nid in existing - keep: conn.execute("DELETE FROM nodes WHERE id = ?", (nid,)) cols = ("type", "client_id", "host", "port", "proto", "label", "iface", "origin", "x", "y") for n in nodes: vals = [n[c] for c in cols] if n["id"] > 0 and n["id"] in existing: conn.execute(f"UPDATE nodes SET {', '.join(c + ' = ?' for c in cols)} WHERE id = ?", vals + [n["id"]]) idmap[n["id"]] = n["id"] else: cur = conn.execute(f"INSERT INTO nodes ({', '.join(cols)}) VALUES ({', '.join('?' * len(cols))})", vals) idmap[n["id"]] = cur.lastrowid conn.execute("DELETE FROM links") for a, b in links: conn.execute("INSERT INTO links (from_node, to_node) VALUES (?, ?)", (idmap[a], idmap[b])) conn.execute("COMMIT") except Exception: conn.execute("ROLLBACK") raise res = daemon.reload() return jsonify(ok=True, ids={str(k): v for k, v in idmap.items()}, daemon=res.get("ok", False), daemon_error=res.get("error")) @bp.get("/status") @login_required def status(): st = daemon.request("STATUS") return jsonify(daemon=st.get("ok", False), error=st.get("error"), clients=st.get("clients", {}), sinks=st.get("sinks", {})) @bp.get("/live") @login_required def live(): st = daemon.request("LIVE") return jsonify(daemon=st.get("ok", False), sinks=st.get("sinks", {})) @bp.post("/services/refresh") @login_required def services_refresh(): res = daemon.request("SERVICES") return jsonify(ok=res.get("ok", False), error=res.get("error")) @bp.get("/stats") @login_required def stats(): rng = request.args.get("range", "24h") if rng not in RANGES: return jsonify(error="bad range"), 400 span, tier, step = RANGES[rng] try: sink = int(request.args.get("sink", "0")) except ValueError: return jsonify(error="bad sink"), 400 since = db.now() - span if span else 0 rows = db.get().execute( "SELECT ts, bytes_in, bytes_out, conns FROM stats WHERE sink_id = ? AND tier = ? AND ts >= ? ORDER BY ts", (sink, tier, since)).fetchall() return jsonify(range=rng, step=step, since=since, now=db.now(), points=[[r["ts"], r["bytes_in"], r["bytes_out"], r["conns"]] for r in rows])