#!/usr/bin/env python3 """infra -- singura poarta prin care puntea Discord atinge infrastructura. infra ruleaza comanda pe hostul din lista infra --list arata hosturile permise infra --help Hosturile sunt o lista EXPLICITA. Un host care nu e in lista este refuzat, fara incercare de rezolvare DNS. Fiecare apel este jurnalizat in ~/.claude-discord/logs/infra.log cu data, host, comanda completa si rezultat. Lista implicita poate fi inlocuita cu ~/.claude-discord/infra-hosts.json: {"pvemini": {"addr": "10.0.20.201", "user": "root", "prod": true, "desc": "nod Proxmox principal"}} Coduri de iesire proprii wrapper-ului (comenzile remote isi pastreaza codul lor): 2 utilizare gresita (lipseste hostul sau comanda) 3 host in afara listei 4 eroare de configurare (fisier de hosturi corupt) """ from __future__ import annotations import datetime import json import os import pathlib import shlex import subprocess import sys import time EXIT_USAGE = 2 EXIT_UNKNOWN_HOST = 3 EXIT_CONFIG = 4 # Lista implicita, din tabelul de retea al repo-ului (CLAUDE.md, proxmox/README.md). DEFAULT_HOSTS: dict[str, dict] = { # noduri Proxmox "pve1": {"addr": "10.0.20.200", "user": "root", "prod": True, "desc": "nod Proxmox pve1"}, "pvemini": {"addr": "10.0.20.201", "user": "root", "prod": True, "desc": "nod Proxmox principal"}, "pveelite": {"addr": "10.0.20.202", "user": "root", "prod": True, "desc": "nod Proxmox pveelite"}, # servicii interne "oracle": {"addr": "10.0.20.121", "user": "root", "prod": False, "desc": "LXC 108 Oracle XE 21c/18c"}, "flowise": {"addr": "10.0.20.161", "user": "root", "prod": False, "desc": "LXC 104 Flowise"}, "gitea": {"addr": "10.0.20.165", "user": "root", "prod": False, "desc": "LXC Gitea"}, "docker": {"addr": "10.0.20.113", "user": "root", "prod": False, "desc": "LXC 102 Docker + Portainer"}, "moltbot": {"addr": "10.0.20.173", "user": "root", "prod": False, "desc": "LXC 110 MoltBot"}, "dokploy": {"addr": "10.0.20.167", "user": "root", "prod": False, "desc": "LXC 103 Dokploy"}, # productie / clienti "oracle-prod": {"addr": "10.0.20.36", "user": "romfast", "prod": True, "desc": "server Oracle de PRODUCTIE"}, "oracle-dr": {"addr": "10.0.20.37", "user": "romfast", "prod": True, "desc": "server Oracle DR"}, "roacentral": {"addr": "10.0.20.122", "user": "romfast", "prod": True, "desc": "VM 201 Windows, IIS reverse proxy"}, "oracle-test": {"addr": "10.0.20.130", "user": "romfast", "prod": False, "desc": "VM 302 mediu de test"}, } SSH_OPTS = [ "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=10", ] def state_dir() -> pathlib.Path: override = os.environ.get("CLAUDE_DISCORD_DIR") if override: return pathlib.Path(override) return pathlib.Path.home() / ".claude-discord" def hosts_file() -> pathlib.Path: return state_dir() / "infra-hosts.json" def log_file() -> pathlib.Path: return state_dir() / "logs" / "infra.log" def load_hosts() -> dict[str, dict]: """Lista de hosturi: fisierul de pe disc daca exista, altfel cea implicita.""" path = hosts_file() if not path.is_file(): return dict(DEFAULT_HOSTS) try: data = json.loads(path.read_text(encoding="utf-8")) except (OSError, ValueError) as exc: raise SystemExit(_fail(EXIT_CONFIG, f"infra: {path} nu e JSON valid ({exc})")) if not isinstance(data, dict) or not data: raise SystemExit(_fail(EXIT_CONFIG, f"infra: {path} nu contine hosturi")) out: dict[str, dict] = {} for name, spec in data.items(): if isinstance(spec, str): spec = {"addr": spec} if not isinstance(spec, dict) or not spec.get("addr"): raise SystemExit(_fail(EXIT_CONFIG, f"infra: intrare invalida pentru '{name}'")) out[str(name)] = { "addr": str(spec["addr"]), "user": str(spec.get("user") or "root"), "prod": bool(spec.get("prod", False)), "desc": str(spec.get("desc") or ""), } return out def _fail(code: int, msg: str) -> int: sys.stderr.write(msg + "\n") return code def log(host: str, target: str, cmd: list[str], rc, dur_s: float, note: str = "") -> None: """Jurnal pe o linie: data, host, comanda completa, rezultat.""" line = ( f"{datetime.datetime.now().isoformat(timespec='seconds')}\t" f"host={host}\ttarget={target}\trc={rc}\tdur={dur_s:.2f}s\t" f"cmd={shlex.join(cmd) if cmd else ''}" ) if note: line += f"\tnote={note}" try: path = log_file() path.parent.mkdir(parents=True, exist_ok=True) with open(path, "a", encoding="utf-8") as fh: fh.write(line + "\n") except OSError: pass # un jurnal care nu se poate scrie nu opreste comanda def usage(hosts: dict[str, dict]) -> str: lines = ["infra ", "", "Hosturi permise:"] for name, spec in sorted(hosts.items()): flag = " [PRODUCTIE]" if spec.get("prod") else "" lines.append(f" {name:<13} {spec['user']}@{spec['addr']:<13} {spec.get('desc','')}{flag}") lines.append("") lines.append(f"Lista se poate inlocui prin {hosts_file()}") return "\n".join(lines) def main(argv: list[str]) -> int: hosts = load_hosts() if not argv or argv[0] in ("-h", "--help"): print(usage(hosts)) return 0 if argv else EXIT_USAGE if argv[0] in ("-l", "--list"): print(usage(hosts)) return 0 host = argv[0] cmd = argv[1:] if host not in hosts: log(host, "-", cmd, "refuzat", 0.0, note="host in afara listei") return _fail( EXIT_UNKNOWN_HOST, f"infra: host necunoscut '{host}'. Hosturi permise: " + ", ".join(sorted(hosts)), ) if not cmd: return _fail(EXIT_USAGE, f"infra: lipseste comanda pentru '{host}'") spec = hosts[host] target = f"{spec['user']}@{spec['addr']}" ssh_argv = ["ssh", *SSH_OPTS, target, "--", *cmd] if os.environ.get("INFRA_DRY_RUN"): log(host, target, cmd, "dry-run", 0.0, note="INFRA_DRY_RUN") print(shlex.join(ssh_argv)) return 0 t0 = time.monotonic() try: proc = subprocess.run(ssh_argv) rc = proc.returncode except FileNotFoundError: log(host, target, cmd, "eroare", time.monotonic() - t0, note="ssh lipseste") return _fail(EXIT_CONFIG, "infra: `ssh` nu exista in PATH") except KeyboardInterrupt: log(host, target, cmd, "intrerupt", time.monotonic() - t0) return 130 log(host, target, cmd, rc, time.monotonic() - t0) return rc if __name__ == "__main__": try: sys.exit(main(sys.argv[1:])) except SystemExit: raise except Exception as exc: # nimic nu iese neraportat sys.stderr.write(f"infra: eroare interna: {exc}\n") sys.exit(1)