## Summary
- Add per-host rsync bandwidth limit overrides with inherit/unlimited semantics. - Store the effective bwlimit in run metadata/results and show it in host/run detail views. - Document recommended starting values for VPN and remote backups. ## Tests - `.venv/bin/python manage.py makemigrations --check --dry-run` - `.venv/bin/python manage.py test src.pobsync_backend.tests.test_django_config_source.DjangoConfigSourceTests.test_returns_effective_config_from_database src.pobsync_backend.tests.test_django_config_source.DjangoConfigSourceTests.test_host_can_disable_global_rsync_bandwidth_limit src.pobsync_backend.tests.test_configure_commands.ConfigureCommandsTests.test_configure_host_uses_global_retention_defaults src.pobsync_backend.tests.test_run_scheduled_config_source.RunScheduledConfigSourceTests.test_dry_run_applies_configured_bandwidth_limit src.pobsync_backend.tests.test_run_scheduled_config_source.RunScheduledConfigSourceTests.test_real_run_can_request_verbose_output_args --verbosity 2` - `.venv/bin/python manage.py test src.pobsync_backend.tests.test_views.ViewTests.test_create_host_config_form_creates_host src.pobsync_backend.tests.test_views.ViewTests.test_host_detail_renders_effective_config_preview src.pobsync_backend.tests.test_views.ViewTests.test_run_detail_renders_result_payload src.pobsync_backend.tests.test_views.ViewTests.test_host_config_form_updates_host_config --verbosity 2` - `.venv/bin/python manage.py check` Closes #51
This commit is contained in:
@@ -42,6 +42,7 @@ class ConfigureCommandsTests(TestCase):
|
||||
address="web-01.example.test",
|
||||
exclude_add=["/tmp/***"],
|
||||
rsync_extra_arg=["--delete"],
|
||||
rsync_bwlimit_kbps=4096,
|
||||
stdout=out,
|
||||
)
|
||||
|
||||
@@ -49,10 +50,12 @@ class ConfigureCommandsTests(TestCase):
|
||||
self.assertEqual(host.retention_daily, 5)
|
||||
self.assertEqual(host.excludes_add, ["/tmp/***"])
|
||||
self.assertEqual(host.rsync_extra_args, ["--delete"])
|
||||
self.assertEqual(host.rsync_bwlimit_kbps, 4096)
|
||||
|
||||
effective = DjangoConfigSource().effective_config_for_host("web-01")
|
||||
self.assertEqual(effective["retention"]["yearly"], 2)
|
||||
self.assertEqual(effective["excludes_effective"], ["/tmp/***"])
|
||||
self.assertEqual(effective["rsync"]["bwlimit_kbps"], 4096)
|
||||
|
||||
def test_configure_schedule_creates_sql_schedule(self) -> None:
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
|
||||
@@ -17,6 +17,7 @@ class DjangoConfigSourceTests(TestCase):
|
||||
backup_root="/backups",
|
||||
rsync_args=["--archive"],
|
||||
rsync_extra_args=["--numeric-ids"],
|
||||
rsync_bwlimit_kbps=10000,
|
||||
excludes_default=["/proc/***"],
|
||||
retention_daily=7,
|
||||
retention_weekly=4,
|
||||
@@ -28,6 +29,7 @@ class DjangoConfigSourceTests(TestCase):
|
||||
address="web-01.example.test",
|
||||
excludes_add=["/tmp/***"],
|
||||
rsync_extra_args=["--delete"],
|
||||
rsync_bwlimit_kbps=2500,
|
||||
retention_daily=7,
|
||||
retention_weekly=4,
|
||||
retention_monthly=3,
|
||||
@@ -46,6 +48,24 @@ class DjangoConfigSourceTests(TestCase):
|
||||
self.assertEqual(cfg["address"], "web-01.example.test")
|
||||
self.assertEqual(cfg["excludes_effective"], ["/proc/***", "/tmp/***"])
|
||||
self.assertEqual(cfg["rsync"]["args_effective"], ["--archive", "--numeric-ids", "--delete"])
|
||||
self.assertEqual(cfg["rsync"]["bwlimit_kbps"], 2500)
|
||||
|
||||
def test_host_can_disable_global_rsync_bandwidth_limit(self) -> None:
|
||||
GlobalConfig.objects.create(
|
||||
name="default",
|
||||
backup_root="/backups",
|
||||
rsync_args=["--archive"],
|
||||
rsync_bwlimit_kbps=5000,
|
||||
)
|
||||
HostConfig.objects.create(
|
||||
host="web-01",
|
||||
address="web-01.example.test",
|
||||
rsync_bwlimit_kbps=0,
|
||||
)
|
||||
|
||||
cfg = DjangoConfigSource().effective_config_for_host("web-01")
|
||||
|
||||
self.assertEqual(cfg["rsync"]["bwlimit_kbps"], 0)
|
||||
|
||||
def test_materializes_global_ssh_credential_for_runtime_config(self) -> None:
|
||||
credential = SshCredential.objects.create(
|
||||
|
||||
@@ -12,8 +12,9 @@ from pobsync.rsync import RsyncResult
|
||||
|
||||
|
||||
class FakeConfigSource:
|
||||
def __init__(self, backup_root: str = "/tmp/pobsync-test-backups") -> None:
|
||||
def __init__(self, backup_root: str = "/tmp/pobsync-test-backups", bwlimit_kbps: int = 0) -> None:
|
||||
self.backup_root = backup_root
|
||||
self.bwlimit_kbps = bwlimit_kbps
|
||||
|
||||
def effective_config_for_host(self, host: str) -> dict:
|
||||
return {
|
||||
@@ -25,7 +26,7 @@ class FakeConfigSource:
|
||||
"binary": "rsync",
|
||||
"args_effective": ["--archive"],
|
||||
"timeout_seconds": 0,
|
||||
"bwlimit_kbps": 0,
|
||||
"bwlimit_kbps": self.bwlimit_kbps,
|
||||
},
|
||||
"source_root": "/",
|
||||
"includes": [],
|
||||
@@ -54,6 +55,21 @@ class RunScheduledConfigSourceTests(SimpleTestCase):
|
||||
self.assertEqual(result["host"], "web-01")
|
||||
run_rsync.assert_called_once()
|
||||
|
||||
def test_dry_run_applies_configured_bandwidth_limit(self) -> None:
|
||||
with patch("pobsync.commands.run_scheduled.run_rsync") as run_rsync:
|
||||
run_rsync.return_value = RsyncResult(exit_code=0, command=["rsync", "--bwlimit=4096"])
|
||||
|
||||
result = run_scheduled(
|
||||
prefix=Path("/missing-prefix"),
|
||||
host="web-01",
|
||||
dry_run=True,
|
||||
config_source=FakeConfigSource(bwlimit_kbps=4096),
|
||||
)
|
||||
|
||||
command = run_rsync.call_args.args[0]
|
||||
self.assertIn("--bwlimit=4096", command)
|
||||
self.assertEqual(result["rsync"]["bwlimit_kbps"], 4096)
|
||||
|
||||
def test_failed_dry_run_includes_log_tail(self) -> None:
|
||||
def fake_run_rsync(command, log_path, timeout_seconds, cancel_check=None):
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -186,11 +202,13 @@ class RunScheduledConfigSourceTests(SimpleTestCase):
|
||||
host="web-01",
|
||||
dry_run=False,
|
||||
verbose_output=True,
|
||||
config_source=FakeConfigSource(backup_root=str(Path(tmp) / "backups")),
|
||||
config_source=FakeConfigSource(backup_root=str(Path(tmp) / "backups"), bwlimit_kbps=2048),
|
||||
)
|
||||
|
||||
command = run_rsync.call_args.args[0]
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertIn("--bwlimit=2048", command)
|
||||
self.assertEqual(result["rsync"]["bwlimit_kbps"], 2048)
|
||||
self.assertIn("--stats", command)
|
||||
self.assertIn("--itemize-changes", command)
|
||||
self.assertIn("--info=flist2,progress2,stats2", command)
|
||||
|
||||
@@ -921,6 +921,7 @@ class ViewTests(TestCase):
|
||||
"excludes_add": "*.tmp",
|
||||
"excludes_replace": "",
|
||||
"rsync_extra_args": "--numeric-ids",
|
||||
"rsync_bwlimit_kbps": "4096",
|
||||
"retention_daily": "7",
|
||||
"retention_weekly": "4",
|
||||
"retention_monthly": "2",
|
||||
@@ -938,6 +939,7 @@ class ViewTests(TestCase):
|
||||
self.assertEqual(host.includes, ["/srv/www", "/srv/db"])
|
||||
self.assertEqual(host.excludes_add, ["*.tmp"])
|
||||
self.assertEqual(host.rsync_extra_args, ["--numeric-ids"])
|
||||
self.assertEqual(host.rsync_bwlimit_kbps, 4096)
|
||||
self.assertEqual(host.retention_weekly, 4)
|
||||
|
||||
def test_create_host_config_uses_global_defaults_and_prepares_directories(self) -> None:
|
||||
@@ -1077,6 +1079,7 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "default-key")
|
||||
self.assertContains(response, "-oBatchMode=yes")
|
||||
self.assertContains(response, "--archive --numeric-ids --delete --one-file-system")
|
||||
self.assertContains(response, "2048 KB/s")
|
||||
self.assertContains(response, "/srv/www/***")
|
||||
self.assertContains(response, "/srv/www/cache/***")
|
||||
self.assertContains(response, "d14")
|
||||
@@ -1473,9 +1476,10 @@ class ViewTests(TestCase):
|
||||
"ok": True,
|
||||
"snapshot": snapshot.path,
|
||||
"rsync": {
|
||||
"command": ["rsync", "--archive", "root@web-01:/", snapshot.path],
|
||||
"command": ["rsync", "--archive", "--bwlimit=2048", "root@web-01:/", snapshot.path],
|
||||
"exit_code": 0,
|
||||
"log_tail": ["sending incremental file list", "sent 500 bytes"],
|
||||
"bwlimit_kbps": 2048,
|
||||
},
|
||||
"requested": {
|
||||
"dry_run": True,
|
||||
@@ -1510,6 +1514,8 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "Dry run:</strong> yes")
|
||||
self.assertContains(response, "Verbose rsync output:</strong> yes")
|
||||
self.assertContains(response, "Rsync Command")
|
||||
self.assertContains(response, "Bandwidth limit:</strong>")
|
||||
self.assertContains(response, "2048 KB/s")
|
||||
self.assertContains(response, "--archive")
|
||||
self.assertContains(response, "Rsync Log")
|
||||
self.assertContains(response, "sending incremental file list")
|
||||
@@ -2464,6 +2470,7 @@ class ViewTests(TestCase):
|
||||
"excludes_add": "*.tmp\ncache/",
|
||||
"excludes_replace": "",
|
||||
"rsync_extra_args": "--numeric-ids\n--delete",
|
||||
"rsync_bwlimit_kbps": "8192",
|
||||
"retention_daily": "7",
|
||||
"retention_weekly": "4",
|
||||
"retention_monthly": "2",
|
||||
@@ -2483,6 +2490,7 @@ class ViewTests(TestCase):
|
||||
self.assertEqual(host.excludes_add, ["*.tmp", "cache/"])
|
||||
self.assertIsNone(host.excludes_replace)
|
||||
self.assertEqual(host.rsync_extra_args, ["--numeric-ids", "--delete"])
|
||||
self.assertEqual(host.rsync_bwlimit_kbps, 8192)
|
||||
self.assertEqual(host.retention_daily, 7)
|
||||
self.assertEqual(host.retention_yearly, 1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user