Files
app-backup/RESTORE_TEST.md
T

21 KiB

Monatlicher Offsite-Restore-Test

Der Restore-Test prüft den neuesten vollständigen Sicherungssatz direkt aus dem konfigurierten rclone-Crypt-Remote. Alle Komponenten werden heruntergeladen, entschlüsselt, validiert und in ein privates Testverzeichnis entpackt. SQL-Dumps werden zusätzlich in eine isolierte MariaDB importiert und mit mariadb-check geprüft. Produktive Anwendungen werden dabei nicht angehalten oder überschrieben.

Seit 16.09.2026 werden die Archive beim täglichen Backup und bei der Update-Abnahme nur auf Remote-Dateiexistenz und Größe geprüft. Der hier beschriebene Monatslauf übernimmt die vollständige Inhalts- und Restore-Prüfung. Auch ai-rem-Zusatzarchive (extra-N) werden heruntergeladen und entpackt; ein Start der ai-rem-Anwendung oder ein Kuzu-Datenbanktest gehört nicht dazu.

Downloadfehler bei der Abnahme am 15.09.2026

Das Update 4f220dbc6447 erzeugte den Satz appbackup_2026-09-15_09-03-06_20b0f7a6. Sein Upload und der vollständige Bytevergleich aller 15 Dateien bestanden, auch für die rund 5 GiB große nextcloud-data.tar.zst. Der Abschlussmarker wurde bestätigt. Anschließend bestand der separate Restore-Test Metadaten, drei Datenbanken, WordPress und Nextcloud; beim erneuten Abruf der Nextcloud-Daten folgte Command failed: bash. Der Test meldete FAIL; cleanup OK, das gesamte Update deshalb FAILED.

Der Fehler ist dem Shell-Aufruf für den Komponentendownload einschließlich Remote-Vorprüfung zuzuordnen: Tar-Prüfung und Entpacken würden einen anderen Programmnamen melden; Fehler der separaten Platzbeschaffung werden bereits mit einer eigenen Meldung versehen. Der Supervisor verwarf jedoch stderr und nannte weder Exit-Code noch genaue Phase. Ob diesmal Crypt-Header, Timeout, HTTP-Fehler oder eine andere Bridge-Ursache vorlag, ist damit nicht mehr aus diesem Log rekonstruierbar. Ein erneuter bad magic string ist nicht belegt.

Die erste Korrektur erfasste rclone check, aber nicht diesen eigenen rclone cat-Pfad. Auch cat wiederholt den gesamten Befehl in rclone 1.74.3 nicht automatisch über --retries. Quelle: rclone-1.74.3-cat-Aufruf.

Korrigierter Ablauf

Der Restore-Test verwendet für seine lesenden Remote-Aufrufe jetzt dieselben Konfigurationswerte wie die Remote-Metadatenprüfung:

  • RCLONE_CHECK_ATTEMPTS=3: höchstens drei Versuche je Metadatenabruf/Download.
  • RCLONE_CHECK_DEADLINE=3600: maximal eine Stunde je Versuch einschließlich Remote-Vorprüfung. Der Python-Supervisor beendet die eigene Prozessgruppe bei Überschreitung mit TERM, nötigenfalls nach fünf Sekunden mit KILL.
  • RCLONE_CHECK_TIMEOUT=300 und RCLONE_CHECK_LOW_LEVEL_RETRIES=3: kürzere rclone-Leerlauffrist und begrenzte interne Versuche, auch bei der Vorprüfung.
  • RCLONE_CHECK_RETRY_DELAY=30: Pause zwischen fehlgeschlagenen Versuchen. Die Gesamtfrist RESTORE_TEST_TIMEOUT und die Speicherüberwachung bleiben während Downloads und Wartepausen wirksam. Die Fristen addieren sich nicht zu einer unbegrenzten Verlängerung des gesamten Restore-Tests.

