Trading-Plattform-Pruefung: Abendkontrolle holt die Sicherung des Docker-Hosts (Paket E9, Task 725)
abendkontrolle.py holt je Teil mit Quelle dockerhost die Sicherungen der letzten zehn Kalendertage, prueft die Summe gegen die des Hosts, legt unter D:\Trading-Sicherung ab, bestaetigt beim Host und schreibt zuletzt die Summendatei. Zeitbudget 1500 s ab dem Start des Laufs. abendkontrolle_einrichten.ps1 setzt den Abend-Ausloeser in Ortszeit ohne UTC-Versatz (S10). Treiber: 32 Faelle, 42 Mutanten. Gegengeprueft: byte-gleich mit der Plankopie, Kette mit echten Archiven ueber das echte abendkontrolle-lesen.sh gruen. SICHERUNG_ERSTER_TAG bleibt None bis zum Einspielen. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_0126RDfXiECscXipzf67uypJ
This commit is contained in:
1 parent
77de5059e4
commit
c5db92c6de
3 files changed
+1174
-7
No files matched your search
@@ -29,13 +29,21 @@ Augenblick schreibt, schuetzt das nicht (Zeitfenster Lesen bis Ersetzen, Millise
|
|||||||
|
|
||||||
Annahme, benannt: Die Uhr dieses Rechners geht nach Berliner Zeit (tzutil: W. Europe Standard Time,
|
Annahme, benannt: Die Uhr dieses Rechners geht nach Berliner Zeit (tzutil: W. Europe Standard Time,
|
||||||
gelesen am 2026-09-24); ohne das Paket tzdata kennt Python unter Windows keine Zeitzonen-Datenbank.
|
gelesen am 2026-09-24); ohne das Paket tzdata kennt Python unter Windows keine Zeitzonen-Datenbank.
|
||||||
|
Seit Paket E9 holt die Aufgabe fuer jeden Teil mit Quelle dockerhost auch die Sicherung des Hosts ab
|
||||||
|
(hole_sicherungen): je Kalendertag der letzten SICHERUNG_NACHHOLEN_TAGE die Summe des Hosts fragen; wo
|
||||||
|
die Summendatei auf D: fehlt oder abweicht, <tag>-<instanz>.tar ueber denselben Schluessel holen, Summe
|
||||||
|
pruefen, nach SICHERUNG_ZIEL/<tag>/<instanz>.tar umbenennen, beim Host bestaetigen, zuletzt
|
||||||
|
<instanz>.tar.sha256 schreiben; eine Zeile in der Notiz je Tag mit Neuem. Solange SICHERUNG_ERSTER_TAG
|
||||||
|
None ist, holt sie nichts. Den Abend-Ausloeser legt abendkontrolle_einrichten.ps1 in Ortszeit an (S10).
|
||||||
Aufruf von Hand: python abendkontrolle.py [--tag JJJJ-MM-TT] [--vault D:\\Claude] [--ohne-wsl <json-datei>]
|
Aufruf von Hand: python abendkontrolle.py [--tag JJJJ-MM-TT] [--vault D:\\Claude] [--ohne-wsl <json-datei>]
|
||||||
[--quelle wsl|dockerhost] [--quelle-ibkr wsl|dockerhost]
|
[--quelle wsl|dockerhost] [--quelle-ibkr wsl|dockerhost] [--sicherung-ziel <ordner>]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
@@ -44,6 +52,7 @@ from datetime import datetime
|
|||||||
from datetime import time
|
from datetime import time
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from time import monotonic
|
||||||
|
|
||||||
VAULT = Path(r"D:\Claude")
|
VAULT = Path(r"D:\Claude")
|
||||||
PROJEKT_TEIL = Path("Code Projekte") / "active - Trading Plattform"
|
PROJEKT_TEIL = Path("Code Projekte") / "active - Trading Plattform"
|
||||||
@@ -82,6 +91,34 @@ KOEPFE = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Seit Paket E9 (Trading Plattform 0.39.0): Der Host sichert werktags 16:45 New Yorker Zeit je
|
||||||
|
# Instanz Register, data/paper und artifacts/paper (sicherung-host.sh). Diese Aufgabe holt fuer
|
||||||
|
# jeden Teil mit Quelle dockerhost die Sicherung ueber denselben Schluessel ab (abendkontrolle-lesen.sh
|
||||||
|
# mit sicherung-summe, sicherung und sicherung-bestaetigt), prueft die Summe und legt sie unter
|
||||||
|
# SICHERUNG_ZIEL/<tag>/<instanz>.tar samt <instanz>.tar.sha256 ab. Massstab ist die aktuelle Summe des
|
||||||
|
# Hosts: Fehlt die Summendatei auf D: oder weicht sie ab, holt der Lauf neu und ersetzt die alte Fassung
|
||||||
|
# (Plankritik Runde 1, Befunde 5 und 24). Sonst loescht er auf D: nichts.
|
||||||
|
SICHERUNG_ZIEL = Path(r"D:\Trading-Sicherung")
|
||||||
|
# Der erste Tag, den die Aufgabe abholt; None holt nichts. Gesetzt beim Einspielen von E9 (S8): Diese
|
||||||
|
# Datei laeuft schon vorher jeden Abend, der Host kennt die Lesebefehle erst danach.
|
||||||
|
SICHERUNG_ERSTER_TAG: date | None = None
|
||||||
|
# So viele Kalendertage vor heute fragt jeder Lauf, dazu heute. Das Fenster liegt in der Aufbewahrung
|
||||||
|
# des Hosts: Er haelt die neuesten zehn Sicherungstage, nur werktags gesichert, also gut zwei Kalenderwochen.
|
||||||
|
# (S3). Die Sicherung entsteht 16:45 New Yorker Zeit, in Berlin meist 22:45, vom 25.10. bis 31.10.2026
|
||||||
|
# und vom 14.3. bis 27.3.2027 schon 21:45. Die Aufgabe startet 23:15 Ortszeit (S10); fehlt die
|
||||||
|
# Sicherung von heute trotzdem noch, ist das kein Fehlschlag.
|
||||||
|
SICHERUNG_NACHHOLEN_TAGE = 10
|
||||||
|
# Je Archiv; Summe und Bestaetigung nehmen SSH_TIMEOUT_S. Die Aufgabe selbst endet nach 45 Minuten.
|
||||||
|
SICHERUNG_TIMEOUT_S = 600
|
||||||
|
# Zeitbudget je Lauf fuer die Sicherung, geprueft vor jeder Instanz: Ist es um, faengt der Lauf nichts
|
||||||
|
# mehr an, und der naechste holt den Rest (Plankritik Runde 1, Befund 12).
|
||||||
|
SICHERUNG_BUDGET_S = 1500
|
||||||
|
SICHERUNG_KOPF = "Sicherung Trading Plattform (automatisch)"
|
||||||
|
# Eine Zeile im Format von sha256sum: 64 Hexzeichen in Kleinbuchstaben, zwei Leerzeichen, Name, LF.
|
||||||
|
SUMMENZEILE = re.compile(r"([0-9a-f]{64}) (\S+)\n")
|
||||||
|
TAGFORM = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
|
||||||
|
|
||||||
|
|
||||||
def faellige_tage(jetzt: datetime, ablage: Path, erster_tag: date = ERSTER_TAG, endung: str = "") -> list[str]:
|
def faellige_tage(jetzt: datetime, ablage: Path, erster_tag: date = ERSTER_TAG, endung: str = "") -> list[str]:
|
||||||
tage = []
|
tage = []
|
||||||
for zurueck in range(NACHHOLEN_TAGE, -1, -1):
|
for zurueck in range(NACHHOLEN_TAGE, -1, -1):
|
||||||
@@ -155,6 +192,227 @@ def fahre_dockerhost(tag: str, teil: str = "alpaca") -> dict:
|
|||||||
raise RuntimeError(f"keine JSON-Zeile vom Docker-Host (Exit {lauf.returncode}; {' | '.join(fehler)[:300]})")
|
raise RuntimeError(f"keine JSON-Zeile vom Docker-Host (Exit {lauf.returncode}; {' | '.join(fehler)[:300]})")
|
||||||
|
|
||||||
|
|
||||||
|
def ssh_aufruf(befehl: str) -> list[str]:
|
||||||
|
"""Die Befehlszeile fuer einen Befehl der Sicherung, mit denselben Optionen wie fahre_dockerhost."""
|
||||||
|
return [
|
||||||
|
SSH,
|
||||||
|
"-p", SSH_PORT,
|
||||||
|
"-i", str(SSH_SCHLUESSEL),
|
||||||
|
"-o", "BatchMode=yes",
|
||||||
|
"-o", "IdentitiesOnly=yes",
|
||||||
|
"-o", "StrictHostKeyChecking=yes",
|
||||||
|
"-o", f"UserKnownHostsFile={SSH_BEKANNTE}",
|
||||||
|
"-o", "ConnectTimeout=20",
|
||||||
|
SSH_ZIEL,
|
||||||
|
befehl,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def fehlerzeile(lauf: subprocess.CompletedProcess) -> str:
|
||||||
|
"""Exit und die letzten Zeilen von stderr, gekuerzt."""
|
||||||
|
fehler = (lauf.stderr or b"").decode("utf-8", errors="replace").strip().splitlines()[-3:]
|
||||||
|
return f"Exit {lauf.returncode}; {' | '.join(fehler)[:300]}"
|
||||||
|
|
||||||
|
|
||||||
|
def summe_von(datei: Path) -> str:
|
||||||
|
"""SHA-256 einer Datei in Kleinbuchstaben, stueckweise gelesen."""
|
||||||
|
summe = hashlib.sha256()
|
||||||
|
with datei.open("rb") as strom:
|
||||||
|
for stueck in iter(lambda: strom.read(1 << 20), b""):
|
||||||
|
summe.update(stueck)
|
||||||
|
return summe.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def ssh_lauf(befehl: str, timeout, stdout=None) -> subprocess.CompletedProcess:
|
||||||
|
"""Ein Befehl der Sicherung beim Host. Mit stdout (offene Datei) geht die Ausgabe dorthin.
|
||||||
|
|
||||||
|
Eigene Funktion, damit die Angriffsfaelle Zeitlimit und Dauer jedes Befehls sehen.
|
||||||
|
"""
|
||||||
|
if stdout is None:
|
||||||
|
return subprocess.run(ssh_aufruf(befehl), capture_output=True, timeout=timeout, creationflags=KEIN_FENSTER)
|
||||||
|
return subprocess.run(
|
||||||
|
ssh_aufruf(befehl), stdout=stdout, stderr=subprocess.PIPE, timeout=timeout, creationflags=KEIN_FENSTER,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def summe_auf_d(ordner: Path, instanz: str) -> str | None:
|
||||||
|
"""Die Summe aus <instanz>.tar.sha256 in ordner, wenn sie dort richtig steht und das Archiv daneben liegt.
|
||||||
|
|
||||||
|
Die Summendatei schreibt nur hole_sicherung, nach Pruefung und Bestaetigung: Sie steht fuer ein
|
||||||
|
geprueftes und bestaetigtes Archiv. Das Archiv selbst hasht dieser Lauf nicht nach (Plankritik
|
||||||
|
Runde 1, Befund 21); sonst laese jeder Lauf alle Archive des Fensters.
|
||||||
|
"""
|
||||||
|
archiv = ordner / f"{instanz}.tar"
|
||||||
|
try:
|
||||||
|
treffer = SUMMENZEILE.fullmatch((ordner / f"{instanz}.tar.sha256").read_bytes().decode("ascii"))
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
return None
|
||||||
|
if treffer is None or treffer.group(2) != archiv.name or not archiv.is_file():
|
||||||
|
return None
|
||||||
|
return treffer.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def sicherungstage(jetzt: datetime, tag: str | None = None) -> list[str]:
|
||||||
|
"""Die Tage, fuer die der Lauf den Host fragt, der aelteste zuerst.
|
||||||
|
|
||||||
|
Ohne SICHERUNG_ERSTER_TAG keiner. Mit tag (--tag) nur dieser. Sonst jeder Kalendertag der letzten
|
||||||
|
SICHERUNG_NACHHOLEN_TAGE bis heute ab SICHERUNG_ERSTER_TAG, auch Samstag und Sonntag (ein
|
||||||
|
nachgeholter Lauf des Hosts traegt das Datum des Nachholens) und auch heute (Plankritik Runde 1,
|
||||||
|
Befunde 8, 16, 18 und 19). Ob das Ergebnis der Abendkontrolle fuer den Tag schon liegt, spielt
|
||||||
|
keine Rolle.
|
||||||
|
"""
|
||||||
|
erster = SICHERUNG_ERSTER_TAG
|
||||||
|
if erster is None:
|
||||||
|
return []
|
||||||
|
if tag is not None:
|
||||||
|
# Der Tag wird Teil eines Pfads und eines Befehls auf dem Host: nur JJJJ-MM-TT.
|
||||||
|
if not TAGFORM.fullmatch(tag):
|
||||||
|
raise ValueError(f"--tag {tag!r} ist kein Datum JJJJ-MM-TT")
|
||||||
|
date.fromisoformat(tag)
|
||||||
|
return [tag]
|
||||||
|
tage = []
|
||||||
|
for zurueck in range(SICHERUNG_NACHHOLEN_TAGE, -1, -1):
|
||||||
|
kandidat = jetzt.date() - timedelta(days=zurueck)
|
||||||
|
if kandidat >= erster:
|
||||||
|
tage.append(kandidat.isoformat())
|
||||||
|
return tage
|
||||||
|
|
||||||
|
|
||||||
|
def summe_vom_host(name: str) -> str | None:
|
||||||
|
"""Die Summe, die der Host fuer <name> fuehrt; None bei Exit 3 (dort liegt keine Sicherung)."""
|
||||||
|
lauf = ssh_lauf(f"sicherung-summe {name}", SSH_TIMEOUT_S)
|
||||||
|
if lauf.returncode == 3:
|
||||||
|
return None
|
||||||
|
if lauf.returncode != 0:
|
||||||
|
raise RuntimeError(f"Summe nicht gelesen ({fehlerzeile(lauf)})")
|
||||||
|
treffer = SUMMENZEILE.fullmatch(lauf.stdout.decode("ascii", errors="replace"))
|
||||||
|
if treffer is None or treffer.group(2) != f"{name}.tar":
|
||||||
|
raise RuntimeError("Summendatei des Hosts in unerwarteter Form")
|
||||||
|
return treffer.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def ohne_sicherung_still(tag: str, jetzt: datetime) -> bool:
|
||||||
|
"""Ist Exit 3 fuer diesen Tag kein Fehlschlag? Ja fuer heute (und spaeter) und fuer Samstag und Sonntag.
|
||||||
|
|
||||||
|
Heute kann die Sicherung des Hosts noch ausstehen; am Wochenende sichert er nur, wenn er einen
|
||||||
|
verpassten Lauf nachholt. Fehlt sie an einem vergangenen Werktag, ist das ein Fehlschlag.
|
||||||
|
"""
|
||||||
|
kandidat = date.fromisoformat(tag)
|
||||||
|
return kandidat >= jetzt.date() or kandidat.weekday() >= 5
|
||||||
|
|
||||||
|
|
||||||
|
def sichere(tag: str, instanz: str, ziel: Path, jetzt: datetime) -> tuple[str, bool]:
|
||||||
|
"""Eine Instanz eines Tags. Liefert ihren Teil der Zeile und ob geholt wurde; Fehlschlag: Ausnahme.
|
||||||
|
|
||||||
|
Massstab ist die aktuelle Summe des Hosts (Plankritik Runde 1, Befunde 5 und 24). Steht dieselbe in
|
||||||
|
der Summendatei auf D:, ist der Tag geholt und bestaetigt. Sonst holt der Lauf neu und ersetzt die
|
||||||
|
alte Fassung, etwa nachdem der Host den Tag ein zweites Mal gesichert hat.
|
||||||
|
"""
|
||||||
|
name = f"{tag}-{instanz}"
|
||||||
|
summe = summe_vom_host(name)
|
||||||
|
if summe is None:
|
||||||
|
if ohne_sicherung_still(tag, jetzt):
|
||||||
|
return f"{instanz} auf dem Docker-Host noch nicht gesichert", False
|
||||||
|
raise RuntimeError(f"auf dem Docker-Host liegt fuer {name} noch keine Sicherung")
|
||||||
|
if summe_auf_d(ziel / tag, instanz) == summe:
|
||||||
|
return f"{instanz} lag schon vollständig", False
|
||||||
|
return hole_sicherung(tag, instanz, ziel, summe), True
|
||||||
|
|
||||||
|
|
||||||
|
def hole_sicherung(tag: str, instanz: str, ziel: Path, summe: str) -> str:
|
||||||
|
"""Holt die Sicherung <tag>-<instanz> mit der Summe des Hosts, prueft, legt ab und bestaetigt.
|
||||||
|
|
||||||
|
Reihenfolge: Archiv in die Nachbardatei <instanz>.tar.teil holen, Summe pruefen, eine alte
|
||||||
|
<instanz>.tar.sha256 entfernen, auf <instanz>.tar umbenennen (ersetzt eine alte Fassung), beim
|
||||||
|
Host bestaetigen, zuletzt <instanz>.tar.sha256 schreiben. Erst die Summendatei macht die Ablage
|
||||||
|
vollstaendig: Scheitert die Bestaetigung, fehlt sie, und der naechste Lauf holt und bestaetigt
|
||||||
|
erneut. Bei falscher Summe wird nichts umbenannt und nichts bestaetigt. Jeder Fehlschlag ist
|
||||||
|
eine Ausnahme mit dem Grund. Liefert den Teil der Zeile.
|
||||||
|
"""
|
||||||
|
name = f"{tag}-{instanz}"
|
||||||
|
ordner = ziel / tag
|
||||||
|
ordner.mkdir(parents=True, exist_ok=True)
|
||||||
|
teil = ordner / f"{instanz}.tar.teil"
|
||||||
|
archiv = ordner / f"{instanz}.tar"
|
||||||
|
summendatei = ordner / f"{instanz}.tar.sha256"
|
||||||
|
try:
|
||||||
|
with teil.open("wb") as strom:
|
||||||
|
lauf = ssh_lauf(f"sicherung {name}", SICHERUNG_TIMEOUT_S, strom)
|
||||||
|
if lauf.returncode != 0:
|
||||||
|
raise RuntimeError(f"Archiv nicht geholt ({fehlerzeile(lauf)})")
|
||||||
|
if summe_von(teil) != summe:
|
||||||
|
raise RuntimeError("Summe des geholten Archivs ist nicht die des Hosts; nicht abgelegt, nicht bestaetigt")
|
||||||
|
summendatei.unlink(missing_ok=True)
|
||||||
|
os.replace(teil, archiv)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
teil.unlink(missing_ok=True)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
lauf = ssh_lauf(f"sicherung-bestaetigt {name} {summe}", SSH_TIMEOUT_S)
|
||||||
|
if lauf.returncode != 0:
|
||||||
|
raise RuntimeError(f"abgelegt, aber nicht bestaetigt ({fehlerzeile(lauf)})")
|
||||||
|
neu = ordner / f"{instanz}.tar.sha256.neu"
|
||||||
|
neu.write_bytes(f"{summe} {archiv.name}\n".encode("ascii"))
|
||||||
|
os.replace(neu, summendatei)
|
||||||
|
return f"{instanz} abgelegt und bestätigt ({archiv.stat().st_size} Bytes)"
|
||||||
|
|
||||||
|
|
||||||
|
def hole_sicherungen(jetzt: datetime, vault: Path, ablage: Path, ziel: Path, instanzen: list[str],
|
||||||
|
frist: float, tag: str | None = None) -> int:
|
||||||
|
"""Je Tag des Fensters und Instanz: Summe des Hosts fragen und holen, wo D: sie nicht hat.
|
||||||
|
|
||||||
|
Eine Zeile in der Notiz je Tag, an dem etwas geholt wurde, etwas scheiterte oder das Zeitbudget
|
||||||
|
endete; ein Fehlschlag traegt #needs-review. Ein Tag ohne Neues bekommt keine Zeile. frist ist der
|
||||||
|
Stand von monotonic(), an dem SICHERUNG_BUDGET_S ab dem Start des Laufs um ist (main); danach
|
||||||
|
faengt der Lauf keine Instanz mehr an, und der naechste holt den Rest.
|
||||||
|
"""
|
||||||
|
if not instanzen:
|
||||||
|
return 0
|
||||||
|
code = 0
|
||||||
|
erschoepft = False
|
||||||
|
for kandidat in sicherungstage(jetzt, tag):
|
||||||
|
if erschoepft:
|
||||||
|
break
|
||||||
|
uhrzeit = jetzt.strftime("%H:%M") if kandidat == jetzt.date().isoformat() else f"{jetzt:%Y-%m-%d %H:%M}"
|
||||||
|
stuecke = []
|
||||||
|
gescheitert = False
|
||||||
|
getan = False
|
||||||
|
for instanz in instanzen:
|
||||||
|
if monotonic() >= frist:
|
||||||
|
erschoepft = getan = True
|
||||||
|
stuecke.append(
|
||||||
|
f"{instanz} nicht angefangen: Zeitbudget des Laufs ({SICHERUNG_BUDGET_S} s) erschöpft, "
|
||||||
|
"der nächste Lauf holt den Rest"
|
||||||
|
)
|
||||||
|
logge(ablage, f"sicherung {kandidat}-{instanz}: Zeitbudget erschoepft")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
stueck, geholt = sichere(kandidat, instanz, ziel, jetzt)
|
||||||
|
stuecke.append(stueck)
|
||||||
|
if geholt:
|
||||||
|
getan = True
|
||||||
|
logge(ablage, f"sicherung {kandidat}-{instanz}: abgelegt und bestaetigt")
|
||||||
|
except Exception as ausnahme:
|
||||||
|
gescheitert = True
|
||||||
|
stuecke.append(f"{instanz} gescheitert ({type(ausnahme).__name__}: {str(ausnahme)[:200]})")
|
||||||
|
logge(ablage, f"sicherung {kandidat}-{instanz}: gescheitert {ausnahme!r}")
|
||||||
|
if not (gescheitert or getan):
|
||||||
|
continue
|
||||||
|
kopf = f"- {uhrzeit} Uhr — {SICHERUNG_KOPF}"
|
||||||
|
if gescheitert:
|
||||||
|
code = 1
|
||||||
|
zeile = f"{kopf}: **gescheitert** #needs-review. {'; '.join(stuecke)}. Der nächste Lauf versucht es erneut."
|
||||||
|
else:
|
||||||
|
zeile = f"{kopf}: {'; '.join(stuecke)}."
|
||||||
|
try:
|
||||||
|
schreibe_notiz(vault, kandidat, zeile)
|
||||||
|
except Exception as ausnahme:
|
||||||
|
code = 1
|
||||||
|
logge(ablage, f"sicherung {kandidat}: Notiz nicht geschrieben {ausnahme!r}")
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
def zeile_fuer(ergebnis: dict, uhrzeit: str) -> str:
|
def zeile_fuer(ergebnis: dict, uhrzeit: str) -> str:
|
||||||
kopf = f"- {uhrzeit} Uhr — Abendkontrolle Trading Plattform (automatisch)"
|
kopf = f"- {uhrzeit} Uhr — Abendkontrolle Trading Plattform (automatisch)"
|
||||||
befunde = ergebnis.get("befunde") or []
|
befunde = ergebnis.get("befunde") or []
|
||||||
@@ -284,7 +542,14 @@ def main(argv=None) -> int:
|
|||||||
"--quelle-ibkr", choices=QUELLEN,
|
"--quelle-ibkr", choices=QUELLEN,
|
||||||
help="die Quelle fuer den Teil ibkr; ohne Angabe dieselbe wie --quelle",
|
help="die Quelle fuer den Teil ibkr; ohne Angabe dieselbe wie --quelle",
|
||||||
)
|
)
|
||||||
|
zerleger.add_argument(
|
||||||
|
"--sicherung-ziel",
|
||||||
|
help="Ordner fuer die Sicherungen statt SICHERUNG_ZIEL (Angriffsfaelle)",
|
||||||
|
)
|
||||||
argumente = zerleger.parse_args(argv)
|
argumente = zerleger.parse_args(argv)
|
||||||
|
# Seit Paket E9: Das Zeitbudget der Sicherung zaehlt ab hier, also mit der Zeit fuer die
|
||||||
|
# Ergebnisse (bis zu zwoelf Abfragen mit je SSH_TIMEOUT_S; Plankritik Runde 2, Befund 1).
|
||||||
|
frist = monotonic() + SICHERUNG_BUDGET_S
|
||||||
vault = Path(argumente.vault)
|
vault = Path(argumente.vault)
|
||||||
ablage = vault / ABLAGE_TEIL
|
ablage = vault / ABLAGE_TEIL
|
||||||
jetzt = datetime.fromisoformat(argumente.jetzt) if argumente.jetzt else datetime.now()
|
jetzt = datetime.fromisoformat(argumente.jetzt) if argumente.jetzt else datetime.now()
|
||||||
@@ -313,6 +578,21 @@ def main(argv=None) -> int:
|
|||||||
logge(ablage, f"{vorsatz}abgebrochen: {type(ausnahme).__name__}")
|
logge(ablage, f"{vorsatz}abgebrochen: {type(ausnahme).__name__}")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# Seit Paket E9: die Sicherung jedes Teils mit Quelle dockerhost, nach den Ergebnissen beider
|
||||||
|
# Teile und unabhaengig davon, ob ein Ergebnis gescheitert ist.
|
||||||
|
instanzen = [
|
||||||
|
teil for teil in teile
|
||||||
|
if ((argumente.quelle_ibkr or argumente.quelle) if teil == "ibkr" else argumente.quelle) == "dockerhost"
|
||||||
|
]
|
||||||
|
ziel = Path(argumente.sicherung_ziel) if argumente.sicherung_ziel else SICHERUNG_ZIEL
|
||||||
|
try:
|
||||||
|
code = max(code, hole_sicherungen(jetzt, vault, ablage, ziel, instanzen, frist, argumente.tag))
|
||||||
|
except Exception as ausnahme:
|
||||||
|
code = 1
|
||||||
|
try:
|
||||||
|
logge(ablage, f"sicherung abgebrochen: {type(ausnahme).__name__}: {ausnahme}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return code
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,21 +9,36 @@ bricht argparse damit ab, und das soll ein Fall sein, nicht das Ende des Laufs.
|
|||||||
Abgedichtet (Plankritik DH, 2026-09-26): WSL zeigt in jedem Lauf ins Leere, und fahre_wsl ist
|
Abgedichtet (Plankritik DH, 2026-09-26): WSL zeigt in jedem Lauf ins Leere, und fahre_wsl ist
|
||||||
ohne eigene Attrappe des Falls eine Attrappe, die ablehnt; ein Aufruf des echten Wegs ist eine
|
ohne eigene Attrappe des Falls eine Attrappe, die ablehnt; ein Aufruf des echten Wegs ist eine
|
||||||
Abweichung, kein Lauf. Zwei Faelle belegen beide Sperren.
|
Abweichung, kein Lauf. Zwei Faelle belegen beide Sperren.
|
||||||
Aufruf: python abendkontrolle_dockerhost_angriffe.py
|
Seit Paket E9 (Sicherung): Die Attrappe kennt auch sicherung-summe, sicherung und
|
||||||
Exit 0 ohne Abweichung, 2 mit Abweichung.
|
sicherung-bestaetigt, so streng wie abendkontrolle-lesen.sh (Task 724: Formen, Exit 0, 1, 2, 3, 4),
|
||||||
|
gegen eine Host-Ablage im Wegwerf-Ordner; ANGRIFF_SICHERUNG stoert sie gezielt. Abgedichtet:
|
||||||
|
lauf() setzt SICHERUNG_ERSTER_TAG auf None und SICHERUNG_ZIEL in den Wegwerf-Ordner;
|
||||||
|
sicherungslauf() lenkt mit --sicherung-ziel um, und das Ziel der Konstante muss dort leer bleiben.
|
||||||
|
Nichts landet unter D:\\Trading-Sicherung.
|
||||||
|
Aufruf: python abendkontrolle_dockerhost_angriffe.py [--mutanten]
|
||||||
|
Exit 0 ohne Abweichung, 2 mit Abweichung. --mutanten faehrt MUTANTEN auf Kopien von
|
||||||
|
abendkontrolle.py und MUTANTEN_AUFGABE auf Kopien von abendkontrolle_einrichten.ps1: Jeder genannte
|
||||||
|
Fall muss rot werden; Exit 0, wenn keiner ueberlebt, sonst 2. Die .ps1 liest der Treiber nur als Text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import hashlib
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from datetime import date
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
HIER = Path(__file__).resolve().parent
|
HIER = Path(__file__).resolve().parent
|
||||||
|
# Seit Paket E9: das geladene Werkzeug; im Modus --mutanten eine kaputte Kopie (mutanten()).
|
||||||
|
QUELLE = HIER / "abendkontrolle.py"
|
||||||
|
# Seit der Plankritik Runde 1 von E9: das Skript der Aufgabe, nur als Text gelesen; im Modus
|
||||||
|
# --mutanten auch hier eine kaputte Kopie.
|
||||||
|
AUFGABE = HIER / "abendkontrolle_einrichten.ps1"
|
||||||
|
|
||||||
ATTRAPPE_PY = r'''
|
ATTRAPPE_PY = r'''
|
||||||
import json
|
import json
|
||||||
@@ -32,6 +47,71 @@ import sys
|
|||||||
|
|
||||||
with open(os.environ["ANGRIFF_PROTOKOLL"], "a", encoding="utf-8") as datei:
|
with open(os.environ["ANGRIFF_PROTOKOLL"], "a", encoding="utf-8") as datei:
|
||||||
datei.write(json.dumps(sys.argv[1:]) + "\n")
|
datei.write(json.dumps(sys.argv[1:]) + "\n")
|
||||||
|
befehl = sys.argv[-1]
|
||||||
|
if befehl.startswith("sicherung"):
|
||||||
|
# Seit Paket E9: die Lesebefehle der Sicherung, so streng wie abendkontrolle-lesen.sh (Task 724),
|
||||||
|
# gegen die Ablage unter ANGRIFF_HOST. ANGRIFF_SICHERUNG ist <stoerung> oder <stoerung>:<instanz>.
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
host = os.environ["ANGRIFF_HOST"]
|
||||||
|
stoerung, _, nur = os.environ.get("ANGRIFF_SICHERUNG", "").partition(":")
|
||||||
|
lesen = re.fullmatch(r"(sicherung-summe|sicherung) ([0-9]{4}-[0-9]{2}-[0-9]{2}-(?:alpaca|ibkr))", befehl)
|
||||||
|
bestaetigen = re.fullmatch(
|
||||||
|
r"sicherung-bestaetigt ([0-9]{4}-[0-9]{2}-[0-9]{2}-(?:alpaca|ibkr)) ([0-9a-f]{64})", befehl
|
||||||
|
)
|
||||||
|
if lesen is None and bestaetigen is None:
|
||||||
|
sys.stderr.write(
|
||||||
|
"abendkontrolle-lesen: erwartet sicherung-summe|sicherung <JJJJ-MM-TT>-<alpaca|ibkr> "
|
||||||
|
"oder sicherung-bestaetigt <JJJJ-MM-TT>-<alpaca|ibkr> <sha256>\n"
|
||||||
|
)
|
||||||
|
sys.exit(2)
|
||||||
|
name = lesen.group(2) if lesen else bestaetigen.group(1)
|
||||||
|
gestoert = stoerung if not nur or name.endswith("-" + nur) else ""
|
||||||
|
archiv = os.path.join(host, name + ".tar")
|
||||||
|
summendatei = archiv + ".sha256"
|
||||||
|
gemeint = archiv if lesen and lesen.group(1) == "sicherung" else summendatei
|
||||||
|
if not os.path.isfile(gemeint):
|
||||||
|
sys.stderr.write("abendkontrolle-lesen: " + os.path.basename(gemeint) + " liegt auf dem Docker-Host nicht vor\n")
|
||||||
|
sys.exit(3)
|
||||||
|
with open(gemeint, "rb") as quelle:
|
||||||
|
inhalt = quelle.read()
|
||||||
|
if lesen and lesen.group(1) == "sicherung-summe":
|
||||||
|
if gestoert == "summe_gross":
|
||||||
|
inhalt = inhalt[:64].upper() + inhalt[64:]
|
||||||
|
if gestoert == "summe_fremd":
|
||||||
|
inhalt = inhalt.replace(name.encode("ascii"), b"2026-01-02-alpaca")
|
||||||
|
sys.stdout.buffer.write(inhalt)
|
||||||
|
sys.exit(0)
|
||||||
|
if lesen:
|
||||||
|
if gestoert == "kaputt":
|
||||||
|
inhalt = inhalt[:-1] + bytes([inhalt[-1] ^ 1])
|
||||||
|
if gestoert == "abbruch":
|
||||||
|
sys.stdout.buffer.write(inhalt[: len(inhalt) // 2])
|
||||||
|
sys.stdout.buffer.flush()
|
||||||
|
sys.stderr.write("Connection reset by peer\n")
|
||||||
|
sys.exit(255)
|
||||||
|
sys.stdout.buffer.write(inhalt)
|
||||||
|
sys.stdout.buffer.flush()
|
||||||
|
if gestoert == "neu_gesichert":
|
||||||
|
# Der Host sichert denselben Tag neu, waehrend Windows noch holt.
|
||||||
|
neu = inhalt + b" neu gesichert"
|
||||||
|
with open(archiv, "wb") as ziel:
|
||||||
|
ziel.write(neu)
|
||||||
|
with open(summendatei, "wb") as ziel:
|
||||||
|
ziel.write((hashlib.sha256(neu).hexdigest() + " " + name + ".tar\n").encode("ascii"))
|
||||||
|
sys.exit(0)
|
||||||
|
summe = bestaetigen.group(2)
|
||||||
|
if inhalt.split(b"\n")[0].split(b" ")[0].decode("ascii") != summe:
|
||||||
|
sys.stderr.write("abendkontrolle-lesen: die Summe fuer " + name + " ist nicht die des Hosts, nichts bestaetigt\n")
|
||||||
|
sys.exit(4)
|
||||||
|
bestaetigt = os.path.join(host, "bestaetigt")
|
||||||
|
if gestoert == "bestaetigung_scheitert" or not os.path.isdir(bestaetigt):
|
||||||
|
sys.stderr.write("abendkontrolle-lesen: Bestaetigung fuer " + name + " nicht geschrieben\n")
|
||||||
|
sys.exit(1)
|
||||||
|
with open(os.path.join(bestaetigt, name), "wb") as ziel:
|
||||||
|
ziel.write((summe + "\n").encode("ascii"))
|
||||||
|
sys.exit(0)
|
||||||
art = os.environ.get("ANGRIFF_ART", "gut")
|
art = os.environ.get("ANGRIFF_ART", "gut")
|
||||||
if art == "fehlt":
|
if art == "fehlt":
|
||||||
sys.stderr.write("abendkontrolle-lesen: fuer diesen Tag liegt noch kein Ergebnis\n")
|
sys.stderr.write("abendkontrolle-lesen: fuer diesen Tag liegt noch kein Ergebnis\n")
|
||||||
@@ -55,7 +135,8 @@ def fall(funktion):
|
|||||||
|
|
||||||
|
|
||||||
def lade():
|
def lade():
|
||||||
spec = importlib.util.spec_from_file_location("abendkontrolle_unter_angriff", HIER / "abendkontrolle.py")
|
# QUELLE zeigt im Modus --mutanten auf eine kaputte Kopie (seit Paket E9).
|
||||||
|
spec = importlib.util.spec_from_file_location("abendkontrolle_unter_angriff", QUELLE)
|
||||||
modul = importlib.util.module_from_spec(spec)
|
modul = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(modul)
|
spec.loader.exec_module(modul)
|
||||||
return modul
|
return modul
|
||||||
@@ -80,6 +161,13 @@ def lauf(art, *argumente, modul=None):
|
|||||||
# Pfaden mitschreibt und ablehnt -- ausser ein Fall hat seine eigene gesetzt (wsl_attrappe).
|
# Pfaden mitschreibt und ablehnt -- ausser ein Fall hat seine eigene gesetzt (wsl_attrappe).
|
||||||
modul.WSL = str(wurzel / "kein-wsl" / "wsl.exe")
|
modul.WSL = str(wurzel / "kein-wsl" / "wsl.exe")
|
||||||
modul.WSL_SKRIPT = "/nicht-vorhanden/abendkontrolle_wsl.py"
|
modul.WSL_SKRIPT = "/nicht-vorhanden/abendkontrolle_wsl.py"
|
||||||
|
# Abdichtung (Paket E9): Ohne ersten Sicherungstag holt kein Lauf eine Sicherung, und das
|
||||||
|
# Ziel liegt im Wegwerf-Ordner statt unter D:\Trading-Sicherung. Die Faelle der Sicherung
|
||||||
|
# laufen ueber sicherungslauf().
|
||||||
|
modul.SICHERUNG_ERSTER_TAG = None
|
||||||
|
modul.SICHERUNG_ZIEL = wurzel / "sicherung-ziel"
|
||||||
|
os.environ.pop("ANGRIFF_HOST", None)
|
||||||
|
os.environ.pop("ANGRIFF_SICHERUNG", None)
|
||||||
unerwartet = []
|
unerwartet = []
|
||||||
if not getattr(modul.fahre_wsl, "attrappe", False):
|
if not getattr(modul.fahre_wsl, "attrappe", False):
|
||||||
|
|
||||||
@@ -233,8 +321,12 @@ def eine_unbekannte_quelle_ist_ein_bedienfehler():
|
|||||||
|
|
||||||
@fall
|
@fall
|
||||||
def die_heutige_frist_und_die_aufgabe_passen_zueinander():
|
def die_heutige_frist_und_die_aufgabe_passen_zueinander():
|
||||||
quelle = (HIER / "abendkontrolle.py").read_text(encoding="utf-8")
|
# Seit Paket E9 ueber QUELLE und AUFGABE, damit --mutanten auch diesen Fall trifft. Die Uhrzeit der
|
||||||
aufgabe = (HIER / "abendkontrolle_einrichten.ps1").read_text(encoding="utf-8")
|
# Aufgabe gilt nur in Ortszeit, wenn das Skript StartBoundary ohne Versatz setzt (S10; die Form
|
||||||
|
# prueft der_abendausloeser_steht_in_ortszeit_ohne_versatz).
|
||||||
|
quelle = QUELLE.read_text(encoding="utf-8")
|
||||||
|
aufgabe = AUFGABE.read_text(encoding="utf-8")
|
||||||
|
assert "$Abends.StartBoundary = " in aufgabe, "Abend-Ausloeser ohne StartBoundary in Ortszeit"
|
||||||
assert "StrictHostKeyChecking=no" not in quelle
|
assert "StrictHostKeyChecking=no" not in quelle
|
||||||
assert "if tag == jetzt.date() and jetzt.time() < FRUEHESTENS:" in quelle
|
assert "if tag == jetzt.date() and jetzt.time() < FRUEHESTENS:" in quelle
|
||||||
assert aufgabe.count('[ValidateSet("wsl", "dockerhost")]') == 2
|
assert aufgabe.count('[ValidateSet("wsl", "dockerhost")]') == 2
|
||||||
@@ -273,7 +365,797 @@ def der_echte_wsl_aufruf_findet_kein_wsl():
|
|||||||
raise AssertionError("fahre_wsl lief ohne wsl.exe weiter")
|
raise AssertionError("fahre_wsl lief ohne wsl.exe weiter")
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
# --- Seit Paket E9: das Abholen der Sicherung -------------------------------------------------
|
||||||
|
|
||||||
|
TAG = "2026-10-05"
|
||||||
|
KOPF = "Sicherung Trading Plattform (automatisch)"
|
||||||
|
# Bytes, die kein Text sind, auch CR, LF und Strg-Z: Die Uebertragung muss sie unveraendert lassen.
|
||||||
|
ARCHIVE = {
|
||||||
|
"alpaca": b"tar-attrappe alpaca\r\n" + bytes(range(256)),
|
||||||
|
"ibkr": b"tar-attrappe ibkr\n" + bytes(reversed(range(256))),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def host_ablage(wurzel, tage=(TAG,), instanzen=("alpaca", "ibkr")):
|
||||||
|
"""Die Ablage des Hosts wie nach sicherung-host.sh (Task 723): Archiv, Summendatei, bestaetigt/."""
|
||||||
|
host = wurzel / "host"
|
||||||
|
(host / "bestaetigt").mkdir(parents=True)
|
||||||
|
for tag in tage:
|
||||||
|
for instanz in instanzen:
|
||||||
|
inhalt = ARCHIVE[instanz] + tag.encode("ascii")
|
||||||
|
(host / f"{tag}-{instanz}.tar").write_bytes(inhalt)
|
||||||
|
zeile = f"{hashlib.sha256(inhalt).hexdigest()} {tag}-{instanz}.tar\n"
|
||||||
|
(host / f"{tag}-{instanz}.tar.sha256").write_bytes(zeile.encode("ascii"))
|
||||||
|
return host
|
||||||
|
|
||||||
|
|
||||||
|
def host_summe(host, instanz, tag=TAG):
|
||||||
|
"""Die Summe, die der Host fuer <tag>-<instanz> fuehrt."""
|
||||||
|
return (host / f"{tag}-{instanz}.tar.sha256").read_bytes()[:64].decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def sicherungszeilen(notiz):
|
||||||
|
"""Die Zeilen der Notizen, die zur Sicherung gehoeren."""
|
||||||
|
return [zeile for zeile in notiz.splitlines() if KOPF in zeile]
|
||||||
|
|
||||||
|
|
||||||
|
def einzige_zeile(notiz):
|
||||||
|
"""Genau eine Zeile zur Sicherung, sonst eine Abweichung."""
|
||||||
|
zeilen = sicherungszeilen(notiz)
|
||||||
|
assert len(zeilen) == 1, notiz
|
||||||
|
return zeilen[0]
|
||||||
|
|
||||||
|
|
||||||
|
def namen(ordner):
|
||||||
|
return sorted(p.name for p in ordner.iterdir())
|
||||||
|
|
||||||
|
|
||||||
|
def sicherungslauf(wurzel, *argumente, modul=None, stoerung="", erster_tag=date(2026, 10, 5), schalter=True):
|
||||||
|
"""Ein Lauf von main() mit Quelle dockerhost und Sicherung, alles unter wurzel.
|
||||||
|
|
||||||
|
Vault, Host-Ablage (host_ablage) und Ziel bleiben stehen, damit ein zweiter Lauf daran
|
||||||
|
weiterarbeiten kann. Abgedichtet wie lauf(): ssh ist die Attrappe, WSL zeigt ins Leere, und
|
||||||
|
fahre_wsl lehnt ohne eigene Attrappe des Falls ab. Mit schalter geht die Sicherung ueber
|
||||||
|
--sicherung-ziel nach wurzel/ziel, und das Ziel der Konstante (wurzel/konstante-ziel) muss leer
|
||||||
|
bleiben; ohne schalter gilt die Konstante. Liefert Exit, Notizen, die Befehle der Sicherung an
|
||||||
|
den Host und ihre ganzen Befehlszeilen.
|
||||||
|
"""
|
||||||
|
vault = wurzel / "vault"
|
||||||
|
(vault / "Inbox").mkdir(parents=True, exist_ok=True)
|
||||||
|
protokoll = wurzel / "ssh.jsonl"
|
||||||
|
protokoll.unlink(missing_ok=True)
|
||||||
|
attrappe = wurzel / "ssh_attrappe.py"
|
||||||
|
attrappe.write_text(ATTRAPPE_PY, encoding="utf-8")
|
||||||
|
cmd = wurzel / "ssh_attrappe.cmd"
|
||||||
|
cmd.write_text(f'@"{sys.executable}" "{attrappe}" %*\r\n', encoding="utf-8")
|
||||||
|
modul = modul or lade()
|
||||||
|
modul.SSH = str(cmd)
|
||||||
|
modul.WSL = str(wurzel / "kein-wsl" / "wsl.exe")
|
||||||
|
modul.WSL_SKRIPT = "/nicht-vorhanden/abendkontrolle_wsl.py"
|
||||||
|
unerwartet = []
|
||||||
|
if not getattr(modul.fahre_wsl, "attrappe", False):
|
||||||
|
|
||||||
|
def wsl_verboten(tag, teil="alpaca"):
|
||||||
|
unerwartet.append((tag, teil))
|
||||||
|
raise RuntimeError("echter WSL-Weg ohne Attrappe")
|
||||||
|
|
||||||
|
modul.fahre_wsl = wsl_verboten
|
||||||
|
modul.SICHERUNG_ERSTER_TAG = erster_tag
|
||||||
|
modul.SICHERUNG_ZIEL = wurzel / "konstante-ziel"
|
||||||
|
os.environ["ANGRIFF_PROTOKOLL"] = str(protokoll)
|
||||||
|
os.environ["ANGRIFF_ART"] = "gut"
|
||||||
|
os.environ["ANGRIFF_HOST"] = str(wurzel / "host")
|
||||||
|
os.environ["ANGRIFF_SICHERUNG"] = stoerung
|
||||||
|
vorne = ["--vault", str(vault), "--quelle", "dockerhost"]
|
||||||
|
if schalter:
|
||||||
|
vorne += ["--sicherung-ziel", str(wurzel / "ziel")]
|
||||||
|
code = modul.main([*vorne, *argumente])
|
||||||
|
if unerwartet:
|
||||||
|
raise AssertionError(f"WSL-Weg ohne Attrappe gerufen: {unerwartet}")
|
||||||
|
if schalter and (wurzel / "konstante-ziel").exists():
|
||||||
|
raise AssertionError("Sicherung unter SICHERUNG_ZIEL statt unter --sicherung-ziel")
|
||||||
|
notiz = "\n".join(n.read_text(encoding="utf-8") for n in sorted((vault / "Inbox").glob("*.md")))
|
||||||
|
zeilen = protokoll.read_text(encoding="utf-8").splitlines() if protokoll.exists() else []
|
||||||
|
ganz = [json.loads(z) for z in zeilen if z.strip()]
|
||||||
|
ganz = [argumente_ssh for argumente_ssh in ganz if argumente_ssh[-1].startswith("sicherung")]
|
||||||
|
return code, notiz, [argumente_ssh[-1] for argumente_ssh in ganz], ganz
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def sicherung_wird_geholt_geprueft_abgelegt_und_bestaetigt():
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host = host_ablage(wurzel)
|
||||||
|
code, notiz, befehle, ganz = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30")
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
erwartet = []
|
||||||
|
for instanz in ("alpaca", "ibkr"):
|
||||||
|
erwartet += [
|
||||||
|
f"sicherung-summe {TAG}-{instanz}",
|
||||||
|
f"sicherung {TAG}-{instanz}",
|
||||||
|
f"sicherung-bestaetigt {TAG}-{instanz} {host_summe(host, instanz)}",
|
||||||
|
]
|
||||||
|
assert befehle == erwartet, befehle
|
||||||
|
for argumente_ssh in ganz:
|
||||||
|
assert argumente_ssh[:3] == ["-p", "2498", "-i"], argumente_ssh
|
||||||
|
assert argumente_ssh[3].endswith("trading-dh-kontrolle"), argumente_ssh
|
||||||
|
assert argumente_ssh[4:15] == [
|
||||||
|
"-o", "BatchMode=yes",
|
||||||
|
"-o", "IdentitiesOnly=yes",
|
||||||
|
"-o", "StrictHostKeyChecking=yes",
|
||||||
|
"-o", "UserKnownHostsFile=~/.ssh/known_hosts_trading_dh",
|
||||||
|
"-o", "ConnectTimeout=20",
|
||||||
|
"[email protected]",
|
||||||
|
], argumente_ssh
|
||||||
|
assert len(argumente_ssh) == 16, argumente_ssh
|
||||||
|
ziel = wurzel / "ziel" / TAG
|
||||||
|
assert namen(ziel) == ["alpaca.tar", "alpaca.tar.sha256", "ibkr.tar", "ibkr.tar.sha256"], namen(ziel)
|
||||||
|
groessen = {}
|
||||||
|
for instanz in ("alpaca", "ibkr"):
|
||||||
|
inhalt = (host / f"{TAG}-{instanz}.tar").read_bytes()
|
||||||
|
summe = hashlib.sha256(inhalt).hexdigest()
|
||||||
|
assert (ziel / f"{instanz}.tar").read_bytes() == inhalt, instanz
|
||||||
|
assert (ziel / f"{instanz}.tar.sha256").read_bytes() == f"{summe} {instanz}.tar\n".encode("ascii"), instanz
|
||||||
|
assert (host / "bestaetigt" / f"{TAG}-{instanz}").read_bytes() == f"{summe}\n".encode("ascii"), instanz
|
||||||
|
groessen[instanz] = len(inhalt)
|
||||||
|
assert einzige_zeile(notiz) == (
|
||||||
|
f"- 23:30 Uhr — {KOPF}: alpaca abgelegt und bestätigt ({groessen['alpaca']} Bytes); "
|
||||||
|
f"ibkr abgelegt und bestätigt ({groessen['ibkr']} Bytes)."
|
||||||
|
), notiz
|
||||||
|
assert notiz.index("## Agentenprotokoll") < notiz.index(KOPF), notiz
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def eine_falsche_summe_wird_nie_abgelegt_und_nie_bestaetigt():
|
||||||
|
# Reihenfolge: derselbe Schaden an der ersten und an der letzten Instanz.
|
||||||
|
for kaputt, heil in (("alpaca", "ibkr"), ("ibkr", "alpaca")):
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host = host_ablage(wurzel)
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(
|
||||||
|
wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30", stoerung=f"kaputt:{kaputt}"
|
||||||
|
)
|
||||||
|
zeile = einzige_zeile(notiz)
|
||||||
|
assert code == 1, code
|
||||||
|
assert namen(wurzel / "ziel" / TAG) == [f"{heil}.tar", f"{heil}.tar.sha256"], namen(wurzel / "ziel" / TAG)
|
||||||
|
assert namen(host / "bestaetigt") == [f"{TAG}-{heil}"], namen(host / "bestaetigt")
|
||||||
|
assert [b for b in befehle if b.startswith("sicherung-bestaetigt ")] == [
|
||||||
|
f"sicherung-bestaetigt {TAG}-{heil} {host_summe(host, heil)}"
|
||||||
|
], befehle
|
||||||
|
assert "**gescheitert** #needs-review" in zeile, zeile
|
||||||
|
assert f"{kaputt} gescheitert (RuntimeError: Summe des geholten Archivs ist nicht die des Hosts" in zeile, zeile
|
||||||
|
assert f"{heil} abgelegt und bestätigt" in zeile, zeile
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def eine_abgebrochene_uebertragung_hinterlaesst_nichts():
|
||||||
|
for kaputt, heil in (("alpaca", "ibkr"), ("ibkr", "alpaca")):
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host = host_ablage(wurzel)
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(
|
||||||
|
wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30", stoerung=f"abbruch:{kaputt}"
|
||||||
|
)
|
||||||
|
zeile = einzige_zeile(notiz)
|
||||||
|
assert code == 1, code
|
||||||
|
assert namen(wurzel / "ziel" / TAG) == [f"{heil}.tar", f"{heil}.tar.sha256"], namen(wurzel / "ziel" / TAG)
|
||||||
|
assert namen(host / "bestaetigt") == [f"{TAG}-{heil}"], namen(host / "bestaetigt")
|
||||||
|
assert not any(b.startswith(f"sicherung-bestaetigt {TAG}-{kaputt} ") for b in befehle), befehle
|
||||||
|
assert f"{kaputt} gescheitert (RuntimeError: Archiv nicht geholt (Exit 255;" in zeile, zeile
|
||||||
|
assert "#needs-review" in zeile, zeile
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def exit_3_heisst_noch_keine_sicherung_auf_dem_host():
|
||||||
|
for vorhanden in ((), ("ibkr",), ("alpaca",)):
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host = host_ablage(wurzel, instanzen=vorhanden)
|
||||||
|
# Seit der Plankritik Runde 1 ist Exit 3 nur fuer einen vergangenen Werktag ein Fehlschlag:
|
||||||
|
# TAG ist ein Montag, der Lauf kommt am Dienstag.
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-06T23:30")
|
||||||
|
zeile = einzige_zeile(notiz)
|
||||||
|
assert code == 1, (vorhanden, code)
|
||||||
|
assert "**gescheitert** #needs-review" in zeile, zeile
|
||||||
|
for instanz in ("alpaca", "ibkr"):
|
||||||
|
if instanz in vorhanden:
|
||||||
|
assert f"{instanz} abgelegt und bestätigt" in zeile, zeile
|
||||||
|
else:
|
||||||
|
assert f"auf dem Docker-Host liegt fuer {TAG}-{instanz} noch keine Sicherung" in zeile, zeile
|
||||||
|
assert f"sicherung {TAG}-{instanz}" not in befehle, befehle
|
||||||
|
assert not (wurzel / "ziel" / TAG / f"{instanz}.tar").exists(), instanz
|
||||||
|
assert namen(host / "bestaetigt") == [f"{TAG}-{i}" for i in vorhanden], namen(host / "bestaetigt")
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def exit_3_fuer_heute_und_das_wochenende_ist_still():
|
||||||
|
# Plankritik Runde 1, Befunde 8, 16 und 18: Heute kann die Sicherung des Hosts noch ausstehen, und
|
||||||
|
# am Wochenende sichert er nur, wenn er einen Lauf nachholt. Exit 3 ist dann kein Fehlschlag und
|
||||||
|
# gibt fuer sich allein keine Zeile.
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host_ablage(wurzel, tage=())
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(
|
||||||
|
wurzel, "--jetzt", "2026-10-05T18:00", erster_tag=date(2026, 10, 3)
|
||||||
|
)
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
assert befehle == [
|
||||||
|
f"sicherung-summe {t}-{i}" for t in ("2026-10-03", "2026-10-04", "2026-10-05") for i in ("alpaca", "ibkr")
|
||||||
|
], befehle
|
||||||
|
assert sicherungszeilen(notiz) == [], notiz
|
||||||
|
assert not (wurzel / "ziel").exists()
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30")
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
assert befehle == [f"sicherung-summe {TAG}-alpaca", f"sicherung-summe {TAG}-ibkr"], befehle
|
||||||
|
assert sicherungszeilen(notiz) == [], notiz
|
||||||
|
# Fehlt heute nur eine Instanz, nennt die Zeile sie ohne #needs-review.
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host_ablage(wurzel, instanzen=("alpaca",))
|
||||||
|
code, notiz, _, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30")
|
||||||
|
zeile = einzige_zeile(notiz)
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
assert zeile.endswith("ibkr auf dem Docker-Host noch nicht gesichert."), zeile
|
||||||
|
assert "#needs-review" not in zeile, zeile
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def eine_vollstaendige_sicherung_auf_d_wird_nicht_nochmal_geholt():
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host_ablage(wurzel)
|
||||||
|
erster = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30")
|
||||||
|
assert erster[0] == 0, erster[:2]
|
||||||
|
ziel = wurzel / "ziel" / TAG
|
||||||
|
vorher = {p.name: p.read_bytes() for p in ziel.iterdir()}
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:45")
|
||||||
|
assert code == 0, code
|
||||||
|
# Seit der Plankritik Runde 1 fragt jeder Lauf die Summe des Hosts; geholt wird nur bei Abweichung.
|
||||||
|
assert befehle == [f"sicherung-summe {TAG}-alpaca", f"sicherung-summe {TAG}-ibkr"], befehle
|
||||||
|
assert {p.name: p.read_bytes() for p in ziel.iterdir()} == vorher
|
||||||
|
assert len(sicherungszeilen(notiz)) == 1, notiz
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def ein_neu_gesicherter_tag_wird_neu_geholt_und_ersetzt():
|
||||||
|
# Plankritik Runde 1, Befunde 5 und 24: Sichert der Host einen Tag nach dem Abholen ein zweites Mal
|
||||||
|
# (Abnahme 3 von Hand, um 16:45 der Zeitgeber), entfernt er dessen Bestaetigung (Task 723). Der
|
||||||
|
# naechste Lauf sieht die neue Summe, holt neu, ersetzt die alte Fassung auf D: und bestaetigt.
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host = host_ablage(wurzel)
|
||||||
|
erster = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T10:30")
|
||||||
|
assert erster[0] == 0, erster[:2]
|
||||||
|
neu = ARCHIVE["alpaca"] + b" zweiter Lauf"
|
||||||
|
summe = hashlib.sha256(neu).hexdigest()
|
||||||
|
(host / f"{TAG}-alpaca.tar").write_bytes(neu)
|
||||||
|
(host / f"{TAG}-alpaca.tar.sha256").write_bytes(f"{summe} {TAG}-alpaca.tar\n".encode("ascii"))
|
||||||
|
(host / "bestaetigt" / f"{TAG}-alpaca").unlink()
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:15")
|
||||||
|
ziel = wurzel / "ziel" / TAG
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
assert befehle == [
|
||||||
|
f"sicherung-summe {TAG}-alpaca",
|
||||||
|
f"sicherung {TAG}-alpaca",
|
||||||
|
f"sicherung-bestaetigt {TAG}-alpaca {summe}",
|
||||||
|
f"sicherung-summe {TAG}-ibkr",
|
||||||
|
], befehle
|
||||||
|
assert namen(ziel) == ["alpaca.tar", "alpaca.tar.sha256", "ibkr.tar", "ibkr.tar.sha256"], namen(ziel)
|
||||||
|
assert (ziel / "alpaca.tar").read_bytes() == neu
|
||||||
|
assert (ziel / "alpaca.tar.sha256").read_bytes() == f"{summe} alpaca.tar\n".encode("ascii")
|
||||||
|
assert (host / "bestaetigt" / f"{TAG}-alpaca").read_bytes() == f"{summe}\n".encode("ascii")
|
||||||
|
zeilen = sicherungszeilen(notiz)
|
||||||
|
assert len(zeilen) == 2, notiz
|
||||||
|
assert "alpaca abgelegt und bestätigt" in zeilen[1] and "ibkr lag schon vollständig" in zeilen[1], zeilen[1]
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def nur_die_fehlende_instanz_wird_geholt():
|
||||||
|
for da, fehlt in (("alpaca", "ibkr"), ("ibkr", "alpaca")):
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host = host_ablage(wurzel)
|
||||||
|
ziel = wurzel / "ziel" / TAG
|
||||||
|
ziel.mkdir(parents=True)
|
||||||
|
(ziel / f"{da}.tar").write_bytes((host / f"{TAG}-{da}.tar").read_bytes())
|
||||||
|
(ziel / f"{da}.tar.sha256").write_bytes(f"{host_summe(host, da)} {da}.tar\n".encode("ascii"))
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30")
|
||||||
|
zeile = einzige_zeile(notiz)
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
summen = [b.split(" ")[1] for b in befehle if b.startswith("sicherung-summe ")]
|
||||||
|
assert summen == [f"{TAG}-alpaca", f"{TAG}-ibkr"], befehle
|
||||||
|
assert [b.split(" ")[1] for b in befehle if not b.startswith("sicherung-summe ")] == [f"{TAG}-{fehlt}"] * 2, befehle
|
||||||
|
assert f"{da} lag schon vollständig" in zeile and f"{fehlt} abgelegt und bestätigt" in zeile, zeile
|
||||||
|
assert namen(host / "bestaetigt") == [f"{TAG}-{fehlt}"], namen(host / "bestaetigt")
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def fremder_inhalt_im_zielordner_bleibt_und_zaehlt_nicht_als_sicherung():
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host = host_ablage(wurzel)
|
||||||
|
ziel = wurzel / "ziel" / TAG
|
||||||
|
ziel.mkdir(parents=True)
|
||||||
|
(ziel / "notiz.txt").write_bytes(b"fremd\n")
|
||||||
|
(ziel / "alpaca.tar.teil").write_bytes(b"Rest eines abgebrochenen Laufs")
|
||||||
|
# alpaca: die passende Summendatei, aber kein Archiv daneben. ibkr: richtiges Archiv und richtige
|
||||||
|
# Summe, aber in der Summendatei der Name einer anderen Datei. Das Archiv selbst hasht der Lauf
|
||||||
|
# nicht nach (Plankritik Runde 1, Befund 21); beides zaehlt trotzdem nicht als Sicherung.
|
||||||
|
(ziel / "alpaca.tar.sha256").write_bytes(f"{host_summe(host, 'alpaca')} alpaca.tar\n".encode("ascii"))
|
||||||
|
(ziel / "ibkr.tar").write_bytes((host / f"{TAG}-ibkr.tar").read_bytes())
|
||||||
|
(ziel / "ibkr.tar.sha256").write_bytes(f"{host_summe(host, 'ibkr')} alpaca.tar\n".encode("ascii"))
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30")
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
assert len([b for b in befehle if b.startswith("sicherung-bestaetigt ")]) == 2, befehle
|
||||||
|
assert namen(ziel) == ["alpaca.tar", "alpaca.tar.sha256", "ibkr.tar", "ibkr.tar.sha256", "notiz.txt"], namen(ziel)
|
||||||
|
assert (ziel / "notiz.txt").read_bytes() == b"fremd\n"
|
||||||
|
for instanz in ("alpaca", "ibkr"):
|
||||||
|
summe = host_summe(host, instanz)
|
||||||
|
assert (ziel / f"{instanz}.tar").read_bytes() == (host / f"{TAG}-{instanz}.tar").read_bytes(), instanz
|
||||||
|
assert (ziel / f"{instanz}.tar.sha256").read_bytes() == f"{summe} {instanz}.tar\n".encode("ascii"), instanz
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def ein_tagesordner_der_eine_datei_ist_ist_ein_befund():
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host = host_ablage(wurzel)
|
||||||
|
(wurzel / "ziel").mkdir()
|
||||||
|
(wurzel / "ziel" / TAG).write_bytes(b"keine Sicherung\n")
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30")
|
||||||
|
zeile = einzige_zeile(notiz)
|
||||||
|
assert code == 1, code
|
||||||
|
assert befehle == [f"sicherung-summe {TAG}-alpaca", f"sicherung-summe {TAG}-ibkr"], befehle
|
||||||
|
assert (wurzel / "ziel" / TAG).read_bytes() == b"keine Sicherung\n"
|
||||||
|
assert namen(host / "bestaetigt") == [], namen(host / "bestaetigt")
|
||||||
|
assert "**gescheitert** #needs-review" in zeile and zeile.count("FileExistsError") == 2, zeile
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def eine_gescheiterte_bestaetigung_laesst_die_summendatei_weg_und_wird_nachgeholt():
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host = host_ablage(wurzel)
|
||||||
|
ziel = wurzel / "ziel" / TAG
|
||||||
|
ziel.mkdir(parents=True)
|
||||||
|
# Eine aeltere, in sich stimmige Fassung: Der Host hat den Tag seitdem neu gesichert. Ihre
|
||||||
|
# Summendatei muss vor dem Umbenennen weg, sonst laege neben dem neuen Archiv eine Summe, die
|
||||||
|
# nicht passt, und sha256sum -c schluege bei der Ruecksicherung fehl.
|
||||||
|
alt = b"aeltere Fassung"
|
||||||
|
(ziel / "alpaca.tar").write_bytes(alt)
|
||||||
|
(ziel / "alpaca.tar.sha256").write_bytes(f"{hashlib.sha256(alt).hexdigest()} alpaca.tar\n".encode("ascii"))
|
||||||
|
code, notiz, _, _ = sicherungslauf(
|
||||||
|
wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30", stoerung="bestaetigung_scheitert"
|
||||||
|
)
|
||||||
|
zeile = einzige_zeile(notiz)
|
||||||
|
assert code == 1, code
|
||||||
|
assert namen(ziel) == ["alpaca.tar", "ibkr.tar"], namen(ziel)
|
||||||
|
assert namen(host / "bestaetigt") == [], namen(host / "bestaetigt")
|
||||||
|
assert "#needs-review" in zeile and zeile.count("abgelegt, aber nicht bestaetigt (Exit 1;") == 2, zeile
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:45")
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
assert len([b for b in befehle if b.startswith("sicherung-bestaetigt ")]) == 2, befehle
|
||||||
|
assert namen(ziel) == ["alpaca.tar", "alpaca.tar.sha256", "ibkr.tar", "ibkr.tar.sha256"], namen(ziel)
|
||||||
|
assert namen(host / "bestaetigt") == [f"{TAG}-alpaca", f"{TAG}-ibkr"], namen(host / "bestaetigt")
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def eine_vom_host_abgelehnte_summe_ist_ein_befund_und_wird_neu_geholt():
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host = host_ablage(wurzel)
|
||||||
|
alt = (host / f"{TAG}-ibkr.tar").read_bytes()
|
||||||
|
code, notiz, _, _ = sicherungslauf(
|
||||||
|
wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30", stoerung="neu_gesichert:ibkr"
|
||||||
|
)
|
||||||
|
ziel = wurzel / "ziel" / TAG
|
||||||
|
zeile = einzige_zeile(notiz)
|
||||||
|
assert code == 1, code
|
||||||
|
assert namen(ziel) == ["alpaca.tar", "alpaca.tar.sha256", "ibkr.tar"], namen(ziel)
|
||||||
|
assert (ziel / "ibkr.tar").read_bytes() == alt
|
||||||
|
assert namen(host / "bestaetigt") == [f"{TAG}-alpaca"], namen(host / "bestaetigt")
|
||||||
|
assert "ibkr gescheitert (RuntimeError: abgelegt, aber nicht bestaetigt (Exit 4;" in zeile, zeile
|
||||||
|
assert "#needs-review" in zeile, zeile
|
||||||
|
code, _, _, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:45")
|
||||||
|
neu = (host / f"{TAG}-ibkr.tar").read_bytes()
|
||||||
|
assert code == 0, code
|
||||||
|
assert neu != alt and (ziel / "ibkr.tar").read_bytes() == neu
|
||||||
|
assert (host / "bestaetigt" / f"{TAG}-ibkr").read_bytes() == f"{host_summe(host, 'ibkr')}\n".encode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def eine_summendatei_des_hosts_in_falscher_form_holt_kein_archiv():
|
||||||
|
for stoerung in ("summe_gross", "summe_fremd"):
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host = host_ablage(wurzel)
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(
|
||||||
|
wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30", stoerung=stoerung
|
||||||
|
)
|
||||||
|
zeile = einzige_zeile(notiz)
|
||||||
|
assert code == 1, (stoerung, code)
|
||||||
|
assert befehle == [f"sicherung-summe {TAG}-alpaca", f"sicherung-summe {TAG}-ibkr"], (stoerung, befehle)
|
||||||
|
assert not (wurzel / "ziel" / TAG).exists(), stoerung
|
||||||
|
assert namen(host / "bestaetigt") == [], (stoerung, namen(host / "bestaetigt"))
|
||||||
|
assert "#needs-review" in zeile and zeile.count("Summendatei des Hosts in unerwarteter Form") == 2, zeile
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def ohne_ersten_sicherungstag_holt_kein_lauf_eine_sicherung():
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host_ablage(wurzel)
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(
|
||||||
|
wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30", erster_tag=None
|
||||||
|
)
|
||||||
|
assert code == 0, code
|
||||||
|
assert befehle == [], befehle
|
||||||
|
assert not (wurzel / "ziel").exists()
|
||||||
|
assert sicherungszeilen(notiz) == [], notiz
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def faellige_tage_der_sicherung_sind_alle_kalendertage_des_fensters():
|
||||||
|
# Plankritik Runde 1, Befunde 16, 18 und 19: Gefragt wird jeder Kalendertag der letzten
|
||||||
|
# SICHERUNG_NACHHOLEN_TAGE bis heute, der aelteste zuerst; auch Samstag und Sonntag (ein
|
||||||
|
# nachgeholter Lauf des Hosts traegt das Datum des Nachholens) und heute schon vor 23 Uhr.
|
||||||
|
auf_dem_host = [
|
||||||
|
"2026-10-01", "2026-10-02", "2026-10-05", "2026-10-06", "2026-10-07", "2026-10-08", "2026-10-09",
|
||||||
|
"2026-10-10", "2026-10-12",
|
||||||
|
]
|
||||||
|
fenster = [f"2026-10-{t:02d}" for t in range(2, 13)]
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host_ablage(wurzel, tage=auf_dem_host)
|
||||||
|
modul = lade()
|
||||||
|
# Die Ergebnisse der Abendkontrolle liegen schon alle; die Sicherung ist trotzdem faellig.
|
||||||
|
ablage = wurzel / "vault" / modul.ABLAGE_TEIL
|
||||||
|
ablage.mkdir(parents=True)
|
||||||
|
for tag in fenster:
|
||||||
|
(ablage / f"{tag}.json").write_text("{}", encoding="utf-8")
|
||||||
|
(ablage / f"{tag}-ibkr.json").write_text("{}", encoding="utf-8")
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(
|
||||||
|
wurzel, "--jetzt", "2026-10-12T10:00", modul=modul, erster_tag=date(2026, 9, 1)
|
||||||
|
)
|
||||||
|
summen = [b.split(" ")[1] for b in befehle if b.startswith("sicherung-summe ")]
|
||||||
|
geholt = [b.split(" ")[1] for b in befehle if b.startswith("sicherung ")]
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
assert summen == [f"{t}-{i}" for t in fenster for i in ("alpaca", "ibkr")], summen
|
||||||
|
# 2026-10-01 liegt vor dem Fenster; 10-03, 10-04 und 10-11 fehlen auf dem Host (Wochenende, still).
|
||||||
|
assert geholt == [f"{t}-{i}" for t in auf_dem_host[1:] for i in ("alpaca", "ibkr")], geholt
|
||||||
|
# Der erste Sicherungstag begrenzt das Fenster von unten; schon Geholtes wird nur nachgefragt.
|
||||||
|
code, _, befehle, _ = sicherungslauf(wurzel, "--jetzt", "2026-10-12T10:30", erster_tag=date(2026, 10, 6))
|
||||||
|
summen = [b.split(" ")[1] for b in befehle if b.startswith("sicherung-summe ")]
|
||||||
|
assert code == 0, code
|
||||||
|
assert summen == [f"2026-10-{t:02d}-{i}" for t in range(6, 13) for i in ("alpaca", "ibkr")], summen
|
||||||
|
assert [b for b in befehle if not b.startswith("sicherung-summe ")] == [], befehle
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def das_zeitbudget_begrenzt_das_holen_und_jedes_archiv_hat_sein_zeitlimit():
|
||||||
|
# Plankritik Runde 1, Befund 12: Jedes Archiv hat SICHERUNG_TIMEOUT_S (600 s), der ganze Lauf
|
||||||
|
# SICHERUNG_BUDGET_S (1500 s). Runde 2, Befunde 1 und 4: Das Budget zaehlt ab dem Start des Laufs,
|
||||||
|
# die Ergebnisse eingerechnet, und ist bei genau 1500 s um. Die Uhr ist eine Attrappe: Jeder
|
||||||
|
# Ergebnisteil stellt sie 400 s vor, jede Uebertragung 700 s; vor ibkr steht sie auf 1500.
|
||||||
|
tage = ["2026-10-01", "2026-10-02", "2026-10-05"]
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host_ablage(wurzel, tage=tage)
|
||||||
|
modul = lade()
|
||||||
|
echt = modul.ssh_lauf
|
||||||
|
echt_teil = modul.fahre_teil
|
||||||
|
uhr = [0.0]
|
||||||
|
zeitlimits = []
|
||||||
|
|
||||||
|
def ssh_lauf(befehl, timeout, stdout=None):
|
||||||
|
if befehl.startswith("sicherung "):
|
||||||
|
zeitlimits.append(timeout)
|
||||||
|
uhr[0] += 700
|
||||||
|
return echt(befehl, timeout, stdout)
|
||||||
|
|
||||||
|
def fahre_teil(*argumente, **benannt):
|
||||||
|
uhr[0] += 400
|
||||||
|
return echt_teil(*argumente, **benannt)
|
||||||
|
|
||||||
|
modul.ssh_lauf = ssh_lauf
|
||||||
|
modul.fahre_teil = fahre_teil
|
||||||
|
modul.monotonic = lambda: uhr[0]
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(
|
||||||
|
wurzel, "--jetzt", "2026-10-05T23:30", modul=modul, erster_tag=date(2026, 10, 1)
|
||||||
|
)
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
assert uhr[0] == 1500, uhr
|
||||||
|
assert zeitlimits == [600], zeitlimits
|
||||||
|
assert [b.split(" ")[1] for b in befehle if b.startswith("sicherung ")] == ["2026-10-01-alpaca"], befehle
|
||||||
|
# Nach dem Budget fragt der Lauf auch keine Summe mehr.
|
||||||
|
assert [b.split(" ")[1] for b in befehle if b.startswith("sicherung-summe ")] == [
|
||||||
|
"2026-10-01-alpaca"
|
||||||
|
], befehle
|
||||||
|
zeilen = sicherungszeilen(notiz)
|
||||||
|
assert len(zeilen) == 1, notiz
|
||||||
|
assert zeilen[0].endswith(
|
||||||
|
"ibkr nicht angefangen: Zeitbudget des Laufs (1500 s) erschöpft, der nächste Lauf holt den Rest."
|
||||||
|
), zeilen[0]
|
||||||
|
assert "#needs-review" not in "\n".join(zeilen), zeilen
|
||||||
|
# Der naechste Lauf holt den Rest; die schon geholten Tage fragt er nur nach der Summe. Seine
|
||||||
|
# Ergebnisse liegen schon, sein Ergebnisteil kostet hier keine Zeit.
|
||||||
|
modul.fahre_teil = echt_teil
|
||||||
|
uhr[0] = 0.0
|
||||||
|
zeitlimits.clear()
|
||||||
|
code, _, befehle, _ = sicherungslauf(
|
||||||
|
wurzel, "--jetzt", "2026-10-05T23:45", modul=modul, erster_tag=date(2026, 10, 1)
|
||||||
|
)
|
||||||
|
assert code == 0, code
|
||||||
|
assert [b.split(" ")[1] for b in befehle if b.startswith("sicherung ")] == [
|
||||||
|
"2026-10-01-ibkr", "2026-10-02-alpaca", "2026-10-02-ibkr"
|
||||||
|
], befehle
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def nur_teile_mit_quelle_dockerhost_holen_eine_sicherung():
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host_ablage(wurzel)
|
||||||
|
modul = lade()
|
||||||
|
gerufen = []
|
||||||
|
wsl_attrappe(modul, gerufen)
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(
|
||||||
|
wurzel, "--quelle-ibkr", "wsl", "--tag", TAG, "--jetzt", "2026-10-05T23:30", modul=modul
|
||||||
|
)
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
assert gerufen == [(TAG, "ibkr")], gerufen
|
||||||
|
assert [b.split(" ")[1] for b in befehle] == [f"{TAG}-alpaca"] * 3, befehle
|
||||||
|
assert "ibkr" not in einzige_zeile(notiz), notiz
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host_ablage(wurzel)
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(wurzel, "--teil", "ibkr", "--tag", TAG, "--jetzt", "2026-10-05T23:30")
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
assert [b.split(" ")[1] for b in befehle] == [f"{TAG}-ibkr"] * 3, befehle
|
||||||
|
assert "alpaca" not in einzige_zeile(notiz), notiz
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def ohne_schalter_gilt_das_ziel_der_konstante():
|
||||||
|
assert lade().SICHERUNG_ZIEL == Path(r"D:\Trading-Sicherung"), "Ziel der Konstante"
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host_ablage(wurzel)
|
||||||
|
code, notiz, _, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30", schalter=False)
|
||||||
|
assert code == 0, (code, notiz)
|
||||||
|
ziel = wurzel / "konstante-ziel" / TAG
|
||||||
|
assert namen(ziel) == ["alpaca.tar", "alpaca.tar.sha256", "ibkr.tar", "ibkr.tar.sha256"], namen(ziel)
|
||||||
|
assert not (wurzel / "ziel").exists()
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def ein_tag_ausser_der_form_holt_keine_sicherung():
|
||||||
|
for tag in ("2026-10-5", "2026-13-05", "20261005", "../2026-10-05"):
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
wurzel = Path(ordner)
|
||||||
|
host_ablage(wurzel)
|
||||||
|
code, notiz, befehle, _ = sicherungslauf(wurzel, "--tag", tag, "--jetzt", "2026-10-05T23:30")
|
||||||
|
assert code == 1, (tag, code)
|
||||||
|
assert befehle == [], (tag, befehle)
|
||||||
|
assert not (wurzel / "ziel").exists(), tag
|
||||||
|
assert sicherungszeilen(notiz) == [], (tag, notiz)
|
||||||
|
|
||||||
|
|
||||||
|
@fall
|
||||||
|
def der_abendausloeser_steht_in_ortszeit_ohne_versatz():
|
||||||
|
# S10 (Plankritik Runde 1, Befund 8): New-ScheduledTaskTrigger -At legt StartBoundary mit UTC-Versatz
|
||||||
|
# an, und die Aufgabe liefe im Winter um 22:15 Ortszeit. Ohne Versatz gesetzt, folgt sie der Ortszeit
|
||||||
|
# (gemessen vom Koordinator am 2026-10-11). Geprueft wird nur der Text des Skripts; nichts wird
|
||||||
|
# eingetragen.
|
||||||
|
zeilen = AUFGABE.read_text(encoding="utf-8").splitlines()
|
||||||
|
zeile = "$Abends.StartBoundary = (Get-Date -Format 'yyyy-MM-dd') + 'T' + $Uhrzeit + ':00'"
|
||||||
|
assert zeilen.count(zeile) == 1, f"StartBoundary in Ortszeit {zeilen.count(zeile)}-mal"
|
||||||
|
gesetzt = [z for z in zeilen if ".StartBoundary =" in z]
|
||||||
|
assert gesetzt == [zeile], gesetzt
|
||||||
|
ausloeser = [i for i, z in enumerate(zeilen) if z.startswith("$Abends = New-ScheduledTaskTrigger -Weekly ")]
|
||||||
|
eintragen = [i for i, z in enumerate(zeilen) if z.startswith("Register-ScheduledTask ")]
|
||||||
|
assert len(ausloeser) == 1 and len(eintragen) == 1, (ausloeser, eintragen)
|
||||||
|
assert ausloeser[0] < zeilen.index(zeile) < eintragen[0], (ausloeser, zeilen.index(zeile), eintragen)
|
||||||
|
|
||||||
|
|
||||||
|
# Mutanten fuer --mutanten: Name, Text in abendkontrolle.py (genau einmal), Ersatz und die Faelle,
|
||||||
|
# die davon rot werden muessen. MUTANTEN_AUFGABE (unten) tut dasselbe fuer abendkontrolle_einrichten.ps1.
|
||||||
|
# Ein Mutant, der das Verhalten nicht aendert, gehoert nicht hierher
|
||||||
|
# (AI/Regelwerk/pruefwerkzeuge.md, Abschnitt 8).
|
||||||
|
MUTANTEN = [
|
||||||
|
("Summe ungeprueft", " if summe_von(teil) != summe:", " if False:",
|
||||||
|
["eine_falsche_summe_wird_nie_abgelegt_und_nie_bestaetigt"]),
|
||||||
|
("Exit des Archivs egal",
|
||||||
|
' if lauf.returncode != 0:\n raise RuntimeError(f"Archiv nicht geholt',
|
||||||
|
' if False:\n raise RuntimeError(f"Archiv nicht geholt',
|
||||||
|
["eine_abgebrochene_uebertragung_hinterlaesst_nichts"]),
|
||||||
|
("Nachbardatei bleibt", " teil.unlink(missing_ok=True)", " pass",
|
||||||
|
["eine_falsche_summe_wird_nie_abgelegt_und_nie_bestaetigt", "eine_abgebrochene_uebertragung_hinterlaesst_nichts"]),
|
||||||
|
("Bestaetigung ungeprueft",
|
||||||
|
' if lauf.returncode != 0:\n raise RuntimeError(f"abgelegt, aber nicht bestaetigt',
|
||||||
|
' if False:\n raise RuntimeError(f"abgelegt, aber nicht bestaetigt',
|
||||||
|
["eine_gescheiterte_bestaetigung_laesst_die_summendatei_weg_und_wird_nachgeholt",
|
||||||
|
"eine_vom_host_abgelehnte_summe_ist_ein_befund_und_wird_neu_geholt"]),
|
||||||
|
("alte Summendatei bleibt", " summendatei.unlink(missing_ok=True)", " pass",
|
||||||
|
["eine_gescheiterte_bestaetigung_laesst_die_summendatei_weg_und_wird_nachgeholt"]),
|
||||||
|
("erster Tag egal", " if erster is None:\n return []", " if False:\n return []",
|
||||||
|
["ohne_ersten_sicherungstag_holt_kein_lauf_eine_sicherung", "ergebnis_vom_host_landet_in_ablage_und_notiz"]),
|
||||||
|
("Fenster 9 Tage", "SICHERUNG_NACHHOLEN_TAGE = 10", "SICHERUNG_NACHHOLEN_TAGE = 9",
|
||||||
|
["faellige_tage_der_sicherung_sind_alle_kalendertage_des_fensters"]),
|
||||||
|
("Fenster 11 Tage", "SICHERUNG_NACHHOLEN_TAGE = 10", "SICHERUNG_NACHHOLEN_TAGE = 11",
|
||||||
|
["faellige_tage_der_sicherung_sind_alle_kalendertage_des_fensters"]),
|
||||||
|
("heute nicht im Fenster", "range(SICHERUNG_NACHHOLEN_TAGE, -1, -1)", "range(SICHERUNG_NACHHOLEN_TAGE, 0, -1)",
|
||||||
|
["faellige_tage_der_sicherung_sind_alle_kalendertage_des_fensters"]),
|
||||||
|
("erster Tag im Fenster egal", " if kandidat >= erster:", " if True:",
|
||||||
|
["faellige_tage_der_sicherung_sind_alle_kalendertage_des_fensters"]),
|
||||||
|
("Wochenende nicht im Fenster", " if kandidat >= erster:",
|
||||||
|
" if kandidat >= erster and kandidat.weekday() < 5:",
|
||||||
|
["faellige_tage_der_sicherung_sind_alle_kalendertage_des_fensters"]),
|
||||||
|
("Archiv auf D: egal", " if treffer is None or treffer.group(2) != archiv.name or not archiv.is_file():",
|
||||||
|
" if treffer is None or treffer.group(2) != archiv.name:",
|
||||||
|
["fremder_inhalt_im_zielordner_bleibt_und_zaehlt_nicht_als_sicherung"]),
|
||||||
|
("Name auf D: ungeprueft", " if treffer is None or treffer.group(2) != archiv.name or not archiv.is_file():",
|
||||||
|
" if treffer is None or not archiv.is_file():",
|
||||||
|
["fremder_inhalt_im_zielordner_bleibt_und_zaehlt_nicht_als_sicherung"]),
|
||||||
|
("nie vollstaendig", " if summe_auf_d(ziel / tag, instanz) == summe:", " if False:",
|
||||||
|
["eine_vollstaendige_sicherung_auf_d_wird_nicht_nochmal_geholt", "nur_die_fehlende_instanz_wird_geholt"]),
|
||||||
|
("Summe des Hosts egal", " if summe_auf_d(ziel / tag, instanz) == summe:",
|
||||||
|
" if summe_auf_d(ziel / tag, instanz) is not None:",
|
||||||
|
["ein_neu_gesicherter_tag_wird_neu_geholt_und_ersetzt"]),
|
||||||
|
("Zeile fuer jeden Tag", " if not (gescheitert or getan):\n continue",
|
||||||
|
" if False:\n continue",
|
||||||
|
["eine_vollstaendige_sicherung_auf_d_wird_nicht_nochmal_geholt", "exit_3_fuer_heute_und_das_wochenende_ist_still"]),
|
||||||
|
("Quelle egal",
|
||||||
|
' if ((argumente.quelle_ibkr or argumente.quelle) if teil == "ibkr" else argumente.quelle) == "dockerhost"',
|
||||||
|
" if True", ["nur_teile_mit_quelle_dockerhost_holen_eine_sicherung"]),
|
||||||
|
("Exit 3 ohne Grund", " if lauf.returncode == 3:\n return None", " if False:\n return None",
|
||||||
|
["exit_3_heisst_noch_keine_sicherung_auf_dem_host", "exit_3_fuer_heute_und_das_wochenende_ist_still"]),
|
||||||
|
("Exit 3 immer still", " if ohne_sicherung_still(tag, jetzt):", " if True:",
|
||||||
|
["exit_3_heisst_noch_keine_sicherung_auf_dem_host"]),
|
||||||
|
("heute nicht still", " return kandidat >= jetzt.date() or kandidat.weekday() >= 5",
|
||||||
|
" return kandidat > jetzt.date() or kandidat.weekday() >= 5",
|
||||||
|
["exit_3_fuer_heute_und_das_wochenende_ist_still"]),
|
||||||
|
("Wochenende nicht still", " return kandidat >= jetzt.date() or kandidat.weekday() >= 5",
|
||||||
|
" return kandidat >= jetzt.date()", ["exit_3_fuer_heute_und_das_wochenende_ist_still"]),
|
||||||
|
("Fehlschlag ohne Markierung", "**gescheitert** #needs-review. {'; '.join(stuecke)}",
|
||||||
|
"**gescheitert**. {'; '.join(stuecke)}", ["eine_falsche_summe_wird_nie_abgelegt_und_nie_bestaetigt"]),
|
||||||
|
("Fehlschlag reisst den Tag ab",
|
||||||
|
" except Exception as ausnahme:\n gescheitert = True",
|
||||||
|
" except ZeroDivisionError as ausnahme:\n gescheitert = True",
|
||||||
|
["eine_falsche_summe_wird_nie_abgelegt_und_nie_bestaetigt", "ein_tagesordner_der_eine_datei_ist_ist_ein_befund"]),
|
||||||
|
("Zielschalter egal",
|
||||||
|
" ziel = Path(argumente.sicherung_ziel) if argumente.sicherung_ziel else SICHERUNG_ZIEL",
|
||||||
|
" ziel = SICHERUNG_ZIEL", ["sicherung_wird_geholt_geprueft_abgelegt_und_bestaetigt"]),
|
||||||
|
("Rueckfall nicht auf die Konstante",
|
||||||
|
" ziel = Path(argumente.sicherung_ziel) if argumente.sicherung_ziel else SICHERUNG_ZIEL",
|
||||||
|
' ziel = Path(argumente.sicherung_ziel) if argumente.sicherung_ziel else SICHERUNG_ZIEL / "x"',
|
||||||
|
["ohne_schalter_gilt_das_ziel_der_konstante"]),
|
||||||
|
("Ziel der Konstante", r'SICHERUNG_ZIEL = Path(r"D:\Trading-Sicherung")',
|
||||||
|
r'SICHERUNG_ZIEL = Path(r"D:\Trading-Sicherung-alt")', ["ohne_schalter_gilt_das_ziel_der_konstante"]),
|
||||||
|
("Tag ungeprueft",
|
||||||
|
' if not TAGFORM.fullmatch(tag):\n raise ValueError(f"--tag {tag!r} ist kein Datum JJJJ-MM-TT")\n'
|
||||||
|
" date.fromisoformat(tag)",
|
||||||
|
" pass", ["ein_tag_ausser_der_form_holt_keine_sicherung"]),
|
||||||
|
("Datum ungeprueft", " date.fromisoformat(tag)", " pass", ["ein_tag_ausser_der_form_holt_keine_sicherung"]),
|
||||||
|
("Name der Summendatei beim Host egal", ' if treffer is None or treffer.group(2) != f"{name}.tar":',
|
||||||
|
" if treffer is None:", ["eine_summendatei_des_hosts_in_falscher_form_holt_kein_archiv"]),
|
||||||
|
("Grossbuchstaben in der Summe", r'SUMMENZEILE = re.compile(r"([0-9a-f]{64}) (\S+)\n")',
|
||||||
|
r'SUMMENZEILE = re.compile(r"([0-9a-fA-F]{64}) (\S+)\n")',
|
||||||
|
["eine_summendatei_des_hosts_in_falscher_form_holt_kein_archiv"]),
|
||||||
|
("Archiv direkt unter dem endgueltigen Namen", ' teil = ordner / f"{instanz}.tar.teil"',
|
||||||
|
' teil = ordner / f"{instanz}.tar"', ["sicherung_wird_geholt_geprueft_abgelegt_und_bestaetigt"]),
|
||||||
|
("Summendatei auf D: mit dem Namen des Hosts",
|
||||||
|
r' neu.write_bytes(f"{summe} {archiv.name}\n".encode("ascii"))',
|
||||||
|
r' neu.write_bytes(f"{summe} {tag}-{archiv.name}\n".encode("ascii"))',
|
||||||
|
["sicherung_wird_geholt_geprueft_abgelegt_und_bestaetigt"]),
|
||||||
|
("Zeitlimit des Archivs weg", ' lauf = ssh_lauf(f"sicherung {name}", SICHERUNG_TIMEOUT_S, strom)',
|
||||||
|
' lauf = ssh_lauf(f"sicherung {name}", None, strom)',
|
||||||
|
["das_zeitbudget_begrenzt_das_holen_und_jedes_archiv_hat_sein_zeitlimit"]),
|
||||||
|
("Zeitlimit des Archivs anders", "SICHERUNG_TIMEOUT_S = 600", "SICHERUNG_TIMEOUT_S = 6000",
|
||||||
|
["das_zeitbudget_begrenzt_das_holen_und_jedes_archiv_hat_sein_zeitlimit"]),
|
||||||
|
("Budget egal", " if monotonic() >= frist:", " if False:",
|
||||||
|
["das_zeitbudget_begrenzt_das_holen_und_jedes_archiv_hat_sein_zeitlimit"]),
|
||||||
|
("Budget anders", "SICHERUNG_BUDGET_S = 1500", "SICHERUNG_BUDGET_S = 3600",
|
||||||
|
["das_zeitbudget_begrenzt_das_holen_und_jedes_archiv_hat_sein_zeitlimit"]),
|
||||||
|
("nach dem Budget weiter", " if erschoepft:\n break", " if False:\n break",
|
||||||
|
["das_zeitbudget_begrenzt_das_holen_und_jedes_archiv_hat_sein_zeitlimit"]),
|
||||||
|
("Budget erst nach der Grenze", " if monotonic() >= frist:", " if monotonic() > frist:",
|
||||||
|
["das_zeitbudget_begrenzt_das_holen_und_jedes_archiv_hat_sein_zeitlimit"]),
|
||||||
|
("Budget erst ab dem Holen", "hole_sicherungen(jetzt, vault, ablage, ziel, instanzen, frist, argumente.tag)",
|
||||||
|
"hole_sicherungen(jetzt, vault, ablage, ziel, instanzen, monotonic() + SICHERUNG_BUDGET_S, argumente.tag)",
|
||||||
|
["das_zeitbudget_begrenzt_das_holen_und_jedes_archiv_hat_sein_zeitlimit"]),
|
||||||
|
("Summendatei vor der Bestaetigung",
|
||||||
|
' lauf = ssh_lauf(f"sicherung-bestaetigt {name} {summe}", SSH_TIMEOUT_S)\n'
|
||||||
|
" if lauf.returncode != 0:\n"
|
||||||
|
' raise RuntimeError(f"abgelegt, aber nicht bestaetigt ({fehlerzeile(lauf)})")\n'
|
||||||
|
' neu = ordner / f"{instanz}.tar.sha256.neu"\n'
|
||||||
|
' neu.write_bytes(f"{summe} {archiv.name}\\n".encode("ascii"))\n'
|
||||||
|
" os.replace(neu, summendatei)\n",
|
||||||
|
' neu = ordner / f"{instanz}.tar.sha256.neu"\n'
|
||||||
|
' neu.write_bytes(f"{summe} {archiv.name}\\n".encode("ascii"))\n'
|
||||||
|
" os.replace(neu, summendatei)\n"
|
||||||
|
' lauf = ssh_lauf(f"sicherung-bestaetigt {name} {summe}", SSH_TIMEOUT_S)\n'
|
||||||
|
" if lauf.returncode != 0:\n"
|
||||||
|
' raise RuntimeError(f"abgelegt, aber nicht bestaetigt ({fehlerzeile(lauf)})")\n',
|
||||||
|
["eine_gescheiterte_bestaetigung_laesst_die_summendatei_weg_und_wird_nachgeholt",
|
||||||
|
"eine_vom_host_abgelehnte_summe_ist_ein_befund_und_wird_neu_geholt"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Seit der Plankritik Runde 1: Mutanten von abendkontrolle_einrichten.ps1 (AUFGABE), gleiche Form.
|
||||||
|
MUTANTEN_AUFGABE = [
|
||||||
|
("Ausloeser mit Versatz", "$Abends.StartBoundary = (Get-Date -Format 'yyyy-MM-dd') + 'T' + $Uhrzeit + ':00'\n", "",
|
||||||
|
["der_abendausloeser_steht_in_ortszeit_ohne_versatz", "die_heutige_frist_und_die_aufgabe_passen_zueinander"]),
|
||||||
|
("Ausloeser in UTC", "+ $Uhrzeit + ':00'", "+ $Uhrzeit + ':00Z'",
|
||||||
|
["der_abendausloeser_steht_in_ortszeit_ohne_versatz"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def mutanten() -> int:
|
||||||
|
"""Jeder Mutant auf einer Kopie seiner Datei; jeder seiner Faelle muss rot werden.
|
||||||
|
|
||||||
|
MUTANTEN aendern abendkontrolle.py (QUELLE), MUTANTEN_AUFGABE abendkontrolle_einrichten.ps1 (AUFGABE).
|
||||||
|
"""
|
||||||
|
global QUELLE, AUFGABE
|
||||||
|
faelle = {funktion.__name__: funktion for funktion in FAELLE}
|
||||||
|
alle = [("abendkontrolle.py", *mutant) for mutant in MUTANTEN]
|
||||||
|
alle += [("abendkontrolle_einrichten.ps1", *mutant) for mutant in MUTANTEN_AUFGABE]
|
||||||
|
|
||||||
|
def gruen_bleiben(namen_der_faelle):
|
||||||
|
gruen = []
|
||||||
|
for fallname in namen_der_faelle:
|
||||||
|
try:
|
||||||
|
faelle[fallname]()
|
||||||
|
except (Exception, SystemExit):
|
||||||
|
continue
|
||||||
|
gruen.append(fallname)
|
||||||
|
return gruen
|
||||||
|
|
||||||
|
genannt = sorted({fallname for *_, rot in alle for fallname in rot})
|
||||||
|
fehlend = [fallname for fallname in genannt if fallname not in faelle]
|
||||||
|
if fehlend:
|
||||||
|
print(f"FALL FEHLT: {fehlend}")
|
||||||
|
return 2
|
||||||
|
# Grundlinie: Ohne Mutant muss jeder genannte Fall gruen sein, sonst misst der Rest nichts.
|
||||||
|
rot_ohne_mutant = [fallname for fallname in genannt if not gruen_bleiben([fallname])]
|
||||||
|
if rot_ohne_mutant:
|
||||||
|
print(f"GRUNDLINIE rot: {rot_ohne_mutant}")
|
||||||
|
return 2
|
||||||
|
ueberlebt = []
|
||||||
|
for datei, name, alt, neu, rot in alle:
|
||||||
|
original = (HIER / datei).read_text(encoding="utf-8")
|
||||||
|
anzahl = original.count(alt)
|
||||||
|
if anzahl != 1:
|
||||||
|
print(f"ANKER {anzahl}-mal: {name}")
|
||||||
|
ueberlebt.append(name)
|
||||||
|
continue
|
||||||
|
with tempfile.TemporaryDirectory() as ordner:
|
||||||
|
kopie = Path(ordner) / datei
|
||||||
|
kopie.write_text(original.replace(alt, neu), encoding="utf-8", newline="\n")
|
||||||
|
if datei == "abendkontrolle.py":
|
||||||
|
QUELLE = kopie
|
||||||
|
else:
|
||||||
|
AUFGABE = kopie
|
||||||
|
try:
|
||||||
|
gruen = gruen_bleiben(rot)
|
||||||
|
finally:
|
||||||
|
QUELLE = HIER / "abendkontrolle.py"
|
||||||
|
AUFGABE = HIER / "abendkontrolle_einrichten.ps1"
|
||||||
|
if gruen:
|
||||||
|
ueberlebt.append(name)
|
||||||
|
print(f"UEBERLEBT {name}: gruen blieben {gruen}")
|
||||||
|
else:
|
||||||
|
print(f"OK {name}: rot wurden {rot}")
|
||||||
|
print(f"{len(alle)} Mutanten, {len(ueberlebt)} ueberlebt ({datetime.now():%Y-%m-%d %H:%M})")
|
||||||
|
return 2 if ueberlebt else 0
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
argumente = sys.argv[1:] if argv is None else argv
|
||||||
|
if argumente == ["--mutanten"]:
|
||||||
|
return mutanten()
|
||||||
|
if argumente:
|
||||||
|
print(f"unbekannte Argumente {argumente}; erlaubt ist nur --mutanten")
|
||||||
|
return 2
|
||||||
abweichungen = 0
|
abweichungen = 0
|
||||||
for funktion in FAELLE:
|
for funktion in FAELLE:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ if (-not $QuelleIbkr) { $QuelleIbkr = $Quelle }
|
|||||||
$Aufruf = New-ScheduledTaskAction -Execute $Py -Argument ('"' + $Skript + '" --quelle ' + $Quelle + ' --quelle-ibkr ' + $QuelleIbkr) -WorkingDirectory "D:\Claude"
|
$Aufruf = New-ScheduledTaskAction -Execute $Py -Argument ('"' + $Skript + '" --quelle ' + $Quelle + ' --quelle-ibkr ' + $QuelleIbkr) -WorkingDirectory "D:\Claude"
|
||||||
if ($Quelle -eq "dockerhost" -or $QuelleIbkr -eq "dockerhost") { $Uhrzeit = "23:15" } else { $Uhrzeit = "22:45" }
|
if ($Quelle -eq "dockerhost" -or $QuelleIbkr -eq "dockerhost") { $Uhrzeit = "23:15" } else { $Uhrzeit = "22:45" }
|
||||||
$Abends = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday, Tuesday, Wednesday, Thursday, Friday -At $Uhrzeit
|
$Abends = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday, Tuesday, Wednesday, Thursday, Friday -At $Uhrzeit
|
||||||
|
# Seit Paket E9 (S10): -At legt den Startzeitpunkt mit UTC-Versatz an (gemessen 2026-10-11:
|
||||||
|
# ...T21:15:00Z). Eine Aufgabe mit Versatz startet im Winter eine Stunde frueher nach Ortszeit, vor
|
||||||
|
# der Abendkontrolle des Hosts. Ohne Versatz gesetzt, speichert die Aufgabenplanung den Wert so und
|
||||||
|
# startet nach Ortszeit.
|
||||||
|
$Abends.StartBoundary = (Get-Date -Format 'yyyy-MM-dd') + 'T' + $Uhrzeit + ':00'
|
||||||
$Anmeldung = New-ScheduledTaskTrigger -AtLogOn -User $Benutzer
|
$Anmeldung = New-ScheduledTaskTrigger -AtLogOn -User $Benutzer
|
||||||
$Anmeldung.Delay = "PT5M"
|
$Anmeldung.Delay = "PT5M"
|
||||||
$Wer = New-ScheduledTaskPrincipal -UserId $Benutzer -LogonType Interactive -RunLevel Limited
|
$Wer = New-ScheduledTaskPrincipal -UserId $Benutzer -LogonType Interactive -RunLevel Limited
|
||||||
|
|||||||
Reference in new issue
Block a user