Welle 2 · „Was ist neu?“ vor dem Knopf: Release Notes, Debian-Änderungslisten und Lucys Kurzfassung
Vor einem App-Update sammelt der Orchestrator die Release Notes aller Versionen zwischen installiert und neu (GitHub), beim Proxmox-Host die Debian-Änderungslisten der wichtigsten Pakete (neue Ausführer-Aktion changelog, nur lesend, nur für anstehende Pakete). Lucy fasst sie über das Modell fast der KI-Box auf Deutsch zusammen: Handarbeit ganz oben, höchstens fünf Stichpunkte, Brüche zuerst. Die Update-Tabelle hat „Was ist neu?“ je Gerät, und die Rückfrage vor dem Update zeigt die Kurzfassung gleich mit. Gemerkt je Version; der Ausführer braucht ausfuehrer-einrichten.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0190c25d5a
commit
45f11bc3dc
@@ -6,6 +6,7 @@
|
||||
POST /api/homelab/ziele/{id}/suchen Paketlisten im Gast erneuern
|
||||
POST /api/homelab/ziele/{id}/docker Docker über Arcane (zuerst Probelauf)
|
||||
POST /api/homelab/ziele/{id}/rueckweg-probe Rückweg testen: Snapshot, absichtlich rot, zurück, nachprüfen
|
||||
GET /api/homelab/ziele/{id}/neuerungen?baustein=app|pakete Was ist neu? Release Notes + Lucys Kurzfassung
|
||||
POST /api/homelab/ziele/pve/neustart Proxmox-Host neu starten (eigener Knopf)
|
||||
GET /api/homelab/laeufe Die letzten Läufe (Updates, Snapshot-Aktionen) mit ihren Schritten
|
||||
GET /api/homelab/snapshots Snapshots und Sicherungen je Gerät (Aktionen: siehe unten)
|
||||
@@ -166,6 +167,14 @@ def speicher_lage() -> dict:
|
||||
return speicher.lage()
|
||||
|
||||
|
||||
@router.get("/ziele/{ziel_id}/neuerungen")
|
||||
def neuerungen(ziel_id: str, baustein: str = "app") -> dict:
|
||||
"""Release Notes bzw. Änderungslisten vor dem Update, mit Lucys Kurzfassung (seit 25.09.2026). Kann beim ersten
|
||||
Mal einige Sekunden dauern (GitHub, Ausführer, Modell); danach ist es gemerkt."""
|
||||
from services.homelab import neuerungen as neu
|
||||
return neu.fuer(ziel_id, baustein)
|
||||
|
||||
|
||||
@router.get("/neu")
|
||||
def neu_entdeckt() -> dict:
|
||||
"""Geräte, die der Orchestrator noch nicht kennt, mit Vorschlägen (seit 25.09.2026)."""
|
||||
|
||||
@@ -34,7 +34,7 @@ except ImportError: # Windows (Entwicklung): nur die Thread-Sperre
|
||||
|
||||
AKTIONEN = {"bericht", "snapshot", "update", "os_update", "suchen", "zurueck", "snapshot_loeschen",
|
||||
"host_update", "host_neustart", "sichern", "sicherung_zurueck", "sicherung_loeschen",
|
||||
"kernel_aufraeumen", "sicherung_probe", "etikett_setzen"}
|
||||
"kernel_aufraeumen", "sicherung_probe", "etikett_setzen", "changelog"}
|
||||
VERLOREN_S = 3 * 3600 # abgeholt, aber nie beantwortet (Ausführer abgestürzt, Host neu gestartet)
|
||||
BEHALTEN = 500 # so viele erledigte Aufträge bleiben sichtbar (Protokoll; bis 24.09.2026: 60)
|
||||
VOLLTEXT = 60 # die jüngsten erledigten behalten ihre ganze Ausgabe …
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
"""Was ein Update bringt, vor dem Knopf (Ausbauplan Welle 2, Punkt 9, seit 25.09.2026).
|
||||
|
||||
App (Community-Script mit GitHub-Projekt) die Release Notes aller Versionen zwischen installiert und neu
|
||||
(GitHub-API, höchstens zehn, je auf 4 000 Zeichen gekürzt)
|
||||
Proxmox-Host (Pakete) die Debian-Änderungslisten der wichtigsten Pakete (Proxmox, Kernel,
|
||||
QEMU, LXC, ZFS …, höchstens acht) — der Ausführer holt sie nur lesend
|
||||
(Aktion changelog), jeweils bis zur installierten Version
|
||||
Lucy fasst das auf Deutsch zusammen (Modell `fast` der KI-Box über deren /v1, Aufbau wie bei den Updates der Box):
|
||||
eine erste Zeile, ob der User selbst etwas tun muss („Handarbeit“), dann höchstens fünf Stichpunkte, Brüche und
|
||||
geänderte Einstellungen zuerst und mit ⚠️. Fakten kommen aus den Notes, die Worte vom Modell. Antwortet die Box
|
||||
nicht, stehen die Notes ohne Kurzfassung da. Ergebnisse werden je (Gerät, Baustein, von, nach) gemerkt
|
||||
(<Datenordner>/neuerungen.json) — dieselbe Version fasst Lucy nur einmal zusammen.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from kern.einstellungen import einstellungen
|
||||
from kern.github import github_json
|
||||
|
||||
from services.homelab import apps, inventar, kanal
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MAX_VERSIONEN = 10
|
||||
MAX_ZEICHEN = 4000
|
||||
MAX_PAKETE = 8
|
||||
WICHTIG = re.compile(r"pve|proxmox|qemu|lxc|kernel|zfs|ceph|corosync|spiceterm|novnc|libpve")
|
||||
ZEITLIMIT_LLM = httpx.Timeout(120.0, connect=5.0)
|
||||
FEHLER_MERKEN_S = 600
|
||||
_AKTION = re.compile(r"^\s*AKTION:\s*(JA|NEIN)\b\s*[—–-]?\s*(.*)$", re.IGNORECASE)
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
def _pfad() -> Path:
|
||||
return einstellungen().daten_dir / "neuerungen.json"
|
||||
|
||||
|
||||
def _gemerkt() -> dict:
|
||||
try:
|
||||
daten = json.loads(_pfad().read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
return daten if isinstance(daten, dict) else {}
|
||||
|
||||
|
||||
def _merken(schluessel: str, ergebnis: dict) -> None:
|
||||
with _lock:
|
||||
daten = _gemerkt()
|
||||
daten[schluessel] = {"zeit": time.time(), "ergebnis": ergebnis}
|
||||
# Nur die jüngsten 100 Einträge: alte Versionen braucht niemand mehr.
|
||||
daten = dict(sorted(daten.items(), key=lambda t: t[1].get("zeit", 0))[-100:])
|
||||
pfad = _pfad()
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = pfad.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(daten, ensure_ascii=False), encoding="utf-8")
|
||||
tmp.replace(pfad)
|
||||
|
||||
|
||||
# --- Quellen -----------------------------------------------------------------------------------------------------
|
||||
|
||||
def zwischen(releases: object, installiert: str, neu: str) -> list[dict]:
|
||||
"""Die Releases mit installiert < Version <= neu, neueste zuerst (ohne Entwürfe und Vorabversionen)."""
|
||||
von, bis = inventar._teile(installiert), inventar._teile(neu)
|
||||
if not von or not bis:
|
||||
return []
|
||||
auswahl = []
|
||||
for r in releases if isinstance(releases, list) else []:
|
||||
if not isinstance(r, dict) or r.get("draft") or r.get("prerelease"):
|
||||
continue
|
||||
version = inventar._teile(str(r.get("tag_name") or ""))
|
||||
if version and von < version <= bis:
|
||||
auswahl.append((version, {"version": str(r.get("tag_name")), "titel": r.get("name") or r.get("tag_name"),
|
||||
"datum": r.get("published_at"), "url": r.get("html_url"),
|
||||
"text": str(r.get("body") or "").strip()[:MAX_ZEICHEN]}))
|
||||
auswahl.sort(key=lambda t: t[0], reverse=True)
|
||||
return [r for _, r in auswahl[:MAX_VERSIONEN]]
|
||||
|
||||
|
||||
def wichtige_pakete(updates: list[dict]) -> list[str]:
|
||||
"""Die Pakete, deren Änderungsliste zählt: Proxmox, Kernel und Co. zuerst, höchstens MAX_PAKETE."""
|
||||
namen = [str(u.get("paket")) for u in updates if isinstance(u, dict) and u.get("paket")]
|
||||
return sorted(namen, key=lambda n: (not WICHTIG.search(n), n))[:MAX_PAKETE]
|
||||
|
||||
|
||||
def _changelogs(pakete: list[str]) -> dict[str, str]:
|
||||
"""Über den Ausführer (Aktion changelog, nur lesend). Leer, wenn er nicht antwortet."""
|
||||
auftrag = kanal.anlegen("changelog", {"pakete": pakete})
|
||||
ergebnis = kanal.warten(auftrag, 120)
|
||||
if not ergebnis or ergebnis.get("status") != "fertig":
|
||||
return {}
|
||||
try:
|
||||
daten = json.loads(ergebnis.get("text") or "{}")
|
||||
except ValueError:
|
||||
return {}
|
||||
return {k: str(v) for k, v in daten.items()} if isinstance(daten, dict) else {}
|
||||
|
||||
|
||||
# --- Lucys Kurzfassung ---------------------------------------------------------------------------------------------
|
||||
|
||||
def auswerten(text: str) -> dict:
|
||||
"""Die Antwort des Modells: {"aktion": bool|None, "aktion_text", "punkte": […]}. Tolerant: Die AKTION-Zeile darf
|
||||
irgendwo stehen, Stichpunkte beginnen mit „-“ oder „•“."""
|
||||
aktion, aktion_text, punkte = None, "", []
|
||||
for zeile in text.splitlines():
|
||||
if aktion is None and (m := _AKTION.match(zeile)):
|
||||
aktion, aktion_text = m.group(1).upper() == "JA", m.group(2).strip()
|
||||
elif zeile.strip().startswith(("-", "•", "*")):
|
||||
punkte.append(zeile.strip().lstrip("-•* ").strip())
|
||||
return {"aktion": aktion, "aktion_text": aktion_text, "punkte": [p for p in punkte if p][:5]}
|
||||
|
||||
|
||||
def zusammenfassen(was: str, von: str, nach: str, quellen: str) -> dict | None:
|
||||
"""Lucy fasst zusammen (Modell `fast` der KI-Box). None, wenn es keine Box gibt oder sie nicht antwortet."""
|
||||
box = einstellungen().partner_url
|
||||
if not box or not quellen.strip():
|
||||
return None
|
||||
prompt = (
|
||||
f"Du bist Lucy und betreust ein Homelab. Vor einem Update von {was} ({von} → {nach}) liest du die "
|
||||
"Änderungen und sagst dem Besitzer, was ihn betrifft. Die Änderungen:\n\n" + quellen[:14000] + "\n\n"
|
||||
"Antworte auf DEUTSCH, knapp und klar, GENAU in diesem Format:\n"
|
||||
"Zeile 1 beginnt mit 'AKTION: ':\n"
|
||||
" • 'AKTION: NEIN' — wenn er nichts selbst tun muss (der Orchestrator legt vorher einen Snapshot an und geht "
|
||||
"bei Problemen von allein zurück). Das ist der Normalfall.\n"
|
||||
" • 'AKTION: JA — <was er konkret tun muss>' — NUR bei Handarbeit: Migration, geänderte oder entfernte "
|
||||
"Einstellungen, neue Pflichtangaben, Brüche.\n"
|
||||
"Danach höchstens 5 kurze Stichpunkte (je mit '- '), Wichtigstes zuerst: Brüche und geänderte Einstellungen "
|
||||
"ZUERST und mit '⚠️', dann Sicherheitskorrekturen, dann lohnende Neuerungen. Keine Einleitung, keine "
|
||||
"Überschrift, nichts erfinden, was nicht in den Änderungen steht.")
|
||||
try:
|
||||
r = httpx.post(f"{box}/v1/chat/completions", timeout=ZEITLIMIT_LLM, json={
|
||||
"model": "fast", "max_tokens": 600, "temperature": 0.2,
|
||||
"chat_template_kwargs": {"enable_thinking": False},
|
||||
"messages": [{"role": "user", "content": prompt}]})
|
||||
r.raise_for_status()
|
||||
wahl = (r.json().get("choices") or [{}])[0]
|
||||
text = ((wahl.get("message") or {}).get("content") or "").strip()
|
||||
except (httpx.HTTPError, ValueError, AttributeError):
|
||||
log.warning("neuerungen: Lucy war nicht zu erreichen", exc_info=True)
|
||||
return None
|
||||
if wahl.get("finish_reason") == "length":
|
||||
text = text.rpartition("\n")[0]
|
||||
ergebnis = auswerten(text)
|
||||
return ergebnis if ergebnis["aktion"] is not None or ergebnis["punkte"] else None
|
||||
|
||||
|
||||
# --- Für die Oberfläche --------------------------------------------------------------------------------------------
|
||||
|
||||
def _baustein(zid: str, baustein: str) -> tuple[dict | None, dict | None]:
|
||||
ziel = next((z for z in inventar.ziele()["ziele"] if z["id"] == zid), None)
|
||||
b = next((x for x in (ziel or {}).get("bausteine") or [] if x["id"] == baustein), None)
|
||||
return ziel, b
|
||||
|
||||
|
||||
def fuer(zid: str, baustein: str) -> dict:
|
||||
"""GET /api/homelab/ziele/{zid}/neuerungen?baustein=…: {"verfuegbar", "grund", "von", "nach", "versionen" |
|
||||
"pakete", "zusammenfassung", "quelle_url"}."""
|
||||
ziel, b = _baustein(zid, baustein)
|
||||
if not ziel or not b or b.get("zustand") != "neu":
|
||||
return {"verfuegbar": False, "grund": "Für diesen Baustein steht kein Update an."}
|
||||
von, nach = str(b.get("installiert") or ""), str(b.get("verfuegbar") or b.get("kurz") or "")
|
||||
schluessel = f"{zid}:{baustein}:{von}:{nach}"
|
||||
alt = _gemerkt().get(schluessel)
|
||||
if alt and (alt["ergebnis"].get("zusammenfassung") or time.time() - alt.get("zeit", 0) < FEHLER_MERKEN_S):
|
||||
return alt["ergebnis"]
|
||||
if zid == "pve" and baustein == "pakete":
|
||||
ergebnis = _host(von, nach)
|
||||
elif baustein == "app":
|
||||
ergebnis = _app(ziel, von, nach)
|
||||
else:
|
||||
return {"verfuegbar": False, "grund": "Für Paket-Updates im Gast und Docker-Images gibt es keine Release Notes."}
|
||||
if ergebnis.get("verfuegbar"):
|
||||
_merken(schluessel, ergebnis)
|
||||
return ergebnis
|
||||
|
||||
|
||||
def _app(ziel: dict, von: str, nach: str) -> dict:
|
||||
gast = inventar.gast(ziel["id"]) or {}
|
||||
app = apps.app_fuer((gast.get("app") or {}).get("kennung"), gast.get("name"))
|
||||
if not app or not app.quelle:
|
||||
return {"verfuegbar": False, "grund": "Zu dieser App kennt der Orchestrator kein GitHub-Projekt."}
|
||||
url = f"https://github.com/{app.quelle}/releases"
|
||||
try:
|
||||
versionen = zwischen(github_json(f"repos/{app.quelle}/releases?per_page=40"), von, nach)
|
||||
except Exception:
|
||||
return {"verfuegbar": False, "grund": "GitHub antwortet gerade nicht.", "quelle_url": url}
|
||||
if not versionen:
|
||||
return {"verfuegbar": False, "grund": "Zu diesen Versionen stehen auf GitHub keine Release Notes.", "quelle_url": url}
|
||||
quellen = "\n\n".join(f"## {v['titel']} ({v['version']})\n{v['text']}" for v in versionen)
|
||||
return {"verfuegbar": True, "grund": None, "von": von, "nach": nach, "versionen": versionen, "quelle_url": url,
|
||||
"zusammenfassung": zusammenfassen(app.name, von, nach, quellen)}
|
||||
|
||||
|
||||
def _host(von: str, nach: str) -> dict:
|
||||
host = ((kanal.bericht() or {}).get("bericht") or {}).get("host") or {}
|
||||
pakete = wichtige_pakete(host.get("updates") or [])
|
||||
if not pakete:
|
||||
return {"verfuegbar": False, "grund": "Der Bericht nennt keine Pakete."}
|
||||
logs = _changelogs(pakete)
|
||||
if not logs:
|
||||
return {"verfuegbar": False, "grund": "Der Ausführer hat die Änderungslisten nicht geliefert."}
|
||||
alle = len(host.get("updates") or [])
|
||||
quellen = "\n\n".join(f"## {name}\n{text}" for name, text in logs.items())
|
||||
return {"verfuegbar": True, "grund": None, "von": von, "nach": nach,
|
||||
"pakete": [{"name": n, "text": t} for n, t in logs.items()], "weitere": max(0, alle - len(logs)),
|
||||
"zusammenfassung": zusammenfassen(f"Proxmox VE ({alle} Pakete)", von or "installiert", nach or "neu",
|
||||
quellen)}
|
||||
@@ -70,8 +70,10 @@ _AKTION = {
|
||||
"sicherung_probe": ("Probe-Wiederherstellung gelungen", "Probe-Wiederherstellung gescheitert",
|
||||
"Probe-Wiederherstellung läuft", "Probe-Wiederherstellung"),
|
||||
"etikett_setzen": ("Etikett gesetzt", "Etikett setzen gescheitert", "Etikett wird gesetzt", "Etikett setzen"),
|
||||
"changelog": ("Änderungslisten gelesen", "Änderungslisten nicht gelesen", "Änderungslisten werden gelesen",
|
||||
"Änderungslisten"),
|
||||
}
|
||||
_HOST_AKTIONEN = {"bericht", "host_update", "host_neustart", "kernel_aufraeumen", "sicherung_probe"}
|
||||
_HOST_AKTIONEN = {"bericht", "host_update", "host_neustart", "kernel_aufraeumen", "sicherung_probe", "changelog"}
|
||||
_LAUF_WAS = {"app": "Update", "os": "Paket-Update", "pakete": "Paket-Update", "docker": "Docker-Update",
|
||||
"neustart": "Neustart", "kernel": "Kernel-Aufräumen", "probe": "Rückweg-Probe"}
|
||||
_SCHRITT_WORT = {"wartet": "wartet", "laeuft": "läuft", "eingespielt": "eingespielt",
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""„Was ist neu?“ vor dem Knopf (Ausbauplan Welle 2, Punkt 9, 25.09.2026): Release Notes zwischen installiert und neu,
|
||||
Debian-Änderungslisten des Proxmox-Hosts über den Ausführer und Lucys Kurzfassung. Die Änderungsliste ist ein Auszug
|
||||
von pvesh get /nodes/pve/apt/changelog --name pve-manager vom 25.09."""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from kern import einstellungen as einstellungen_mod
|
||||
from services.homelab import inventar, kanal, neuerungen
|
||||
|
||||
AUSFUEHRER = Path(__file__).resolve().parents[2] / "deploy" / "homelab" / "ausfuehrer.py"
|
||||
|
||||
RELEASES = [
|
||||
{"tag_name": "v1.27.0-rc1", "prerelease": True, "body": "Vorab"},
|
||||
{"tag_name": "v1.26.1", "name": "Gitea 1.26.1", "published_at": "2026-09-20T10:00:00Z", "html_url": "u1",
|
||||
"body": "* Security: fix XSS in markdown renderer"},
|
||||
{"tag_name": "v1.26.0", "name": "Gitea 1.26.0", "published_at": "2026-09-01T10:00:00Z", "html_url": "u0",
|
||||
"body": "BREAKING: `[server] LFS_JWT_SECRET` moved to `[lfs] JWT_SECRET`"},
|
||||
{"tag_name": "v1.25.4", "body": "alt"},
|
||||
{"tag_name": "v1.26.2", "draft": True, "body": "Entwurf"},
|
||||
]
|
||||
CHANGELOG = """pve-manager (9.2.20) trixie; urgency=medium
|
||||
|
||||
* Revert "partially fix #1989: ui: qemu: disk: add qcow2 cache size config"
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 11 Sep 2026 14:10:20 +0200
|
||||
|
||||
pve-manager (9.2.19) trixie; urgency=medium
|
||||
|
||||
* pvesh: fix startup failure caused by API endpoints with empty parameter schemas
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 01 Sep 2026 10:00:00 +0200
|
||||
|
||||
pve-manager (9.2.18) trixie; urgency=medium
|
||||
|
||||
* älter
|
||||
"""
|
||||
|
||||
|
||||
def test_releases_zwischen_installiert_und_neu():
|
||||
versionen = neuerungen.zwischen(RELEASES, "1.25.4", "1.26.1")
|
||||
assert [v["version"] for v in versionen] == ["v1.26.1", "v1.26.0"] # ohne Vorab und Entwurf
|
||||
assert versionen[1]["text"].startswith("BREAKING") and versionen[0]["url"] == "u1"
|
||||
assert neuerungen.zwischen(RELEASES, "Image vom 2026-08-27", "") == [] # Datum statt Version
|
||||
assert neuerungen.zwischen("kaputt", "1.0", "2.0") == []
|
||||
|
||||
|
||||
def test_wichtige_pakete_zuerst():
|
||||
updates = [{"paket": n} for n in ("libc6", "pve-manager", "zlib1g", "proxmox-kernel-7.0", "qemu-server", "curl")]
|
||||
assert neuerungen.wichtige_pakete(updates)[:3] == ["proxmox-kernel-7.0", "pve-manager", "qemu-server"]
|
||||
|
||||
|
||||
def test_lucys_antwort_auswerten():
|
||||
a = neuerungen.auswerten("AKTION: JA — In app.ini [lfs] JWT_SECRET setzen\n- ⚠️ LFS-Schlüssel umgezogen\n"
|
||||
"- Sicherheitskorrektur im Markdown\n\n* noch was")
|
||||
assert a == {"aktion": True, "aktion_text": "In app.ini [lfs] JWT_SECRET setzen",
|
||||
"punkte": ["⚠️ LFS-Schlüssel umgezogen", "Sicherheitskorrektur im Markdown", "noch was"]}
|
||||
assert neuerungen.auswerten("\nAKTION: NEIN\n- nur Fehlerkorrekturen")["aktion"] is False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def daten(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path))
|
||||
monkeypatch.setenv("MC_PARTNER_URL", "http://box:9001")
|
||||
einstellungen_mod.einstellungen.cache_clear()
|
||||
yield tmp_path
|
||||
einstellungen_mod.einstellungen.cache_clear()
|
||||
|
||||
|
||||
class _Antwort:
|
||||
def __init__(self, daten: dict):
|
||||
self._daten = daten
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def json(self):
|
||||
return self._daten
|
||||
|
||||
|
||||
def test_kurzfassung_von_der_box(daten, monkeypatch):
|
||||
gesendet: list[dict] = []
|
||||
monkeypatch.setattr(neuerungen.httpx, "post", lambda url, timeout, json: gesendet.append({"url": url, **json}) or _Antwort(
|
||||
{"choices": [{"message": {"content": "AKTION: NEIN\n- Fehler behoben"}, "finish_reason": "stop"}]}))
|
||||
z = neuerungen.zusammenfassen("Gitea", "1.25.4", "1.26.1", "## v1.26.1\n* fix")
|
||||
assert z == {"aktion": False, "aktion_text": "", "punkte": ["Fehler behoben"]}
|
||||
assert gesendet[0]["url"] == "http://box:9001/v1/chat/completions" and gesendet[0]["model"] == "fast"
|
||||
assert "Gitea (1.25.4 → 1.26.1)" in gesendet[0]["messages"][0]["content"]
|
||||
monkeypatch.delenv("MC_PARTNER_URL")
|
||||
einstellungen_mod.einstellungen.cache_clear()
|
||||
assert neuerungen.zusammenfassen("Gitea", "1", "2", "x") is None # ohne Box keine Kurzfassung
|
||||
|
||||
|
||||
def test_fuer_eine_app_und_gemerkt(daten, monkeypatch):
|
||||
ziel = {"id": "ct-104", "bausteine": [{"id": "app", "zustand": "neu", "installiert": "1.25.4", "verfuegbar": "1.26.1"}]}
|
||||
monkeypatch.setattr(inventar, "ziele", lambda: {"ziele": [ziel]})
|
||||
monkeypatch.setattr(inventar, "gast", lambda zid: {"vmid": 104, "name": "gitea", "app": {"kennung": "gitea"}})
|
||||
abrufe: list[str] = []
|
||||
monkeypatch.setattr(neuerungen, "github_json", lambda pfad: abrufe.append(pfad) or RELEASES)
|
||||
monkeypatch.setattr(neuerungen, "zusammenfassen", lambda was, von, nach, quellen: {
|
||||
"aktion": True, "aktion_text": "JWT_SECRET umziehen", "punkte": [quellen.count("## ")]})
|
||||
e = neuerungen.fuer("ct-104", "app")
|
||||
assert e["verfuegbar"] and [v["version"] for v in e["versionen"]] == ["v1.26.1", "v1.26.0"]
|
||||
assert e["zusammenfassung"]["punkte"] == [2] and e["quelle_url"] == "https://github.com/go-gitea/gitea/releases"
|
||||
assert abrufe == ["repos/go-gitea/gitea/releases?per_page=40"]
|
||||
assert neuerungen.fuer("ct-104", "app") == e and len(abrufe) == 1 # gemerkt
|
||||
assert neuerungen.fuer("ct-104", "os")["verfuegbar"] is False
|
||||
|
||||
|
||||
def test_fuer_den_proxmox_host(daten, monkeypatch):
|
||||
ziel = {"id": "pve", "bausteine": [{"id": "pakete", "zustand": "neu", "installiert": "9.2.19", "kurz": "3 Pakete"}]}
|
||||
monkeypatch.setattr(inventar, "ziele", lambda: {"ziele": [ziel]})
|
||||
monkeypatch.setattr(kanal, "bericht", lambda: {"bericht": {"host": {"updates": [
|
||||
{"paket": "pve-manager"}, {"paket": "curl"}, {"paket": "libc6"}]}}})
|
||||
angefragt: list[list[str]] = []
|
||||
monkeypatch.setattr(neuerungen, "_changelogs", lambda pakete: angefragt.append(pakete) or {"pve-manager": "* Revert"})
|
||||
monkeypatch.setattr(neuerungen, "zusammenfassen", lambda was, von, nach, quellen: {"aktion": False, "aktion_text": "",
|
||||
"punkte": [was]})
|
||||
e = neuerungen.fuer("pve", "pakete")
|
||||
assert angefragt == [["pve-manager", "curl", "libc6"]]
|
||||
assert e["pakete"] == [{"name": "pve-manager", "text": "* Revert"}] and e["weitere"] == 2
|
||||
assert e["zusammenfassung"]["punkte"] == ["Proxmox VE (3 Pakete)"]
|
||||
|
||||
|
||||
# --- Ausführer: changelog -------------------------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def a():
|
||||
spec = importlib.util.spec_from_file_location("ausfuehrer_changelog", AUSFUEHRER)
|
||||
modul = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(modul)
|
||||
return modul
|
||||
|
||||
|
||||
def test_changelog_bis_zur_installierten_version(a):
|
||||
neu = a.changelog_neu(CHANGELOG, "9.2.19")
|
||||
assert neu.startswith("pve-manager (9.2.20)") and "9.2.19" not in neu
|
||||
assert a.changelog_neu(CHANGELOG, None) == CHANGELOG.strip()
|
||||
assert a.changelog_neu(CHANGELOG, "1.0") == CHANGELOG.strip() # unbekannt: alles
|
||||
|
||||
|
||||
def test_changelog_nur_fuer_anstehende_pakete_und_nur_lesend(a, monkeypatch):
|
||||
def pvesh(pfad, *parameter):
|
||||
if pfad.endswith("/apt/update"):
|
||||
return [{"Package": "pve-manager", "OldVersion": "9.2.19"}]
|
||||
return CHANGELOG
|
||||
|
||||
monkeypatch.setattr(a, "NODE", "pve")
|
||||
monkeypatch.setattr(a, "_pvesh", pvesh)
|
||||
monkeypatch.setattr(a, "NUR_LESEN", True)
|
||||
antwort = a.ausfuehren({"aktion": "changelog", "parameter": {"pakete": ["pve-manager", "curl"]}})
|
||||
assert antwort["code"] == 0
|
||||
assert list(json.loads(antwort["text"])) == ["pve-manager"] # curl steht nicht an
|
||||
kaputt = a.ausfuehren({"aktion": "changelog", "parameter": {"pakete": ["pve; rm -rf /"]}})
|
||||
assert kaputt["code"] == 2 and "ungültige Paketliste" in kaputt["text"]
|
||||
Reference in New Issue
Block a user