refactor: replace legacy CLI with Django command surface
Retire the old YAML and cron oriented pobsync CLI commands and expose a SQL-first Django-backed command surface instead. Add schedule and retention management commands, move shared defaults/parsing out of legacy commands, remove obsolete command modules, and update documentation and tests for the new workflow.
This commit is contained in:
@@ -6,8 +6,8 @@ from typing import Any
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from pobsync.cli import parse_retention
|
||||
from pobsync.commands.install import DEFAULT_EXCLUDES, DEFAULT_RSYNC_ARGS
|
||||
from pobsync.config.retention import parse_retention
|
||||
from pobsync.config.defaults import DEFAULT_EXCLUDES, DEFAULT_RSYNC_ARGS
|
||||
from pobsync.util import is_absolute_non_root
|
||||
from pobsync_backend.models import GlobalConfig
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from pobsync.cli import parse_retention
|
||||
from pobsync.config.retention import parse_retention
|
||||
from pobsync.util import sanitize_host
|
||||
from pobsync_backend.models import GlobalConfig, HostConfig
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from pobsync_backend.models import HostConfig, ScheduleConfig
|
||||
from pobsync_backend.scheduler import parse_cron_expr
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Create, update, disable, or remove a SQL-backed pobsync schedule."
|
||||
|
||||
def add_arguments(self, parser) -> None:
|
||||
parser.add_argument("host")
|
||||
parser.add_argument("--cron", help='Cron expression, e.g. "15 2 * * *"')
|
||||
parser.add_argument("--user", default="root")
|
||||
parser.add_argument("--prune", action="store_true")
|
||||
parser.add_argument("--prune-max-delete", type=int, default=10)
|
||||
parser.add_argument("--prune-protect-bases", action="store_true")
|
||||
parser.add_argument("--disabled", action="store_true")
|
||||
parser.add_argument("--delete", action="store_true")
|
||||
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
try:
|
||||
host = HostConfig.objects.get(host=options["host"])
|
||||
except HostConfig.DoesNotExist as exc:
|
||||
raise CommandError(f"Missing HostConfig {options['host']!r}") from exc
|
||||
|
||||
if options["delete"]:
|
||||
deleted, _details = ScheduleConfig.objects.filter(host=host).delete()
|
||||
self.stdout.write(self.style.SUCCESS(f"Deleted {deleted} schedule row(s) for {host.host!r}."))
|
||||
return
|
||||
|
||||
if not options["cron"]:
|
||||
raise CommandError("--cron is required unless --delete is used")
|
||||
try:
|
||||
parse_cron_expr(options["cron"])
|
||||
except ValueError as exc:
|
||||
raise CommandError(str(exc)) from exc
|
||||
|
||||
schedule, created = ScheduleConfig.objects.update_or_create(
|
||||
host=host,
|
||||
defaults={
|
||||
"cron_expr": options["cron"],
|
||||
"user": options["user"],
|
||||
"enabled": not options["disabled"],
|
||||
"prune": bool(options["prune"]),
|
||||
"prune_max_delete": int(options["prune_max_delete"]),
|
||||
"prune_protect_bases": bool(options["prune_protect_bases"]),
|
||||
},
|
||||
)
|
||||
action = "Created" if created else "Updated"
|
||||
state = "enabled" if schedule.enabled else "disabled"
|
||||
self.stdout.write(self.style.SUCCESS(f"{action} {state} schedule for {host.host!r}."))
|
||||
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from pobsync.commands.retention_apply import run_retention_apply
|
||||
from pobsync.commands.retention_plan import run_retention_plan
|
||||
from pobsync_backend.config_source import DjangoConfigSource
|
||||
from pobsync_backend.models import HostConfig
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Plan or apply retention using SQL-backed pobsync configuration."
|
||||
|
||||
def add_arguments(self, parser) -> None:
|
||||
parser.add_argument("host")
|
||||
parser.add_argument("--prefix", default=settings.POBSYNC_HOME)
|
||||
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")
|
||||
parser.add_argument("--yes", action="store_true")
|
||||
parser.add_argument("--max-delete", type=int, default=10)
|
||||
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
host = options["host"]
|
||||
if not HostConfig.objects.filter(host=host, enabled=True).exists():
|
||||
raise CommandError(f"Missing enabled HostConfig {host!r}")
|
||||
|
||||
config_source = DjangoConfigSource()
|
||||
if options["apply"]:
|
||||
if not options["yes"]:
|
||||
raise CommandError("--yes is required with --apply")
|
||||
result = run_retention_apply(
|
||||
prefix=Path(options["prefix"]),
|
||||
host=host,
|
||||
kind=options["kind"],
|
||||
protect_bases=bool(options["protect_bases"]),
|
||||
yes=True,
|
||||
max_delete=int(options["max_delete"]),
|
||||
config_source=config_source,
|
||||
)
|
||||
else:
|
||||
result = run_retention_plan(
|
||||
prefix=Path(options["prefix"]),
|
||||
host=host,
|
||||
kind=options["kind"],
|
||||
protect_bases=bool(options["protect_bases"]),
|
||||
config_source=config_source,
|
||||
)
|
||||
|
||||
self.stdout.write(json.dumps(result, indent=2, sort_keys=False))
|
||||
@@ -6,7 +6,7 @@ from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
|
||||
from pobsync_backend.config_source import DjangoConfigSource
|
||||
from pobsync_backend.models import GlobalConfig, HostConfig
|
||||
from pobsync_backend.models import GlobalConfig, HostConfig, ScheduleConfig
|
||||
|
||||
|
||||
class ConfigureCommandsTests(TestCase):
|
||||
@@ -54,3 +54,19 @@ class ConfigureCommandsTests(TestCase):
|
||||
effective = DjangoConfigSource().effective_config_for_host("web-01")
|
||||
self.assertEqual(effective["retention"]["yearly"], 2)
|
||||
self.assertEqual(effective["excludes_effective"], ["/tmp/***"])
|
||||
|
||||
def test_configure_schedule_creates_sql_schedule(self) -> None:
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
out = StringIO()
|
||||
|
||||
call_command(
|
||||
"configure_pobsync_schedule",
|
||||
host.host,
|
||||
cron="15 2 * * *",
|
||||
prune=True,
|
||||
stdout=out,
|
||||
)
|
||||
|
||||
schedule = ScheduleConfig.objects.get(host=host)
|
||||
self.assertEqual(schedule.cron_expr, "15 2 * * *")
|
||||
self.assertTrue(schedule.prune)
|
||||
|
||||
41
src/pobsync_backend/tests/test_console_entrypoint.py
Normal file
41
src/pobsync_backend/tests/test_console_entrypoint.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from pobsync.cli import main
|
||||
|
||||
|
||||
class ConsoleEntrypointTests(SimpleTestCase):
|
||||
def test_maps_backup_alias_to_django_command(self) -> None:
|
||||
with patch("pobsync.cli.execute_from_command_line") as execute:
|
||||
exit_code = main(["backup", "web-01", "--dry-run"])
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
execute.assert_called_once_with(["pobsync", "run_pobsync_backup", "web-01", "--dry-run"])
|
||||
|
||||
def test_unknown_command_returns_usage_error(self) -> None:
|
||||
stderr = StringIO()
|
||||
with patch("sys.stderr", stderr):
|
||||
exit_code = main(["run-scheduled", "web-01"])
|
||||
|
||||
self.assertEqual(exit_code, 2)
|
||||
self.assertIn("Unknown pobsync command", stderr.getvalue())
|
||||
|
||||
def test_django_passthrough_keeps_management_command_name(self) -> None:
|
||||
with patch("pobsync.cli.execute_from_command_line") as execute:
|
||||
exit_code = main(["django", "check"])
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
execute.assert_called_once_with(["pobsync", "check"])
|
||||
|
||||
def test_maps_schedule_alias_to_django_command(self) -> None:
|
||||
with patch("pobsync.cli.execute_from_command_line") as execute:
|
||||
exit_code = main(["schedule", "web-01", "--cron", "15 2 * * *"])
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
execute.assert_called_once_with(
|
||||
["pobsync", "configure_pobsync_schedule", "web-01", "--cron", "15 2 * * *"]
|
||||
)
|
||||
Reference in New Issue
Block a user