feat(discord-bridge): comanda /infra cu confirmare prin butoane
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KddsXCqEbKMhdFJDYbAsx8
This commit is contained in:
@@ -24,6 +24,7 @@ from dataclasses import dataclass, field
|
|||||||
|
|
||||||
import commands_slash
|
import commands_slash
|
||||||
import config
|
import config
|
||||||
|
import infra_actions
|
||||||
import limits as limits_mod
|
import limits as limits_mod
|
||||||
import render
|
import render
|
||||||
import runner as runner_mod
|
import runner as runner_mod
|
||||||
@@ -355,7 +356,7 @@ class Command:
|
|||||||
return " ".join(self.args)
|
return " ".join(self.args)
|
||||||
|
|
||||||
|
|
||||||
COMMANDS = ("new", "cd", "model", "status", "stop", "cleanup", "permisiuni", "help")
|
COMMANDS = ("new", "cd", "model", "status", "stop", "cleanup", "permisiuni", "help", "infra")
|
||||||
|
|
||||||
# Optiunile slash traduse in argumentele pe care cmd_* le stiu deja.
|
# Optiunile slash traduse in argumentele pe care cmd_* le stiu deja.
|
||||||
SLASH_FLAGS = {
|
SLASH_FLAGS = {
|
||||||
@@ -367,6 +368,9 @@ SLASH_FLAGS = {
|
|||||||
|
|
||||||
def slash_args(name: str, opts: dict) -> list[str]:
|
def slash_args(name: str, opts: dict) -> list[str]:
|
||||||
"""`/new fork:True` -> `["--fork"]`, `/cd cale:/x` -> `["/x"]`."""
|
"""`/new fork:True` -> `["--fork"]`, `/cd cale:/x` -> `["/x"]`."""
|
||||||
|
if name == "infra":
|
||||||
|
s = opts.get("simulare")
|
||||||
|
return [opts.get("actiune", "")] + (["simulare"] if s is True else ["real"] if s is False else [])
|
||||||
flag = SLASH_FLAGS.get(name)
|
flag = SLASH_FLAGS.get(name)
|
||||||
if flag is not None:
|
if flag is not None:
|
||||||
return [flag[1]] if opts.get(flag[0]) else []
|
return [flag[1]] if opts.get(flag[0]) else []
|
||||||
@@ -444,6 +448,60 @@ def _reset_text(resets_at: int) -> str:
|
|||||||
return "?"
|
return "?"
|
||||||
|
|
||||||
|
|
||||||
|
def infra_status_text(st: dict) -> str:
|
||||||
|
"""Rezumatul `/infra stare`: cvorum, noduri, HA, replicare, UPS, backup, SSL."""
|
||||||
|
quorum = st.get("quorum", {})
|
||||||
|
guests = st.get("guests", {})
|
||||||
|
ha = st.get("ha", [])
|
||||||
|
repl = st.get("replication", {})
|
||||||
|
ups = st.get("ups")
|
||||||
|
backup = st.get("backup")
|
||||||
|
ssl = st.get("ssl")
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"**Stare cluster**",
|
||||||
|
"```",
|
||||||
|
f"cvorum : {'da' if quorum.get('quorate') else 'NU'} ({quorum.get('votes', 0)}/{quorum.get('expected', 0)})",
|
||||||
|
f"guest-uri: {guests.get('running', 0)}/{guests.get('total', 0)} pornite",
|
||||||
|
f"HA : {len(ha)} servicii" + (
|
||||||
|
f", {sum(1 for h in ha if h.get('state') != 'started')} nu sunt 'started'" if ha else ""
|
||||||
|
),
|
||||||
|
f"replicare: {repl.get('ok', 0)}/{repl.get('total', 0)} ok" + (
|
||||||
|
f", esuate: {', '.join(repl.get('failed', []))}" if repl.get("failed") else ""
|
||||||
|
),
|
||||||
|
]
|
||||||
|
if ups:
|
||||||
|
lines.append(f"UPS : {ups.get('status', '?')}, incarcare {ups.get('charge', 0):.0f}%")
|
||||||
|
if backup:
|
||||||
|
full, cum = backup.get("full_age_h"), backup.get("cum_age_h")
|
||||||
|
lines.append(
|
||||||
|
"backup : FULL " + (f"{full:.1f}h" if full is not None else "?")
|
||||||
|
+ ", CUM " + (f"{cum:.1f}h" if cum is not None else "?")
|
||||||
|
)
|
||||||
|
if ssl:
|
||||||
|
lines.append(f"SSL : {ssl.get('domain')} expira in {ssl.get('min_days')} zile")
|
||||||
|
for n in st.get("nodes", []):
|
||||||
|
lines.append(f" {n['name']:<9}: {'online' if n['online'] else 'OFFLINE'}, {n.get('guests', 0)} guest-uri")
|
||||||
|
lines.append("```")
|
||||||
|
if st.get("errors"):
|
||||||
|
lines.append("⚠️ erori: " + ", ".join(f"{h}: {e}" for h, e in st["errors"].items()))
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def infra_result_text(label: str, result: dict) -> str:
|
||||||
|
"""Mesajul din `run()`/`stop()` al infra_actions, gata de trimis in fir."""
|
||||||
|
if not result.get("ok"):
|
||||||
|
return f"⛔ {label} a esuat (cod {result.get('code')}): {result.get('error', '?')}"
|
||||||
|
parts = [f"✅ {label} pornit."]
|
||||||
|
if result.get("log"):
|
||||||
|
parts.append(f"Jurnal: `{result['log']}`")
|
||||||
|
if result.get("output"):
|
||||||
|
parts.append(f"```\n{result['output'][:1500]}\n```")
|
||||||
|
if result.get("warning"):
|
||||||
|
parts.append(f"⚠️ {result['warning']}")
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
def _tool_line(ev) -> str:
|
def _tool_line(ev) -> str:
|
||||||
if isinstance(ev, stream_mod.ToolUse):
|
if isinstance(ev, stream_mod.ToolUse):
|
||||||
inp = ev.input or {}
|
inp = ev.input or {}
|
||||||
@@ -493,6 +551,7 @@ class Bridge:
|
|||||||
# (abonament, nu API), fereastra de utilizare e singura limita reala.
|
# (abonament, nu API), fereastra de utilizare e singura limita reala.
|
||||||
self.rate_limit = None
|
self.rate_limit = None
|
||||||
self._rate_limit_warned: str | None = None
|
self._rate_limit_warned: str | None = None
|
||||||
|
self._infra_boot_task: asyncio.Task | None = None
|
||||||
|
|
||||||
# ------------------------------------------------------------ iesire
|
# ------------------------------------------------------------ iesire
|
||||||
async def _edit(self, target, text: str) -> None:
|
async def _edit(self, target, text: str) -> None:
|
||||||
@@ -541,9 +600,16 @@ class Bridge:
|
|||||||
await self.say(channel, f"⚠️ {item.get('warning')}")
|
await self.say(channel, f"⚠️ {item.get('warning')}")
|
||||||
self.runner.start_reaper()
|
self.runner.start_reaper()
|
||||||
self.wire_approvals()
|
self.wire_approvals()
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
self._infra_boot_task = asyncio.create_task(self.infra_boot_check())
|
||||||
return lost
|
return lost
|
||||||
|
|
||||||
async def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
|
if self._infra_boot_task is not None:
|
||||||
|
self._infra_boot_task.cancel()
|
||||||
|
with contextlib.suppress(Exception, asyncio.CancelledError):
|
||||||
|
await self._infra_boot_task
|
||||||
|
self._infra_boot_task = None
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
await self.render.stop_all()
|
await self.render.stop_all()
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
@@ -724,6 +790,7 @@ class Bridge:
|
|||||||
"`/status` starea firului · `/stop` opreste turul curent\n"
|
"`/status` starea firului · `/stop` opreste turul curent\n"
|
||||||
"`/cleanup [force:True]` procese lasate in urma\n"
|
"`/cleanup [force:True]` procese lasate in urma\n"
|
||||||
"`/permisiuni [revoca:True]` ce s-a aprobat pentru tot firul\n"
|
"`/permisiuni [revoca:True]` ce s-a aprobat pentru tot firul\n"
|
||||||
|
"`/infra actiune:<...>` infrastructura: stare, oprire/pornire cluster, UPS, DR\n"
|
||||||
"Orice alt mesaj din canal pleaca la Claude ca prompt.",
|
"Orice alt mesaj din canal pleaca la Claude ca prompt.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -863,6 +930,171 @@ class Bridge:
|
|||||||
lines += ["```", "`/permisiuni revoca` le sterge pe toate."]
|
lines += ["```", "`/permisiuni revoca` le sterge pe toate."]
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- infra
|
||||||
|
async def cmd_infra(self, message, tid: str, cmd: Command) -> None:
|
||||||
|
if not cmd.args:
|
||||||
|
await self.say(message.channel, "Foloseste `/infra actiune:<...>`.")
|
||||||
|
return
|
||||||
|
action_id = cmd.args[0]
|
||||||
|
spec = infra_actions.ACTIONS.get(action_id)
|
||||||
|
if spec is None:
|
||||||
|
await self.say(message.channel, f"⛔ Actiune necunoscuta: `{action_id}`.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if action_id == "stare":
|
||||||
|
st = await asyncio.to_thread(infra_actions.status, True)
|
||||||
|
out = infra_status_text(st)
|
||||||
|
await self.say(message.channel, out)
|
||||||
|
return
|
||||||
|
|
||||||
|
if action_id == "sarcini":
|
||||||
|
st = await asyncio.to_thread(infra_actions.status, True)
|
||||||
|
await self.say(message.channel, await self.infra_jobs_text(st))
|
||||||
|
return
|
||||||
|
|
||||||
|
dry = ("real" not in cmd.args) if spec["dry"] else False
|
||||||
|
st = await asyncio.to_thread(infra_actions.status, True)
|
||||||
|
ok, reason = infra_actions.check(action_id, st, dry)
|
||||||
|
if not ok:
|
||||||
|
await self.say(message.channel, f"⛔ {spec['label']}: {reason}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if spec["confirm"] and not dry:
|
||||||
|
text = (
|
||||||
|
f"⚠️ **{spec['label']}** — confirmi?\n{spec['desc']}\n"
|
||||||
|
f"Precondiție: {spec['pre']}"
|
||||||
|
)
|
||||||
|
if action_id == "oprire":
|
||||||
|
text += "\nInstructiunile de pornire se fixeaza in canal la finalul opririi."
|
||||||
|
view = self.infra_view(action_id, dry)
|
||||||
|
if view is not None:
|
||||||
|
await message.channel.send(text, view=view)
|
||||||
|
else:
|
||||||
|
await self.say(message.channel, text + "\n(fara discord.py, nu pot arata butoane de confirmare)")
|
||||||
|
return
|
||||||
|
|
||||||
|
result = await asyncio.to_thread(infra_actions.run, action_id, dry)
|
||||||
|
await self.say(message.channel, infra_result_text(spec["label"], result))
|
||||||
|
|
||||||
|
async def infra_jobs_text(self, st: dict) -> str:
|
||||||
|
jobs = [j for j in st.get("jobs", []) if j.get("active")]
|
||||||
|
if not jobs:
|
||||||
|
return "Niciun job activ."
|
||||||
|
parts = []
|
||||||
|
for j in jobs:
|
||||||
|
tail = await asyncio.to_thread(infra_actions.log_tail, j["id"], 15)
|
||||||
|
lines = "\n".join(tail.get("lines", []))
|
||||||
|
parts.append(f"**{j['id']}** ({j['host']})\n```\n{lines}\n```")
|
||||||
|
text = "\n".join(parts)
|
||||||
|
return text if len(text) <= 1800 else text[:1800] + "\n… (trunchiat)"
|
||||||
|
|
||||||
|
def infra_view(self, action_id: str, dry: bool):
|
||||||
|
"""View cu Confirma / Anuleaza pentru o actiune infra. Fara discord.py: None."""
|
||||||
|
if discord is None:
|
||||||
|
return None
|
||||||
|
spec = infra_actions.ACTIONS[action_id]
|
||||||
|
view = discord.ui.View(timeout=300)
|
||||||
|
|
||||||
|
async def _confirm_cb(interaction): # pragma: no cover - are nevoie de Discord real
|
||||||
|
if str(interaction.user.id) not in user_ids():
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await interaction.response.send_message("⛔ Nu esti autorizat.", ephemeral=True)
|
||||||
|
return
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await interaction.response.edit_message(content=f"⏳ {spec['label']}…", view=None)
|
||||||
|
st = await asyncio.to_thread(infra_actions.status, True)
|
||||||
|
ok, reason = infra_actions.check(action_id, st, dry)
|
||||||
|
if not ok:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await interaction.edit_original_response(content=f"⛔ {spec['label']}: {reason}")
|
||||||
|
return
|
||||||
|
result = await asyncio.to_thread(infra_actions.run, action_id, dry)
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await interaction.edit_original_response(content=infra_result_text(spec["label"], result))
|
||||||
|
|
||||||
|
async def _cancel_cb(interaction): # pragma: no cover - are nevoie de Discord real
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await interaction.response.edit_message(content=f"Anulat: {spec['label']}.", view=None)
|
||||||
|
|
||||||
|
confirm = discord.ui.Button(label="Confirma", style=discord.ButtonStyle.danger)
|
||||||
|
cancel = discord.ui.Button(label="Anuleaza", style=discord.ButtonStyle.secondary)
|
||||||
|
confirm.callback = _confirm_cb
|
||||||
|
cancel.callback = _cancel_cb
|
||||||
|
view.add_item(confirm)
|
||||||
|
view.add_item(cancel)
|
||||||
|
return view
|
||||||
|
|
||||||
|
def infra_boot_view(self):
|
||||||
|
"""View pentru reamintirea de pornire la boot: Porneste / Simulare / Nu acum."""
|
||||||
|
if discord is None:
|
||||||
|
return None
|
||||||
|
view = discord.ui.View(timeout=None)
|
||||||
|
label = infra_actions.ACTIONS["pornire"]["label"]
|
||||||
|
|
||||||
|
async def _run(interaction, dry: bool): # pragma: no cover - are nevoie de Discord real
|
||||||
|
if str(interaction.user.id) not in user_ids():
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await interaction.response.send_message("⛔ Nu esti autorizat.", ephemeral=True)
|
||||||
|
return
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await interaction.response.edit_message(content=f"⏳ {label}…", view=None)
|
||||||
|
result = await asyncio.to_thread(infra_actions.run, "pornire", dry)
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await interaction.edit_original_response(content=infra_result_text(label, result))
|
||||||
|
|
||||||
|
async def _skip_cb(interaction): # pragma: no cover - are nevoie de Discord real
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await interaction.response.edit_message(view=None)
|
||||||
|
|
||||||
|
start = discord.ui.Button(label="Porneste guest-urile", style=discord.ButtonStyle.success)
|
||||||
|
sim = discord.ui.Button(label="Simulare", style=discord.ButtonStyle.secondary)
|
||||||
|
skip = discord.ui.Button(label="Nu acum", style=discord.ButtonStyle.secondary)
|
||||||
|
start.callback = lambda interaction: _run(interaction, False)
|
||||||
|
sim.callback = lambda interaction: _run(interaction, True)
|
||||||
|
skip.callback = _skip_cb
|
||||||
|
view.add_item(start)
|
||||||
|
view.add_item(sim)
|
||||||
|
view.add_item(skip)
|
||||||
|
return view
|
||||||
|
|
||||||
|
async def infra_boot_check(self) -> None:
|
||||||
|
"""La pornire: daca clusterul e deja sus dar inca in mentenanta (repornit
|
||||||
|
manual, crontab-ul inca poarta #MENTENANTA), aminteste sa se dea /infra
|
||||||
|
pornire. Nu are voie sa doboare botul: orice eroare ramane doar in log."""
|
||||||
|
try:
|
||||||
|
for attempt in range(20):
|
||||||
|
try:
|
||||||
|
st = await asyncio.to_thread(infra_actions.status, True)
|
||||||
|
except Exception:
|
||||||
|
log.exception("infra_boot_check: status() a esuat")
|
||||||
|
st = None
|
||||||
|
if st is not None:
|
||||||
|
if attempt == 0 and not st.get("maint", 0):
|
||||||
|
return
|
||||||
|
if infra_actions.maintenance_pending(st):
|
||||||
|
await self._infra_post_maint_reminder(st)
|
||||||
|
return
|
||||||
|
await asyncio.sleep(30)
|
||||||
|
except Exception: # pragma: no cover - plasa de siguranta
|
||||||
|
log.exception("infra_boot_check a esuat")
|
||||||
|
|
||||||
|
async def _infra_post_maint_reminder(self, st: dict) -> None:
|
||||||
|
chans = config.get_list("DISCORD_CHANNEL_IDS") or config.get_list("DISCORD_CHANNEL_ID")
|
||||||
|
if not chans:
|
||||||
|
return
|
||||||
|
channel = self.channel_of(chans[0])
|
||||||
|
if channel is None:
|
||||||
|
return
|
||||||
|
guests = st.get("guests", {})
|
||||||
|
n = max(0, int(guests.get("total", 0)) - int(guests.get("running", 0)))
|
||||||
|
text = f"🟢 Cluster sus — 3/3 noduri · {n} guest-uri de pornit."
|
||||||
|
view = self.infra_boot_view()
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
if view is not None:
|
||||||
|
await channel.send(text, view=view)
|
||||||
|
else:
|
||||||
|
await channel.send(text)
|
||||||
|
|
||||||
async def cmd_stop(self, message, tid: str, cmd: Command) -> None:
|
async def cmd_stop(self, message, tid: str, cmd: Command) -> None:
|
||||||
proc = self.runner.procs.get(tid)
|
proc = self.runner.procs.get(tid)
|
||||||
if proc is None or not proc.alive:
|
if proc is None or not proc.alive:
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import infra_actions
|
||||||
|
|
||||||
log = logging.getLogger("discord-bridge.slash")
|
log = logging.getLogger("discord-bridge.slash")
|
||||||
|
|
||||||
try: # pragma: no cover - depinde de mediu
|
try: # pragma: no cover - depinde de mediu
|
||||||
@@ -82,6 +84,14 @@ def build_tree(client, bridge):
|
|||||||
async def _help(interaction):
|
async def _help(interaction):
|
||||||
await bridge.handle_slash(interaction, "help")
|
await bridge.handle_slash(interaction, "help")
|
||||||
|
|
||||||
|
@tree.command(name="infra", description="Infrastructura: stare, oprire/pornire cluster, UPS, DR")
|
||||||
|
@app_commands.describe(actiune="Ce faci", simulare="Doar simulare (implicit da unde exista)")
|
||||||
|
@app_commands.choices(actiune=[
|
||||||
|
app_commands.Choice(name=a["label"][:100], value=k) for k, a in infra_actions.ACTIONS.items()
|
||||||
|
])
|
||||||
|
async def _infra(interaction, actiune: app_commands.Choice[str], simulare: bool | None = None):
|
||||||
|
await bridge.handle_slash(interaction, "infra", actiune=actiune.value, simulare=simulare)
|
||||||
|
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import bot
|
import bot
|
||||||
@@ -345,3 +347,156 @@ async def test_sync_fara_arbore_sau_fara_guilduri_nu_arunca():
|
|||||||
|
|
||||||
assert await commands_slash.sync_guilds(None, ["100"]) == {}
|
assert await commands_slash.sync_guilds(None, ["100"]) == {}
|
||||||
assert await commands_slash.sync_guilds(_FakeTree(), []) == {}
|
assert await commands_slash.sync_guilds(_FakeTree(), []) == {}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------- /infra
|
||||||
|
import infra_actions # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _st(**over) -> dict:
|
||||||
|
base = {
|
||||||
|
"quorum": {"quorate": True, "votes": 3, "expected": 3},
|
||||||
|
"guests": {"running": 2, "total": 3},
|
||||||
|
"ha": [], "replication": {"ok": 0, "total": 0, "last": 0, "failed": []},
|
||||||
|
"ups": None, "backup": None, "ssl": None, "vm109": None,
|
||||||
|
"dr_failover_active": None, "maint": 0, "jobs": [], "errors": {},
|
||||||
|
"nodes": [{"name": "pvemini", "ip": "10.0.20.201", "online": True,
|
||||||
|
"cpu": 0.0, "mem": 0, "maxmem": 0, "uptime": 0, "guests": 2}],
|
||||||
|
}
|
||||||
|
base.update(over)
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def test_slash_args_infra():
|
||||||
|
assert bot.slash_args("infra", {"actiune": "oprire", "simulare": None}) == ["oprire"]
|
||||||
|
assert bot.slash_args("infra", {"actiune": "oprire", "simulare": False}) == ["oprire", "real"]
|
||||||
|
assert bot.slash_args("infra", {"actiune": "oprire", "simulare": True}) == ["oprire", "simulare"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_infra_actiune_necunoscuta_fara_subprocess(bridge, monkeypatch):
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "run", lambda *a, **k: pytest.fail("nu trebuia sa ruleze nimic"))
|
||||||
|
_, ch, _ = await slash(bridge, "infra", actiune="nu-exista", simulare=None)
|
||||||
|
assert "necunoscuta" in ch.all_text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_infra_fara_actiune(bridge):
|
||||||
|
_, ch, _ = await slash(bridge, "infra", actiune="", simulare=None)
|
||||||
|
assert "necunoscuta" in ch.all_text or "Foloseste" in ch.all_text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_infra_stare(bridge, monkeypatch):
|
||||||
|
st = _st(ups={"status": "OL", "charge": 100.0, "runtime_s": 1200})
|
||||||
|
monkeypatch.setattr(infra_actions, "status", lambda fresh=False: st)
|
||||||
|
_, ch, _ = await slash(bridge, "infra", actiune="stare", simulare=None)
|
||||||
|
text = ch.all_text
|
||||||
|
assert "cvorum" in text and "3/3" in text and "guest-uri" in text and "pvemini" in text
|
||||||
|
assert "UPS" in text and "OL" in text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_infra_sarcini_fara_joburi(bridge, monkeypatch):
|
||||||
|
monkeypatch.setattr(infra_actions, "status", lambda fresh=False: _st(jobs=[]))
|
||||||
|
_, ch, _ = await slash(bridge, "infra", actiune="sarcini", simulare=None)
|
||||||
|
assert "Niciun job activ" in ch.all_text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_infra_sarcini_cu_job_activ(bridge, monkeypatch):
|
||||||
|
jobs = [{"id": "oprire", "host": "pvemini", "active": True, "log": "/var/log/x.log"}]
|
||||||
|
monkeypatch.setattr(infra_actions, "status", lambda fresh=False: _st(jobs=jobs))
|
||||||
|
monkeypatch.setattr(infra_actions, "log_tail", lambda aid, n=60: {"file": "/x", "lines": ["linia 1"]})
|
||||||
|
_, ch, _ = await slash(bridge, "infra", actiune="sarcini", simulare=None)
|
||||||
|
assert "oprire" in ch.all_text and "linia 1" in ch.all_text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_infra_precondition_esuata(bridge, monkeypatch):
|
||||||
|
monkeypatch.setattr(infra_actions, "status", lambda fresh=False: _st())
|
||||||
|
monkeypatch.setattr(infra_actions, "check", lambda aid, st, dry: (False, "motiv de test"))
|
||||||
|
apelat = []
|
||||||
|
monkeypatch.setattr(infra_actions, "run", lambda *a, **k: apelat.append(a) or {"ok": True, "code": 200})
|
||||||
|
_, ch, _ = await slash(bridge, "infra", actiune="wol", simulare=None)
|
||||||
|
assert "⛔" in ch.all_text and "motiv de test" in ch.all_text
|
||||||
|
assert apelat == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_infra_fara_confirmare_ruleaza_direct(bridge, monkeypatch):
|
||||||
|
"""`wol` nu cere confirmare: ruleaza imediat daca precondiția trece."""
|
||||||
|
monkeypatch.setattr(infra_actions, "status", lambda fresh=False: _st())
|
||||||
|
monkeypatch.setattr(infra_actions, "check", lambda aid, st, dry: (True, ""))
|
||||||
|
apelat = []
|
||||||
|
|
||||||
|
def _run(aid, dry):
|
||||||
|
apelat.append((aid, dry))
|
||||||
|
return {"ok": True, "code": 200, "host": "oracle-prod-admin"}
|
||||||
|
|
||||||
|
monkeypatch.setattr(infra_actions, "run", _run)
|
||||||
|
_, ch, _ = await slash(bridge, "infra", actiune="wol", simulare=None)
|
||||||
|
assert apelat == [("wol", False)] # "wol" nu are simulare -> dry mereu False
|
||||||
|
assert "pornit" in ch.all_text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_infra_cu_confirmare_arata_intrebarea_si_nu_ruleaza(bridge, monkeypatch):
|
||||||
|
monkeypatch.setattr(infra_actions, "status", lambda fresh=False: _st())
|
||||||
|
monkeypatch.setattr(infra_actions, "check", lambda aid, st, dry: (True, ""))
|
||||||
|
apelat = []
|
||||||
|
monkeypatch.setattr(infra_actions, "run", lambda *a, **k: apelat.append(a) or {"ok": True, "code": 200})
|
||||||
|
_, ch, _ = await slash(bridge, "infra", actiune="oprire", simulare=False) # "real" -> dry=False
|
||||||
|
assert "confirmi" in ch.all_text.lower()
|
||||||
|
assert apelat == [] # butonul nu a fost apasat, comanda nu a rulat nimic
|
||||||
|
|
||||||
|
|
||||||
|
async def test_infra_actiune_de_simulare_are_dry_true(bridge, monkeypatch):
|
||||||
|
vazute = []
|
||||||
|
monkeypatch.setattr(infra_actions, "status", lambda fresh=False: _st())
|
||||||
|
|
||||||
|
def _check(aid, st, dry):
|
||||||
|
vazute.append(dry)
|
||||||
|
return (True, "")
|
||||||
|
|
||||||
|
monkeypatch.setattr(infra_actions, "check", _check)
|
||||||
|
monkeypatch.setattr(infra_actions, "run", lambda *a, **k: {"ok": True, "code": 200})
|
||||||
|
await slash(bridge, "infra", actiune="oprire", simulare=None) # implicit: simulare
|
||||||
|
await slash(bridge, "infra", actiune="oprire", simulare=False) # explicit: real
|
||||||
|
assert vazute == [True, False]
|
||||||
|
|
||||||
|
|
||||||
|
def test_infra_view_e_none_fara_discordpy(bridge):
|
||||||
|
if bot.discord is None:
|
||||||
|
assert bridge.infra_view("oprire", True) is None
|
||||||
|
assert bridge.infra_boot_view() is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_infra_boot_check_anunta_mentenanta_apoi_se_opreste(bridge, monkeypatch):
|
||||||
|
ch = FakeChannel()
|
||||||
|
monkeypatch.setenv("DISCORD_CHANNEL_IDS", str(ch.id))
|
||||||
|
bridge.get_channel = lambda cid: ch if str(cid) == str(ch.id) else None
|
||||||
|
|
||||||
|
st = _st(maint=1)
|
||||||
|
monkeypatch.setattr(infra_actions, "status", lambda fresh=False: st)
|
||||||
|
monkeypatch.setattr(infra_actions, "maintenance_pending", lambda s: True)
|
||||||
|
|
||||||
|
await bridge.infra_boot_check()
|
||||||
|
assert "guest-uri de pornit" in ch.all_text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_infra_boot_check_iese_daca_nu_e_in_mentenanta(bridge, monkeypatch):
|
||||||
|
st = _st(maint=0)
|
||||||
|
monkeypatch.setattr(infra_actions, "status", lambda fresh=False: st)
|
||||||
|
apelat = []
|
||||||
|
monkeypatch.setattr(infra_actions, "maintenance_pending", lambda s: apelat.append(s) or False)
|
||||||
|
await bridge.infra_boot_check()
|
||||||
|
assert apelat == [] # a iesit la prima citire (maint == 0), fara sa mai verifice
|
||||||
|
|
||||||
|
|
||||||
|
async def test_infra_boot_check_nu_doboara_botul_la_eroare(bridge, monkeypatch):
|
||||||
|
"""`status()` care arunca nu are voie sa opreasca bucla sau botul."""
|
||||||
|
|
||||||
|
def _boom(fresh=False):
|
||||||
|
raise RuntimeError("retea picata")
|
||||||
|
|
||||||
|
async def _fast_sleep(_):
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr(infra_actions, "status", _boom)
|
||||||
|
monkeypatch.setattr(asyncio, "sleep", _fast_sleep)
|
||||||
|
await bridge.infra_boot_check() # 20 incercari esuate, nici o exceptie in afara
|
||||||
|
|||||||
Reference in New Issue
Block a user