(feature) Add host doctor checks and Django log viewer
Add host-level checks for address, enabled state, SSH credential selection, and backup directory readiness, and show them on the host detail page. Create host backup directories during host creation and prefill new hosts from the default global config. Add a staff-only logs view backed by journalctl with filtering by pobsync unit, priority, and message text. Improve runtime checks for gunicorn in virtualenv installs and ensure the native installer grants the service user access to the backup root.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
@@ -95,6 +96,27 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "Database connection")
|
||||
self.assertContains(response, "POBSYNC_HOME")
|
||||
|
||||
def test_logs_view_renders_filtered_journal_messages(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
completed = subprocess.CompletedProcess(
|
||||
args=["journalctl"],
|
||||
returncode=0,
|
||||
stdout="2026-05-19 pobsync-worker.service failed backup\n2026-05-19 pobsync-web.service started\n",
|
||||
stderr="",
|
||||
)
|
||||
|
||||
with patch("pobsync_backend.views.shutil.which", return_value="/usr/bin/journalctl"), patch(
|
||||
"pobsync_backend.views.subprocess.run", return_value=completed
|
||||
) as run:
|
||||
response = self.client.get(reverse("logs"), {"unit": "pobsync-worker.service", "priority": "0..3", "q": "failed"})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Logs")
|
||||
self.assertContains(response, "failed backup")
|
||||
self.assertNotContains(response, "started")
|
||||
self.assertIn("-u", run.call_args.args[0])
|
||||
self.assertIn("pobsync-worker.service", run.call_args.args[0])
|
||||
|
||||
def test_ssh_credentials_view_creates_key(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
|
||||
@@ -352,6 +374,57 @@ class ViewTests(TestCase):
|
||||
self.assertEqual(host.rsync_extra_args, ["--numeric-ids"])
|
||||
self.assertEqual(host.retention_weekly, 4)
|
||||
|
||||
def test_create_host_config_uses_global_defaults_and_prepares_directories(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
credential = SshCredential.objects.create(name="global-key", private_key="PRIVATE KEY")
|
||||
with TemporaryDirectory() as tmp:
|
||||
backup_root = Path(tmp) / "backups"
|
||||
GlobalConfig.objects.create(
|
||||
name="default",
|
||||
backup_root=str(backup_root),
|
||||
default_ssh_credential=credential,
|
||||
ssh_user="backup",
|
||||
ssh_port=2222,
|
||||
default_source_root="/srv",
|
||||
retention_daily=3,
|
||||
retention_weekly=2,
|
||||
retention_monthly=1,
|
||||
retention_yearly=0,
|
||||
)
|
||||
|
||||
get_response = self.client.get(reverse("create_host_config"))
|
||||
self.assertContains(get_response, 'value="backup"')
|
||||
self.assertContains(get_response, 'value="2222"')
|
||||
self.assertContains(get_response, 'value="/srv"')
|
||||
|
||||
response = self.client.post(
|
||||
reverse("create_host_config"),
|
||||
{
|
||||
"host": "web-01",
|
||||
"address": "web-01.example.test",
|
||||
"enabled": "on",
|
||||
"ssh_credential": str(credential.id),
|
||||
"ssh_user": "backup",
|
||||
"ssh_port": "2222",
|
||||
"source_root": "/srv",
|
||||
"includes": "",
|
||||
"excludes_add": "",
|
||||
"excludes_replace": "",
|
||||
"rsync_extra_args": "",
|
||||
"retention_daily": "3",
|
||||
"retention_weekly": "2",
|
||||
"retention_monthly": "1",
|
||||
"retention_yearly": "0",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse("host_detail", args=["web-01"]))
|
||||
self.assertContains(response, "prepared")
|
||||
self.assertTrue((backup_root / "web-01" / "scheduled").is_dir())
|
||||
self.assertTrue((backup_root / "web-01" / "manual").is_dir())
|
||||
self.assertTrue((backup_root / "web-01" / ".incomplete").is_dir())
|
||||
|
||||
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")
|
||||
@@ -378,6 +451,7 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "Backup Control")
|
||||
self.assertContains(response, "Queue dry-run")
|
||||
self.assertContains(response, "Queue backup")
|
||||
self.assertContains(response, "Host Check")
|
||||
self.assertContains(response, "ready")
|
||||
self.assertContains(response, "Snapshot Discovery")
|
||||
self.assertContains(response, reverse("queue_manual_backup", args=[host.host]))
|
||||
|
||||
Reference in New Issue
Block a user