(feature) Add global and effective host config checks
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.
This commit is contained in:
201
src/pobsync_backend/config_checks.py
Normal file
201
src/pobsync_backend/config_checks.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from .models import GlobalConfig, HostConfig, SshCredential
|
||||||
|
from .self_check import SelfCheck
|
||||||
|
from .ssh_keys import identity_path
|
||||||
|
|
||||||
|
|
||||||
|
CRITICAL_ROOT_EXCLUDES = ("/proc/***", "/sys/***", "/dev/***", "/run/***", "/tmp/***")
|
||||||
|
|
||||||
|
|
||||||
|
def collect_global_config_checks(global_config: GlobalConfig) -> list[SelfCheck]:
|
||||||
|
checks = [
|
||||||
|
_absolute_path_check("Global backup root", global_config.backup_root),
|
||||||
|
_absolute_path_check("Global pobsync home", global_config.pobsync_home),
|
||||||
|
_runtime_backup_root_check(global_config),
|
||||||
|
_rsync_binary_check(global_config.rsync_binary),
|
||||||
|
_rsync_recursion_check(
|
||||||
|
"Global rsync recursion",
|
||||||
|
[*list(global_config.rsync_args or []), *list(global_config.rsync_extra_args or [])],
|
||||||
|
),
|
||||||
|
_source_root_check("Global source root", global_config.default_source_root),
|
||||||
|
_root_excludes_check(global_config.default_source_root, list(global_config.excludes_default or [])),
|
||||||
|
_retention_check(
|
||||||
|
"Global retention",
|
||||||
|
global_config.retention_daily,
|
||||||
|
global_config.retention_weekly,
|
||||||
|
global_config.retention_monthly,
|
||||||
|
global_config.retention_yearly,
|
||||||
|
),
|
||||||
|
_ssh_port_check("Global SSH port", global_config.ssh_port),
|
||||||
|
_credential_check("Global SSH credential", global_config.default_ssh_credential),
|
||||||
|
]
|
||||||
|
return checks
|
||||||
|
|
||||||
|
|
||||||
|
def collect_effective_host_config_checks(host: HostConfig, global_config: GlobalConfig) -> list[SelfCheck]:
|
||||||
|
source_root = host.source_root or global_config.default_source_root
|
||||||
|
ssh_user = host.ssh_user or global_config.ssh_user
|
||||||
|
ssh_port = host.ssh_port or global_config.ssh_port
|
||||||
|
credential = host.ssh_credential or global_config.default_ssh_credential
|
||||||
|
if host.excludes_replace is not None:
|
||||||
|
excludes = list(host.excludes_replace)
|
||||||
|
else:
|
||||||
|
excludes = [*list(global_config.excludes_default or []), *list(host.excludes_add or [])]
|
||||||
|
rsync_args = [
|
||||||
|
*list(global_config.rsync_args or []),
|
||||||
|
*list(global_config.rsync_extra_args or []),
|
||||||
|
*list(host.rsync_extra_args or []),
|
||||||
|
]
|
||||||
|
|
||||||
|
checks = [
|
||||||
|
_source_root_check("Host effective source root", source_root),
|
||||||
|
_ssh_user_check(ssh_user),
|
||||||
|
_ssh_port_check("Host effective SSH port", ssh_port),
|
||||||
|
_credential_check("Host effective SSH credential", credential),
|
||||||
|
_rsync_recursion_check("Host effective rsync recursion", rsync_args),
|
||||||
|
_root_excludes_check(source_root, excludes, host=host),
|
||||||
|
_includes_check(host),
|
||||||
|
_retention_check(
|
||||||
|
"Host retention",
|
||||||
|
host.retention_daily,
|
||||||
|
host.retention_weekly,
|
||||||
|
host.retention_monthly,
|
||||||
|
host.retention_yearly,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
return checks
|
||||||
|
|
||||||
|
|
||||||
|
def has_recursive_rsync_arg(args: list[str]) -> bool:
|
||||||
|
for arg in args:
|
||||||
|
if arg in {"--archive", "--recursive"}:
|
||||||
|
return True
|
||||||
|
if arg.startswith("-") and not arg.startswith("--") and any(flag in arg for flag in ("a", "r")):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _absolute_path_check(name: str, value: str) -> SelfCheck:
|
||||||
|
path = Path(value)
|
||||||
|
if not value:
|
||||||
|
return SelfCheck(name, "failed", "Path is empty.")
|
||||||
|
if not path.is_absolute():
|
||||||
|
return SelfCheck(name, "failed", f"{value} is not absolute.")
|
||||||
|
return SelfCheck(name, "ok", value)
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_backup_root_check(global_config: GlobalConfig) -> SelfCheck:
|
||||||
|
if global_config.backup_root == settings.POBSYNC_BACKUP_ROOT:
|
||||||
|
return SelfCheck("Runtime backup root", "ok", global_config.backup_root)
|
||||||
|
return SelfCheck(
|
||||||
|
"Runtime backup root",
|
||||||
|
"warning",
|
||||||
|
"Database backup root differs from runtime POBSYNC_BACKUP_ROOT.",
|
||||||
|
f"database={global_config.backup_root} runtime={settings.POBSYNC_BACKUP_ROOT}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rsync_binary_check(binary: str) -> SelfCheck:
|
||||||
|
if not binary:
|
||||||
|
return SelfCheck("Global rsync binary", "failed", "Rsync binary is empty.")
|
||||||
|
if Path(binary).is_absolute():
|
||||||
|
exists = Path(binary).exists()
|
||||||
|
message = binary if exists else f"{binary} does not exist."
|
||||||
|
return SelfCheck("Global rsync binary", "ok" if exists else "failed", message)
|
||||||
|
path = shutil.which(binary)
|
||||||
|
return SelfCheck("Global rsync binary", "ok" if path else "failed", path or f"{binary} was not found in PATH.")
|
||||||
|
|
||||||
|
|
||||||
|
def _rsync_recursion_check(name: str, args: list[str]) -> SelfCheck:
|
||||||
|
if has_recursive_rsync_arg(args):
|
||||||
|
return SelfCheck(name, "ok", "Rsync args include archive or recursive transfer.", " ".join(args))
|
||||||
|
return SelfCheck(
|
||||||
|
name,
|
||||||
|
"failed",
|
||||||
|
"Rsync args do not include archive or recursive transfer.",
|
||||||
|
"Add --archive or --recursive before running a real backup.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _source_root_check(name: str, source_root: str) -> SelfCheck:
|
||||||
|
if not source_root:
|
||||||
|
return SelfCheck(name, "failed", "Source root is empty.")
|
||||||
|
if not source_root.startswith("/"):
|
||||||
|
return SelfCheck(name, "failed", f"{source_root} is not absolute.")
|
||||||
|
return SelfCheck(name, "ok", source_root)
|
||||||
|
|
||||||
|
|
||||||
|
def _root_excludes_check(source_root: str, excludes: list[str], host: HostConfig | None = None) -> SelfCheck:
|
||||||
|
if source_root != "/":
|
||||||
|
return SelfCheck("Effective root excludes", "ok", "Source root is not /, critical OS excludes are less important.")
|
||||||
|
missing = [pattern for pattern in CRITICAL_ROOT_EXCLUDES if pattern not in excludes]
|
||||||
|
if missing:
|
||||||
|
detail = ", ".join(missing)
|
||||||
|
if host and host.excludes_replace is not None:
|
||||||
|
detail = f"excludes_replace is active; missing {detail}"
|
||||||
|
return SelfCheck(
|
||||||
|
"Effective root excludes",
|
||||||
|
"warning",
|
||||||
|
"Source root is / but some critical default excludes are missing.",
|
||||||
|
detail,
|
||||||
|
)
|
||||||
|
return SelfCheck("Effective root excludes", "ok", "Critical root excludes are present.")
|
||||||
|
|
||||||
|
|
||||||
|
def _includes_check(host: HostConfig) -> SelfCheck:
|
||||||
|
includes = list(host.includes or [])
|
||||||
|
if not includes:
|
||||||
|
return SelfCheck("Host includes", "ok", "No host include rules are configured.")
|
||||||
|
return SelfCheck(
|
||||||
|
"Host includes",
|
||||||
|
"warning",
|
||||||
|
"Includes are passed to rsync as raw --include rules.",
|
||||||
|
"Verify matching exclude rules if you intend to limit the backup scope.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _retention_check(name: str, daily: int, weekly: int, monthly: int, yearly: int) -> SelfCheck:
|
||||||
|
if any(value > 0 for value in (daily, weekly, monthly, yearly)):
|
||||||
|
return SelfCheck(name, "ok", f"d{daily} w{weekly} m{monthly} y{yearly}")
|
||||||
|
return SelfCheck(name, "warning", "All retention windows are zero.")
|
||||||
|
|
||||||
|
|
||||||
|
def _ssh_user_check(user: str) -> SelfCheck:
|
||||||
|
if user.strip():
|
||||||
|
return SelfCheck("Host effective SSH user", "ok", user.strip())
|
||||||
|
return SelfCheck("Host effective SSH user", "failed", "SSH user is empty.")
|
||||||
|
|
||||||
|
|
||||||
|
def _ssh_port_check(name: str, port: int | None) -> SelfCheck:
|
||||||
|
if port is None:
|
||||||
|
return SelfCheck(name, "failed", "SSH port is empty.")
|
||||||
|
if 1 <= int(port) <= 65535:
|
||||||
|
return SelfCheck(name, "ok", str(port))
|
||||||
|
return SelfCheck(name, "failed", f"{port} is outside the valid TCP port range.")
|
||||||
|
|
||||||
|
|
||||||
|
def _credential_check(name: str, credential: SshCredential | None) -> SelfCheck:
|
||||||
|
if credential is None:
|
||||||
|
return SelfCheck(name, "warning", "No SSH credential selected.")
|
||||||
|
if credential.key_path:
|
||||||
|
key_path = identity_path(credential)
|
||||||
|
if not key_path.exists():
|
||||||
|
return SelfCheck(name, "failed", f"{key_path} does not exist.")
|
||||||
|
if not os.access(key_path, os.R_OK):
|
||||||
|
return SelfCheck(name, "failed", f"{key_path} is not readable by this process.")
|
||||||
|
return SelfCheck(name, "ok", str(credential), str(key_path))
|
||||||
|
if credential.private_key:
|
||||||
|
return SelfCheck(
|
||||||
|
name,
|
||||||
|
"warning",
|
||||||
|
f"{credential} stores private key material in the database.",
|
||||||
|
"Generated filesystem keys are recommended for native installs.",
|
||||||
|
)
|
||||||
|
return SelfCheck(name, "failed", f"{credential} has no private key material or key path.")
|
||||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from pobsync.snapshot_meta import resolve_host_root
|
from pobsync.snapshot_meta import resolve_host_root
|
||||||
|
|
||||||
|
from .config_checks import collect_effective_host_config_checks
|
||||||
from .models import GlobalConfig, HostConfig
|
from .models import GlobalConfig, HostConfig
|
||||||
from .self_check import SelfCheck
|
from .self_check import SelfCheck
|
||||||
from .ssh_keys import identity_path
|
from .ssh_keys import identity_path
|
||||||
@@ -78,31 +79,10 @@ def collect_host_checks(host: HostConfig, global_config: GlobalConfig | None = N
|
|||||||
checks.append(_host_path_check("Host backup root", host_root, must_exist=True, must_be_writable=True))
|
checks.append(_host_path_check("Host backup root", host_root, must_exist=True, must_be_writable=True))
|
||||||
for subdir in HOST_BACKUP_SUBDIRS:
|
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.append(_host_path_check(f"Host directory: {subdir}", host_root / subdir, must_exist=True, must_be_writable=True))
|
||||||
checks.append(_rsync_recursion_check(host, global_config))
|
checks.extend(collect_effective_host_config_checks(host, global_config))
|
||||||
return checks
|
return checks
|
||||||
|
|
||||||
|
|
||||||
def _rsync_recursion_check(host: HostConfig, global_config: GlobalConfig) -> SelfCheck:
|
|
||||||
args = [*list(global_config.rsync_args or []), *list(global_config.rsync_extra_args or []), *list(host.rsync_extra_args or [])]
|
|
||||||
if _has_recursive_rsync_arg(args):
|
|
||||||
return SelfCheck("Host rsync recursion", "ok", "Rsync args include archive or recursive transfer.", " ".join(args))
|
|
||||||
return SelfCheck(
|
|
||||||
"Host rsync recursion",
|
|
||||||
"failed",
|
|
||||||
"Rsync args do not include archive or recursive transfer.",
|
|
||||||
"Add --archive or --recursive before running a real backup.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _has_recursive_rsync_arg(args: list[str]) -> bool:
|
|
||||||
for arg in args:
|
|
||||||
if arg in {"--archive", "--recursive"}:
|
|
||||||
return True
|
|
||||||
if arg.startswith("-") and not arg.startswith("--") and any(flag in arg for flag in ("a", "r")):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _host_path_check(
|
def _host_path_check(
|
||||||
name: str,
|
name: str,
|
||||||
path: Path,
|
path: Path,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<h2>{% if global_config %}Edit Global Config{% else %}Create Global Config{% endif %}</h2>
|
<h2>{% if global_config %}Edit Global Config{% else %}Create Global Config{% endif %}</h2>
|
||||||
<div class="stack spaced">
|
<div class="stack spaced">
|
||||||
<div><strong>Backup root:</strong> {{ backup_root }}</div>
|
<div><strong>Backup root:</strong> {{ backup_root }}</div>
|
||||||
<div class="muted">This is the fixed path inside the Docker containers. Change the host directory by changing the Docker mount.</div>
|
<div class="muted">This path comes from the runtime environment and is written back when the config is saved.</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" class="form-grid">
|
<form method="post" class="form-grid">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -33,4 +33,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{% if config_checks %}
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Config Check</h2>
|
||||||
|
<section class="grid" aria-label="Global config check summary">
|
||||||
|
<div class="metric"><div class="label">OK</div><div class="value">{{ config_check_summary.ok }}</div></div>
|
||||||
|
<div class="metric"><div class="label">Warnings</div><div class="value">{{ config_check_summary.warning }}</div></div>
|
||||||
|
<div class="metric"><div class="label">Failed</div><div class="value">{{ config_check_summary.failed }}</div></div>
|
||||||
|
<div class="metric"><div class="label">Skipped</div><div class="value">{{ config_check_summary.skipped }}</div></div>
|
||||||
|
</section>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Check</th>
|
||||||
|
<th>Message</th>
|
||||||
|
<th>Detail</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for check in config_checks %}
|
||||||
|
<tr>
|
||||||
|
<td><span class="status {{ check.status }}">{{ check.status }}</span></td>
|
||||||
|
<td>{{ check.name }}</td>
|
||||||
|
<td>{{ check.message }}</td>
|
||||||
|
<td class="muted">{{ check.detail }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -33,4 +33,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{% if config_checks %}
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Effective Config Check</h2>
|
||||||
|
<section class="grid" aria-label="Host config check summary">
|
||||||
|
<div class="metric"><div class="label">OK</div><div class="value">{{ config_check_summary.ok }}</div></div>
|
||||||
|
<div class="metric"><div class="label">Warnings</div><div class="value">{{ config_check_summary.warning }}</div></div>
|
||||||
|
<div class="metric"><div class="label">Failed</div><div class="value">{{ config_check_summary.failed }}</div></div>
|
||||||
|
<div class="metric"><div class="label">Skipped</div><div class="value">{{ config_check_summary.skipped }}</div></div>
|
||||||
|
</section>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Check</th>
|
||||||
|
<th>Message</th>
|
||||||
|
<th>Detail</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for check in config_checks %}
|
||||||
|
<tr>
|
||||||
|
<td><span class="status {{ check.status }}">{{ check.status }}</span></td>
|
||||||
|
<td>{{ check.name }}</td>
|
||||||
|
<td>{{ check.message }}</td>
|
||||||
|
<td class="muted">{{ check.detail }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -347,10 +347,21 @@ class ViewTests(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "Backup root:")
|
self.assertContains(response, "Backup root:")
|
||||||
self.assertContains(response, "/backups")
|
self.assertContains(response, "/backups")
|
||||||
self.assertNotContains(response, "/mnt/pobsync/backups")
|
self.assertContains(response, "Config Check")
|
||||||
|
self.assertContains(response, "Runtime backup root")
|
||||||
self.assertNotContains(response, "/opt/pobsync/backups")
|
self.assertNotContains(response, "/opt/pobsync/backups")
|
||||||
self.assertNotContains(response, "Pobsync home")
|
self.assertNotContains(response, "Pobsync home")
|
||||||
|
|
||||||
|
def test_global_config_form_renders_config_check_for_non_recursive_rsync(self) -> None:
|
||||||
|
self.client.force_login(self.staff_user)
|
||||||
|
GlobalConfig.objects.create(name="default", backup_root="/backups", rsync_args=["--numeric-ids"])
|
||||||
|
|
||||||
|
response = self.client.get(reverse("edit_global_config"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Global rsync recursion")
|
||||||
|
self.assertContains(response, "Rsync args do not include archive or recursive transfer.")
|
||||||
|
|
||||||
def test_global_config_form_resets_backup_root_to_static_container_path(self) -> None:
|
def test_global_config_form_resets_backup_root_to_static_container_path(self) -> None:
|
||||||
self.client.force_login(self.staff_user)
|
self.client.force_login(self.staff_user)
|
||||||
GlobalConfig.objects.create(
|
GlobalConfig.objects.create(
|
||||||
@@ -532,9 +543,44 @@ class ViewTests(TestCase):
|
|||||||
response = self.client.get(reverse("host_detail", args=[host.host]))
|
response = self.client.get(reverse("host_detail", args=[host.host]))
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "Host rsync recursion")
|
self.assertContains(response, "Host effective rsync recursion")
|
||||||
self.assertContains(response, "Rsync args do not include archive or recursive transfer.")
|
self.assertContains(response, "Rsync args do not include archive or recursive transfer.")
|
||||||
|
|
||||||
|
def test_host_detail_warns_when_replace_excludes_drops_root_defaults(self) -> None:
|
||||||
|
self.client.force_login(self.staff_user)
|
||||||
|
GlobalConfig.objects.create(
|
||||||
|
name="default",
|
||||||
|
backup_root="/backups",
|
||||||
|
rsync_args=["--archive"],
|
||||||
|
excludes_default=["/proc/***", "/sys/***", "/dev/***", "/run/***", "/tmp/***"],
|
||||||
|
)
|
||||||
|
host = HostConfig.objects.create(
|
||||||
|
host="web-01",
|
||||||
|
address="web-01.example.test",
|
||||||
|
excludes_replace=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("host_detail", args=[host.host]))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Effective root excludes")
|
||||||
|
self.assertContains(response, "excludes_replace is active")
|
||||||
|
|
||||||
|
def test_host_detail_warns_that_includes_are_raw_rsync_rules(self) -> None:
|
||||||
|
self.client.force_login(self.staff_user)
|
||||||
|
GlobalConfig.objects.create(name="default", backup_root="/backups", rsync_args=["--archive"])
|
||||||
|
host = HostConfig.objects.create(
|
||||||
|
host="web-01",
|
||||||
|
address="web-01.example.test",
|
||||||
|
includes=["/srv/www"],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("host_detail", args=[host.host]))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Host includes")
|
||||||
|
self.assertContains(response, "Includes are passed to rsync as raw --include rules.")
|
||||||
|
|
||||||
def test_scan_host_known_key_action_updates_selected_credential(self) -> None:
|
def test_scan_host_known_key_action_updates_selected_credential(self) -> None:
|
||||||
self.client.force_login(self.staff_user)
|
self.client.force_login(self.staff_user)
|
||||||
credential = SshCredential.objects.create(name="default-key", key_path="/var/lib/pobsync/state/ssh-credentials/1/identity")
|
credential = SshCredential.objects.create(name="default-key", key_path="/var/lib/pobsync/state/ssh-credentials/1/identity")
|
||||||
@@ -997,6 +1043,18 @@ class ViewTests(TestCase):
|
|||||||
self.assertContains(response, "*.tmp")
|
self.assertContains(response, "*.tmp")
|
||||||
self.assertContains(response, "--numeric-ids")
|
self.assertContains(response, "--numeric-ids")
|
||||||
|
|
||||||
|
def test_host_config_form_renders_effective_config_check(self) -> None:
|
||||||
|
self.client.force_login(self.staff_user)
|
||||||
|
GlobalConfig.objects.create(name="default", backup_root="/backups", rsync_args=["--numeric-ids"])
|
||||||
|
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||||
|
|
||||||
|
response = self.client.get(reverse("edit_host_config", args=[host.host]))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Effective Config Check")
|
||||||
|
self.assertContains(response, "Host effective rsync recursion")
|
||||||
|
self.assertContains(response, "Rsync args do not include archive or recursive transfer.")
|
||||||
|
|
||||||
def test_host_config_form_updates_host_config(self) -> None:
|
def test_host_config_form_updates_host_config(self) -> None:
|
||||||
self.client.force_login(self.staff_user)
|
self.client.force_login(self.staff_user)
|
||||||
host = HostConfig.objects.create(host="web-01", address="old.example.test")
|
host = HostConfig.objects.create(host="web-01", address="old.example.test")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from pobsync.errors import PobsyncError
|
|||||||
from pobsync.config.defaults import DEFAULT_EXCLUDES, DEFAULT_RSYNC_ARGS
|
from pobsync.config.defaults import DEFAULT_EXCLUDES, DEFAULT_RSYNC_ARGS
|
||||||
|
|
||||||
from .backup_runner import queue_backup_run
|
from .backup_runner import queue_backup_run
|
||||||
|
from .config_checks import collect_effective_host_config_checks, collect_global_config_checks
|
||||||
from .forms import (
|
from .forms import (
|
||||||
CreateHostConfigForm,
|
CreateHostConfigForm,
|
||||||
GlobalConfigForm,
|
GlobalConfigForm,
|
||||||
@@ -203,6 +204,7 @@ def edit_global_config(request):
|
|||||||
return redirect("dashboard")
|
return redirect("dashboard")
|
||||||
else:
|
else:
|
||||||
form = GlobalConfigForm(instance=global_config) if global_config else GlobalConfigForm(initial=_default_global_initial())
|
form = GlobalConfigForm(instance=global_config) if global_config else GlobalConfigForm(initial=_default_global_initial())
|
||||||
|
config_checks = collect_global_config_checks(global_config) if global_config else []
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -211,6 +213,8 @@ def edit_global_config(request):
|
|||||||
"global_config": global_config,
|
"global_config": global_config,
|
||||||
"form": form,
|
"form": form,
|
||||||
"backup_root": settings.POBSYNC_BACKUP_ROOT,
|
"backup_root": settings.POBSYNC_BACKUP_ROOT,
|
||||||
|
"config_checks": config_checks,
|
||||||
|
"config_check_summary": summarize_self_checks(config_checks),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -452,6 +456,8 @@ def apply_host_retention(request, host: str):
|
|||||||
@staff_member_required
|
@staff_member_required
|
||||||
def edit_host_config(request, host: str):
|
def edit_host_config(request, host: str):
|
||||||
host_config = get_object_or_404(HostConfig, host=host)
|
host_config = get_object_or_404(HostConfig, host=host)
|
||||||
|
global_config = GlobalConfig.objects.filter(name="default").first()
|
||||||
|
config_checks = collect_effective_host_config_checks(host_config, global_config) if global_config else []
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = HostConfigForm(request.POST, instance=host_config)
|
form = HostConfigForm(request.POST, instance=host_config)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
@@ -467,6 +473,8 @@ def edit_host_config(request, host: str):
|
|||||||
{
|
{
|
||||||
"host": host_config,
|
"host": host_config,
|
||||||
"form": form,
|
"form": form,
|
||||||
|
"config_checks": config_checks,
|
||||||
|
"config_check_summary": summarize_self_checks(config_checks),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user