feat(discord-bridge): registru infra_actions (stare cluster + actiuni de mentenanta)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KddsXCqEbKMhdFJDYbAsx8
This commit is contained in:
Claude Agent
2026-09-13 10:18:12 +00:00
parent d82ac4e9c3
commit dc61c43b32
4 changed files with 1190 additions and 1 deletions

View File

@@ -163,8 +163,13 @@ def test_forma_scurta_addr_ca_string(home):
def test_toate_hosturile_implicite_au_adresa():
mod = load_infra()
# oracle-prod-admin e un alias din ~/.ssh/config (portul/cheia vin de acolo),
# nu o adresa IP -- restul hosturilor raman IP-uri clasice.
for name, spec in mod.DEFAULT_HOSTS.items():
assert spec["addr"].count(".") == 3, name
if name == "oracle-prod-admin":
assert spec["addr"], name
else:
assert spec["addr"].count(".") == 3, name
assert spec["user"], name
# hosturile de productie sunt marcate ca atare
assert mod.DEFAULT_HOSTS["oracle-prod"]["prod"] is True

View File

@@ -0,0 +1,342 @@
"""Teste pentru infra_actions.py (WP4).
Fara retea: subprocess.run e inlocuit peste tot cu un dublu. Singurele lucruri
verificate impotriva realitatii sunt forma registrului ACTIONS si logica pura
(check, parse_status).
"""
from __future__ import annotations
import pathlib
import subprocess
import sys
import pytest
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
import infra_actions as ia # noqa: E402
def _cp(stdout: str = "", rc: int = 0, stderr: str = "") -> subprocess.CompletedProcess:
return subprocess.CompletedProcess(args=[], returncode=rc, stdout=stdout, stderr=stderr)
@pytest.fixture(autouse=True)
def _sin_cache():
"""Fiecare test porneste fara cache-ul de status intre ele."""
ia._cache["data"] = None
ia._cache["at"] = 0.0
yield
ia._cache["data"] = None
ia._cache["at"] = 0.0
# ------------------------------------------------------------------ registru
def test_id_uri_valide():
for aid in ia.ACTIONS:
assert ia._VALID_ID.match(aid), aid
def test_chei_obligatorii():
required = {"label", "desc", "host", "cmd", "dry", "job", "confirm", "pre"}
for aid, spec in ia.ACTIONS.items():
assert required <= spec.keys(), aid
def test_nimic_fara_dry_run_pe_ups_shutdown():
for aid, spec in ia.ACTIONS.items():
cmds = [c for c in (spec["cmd"], spec["dry"]) if c]
for c in cmds:
if "ups-shutdown-cluster.sh" in c:
assert "--dry-run" in c, aid
def test_nimic_cu_vm201():
for aid, spec in ia.ACTIONS.items():
for c in (spec["cmd"], spec["dry"]):
if c:
assert "vm201" not in c.lower(), aid
def test_confirm_pentru_tot_ce_modifica_real():
# singurele actiuni fara confirmare sunt cele read-only / fara efect real
fara_confirm = {aid for aid, s in ia.ACTIONS.items() if not s["confirm"]}
assert fara_confirm == {"stare", "sarcini", "wol", "ups-simulare", "ups-istoric"}
def test_oprire_pornire_au_yes():
assert "--yes" in ia.ACTIONS["oprire"]["cmd"]
assert "--yes" in ia.ACTIONS["pornire"]["cmd"]
assert "--dry-run" in ia.ACTIONS["oprire"]["dry"]
assert "--dry-run" in ia.ACTIONS["pornire"]["dry"]
# ----------------------------------------------------------------------- run
def test_run_id_necunoscut_fara_subprocess(monkeypatch):
apelat = []
monkeypatch.setattr(subprocess, "run", lambda *a, **k: apelat.append(a) or _cp())
res = ia.run("rm -rf /", False)
assert res == {"ok": False, "code": 400, "error": "actiune necunoscuta: rm -rf /"}
assert apelat == []
def _st_full(**overrides) -> dict:
st = {
"nodes": [
{"name": "pve1", "online": True}, {"name": "pvemini", "online": True},
{"name": "pveelite", "online": True},
],
"quorum": {"quorate": True, "votes": 3, "expected": 3},
"jobs": [], "ups": {"status": "OL", "charge": 100.0, "runtime_s": None},
"vm109": {"node": "pveelite", "status": "stopped"},
"dr_failover_active": False,
}
st.update(overrides)
return st
def test_run_oprire_dry_run_argv(monkeypatch):
monkeypatch.setattr(ia, "status", lambda fresh=False: _st_full())
vazut = {}
def fake_run(argv, **kw):
vazut["argv"] = argv
return _cp(stdout="")
monkeypatch.setattr(subprocess, "run", fake_run)
res = ia.run("oprire", True)
assert res["ok"] is True
argv = vazut["argv"]
assert "--dry-run" in argv[-1]
assert ia.ACTIONS["oprire"]["dry"].split()[0] in argv[-1]
def test_run_precondictie_esuata_412(monkeypatch):
monkeypatch.setattr(ia, "status", lambda fresh=False: _st_full(
nodes=[{"name": "pve1", "online": False}, {"name": "pvemini", "online": True},
{"name": "pveelite", "online": True}]))
apelat = []
monkeypatch.setattr(subprocess, "run", lambda *a, **k: apelat.append(a) or _cp())
res = ia.run("oprire", False)
assert res["code"] == 412
assert res["ok"] is False
assert apelat == [] # precondictia pica inainte de orice subprocess
def test_run_backup_pe_pvemini_blocat_cu_pveelite_online(monkeypatch):
monkeypatch.setattr(ia, "status", lambda fresh=False: _st_full())
res = ia.run("backup-pe-pvemini", False)
assert res["code"] == 412
def test_run_backup_pe_pveelite_blocat_fara_failover_activ(monkeypatch):
monkeypatch.setattr(ia, "status", lambda fresh=False: _st_full(dr_failover_active=False))
res = ia.run("backup-pe-pveelite", False)
assert res["code"] == 412
def test_run_vm109_pornire_blocat_cu_job_dr_activ(monkeypatch):
monkeypatch.setattr(ia, "status", lambda fresh=False: _st_full(
jobs=[{"id": "dr-test", "host": "pveelite", "active": True, "log": None}]))
res = ia.run("vm109-pornire", False)
assert res["code"] == 412
def test_run_wol_blocat_cu_toate_online(monkeypatch):
monkeypatch.setattr(ia, "status", lambda fresh=False: _st_full())
res = ia.run("wol", False)
assert res["code"] == 412
def test_run_job_lansat_prin_wrapper(monkeypatch):
monkeypatch.setattr(ia, "status", lambda fresh=False: _st_full())
monkeypatch.setattr(ia, "post_startup_instructions", lambda: None)
vazut = {}
def fake_run(argv, **kw):
vazut["argv"] = argv
return _cp(stdout="/var/log/infra-actions/ups-test-20260913-120000.log\n")
monkeypatch.setattr(subprocess, "run", fake_run)
res = ia.run("ups-test", False)
assert res["ok"] is True
assert res["code"] == 200
assert res["log"].endswith(".log")
argv = vazut["argv"]
assert argv[0] == sys.executable
assert argv[1] == ia.INFRA_BIN
assert argv[2] == "pvemini"
assert "systemd-run" in argv[3]
assert "infra-ups-test" in argv[3]
def test_run_job_deja_activ_409(monkeypatch):
monkeypatch.setattr(ia, "status", lambda fresh=False: _st_full())
monkeypatch.setattr(ia, "post_startup_instructions", lambda: None)
monkeypatch.setattr(subprocess, "run", lambda *a, **k: _cp(rc=1, stderr="Unit infra-ups-test.service already exists"))
res = ia.run("ups-test", False)
assert res["code"] == 409
def test_run_dry_fara_simulare_400(monkeypatch):
monkeypatch.setattr(ia, "status", lambda fresh=False: _st_full())
res = ia.run("ups-test", True)
assert res["code"] == 400
def test_run_vm109_host_rezolvat_din_status(monkeypatch):
monkeypatch.setattr(ia, "status", lambda fresh=False: _st_full(vm109={"node": "pveelite", "status": "stopped"}))
monkeypatch.setattr(ia, "post_startup_instructions", lambda: None)
vazut = {}
def fake_run(argv, **kw):
vazut["argv"] = argv
return _cp(stdout="x.log")
monkeypatch.setattr(subprocess, "run", fake_run)
ia.run("dr-test", False)
assert vazut["argv"][2] == "pveelite"
def test_stop_id_necunoscut():
res = ia.stop("nu-exista")
assert res == {"ok": False, "code": 400, "error": "actiune necunoscuta"}
# --------------------------------------------------------------------- check
def test_check_oprire_ok():
ok, reason = ia.check("oprire", _st_full(), False)
assert ok is True and reason == ""
def test_check_pornire_blocat_de_job():
ok, reason = ia.check("pornire", _st_full(jobs=[{"id": "oprire", "active": True}]), False)
assert ok is False
def test_check_ups_test_charge_mic():
ok, _ = ia.check("ups-test", _st_full(ups={"status": "OL", "charge": 40.0, "runtime_s": None}), False)
assert ok is False
def test_check_id_necunoscut():
ok, reason = ia.check("nu-exista", _st_full(), False)
assert ok is False
assert "necunoscuta" in reason
# ---------------------------------------------------------------- parse_status
def test_parse_status_host_cu_timeout_nu_arunca():
outputs = {
"pve1": subprocess.TimeoutExpired(cmd="ssh", timeout=20),
"pvemini": "",
"pveelite": "",
}
st = ia.parse_status(outputs)
assert "pve1" in st["errors"]
assert st["nodes"] == [
{"name": "pve1", "ip": "10.0.20.200", "online": False, "cpu": 0.0, "mem": 0, "maxmem": 0, "uptime": 0, "guests": 0},
{"name": "pvemini", "ip": "10.0.20.201", "online": True, "cpu": 0.0, "mem": 0, "maxmem": 0, "uptime": 0, "guests": 0},
{"name": "pveelite", "ip": "10.0.20.202", "online": True, "cpu": 0.0, "mem": 0, "maxmem": 0, "uptime": 0, "guests": 0},
]
def test_parse_status_cluster_json():
cluster = (
'[{"type":"node","node":"pvemini","status":"online","cpu":0.1,"mem":100,"maxmem":200,"uptime":10},'
'{"type":"node","node":"pve1","status":"online","cpu":0.0,"mem":0,"maxmem":0,"uptime":0},'
'{"type":"node","node":"pveelite","status":"online","cpu":0.0,"mem":0,"maxmem":0,"uptime":0},'
'{"type":"qemu","vmid":109,"node":"pveelite","status":"stopped"},'
'{"type":"lxc","vmid":171,"node":"pvemini","status":"running"}]'
)
outputs = {
"pve1": "", "pveelite": "",
"pvemini": f"@@cluster\n{cluster}\n@@quorum\nQuorate: Yes\nTotal votes: 3\nExpected votes: 3\n",
}
st = ia.parse_status(outputs)
pvemini = next(n for n in st["nodes"] if n["name"] == "pvemini")
assert pvemini["guests"] == 1
assert pvemini["cpu"] == 0.1
assert st["vm109"] == {"node": "pveelite", "status": "stopped"}
assert st["guests"] == {"running": 1, "total": 2}
assert st["quorum"] == {"quorate": True, "votes": 3, "expected": 3}
def test_parse_status_ups_si_zfs():
outputs = {
"pve1": "", "pveelite": "",
"pvemini": "@@ups\nups.status: OL\nbattery.charge: 97\n@@zfs\noff\n@@maint\n1\n",
}
st = ia.parse_status(outputs)
assert st["ups"] == {"status": "OL", "charge": 97.0, "runtime_s": None}
assert st["dr_failover_active"] is True
assert st["maint"] == 1
def test_parse_status_jobs():
outputs = {
"pve1": "", "pveelite": "",
"pvemini": (
"@@jobs\n"
"infra-ups-test.service loaded active running /bin/bash -c ...\n"
"ups-test-20260913-120000.log\n"
"oprire-20260901-010000.log\n"
),
}
st = ia.parse_status(outputs)
by_id = {j["id"]: j for j in st["jobs"]}
assert by_id["ups-test"]["active"] is True
assert by_id["ups-test"]["log"].endswith("ups-test-20260913-120000.log")
assert by_id["oprire"]["active"] is False
def test_parse_status_fara_date_e_sigur():
st = ia.parse_status({"pve1": Exception("boom"), "pvemini": Exception("x"), "pveelite": Exception("y")})
assert st["ups"] is None
assert st["ssl"] is None
assert st["backup"] is None
assert st["vm109"] is None
assert st["dr_failover_active"] is None
assert set(st["errors"]) == {"pve1", "pvemini", "pveelite"}
# ------------------------------------------------------------- maintenance
def test_maintenance_pending_true():
st = _st_full()
st["maint"] = 1
assert ia.maintenance_pending(st) is True
def test_maintenance_pending_false_fara_marcaj():
st = _st_full()
st["maint"] = 0
assert ia.maintenance_pending(st) is False
def test_maintenance_pending_false_cu_job_activ():
st = _st_full(jobs=[{"id": "pornire", "active": True}])
st["maint"] = 1
assert ia.maintenance_pending(st) is False
# --------------------------------------------------------- post_startup_instructions
def test_post_startup_instructions_fara_config():
import config
config.reload({"__nu_exista__": True} and "/tmp/nu-exista-claude-discord-test")
# fara DISCORD_TOKEN in acest mediu izolat
import os
old = os.environ.pop("DISCORD_TOKEN", None)
try:
res = ia.post_startup_instructions()
assert res is not None
finally:
if old is not None:
os.environ["DISCORD_TOKEN"] = old
config.reload()