feat(2.0): W8 — Wartung (OS/Engine/Modell-Updates, Restart, Reboot, Logs)
services/maintenance.py + routers/maintenance.py: updates-Badge (apt/Engine- Release/dyn. Modell-Upgrades via discover), os-update + engine-update als jobengine-Jobs, system-/user-aware Restart (llama-swap via sudo -n NOPASSWD), reboot, logs (journalctl). Frontend: Wartungs-Block in SystemView mit Badge, Buttons + Modell-Upgrade-Vorschlaegen. Passwortfrei (sudo -n). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Wartung: Updates (OS/Engine/Modelle), Dienst-Neustart (system- vs user-aware),
|
||||
Reboot, Logs. Portiert/modernisiert aus Mission Control v1 (routers/maintenance.py).
|
||||
|
||||
Passwortfrei über NOPASSWD-Whitelist (sudo -n). OS-Update/Reboot brauchen einmalig
|
||||
erweiterte sudoers (siehe docs/BEDIENUNG.md). Lange Ops laufen als jobengine-Job.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
import psutil
|
||||
|
||||
from services import discover, jobengine, llamaswap
|
||||
|
||||
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
|
||||
SYSTEM_SERVICES = {"llama-swap"}
|
||||
USER_SERVICES = {"mission-control-2", "hermes-gateway", "hermes-dashboard", "hermes-webui", "litellm-gateway"}
|
||||
|
||||
ENGINE_UPDATE_CMD = os.environ.get("MC_ENGINE_UPDATE_CMD", "")
|
||||
ENGINE_PATH = os.environ.get("MC_ENGINE_PATH", "/opt/llamacpp")
|
||||
_engine_cache = {"ts": 0.0, "avail": False}
|
||||
|
||||
|
||||
def _ram_gb() -> float:
|
||||
return psutil.virtual_memory().total / (1024 ** 3)
|
||||
|
||||
|
||||
def _os_upgradable() -> int:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["bash", "-c", "apt list --upgradable 2>/dev/null | grep -c upgradable || true"],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
return int((out.stdout or "0").strip() or 0)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def _engine_update_available() -> bool:
|
||||
now = time.time()
|
||||
if now - _engine_cache["ts"] < 3600:
|
||||
return _engine_cache["avail"]
|
||||
avail = False
|
||||
try:
|
||||
rel = httpx.get("https://api.github.com/repos/lemonade-sdk/llamacpp-rocm/releases/latest",
|
||||
timeout=6, headers={"User-Agent": "MissionControl2"}).json()
|
||||
pub = datetime.fromisoformat(rel["published_at"].replace("Z", "+00:00")).timestamp()
|
||||
inst = os.path.getmtime(ENGINE_PATH)
|
||||
avail = pub > inst + 86400
|
||||
except Exception:
|
||||
avail = False
|
||||
_engine_cache.update(ts=now, avail=avail)
|
||||
return avail
|
||||
|
||||
|
||||
def model_upgrades() -> list[dict]:
|
||||
"""Dynamisch: je discover-Kategorie das empfohlene Modell, das NOCH NICHT installiert ist
|
||||
→ Upgrade-Vorschlag für diese Rolle. Self-updating (kein Hardcode wie v1)."""
|
||||
disc = discover.safe_discover(_ram_gb())
|
||||
if not disc:
|
||||
return []
|
||||
cmds = " ".join(str(s.get("cmd", "")).lower()
|
||||
for s in (llamaswap.read_config().get("models") or {}).values())
|
||||
out = []
|
||||
for c in disc.get("categories", []):
|
||||
rec = c.get("recommended")
|
||||
if not rec:
|
||||
continue
|
||||
base = rec.split("/")[-1].lower()
|
||||
stem = base[:-5] if base.endswith("-gguf") else base
|
||||
if base in cmds or (stem and stem in cmds):
|
||||
continue
|
||||
out.append({"role": c["role"], "title": c["title"], "repo": rec})
|
||||
return out
|
||||
|
||||
|
||||
def updates() -> dict:
|
||||
ups = model_upgrades()
|
||||
return {"os": _os_upgradable(), "engine": 1 if _engine_update_available() else 0,
|
||||
"models": len(ups), "model_list": ups}
|
||||
|
||||
|
||||
def _run(cmd: list[str]) -> dict:
|
||||
try:
|
||||
p = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
return {"ok": p.returncode == 0, "out": (p.stdout or "")[-4000:], "err": (p.stderr or "")[-2000:]}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"ok": False, "out": "", "err": str(exc)}
|
||||
|
||||
|
||||
def restart_service(name: str) -> dict:
|
||||
if name in SYSTEM_SERVICES:
|
||||
return _run(["sudo", "-n", "systemctl", "restart", name])
|
||||
if name in USER_SERVICES:
|
||||
return _run(["systemctl", "--user", "restart", name])
|
||||
return {"ok": False, "err": f"Dienst '{name}' nicht erlaubt."}
|
||||
|
||||
|
||||
def logs(service: str, lines: int = 200) -> dict:
|
||||
lines = max(1, min(lines, 1000))
|
||||
if service in SYSTEM_SERVICES:
|
||||
cmd = ["sudo", "-n", "journalctl", "-u", service, "-n", str(lines), "--no-pager"]
|
||||
elif service in USER_SERVICES:
|
||||
cmd = ["journalctl", "--user", "-u", service, "-n", str(lines), "--no-pager"]
|
||||
else:
|
||||
return {"ok": False, "text": "", "err": "Dienst nicht erlaubt."}
|
||||
r = _run(cmd)
|
||||
return {"ok": r["ok"], "text": r["out"] or r["err"]}
|
||||
|
||||
|
||||
def os_update_job() -> str:
|
||||
cmd = "sudo -n apt-get update && sudo -n DEBIAN_FRONTEND=noninteractive apt-get upgrade -y"
|
||||
return jobengine.start_job(["bash", "-c", cmd], "OS-Update (apt)")
|
||||
|
||||
|
||||
def engine_update_job() -> str | None:
|
||||
if not ENGINE_UPDATE_CMD:
|
||||
return None
|
||||
cmd = f"{ENGINE_UPDATE_CMD} && sudo -n systemctl restart llama-swap"
|
||||
return jobengine.start_job(["bash", "-c", cmd], "Engine-Update (llama.cpp)")
|
||||
|
||||
|
||||
def reboot() -> dict:
|
||||
return _run(["sudo", "-n", "reboot"])
|
||||
Reference in New Issue
Block a user