Bei einer Wiederholung wird die angefangene Archivdatei auf null gekürzt und vollständig neu geladen; es wird niemals an Teilbytes angehängt. Metadaten werden je Versuch in einem neuen begrenzten Puffer gelesen. Erst ein erfolgreich beendeter Download mit passender Größe und SHA-256 darf zur Archivprüfung und zum Entpacken gelangen. Größenüberschreitungen, Prüfsummenfehler, Speichergrenzen, Gesamtfrist und Signalabbrüche lösen keine Download-Wiederholung aus. Die Platzbereinigung wird nicht als Remote-Leseoperation wiederholt.

Jede Komponente meldet und speichert nun ihre konkrete Phase: Download, Prüfsumme, Archivprüfung, Platzabschätzung, Entpacken, DB-Import/-Prüfung und Bereinigung. Während eines Remote-Aufrufs erscheint etwa jede Minute ein Fortschrittsbericht mit gelesenen Bytes und verstrichener Zeit, auch bei Stillstand. Nicht erfolgreiche Prozesse nennen Exit-Code oder Fristablauf.

Remote-stderr wird parallel zum Archivstrom gelesen und ausschließlich in feste Fehlerklassen übersetzt, etwa crypt-header, io-timeout, tls, dns, connection, http-429 oder http-503. Die Auswertung hat einen begrenzten Textpuffer; große stderr-Ausgaben können die Download-Pipe nicht blockieren. Es werden keine rohen URLs, Tokens, Header, Dateiinhalte oder Providertexte übernommen. Unbekannte Meldungen erscheinen als unclassified remote error. Diese Klassifizierung ist ein Diagnosehinweis, kein Beweis der Providerursache. DB- und sonstige Tool-stderr-Ausgaben bleiben vollständig unterdrückt, da sie SQL-Inhalte enthalten können. Phase und Exit-Code bleiben trotzdem sichtbar.

Ein bleibender Fehler erhält den bereits verifizierten Backup-Satz und den vorherigen Restore-Erfolgsnachweis, meldet die betroffene Komponente als FAIL und bereinigt die Testdaten. Nach fehlgeschlagener Update-Abnahme bleiben Backup- und Restore-Test-Timer gestoppt. Das neue Paket erneut im Wartungsfenster installieren; es führt ein neues Backup samt Remote-Dateiprüfung aus. Die neue result.txt mit SUCCESS bestätigt die Update-Abnahme; der vollständige Restore-Test erfolgt separat beim Monatstermin. Installationsschritte.

Der vorhandene erfolgreiche Backup-Satz darf alternativ nach Installation des Fixes mit app-restore-test.sh --remote-run erneut geprüft werden. Dabei bleibt die Altersgrenze aktiv. Ein solcher Einzeltest setzt nicht nachträglich das alte Update-Ergebnis auf SUCCESS und aktiviert keine Update-Timer.

Speicherbedarf und Bereinigung

Wegen des knappen Speichers auf Gemini läuft immer nur eine Komponente:

  1. Archiv herunterladen; Größe und SHA-256 gegen das Manifest prüfen.
  2. Archivstruktur und Kompression vollständig validieren.
  3. Platz für die entpackte Komponente abschätzen und diese mit GNU tar entpacken.
  4. Das komprimierte Archiv sofort löschen.
  5. Bei einem DB-Dump eine frische Test-DB starten, importieren und Tabellen prüfen.
  6. Den eigenen DB-Container samt eventuellen anonymen Volumes entfernen, danach alle temporären Dateien dieser Komponente löschen; erst dann die nächste laden.

Es liegt somit nie der gesamte Sicherungssatz gleichzeitig auf der Platte. Der Spitzenbedarf ist ungefähr das Maximum aus:

  • komprimiertem Archiv plus entpackter größter Komponente und Dateisystem-Overhead;
  • SQL-Dump plus zugehöriger Testdatenbank einschließlich MariaDB-Systemtabellen, Indexen und temporären DB-Dateien.

Standard: adaptives Arbeitsbudget (RESTORE_TEST_MAX_WORK_GB=0) mit mindestens 2 GiB freier Reserve. Ein positiver Wert setzt zusätzlich eine feste Grenze. Die größte Komponente muss vollständig passen. Die frühere Vorgabe von 4 GiB reicht für Geminis rund 8,8 GB Nextcloud-Daten nicht; die Update-Option --enable-restore-test setzt deshalb ausdrücklich das adaptive Budget. Bei Platzdruck versucht der Test zwischen Arbeitsschritten die sichere lokale Bereinigung aus SPEICHER.md. Schlägt sie fehl, wird der Test mit Fehlerbericht beendet und sein Scratch bereinigt. Alle Komponenten werden geprüft; ein fehlender Platz wird nicht durch eine Stichprobe kaschiert.

