Pause backup scheduling and atomically deploy workstation updates

This commit is contained in:
admin committed 2026-10-26 12:38:11 +01:00
1 parent 8245571f15
commit a1a843a6f7
4 files changed
+39 -6

No files matched your search

+2 -2
View File
@@ -106,9 +106,9 @@ Passphrase und exportierten Schlüssel unabhängig vom NAS und der Workstation s
`make check` prüft Bash-Syntax, Python, alle vier systemd-Units und automatisierte Ausfallszenarien. Integrationstests verwenden echtes rclone mit lokalem Test-Remote sowie echtes Borg in temporären Verzeichnissen: Archiv erstellen, Daten prüfen, extrahieren und Bytevergleich. Sie verändern weder echte Cloud noch produktives Repository.
17 Tests prüfen außerdem leere/geschrumpfte Quelle, falsches NAS, fremdes/symlinkendes Staging, Netzfehler in Sync und Check, geänderte Cloud-Inventur, Repository-Austausch, konkurrierende Läufe, Timeout, Zeitrücksprung und Wartungswiederholung. Gleiche Dateigröße und gleicher Zeitstempel bei geändertem Inhalt werden im Integrationstest ausdrücklich abgedeckt.
18 Tests prüfen außerdem leere/geschrumpfte Quelle, falsches NAS, fremdes/symlinkendes Staging, Netzfehler in Sync und Check, geänderte Cloud-Inventur, Repository-Austausch, konkurrierende Läufe, Timeout, Zeitrücksprung und Wartungswiederholung. Gleiche Dateigröße und gleicher Zeitstempel bei geändertem Inhalt werden im Integrationstest ausdrücklich abgedeckt.
Der Installer aktualisiert Programme und Units, erhält private Konfiguration und Schlüssel und startet die Timer neu. Eine fehlende Passphrase bei vorhandenem Repository führt zum Abbruch. Die Deinstallation stoppt Backup/Timer und entfernt Programme/Units; Daten, Schlüssel, State und Archive bleiben erhalten.
Der Installer erhält private Konfiguration und Schlüssel, hält vorhandenen Backup-Dienst und Timer gemeinsam an, ersetzt Programme und Units atomar und startet die Timer neu. Der Update-Test prüft die Erhaltung von Konfiguration und Schlüssel sowie den Timer-Stopp. Eine fehlende Passphrase bei vorhandenem Repository führt zum Abbruch. Die Deinstallation stoppt Backup/Timer und entfernt Programme/Units; Daten, Schlüssel, State und Archive bleiben erhalten.
## Grenzen
+12 -4
View File
@@ -20,9 +20,6 @@ EXISTING_CONFIG=false
mkdir -p "${HOME}/.local/libexec" "$BIN_DIR" "$CONFIG_DIR" "$STATE_DIR/logs" "$SYSTEMD_USER_DIR"
install -m 0750 "$ROOT_DIR/scripts/nextcloud-borg-runner.py" "${HOME}/.local/libexec/nextcloud-borg-runner.py"
install -m 0750 "$ROOT_DIR/scripts/nextcloud-borg-backup" "$BIN_DIR/nextcloud-borg-backup"
if [[ ! -f "$CONFIG_DIR/config" ]]; then
install -m 0600 "$ROOT_DIR/etc/config.example" "$CONFIG_DIR/config"
printf 'Installed config: %s\n' "$CONFIG_DIR/config"
@@ -60,8 +57,19 @@ if [[ ! -f "$KEY_PATH" ]]; then
printf 'Store a copy of this passphrase somewhere safe for disaster recovery.\n'
fi
# Prevent a pending timer event from starting between replacing wrapper and runner.
if systemctl --user cat nextcloud-borg-backup.service >/dev/null 2>&1; then
systemctl --user stop nextcloud-borg-backup.service nextcloud-borg-backup.timer
fi
atomic_install() {
install -m "$3" "$1" "$2.new"
mv -f "$2.new" "$2"
}
atomic_install "$ROOT_DIR/scripts/nextcloud-borg-runner.py" "${HOME}/.local/libexec/nextcloud-borg-runner.py" 0750
atomic_install "$ROOT_DIR/scripts/nextcloud-borg-backup" "$BIN_DIR/nextcloud-borg-backup" 0750
for unit in nextcloud-borg-backup.service nextcloud-borg-backup.timer nextcloud-borg-health.service nextcloud-borg-health.timer; do
install -m 0644 "$ROOT_DIR/systemd/user/$unit" "$SYSTEMD_USER_DIR/$unit"
atomic_install "$ROOT_DIR/systemd/user/$unit" "$SYSTEMD_USER_DIR/$unit" 0644
done
systemctl --user daemon-reload
systemctl --user enable nextcloud-borg-backup.timer nextcloud-borg-health.timer
+1
View File
@@ -400,6 +400,7 @@ def main():
sys.stdout = Tee()
# Child output goes to journal; lifecycle/phase/status additionally go to the run log.
try:
log('Verified cloud-to-NAS runner 2.0.0, revision 3')
runner.backup('--if-due' in sys.argv)
finally:
sys.stdout = sys.__stdout__
+24
View File
@@ -175,6 +175,30 @@ class BackupTests(unittest.TestCase):
with self.assertRaises(m.BackupError):
self.runner.validate_final_inventory(before, after)
def test_installer_preserves_config_and_key_and_pauses_timer(self):
home = self.root/'install-home'
config = home/'.config/nextcloud-borg-backup'
config.mkdir(parents=True)
(config/'config').write_text('# preserved custom config\n')
(config/'passphrase').write_text('existing-test-key')
fake = self.root/'fake-bin'
fake.mkdir()
commands = self.root/'systemctl-calls'
(fake/'systemctl').write_text('#!/bin/sh\nprintf "%s\\n" "$*" >> "$CALL_LOG"\nexit 0\n')
(fake/'systemctl').chmod(0o755)
env = dict(os.environ, HOME=str(home), XDG_CONFIG_HOME=str(home/'.config'),
XDG_STATE_HOME=str(home/'.local/state'), PATH=str(fake)+':'+os.environ['PATH'],
CALL_LOG=str(commands))
subprocess.run([str(ROOT/'scripts/install.sh')], env=env, check=True, stdout=subprocess.DEVNULL)
self.assertEqual((config/'config').read_text(), '# preserved custom config\n')
self.assertEqual((config/'passphrase').read_text(), 'existing-test-key')
calls = commands.read_text()
self.assertIn('stop nextcloud-borg-backup.service nextcloud-borg-backup.timer', calls)
self.assertIn('restart nextcloud-borg-backup.timer nextcloud-borg-health.timer', calls)
self.assertEqual((home/'.local/libexec/nextcloud-borg-runner.py').read_bytes(),
(ROOT/'scripts/nextcloud-borg-runner.py').read_bytes())
self.assertEqual(list(home.rglob('*.new')), [])
def test_pending_maintenance_survives_interrupted_attempt_record(self):
import time
self.runner.success = {'completed_at': time.time(), 'maintenance_pending': True}