#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# 30-pct — 42i-hc Drop-in: sammelt die LXCs eines Proxmox-Hosts.
# hc-tags: recursive, children
# hc-ttl: 30
#
# (Kein "net"-Tag: ?skip=net beim Peer-Abruf soll nur den HTTP-Collector
#  90-peers ausschalten, nicht die lokale pct-Aggregation.)
#
# Liefert {"children": [...]} — je LXC einen Knoten-Subtree:
#   - Basissicht immer billig aus `pct config`/`pct list` (name, ctid, ips,
#     state, Limits) — ohne Container-Eintritt;
#   - laeuft der Container und hat eine IP, wird der volle Subtree per HTTP von
#     http://<ip>:4711/?skip=net geholt (echt parallel, kein lxc-Lock).
#
# Bewusst HTTP statt `pct exec`: pct/lxc serialisiert exec (~2 s je Container),
# HTTP gegen die Container-IP skaliert dagegen flach. So sieht der PVE auch
# gestoppte Container, die sich selbst nicht melden koennen.
# Geliefert von 42i-pve. Schema: 42i/intern#236. Laeuft als root.

import concurrent.futures
import json
import os
import re
import subprocess
import sys
import urllib.request

PCT = os.environ.get("HC_PCT") or "pct"
LXC_CONF_DIR = os.environ.get("HC_LXC_CONF_DIR") or "/etc/pve/lxc"
HC_PORT = int(os.environ.get("HC_HC_PORT") or 4711)
HTTP_TIMEOUT = float(os.environ.get("HC_PCT_HTTP_TIMEOUT") or 4)


def pct(*args, timeout=5):
    """Ruft `pct <args>` auf und gibt stdout (oder None bei Fehler/Timeout)."""
    try:
        proc = subprocess.run([PCT, *args], capture_output=True, text=True,
                              timeout=timeout)
    except Exception:
        return None
    if proc.returncode != 0:
        return None
    return proc.stdout


def list_cts():
    """Parst `pct list` zu [(ctid, state, name), ...]."""
    out = pct("list")
    cts = []
    if not out:
        return cts
    for line in out.splitlines()[1:]:  # Kopfzeile ueberspringen
        parts = line.split()
        if len(parts) < 2 or not parts[0].isdigit():
            continue
        ctid = parts[0]
        state = parts[1].lower()
        name = parts[-1] if len(parts) >= 3 else ""
        cts.append((ctid, state, name))
    return cts


def _parse_conf(lines, cfg):
    """Fuellt cfg aus key: value-Zeilen; stoppt an der ersten Snapshot-Sektion."""
    for line in lines:
        if line.startswith("["):  # [snapshot] -> Ende des Haupt-Teils
            break
        if ":" in line:
            k, v = line.split(":", 1)
            cfg[k.strip()] = v.strip()
    return cfg


def ct_config(ctid):
    """LXC-Config als key->value-Dict — direkt aus /etc/pve/lxc/<ctid>.conf.

    Der Datei-Read ist ~7x schneller als `pct config` (kein Proxmox-Python/
    pmxcfs-Overhead). Fallback auf `pct config`, falls die Datei nicht lesbar ist.
    """
    cfg = {}
    path = os.path.join(LXC_CONF_DIR, "%s.conf" % ctid)
    try:
        with open(path, encoding="utf-8") as fh:
            return _parse_conf(fh, cfg)
    except Exception:
        out = pct("config", ctid)
        return _parse_conf(out.splitlines(), cfg) if out else cfg


def config_ips(cfg):
    """Extrahiert die statisch konfigurierten IPv4-Adressen aus net*-Eintraegen."""
    ips = []
    for key, val in cfg.items():
        if key.startswith("net"):
            m = re.search(r"ip=([0-9.]+)", val)
            if m:
                ips.append(m.group(1))
    return ips


def base_node(ctid, state, name, cfg):
    """Baut die Basissicht eines Containers ohne hc (state, Limits, IPs)."""
    host = {"name": name or cfg.get("hostname", "ct%s" % ctid),
            "ctid": ctid, "state": state, "ips": config_ips(cfg)}
    metrics = {}
    if cfg.get("cores"):
        try:
            metrics["cores"] = int(cfg["cores"])
        except ValueError:
            pass
    if cfg.get("memory"):
        try:
            metrics["mem"] = {"limit_mb": int(cfg["memory"])}
        except ValueError:
            pass
    if metrics:
        host["metrics"] = metrics
    status = "stopped" if state != "running" else "no-agent"
    host["status"] = status
    return {"schema": "42i-hc/1", "status": status, "host": host}


def agent_node_http(ctid, state, name, ips):
    """Holt den vollen Subtree per HTTP von der Container-IP (?skip=net), oder None."""
    for ip in ips:
        try:
            with urllib.request.urlopen("http://%s:%d/?skip=net" % (ip, HC_PORT),
                                        timeout=HTTP_TIMEOUT) as resp:
                node = json.loads(resp.read().decode("utf-8"))
        except Exception:
            continue
        if isinstance(node, dict):
            node.setdefault("host", {})
            node["host"].setdefault("ctid", ctid)
            node["host"]["state"] = state
            return node
    return None


def collect_one(ct):
    """Sammelt einen Container: voller Subtree per HTTP (laufend + hc) oder Basissicht."""
    ctid, state, name = ct
    cfg = ct_config(ctid)
    ips = config_ips(cfg)
    node = None
    if state == "running" and ips:
        try:
            node = agent_node_http(ctid, state, name, ips)
        except Exception:
            node = None
    if node is None:
        node = base_node(ctid, state, name, cfg)
    return node


def main():
    """Sammelt alle LXCs des Hosts parallel und gibt {"children": [...]} aus."""
    cts = list_cts()
    children = []
    if cts:
        with concurrent.futures.ThreadPoolExecutor(max_workers=min(32, len(cts))) as ex:
            children = list(ex.map(collect_one, cts))  # Reihenfolge bleibt erhalten
    json.dump({"children": children}, sys.stdout, ensure_ascii=False)
    sys.stdout.write("\n")


if __name__ == "__main__":
    main()
