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:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user