Vor Download und Entpacken wird der Bedarf geprüft. Während externer Prozesse wird freier Speicher laufend kontrolliert, die tatsächliche Belegung regelmäßig nachgemessen und bei Grenzverletzung abgebrochen. Dies ist keine harte Dateisystemquote: schnelle DB-Schreibvorgänge und fremde Prozesse können zwischen Messungen weiteren Platz belegen. Daher die Reserve nicht knapp auf null setzen. Für den DB-Start werden zusätzlich mindestens 512 MiB eingeplant; das ist eine Mindestreserve, keine obere Grenze für die Größe der importierten Datenbank.

Normale Fehler, Speicher-/Zeitlimits und SIGTERM/SIGINT führen zur Bereinigung. Nach einem harten Abbruch führt systemd ExecStopPost aus. Bei einem Fehler wird außerdem der Bereinigungsdienst gestartet; er versucht eine fehlgeschlagene Bereinigung alle 15 Minuten erneut. Ein aktivierter Boot-Bereinigungsdienst und der nächste Teststart räumen unterbrochene Tests ebenfalls auf. Beide verwenden dieselbe Sperre wie Backup und Update.

Kann beispielsweise der Docker-Daemon nicht erreicht und der Testcontainer nicht sicher entfernt werden, lautet das Ergebnis FAIL / Bereinigung FAILED. Die Dateien werden dann nicht unter einer möglicherweise noch laufenden DB gelöscht. Die Bereinigung wird wiederholt; ein solcher Zustand darf nicht als Erfolg gelten. Bei einem ausgeschalteten Server erfolgt sie naturgemäß erst nach dem Neustart.

Nur der eindeutig zu diesem Test gehörende Container und das markierte $WORKDIR/restore-test/scratch werden bereinigt. Reguläre manuelle Restore-Verzeichnisse und fremde Docker-Ressourcen bleiben erhalten. Die getrennte Platzbereinigung darf ältere lokale Sicherungskopien nur nach lokaler Inhaltsprüfung und erneuter Remote-Datei-/Größenprüfung sowie bytegenauem Vergleich von Manifest/Abschlussmarker entfernen. Die Mindestanzahl bei Platznot ist SPACE_KEEP_LOCAL (Default eins); Details stehen in SPEICHER.md. Es gibt kein allgemeines docker prune. Das einmal vorbereitete DB-Image bleibt als wiederverwendbare Abhängigkeit erhalten; sein einmaliger Platzbedarf kommt zum Arbeitsbudget hinzu. Images werden im Test weder heruntergeladen noch gebaut.

Automatische Vorbereitung auf Gemini

Docker Engine und CLI müssen lokal vorhanden sein. Gemini verwendet laut Bestandsaufnahme vom 12.09.2026 MariaDB 10.11.18 und besitzt Docker bereits. Der Installer liest SELECT VERSION() mit den vorhandenen Backup-Zugangsdaten. Alle zu prüfenden Datenbanken müssen dieselbe MariaDB-Major-/Minor-Serie verwenden. Ohne gesetztes Image wählt er beispielsweise mariadb:10.11. Ein vorhandenes Image wird wiederverwendet, ein fehlendes einmalig mit Zeitlimit geladen. Ein explizites Image wird erhalten, muss aber zur Produktionsserie passen.

sha256sum -c app-backup-update-VERSION.run.sha256
sudo bash app-backup-update-VERSION.run --enable-restore-test

Die Option aktiviert den Test und das adaptive Arbeitsbudget in der gesicherten Kandidatenkonfiguration. Die freie Docker-Kapazität wird vor einem Download geprüft. Der Installer startet bei Bedarf den vorhandenen Docker-Dienst. Er initialisiert eine isolierte Test-MariaDB, importiert eine kleine Prüftabelle, führt mariadb-check aus und entfernt Container und Daten wieder. Erst nach Erfolg wird ein automatisch ermitteltes Image in die Konfiguration übernommen. Der Probeimport verändert keine produktive Datenbank und setzt keinen erfolgreichen monatlichen Sicherungsnachweis.

