Trading-Plattform-Pruefung: Host nicht erreichbar gibt eine Zeile, --tag frueh geprueft (Paket NT, Task 729)
- abendkontrolle.py: Exit 255 oder Zeitlimit bei Summe oder Bestaetigung beendet die Sicherung mit genau einer Zeile in der Notiz von heute und Exit 1; ein Abbruch im Archiv laesst nur die Instanz scheitern. --tag ausser der Form endet gleich mit Exit 2, ohne Notiz und ssh. - abendkontrolle_dockerhost_angriffe.py: sieben neue Faelle, zwei angepasste, Mutanten E729-01 bis E729-15 und E729-25 (38 Faelle, 56 Mutanten, keiner ueberlebt). - abendkontrolle_wsl.py: Kopf nennt die neuen fehler-Gruende. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_0126RDfXiECscXipzf67uypJ
This commit is contained in:
1 parent
02ff0e878d
commit
88ec4fd40f
3 files changed
+355
-28
No files matched your search
@@ -35,6 +35,10 @@ die Summendatei auf D: fehlt oder abweicht, <tag>-<instanz>.tar ueber denselben
|
||||
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).
|
||||
Seit Paket NT (Quorum 4): Endet die Frage nach einer Summe oder die Bestaetigung mit ssh-Exit 255 oder
|
||||
im Zeitlimit, ist der Docker-Host nicht erreichbar, und die Sicherung hoert sofort auf: eine Zeile in der
|
||||
Notiz von heute, Exit 1. Bricht nur das Archiv so ab, scheitert allein diese Instanz. --tag ausser der
|
||||
Form JJJJ-MM-TT oder ohne gueltiges Datum endet gleich nach dem Zerlegen mit Exit 2, ohne Notiz und ssh.
|
||||
Aufruf von Hand: python abendkontrolle.py [--tag JJJJ-MM-TT] [--vault D:\\Claude] [--ohne-wsl <json-datei>]
|
||||
[--quelle wsl|dockerhost] [--quelle-ibkr wsl|dockerhost] [--sicherung-ziel <ordner>]
|
||||
"""
|
||||
@@ -104,7 +108,7 @@ SICHERUNG_ZIEL = Path(r"D:\Trading-Sicherung")
|
||||
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
|
||||
# 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
|
||||
@@ -119,6 +123,20 @@ SUMMENZEILE = re.compile(r"([0-9a-f]{64}) (\S+)\n")
|
||||
TAGFORM = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
|
||||
|
||||
|
||||
def tag_gueltig(tag: str) -> bool:
|
||||
"""Ist tag ein Datum JJJJ-MM-TT, das es gibt, nur mit ASCII-Ziffern? Seit Paket NT fuer --tag in main.
|
||||
|
||||
TAGFORM allein liesse 2026-13-05 durch, date.fromisoformat allein seit Python 3.11 auch 20261005.
|
||||
"""
|
||||
if not TAGFORM.fullmatch(tag):
|
||||
return False
|
||||
try:
|
||||
date.fromisoformat(tag)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def faellige_tage(jetzt: datetime, ablage: Path, erster_tag: date = ERSTER_TAG, endung: str = "") -> list[str]:
|
||||
tage = []
|
||||
for zurueck in range(NACHHOLEN_TAGE, -1, -1):
|
||||
@@ -223,16 +241,36 @@ def summe_von(datei: Path) -> str:
|
||||
return summe.hexdigest()
|
||||
|
||||
|
||||
class HostNichtErreichbar(Exception):
|
||||
"""ssh selbst scheiterte (Exit 255) oder ein Befehl der Sicherung lief in sein Zeitlimit (seit Paket NT).
|
||||
|
||||
Beim Archiv macht hole_sicherung daraus den Fehlschlag dieser einen Instanz.
|
||||
"""
|
||||
|
||||
|
||||
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.
|
||||
Eigene Funktion, damit die Angriffsfaelle Zeitlimit und Dauer jedes Befehls sehen. Seit Paket NT
|
||||
(Quorum 4, G1): abendkontrolle-lesen.sh endet mit 0 bis 4. Exit 255 meldet ssh selbst: keine
|
||||
Verbindung oder Anmeldung, aber auch ein Befehl, den auf dem Host ein Signal beendet. Er und ein
|
||||
abgelaufenes Zeitlimit heissen: Der Docker-Host ist nicht erreichbar (HostNichtErreichbar).
|
||||
"""
|
||||
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,
|
||||
)
|
||||
try:
|
||||
if stdout is None:
|
||||
lauf = subprocess.run(
|
||||
ssh_aufruf(befehl), capture_output=True, timeout=timeout, creationflags=KEIN_FENSTER,
|
||||
)
|
||||
else:
|
||||
lauf = subprocess.run(
|
||||
ssh_aufruf(befehl), stdout=stdout, stderr=subprocess.PIPE, timeout=timeout,
|
||||
creationflags=KEIN_FENSTER,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise HostNichtErreichbar(f"{befehl}: Zeitlimit {timeout} s abgelaufen") from None
|
||||
if lauf.returncode == 255:
|
||||
raise HostNichtErreichbar(f"{befehl}: {fehlerzeile(lauf)}")
|
||||
return lauf
|
||||
|
||||
|
||||
def summe_auf_d(ordner: Path, instanz: str) -> str | None:
|
||||
@@ -265,10 +303,8 @@ def sicherungstage(jetzt: datetime, tag: str | None = None) -> list[str]:
|
||||
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)
|
||||
# Der Tag wird Teil eines Pfads und eines Befehls auf dem Host. Seine Form (JJJJ-MM-TT, ein Datum,
|
||||
# das es gibt) prueft seit Paket NT main gleich nach dem Zerlegen der Argumente (tag_gueltig).
|
||||
return [tag]
|
||||
tage = []
|
||||
for zurueck in range(SICHERUNG_NACHHOLEN_TAGE, -1, -1):
|
||||
@@ -337,7 +373,14 @@ def hole_sicherung(tag: str, instanz: str, ziel: Path, summe: str) -> str:
|
||||
summendatei = ordner / f"{instanz}.tar.sha256"
|
||||
try:
|
||||
with teil.open("wb") as strom:
|
||||
lauf = ssh_lauf(f"sicherung {name}", SICHERUNG_TIMEOUT_S, strom)
|
||||
try:
|
||||
lauf = ssh_lauf(f"sicherung {name}", SICHERUNG_TIMEOUT_S, strom)
|
||||
except HostNichtErreichbar as abbruch:
|
||||
# Seit Paket NT (Plankritik, Befund 1): Mitten im Archiv heissen Zeitlimit und Exit 255 nicht,
|
||||
# dass der Host fehlt; er hat die Summe gerade beantwortet. Nur diese Instanz scheitert, und
|
||||
# ein einzelnes zu langsames Archiv haelt nicht jeden Lauf an derselben Stelle an. Ist der
|
||||
# Host wirklich weg, merkt es die naechste Frage nach einer Summe.
|
||||
raise RuntimeError(f"Archiv nicht geholt ({abbruch})") from None
|
||||
if lauf.returncode != 0:
|
||||
raise RuntimeError(f"Archiv nicht geholt ({fehlerzeile(lauf)})")
|
||||
if summe_von(teil) != summe:
|
||||
@@ -393,6 +436,24 @@ def hole_sicherungen(jetzt: datetime, vault: Path, ablage: Path, ziel: Path, ins
|
||||
if geholt:
|
||||
getan = True
|
||||
logge(ablage, f"sicherung {kandidat}-{instanz}: abgelegt und bestaetigt")
|
||||
except HostNichtErreichbar as ausnahme:
|
||||
# Seit Paket NT (Quorum 4, G1): Ohne Host scheiterte sonst jeder Tag des Fensters mit eigener
|
||||
# Zeile, auch in neu angelegten Notizen von Wochenenden. Die Sicherung hoert hier auf: eine
|
||||
# Zeile in der Notiz von heute, Exit 1. Fertige Tage behalten ihre Zeilen; was der
|
||||
# angefangene Tag schon hat, nennt diese Zeile. Auf D: entsteht danach nichts mehr.
|
||||
logge(ablage, f"sicherung {kandidat}-{instanz}: Docker-Host nicht erreichbar ({ausnahme}), Sicherung abgebrochen")
|
||||
zeile = (
|
||||
f"- {jetzt:%H:%M} Uhr — {SICHERUNG_KOPF}: **gescheitert** #needs-review. Docker-Host nicht "
|
||||
f"erreichbar ({str(ausnahme)[:200]}), Sicherung abgebrochen."
|
||||
)
|
||||
if stuecke:
|
||||
zeile += f" Bis dahin am {kandidat}: {'; '.join(stuecke)}."
|
||||
zeile += " Der nächste Lauf versucht es erneut."
|
||||
try:
|
||||
schreibe_notiz(vault, jetzt.date().isoformat(), zeile)
|
||||
except Exception as zweite:
|
||||
logge(ablage, f"sicherung: Notiz nicht geschrieben {zweite!r}")
|
||||
return 1
|
||||
except Exception as ausnahme:
|
||||
gescheitert = True
|
||||
stuecke.append(f"{instanz} gescheitert ({type(ausnahme).__name__}: {str(ausnahme)[:200]})")
|
||||
@@ -547,6 +608,13 @@ def main(argv=None) -> int:
|
||||
help="Ordner fuer die Sicherungen statt SICHERUNG_ZIEL (Angriffsfaelle)",
|
||||
)
|
||||
argumente = zerleger.parse_args(argv)
|
||||
# Seit Paket NT (Quorum 4): --tag gleich hier pruefen. Der Tag wird Name der Notiz, Pfad unter
|
||||
# SICHERUNG_ZIEL und Teil eines Befehls auf dem Host; vorher legte ein Tag ausser der Form
|
||||
# Inbox/<tag>.md an, mit ../ auch ausserhalb von Inbox/. Leer zaehlt als ausser der Form. Die
|
||||
# Ausgabe ist ASCII (!a), damit kein Zeichen an der Kodierung von stderr scheitert.
|
||||
if argumente.tag is not None and not tag_gueltig(argumente.tag):
|
||||
print(f"abendkontrolle: --tag {argumente.tag!a} ist kein Datum JJJJ-MM-TT", file=sys.stderr)
|
||||
return 2
|
||||
# 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
|
||||
|
||||
Reference in new issue
Block a user