feat: record backup snapshots during run completion

Upsert SnapshotRecord rows directly from run_pobsync_backup results so
new successful and failed backup runs are reflected in the database
without requiring a separate discovery pass. Keep discovery for existing
snapshots and repair workflows, and cover success, failure, and dry-run
behavior with tests.
This commit is contained in:
2026-05-19 11:09:20 +02:00
parent 336fb1a5be
commit 0a49c5719c
3 changed files with 140 additions and 11 deletions

View File

@@ -0,0 +1,102 @@
from __future__ import annotations
from io import StringIO
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
from django.core.management import call_command
from django.core.management.base import CommandError
from django.test import TestCase
from pobsync.util import write_yaml_atomic
from pobsync_backend.models import BackupRun, GlobalConfig, HostConfig, SnapshotRecord
class RunBackupRecordsSnapshotTests(TestCase):
def test_successful_backup_upserts_scheduled_snapshot_record(self) -> None:
with TemporaryDirectory() as tmp:
backup_root = Path(tmp) / "backups"
GlobalConfig.objects.create(name="default", backup_root=str(backup_root))
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
snapshot_dir = backup_root / host.host / "scheduled" / "20260519-021500Z__ABCDEFGH"
meta_dir = snapshot_dir / "meta"
meta_dir.mkdir(parents=True)
write_yaml_atomic(
meta_dir / "meta.yaml",
{
"status": "success",
"started_at": "2026-05-19T02:15:00Z",
"ended_at": "2026-05-19T02:16:00Z",
},
)
with patch("pobsync_backend.management.commands.run_pobsync_backup.run_scheduled") as run_scheduled:
run_scheduled.return_value = {
"ok": True,
"dry_run": False,
"host": host.host,
"snapshot": str(snapshot_dir),
"base": None,
"rsync": {"exit_code": 0},
}
call_command("run_pobsync_backup", host.host, prefix=str(Path(tmp) / "home"), stdout=StringIO())
self.assertEqual(BackupRun.objects.count(), 1)
self.assertEqual(SnapshotRecord.objects.count(), 1)
record = SnapshotRecord.objects.get()
self.assertEqual(record.host, host)
self.assertEqual(record.kind, "scheduled")
self.assertEqual(record.status, "success")
def test_failed_backup_upserts_incomplete_snapshot_record(self) -> None:
with TemporaryDirectory() as tmp:
backup_root = Path(tmp) / "backups"
GlobalConfig.objects.create(name="default", backup_root=str(backup_root))
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
snapshot_dir = backup_root / host.host / ".incomplete" / "20260519-021500Z__ABCDEFGH"
meta_dir = snapshot_dir / "meta"
meta_dir.mkdir(parents=True)
write_yaml_atomic(meta_dir / "meta.yaml", {"status": "failed", "started_at": "2026-05-19T02:15:00Z"})
with patch("pobsync_backend.management.commands.run_pobsync_backup.run_scheduled") as run_scheduled:
run_scheduled.return_value = {
"ok": False,
"dry_run": False,
"host": host.host,
"snapshot": str(snapshot_dir),
"status": "failed",
"rsync": {"exit_code": 12},
}
with self.assertRaises(CommandError):
call_command("run_pobsync_backup", host.host, prefix=str(Path(tmp) / "home"), stdout=StringIO())
run = BackupRun.objects.get()
self.assertEqual(run.status, BackupRun.Status.FAILED)
record = SnapshotRecord.objects.get()
self.assertEqual(record.kind, "incomplete")
self.assertEqual(record.status, "failed")
def test_dry_run_does_not_create_snapshot_record(self) -> None:
with TemporaryDirectory() as tmp:
GlobalConfig.objects.create(name="default", backup_root=str(Path(tmp) / "backups"))
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
with patch("pobsync_backend.management.commands.run_pobsync_backup.run_scheduled") as run_scheduled:
run_scheduled.return_value = {
"ok": True,
"dry_run": True,
"host": host.host,
"base": None,
"rsync": {"exit_code": 0},
}
call_command(
"run_pobsync_backup",
host.host,
prefix=str(Path(tmp) / "home"),
dry_run=True,
stdout=StringIO(),
)
self.assertEqual(BackupRun.objects.count(), 1)
self.assertEqual(SnapshotRecord.objects.count(), 0)