Anschließend installiert der Update-Worker das Paket, erstellt ein Backup und prüft diesen konkreten Offsite-Satz vollständig inklusive DB-Import. Erst nach erfolgreicher Prüfung und Bereinigung aktiviert er Backup-Timer, Monatstimer und Boot-Bereinigung. Das Update-Ergebnis und der separate Restore-Bericht dokumentieren die Abnahme. Die zeitaufwendige Worker-Phase läuft unabhängig von SSH; die vorgelagerte Vorbereitung sollte bis zur Übergabemeldung verbunden bleiben.

Ohne --enable-restore-test erhält das Update den bisherigen Aktivierungszustand und ein ausdrücklich eingestelltes Arbeitsbudget. Ist der Test bereits aktiviert, erfolgen Image-Vorbereitung und vollständige Abnahme ebenfalls automatisch. Bei einem MariaDB-Serienwechsel ein bisher ausdrücklich eingetragenes Image anpassen oder leeren; ein inkompatibles Image wird vor Verwendung abgelehnt.

Eine direkte Installation mit install.sh bereitet eine bereits aktivierte Testkonfiguration ebenfalls vor. Für eine spätere Aktivierung ohne Paketupdate:

sudoedit /etc/app-backup/app-backup.conf
# ENABLE_RESTORE_TEST=true; RESTORE_TEST_DB_IMAGE=""; RESTORE_TEST_MAX_WORK_GB=0
sudo bash /usr/local/sbin/app-restore-test.sh --cleanup
sudo bash /usr/local/lib/app-backup/prepare-restore-test.sh /etc/app-backup/app-backup.conf
sudo bash /usr/local/sbin/app-restore-test.sh --check
sudo systemctl start app-restore-test.service
sudo systemctl enable app-restore-test-cleanup.service
sudo systemctl enable --now app-restore-test.timer

--check prüft Konfiguration, lokale Werkzeuge, Image-Metadaten und freie Reserve. Es lädt kein Backup herunter und führt keinen DB-Import aus. Den ersten echten Test nach Installation anstoßen und sein Ergebnis kontrollieren:

sudo systemctl start --no-block app-restore-test.service
sudo journalctl -fu app-restore-test.service --no-pager
sudo cat /var/backups/app-backup/restore-test/state.json
sudo systemctl list-timers app-restore-test.timer --no-pager

Bei angepasstem WORKDIR die Pfade entsprechend ersetzen. Einen bestimmten Satz kann man manuell prüfen; Altersgrenze und alle übrigen Prüfungen bleiben aktiv:

sudo bash /usr/local/sbin/app-restore-test.sh \
  --remote-run appbackup_DATUM_UHRZEIT_ID

Zeitplan, Auswahl und Ergebnisse

Der Standardtermin ist der 1. jedes Monats um 06:00 Uhr Serverzeit, mit bis zu 15 Minuten zufälliger Verzögerung. Persistent=true holt verpasste Termine nach. Ein belegter Backup-/Restore-/Update-Lock liefert Exit-Code 75; systemd versucht es eine Stunde später erneut. Ein echter Prüffehler löst eine Fehlermail aus und wird nicht durch automatische Neuversuche als Erfolg verdeckt.

Unvollständige neuere Sätze ohne COMPLETE werden mit Protokolleintrag übersprungen. Für den neuesten abgeschlossenen Satz müssen Marker und Manifest zusammenpassen, die Lauf-ID muss dem Remote-Verzeichnis entsprechen, alle konfigurierten Komponenten müssen enthalten sein und die Remote-Dateiliste muss vollständig sein. Ein beschädigter abgeschlossener Satz wird nicht durch einen älteren ersetzt. Standardmäßig sind maximal 72 Stunden alte Backups zulässig; ein ungültiger Zeitstempel oder ein Zeitstempel mehr als fünf Minuten in der Zukunft scheitert.

