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:
@@ -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)
|
||||||
|
|||||||
@@ -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}."))
|
||||||
|
|||||||
24
src/pobsync_backend/migrations/0004_backuprun_snapshot.py
Normal file
24
src/pobsync_backend/migrations/0004_backuprun_snapshot.py
Normal 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user