Introduce reusable configuration checks for global settings and effective host runtime configuration. The checks now surface risky backup settings such as missing recursive rsync args, missing critical root excludes, invalid SSH settings, missing credentials, and retention gaps. Show these checks on the global config form, host edit form, and host detail page so operators can validate the compounded host/global config before starting real backup runs.
104 lines
4.1 KiB
Python
104 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from pobsync.snapshot_meta import resolve_host_root
|
|
|
|
from .config_checks import collect_effective_host_config_checks
|
|
from .models import GlobalConfig, HostConfig
|
|
from .self_check import SelfCheck
|
|
from .ssh_keys import identity_path
|
|
|
|
|
|
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
|
|
if credential is None:
|
|
checks.append(SelfCheck("Host SSH credential", "warning", "No host or global SSH credential selected."))
|
|
else:
|
|
checks.append(SelfCheck("Host SSH credential", "ok", str(credential)))
|
|
if credential.key_path:
|
|
key_path = identity_path(credential)
|
|
checks.append(
|
|
_host_path_check("Host SSH key file", key_path, must_exist=True, must_be_writable=False, must_be_readable=True)
|
|
)
|
|
elif credential.private_key:
|
|
checks.append(
|
|
SelfCheck(
|
|
"Host SSH key storage",
|
|
"warning",
|
|
"Selected credential stores private key material in the database.",
|
|
"Generated filesystem keys are recommended for native systemd installs.",
|
|
)
|
|
)
|
|
if credential.known_hosts.strip():
|
|
checks.append(SelfCheck("Host known_hosts", "ok", "Selected credential has known_hosts entries."))
|
|
else:
|
|
checks.append(
|
|
SelfCheck(
|
|
"Host known_hosts",
|
|
"warning",
|
|
"Selected credential has no pinned known_hosts entries.",
|
|
"pobsync will use service-level StrictHostKeyChecking=accept-new on first connect.",
|
|
)
|
|
)
|
|
|
|
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))
|
|
checks.extend(collect_effective_host_config_checks(host, global_config))
|
|
return checks
|
|
|
|
|
|
def _host_path_check(
|
|
name: str,
|
|
path: Path,
|
|
*,
|
|
must_exist: bool,
|
|
must_be_writable: bool,
|
|
must_be_readable: bool = False,
|
|
) -> 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.")
|
|
if must_be_readable and not os.access(target, os.R_OK):
|
|
return SelfCheck(name, "failed", f"{target} is not readable by this process.")
|
|
return SelfCheck(name, "ok", str(path))
|