Der Bericht verwendet die bisherigen Mail-Einstellungen (ENABLE_MAIL_REPORT, MAIL_TO, MAIL_FROM, MAIL_HOSTNAME, MAIL_SENDMAIL_BIN, MAIL_SEND_TIMEOUT). Der Betreff lautet beispielsweise:

[app-restore-test] SUCCESS gemini.jr-it-services.de appbackup_2026-…

Enthalten sind Alter und ID des Sicherungssatzes, Laufzeit, Status je Komponente, Archivgröße, Anzahl der Archiveinträge, Anzahl der importierten Tabellen sowie Bereinigung: OK/FAILED. Es werden weder SQL-Inhalte noch Dateiinhalte in die Mail oder das Testprotokoll übernommen. Der Status je Komponente wird erst nach seiner Bereinigung als erfolgreich gespeichert.

Unter $WORKDIR/restore-test/ bleiben nur kleine Betriebsnachweise erhalten:

Datei Bedeutung
state.json Letzter/aktueller Lauf; bei Start sofort RUNNING, abschließend SUCCESS oder FAIL, Bereinigungs- und Mailstatus
last-success.json Letzter vollständig geprüfter und bereinigter Satz; bleibt bei einem späteren Fehler erhalten
report.eml Jeweils letzter Mailbericht, auch bei Zustellungsfehler
restore-test.log und .1 Rotierendes Testprotokoll, jeweils ungefähr 512 KiB
.owner Kennzeichnung des Testverzeichnisses für sichere Bereinigung

Temporäre Prozess-/Container-Marker verschwinden nach erfolgreicher Bereinigung. Die Nachweise sind root-privat. Für die unabhängige Überwachung sollte ein RUNNING, cleanup != OK, mail == FAILED oder ein mehr als etwa 40 Tage alter last-success.json auffallen. Ein erfolgreicher Restore bei fehlgeschlagener Mail bleibt inhaltlich SUCCESS, liefert aber Exit-Code 1 und mail: FAILED. Der SMTP-Versand ist wie bisher eine Übergabe an den lokalen MTA, kein Nachweis der tatsächlichen Zustellung.

Manuelle Wiederholung der Bereinigung, auch bei deaktivierter Testfunktion:

sudo bash /usr/local/sbin/app-restore-test.sh --cleanup
sudo journalctl -u app-restore-test-cleanup.service -n 50 --no-pager

Zum Aussetzen des Zeitplans app-restore-test.timer deaktivieren. Den Boot-Bereinigungsdienst aktiviert lassen, bis eventuell unterbrochene Testdaten nachweislich entfernt wurden.

Isolation und Aussagekraft

Jeder DB-Dump erhält eine neue Datenbankinstanz. Der Container hat ausschließlich ein Bind-Mount auf sein privates DB-Testverzeichnis, kein Netzwerk, keine veröffentlichten Ports, keine zusätzlichen Linux-Capabilities, ein schreibgeschütztes Root-Dateisystem und keinen persistenten Docker-Logtreiber. Er läuft mit maximal 512 MiB RAM, 128 Prozessen und einer CPU. MariaDB liest keine Hostkonfiguration, verwendet nur ihren eigenen Unix-Socket und hat Event-Scheduler und Binlog aus. Der SQL-Client verwendet Batch-/Binary-Modus, damit Dumps keine lokalen Client-Kommandos ausführen. Docker-API-Ausgaben und SQL-Fehlertexte gelangen nicht ungefiltert in Logs. Hintergrunddatenbanken können keine produktiven DB-Sockets oder Konfigurationen erreichen.

Der Test belegt Download, Entschlüsselung, Integrität, tatsächliches Entpacken und DB-Import mit Tabellenprüfung. Er belegt weder eine vollständige Datenabdeckung, die schon im Backup fehlte, noch fachliche Konsistenz zwischen Anwendungen. Ein Start-/Funktionstest von Nextcloud, WordPress und Gitea auf einer getrennten VM bleibt eine zusätzliche Abnahme. Dateibesitz/ACLs werden beim Entpacken angefordert; die korrekte Anwendung aller Sicherheitskontexte auf einem neuen Produktivsystem bedarf dort einer eigenen Prüfung.

