(feature) Add host backup control actions
Turn the host detail page into a more useful operator surface for starting greenfield backups from Django. Add quick actions for dry-run and real backup runs, keep the advanced manual options available, and show whether a host is ready, disabled, or blocked by missing global config. Surface queued and running counts plus a direct link to the active run. Expose requested backup options on the run detail page and cover the new control flow with view tests.
This commit is contained in:
@@ -211,6 +211,7 @@ class ViewTests(TestCase):
|
||||
|
||||
def test_host_detail_renders_config_schedule_runs_and_snapshots(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
GlobalConfig.objects.create(name="default", backup_root="/backups")
|
||||
host = HostConfig.objects.create(
|
||||
host="web-01",
|
||||
address="web-01.example.test",
|
||||
@@ -231,12 +232,38 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "Discover snapshots")
|
||||
self.assertContains(response, "Edit schedule")
|
||||
self.assertContains(response, "Edit config")
|
||||
self.assertContains(response, "Queue Manual Backup")
|
||||
self.assertContains(response, "Backup Control")
|
||||
self.assertContains(response, "Queue dry-run")
|
||||
self.assertContains(response, "Queue backup")
|
||||
self.assertContains(response, "ready")
|
||||
self.assertContains(response, "Snapshot Discovery")
|
||||
self.assertContains(response, reverse("queue_manual_backup", args=[host.host]))
|
||||
self.assertContains(response, reverse("run_detail", args=[BackupRun.objects.get().id]))
|
||||
self.assertContains(response, reverse("snapshot_detail", args=[snapshot.id]))
|
||||
|
||||
def test_host_detail_surfaces_active_backup_run(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
GlobalConfig.objects.create(name="default", backup_root="/backups")
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
run = BackupRun.objects.create(host=host, status=BackupRun.Status.QUEUED)
|
||||
|
||||
response = self.client.get(reverse("host_detail", args=[host.host]))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "queued")
|
||||
self.assertContains(response, f"Run {run.id}")
|
||||
self.assertContains(response, reverse("run_detail", args=[run.id]))
|
||||
|
||||
def test_host_detail_disables_backup_controls_without_global_config(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
|
||||
response = self.client.get(reverse("host_detail", args=[host.host]))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "missing global config")
|
||||
self.assertContains(response, "Create the default global config before queueing backups.")
|
||||
|
||||
def test_host_detail_renders_discovery_status_for_existing_snapshot_dirs(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
with TemporaryDirectory() as tmp:
|
||||
@@ -292,6 +319,29 @@ class ViewTests(TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_queue_manual_backup_quick_action_can_queue_real_backup(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
GlobalConfig.objects.create(name="default", backup_root="/backups")
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
|
||||
response = self.client.post(
|
||||
reverse("queue_manual_backup", args=[host.host]),
|
||||
{"prune_max_delete": "10"},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
run = BackupRun.objects.get(host=host)
|
||||
self.assertRedirects(response, reverse("run_detail", args=[run.id]))
|
||||
self.assertEqual(
|
||||
run.result["requested"],
|
||||
{
|
||||
"dry_run": False,
|
||||
"prune": False,
|
||||
"prune_max_delete": 10,
|
||||
"prune_protect_bases": False,
|
||||
},
|
||||
)
|
||||
|
||||
def test_queue_manual_backup_requires_default_global_config(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
@@ -332,7 +382,16 @@ class ViewTests(TestCase):
|
||||
snapshot_path=snapshot.path,
|
||||
base_path="/backups/web-01/scheduled/base",
|
||||
rsync_exit_code=0,
|
||||
result={"ok": True, "snapshot": snapshot.path},
|
||||
result={
|
||||
"ok": True,
|
||||
"snapshot": snapshot.path,
|
||||
"requested": {
|
||||
"dry_run": True,
|
||||
"prune": False,
|
||||
"prune_max_delete": 10,
|
||||
"prune_protect_bases": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("run_detail", args=[run.id]))
|
||||
@@ -342,6 +401,8 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "web-01")
|
||||
self.assertContains(response, "success")
|
||||
self.assertContains(response, "ABCDEFGH")
|
||||
self.assertContains(response, "Requested Options")
|
||||
self.assertContains(response, "Dry run:</strong> yes")
|
||||
self.assertContains(response, ""ok": true")
|
||||
self.assertContains(response, reverse("snapshot_detail", args=[snapshot.id]))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user