feat: LLM stack status monitord catalogs and HTML
This commit is contained in:
parent
6200a4d031
commit
666dca1f0d
7 changed files with 6943 additions and 0 deletions
224
site-gen/taler-monitord
Executable file
224
site-gen/taler-monitord
Executable file
|
|
@ -0,0 +1,224 @@
|
|||
#!/usr/bin/env python3
|
||||
"""taler-monitord — probe domain stacks on an interval; UI reads JSON only."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
import stack_status_html as H # noqa: E402
|
||||
import stack_status_lib as L # noqa: E402
|
||||
|
||||
# Match lib.sh: load ~/.config/taler-monitoring/env (or TALER_MONITORING_ENV), then
|
||||
# refresh module flags that were read at import time.
|
||||
_ENV_LOADED = L.load_monitoring_env()
|
||||
if _ENV_LOADED:
|
||||
L.refresh_runtime_flags_from_env()
|
||||
|
||||
|
||||
def _pid_path() -> Path:
|
||||
return L.monitord_state_dir() / "taler-monitord.pid"
|
||||
|
||||
|
||||
def _write_pid() -> None:
|
||||
_pid_path().write_text(str(os.getpid()) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _clear_pid() -> None:
|
||||
p = _pid_path()
|
||||
try:
|
||||
p.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _publish_one(local: Path, ssh_host: str, remote: str) -> str | None:
|
||||
remote_dir = str(Path(remote).parent)
|
||||
mk = subprocess.run(
|
||||
[
|
||||
"ssh",
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=8",
|
||||
ssh_host,
|
||||
f"mkdir -p -- {remote_dir!s}",
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
check=False,
|
||||
)
|
||||
if mk.returncode != 0:
|
||||
err = (mk.stderr or b"").decode("utf-8", "replace").strip()[:200]
|
||||
print(f"publish mkdir fail ({remote}): {err}", file=sys.stderr, flush=True)
|
||||
return None
|
||||
r = subprocess.run(
|
||||
[
|
||||
"scp",
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=8",
|
||||
str(local),
|
||||
f"{ssh_host}:{remote}",
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
err = (r.stderr or b"").decode("utf-8", "replace").strip()[:200]
|
||||
print(f"publish fail ({remote}): {err}", file=sys.stderr, flush=True)
|
||||
return None
|
||||
if "/var/www/monitoring-sites/" in remote:
|
||||
subprocess.run(
|
||||
[
|
||||
"ssh",
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=8",
|
||||
ssh_host,
|
||||
(
|
||||
f"chmod 755 -- {remote_dir!s} && chmod 644 -- {remote!s} && "
|
||||
f"setfacl -m u:caddy:rx,m::r-x -- {remote_dir!s} && "
|
||||
f"setfacl -m u:caddy:r,m::r-- -- {remote!s} || true"
|
||||
),
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
check=False,
|
||||
)
|
||||
else:
|
||||
subprocess.run(
|
||||
[
|
||||
"ssh",
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=8",
|
||||
ssh_host,
|
||||
f"chmod 755 -- {remote_dir!s} && chmod 644 -- {remote!s} || true",
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
check=False,
|
||||
)
|
||||
return f"{ssh_host}:{remote}"
|
||||
|
||||
|
||||
def publish_html(doc: dict) -> str | None:
|
||||
"""Render HTML and scp to Cyon (hacktivism) paths.
|
||||
|
||||
Env:
|
||||
TALER_MONITORD_PUBLISH=0 to disable
|
||||
TALER_MONITORD_PUBLISH_SSH=hacktivism
|
||||
TALER_MONITORD_PUBLISH_PATH=… single path (overrides multi default)
|
||||
TALER_MONITORD_PUBLISH_PATHS=comma-separated paths
|
||||
Default paths: public_html/stacks + public_html/monitoring.hacktivism.ch
|
||||
"""
|
||||
if os.environ.get("TALER_MONITORD_PUBLISH", "1") in ("0", "false", "no", ""):
|
||||
return None
|
||||
ssh_host = os.environ.get("TALER_MONITORD_PUBLISH_SSH", "hacktivism").strip() or "hacktivism"
|
||||
single = (os.environ.get("TALER_MONITORD_PUBLISH_PATH") or "").strip()
|
||||
multi = (os.environ.get("TALER_MONITORD_PUBLISH_PATHS") or "").strip()
|
||||
if single:
|
||||
remotes = [single]
|
||||
elif multi:
|
||||
remotes = [p.strip() for p in multi.split(",") if p.strip()]
|
||||
else:
|
||||
remotes = [
|
||||
"/home/hacktivi/public_html/stacks/index.html",
|
||||
"/home/hacktivi/public_html/monitoring.hacktivism.ch/index.html",
|
||||
]
|
||||
html_text = H.render_html(doc)
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".html", delete=False) as f:
|
||||
f.write(html_text)
|
||||
local = Path(f.name)
|
||||
published: list[str] = []
|
||||
try:
|
||||
for remote in remotes:
|
||||
got = _publish_one(local, ssh_host, remote)
|
||||
if got:
|
||||
published.append(got)
|
||||
return "; ".join(published) if published else None
|
||||
except (OSError, subprocess.TimeoutExpired) as e:
|
||||
print(f"publish error: {e}", file=sys.stderr, flush=True)
|
||||
return None
|
||||
finally:
|
||||
try:
|
||||
local.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def run_once(*, domains: Path, out_paths: list[Path] | None, public_only: bool) -> Path:
|
||||
stacks = L.parse_domains_conf(domains)
|
||||
prev = L.read_status(out_paths[0] if out_paths else None)
|
||||
if not prev and out_paths:
|
||||
for p in out_paths:
|
||||
prev = L.read_status(p)
|
||||
if prev:
|
||||
break
|
||||
doc = L.build_stack_status(stacks, prev_doc=prev, public_only=public_only)
|
||||
paths = out_paths or L.status_json_paths()
|
||||
path = L.write_stack_status(doc, paths)
|
||||
# always keep a local HTML copy too
|
||||
local_html = L.monitord_state_dir() / "index.html"
|
||||
local_html.write_text(H.render_html(doc), encoding="utf-8")
|
||||
published = publish_html(doc)
|
||||
if published:
|
||||
print(f"published {published}", flush=True)
|
||||
return path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Collect Taler stack status for the detached UI")
|
||||
ap.add_argument("--interval", type=float, default=float(os.environ.get("TALER_MONITORD_INTERVAL", "20")))
|
||||
ap.add_argument("--once", action="store_true", help="Single probe cycle then exit")
|
||||
ap.add_argument("--domains-conf", type=Path, default=None)
|
||||
ap.add_argument("--out", type=Path, action="append", default=None, help="Output JSON path (repeatable)")
|
||||
# Default on: include GOA / hacktivism.ch (taler.hacktivism.ch merchant). Opt out: --no-include-local
|
||||
_incl = os.environ.get("TALER_MONITORD_INCLUDE_LOCAL", "1") not in ("0", "false", "no", "")
|
||||
ap.add_argument(
|
||||
"--include-local",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=_incl,
|
||||
help="Probe local=1 stacks (hacktivism GOA). Default on; --no-include-local to skip",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
if _ENV_LOADED:
|
||||
print(f"env {_ENV_LOADED}", flush=True)
|
||||
domains = args.domains_conf or L.default_domains_conf()
|
||||
if not domains.is_file():
|
||||
print(f"domains.conf not found: {domains}", file=sys.stderr)
|
||||
return 2
|
||||
public_only = not bool(args.include_local)
|
||||
out_paths = list(args.out) if args.out else None
|
||||
|
||||
if args.once:
|
||||
path = run_once(domains=domains, out_paths=out_paths, public_only=public_only)
|
||||
print(path)
|
||||
return 0
|
||||
|
||||
_write_pid()
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
path = run_once(domains=domains, out_paths=out_paths, public_only=public_only)
|
||||
print(f"ok {path}", flush=True)
|
||||
except Exception as e: # keep loop alive
|
||||
print(f"error {type(e).__name__}: {e}", file=sys.stderr, flush=True)
|
||||
time.sleep(max(1.0, float(args.interval)))
|
||||
finally:
|
||||
_clear_pid()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in a new issue