from __future__ import annotations from pathlib import Path from typing import Any from django.conf import settings from django.core.management.base import BaseCommand, CommandError from django.utils import timezone from pobsync.commands.run_scheduled import run_scheduled from pobsync.paths import PobsyncPaths from pobsync_backend.config_source import DjangoConfigSource from pobsync_backend.models import BackupRun, HostConfig from pobsync_backend.retention import run_sql_retention_apply from pobsync_backend.snapshot_discovery import infer_snapshot_kind, upsert_snapshot_record class Command(BaseCommand): help = "Run a scheduled pobsync backup and record the result in Django." def add_arguments(self, parser) -> None: parser.add_argument("host", help="Host to back up") parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Pobsync home directory") parser.add_argument("--dry-run", action="store_true", help="Run rsync --dry-run") parser.add_argument("--prune", action="store_true", help="Apply retention after a successful run") parser.add_argument("--prune-max-delete", type=int, default=10) parser.add_argument("--prune-protect-bases", action="store_true") def handle(self, *args: Any, **options: Any) -> None: host_name = options["host"] paths = PobsyncPaths(home=Path(options["prefix"])) try: host = HostConfig.objects.get(host=host_name, enabled=True) except HostConfig.DoesNotExist as exc: raise CommandError(f"Missing enabled HostConfig {host_name!r}") from exc run = BackupRun.objects.create( host=host, run_type=BackupRun.RunType.SCHEDULED, status=BackupRun.Status.RUNNING, started_at=timezone.now(), ) try: result = run_scheduled( prefix=paths.home, host=host.host, dry_run=bool(options["dry_run"]), prune=False, config_source=DjangoConfigSource(), ) except Exception as exc: run.status = BackupRun.Status.FAILED run.ended_at = timezone.now() run.result = {"ok": False, "error": str(exc), "type": type(exc).__name__} run.save(update_fields=["status", "ended_at", "result"]) raise run.status = BackupRun.Status.SUCCESS if result.get("ok") else BackupRun.Status.FAILED run.ended_at = timezone.now() run.snapshot_path = str(result.get("snapshot") or "") run.base_path = str(result.get("base") or "") rsync = result.get("rsync") if isinstance(result.get("rsync"), dict) else {} run.rsync_exit_code = rsync.get("exit_code") run.result = result snapshot_record = None if run.snapshot_path: snapshot_path = Path(run.snapshot_path) try: kind = infer_snapshot_kind(snapshot_path) snapshot_record, _created = upsert_snapshot_record(host=host, kind=kind, snapshot_dir=snapshot_path) except ValueError: snapshot_record = None if result.get("ok") and not result.get("dry_run") and options["prune"]: try: result["prune"] = run_sql_retention_apply( prefix=paths.home, host=host.host, kind="scheduled", protect_bases=bool(options["prune_protect_bases"]), yes=True, max_delete=int(options["prune_max_delete"]), acquire_lock=False, ) except Exception as exc: result["prune"] = {"ok": False, "error": str(exc), "type": type(exc).__name__} run.status = BackupRun.Status.FAILED run.result = result run.snapshot = snapshot_record run.save( update_fields=[ "status", "ended_at", "snapshot_path", "snapshot", "base_path", "rsync_exit_code", "result", ], ) raise run.snapshot = snapshot_record run.result = result run.save( update_fields=[ "status", "ended_at", "snapshot_path", "snapshot", "base_path", "rsync_exit_code", "result", ], ) if result.get("ok"): self.stdout.write(self.style.SUCCESS(f"Backup completed for {host.host}.")) return raise CommandError(f"Backup failed for {host.host}; run id={run.id}")