Fix systemd oneshot upgrade handoff deadlock
This commit is contained in:
1 parent
56fbcdad74
commit
898df2e926
5 files changed
+190
-20
No files matched your search
+15
-2
@@ -261,6 +261,17 @@ Dateisperre. Der Starter übergibt den langen Ablauf an eine systemd-Unit; das
|
|||||||
Ergebnis bleibt im Migrationsordner erhalten. Ein kombinierter Rollback stellt
|
Ergebnis bleibt im Migrationsordner erhalten. Ein kombinierter Rollback stellt
|
||||||
Paketdateien und die vorherige Hauptkonfiguration wieder her.
|
Paketdateien und die vorherige Hauptkonfiguration wieder her.
|
||||||
|
|
||||||
|
Die Übergabe nutzt `systemd-run --no-block` mit `Type=oneshot`: Der Starter wartet
|
||||||
|
nur auf die Annahme des Auftrags, gibt danach zuerst die Backup-Sperre und dann
|
||||||
|
die Update-Sperre frei. Der Worker erwirbt beide selbst und darf nur mit einem
|
||||||
|
weiterhin als `QUEUED` freigegebenen Migrationsstand beginnen. Bei einer
|
||||||
|
fehlgeschlagenen Übergabe wird diese Freigabe unter gehaltener Sperre entzogen.
|
||||||
|
Der Worker richtet Fehlerprotokoll und EXIT-Trap vor dem maximal 60-sekündigen
|
||||||
|
Warten auf die Update-Sperre ein. Erst `SUCCESS` belegt das Gesamtresultat.
|
||||||
|
Das behebt das gegenseitige Warten in den früheren Paketen: Ohne `--no-block`
|
||||||
|
wartete der Starter auf den oneshot-Abschluss, während der Worker auf seine
|
||||||
|
Sperre wartete. Ursache, Diagnose und Wiederanlauf beschreibt die Update-Anleitung.
|
||||||
|
|
||||||
Der Abnahmelauf nutzt `app-backup.sh --no-retention`: keinerlei lokale oder entfernte
|
Der Abnahmelauf nutzt `app-backup.sh --no-retention`: keinerlei lokale oder entfernte
|
||||||
Löschung alter Sicherungssätze. Nach Upload/Rücklesevergleich prüft zusätzlich
|
Löschung alter Sicherungssätze. Nach Upload/Rücklesevergleich prüft zusätzlich
|
||||||
`app-restore.sh --remote-run ... --verify-only` den erneut heruntergeladenen Satz.
|
`app-restore.sh --remote-run ... --verify-only` den erneut heruntergeladenen Satz.
|
||||||
@@ -304,7 +315,7 @@ Die Entfernung der drei DB-Dateien aus dem aktuellen Baum löscht deren Historie
|
|||||||
|
|
||||||
Reproduzierbar mit `tests/Dockerfile` und `tests/run.sh`. Der Container bekommt das Repository nur lesbar und benötigt während der Tests kein Netzwerk. Die MariaDB läuft ohne TCP-Port in einem temporären Verzeichnis. systemd/occ werden simuliert; alle eigentlichen Archive, Dateikopien, Dumps/Imports und Crypt-Operationen sind real.
|
Reproduzierbar mit `tests/Dockerfile` und `tests/run.sh`. Der Container bekommt das Repository nur lesbar und benötigt während der Tests kein Netzwerk. Die MariaDB läuft ohne TCP-Port in einem temporären Verzeichnis. systemd/occ werden simuliert; alle eigentlichen Archive, Dateikopien, Dumps/Imports und Crypt-Operationen sind real.
|
||||||
|
|
||||||
Ergebnis des lokalen Abschlusslaufs am 10.09.2026: **34 Tests erfolgreich**, Shell-Syntax und ShellCheck ohne Befund. Testumgebung: Debian 13.6, MariaDB 11.8.6, rclone 1.60.1-DEV (Distributionspaket), ShellCheck 0.10.0. Diese Versionsangaben beschreiben die Testumgebung, keine Empfehlung für die Produktivversion.
|
Ergebnis des lokalen Abschlusslaufs am 10.09.2026: **37 Tests erfolgreich**, Shell-Syntax und ShellCheck ohne Befund. Testumgebung: Debian 13.6, MariaDB 11.8.6, rclone 1.60.1-DEV (Distributionspaket), ShellCheck 0.10.0. Diese Versionsangaben beschreiben die Testumgebung, keine Empfehlung für die Produktivversion.
|
||||||
|
|
||||||
Geprüfte Fälle umfassen:
|
Geprüfte Fälle umfassen:
|
||||||
|
|
||||||
@@ -321,6 +332,8 @@ Geprüfte Fälle umfassen:
|
|||||||
- Paketupdate mit Erhalt von Config/Zeitplan und ausführbarem Rollback.
|
- Paketupdate mit Erhalt von Config/Zeitplan und ausführbarem Rollback.
|
||||||
- Selbstentpackendes Transferpaket mit erfolgreichem Komplettlauf und kombiniertem Konfigurations-/Paket-Rollback.
|
- Selbstentpackendes Transferpaket mit erfolgreichem Komplettlauf und kombiniertem Konfigurations-/Paket-Rollback.
|
||||||
- Abbruch vor Änderungen bei Vorprüfungsfehlern, gestoppter Timer bei fehlgeschlagenem Abnahmebackup, Ablehnung beschädigter Pakete.
|
- Abbruch vor Änderungen bei Vorprüfungsfehlern, gestoppter Timer bei fehlgeschlagenem Abnahmebackup, Ablehnung beschädigter Pakete.
|
||||||
- Übergabe an einen vom Starter unabhängigen Prozess (systemd-run im Test simuliert), ausdrückliche Cron-Policy und Writer-Wartezeit/Timeout.
|
- Übergabe an einen vom Starter unabhängigen Prozess: Das systemd-run-Testmodell wartet bei `Type=oneshot` ohne `--no-block` auf das Worker-Ende. Der Regressionstest scheiterte am bisherigen gegenseitigen Sperrwarten und besteht nach dem Fix.
|
||||||
|
- Abgewiesene systemd-Übergabe, zurückgezogene Freigabe bei bereits eingereihtem Auftrag und persistente Fehlerprotokolle bei Sperr-Timeout; jeweils ohne Installationsänderung oder Timer-Stopp.
|
||||||
|
- Ausdrückliche Cron-Policy und Writer-Wartezeit/Timeout.
|
||||||
|
|
||||||
**Serverabnahme steht separat aus:** echte Rocky-Linux-10-Umgebung, SELinux-/ACL-Verhalten auf den verwendeten Dateisystemen, installierte Nextcloud-/Gitea-Versionen, vollständige Writer-/Pfadliste, Berechtigungen des echten DB-Backupaccounts, tatsächlicher OneDrive-Upload großer Archive, Laufzeit/Speicherbedarf und fachliche Restore-Probe. Es wurde kein Produktivbackup ausgeführt und keine Serverinstallation verändert.
|
**Serverabnahme steht separat aus:** echte Rocky-Linux-10-Umgebung, SELinux-/ACL-Verhalten auf den verwendeten Dateisystemen, installierte Nextcloud-/Gitea-Versionen, vollständige Writer-/Pfadliste, Berechtigungen des echten DB-Backupaccounts, tatsächlicher OneDrive-Upload großer Archive, Laufzeit/Speicherbedarf und fachliche Restore-Probe. Es wurde kein Produktivbackup ausgeführt und keine Serverinstallation verändert.
|
||||||
@@ -21,6 +21,20 @@ anschließend als systemd-Auftrag unabhängig von der SSH-Verbindung. Bei unklar
|
|||||||
Gitea-/Cron-/rclone-Einstellungen fragt der Starter gezielt nach. Die Bedienung und
|
Gitea-/Cron-/rclone-Einstellungen fragt der Starter gezielt nach. Die Bedienung und
|
||||||
der kombinierte Rückweg stehen in [UPDATE_ANLEITUNG.md](UPDATE_ANLEITUNG.md).
|
der kombinierte Rückweg stehen in [UPDATE_ANLEITUNG.md](UPDATE_ANLEITUNG.md).
|
||||||
|
|
||||||
|
Der Starter bestätigt nur die Übergabe. Erst `SUCCESS` in der genannten
|
||||||
|
`result.txt` bestätigt Installation, Backup und Remote-Prüfung. Den Verlauf ohne
|
||||||
|
Terminal-Pager anzeigen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo journalctl -fu app-backup-upgrade.service --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
Ein Startabbruch nach genau 60 Sekunden in den Paketen `8e6c9a058fa9` und
|
||||||
|
`56fbcdad74f4` ist durch die asynchrone Sperrübergabe behoben. Bei diesem Fehler
|
||||||
|
hatte die Installation noch nicht begonnen; das neue Paket erneut verwenden.
|
||||||
|
Die kurzlebige Unit kann nach einem Fehler verschwinden, das Journal bleibt
|
||||||
|
abfragbar. Diagnose und Details stehen in der Update-Anleitung.
|
||||||
|
|
||||||
Die folgende manuelle Anleitung bleibt als Alternative für individuelle Updates.
|
Die folgende manuelle Anleitung bleibt als Alternative für individuelle Updates.
|
||||||
|
|
||||||
## Wesentliche Änderungen
|
## Wesentliche Änderungen
|
||||||
|
|||||||
+53
-7
@@ -24,7 +24,8 @@ Backups mit zeitweiliger Unterbrechung der konfigurierten Anwendungen.
|
|||||||
5. Crypt-Konfiguration und DB-Dateien prüfen; vorhandene Schlüssel/Passwörter erhalten.
|
5. Crypt-Konfiguration und DB-Dateien prüfen; vorhandene Schlüssel/Passwörter erhalten.
|
||||||
6. Vorprüfung mit geschützten Kopien durchführen. Bei Fehlern bleibt das installierte
|
6. Vorprüfung mit geschützten Kopien durchführen. Bei Fehlern bleibt das installierte
|
||||||
Paket samt Anwendungskonfiguration und Timer unverändert.
|
Paket samt Anwendungskonfiguration und Timer unverändert.
|
||||||
7. Einen von SSH unabhängigen systemd-Auftrag `app-backup-upgrade.service` starten.
|
7. Einen von SSH unabhängigen systemd-Auftrag `app-backup-upgrade.service` mit
|
||||||
|
`--no-block` einreihen und die Sperren an ihn übergeben.
|
||||||
8. Backup-Timer stoppen, Paket mit Rollback-Inventar installieren, Konfiguration
|
8. Backup-Timer stoppen, Paket mit Rollback-Inventar installieren, Konfiguration
|
||||||
atomar übernehmen und Rechte der aktiven root-eigenen Zugangsdaten korrigieren.
|
atomar übernehmen und Rechte der aktiven root-eigenen Zugangsdaten korrigieren.
|
||||||
9. Vollständiges Backup ohne lokale/Remote-Retention durchführen. Anschließend den
|
9. Vollständiges Backup ohne lokale/Remote-Retention durchführen. Anschließend den
|
||||||
@@ -37,7 +38,7 @@ Remote-Prüflauf belegt Archivlesbarkeit, ersetzt aber keinen fachlichen Test-Re
|
|||||||
## Verlauf und Ergebnis
|
## Verlauf und Ergebnis
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo journalctl -fu app-backup-upgrade.service
|
sudo journalctl -fu app-backup-upgrade.service --no-pager
|
||||||
```
|
```
|
||||||
|
|
||||||
Der Starter nennt außerdem den **konkreten** Ergebnisordner:
|
Der Starter nennt außerdem den **konkreten** Ergebnisordner:
|
||||||
@@ -47,9 +48,24 @@ sudo cat /var/backups/app-backup-upgrade-XXXXXXXX/result.txt
|
|||||||
sudo less /var/backups/app-backup-upgrade-XXXXXXXX/update.log
|
sudo less /var/backups/app-backup-upgrade-XXXXXXXX/update.log
|
||||||
```
|
```
|
||||||
|
|
||||||
Erwartet wird `SUCCESS`. Solange `RUNNING` steht, nicht parallel erneut starten.
|
Erwartet wird `SUCCESS`. Bei `QUEUED` oder `RUNNING` nicht parallel erneut starten.
|
||||||
Nach Ende verschwindet die kurzlebige systemd-Unit; Log und Ergebnis bleiben erhalten.
|
`QUEUED` bedeutet, dass der Auftrag zur Übergabe vorbereitet wurde und noch kein
|
||||||
Bei `QUEUED` wurde der Auftrag übergeben und wartet auf den Start.
|
Worker den Start bestätigt hat. Ein erfolgreicher Starter bestätigt nur die
|
||||||
|
Annahme durch systemd; Installation und Backup können anschließend noch scheitern.
|
||||||
|
Nach Ende verschwindet die kurzlebige systemd-Unit durch `--collect` auch bei
|
||||||
|
Fehlern. `Unit ... could not be found` ist daher kein Erfolgsnachweis und verhindert
|
||||||
|
die Journalabfrage nicht. Log und Ergebnis bleiben erhalten. Diagnose ohne Pager:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo journalctl -u app-backup-upgrade.service -n 100 --no-pager -l
|
||||||
|
sudo systemctl status app-backup-upgrade.service --no-pager -l
|
||||||
|
```
|
||||||
|
|
||||||
|
Bleibt `QUEUED` stehen, zuerst das Journal prüfen: Ein Fehler noch vor dem Start
|
||||||
|
des Bash-Workers (z. B. ein nicht ausführbarer Paketpfad) kann noch nicht in
|
||||||
|
`update.log` oder `result.txt` durch den Worker protokolliert werden. Ein späterer
|
||||||
|
Sperr-Timeout erscheint dagegen in beiden Dateien. Nicht allein auf den Exit-Code
|
||||||
|
von `systemd-run` vertrauen.
|
||||||
`--foreground` führt den Vorgang stattdessen im aktuellen Terminal aus und ist
|
`--foreground` führt den Vorgang stattdessen im aktuellen Terminal aus und ist
|
||||||
für Tests/Diagnose gedacht; dabei besteht keine Absicherung gegen SSH-Abbruch.
|
für Tests/Diagnose gedacht; dabei besteht keine Absicherung gegen SSH-Abbruch.
|
||||||
|
|
||||||
@@ -107,6 +123,30 @@ bleiben erhalten; beim Aktivieren kann `Persistent=true` einen Lauf nachholen.
|
|||||||
|
|
||||||
## Fehler und Rückweg
|
## Fehler und Rückweg
|
||||||
|
|
||||||
|
### Startfehler nach genau 60 Sekunden bei älteren Paketen
|
||||||
|
|
||||||
|
Die Pakete `8e6c9a058fa9` und `56fbcdad74f4` enthielten einen Fehler in der
|
||||||
|
systemd-Übergabe: Der Starter hielt die Update-Sperre und wartete synchron auf
|
||||||
|
den Abschluss des Startauftrags einer `Type=oneshot`-Unit. Dieser Startauftrag
|
||||||
|
endet erst mit dem Worker; der Worker wartete aber selbst auf die Sperre des
|
||||||
|
Starters. Nach 60 Sekunden brach seine Sperranforderung ab, noch vor Installation
|
||||||
|
und Timer-Stopp. In diesem Ablauf ist kein Rollback erforderlich. Der ursprüngliche
|
||||||
|
Worker hatte vor dem Sperrwarten noch kein eigenes Log eingerichtet, daher konnte
|
||||||
|
die Fehlermeldung auf den systemd-Exit-Code beschränkt bleiben.
|
||||||
|
|
||||||
|
Das korrigierte Paket verwendet `systemd-run --no-block` und gibt zuerst die
|
||||||
|
Backup-Sperre, dann die Update-Sperre frei. Eine fehlgeschlagene Übergabe entzieht
|
||||||
|
auch einem möglicherweise schon eingereihten Worker die Freigabe. Die Sperren
|
||||||
|
bleiben als Schutz vor parallelen Backups/Updates erhalten. Erneut das **neue**
|
||||||
|
Paket starten; weder Sperrdateien löschen noch alte Worker manuell nachstarten.
|
||||||
|
|
||||||
|
Das Verhalten von `--no-block` und `--collect` ist in der
|
||||||
|
[systemd-Dokumentation](https://github.com/systemd/systemd/blob/v256/man/systemd-run.xml)
|
||||||
|
beschrieben, der Startabschluss von `oneshot` in
|
||||||
|
[systemd.service](https://github.com/systemd/systemd/blob/v256/man/systemd.service.xml).
|
||||||
|
|
||||||
|
### Fehlende Mailablage und erneuter Start
|
||||||
|
|
||||||
Ein Abbruch `Missing source: /var/vmail` bei der **Vorprüfung**, wie beim Paket
|
Ein Abbruch `Missing source: /var/vmail` bei der **Vorprüfung**, wie beim Paket
|
||||||
`8e6c9a058fa9`, erfordert kein Rollback. Mit dem korrigierten Paket erneut starten
|
`8e6c9a058fa9`, erfordert kein Rollback. Mit dem korrigierten Paket erneut starten
|
||||||
und die Mailablage klären. Die bisherige Konfiguration und der Timer sind erhalten;
|
und die Mailablage klären. Die bisherige Konfiguration und der Timer sind erhalten;
|
||||||
@@ -134,6 +174,8 @@ Vorlage vollständig kopiert, sofern es auf demselben Dateisystem liegt und kein
|
|||||||
Verweis auf eine externe Ablage ist. Weitere Benutzer müssen in `MAIL_HOME_DIRS`
|
Verweis auf eine externe Ablage ist. Weitere Benutzer müssen in `MAIL_HOME_DIRS`
|
||||||
stehen; das Vorhandensein eines einzelnen Postfachs belegt nicht deren Abdeckung.
|
stehen; das Vorhandensein eines einzelnen Postfachs belegt nicht deren Abdeckung.
|
||||||
|
|
||||||
|
### Fehler nach Installationsbeginn
|
||||||
|
|
||||||
Bei einem Fehler nach Beginn der Installation bleibt der Backup-Timer gestoppt.
|
Bei einem Fehler nach Beginn der Installation bleibt der Backup-Timer gestoppt.
|
||||||
Es erfolgt kein automatisches Zurücksetzen über möglicherweise noch laufende oder
|
Es erfolgt kein automatisches Zurücksetzen über möglicherweise noch laufende oder
|
||||||
teilweise wiederhergestellte Anwendungen. Logs und gegebenenfalls
|
teilweise wiederhergestellte Anwendungen. Logs und gegebenenfalls
|
||||||
@@ -165,8 +207,12 @@ Das ausführbare Paket ausschließlich über euren vertrauenswürdigen Kanal bez
|
|||||||
|
|
||||||
## Verifikation der Lieferung
|
## Verifikation der Lieferung
|
||||||
|
|
||||||
34 Container-Tests sowie ShellCheck und Shell-Syntaxprüfung erfolgreich.
|
37 Container-Tests sowie ShellCheck und Shell-Syntaxprüfung erfolgreich.
|
||||||
Die Tests enthalten echte MariaDB-Dumps/Imports und lokale rclone-Crypt-Roundtrips;
|
Die Tests enthalten echte MariaDB-Dumps/Imports und lokale rclone-Crypt-Roundtrips;
|
||||||
systemd/occ und die systemd-run-Übergabe sind simuliert. Die tatsächliche
|
systemd/occ und die systemd-run-Übergabe sind simuliert. Das Übergabemodell
|
||||||
|
berücksichtigt jetzt das Warten auf den oneshot-Startauftrag: Der Regressionstest
|
||||||
|
scheiterte mit dem alten Code und war nach dem Fix erfolgreich. Geprüft sind auch
|
||||||
|
abgewiesene Übergaben, eingereihte Worker trotz fehlgeschlagener Antwort und
|
||||||
|
persistente Fehlerprotokolle bei belegter Update-Sperre. Die tatsächliche
|
||||||
Server-/OneDrive-Prüfung erfolgt beim ersten Paketlauf. Ein fachlicher Restore
|
Server-/OneDrive-Prüfung erfolgt beim ersten Paketlauf. Ein fachlicher Restore
|
||||||
auf einem separaten Testsystem bleibt erforderlich.
|
auf einem separaten Testsystem bleibt erforderlich.
|
||||||
+81
-3
@@ -541,20 +541,38 @@ QUIESCE_SERVICES=(nc-cron)
|
|||||||
result=self.run_script('app-backup.sh','--check',ok=False,APP_BACKUP_INHERIT_LOCK='1')
|
result=self.run_script('app-backup.sh','--check',ok=False,APP_BACKUP_INHERIT_LOCK='1')
|
||||||
self.assertIn('Inherited lock descriptor',result.stdout)
|
self.assertIn('Inherited lock descriptor',result.stdout)
|
||||||
|
|
||||||
def test_detached_package_handoff_finishes_after_starter_exits(self):
|
def systemd_launcher(self):
|
||||||
self.configure_upgrade()
|
"""Model start-job waiting, detached FDs, and ambiguous submit failures."""
|
||||||
launcher=self.bin/'systemd-run'
|
launcher=self.bin/'systemd-run'
|
||||||
launcher.write_text('''#!/usr/bin/env python3
|
launcher.write_text('''#!/usr/bin/env python3
|
||||||
import os,subprocess,sys
|
import os,subprocess,sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
args=sys.argv[1:]
|
args=sys.argv[1:]
|
||||||
assert '--unit=app-backup-upgrade' in args
|
assert '--unit=app-backup-upgrade' in args
|
||||||
|
assert '--property=Type=oneshot' in args
|
||||||
command=args[args.index('/bin/bash'):]
|
command=args[args.index('/bin/bash'):]
|
||||||
state=Path(command[-1])
|
state=Path(command[-1])
|
||||||
|
if os.environ.get('TEST_SYSTEMD_SUBMIT') == 'reject':
|
||||||
|
sys.exit('systemd refused submission')
|
||||||
with open(state/'test-worker.log','w') as log:
|
with open(state/'test-worker.log','w') as log:
|
||||||
subprocess.Popen(command,stdin=subprocess.DEVNULL,stdout=log,stderr=subprocess.STDOUT,env=os.environ,close_fds=True,start_new_session=True)
|
worker=subprocess.Popen(command,stdin=subprocess.DEVNULL,stdout=log,stderr=subprocess.STDOUT,env=os.environ,close_fds=True,start_new_session=True)
|
||||||
|
if os.environ.get('TEST_SYSTEMD_SUBMIT') == 'accepted-error':
|
||||||
|
sys.exit('simulated reply failure after systemd accepted the job')
|
||||||
|
# Type=oneshot remains starting until ExecStart exits. Real systemd-run waits
|
||||||
|
# for that start job unless --no-block is used; our old stub missed this.
|
||||||
|
if '--no-block' not in args:
|
||||||
|
try:
|
||||||
|
sys.exit(worker.wait(timeout=3))
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
worker.terminate()
|
||||||
|
worker.wait(timeout=5)
|
||||||
|
sys.exit('oneshot start job still waiting: launcher/worker lock deadlock')
|
||||||
''')
|
''')
|
||||||
launcher.chmod(0o755)
|
launcher.chmod(0o755)
|
||||||
|
|
||||||
|
def test_detached_package_handoff_finishes_after_starter_exits(self):
|
||||||
|
self.configure_upgrade()
|
||||||
|
self.systemd_launcher()
|
||||||
package=self.build_package()
|
package=self.build_package()
|
||||||
result=subprocess.run(['bash',str(package)],env=self.env,capture_output=True,text=True,timeout=60)
|
result=subprocess.run(['bash',str(package)],env=self.env,capture_output=True,text=True,timeout=60)
|
||||||
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
|
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
|
||||||
@@ -566,6 +584,66 @@ with open(state/'test-worker.log','w') as log:
|
|||||||
self.assertTrue(status.startswith('SUCCESS'),status+(state/'test-worker.log').read_text())
|
self.assertTrue(status.startswith('SUCCESS'),status+(state/'test-worker.log').read_text())
|
||||||
self.assertIn('unabhaengig von SSH',result.stdout)
|
self.assertIn('unabhaengig von SSH',result.stdout)
|
||||||
|
|
||||||
|
def test_systemd_submission_failure_preserves_installation_and_reports_result(self):
|
||||||
|
canonical=self.configure_upgrade()
|
||||||
|
original=canonical.read_bytes()
|
||||||
|
self.systemd_launcher()
|
||||||
|
package=self.build_package()
|
||||||
|
result=subprocess.run(['bash',str(package)],env=dict(self.env,TEST_SYSTEMD_SUBMIT='reject'),capture_output=True,text=True,timeout=60)
|
||||||
|
self.assertNotEqual(result.returncode,0)
|
||||||
|
state=Path(result.stdout.split('Update-Sicherung und Protokoll: ')[1].splitlines()[0])
|
||||||
|
self.assertIn('FAILED: systemd submission failed',(state/'result.txt').read_text())
|
||||||
|
self.assertIn(str(state/'result.txt'),result.stdout)
|
||||||
|
self.assertEqual(canonical.read_bytes(),original)
|
||||||
|
self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active')
|
||||||
|
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||||
|
|
||||||
|
def test_failed_submission_revokes_even_an_already_queued_worker(self):
|
||||||
|
canonical=self.configure_upgrade()
|
||||||
|
original=canonical.read_bytes()
|
||||||
|
self.systemd_launcher()
|
||||||
|
package=self.build_package()
|
||||||
|
result=subprocess.run(['bash',str(package)],env=dict(self.env,TEST_SYSTEMD_SUBMIT='accepted-error'),capture_output=True,text=True,timeout=60)
|
||||||
|
self.assertNotEqual(result.returncode,0)
|
||||||
|
state=Path(result.stdout.split('Update-Sicherung und Protokoll: ')[1].splitlines()[0])
|
||||||
|
for _ in range(100):
|
||||||
|
log=(state/'test-worker.log').read_text()
|
||||||
|
if 'Installation not started' in log: break
|
||||||
|
time.sleep(.1)
|
||||||
|
self.assertIn('submission was cancelled',log)
|
||||||
|
self.assertIn('Installation not started',log)
|
||||||
|
self.assertTrue((state/'result.txt').read_text().startswith('FAILED'))
|
||||||
|
self.assertEqual(canonical.read_bytes(),original)
|
||||||
|
self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active')
|
||||||
|
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||||
|
self.assertFalse((state/'package-rollback').exists())
|
||||||
|
|
||||||
|
def test_worker_lock_timeout_is_persisted_before_installation(self):
|
||||||
|
import fcntl
|
||||||
|
canonical=self.configure_upgrade()
|
||||||
|
original=canonical.read_bytes()
|
||||||
|
self.systemd_launcher()
|
||||||
|
package=self.build_package()
|
||||||
|
# Obtain a complete prepared state without starting an actual worker.
|
||||||
|
result=subprocess.run(['bash',str(package)],env=dict(self.env,TEST_SYSTEMD_SUBMIT='reject'),capture_output=True,text=True,timeout=60)
|
||||||
|
self.assertNotEqual(result.returncode,0)
|
||||||
|
state=Path(result.stdout.split('Update-Sicherung und Protokoll: ')[1].splitlines()[0])
|
||||||
|
(state/'result.txt').write_text('QUEUED\n')
|
||||||
|
wrapper=self.bin/'flock'
|
||||||
|
# Keep real flock contention; shorten only the 60s timeout in this test.
|
||||||
|
wrapper.write_text('#!/usr/bin/env bash\nif [[ $1 == -w && $2 == 60 ]]; then shift 2; exec /usr/bin/flock -w 0 "$@"; fi\nexec /usr/bin/flock "$@"\n')
|
||||||
|
wrapper.chmod(0o755)
|
||||||
|
with open('/run/app-backup-upgrade.lock','a') as lock:
|
||||||
|
fcntl.flock(lock,fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
failed=subprocess.run(['bash',str(REPO/'update.sh'),'--worker',str(state)],env=self.env,capture_output=True,text=True,timeout=10)
|
||||||
|
self.assertNotEqual(failed.returncode,0)
|
||||||
|
self.assertIn('Timed out waiting for update coordinator lock',(state/'update.log').read_text())
|
||||||
|
self.assertIn('Installation not started',(state/'result.txt').read_text())
|
||||||
|
self.assertTrue((state/'result.txt').read_text().startswith('FAILED'))
|
||||||
|
self.assertEqual(canonical.read_bytes(),original)
|
||||||
|
self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active')
|
||||||
|
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||||
|
|
||||||
def test_cron_inventory_requires_explicit_policy_and_sorts_timer_first(self):
|
def test_cron_inventory_requires_explicit_policy_and_sorts_timer_first(self):
|
||||||
canonical=self.configure_upgrade()
|
canonical=self.configure_upgrade()
|
||||||
cron=Path('/etc/cron.d/app-backup-test-nextcloud')
|
cron=Path('/etc/cron.d/app-backup-test-nextcloud')
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ done
|
|||||||
need systemctl; need flock
|
need systemctl; need flock
|
||||||
# This lock excludes two coordinators, independently from ordinary backup jobs.
|
# This lock excludes two coordinators, independently from ordinary backup jobs.
|
||||||
exec 8>/run/app-backup-upgrade.lock
|
exec 8>/run/app-backup-upgrade.lock
|
||||||
if [[ $UP_WORKER == true ]]; then flock -w 60 8; else flock -n 8 || die 'Another updater is preparing/running'; fi
|
# Workers install their persistent error reporting before waiting on this lock.
|
||||||
|
if [[ $UP_WORKER == false ]]; then flock -n 8 || die 'Another updater is preparing/running'; fi
|
||||||
|
|
||||||
prompt() {
|
prompt() {
|
||||||
local answer
|
local answer
|
||||||
@@ -244,20 +245,34 @@ if [[ $UP_WORKER == false ]]; then
|
|||||||
sha256sum "$UP_STATE/candidate.conf" | cut -d ' ' -f 1 > "$UP_STATE/candidate.sha256"
|
sha256sum "$UP_STATE/candidate.conf" | cut -d ' ' -f 1 > "$UP_STATE/candidate.sha256"
|
||||||
echo "Vorpruefung erfolgreich. Writer: $(tr '\n' ' ' < "$UP_STATE/writers")"
|
echo "Vorpruefung erfolgreich. Writer: $(tr '\n' ' ' < "$UP_STATE/writers")"
|
||||||
echo 'Jetzt folgen Installation, ein vollstaendiges Backup und Remote-Restore-Pruefung.'
|
echo 'Jetzt folgen Installation, ein vollstaendiges Backup und Remote-Restore-Pruefung.'
|
||||||
|
printf 'QUEUED\n' > "$UP_STATE/result.txt"
|
||||||
if [[ $UP_FOREGROUND == true ]]; then
|
if [[ $UP_FOREGROUND == true ]]; then
|
||||||
flock -u 9; flock -u 8
|
flock -u 9; flock -u 8
|
||||||
exec bash "$UPDATE_ROOT/update.sh" --worker "$UP_STATE"
|
exec bash "$UPDATE_ROOT/update.sh" --worker "$UP_STATE"
|
||||||
fi
|
fi
|
||||||
# A new systemd service inherits no shell FDs; it acquires both locks itself.
|
# A oneshot start job completes only when ExecStart exits. Waiting for that
|
||||||
printf 'QUEUED\n' > "$UP_STATE/result.txt"
|
# job while holding FD 8 deadlocks with the worker waiting for the same lock.
|
||||||
systemd-run --unit=app-backup-upgrade --collect --property=Type=oneshot \
|
# Only enqueue here; result.txt, not systemd-run's exit code, proves success.
|
||||||
|
# The launcher must not keep copies of our locks; systemd starts a fresh worker.
|
||||||
|
systemd-run --no-block --unit=app-backup-upgrade --collect --property=Type=oneshot \
|
||||||
--property=TimeoutStartSec=24h --property=TimeoutStopSec=5min --property=KillMode=mixed \
|
--property=TimeoutStartSec=24h --property=TimeoutStopSec=5min --property=KillMode=mixed \
|
||||||
/bin/bash "$UPDATE_ROOT/update.sh" --worker "$UP_STATE" || {
|
/bin/bash "$UPDATE_ROOT/update.sh" --worker "$UP_STATE" 8>&- 9>&- || {
|
||||||
printf 'FAILED: systemd worker could not be started; installed package/config/timer unchanged.\n' > "$UP_STATE/result.txt"
|
# Revoke the queued state while still holding the coordinator lock. Even
|
||||||
|
# if systemd accepted the job but its reply failed, it must not install.
|
||||||
|
printf 'FAILED: systemd submission failed; installed package/config/timer unchanged.\n' > "$UP_STATE/result.txt"
|
||||||
|
cat "$UP_STATE/result.txt"
|
||||||
|
echo "Details: sudo journalctl -u app-backup-upgrade.service -n 100 --no-pager"
|
||||||
|
echo "Ergebnis: sudo cat $UP_STATE/result.txt"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
echo 'Update laeuft unabhaengig von SSH weiter.'
|
# Release the backup lock FIRST. Once FD 8 is free, the worker may acquire
|
||||||
echo 'Verlauf: sudo journalctl -fu app-backup-upgrade.service'
|
# both immediately; reversing the order can make its backup lock check fail.
|
||||||
|
flock -u 9
|
||||||
|
flock -u 8
|
||||||
|
exec 9>&- 8>&-
|
||||||
|
echo 'Update-Auftrag an systemd uebergeben; Ausfuehrung unabhaengig von SSH.'
|
||||||
|
echo 'Abgeschlossen ist das Update erst bei SUCCESS in der Ergebnisdatei.'
|
||||||
|
echo 'Verlauf: sudo journalctl -fu app-backup-upgrade.service --no-pager'
|
||||||
echo "Ergebnis: sudo cat $UP_STATE/result.txt"
|
echo "Ergebnis: sudo cat $UP_STATE/result.txt"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -289,6 +304,10 @@ worker_finish() {
|
|||||||
trap worker_finish EXIT
|
trap worker_finish EXIT
|
||||||
trap 'exit 130' INT
|
trap 'exit 130' INT
|
||||||
trap 'exit 143' TERM
|
trap 'exit 143' TERM
|
||||||
|
# Record lock timeouts in update.log/result.txt as well as journald. Previously
|
||||||
|
# this wait happened before logging/traps, leaving only systemd's exit status.
|
||||||
|
flock -w 60 8 || die 'Timed out waiting for update coordinator lock (60s); installation not started'
|
||||||
|
[[ $(cat "$UP_STATE/result.txt") == QUEUED ]] || die 'Upgrade is not queued or submission was cancelled; rerun updater'
|
||||||
printf 'RUNNING\n' > "$UP_STATE/result.txt"
|
printf 'RUNNING\n' > "$UP_STATE/result.txt"
|
||||||
acquire_backup_lock
|
acquire_backup_lock
|
||||||
export APP_BACKUP_INHERIT_LOCK=1
|
export APP_BACKUP_INHERIT_LOCK=1
|
||||||
|
|||||||
Reference in new issue
Block a user