157 lines
5.0 KiB
Python
157 lines
5.0 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from django.contrib.admin.views.decorators import staff_member_required
|
||
|
|
from django.db.models import Count
|
||
|
|
from django.http import JsonResponse
|
||
|
|
|
||
|
|
from .models import BackupRun, HostConfig, ScheduleConfig, SnapshotRecord
|
||
|
|
|
||
|
|
|
||
|
|
@staff_member_required
|
||
|
|
def api_index(request) -> JsonResponse:
|
||
|
|
return JsonResponse(
|
||
|
|
{
|
||
|
|
"ok": True,
|
||
|
|
"endpoints": {
|
||
|
|
"hosts": request.build_absolute_uri("/api/hosts/"),
|
||
|
|
"snapshots": request.build_absolute_uri("/api/snapshots/"),
|
||
|
|
"runs": request.build_absolute_uri("/api/runs/"),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@staff_member_required
|
||
|
|
def hosts(request) -> JsonResponse:
|
||
|
|
host_qs = (
|
||
|
|
HostConfig.objects.annotate(snapshot_count=Count("snapshots", distinct=True), run_count=Count("runs", distinct=True))
|
||
|
|
.select_related("schedule")
|
||
|
|
.order_by("host")
|
||
|
|
)
|
||
|
|
return JsonResponse({"ok": True, "hosts": [_host_payload(host) for host in host_qs]})
|
||
|
|
|
||
|
|
|
||
|
|
@staff_member_required
|
||
|
|
def snapshots(request) -> JsonResponse:
|
||
|
|
snapshot_qs = SnapshotRecord.objects.select_related("host", "base").order_by("host__host", "-started_at", "dirname")
|
||
|
|
host_filter = request.GET.get("host")
|
||
|
|
kind_filter = request.GET.get("kind")
|
||
|
|
if host_filter:
|
||
|
|
snapshot_qs = snapshot_qs.filter(host__host=host_filter)
|
||
|
|
if kind_filter:
|
||
|
|
snapshot_qs = snapshot_qs.filter(kind=kind_filter)
|
||
|
|
limit = _limit_from_request(request)
|
||
|
|
return JsonResponse({"ok": True, "snapshots": [_snapshot_payload(snapshot) for snapshot in snapshot_qs[:limit]]})
|
||
|
|
|
||
|
|
|
||
|
|
@staff_member_required
|
||
|
|
def runs(request) -> JsonResponse:
|
||
|
|
run_qs = BackupRun.objects.select_related("host", "snapshot").order_by("-created_at")
|
||
|
|
host_filter = request.GET.get("host")
|
||
|
|
status_filter = request.GET.get("status")
|
||
|
|
if host_filter:
|
||
|
|
run_qs = run_qs.filter(host__host=host_filter)
|
||
|
|
if status_filter:
|
||
|
|
run_qs = run_qs.filter(status=status_filter)
|
||
|
|
limit = _limit_from_request(request)
|
||
|
|
return JsonResponse({"ok": True, "runs": [_run_payload(run) for run in run_qs[:limit]]})
|
||
|
|
|
||
|
|
|
||
|
|
def _host_payload(host: HostConfig) -> dict[str, Any]:
|
||
|
|
try:
|
||
|
|
schedule = host.schedule
|
||
|
|
except ScheduleConfig.DoesNotExist:
|
||
|
|
schedule = None
|
||
|
|
return {
|
||
|
|
"host": host.host,
|
||
|
|
"address": host.address,
|
||
|
|
"enabled": host.enabled,
|
||
|
|
"snapshot_count": host.snapshot_count,
|
||
|
|
"run_count": host.run_count,
|
||
|
|
"retention": {
|
||
|
|
"daily": host.retention_daily,
|
||
|
|
"weekly": host.retention_weekly,
|
||
|
|
"monthly": host.retention_monthly,
|
||
|
|
"yearly": host.retention_yearly,
|
||
|
|
},
|
||
|
|
"schedule": None
|
||
|
|
if schedule is None
|
||
|
|
else {
|
||
|
|
"cron_expr": schedule.cron_expr,
|
||
|
|
"enabled": schedule.enabled,
|
||
|
|
"prune": schedule.prune,
|
||
|
|
"last_status": schedule.last_status,
|
||
|
|
"last_started_at": _iso(schedule.last_started_at),
|
||
|
|
"last_finished_at": _iso(schedule.last_finished_at),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _snapshot_payload(snapshot: SnapshotRecord) -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"host": snapshot.host.host,
|
||
|
|
"kind": snapshot.kind,
|
||
|
|
"dirname": snapshot.dirname,
|
||
|
|
"path": snapshot.path,
|
||
|
|
"status": snapshot.status,
|
||
|
|
"started_at": _iso(snapshot.started_at),
|
||
|
|
"ended_at": _iso(snapshot.ended_at),
|
||
|
|
"discovered_at": _iso(snapshot.discovered_at),
|
||
|
|
"base": _base_payload(snapshot),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _base_payload(snapshot: SnapshotRecord) -> dict[str, Any] | None:
|
||
|
|
if snapshot.base is not None:
|
||
|
|
return {
|
||
|
|
"kind": snapshot.base.kind,
|
||
|
|
"dirname": snapshot.base.dirname,
|
||
|
|
"path": snapshot.base.path,
|
||
|
|
"resolved": True,
|
||
|
|
}
|
||
|
|
if snapshot.base_kind and snapshot.base_dirname:
|
||
|
|
return {
|
||
|
|
"kind": snapshot.base_kind,
|
||
|
|
"dirname": snapshot.base_dirname,
|
||
|
|
"path": snapshot.base_path,
|
||
|
|
"snapshot_id": snapshot.base_snapshot_id,
|
||
|
|
"resolved": False,
|
||
|
|
}
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def _run_payload(run: BackupRun) -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"id": run.pk,
|
||
|
|
"host": run.host.host,
|
||
|
|
"run_type": run.run_type,
|
||
|
|
"status": run.status,
|
||
|
|
"started_at": _iso(run.started_at),
|
||
|
|
"ended_at": _iso(run.ended_at),
|
||
|
|
"snapshot": None
|
||
|
|
if run.snapshot is None
|
||
|
|
else {
|
||
|
|
"kind": run.snapshot.kind,
|
||
|
|
"dirname": run.snapshot.dirname,
|
||
|
|
"path": run.snapshot.path,
|
||
|
|
},
|
||
|
|
"snapshot_path": run.snapshot_path,
|
||
|
|
"base_path": run.base_path,
|
||
|
|
"rsync_exit_code": run.rsync_exit_code,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _limit_from_request(request, *, default: int = 100, maximum: int = 500) -> int:
|
||
|
|
value = request.GET.get("limit", str(default))
|
||
|
|
try:
|
||
|
|
limit = int(value)
|
||
|
|
except ValueError:
|
||
|
|
return default
|
||
|
|
return max(1, min(limit, maximum))
|
||
|
|
|
||
|
|
|
||
|
|
def _iso(value) -> str | None:
|
||
|
|
return value.isoformat() if value is not None else None
|