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:
2026-05-19 05:14:29 +02:00
parent 6d9ddc4457
commit e564262c72
22 changed files with 351 additions and 2043 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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}."))

View File

@@ -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))