(refactor) Normalize runtime config labels
Hide the old pobsync_home field from the Django admin and replace legacy operator-facing labels with runtime state root and backup root terminology. Rename admin compatibility fieldsets, update self-check/config-check text, and refresh management command help so Django/systemd stays the primary mental model.
This commit is contained in:
@@ -196,8 +196,8 @@ rsync.
|
|||||||
## SSH Keys
|
## SSH Keys
|
||||||
|
|
||||||
SSH keys can be managed from `/ssh-credentials/`. The recommended flow is to generate keys from Django or during the
|
SSH keys can be managed from `/ssh-credentials/`. The recommended flow is to generate keys from Django or during the
|
||||||
installer. pobsync stores the private key on disk under `POBSYNC_HOME`, keeps the public key visible in the UI, and lets
|
installer. pobsync stores the private key on disk under the runtime state root (`POBSYNC_HOME`), keeps the public key
|
||||||
you select a credential either as the global default or as a per-host override.
|
visible in the UI, and lets you select a credential either as the global default or as a per-host override.
|
||||||
|
|
||||||
Generated private keys are stored at:
|
Generated private keys are stored at:
|
||||||
|
|
||||||
|
|||||||
@@ -98,13 +98,13 @@ run the Django/runtime refresh steps needed after a code update.
|
|||||||
|
|
||||||
## Migration Helpers
|
## Migration Helpers
|
||||||
|
|
||||||
Import existing legacy YAML configs:
|
Import pre-Django YAML configs during a one-time migration:
|
||||||
|
|
||||||
```
|
```
|
||||||
python3 manage.py import_pobsync_configs --prefix /opt/pobsync
|
python3 manage.py import_pobsync_configs --prefix /opt/pobsync
|
||||||
```
|
```
|
||||||
|
|
||||||
Export SQL config to legacy runtime YAML for inspection or one-off compatibility:
|
Export SQL config back to YAML for inspection or one-off compatibility:
|
||||||
|
|
||||||
```
|
```
|
||||||
python3 manage.py export_pobsync_configs --prefix /opt/pobsync
|
python3 manage.py export_pobsync_configs --prefix /opt/pobsync
|
||||||
@@ -192,4 +192,4 @@ Next refactor targets:
|
|||||||
|
|
||||||
- Move more snapshot lifecycle details into typed domain objects.
|
- Move more snapshot lifecycle details into typed domain objects.
|
||||||
- Replace remaining dictionary-shaped config at engine boundaries.
|
- Replace remaining dictionary-shaped config at engine boundaries.
|
||||||
- Remove legacy YAML import/export once production migration no longer needs it.
|
- Remove YAML migration import/export once production migration no longer needs it.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class GlobalConfigAdmin(admin.ModelAdmin):
|
|||||||
list_display = ("name", "backup_root", "ssh_user", "ssh_port", "updated_at")
|
list_display = ("name", "backup_root", "ssh_user", "ssh_port", "updated_at")
|
||||||
readonly_fields = ("created_at", "updated_at")
|
readonly_fields = ("created_at", "updated_at")
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("name", "backup_root", "pobsync_home")}),
|
(None, {"fields": ("name", "backup_root")}),
|
||||||
("SSH", {"fields": ("default_ssh_credential", "ssh_user", "ssh_port", "ssh_options")}),
|
("SSH", {"fields": ("default_ssh_credential", "ssh_user", "ssh_port", "ssh_options")}),
|
||||||
(
|
(
|
||||||
"Rsync",
|
"Rsync",
|
||||||
@@ -50,7 +50,7 @@ class GlobalConfigAdmin(admin.ModelAdmin):
|
|||||||
),
|
),
|
||||||
("Defaults", {"fields": ("default_source_root", "default_destination_subdir", "excludes_default")}),
|
("Defaults", {"fields": ("default_source_root", "default_destination_subdir", "excludes_default")}),
|
||||||
("Retention defaults", {"fields": ("retention_daily", "retention_weekly", "retention_monthly", "retention_yearly")}),
|
("Retention defaults", {"fields": ("retention_daily", "retention_weekly", "retention_monthly", "retention_yearly")}),
|
||||||
("Legacy JSON", {"fields": ("data",), "classes": ("collapse",)}),
|
("Compatibility data", {"fields": ("data",), "classes": ("collapse",)}),
|
||||||
("Timestamps", {"fields": ("created_at", "updated_at"), "classes": ("collapse",)}),
|
("Timestamps", {"fields": ("created_at", "updated_at"), "classes": ("collapse",)}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ class HostConfigAdmin(admin.ModelAdmin):
|
|||||||
("Source", {"fields": ("source_root", "includes", "excludes_add", "excludes_replace")}),
|
("Source", {"fields": ("source_root", "includes", "excludes_add", "excludes_replace")}),
|
||||||
("Rsync override", {"fields": ("rsync_extra_args",)}),
|
("Rsync override", {"fields": ("rsync_extra_args",)}),
|
||||||
("Retention", {"fields": ("retention_daily", "retention_weekly", "retention_monthly", "retention_yearly")}),
|
("Retention", {"fields": ("retention_daily", "retention_weekly", "retention_monthly", "retention_yearly")}),
|
||||||
("Legacy JSON", {"fields": ("config",), "classes": ("collapse",)}),
|
("Compatibility data", {"fields": ("config",), "classes": ("collapse",)}),
|
||||||
("Timestamps", {"fields": ("created_at", "updated_at"), "classes": ("collapse",)}),
|
("Timestamps", {"fields": ("created_at", "updated_at"), "classes": ("collapse",)}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ CRITICAL_ROOT_EXCLUDES = ("/proc/***", "/sys/***", "/dev/***", "/run/***", "/tmp
|
|||||||
def collect_global_config_checks(global_config: GlobalConfig) -> list[SelfCheck]:
|
def collect_global_config_checks(global_config: GlobalConfig) -> list[SelfCheck]:
|
||||||
checks = [
|
checks = [
|
||||||
_absolute_path_check("Global backup root", global_config.backup_root),
|
_absolute_path_check("Global backup root", global_config.backup_root),
|
||||||
_absolute_path_check("Global pobsync home", global_config.pobsync_home),
|
_absolute_path_check("Runtime state root", settings.POBSYNC_HOME),
|
||||||
_runtime_backup_root_check(global_config),
|
_runtime_backup_root_check(global_config),
|
||||||
_rsync_binary_check(global_config.rsync_binary),
|
_rsync_binary_check(global_config.rsync_binary),
|
||||||
_rsync_recursion_check(
|
_rsync_recursion_check(
|
||||||
@@ -97,7 +97,7 @@ def _runtime_backup_root_check(global_config: GlobalConfig) -> SelfCheck:
|
|||||||
return SelfCheck(
|
return SelfCheck(
|
||||||
"Runtime backup root",
|
"Runtime backup root",
|
||||||
"warning",
|
"warning",
|
||||||
"Database backup root differs from runtime POBSYNC_BACKUP_ROOT.",
|
"Database backup root differs from the runtime backup root.",
|
||||||
f"database={global_config.backup_root} runtime={settings.POBSYNC_BACKUP_ROOT}",
|
f"database={global_config.backup_root} runtime={settings.POBSYNC_BACKUP_ROOT}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class Command(BaseCommand):
|
|||||||
def add_arguments(self, parser) -> None:
|
def add_arguments(self, parser) -> None:
|
||||||
parser.add_argument("--name", default="default")
|
parser.add_argument("--name", default="default")
|
||||||
parser.add_argument("--backup-root", required=True)
|
parser.add_argument("--backup-root", required=True)
|
||||||
parser.add_argument("--pobsync-home", default=settings.POBSYNC_HOME)
|
parser.add_argument("--pobsync-home", default=settings.POBSYNC_HOME, help="Runtime state root")
|
||||||
parser.add_argument("--ssh-user", default="root")
|
parser.add_argument("--ssh-user", default="root")
|
||||||
parser.add_argument("--ssh-port", type=int, default=22)
|
parser.add_argument("--ssh-port", type=int, default=22)
|
||||||
parser.add_argument("--source-root", default="/")
|
parser.add_argument("--source-root", default="/")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class Command(BaseCommand):
|
|||||||
help = "Export Django database configs to pobsync runtime YAML files."
|
help = "Export Django database configs to pobsync runtime YAML files."
|
||||||
|
|
||||||
def add_arguments(self, parser) -> None:
|
def add_arguments(self, parser) -> None:
|
||||||
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Pobsync home directory")
|
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Runtime state root")
|
||||||
parser.add_argument("--host", default=None, help="Export only one enabled host")
|
parser.add_argument("--host", default=None, help="Export only one enabled host")
|
||||||
|
|
||||||
def handle(self, *args: Any, **options: Any) -> None:
|
def handle(self, *args: Any, **options: Any) -> None:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class Command(BaseCommand):
|
|||||||
help = "Import pobsync YAML configs into the Django database."
|
help = "Import pobsync YAML configs into the Django database."
|
||||||
|
|
||||||
def add_arguments(self, parser) -> None:
|
def add_arguments(self, parser) -> None:
|
||||||
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Pobsync home directory")
|
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Runtime state root")
|
||||||
|
|
||||||
def handle(self, *args: Any, **options: Any) -> None:
|
def handle(self, *args: Any, **options: Any) -> None:
|
||||||
paths = PobsyncPaths(home=Path(options["prefix"]))
|
paths = PobsyncPaths(home=Path(options["prefix"]))
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def add_arguments(self, parser) -> None:
|
def add_arguments(self, parser) -> None:
|
||||||
parser.add_argument("host", help="Host to back up")
|
parser.add_argument("host", help="Host to back up")
|
||||||
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Pobsync home directory")
|
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Runtime state root")
|
||||||
parser.add_argument("--dry-run", action="store_true", help="Run rsync --dry-run")
|
parser.add_argument("--dry-run", action="store_true", help="Run rsync --dry-run")
|
||||||
parser.add_argument("--verbose-rsync", action="store_true", help="Write itemized rsync output to the run log")
|
parser.add_argument("--verbose-rsync", action="store_true", help="Write itemized rsync output to the run log")
|
||||||
parser.add_argument("--prune", action="store_true", help="Apply retention after a successful run")
|
parser.add_argument("--prune", action="store_true", help="Apply retention after a successful run")
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def add_arguments(self, parser) -> None:
|
def add_arguments(self, parser) -> None:
|
||||||
parser.add_argument("host")
|
parser.add_argument("host")
|
||||||
parser.add_argument("--prefix", default=settings.POBSYNC_HOME)
|
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Runtime state root")
|
||||||
parser.add_argument("--kind", default="scheduled", choices=["scheduled", "manual", "all"])
|
parser.add_argument("--kind", default="scheduled", choices=["scheduled", "manual", "all"])
|
||||||
parser.add_argument("--protect-bases", action="store_true")
|
parser.add_argument("--protect-bases", action="store_true")
|
||||||
parser.add_argument("--apply", action="store_true")
|
parser.add_argument("--apply", action="store_true")
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class Command(BaseCommand):
|
|||||||
help = "Run due pobsync schedules from the Django database."
|
help = "Run due pobsync schedules from the Django database."
|
||||||
|
|
||||||
def add_arguments(self, parser) -> None:
|
def add_arguments(self, parser) -> None:
|
||||||
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Pobsync home directory")
|
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Runtime state root")
|
||||||
parser.add_argument("--once", action="store_true", help="Check once and exit")
|
parser.add_argument("--once", action="store_true", help="Check once and exit")
|
||||||
parser.add_argument("--loop", action="store_true", help="Keep checking schedules")
|
parser.add_argument("--loop", action="store_true", help="Keep checking schedules")
|
||||||
parser.add_argument("--interval", type=int, default=60, help="Loop interval in seconds")
|
parser.add_argument("--interval", type=int, default=60, help="Loop interval in seconds")
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class Command(BaseCommand):
|
|||||||
help = "Run queued pobsync backup jobs from the Django database."
|
help = "Run queued pobsync backup jobs from the Django database."
|
||||||
|
|
||||||
def add_arguments(self, parser) -> None:
|
def add_arguments(self, parser) -> None:
|
||||||
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Pobsync home directory")
|
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Runtime state root")
|
||||||
parser.add_argument("--once", action="store_true", help="Process one queued run and exit")
|
parser.add_argument("--once", action="store_true", help="Process one queued run and exit")
|
||||||
parser.add_argument("--loop", action="store_true", help="Keep checking for queued runs")
|
parser.add_argument("--loop", action="store_true", help="Keep checking for queued runs")
|
||||||
parser.add_argument("--interval", type=int, default=15, help="Loop interval in seconds")
|
parser.add_argument("--interval", type=int, default=15, help="Loop interval in seconds")
|
||||||
|
|||||||
@@ -76,10 +76,17 @@ def _django_checks() -> list[SelfCheck]:
|
|||||||
|
|
||||||
def _path_checks() -> list[SelfCheck]:
|
def _path_checks() -> list[SelfCheck]:
|
||||||
checks = []
|
checks = []
|
||||||
checks.append(_path_check("POBSYNC_HOME", Path(settings.POBSYNC_HOME), must_be_absolute=True, must_be_writable=True))
|
|
||||||
checks.append(
|
checks.append(
|
||||||
_path_check(
|
_path_check(
|
||||||
"POBSYNC_BACKUP_ROOT",
|
"State root",
|
||||||
|
Path(settings.POBSYNC_HOME),
|
||||||
|
must_be_absolute=True,
|
||||||
|
must_be_writable=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
checks.append(
|
||||||
|
_path_check(
|
||||||
|
"Backup root",
|
||||||
Path(settings.POBSYNC_BACKUP_ROOT),
|
Path(settings.POBSYNC_BACKUP_ROOT),
|
||||||
must_be_absolute=True,
|
must_be_absolute=True,
|
||||||
must_exist=True,
|
must_exist=True,
|
||||||
@@ -259,7 +266,7 @@ def _config_checks() -> list[SelfCheck]:
|
|||||||
message = "Default global config exists."
|
message = "Default global config exists."
|
||||||
if global_config.backup_root != settings.POBSYNC_BACKUP_ROOT:
|
if global_config.backup_root != settings.POBSYNC_BACKUP_ROOT:
|
||||||
status = "warning"
|
status = "warning"
|
||||||
message = "Global config backup root differs from runtime POBSYNC_BACKUP_ROOT."
|
message = "Global config backup root differs from the runtime backup root."
|
||||||
return [
|
return [
|
||||||
SelfCheck(
|
SelfCheck(
|
||||||
"Global config",
|
"Global config",
|
||||||
|
|||||||
@@ -5,11 +5,25 @@ from datetime import datetime, timezone
|
|||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from pobsync_backend.admin import BackupRunAdmin, HostConfigAdmin, SnapshotRecordAdmin
|
from pobsync_backend.admin import BackupRunAdmin, GlobalConfigAdmin, HostConfigAdmin, SnapshotRecordAdmin
|
||||||
from pobsync_backend.models import BackupRun, HostConfig, ScheduleConfig, SnapshotRecord
|
from pobsync_backend.models import BackupRun, GlobalConfig, HostConfig, ScheduleConfig, SnapshotRecord
|
||||||
|
|
||||||
|
|
||||||
class AdminDisplayTests(TestCase):
|
class AdminDisplayTests(TestCase):
|
||||||
|
def test_admin_hides_old_global_state_field_and_uses_compatibility_label(self) -> None:
|
||||||
|
site = AdminSite()
|
||||||
|
global_admin = GlobalConfigAdmin(GlobalConfig, site)
|
||||||
|
host_admin = HostConfigAdmin(HostConfig, site)
|
||||||
|
|
||||||
|
global_fieldsets = list(global_admin.fieldsets)
|
||||||
|
host_fieldsets = list(host_admin.fieldsets)
|
||||||
|
global_fields = [field for _name, options in global_fieldsets for field in options["fields"]]
|
||||||
|
fieldset_names = [name for name, _options in [*global_fieldsets, *host_fieldsets]]
|
||||||
|
|
||||||
|
self.assertNotIn("pobsync_home", global_fields)
|
||||||
|
self.assertIn("Compatibility data", fieldset_names)
|
||||||
|
self.assertNotIn("Legacy JSON", fieldset_names)
|
||||||
|
|
||||||
def test_host_admin_links_to_related_snapshots_and_runs(self) -> None:
|
def test_host_admin_links_to_related_snapshots_and_runs(self) -> None:
|
||||||
site = AdminSite()
|
site = AdminSite()
|
||||||
admin = HostConfigAdmin(HostConfig, site)
|
admin = HostConfigAdmin(HostConfig, site)
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ class ViewTests(TestCase):
|
|||||||
self.assertContains(response, "Self Check")
|
self.assertContains(response, "Self Check")
|
||||||
self.assertContains(response, "Django debug")
|
self.assertContains(response, "Django debug")
|
||||||
self.assertContains(response, "Database connection")
|
self.assertContains(response, "Database connection")
|
||||||
self.assertContains(response, "POBSYNC_HOME")
|
self.assertContains(response, "State root")
|
||||||
|
|
||||||
def test_logs_view_renders_filtered_journal_messages(self) -> None:
|
def test_logs_view_renders_filtered_journal_messages(self) -> None:
|
||||||
self.client.force_login(self.staff_user)
|
self.client.force_login(self.staff_user)
|
||||||
@@ -502,7 +502,7 @@ class ViewTests(TestCase):
|
|||||||
GlobalConfig.objects.create(
|
GlobalConfig.objects.create(
|
||||||
name="default",
|
name="default",
|
||||||
backup_root="/mnt/pobsync/backups",
|
backup_root="/mnt/pobsync/backups",
|
||||||
pobsync_home="/custom/legacy/home",
|
pobsync_home="/custom/state/home",
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.get(reverse("edit_global_config"))
|
response = self.client.get(reverse("edit_global_config"))
|
||||||
@@ -512,8 +512,10 @@ class ViewTests(TestCase):
|
|||||||
self.assertContains(response, "/backups")
|
self.assertContains(response, "/backups")
|
||||||
self.assertContains(response, "Config Check")
|
self.assertContains(response, "Config Check")
|
||||||
self.assertContains(response, "Runtime backup root")
|
self.assertContains(response, "Runtime backup root")
|
||||||
|
self.assertContains(response, "Runtime state root")
|
||||||
self.assertNotContains(response, "/opt/pobsync/backups")
|
self.assertNotContains(response, "/opt/pobsync/backups")
|
||||||
self.assertNotContains(response, "Pobsync home")
|
self.assertNotContains(response, "Pobsync home")
|
||||||
|
self.assertNotContains(response, "Global pobsync home")
|
||||||
|
|
||||||
def test_global_config_form_renders_config_check_for_non_recursive_rsync(self) -> None:
|
def test_global_config_form_renders_config_check_for_non_recursive_rsync(self) -> None:
|
||||||
self.client.force_login(self.staff_user)
|
self.client.force_login(self.staff_user)
|
||||||
@@ -530,7 +532,7 @@ class ViewTests(TestCase):
|
|||||||
GlobalConfig.objects.create(
|
GlobalConfig.objects.create(
|
||||||
name="default",
|
name="default",
|
||||||
backup_root="/mnt/pobsync/backups",
|
backup_root="/mnt/pobsync/backups",
|
||||||
pobsync_home="/custom/legacy/home",
|
pobsync_home="/custom/state/home",
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
|
|||||||
Reference in New Issue
Block a user