From 29f455a15353e504ec633aa7efc203b194aa304b Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Thu, 28 May 2026 21:42:40 +0200 Subject: [PATCH] (bugfix) Enable rsync progress output for live real runs Default queued and management-command backups to verbose rsync output so live run views show progress for long-running real backups, matching dry-run visibility. Add a quiet-rsync escape hatch for operators who intentionally want less noisy real-run logs. --- src/pobsync_backend/backup_runner.py | 2 +- .../management/commands/run_pobsync_backup.py | 6 ++- .../pobsync_backend/host_detail.html | 1 + .../tests/test_backup_worker.py | 11 ++++- .../tests/test_run_backup_records_snapshot.py | 43 +++++++++++++++++++ src/pobsync_backend/tests/test_views.py | 6 +-- src/pobsync_backend/views.py | 1 + 7 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/pobsync_backend/backup_runner.py b/src/pobsync_backend/backup_runner.py index 0c2560c..f485993 100644 --- a/src/pobsync_backend/backup_runner.py +++ b/src/pobsync_backend/backup_runner.py @@ -27,7 +27,7 @@ def queue_backup_run( host: HostConfig, run_type: str = BackupRun.RunType.MANUAL, dry_run: bool = False, - verbose_output: bool = False, + verbose_output: bool = True, prune: bool = False, prune_max_delete: int = 10, prune_protect_bases: bool = False, diff --git a/src/pobsync_backend/management/commands/run_pobsync_backup.py b/src/pobsync_backend/management/commands/run_pobsync_backup.py index 874f7af..d457692 100644 --- a/src/pobsync_backend/management/commands/run_pobsync_backup.py +++ b/src/pobsync_backend/management/commands/run_pobsync_backup.py @@ -19,6 +19,7 @@ class Command(BaseCommand): 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("--quiet-rsync", action="store_true", help="Skip default rsync progress output for real runs") parser.add_argument("--prune", action="store_true", help="Apply retention after a successful run") parser.add_argument("--prune-max-delete", type=int, default=10) parser.add_argument("--prune-protect-bases", action="store_true") @@ -32,6 +33,7 @@ class Command(BaseCommand): except HostConfig.DoesNotExist as exc: raise CommandError(f"Missing enabled host {host_name!r}") from exc + verbose_output = bool(options["dry_run"] or options["verbose_rsync"] or not options["quiet_rsync"]) run = BackupRun.objects.create( host=host, run_type=BackupRun.RunType.MANUAL if options["manual"] else BackupRun.RunType.SCHEDULED, @@ -39,7 +41,7 @@ class Command(BaseCommand): result={ "requested": { "dry_run": bool(options["dry_run"]), - "verbose_output": bool(options["dry_run"] or options["verbose_rsync"]), + "verbose_output": verbose_output, "prune": bool(options["prune"]), "prune_max_delete": int(options["prune_max_delete"]), "prune_protect_bases": bool(options["prune_protect_bases"]), @@ -50,7 +52,7 @@ class Command(BaseCommand): run=run, prefix=paths.home, dry_run=bool(options["dry_run"]), - verbose_output=bool(options["dry_run"] or options["verbose_rsync"]), + verbose_output=verbose_output, prune=bool(options["prune"]), prune_max_delete=int(options["prune_max_delete"]), prune_protect_bases=bool(options["prune_protect_bases"]), diff --git a/src/pobsync_backend/templates/pobsync_backend/host_detail.html b/src/pobsync_backend/templates/pobsync_backend/host_detail.html index 83fb2bc..9bd3d17 100644 --- a/src/pobsync_backend/templates/pobsync_backend/host_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/host_detail.html @@ -105,6 +105,7 @@
{% csrf_token %} +
diff --git a/src/pobsync_backend/tests/test_backup_worker.py b/src/pobsync_backend/tests/test_backup_worker.py index d09600e..c96b746 100644 --- a/src/pobsync_backend/tests/test_backup_worker.py +++ b/src/pobsync_backend/tests/test_backup_worker.py @@ -39,13 +39,20 @@ class BackupWorkerTests(TestCase): }, ) - def test_queue_backup_run_can_request_verbose_output(self) -> None: + def test_queue_backup_run_enables_verbose_output_by_default(self) -> None: host = HostConfig.objects.create(host="web-01", address="web-01.example.test") - run = queue_backup_run(host=host, verbose_output=True) + run = queue_backup_run(host=host) self.assertTrue(run.result["requested"]["verbose_output"]) + def test_queue_backup_run_can_disable_verbose_output(self) -> None: + host = HostConfig.objects.create(host="web-01", address="web-01.example.test") + + run = queue_backup_run(host=host, verbose_output=False) + + self.assertFalse(run.result["requested"]["verbose_output"]) + def test_worker_executes_next_queued_run(self) -> None: with TemporaryDirectory() as tmp: backup_root = Path(tmp) / "backups" diff --git a/src/pobsync_backend/tests/test_run_backup_records_snapshot.py b/src/pobsync_backend/tests/test_run_backup_records_snapshot.py index 291dfa2..acec791 100644 --- a/src/pobsync_backend/tests/test_run_backup_records_snapshot.py +++ b/src/pobsync_backend/tests/test_run_backup_records_snapshot.py @@ -39,12 +39,16 @@ class RunBackupRecordsSnapshotTests(TestCase): "host": host.host, "snapshot": str(snapshot_dir), "base": None, + "verbose_output": True, "rsync": {"exit_code": 0}, } call_command("run_pobsync_backup", host.host, prefix=str(Path(tmp) / "home"), stdout=StringIO()) + run_scheduled.assert_called_once() + self.assertTrue(run_scheduled.call_args.kwargs["verbose_output"]) self.assertEqual(BackupRun.objects.count(), 1) run = BackupRun.objects.get() + self.assertTrue(run.result["verbose_output"]) self.assertEqual(SnapshotRecord.objects.count(), 1) record = SnapshotRecord.objects.get() self.assertEqual(run.snapshot, record) @@ -52,6 +56,45 @@ class RunBackupRecordsSnapshotTests(TestCase): self.assertEqual(record.kind, "scheduled") self.assertEqual(record.status, "success") + def test_backup_command_can_skip_default_verbose_rsync_output(self) -> None: + with TemporaryDirectory() as tmp: + backup_root = Path(tmp) / "backups" + GlobalConfig.objects.create(name="default", backup_root=str(backup_root)) + host = HostConfig.objects.create(host="web-01", address="web-01.example.test") + snapshot_dir = backup_root / host.host / "scheduled" / "20260519-021500Z__ABCDEFGH" + meta_dir = snapshot_dir / "meta" + meta_dir.mkdir(parents=True) + write_yaml_atomic( + meta_dir / "meta.yaml", + { + "status": "success", + "started_at": "2026-05-19T02:15:00Z", + "ended_at": "2026-05-19T02:16:00Z", + }, + ) + + with patch("pobsync_backend.backup_runner.run_scheduled") as run_scheduled: + run_scheduled.return_value = { + "ok": True, + "dry_run": False, + "host": host.host, + "snapshot": str(snapshot_dir), + "base": None, + "verbose_output": False, + "rsync": {"exit_code": 0}, + } + call_command( + "run_pobsync_backup", + host.host, + prefix=str(Path(tmp) / "home"), + quiet_rsync=True, + stdout=StringIO(), + ) + + run_scheduled.assert_called_once() + self.assertFalse(run_scheduled.call_args.kwargs["verbose_output"]) + self.assertFalse(BackupRun.objects.get().result["verbose_output"]) + def test_prune_uses_sql_retention_after_snapshot_record_is_created(self) -> None: with TemporaryDirectory() as tmp: backup_root = Path(tmp) / "backups" diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index 82fb79f..7afb6f4 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -1372,7 +1372,7 @@ class ViewTests(TestCase): response = self.client.post( reverse("queue_manual_backup", args=[host.host]), - {"prune_max_delete": "10"}, + {"verbose_output": "on", "prune_max_delete": "10"}, follow=True, ) @@ -1610,7 +1610,7 @@ class ViewTests(TestCase): response = self.client.post( reverse("queue_manual_backup", args=[host.host]), - {"prune_max_delete": "10"}, + {"verbose_output": "on", "prune_max_delete": "10"}, follow=True, ) @@ -1620,7 +1620,7 @@ class ViewTests(TestCase): run.result["requested"], { "dry_run": False, - "verbose_output": False, + "verbose_output": True, "prune": False, "prune_max_delete": 10, "prune_protect_bases": False, diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py index 7e4a0f0..52b2cd7 100644 --- a/src/pobsync_backend/views.py +++ b/src/pobsync_backend/views.py @@ -1301,6 +1301,7 @@ def _default_manual_backup_initial(host_config: HostConfig) -> dict[str, object] schedule = _schedule_for_host(host_config) return { "dry_run": True, + "verbose_output": True, "prune": bool(schedule.prune) if schedule else False, "prune_max_delete": schedule.prune_max_delete if schedule else 10, "prune_protect_bases": bool(schedule.prune_protect_bases) if schedule else False, -- 2.43.0