From 7300351bb9fb147f4de81a60423c4561a4924c21 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 21 Feb 2026 16:53:38 +0100
Subject: [PATCH] fix: Run backup/restore on host via nsenter for Python venv compatibility
---
app/routers/backups.py | 46 +++++++++++++++-------------------------------
1 files changed, 15 insertions(+), 31 deletions(-)
diff --git a/app/routers/backups.py b/app/routers/backups.py
index d0a6f81..d1cf26e 100644
--- a/app/routers/backups.py
+++ b/app/routers/backups.py
@@ -3,7 +3,7 @@
from fastapi import APIRouter, Depends, HTTPException
from app.auth import verify_token
-from app.ops_runner import run_ops, run_ops_json, _BACKUP_TIMEOUT
+from app.ops_runner import run_ops, run_ops_json, run_ops_host, _BACKUP_TIMEOUT
router = APIRouter()
@@ -12,15 +12,10 @@
async def list_backups(
_: str = Depends(verify_token),
) -> list[dict[str, Any]]:
- """
- Returns a list of local backup records from `ops backups --json`.
- """
+ """Returns a list of local backup records from `ops backups --json`."""
result = await run_ops_json(["backups"])
if not result["success"]:
- raise HTTPException(
- status_code=500,
- detail=f"Failed to list backups: {result['error']}",
- )
+ raise HTTPException(status_code=500, detail=f"Failed to list backups: {result['error']}")
data = result["data"]
if isinstance(data, list):
@@ -37,9 +32,7 @@
async def list_offsite_backups(
_: str = Depends(verify_token),
) -> list[dict[str, Any]]:
- """
- Returns a list of offsite backup records, querying each project separately.
- """
+ """Returns a list of offsite backup records."""
all_backups = []
for project in ["mdf", "seriousletter"]:
result = await run_ops_json(["offsite", "list", project])
@@ -57,9 +50,12 @@
_: str = Depends(verify_token),
) -> dict[str, Any]:
"""
- Runs `ops backup {project} {env}` and returns the result.
+ Runs `ops backup {project} {env}` on the host.
+
+ Runs via nsenter because ops backup delegates to project CLIs
+ that use host Python venvs.
"""
- result = await run_ops(["backup", project, env], timeout=_BACKUP_TIMEOUT)
+ result = await run_ops_host(["backup", project, env], timeout=_BACKUP_TIMEOUT)
if not result["success"]:
raise HTTPException(
status_code=500,
@@ -79,10 +75,8 @@
env: str,
_: str = Depends(verify_token),
) -> dict[str, Any]:
- """
- Runs `ops offsite upload {project} {env}` and returns the result.
- """
- result = await run_ops(
+ """Runs `ops offsite upload {project} {env}` on the host."""
+ result = await run_ops_host(
["offsite", "upload", project, env], timeout=_BACKUP_TIMEOUT
)
if not result["success"]:
@@ -90,28 +84,18 @@
status_code=500,
detail=f"Offsite upload failed: {result['error'] or result['output']}",
)
- return {
- "success": True,
- "output": result["output"],
- "project": project,
- "env": env,
- }
+ return {"success": True, "output": result["output"], "project": project, "env": env}
@router.post("/offsite/retention", summary="Apply offsite retention policy")
async def apply_retention(
_: str = Depends(verify_token),
) -> dict[str, Any]:
- """
- Runs `ops offsite retention` and returns the result.
- """
- result = await run_ops(["offsite", "retention"], timeout=_BACKUP_TIMEOUT)
+ """Runs `ops offsite retention` on the host."""
+ result = await run_ops_host(["offsite", "retention"], timeout=_BACKUP_TIMEOUT)
if not result["success"]:
raise HTTPException(
status_code=500,
detail=f"Retention policy failed: {result['error'] or result['output']}",
)
- return {
- "success": True,
- "output": result["output"],
- }
+ return {"success": True, "output": result["output"]}
--
Gitblit v1.3.1