Files

196 lines
7.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""infra -- singura poarta prin care puntea Discord atinge infrastructura.
infra <host> <comanda...> 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"},
# statia de admin cu WoL pentru cluster (portul/cheia vin din ~/.ssh/config)
"oracle-prod-admin": {"addr": "oracle-prod-admin", "user": "Administrator", "prod": True,
"desc": "10.0.20.36 ca Administrator (WoL cluster)"},
}
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 <host> <comanda...>", "", "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)