124 lines
4.3 KiB
Python
124 lines
4.3 KiB
Python
"""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])
|