refactor: inject config sources into scheduled backups
Introduce a ConfigSource interface so scheduled backups no longer need to load host configuration directly from runtime YAML. Add a Django-backed config source for SQL-driven backup runs, keep file-based config as the CLI default, and make scheduled prune execution actually apply retention after successful runs.
This commit is contained in:
48
src/pobsync_backend/tests/test_django_config_source.py
Normal file
48
src/pobsync_backend/tests/test_django_config_source.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from pobsync_backend.config_source import DjangoConfigSource
|
||||
from pobsync_backend.models import GlobalConfig, HostConfig
|
||||
|
||||
|
||||
class DjangoConfigSourceTests(TestCase):
|
||||
def test_returns_effective_config_from_database(self) -> None:
|
||||
GlobalConfig.objects.create(
|
||||
name="default",
|
||||
backup_root="/backups",
|
||||
pobsync_home="/opt/pobsync",
|
||||
data={
|
||||
"backup_root": "/ignored",
|
||||
"pobsync_home": "/ignored",
|
||||
"ssh": {"user": "root", "port": 22, "options": []},
|
||||
"rsync": {
|
||||
"binary": "rsync",
|
||||
"args": ["--archive"],
|
||||
"timeout_seconds": 0,
|
||||
"bwlimit_kbps": 0,
|
||||
"extra_args": ["--numeric-ids"],
|
||||
},
|
||||
"defaults": {"source_root": "/", "destination_subdir": ""},
|
||||
"excludes_default": ["/proc/***"],
|
||||
"retention_defaults": {"daily": 7, "weekly": 4, "monthly": 3, "yearly": 1},
|
||||
},
|
||||
)
|
||||
HostConfig.objects.create(
|
||||
host="web-01",
|
||||
address="web-01.example.test",
|
||||
config={
|
||||
"retention": {"daily": 7, "weekly": 4, "monthly": 3, "yearly": 1},
|
||||
"excludes_add": ["/tmp/***"],
|
||||
"includes": [],
|
||||
"rsync": {"extra_args": ["--delete"]},
|
||||
},
|
||||
)
|
||||
|
||||
cfg = DjangoConfigSource().effective_config_for_host("web-01")
|
||||
|
||||
self.assertEqual(cfg["backup_root"], "/backups")
|
||||
self.assertEqual(cfg["host"], "web-01")
|
||||
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"])
|
||||
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from pobsync.commands.run_scheduled import run_scheduled
|
||||
from pobsync.rsync import RsyncResult
|
||||
|
||||
|
||||
class FakeConfigSource:
|
||||
def __init__(self, backup_root: str = "/tmp/pobsync-test-backups") -> None:
|
||||
self.backup_root = backup_root
|
||||
|
||||
def effective_config_for_host(self, host: str) -> dict:
|
||||
return {
|
||||
"backup_root": self.backup_root,
|
||||
"host": host,
|
||||
"address": "example.test",
|
||||
"ssh": {"user": "root", "port": 22, "options": []},
|
||||
"rsync": {
|
||||
"binary": "rsync",
|
||||
"args_effective": ["--archive"],
|
||||
"timeout_seconds": 0,
|
||||
"bwlimit_kbps": 0,
|
||||
},
|
||||
"source_root": "/",
|
||||
"includes": [],
|
||||
"excludes_effective": [],
|
||||
"retention": {"daily": 7, "weekly": 4, "monthly": 3, "yearly": 1},
|
||||
}
|
||||
|
||||
|
||||
class RunScheduledConfigSourceTests(SimpleTestCase):
|
||||
def test_dry_run_uses_injected_config_source(self) -> None:
|
||||
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=Path("/missing-prefix"),
|
||||
host="web-01",
|
||||
dry_run=True,
|
||||
config_source=FakeConfigSource(),
|
||||
)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertEqual(result["host"], "web-01")
|
||||
run_rsync.assert_called_once()
|
||||
|
||||
def test_successful_real_run_applies_prune_when_requested(self) -> None:
|
||||
with TemporaryDirectory() as tmp:
|
||||
prefix = Path(tmp) / "home"
|
||||
with (
|
||||
patch("pobsync.commands.run_scheduled.run_rsync") as run_rsync,
|
||||
patch("pobsync.commands.retention_apply.run_retention_plan") as plan,
|
||||
):
|
||||
run_rsync.return_value = RsyncResult(exit_code=0, command=["rsync", "--archive"])
|
||||
plan.return_value = {
|
||||
"ok": True,
|
||||
"delete": [],
|
||||
"keep": [],
|
||||
"reasons": {},
|
||||
"protect_bases": False,
|
||||
}
|
||||
|
||||
result = run_scheduled(
|
||||
prefix=prefix,
|
||||
host="web-01",
|
||||
dry_run=False,
|
||||
prune=True,
|
||||
prune_max_delete=10,
|
||||
config_source=FakeConfigSource(backup_root=str(Path(tmp) / "backups")),
|
||||
)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertIsNotNone(result["prune"])
|
||||
self.assertEqual(result["prune"]["deleted"], [])
|
||||
Reference in New Issue
Block a user