(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:
2026-05-19 19:11:57 +02:00
parent bb7907846e
commit 90f28410ce
10 changed files with 319 additions and 2 deletions

View File

@@ -0,0 +1,69 @@
from __future__ import annotations
import os
from pathlib import Path
from pobsync.snapshot_meta import resolve_host_root
from .models import GlobalConfig, HostConfig
from .self_check import SelfCheck
HOST_BACKUP_SUBDIRS = ("scheduled", "manual", ".incomplete")
def ensure_host_directories(host: HostConfig, global_config: GlobalConfig | None = None) -> Path:
global_config = global_config or GlobalConfig.objects.get(name="default")
host_root = resolve_host_root(global_config.backup_root, host.host)
for subdir in HOST_BACKUP_SUBDIRS:
(host_root / subdir).mkdir(parents=True, exist_ok=True)
return host_root
def collect_host_checks(host: HostConfig, global_config: GlobalConfig | None = None) -> list[SelfCheck]:
checks: list[SelfCheck] = []
try:
global_config = global_config or GlobalConfig.objects.get(name="default")
except GlobalConfig.DoesNotExist:
return [SelfCheck("Host global config", "failed", "Default global config does not exist.")]
checks.append(
SelfCheck(
"Host enabled",
"ok" if host.enabled else "warning",
"Host is enabled." if host.enabled else "Host is disabled.",
)
)
checks.append(
SelfCheck(
"Host address",
"ok" if host.address.strip() else "failed",
host.address.strip() or "Host address is empty.",
)
)
credential = host.ssh_credential or global_config.default_ssh_credential
checks.append(
SelfCheck(
"Host SSH credential",
"ok" if credential else "warning",
str(credential) if credential else "No host or global SSH credential selected.",
)
)
host_root = resolve_host_root(global_config.backup_root, host.host)
checks.append(_host_path_check("Host backup root", host_root, must_exist=True, must_be_writable=True))
for subdir in HOST_BACKUP_SUBDIRS:
checks.append(_host_path_check(f"Host directory: {subdir}", host_root / subdir, must_exist=True, must_be_writable=True))
return checks
def _host_path_check(name: str, path: Path, *, must_exist: bool, must_be_writable: bool) -> SelfCheck:
if must_exist and not path.exists():
return SelfCheck(name, "failed", f"{path} does not exist.")
target = path if path.exists() else path.parent
if not target.exists():
return SelfCheck(name, "failed", f"{target} does not exist.")
if must_be_writable and not os.access(target, os.W_OK):
return SelfCheck(name, "failed", f"{target} is not writable by this process.")
return SelfCheck(name, "ok", str(path))