From c5865a537947b7d5db1380e56389a088c57640e6 Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Thu, 21 May 2026 02:24:55 +0200 Subject: [PATCH] (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. --- README.md | 4 ++-- docs/development.md | 6 +++--- src/pobsync_backend/admin.py | 6 +++--- src/pobsync_backend/config_checks.py | 4 ++-- .../commands/configure_pobsync_global.py | 2 +- .../commands/export_pobsync_configs.py | 2 +- .../commands/import_pobsync_configs.py | 2 +- .../management/commands/run_pobsync_backup.py | 2 +- .../commands/run_pobsync_retention.py | 2 +- .../commands/run_pobsync_scheduler.py | 2 +- .../management/commands/run_pobsync_worker.py | 2 +- src/pobsync_backend/self_check.py | 13 ++++++++++--- src/pobsync_backend/tests/test_admin.py | 18 ++++++++++++++++-- src/pobsync_backend/tests/test_views.py | 8 +++++--- 14 files changed, 48 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 70a68eb..2a3416c 100644 --- a/README.md +++ b/README.md @@ -196,8 +196,8 @@ rsync. ## SSH Keys 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 -you select a credential either as the global default or as a per-host override. +installer. pobsync stores the private key on disk under the runtime state root (`POBSYNC_HOME`), keeps the public key +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: diff --git a/docs/development.md b/docs/development.md index 5b1ce63..a8dcfd7 100644 --- a/docs/development.md +++ b/docs/development.md @@ -98,13 +98,13 @@ run the Django/runtime refresh steps needed after a code update. ## 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 ``` -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 @@ -192,4 +192,4 @@ Next refactor targets: - Move more snapshot lifecycle details into typed domain objects. - 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. diff --git a/src/pobsync_backend/admin.py b/src/pobsync_backend/admin.py index d0afe15..708ae5a 100644 --- a/src/pobsync_backend/admin.py +++ b/src/pobsync_backend/admin.py @@ -34,7 +34,7 @@ class GlobalConfigAdmin(admin.ModelAdmin): list_display = ("name", "backup_root", "ssh_user", "ssh_port", "updated_at") readonly_fields = ("created_at", "updated_at") fieldsets = ( - (None, {"fields": ("name", "backup_root", "pobsync_home")}), + (None, {"fields": ("name", "backup_root")}), ("SSH", {"fields": ("default_ssh_credential", "ssh_user", "ssh_port", "ssh_options")}), ( "Rsync", @@ -50,7 +50,7 @@ class GlobalConfigAdmin(admin.ModelAdmin): ), ("Defaults", {"fields": ("default_source_root", "default_destination_subdir", "excludes_default")}), ("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",)}), ) @@ -76,7 +76,7 @@ class HostConfigAdmin(admin.ModelAdmin): ("Source", {"fields": ("source_root", "includes", "excludes_add", "excludes_replace")}), ("Rsync override", {"fields": ("rsync_extra_args",)}), ("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",)}), ) diff --git a/src/pobsync_backend/config_checks.py b/src/pobsync_backend/config_checks.py index 8fafc71..ea2ed9c 100644 --- a/src/pobsync_backend/config_checks.py +++ b/src/pobsync_backend/config_checks.py @@ -17,7 +17,7 @@ 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), + _absolute_path_check("Runtime state root", settings.POBSYNC_HOME), _runtime_backup_root_check(global_config), _rsync_binary_check(global_config.rsync_binary), _rsync_recursion_check( @@ -97,7 +97,7 @@ def _runtime_backup_root_check(global_config: GlobalConfig) -> SelfCheck: return SelfCheck( "Runtime backup root", "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}", ) diff --git a/src/pobsync_backend/management/commands/configure_pobsync_global.py b/src/pobsync_backend/management/commands/configure_pobsync_global.py index f46eb31..a7cc318 100644 --- a/src/pobsync_backend/management/commands/configure_pobsync_global.py +++ b/src/pobsync_backend/management/commands/configure_pobsync_global.py @@ -18,7 +18,7 @@ class Command(BaseCommand): def add_arguments(self, parser) -> None: parser.add_argument("--name", default="default") 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-port", type=int, default=22) parser.add_argument("--source-root", default="/") diff --git a/src/pobsync_backend/management/commands/export_pobsync_configs.py b/src/pobsync_backend/management/commands/export_pobsync_configs.py index 76317b2..78623bb 100644 --- a/src/pobsync_backend/management/commands/export_pobsync_configs.py +++ b/src/pobsync_backend/management/commands/export_pobsync_configs.py @@ -13,7 +13,7 @@ class Command(BaseCommand): help = "Export Django database configs to pobsync runtime YAML files." 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") def handle(self, *args: Any, **options: Any) -> None: diff --git a/src/pobsync_backend/management/commands/import_pobsync_configs.py b/src/pobsync_backend/management/commands/import_pobsync_configs.py index c64e8ce..a2de4b4 100644 --- a/src/pobsync_backend/management/commands/import_pobsync_configs.py +++ b/src/pobsync_backend/management/commands/import_pobsync_configs.py @@ -15,7 +15,7 @@ class Command(BaseCommand): help = "Import pobsync YAML configs into the Django database." 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: paths = PobsyncPaths(home=Path(options["prefix"])) diff --git a/src/pobsync_backend/management/commands/run_pobsync_backup.py b/src/pobsync_backend/management/commands/run_pobsync_backup.py index d2a9d7d..57e18c9 100644 --- a/src/pobsync_backend/management/commands/run_pobsync_backup.py +++ b/src/pobsync_backend/management/commands/run_pobsync_backup.py @@ -16,7 +16,7 @@ class Command(BaseCommand): def add_arguments(self, parser) -> None: 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("--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") diff --git a/src/pobsync_backend/management/commands/run_pobsync_retention.py b/src/pobsync_backend/management/commands/run_pobsync_retention.py index 63eca5b..5067e40 100644 --- a/src/pobsync_backend/management/commands/run_pobsync_retention.py +++ b/src/pobsync_backend/management/commands/run_pobsync_retention.py @@ -16,7 +16,7 @@ class Command(BaseCommand): def add_arguments(self, parser) -> None: 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("--protect-bases", action="store_true") parser.add_argument("--apply", action="store_true") diff --git a/src/pobsync_backend/management/commands/run_pobsync_scheduler.py b/src/pobsync_backend/management/commands/run_pobsync_scheduler.py index f4e7638..3f70763 100644 --- a/src/pobsync_backend/management/commands/run_pobsync_scheduler.py +++ b/src/pobsync_backend/management/commands/run_pobsync_scheduler.py @@ -18,7 +18,7 @@ class Command(BaseCommand): help = "Run due pobsync schedules from the Django database." 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("--loop", action="store_true", help="Keep checking schedules") parser.add_argument("--interval", type=int, default=60, help="Loop interval in seconds") diff --git a/src/pobsync_backend/management/commands/run_pobsync_worker.py b/src/pobsync_backend/management/commands/run_pobsync_worker.py index e1288c0..1b3b200 100644 --- a/src/pobsync_backend/management/commands/run_pobsync_worker.py +++ b/src/pobsync_backend/management/commands/run_pobsync_worker.py @@ -15,7 +15,7 @@ class Command(BaseCommand): help = "Run queued pobsync backup jobs from the Django database." 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("--loop", action="store_true", help="Keep checking for queued runs") parser.add_argument("--interval", type=int, default=15, help="Loop interval in seconds") diff --git a/src/pobsync_backend/self_check.py b/src/pobsync_backend/self_check.py index ed7a9ff..f8bde76 100644 --- a/src/pobsync_backend/self_check.py +++ b/src/pobsync_backend/self_check.py @@ -76,10 +76,17 @@ def _django_checks() -> list[SelfCheck]: def _path_checks() -> list[SelfCheck]: checks = [] - checks.append(_path_check("POBSYNC_HOME", Path(settings.POBSYNC_HOME), must_be_absolute=True, must_be_writable=True)) checks.append( _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), must_be_absolute=True, must_exist=True, @@ -259,7 +266,7 @@ def _config_checks() -> list[SelfCheck]: message = "Default global config exists." if global_config.backup_root != settings.POBSYNC_BACKUP_ROOT: 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 [ SelfCheck( "Global config", diff --git a/src/pobsync_backend/tests/test_admin.py b/src/pobsync_backend/tests/test_admin.py index 1263dbf..c87d707 100644 --- a/src/pobsync_backend/tests/test_admin.py +++ b/src/pobsync_backend/tests/test_admin.py @@ -5,11 +5,25 @@ from datetime import datetime, timezone from django.contrib.admin.sites import AdminSite from django.test import TestCase -from pobsync_backend.admin import BackupRunAdmin, HostConfigAdmin, SnapshotRecordAdmin -from pobsync_backend.models import BackupRun, HostConfig, ScheduleConfig, SnapshotRecord +from pobsync_backend.admin import BackupRunAdmin, GlobalConfigAdmin, HostConfigAdmin, SnapshotRecordAdmin +from pobsync_backend.models import BackupRun, GlobalConfig, HostConfig, ScheduleConfig, SnapshotRecord 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: site = AdminSite() admin = HostConfigAdmin(HostConfig, site) diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index 031d0ed..08c3360 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -237,7 +237,7 @@ class ViewTests(TestCase): self.assertContains(response, "Self Check") self.assertContains(response, "Django debug") 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: self.client.force_login(self.staff_user) @@ -502,7 +502,7 @@ class ViewTests(TestCase): GlobalConfig.objects.create( name="default", backup_root="/mnt/pobsync/backups", - pobsync_home="/custom/legacy/home", + pobsync_home="/custom/state/home", ) response = self.client.get(reverse("edit_global_config")) @@ -512,8 +512,10 @@ class ViewTests(TestCase): self.assertContains(response, "/backups") self.assertContains(response, "Config Check") self.assertContains(response, "Runtime backup root") + self.assertContains(response, "Runtime state root") self.assertNotContains(response, "/opt/pobsync/backups") 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: self.client.force_login(self.staff_user) @@ -530,7 +532,7 @@ class ViewTests(TestCase): GlobalConfig.objects.create( name="default", backup_root="/mnt/pobsync/backups", - pobsync_home="/custom/legacy/home", + pobsync_home="/custom/state/home", ) response = self.client.post(