(feature) Add optional verbose rsync output for manual backups
Expose a verbose rsync output option in the Django manual backup form and store the selected value with the queued run request. Propagate the option through the worker, direct management command, and rsync command builder so real backups can emit itemized changes, file-list progress, and stats when requested. Dry-runs continue to use verbose output by default and report that consistently in requested options. Cover the queue, worker, view, and rsync command behavior with focused tests.
This commit is contained in:
@@ -32,12 +32,20 @@ class BackupWorkerTests(TestCase):
|
||||
run.result["requested"],
|
||||
{
|
||||
"dry_run": True,
|
||||
"verbose_output": True,
|
||||
"prune": True,
|
||||
"prune_max_delete": 3,
|
||||
"prune_protect_bases": True,
|
||||
},
|
||||
)
|
||||
|
||||
def test_queue_backup_run_can_request_verbose_output(self) -> None:
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
|
||||
run = queue_backup_run(host=host, verbose_output=True)
|
||||
|
||||
self.assertTrue(run.result["requested"]["verbose_output"])
|
||||
|
||||
def test_worker_executes_next_queued_run(self) -> None:
|
||||
with TemporaryDirectory() as tmp:
|
||||
backup_root = Path(tmp) / "backups"
|
||||
@@ -47,7 +55,7 @@ class BackupWorkerTests(TestCase):
|
||||
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"})
|
||||
run = queue_backup_run(host=host)
|
||||
run = queue_backup_run(host=host, verbose_output=True)
|
||||
|
||||
with patch("pobsync_backend.backup_runner.run_scheduled") as run_scheduled:
|
||||
def fake_run_scheduled(**kwargs):
|
||||
@@ -68,6 +76,7 @@ class BackupWorkerTests(TestCase):
|
||||
|
||||
self.assertEqual(count, 1)
|
||||
self.assertEqual(run_scheduled.call_args.kwargs["run_id"], run.id)
|
||||
self.assertTrue(run_scheduled.call_args.kwargs["verbose_output"])
|
||||
run.refresh_from_db()
|
||||
self.assertEqual(run.status, BackupRun.Status.SUCCESS)
|
||||
self.assertEqual(SnapshotRecord.objects.count(), 1)
|
||||
|
||||
@@ -160,6 +160,45 @@ class RunScheduledConfigSourceTests(SimpleTestCase):
|
||||
self.assertNotIn("--info=flist2,progress2,stats2", command)
|
||||
self.assertIn("--info=name1,stats2", command)
|
||||
|
||||
def test_real_run_can_request_verbose_output_args(self) -> None:
|
||||
with TemporaryDirectory() as tmp:
|
||||
prefix = Path(tmp) / "home"
|
||||
|
||||
with patch("pobsync.commands.run_scheduled.run_rsync") as run_rsync:
|
||||
run_rsync.return_value = RsyncResult(exit_code=0, command=["rsync", "--archive"])
|
||||
result = run_scheduled(
|
||||
prefix=prefix,
|
||||
host="web-01",
|
||||
dry_run=False,
|
||||
verbose_output=True,
|
||||
config_source=FakeConfigSource(backup_root=str(Path(tmp) / "backups")),
|
||||
)
|
||||
|
||||
command = run_rsync.call_args.args[0]
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertIn("--itemize-changes", command)
|
||||
self.assertIn("--info=flist2,progress2,stats2", command)
|
||||
self.assertTrue(result["verbose_output"])
|
||||
|
||||
def test_real_run_keeps_default_output_quiet(self) -> None:
|
||||
with TemporaryDirectory() as tmp:
|
||||
prefix = Path(tmp) / "home"
|
||||
|
||||
with patch("pobsync.commands.run_scheduled.run_rsync") as run_rsync:
|
||||
run_rsync.return_value = RsyncResult(exit_code=0, command=["rsync", "--archive"])
|
||||
result = run_scheduled(
|
||||
prefix=prefix,
|
||||
host="web-01",
|
||||
dry_run=False,
|
||||
config_source=FakeConfigSource(backup_root=str(Path(tmp) / "backups")),
|
||||
)
|
||||
|
||||
command = run_rsync.call_args.args[0]
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertNotIn("--itemize-changes", command)
|
||||
self.assertNotIn("--info=flist2,progress2,stats2", command)
|
||||
self.assertFalse(result["verbose_output"])
|
||||
|
||||
def test_dry_run_reports_cancelled_rsync(self) -> None:
|
||||
def fake_run_rsync(command, log_path, timeout_seconds, cancel_check=None):
|
||||
self.assertTrue(cancel_check())
|
||||
|
||||
@@ -655,6 +655,7 @@ class ViewTests(TestCase):
|
||||
reverse("queue_manual_backup", args=[host.host]),
|
||||
{
|
||||
"dry_run": "on",
|
||||
"verbose_output": "on",
|
||||
"prune": "on",
|
||||
"prune_max_delete": "4",
|
||||
"prune_protect_bases": "on",
|
||||
@@ -671,6 +672,7 @@ class ViewTests(TestCase):
|
||||
run.result["requested"],
|
||||
{
|
||||
"dry_run": True,
|
||||
"verbose_output": True,
|
||||
"prune": True,
|
||||
"prune_max_delete": 4,
|
||||
"prune_protect_bases": True,
|
||||
@@ -694,6 +696,7 @@ class ViewTests(TestCase):
|
||||
run.result["requested"],
|
||||
{
|
||||
"dry_run": False,
|
||||
"verbose_output": False,
|
||||
"prune": False,
|
||||
"prune_max_delete": 10,
|
||||
"prune_protect_bases": False,
|
||||
@@ -745,6 +748,7 @@ class ViewTests(TestCase):
|
||||
"snapshot": snapshot.path,
|
||||
"requested": {
|
||||
"dry_run": True,
|
||||
"verbose_output": True,
|
||||
"prune": False,
|
||||
"prune_max_delete": 10,
|
||||
"prune_protect_bases": False,
|
||||
@@ -761,6 +765,7 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "ABCDEFGH")
|
||||
self.assertContains(response, "Requested Options")
|
||||
self.assertContains(response, "Dry run:</strong> yes")
|
||||
self.assertContains(response, "Verbose rsync output:</strong> yes")
|
||||
self.assertContains(response, ""ok": true")
|
||||
self.assertContains(response, reverse("snapshot_detail", args=[snapshot.id]))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user