Replace SpoolBuddy self-update with SSH-based updates from Bambuddy

The daemon's self-update mechanism (git fetch/reset on its own code) was
  fragile: .git permission errors, self-modifying code mid-run, hardcoded
  main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
  update remotely — matching its own branch, with step-by-step progress
  via WebSocket. Install script updated with SSH access, sudoers entry,
  and --ssh-pubkey flag for pairing.
This commit is contained in:
maziggy
2026-03-23 12:51:21 +01:00
parent 511ece791a
commit bc1bba078b
12 changed files with 454 additions and 187 deletions
+29 -5
View File
@@ -1,5 +1,6 @@
"""SpoolBuddy device management API routes."""
import asyncio
import logging
from datetime import datetime, timedelta, timezone
@@ -597,10 +598,17 @@ async def check_daemon_update(
@router.post("/devices/{device_id}/update")
async def trigger_daemon_update(
device_id: str,
req: dict | None = None,
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
):
"""Trigger a daemon update on the SpoolBuddy device via pending_command."""
"""Trigger a SpoolBuddy update over SSH.
Bambuddy SSHes into the device, pulls the matching branch, installs deps,
and restarts the daemon. Progress is broadcast via WebSocket.
"""
from backend.app.services.spoolbuddy_ssh import perform_ssh_update
result = await db.execute(select(SpoolBuddyDevice).where(SpoolBuddyDevice.device_id == device_id))
device = result.scalar_one_or_none()
if not device:
@@ -612,12 +620,11 @@ async def trigger_daemon_update(
if device.update_status == "updating":
return {"status": "already_updating", "message": "Update already in progress"}
device.pending_command = "update"
device.update_status = "pending"
device.update_message = "Waiting for device to pick up update command..."
device.update_message = "Starting SSH update..."
await db.commit()
logger.info("SpoolBuddy %s: update command queued", device_id)
logger.info("SpoolBuddy %s: SSH update triggered (ip=%s)", device_id, device.ip_address)
await ws_manager.broadcast(
{
"type": "spoolbuddy_update",
@@ -626,7 +633,24 @@ async def trigger_daemon_update(
}
)
return {"status": "ok", "message": "Update command sent to device"}
# Run the SSH update in the background
asyncio.create_task(perform_ssh_update(device_id, device.ip_address))
return {"status": "ok", "message": "SSH update started"}
@router.get("/ssh/public-key")
async def get_ssh_public_key(
_: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ),
):
"""Return the SSH public key for SpoolBuddy pairing."""
from backend.app.services.spoolbuddy_ssh import get_public_key
try:
key = await get_public_key()
return {"public_key": key}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get SSH key: {e}") from e
@router.post("/devices/{device_id}/update-status")