Pause backup scheduling and atomically deploy workstation updates
This commit is contained in:
1 parent
8245571f15
commit
a1a843a6f7
4 files changed
+39
-6
No files matched your search
+2
-2
@@ -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.
|
`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
|
## Grenzen
|
||||||
|
|
||||||
|
|||||||
+12
-4
@@ -20,9 +20,6 @@ EXISTING_CONFIG=false
|
|||||||
|
|
||||||
mkdir -p "${HOME}/.local/libexec" "$BIN_DIR" "$CONFIG_DIR" "$STATE_DIR/logs" "$SYSTEMD_USER_DIR"
|
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
|
if [[ ! -f "$CONFIG_DIR/config" ]]; then
|
||||||
install -m 0600 "$ROOT_DIR/etc/config.example" "$CONFIG_DIR/config"
|
install -m 0600 "$ROOT_DIR/etc/config.example" "$CONFIG_DIR/config"
|
||||||
printf 'Installed config: %s\n' "$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'
|
printf 'Store a copy of this passphrase somewhere safe for disaster recovery.\n'
|
||||||
fi
|
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
|
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
|
done
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
systemctl --user enable nextcloud-borg-backup.timer nextcloud-borg-health.timer
|
systemctl --user enable nextcloud-borg-backup.timer nextcloud-borg-health.timer
|
||||||
|
|||||||
@@ -400,6 +400,7 @@ def main():
|
|||||||
sys.stdout = Tee()
|
sys.stdout = Tee()
|
||||||
# Child output goes to journal; lifecycle/phase/status additionally go to the run log.
|
# Child output goes to journal; lifecycle/phase/status additionally go to the run log.
|
||||||
try:
|
try:
|
||||||
|
log('Verified cloud-to-NAS runner 2.0.0, revision 3')
|
||||||
runner.backup('--if-due' in sys.argv)
|
runner.backup('--if-due' in sys.argv)
|
||||||
finally:
|
finally:
|
||||||
sys.stdout = sys.__stdout__
|
sys.stdout = sys.__stdout__
|
||||||
|
|||||||
@@ -175,6 +175,30 @@ class BackupTests(unittest.TestCase):
|
|||||||
with self.assertRaises(m.BackupError):
|
with self.assertRaises(m.BackupError):
|
||||||
self.runner.validate_final_inventory(before, after)
|
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):
|
def test_pending_maintenance_survives_interrupted_attempt_record(self):
|
||||||
import time
|
import time
|
||||||
self.runner.success = {'completed_at': time.time(), 'maintenance_pending': True}
|
self.runner.success = {'completed_at': time.time(), 'maintenance_pending': True}
|
||||||
|
|||||||
Reference in new issue
Block a user