Technische Referenzen: Docker run, Docker ohne Netzwerk, MariaDB-Systemtabellen initialisieren, MariaDB-Serveroptionen.

Entwicklung und lokale Verifikation

Beim abschließenden Prüflauf der Writer-Stop-Korrektur (3730ab4676a5) am 15.09.2026 bestanden 111 Tests der Gesamtsuite. Von 119 erkannten Fällen wurden fünf optionale Docker-Integrationstests und drei Benutzer-systemd-Fälle übersprungen; die drei systemd-Fälle bestanden separat. Die fünf Docker-Fälle wurden bei diesem Lauf nicht erneut ausgeführt. Der folgende Nachweis mit echten DB-Containern stammt aus der vorherigen Prüfung der Restore-Download-Korrektur. Die erneute Serverabnahme auf Gemini ist weiterhin offen.

Der Folgefix vom 15.09.2026 bestand alle 33 Restore-Test-Fälle mit der von Gemini kopierten rclone-1.74.3-Binärdatei und dem echten lokalen mariadb:10.11-Image. Geprüft wurden auch Teil-Downloads/Metadaten mit erneutem Erfolg, dauerhaft fehlgeschlagene Downloads, Fristablauf, Signalabbruch, Gesamtfrist während einer Retry-Pause und zu große Antworten. Große stderr- Ausgaben blockierten nicht; synthetische Tokens und SQL-Inhalte erschienen weder im Log noch im Status oder Bericht. Die Testcontainer und Testdaten waren anschließend entfernt. Diese lokalen Tests ersetzen nicht die erneute OneDrive-/Rocky-Abnahme auf Gemini.

Die vollständige Basissuite tests/run.sh bestand ebenfalls: Bash-Syntax, ShellCheck und 111 erfolgreiche Tests; fünf optionale Docker-Fälle waren in diesem netzlosen Basistest ausgelassen und wurden im oben genannten echten MariaDB-Lauf bestanden. Insgesamt sind damit alle 116 unterschiedlichen Tests erfolgreich abgedeckt, einschließlich Paketinstallation und Rollback.

Am 12.09.2026 bestanden 100 unterschiedliche Tests: 71 Backup-/Restore-/ Installer-Regressionstests, 23 Restore-Test-Fälle mit dem offiziellen lokalen MariaDB-10.11-Image und 6 gezielte Scratch-/Recovery-Schutzprüfungen. ShellCheck und Python-Syntaxprüfung bestanden ebenfalls. Das Testverzeichnis war danach leer, und es blieben keine zugehörigen Testcontainer zurück.

Die automatischen Tests decken echte Crypt-Downloads, Entpacken, DB-Import und Tabellenprüfung ab, außerdem ungültiges SQL, SIGTERM/SIGKILL, Platzmangel, beschädigte Statusdateien und einen zeitweise nicht erreichbaren Docker-Daemon. Die Installer-Vorbereitung wird zusätzlich mit dem offiziellen lokalen mariadb:10.11 geprüft, einschließlich automatischer Versionswahl und Ablehnung einer falschen Produktionsserie. Scratch-, Container- und Konfigurationserhalt werden nachgeprüft; Testmails bleiben lokal in der Testumgebung.

Auf einem separaten Entwicklungs-/Testrechner mit den Abhängigkeiten aus tests/Dockerfile, Docker-CLI und lokal vorbereitetem Image lässt sich die gezielte Prüfung aus dem Quell-Checkout starten:

sudo env RESTORE_TEST_REAL_DOCKER_IMAGE=mariadb:10.11 \
  python3 -m unittest discover -s tests -p test_restore_test.py -v

Diese Tests verwenden ausschließlich selbst angelegte temporäre Konfigurationen, Daten und Container. Die vollständige Basissuite enthält zusätzlich simulierte systemd-/Installertests und gehört in den bestehenden isolierten Testcontainer. Die neuen Units wurden mit systemd-analyze verify geprüft. Ihre tatsächliche Ausführung unter Rocky/SELinux und der produktive OneDrive-/MTA-Pfad sind nach Installation auf Gemini noch abzunehmen.