einstellungen: Zeitfenster, Radar-Schalter, Telegram-Test, Software-Stand und Altreste
Einstellungen, die etwas einstellen (services/einstellungen.py, /api/einstellungen): - Update-Zeitfenster: Wochentag und Uhrzeit des Update-Laufs als Drop-in mc2-autoupdate.timer.d/zeitfenster.conf. Gegen die Nachhol-Falle von Persistent=true (24.09.): Timer vor dem daemon-reload stoppen, Stempel auf jetzt, dann starten. Bei Fehler zurück auf das alte Drop-in. Während eines Update-Laufs abgelehnt. - Neustart nur im eingestellten Fenster (deploy/wartungsfenster.sh, drei Stunden ab der vollen Stunde des Beginns; Standard So 04:00-06:59). - Radar an/aus: disable --now bzw. enable + Stempel + start (kein Nachholen). deploy.sh respektiert den Schalter und startet den Radar-Timer nur mit Stempel; das Zeitfenster-Drop-in fasst er nie an. - Wächter: schläft der Timer eines Timer-Dienstes, ist ein alter Fehlschlag kein Befund mehr (der Dienst selbst ist static und schläft nie). - Telegram-Test je Instanz (/api/einstellungen/telegram-test und /api/homelab/einstellungen/telegram-test) über notify.sh -d; Ergebnis und Grund aus dem Melde-Log. notify.sh schreibt jetzt auch den Grund des gescheiterten Zweitwegs in den FALLBACK-Eintrag. - Einstellungsdatei mc2-einstellungen.json im Datenordner. Software-Stand: deploy.sh und homelab/ausrollen.sh schreiben nach dem Umschalten mc2-stand.json (deploy/stand-schreiben.py); /api/instanz liefert den Stand, die Einstellungen zeigen beide Instanzen und warnen bei Abweichung. Ohne Datei aus git, sonst unbekannt. Aufräumen: Altreste neben dem Betrieb aus einer festen, auf der Box geprüften Liste (Größe, Hinweis, nur was da und nicht in Gebrauch ist). Löschen nur per Kennung und Klick mit Rückfrage; Worktrees mit git worktree prune, Alt-Units mit daemon-reload, /opt/llamacpp notfalls mit sudo -n. Tests: pytest mit einem systemd-Abbild samt Nachhol-Regel, echtem notify.sh (Ersatz-Hermes, Tmp-Zugang, lokale Bot-API), Shell-Funktionen des Wartungsfensters und von deploy.sh; vitest für die Rückfragen der Schublade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
40d2840b16
commit
3d8df1c6fd
@@ -0,0 +1,150 @@
|
||||
"""Wartungsfenster für den Neustart nach Updates (deploy/wartungsfenster.sh, seit 24.09.2026 einstellbar) und wie
|
||||
deploy.sh mit den Einstellungen umgeht: Radar-Schalter respektieren, das Zeitfenster-Drop-in nie anfassen.
|
||||
|
||||
Die Tests fahren die echten Shell-Funktionen mit vorgetäuschter Zeit (MC_FENSTER_JETZT)."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from services import einstellungen
|
||||
|
||||
DEPLOY = Path(__file__).resolve().parents[2] / "deploy"
|
||||
BASH = shutil.which("bash")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
BASH is None or (sys.platform == "win32" and "system32" in BASH.lower()),
|
||||
reason="braucht eine bash (Git-Bash oder Linux)",
|
||||
)
|
||||
|
||||
|
||||
def _fenster(befehl: str, **env: str) -> subprocess.CompletedProcess:
|
||||
skript = f'. "{(DEPLOY / "wartungsfenster.sh").as_posix()}"; {befehl}'
|
||||
umgebung = {k: v for k, v in os.environ.items() if k not in ("MC_WARTUNGSFENSTER", "MC_FENSTER_JETZT")}
|
||||
umgebung.update({"MC_EINSTELLUNGEN": "/gibt/es/nicht.json", **env})
|
||||
return subprocess.run([BASH, "-c", skript], env=umgebung, capture_output=True, text=True, encoding="utf-8",
|
||||
timeout=60)
|
||||
|
||||
|
||||
def _im_fenster(jetzt: str, fenster: str | None = None) -> bool:
|
||||
env = {"MC_FENSTER_JETZT": jetzt}
|
||||
if fenster:
|
||||
env["MC_WARTUNGSFENSTER"] = fenster
|
||||
return _fenster("im_wartungsfenster", **env).returncode == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("jetzt, erwartet", [
|
||||
("7 04:00", True), ("7 04:40", True), ("7 06:59", True),
|
||||
("7 03:59", False), ("7 07:00", False),
|
||||
("4 15:00", False), # Donnerstagnachmittag: der Neustart vom 24.09.
|
||||
("6 05:00", False),
|
||||
])
|
||||
def test_standard_ist_sonntag_frueh(jetzt, erwartet):
|
||||
assert _im_fenster(jetzt) is erwartet
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fenster, jetzt, erwartet", [
|
||||
("6 05:00", "6 05:00", True), ("6 05:00", "6 07:59", True), ("6 05:00", "6 08:00", False),
|
||||
("6 05:00", "7 05:00", False),
|
||||
("6 23:30", "6 23:00", True), ("6 23:30", "7 01:59", True), ("6 23:30", "7 02:00", False),
|
||||
("6 23:30", "6 22:59", False),
|
||||
("7 23:00", "1 01:30", True), ("7 23:00", "1 02:00", False), # über das Wochenende hinaus
|
||||
("9 25:00", "7 04:30", True), ("9 25:00", "6 05:00", False), # Unsinn → Standard So 04:30
|
||||
])
|
||||
def test_eingestelltes_fenster(fenster, jetzt, erwartet):
|
||||
assert _im_fenster(jetzt, fenster) is erwartet
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag, uhrzeit", [(7, "04:30"), (6, "05:00"), (6, "23:30"), (7, "22:00"), (1, "00:00"),
|
||||
(3, "21:30")])
|
||||
def test_text_wie_in_der_oberflaeche(tag, uhrzeit):
|
||||
lauf = _fenster("fenster_text", MC_WARTUNGSFENSTER=f"{tag} {uhrzeit}")
|
||||
assert lauf.returncode == 0, lauf.stderr
|
||||
assert lauf.stdout == einstellungen.neustart_fenster(tag, uhrzeit)
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("jq") is None, reason="jq fehlt hier (auf der Box ist es da)")
|
||||
def test_fenster_aus_der_einstellungsdatei(tmp_path):
|
||||
datei = tmp_path / "mc2-einstellungen.json"
|
||||
datei.write_text(json.dumps({"update_fenster": {"tag": 6, "uhrzeit": "05:00"}}), encoding="utf-8")
|
||||
lauf = _fenster("fenster_text", MC_EINSTELLUNGEN=datei.as_posix())
|
||||
assert lauf.stdout == "Samstag 05:00–07:59"
|
||||
|
||||
|
||||
def test_ohne_datei_gilt_sonntag():
|
||||
assert _fenster("fenster_text").stdout == "Sonntag 04:00–06:59"
|
||||
|
||||
|
||||
def test_autoupdate_nutzt_das_fenster():
|
||||
skript = (DEPLOY / "autoupdate.sh").read_text(encoding="utf-8")
|
||||
assert '. "$SRC_DIR/wartungsfenster.sh"' in skript
|
||||
assert "! im_wartungsfenster" in skript and "$(fenster_text)" in skript
|
||||
assert '[ "$tag" != "7" ]' not in skript # die alte, feste Sonntagsregel ist raus
|
||||
|
||||
|
||||
# --- deploy.sh und die Einstellungen ------------------------------------------------------------------
|
||||
|
||||
def _befehlszeilen(skript: str) -> list[str]:
|
||||
return [z for z in skript.splitlines() if z.strip() and not z.strip().startswith("#")]
|
||||
|
||||
|
||||
def test_deploy_fasst_das_zeitfenster_nicht_an():
|
||||
"""Das Drop-in gehört der Oberfläche: kein Befehl in deploy.sh nennt es, keiner räumt Drop-in-Ordner weg."""
|
||||
for zeile in _befehlszeilen((DEPLOY / "deploy.sh").read_text(encoding="utf-8")):
|
||||
assert "mc2-autoupdate.timer.d" not in zeile and "zeitfenster" not in zeile, zeile
|
||||
assert not re.search(r"\brm\b.*(\.d\b|systemd/user)", zeile), zeile
|
||||
|
||||
|
||||
def test_deploy_respektiert_den_radar_schalter():
|
||||
skript = (DEPLOY / "deploy.sh").read_text(encoding="utf-8")
|
||||
stufe2 = skript[skript.index("stufe2() {"):skript.index("homelab_mitziehen() {")]
|
||||
assert "if radar_aus; then" in stufe2 and 'aktiv="${aktiv//mc2-radar.timer/}"' in stufe2
|
||||
assert "systemctl --user disable --now mc2-radar.timer" in stufe2
|
||||
assert "systemctl --user enable $aktiv" in stufe2 and "enable $AKTIV" not in stufe2
|
||||
# Kein Start des Radars ohne den Schalter und ohne den Stempel gegen das Nachholen.
|
||||
for zeile in _befehlszeilen(stufe2):
|
||||
if "systemctl --user start" in zeile and "mc2-radar.timer" in zeile:
|
||||
assert zeile.strip() == "systemctl --user start mc2-radar.timer"
|
||||
start = stufe2.index("systemctl --user start mc2-radar.timer")
|
||||
assert stufe2.rindex('touch "$HOME/.local/share/systemd/timers/stamp-mc2-radar.timer"', 0, start) > \
|
||||
stufe2.rindex('[ "$radar_an" = 1 ]', 0, start)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def radar_aus(tmp_path):
|
||||
"""Die echte Funktion radar_aus aus deploy.sh, mit einem python3, das es auch am PC gibt."""
|
||||
skript = (DEPLOY / "deploy.sh").read_text(encoding="utf-8")
|
||||
funktion = re.search(r"^radar_aus\(\) \{\n.*?^\}\n", skript, re.DOTALL | re.MULTILINE).group(0)
|
||||
shim = tmp_path / "bin"
|
||||
shim.mkdir()
|
||||
(shim / "python3").write_text(f'#!/bin/sh\nexec "{Path(sys.executable).as_posix()}" "$@"\n',
|
||||
encoding="utf-8", newline="\n")
|
||||
(shim / "python3").chmod(0o755)
|
||||
|
||||
def pruefen(inhalt: str | None) -> bool:
|
||||
datei = tmp_path / "mc2-einstellungen.json"
|
||||
datei.unlink(missing_ok=True)
|
||||
if inhalt is not None:
|
||||
datei.write_text(inhalt, encoding="utf-8")
|
||||
lauf = subprocess.run([BASH, "-c", f'{funktion}\nradar_aus'], capture_output=True, text=True, timeout=60,
|
||||
env={**os.environ, "EINSTELLUNGEN": datei.as_posix(),
|
||||
"PATH": f"{shim}{os.pathsep}{os.environ.get('PATH', '')}"})
|
||||
return lauf.returncode == 0
|
||||
|
||||
return pruefen
|
||||
|
||||
|
||||
@pytest.mark.parametrize("inhalt, aus", [
|
||||
(None, False), # keine Datei: an, wie vor dem Schalter
|
||||
('{"radar": {"an": false}}', True),
|
||||
('{"radar": {"an": true}}', False),
|
||||
('{"update_fenster": {"tag": 6, "uhrzeit": "05:00"}}', False),
|
||||
("kaputt", False), ("[]", False), ('{"radar": "aus"}', False),
|
||||
])
|
||||
def test_radar_aus_liest_den_schalter(radar_aus, inhalt, aus):
|
||||
assert radar_aus(inhalt) is aus
|
||||
Reference in New Issue
Block a user