(release) Add explicit incomplete snapshot cleanup
Add a dedicated cleanup path for incomplete snapshots instead of letting retention prune them implicitly. The retention plan now exposes a guarded form that requires host and delete-count confirmation before removing .incomplete snapshot directories and their SQL records. Keep scheduled/manual retention behavior unchanged, add path safety checks, and cover cleanup success, confirmation failures, max-delete limits, and unexpected paths in tests. Refs #10
This commit is contained in:
@@ -1620,6 +1620,68 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "Incomplete Snapshots")
|
||||
self.assertContains(response, "20260519-031500Z__BROKEN01")
|
||||
self.assertContains(response, "excluded from retention cleanup")
|
||||
self.assertContains(response, "Delete incomplete snapshots")
|
||||
self.assertContains(response, "Type 1 to confirm the current number of incomplete snapshots.")
|
||||
|
||||
def test_incomplete_cleanup_deletes_incomplete_snapshot_after_confirmation(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
with TemporaryDirectory() as tmp:
|
||||
home = Path(tmp) / "home"
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
incomplete_dir = Path(tmp) / "backups" / host.host / ".incomplete" / "20260519-031500Z__BROKEN01"
|
||||
incomplete_dir.mkdir(parents=True)
|
||||
incomplete_dir.joinpath("partial-file").write_text("interrupted\n")
|
||||
record = SnapshotRecord.objects.create(
|
||||
host=host,
|
||||
kind=SnapshotRecord.Kind.INCOMPLETE,
|
||||
dirname=incomplete_dir.name,
|
||||
path=str(incomplete_dir),
|
||||
status="failed",
|
||||
started_at=datetime(2026, 5, 19, 3, 15, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
with override_settings(POBSYNC_HOME=str(home)):
|
||||
response = self.client.post(
|
||||
reverse("cleanup_host_incomplete_snapshots", args=[host.host]),
|
||||
{
|
||||
"max_delete": "1",
|
||||
"confirm_host": host.host,
|
||||
"confirm_delete_count": "1",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertFalse(incomplete_dir.exists())
|
||||
|
||||
self.assertRedirects(response, reverse("host_retention_plan", args=[host.host]))
|
||||
self.assertContains(response, "Deleted 1 incomplete snapshot(s) for web-01.")
|
||||
self.assertFalse(SnapshotRecord.objects.filter(pk=record.pk).exists())
|
||||
|
||||
def test_incomplete_cleanup_rejects_bad_confirmation(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
SnapshotRecord.objects.create(
|
||||
host=host,
|
||||
kind=SnapshotRecord.Kind.INCOMPLETE,
|
||||
dirname="20260519-031500Z__BROKEN01",
|
||||
path=f"/backups/{host.host}/.incomplete/20260519-031500Z__BROKEN01",
|
||||
status="failed",
|
||||
started_at=datetime(2026, 5, 19, 3, 15, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("cleanup_host_incomplete_snapshots", args=[host.host]),
|
||||
{
|
||||
"max_delete": "1",
|
||||
"confirm_host": "wrong",
|
||||
"confirm_delete_count": "1",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse("host_retention_plan", args=[host.host]))
|
||||
self.assertContains(response, "Incomplete cleanup confirmation is invalid.")
|
||||
self.assertEqual(SnapshotRecord.objects.filter(kind=SnapshotRecord.Kind.INCOMPLETE).count(), 1)
|
||||
|
||||
def test_host_detail_surfaces_retention_warnings(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
|
||||
Reference in New Issue
Block a user