SysDeck/bridge/fleet.py

117 lines
3.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""
SysDeck - Fleet Bridge Helper
Author: Jeremy Anderson (https://dcos.net)
Aggregates local host info (hostname, uptime, load, network addresses)
and the cockpit multi-host peer list (if /etc/cockpit/machines.d/ is
configured) into a single JSON document.
Usage:
python3 -m sysdeck.bridge.fleet local
python3 -m sysdeck.bridge.fleet peers
python3 -m sysdeck.bridge.fleet summary
"""
import json
import os
import re
import subprocess
import sys
from typing import Any
# /etc/cockpit/machines.d/<host>.json schema (subset).
MACHINE_FILE_RE = re.compile(r"^(?P<host>\S+)\s+.*$")
def run(argv: list[str]) -> str:
"""Run a command, returning stdout. Returns '' on failure."""
try:
return subprocess.run(
argv, capture_output=True, text=True, check=True, timeout=20,
).stdout
except (subprocess.CalledProcessError, FileNotFoundError):
return ""
def local_host() -> dict[str, Any]:
"""Aggregate hostname, uptime, load, and IP addresses."""
uptime_raw = run(["uptime"]).strip()
addresses = [a for a in run(["hostname", "-I"]).split() if a]
hostname = run(["hostname"]).strip() or "unknown"
# Parse load average from uptime output.
# Typical: " 14:23:01 up 12 days, 3:45, 2 users, load average: 0.42, 0.58, 0.61"
load_match = re.search(r"load average:\s*([\d.]+),\s*([\d.]+),\s*([\d.]+)", uptime_raw)
load = [float(load_match.group(i)) for i in (1, 2, 3)] if load_match else []
return {
"hostname": hostname,
"uptime": uptime_raw,
"addresses": addresses,
"load": load,
}
def peers() -> list[dict[str, str]]:
"""Parse /etc/cockpit/machines.d/*.json for peer host entries.
Each file is a JSON document with a top-level "host" key. Files that
fail to parse are skipped — the panel fails closed.
"""
peer_dir = "/etc/cockpit/machines.d"
if not os.path.isdir(peer_dir):
return []
peers_list: list[dict[str, str]] = []
for entry in sorted(os.listdir(peer_dir)):
if not entry.endswith(".json"):
continue
path = os.path.join(peer_dir, entry)
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
except (OSError, json.JSONDecodeError):
continue
# Schema is flexible; pull common keys.
peers_list.append({
"host": data.get("host", ""),
"address": data.get("address", data.get("host", "")),
"label": data.get("label", data.get("host", "")),
"visible": str(data.get("visible", "true")).lower(),
})
return peers_list
def summary() -> dict[str, Any]:
"""Local host info plus peer list."""
peers_list = peers()
return {
"local": local_host(),
"peers": peers_list,
"peerCount": len(peers_list),
}
COMMANDS = {
"local": lambda _args: local_host(),
"peers": lambda _args: peers(),
"summary": lambda _args: summary(),
}
def main(argv: list[str]) -> int:
if not argv or argv[0] in ("-h", "--help"):
print(__doc__)
return 0
cmd = COMMANDS.get(argv[0])
if not cmd:
print(f"Unknown subcommand: {argv[0]}", file=sys.stderr)
return 2
print(json.dumps(cmd(argv[1:]), indent=2))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))