feat: link backup runs to snapshot records

Add a nullable SnapshotRecord foreign key to BackupRun and populate it
when run_pobsync_backup records a completed or failed snapshot. Keep the
existing snapshot_path for audit compatibility while making run-to-snapshot
navigation explicit in the database and admin.
This commit is contained in:
2026-05-19 11:13:06 +02:00
parent 0a49c5719c
commit 5808800981
5 changed files with 48 additions and 9 deletions

View File

@@ -50,9 +50,10 @@ class HostConfigAdmin(admin.ModelAdmin):
@admin.register(BackupRun) @admin.register(BackupRun)
class BackupRunAdmin(admin.ModelAdmin): class BackupRunAdmin(admin.ModelAdmin):
list_display = ("host", "run_type", "status", "started_at", "ended_at", "snapshot_path") list_display = ("host", "run_type", "status", "started_at", "ended_at", "snapshot")
list_filter = ("run_type", "status", "started_at") list_filter = ("run_type", "status", "started_at")
search_fields = ("host__host", "snapshot_path") search_fields = ("host__host", "snapshot_path", "snapshot__dirname", "snapshot__path")
autocomplete_fields = ("snapshot",)
@admin.register(SnapshotRecord) @admin.register(SnapshotRecord)

View File

@@ -64,23 +64,26 @@ class Command(BaseCommand):
rsync = result.get("rsync") if isinstance(result.get("rsync"), dict) else {} rsync = result.get("rsync") if isinstance(result.get("rsync"), dict) else {}
run.rsync_exit_code = rsync.get("exit_code") run.rsync_exit_code = rsync.get("exit_code")
run.result = result run.result = result
snapshot_record = None
if run.snapshot_path:
snapshot_path = Path(run.snapshot_path)
try:
kind = infer_snapshot_kind(snapshot_path)
snapshot_record, _created = upsert_snapshot_record(host=host, kind=kind, snapshot_dir=snapshot_path)
except ValueError:
snapshot_record = None
run.snapshot = snapshot_record
run.save( run.save(
update_fields=[ update_fields=[
"status", "status",
"ended_at", "ended_at",
"snapshot_path", "snapshot_path",
"snapshot",
"base_path", "base_path",
"rsync_exit_code", "rsync_exit_code",
"result", "result",
], ],
) )
if run.snapshot_path:
snapshot_path = Path(run.snapshot_path)
try:
kind = infer_snapshot_kind(snapshot_path)
upsert_snapshot_record(host=host, kind=kind, snapshot_dir=snapshot_path)
except ValueError:
pass
if result.get("ok"): if result.get("ok"):
self.stdout.write(self.style.SUCCESS(f"Backup completed for {host.host}.")) self.stdout.write(self.style.SUCCESS(f"Backup completed for {host.host}."))

View File

@@ -0,0 +1,24 @@
from __future__ import annotations
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pobsync_backend", "0003_structured_config_fields"),
]
operations = [
migrations.AddField(
model_name="backuprun",
name="snapshot",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="backup_runs",
to="pobsync_backend.snapshotrecord",
),
),
]

View File

@@ -82,6 +82,13 @@ class BackupRun(models.Model):
started_at = models.DateTimeField(null=True, blank=True) started_at = models.DateTimeField(null=True, blank=True)
ended_at = models.DateTimeField(null=True, blank=True) ended_at = models.DateTimeField(null=True, blank=True)
snapshot_path = models.CharField(max_length=1024, blank=True) snapshot_path = models.CharField(max_length=1024, blank=True)
snapshot = models.ForeignKey(
"SnapshotRecord",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="backup_runs",
)
base_path = models.CharField(max_length=1024, blank=True) base_path = models.CharField(max_length=1024, blank=True)
rsync_exit_code = models.IntegerField(null=True, blank=True) rsync_exit_code = models.IntegerField(null=True, blank=True)
result = models.JSONField(default=dict, blank=True) result = models.JSONField(default=dict, blank=True)

View File

@@ -43,8 +43,10 @@ class RunBackupRecordsSnapshotTests(TestCase):
call_command("run_pobsync_backup", host.host, prefix=str(Path(tmp) / "home"), stdout=StringIO()) call_command("run_pobsync_backup", host.host, prefix=str(Path(tmp) / "home"), stdout=StringIO())
self.assertEqual(BackupRun.objects.count(), 1) self.assertEqual(BackupRun.objects.count(), 1)
run = BackupRun.objects.get()
self.assertEqual(SnapshotRecord.objects.count(), 1) self.assertEqual(SnapshotRecord.objects.count(), 1)
record = SnapshotRecord.objects.get() record = SnapshotRecord.objects.get()
self.assertEqual(run.snapshot, record)
self.assertEqual(record.host, host) self.assertEqual(record.host, host)
self.assertEqual(record.kind, "scheduled") self.assertEqual(record.kind, "scheduled")
self.assertEqual(record.status, "success") self.assertEqual(record.status, "success")
@@ -74,6 +76,7 @@ class RunBackupRecordsSnapshotTests(TestCase):
run = BackupRun.objects.get() run = BackupRun.objects.get()
self.assertEqual(run.status, BackupRun.Status.FAILED) self.assertEqual(run.status, BackupRun.Status.FAILED)
record = SnapshotRecord.objects.get() record = SnapshotRecord.objects.get()
self.assertEqual(run.snapshot, record)
self.assertEqual(record.kind, "incomplete") self.assertEqual(record.kind, "incomplete")
self.assertEqual(record.status, "failed") self.assertEqual(record.status, "failed")
@@ -99,4 +102,5 @@ class RunBackupRecordsSnapshotTests(TestCase):
) )
self.assertEqual(BackupRun.objects.count(), 1) self.assertEqual(BackupRun.objects.count(), 1)
self.assertIsNone(BackupRun.objects.get().snapshot)
self.assertEqual(SnapshotRecord.objects.count(), 0) self.assertEqual(SnapshotRecord.objects.count(), 0)