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