Browse Source

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.

  SSH key pairing is automatic: Bambuddy generates an ED25519 keypair and
  returns the public key in the registration response. The daemon deploys
  it to authorized_keys on first connect — no manual setup needed.

  - New: backend/app/services/spoolbuddy_ssh.py (keypair, SSH commands, update orchestration)
  - Rewritten: trigger_daemon_update endpoint uses SSH instead of pending_command
  - New: GET /spoolbuddy/ssh/public-key endpoint for manual pairing
  - Removed: daemon _perform_update() and cmd=="update" heartbeat handler
  - Updated: install.sh — bash shell, sudoers for systemctl restart, .ssh/ setup
  - Updated: Dockerfile — added openssh-client
  - Updated: frontend — SSH key display, force update button
  - Fixed: update check now compares against APP_VERSION, not GitHub releases
maziggy 2 months ago
parent
commit
5e9481859b

+ 1 - 1
CHANGELOG.md

@@ -6,7 +6,7 @@ All notable changes to Bambuddy will be documented in this file.
 
 
 ### Fixed
 ### Fixed
 - **SpoolBuddy Update Check Always Shows "Up to Date"** — The SpoolBuddy daemon update check compared the device's firmware version against GitHub releases instead of the running Bambuddy backend version. This meant the check could incorrectly report "up to date" even when the daemon was behind. Fixed by comparing directly against `APP_VERSION` from the backend config.
 - **SpoolBuddy Update Check Always Shows "Up to Date"** — The SpoolBuddy daemon update check compared the device's firmware version against GitHub releases instead of the running Bambuddy backend version. This meant the check could incorrectly report "up to date" even when the daemon was behind. Fixed by comparing directly against `APP_VERSION` from the backend config.
-- **SpoolBuddy Updates Now Use SSH** — Replaced the fragile self-update mechanism (daemon pulls its own code via git, permission errors on `.git/`, hardcoded `main` branch) with SSH-based updates driven by the Bambuddy backend. Bambuddy now SSHes into the SpoolBuddy Pi and runs git fetch/checkout, pip install, and systemctl restart remotely. Updates automatically use the same branch as Bambuddy. The install script now creates the `spoolbuddy` user with SSH access and a narrow sudoers entry for service restart only. A "Force Update" button allows re-deploying even when versions match. The SSH public key is shown in SpoolBuddy Settings for easy pairing.
+- **SpoolBuddy Updates Now Use SSH** — Replaced the fragile self-update mechanism (daemon pulls its own code via git, permission errors on `.git/`, hardcoded `main` branch) with SSH-based updates driven by the Bambuddy backend. Bambuddy now SSHes into the SpoolBuddy Pi and runs git fetch/checkout, pip install, and systemctl restart remotely. Updates automatically use the same branch as Bambuddy. SSH key pairing is fully automatic — Bambuddy generates an ED25519 keypair and includes the public key in the device registration response; the daemon deploys it to `authorized_keys` on first connect. The install script creates the `spoolbuddy` user with a bash shell and a narrow sudoers entry for service restart only. A "Force Update" button allows re-deploying even when versions match. The SSH public key is also shown in SpoolBuddy Settings → Updates → SSH Setup for manual pairing if needed.
 - **Virtual Printer Proxy A1 Printing Fails** ([#757](https://github.com/maziggy/bambuddy/issues/757)) — BambuStudio could not send prints to A1 (and potentially P1S) virtual printers in proxy mode. The slicer connects to undocumented proprietary ports 2024-2026 on these models, which the proxy was not forwarding, causing BambuStudio to show an access code dialog instead of printing. Added transparent TCP pass-through proxying for ports 2024-2026. These ports are silently ignored on models that don't use them (X1C, H2C, P2S). Reported by @Utility9298.
 - **Virtual Printer Proxy A1 Printing Fails** ([#757](https://github.com/maziggy/bambuddy/issues/757)) — BambuStudio could not send prints to A1 (and potentially P1S) virtual printers in proxy mode. The slicer connects to undocumented proprietary ports 2024-2026 on these models, which the proxy was not forwarding, causing BambuStudio to show an access code dialog instead of printing. Added transparent TCP pass-through proxying for ports 2024-2026. These ports are silently ignored on models that don't use them (X1C, H2C, P2S). Reported by @Utility9298.
 - **Spool Assignment on Empty AMS Slots** ([#784](https://github.com/maziggy/bambuddy/issues/784)) — Empty AMS slots (no physical spool detected) showed "Assign Spool" and "Configure" buttons in the hover popup. Assigning a spool to an empty slot created a stuck state because no "Unassign" button is available for empty slots. Removed both buttons from empty AMS and HT AMS slot popups. External spool holders are unaffected. Reported by @RosdasHH.
 - **Spool Assignment on Empty AMS Slots** ([#784](https://github.com/maziggy/bambuddy/issues/784)) — Empty AMS slots (no physical spool detected) showed "Assign Spool" and "Configure" buttons in the hover popup. Assigning a spool to an empty slot created a stuck state because no "Unassign" button is available for empty slots. Removed both buttons from empty AMS and HT AMS slot popups. External spool holders are unaffected. Reported by @RosdasHH.
 
 

+ 11 - 1
backend/app/api/routes/spoolbuddy.py

@@ -131,7 +131,17 @@ async def register_device(
         }
         }
     )
     )
 
 
-    return _device_to_response(device)
+    response = _device_to_response(device)
+
+    # Include SSH public key so the daemon can auto-deploy it
+    try:
+        from backend.app.services.spoolbuddy_ssh import get_public_key
+
+        response.ssh_public_key = await get_public_key()
+    except Exception:
+        pass  # Key not generated yet — daemon can still work without it
+
+    return response
 
 
 
 
 @router.get("/devices", response_model=list[DeviceResponse])
 @router.get("/devices", response_model=list[DeviceResponse])

+ 1 - 0
backend/app/schemas/spoolbuddy.py

@@ -43,6 +43,7 @@ class DeviceResponse(BaseModel):
     update_status: str | None = None
     update_status: str | None = None
     update_message: str | None = None
     update_message: str | None = None
     online: bool = False
     online: bool = False
+    ssh_public_key: str | None = None
     created_at: datetime
     created_at: datetime
     updated_at: datetime
     updated_at: datetime
 
 

+ 29 - 0
spoolbuddy/daemon/main.py

@@ -37,6 +37,30 @@ def _get_ip() -> str:
         return "unknown"
         return "unknown"
 
 
 
 
+def _deploy_ssh_key(public_key: str) -> None:
+    """Write Bambuddy's SSH public key to authorized_keys if not already present."""
+    home = Path.home()
+    ssh_dir = home / ".ssh"
+    auth_keys = ssh_dir / "authorized_keys"
+
+    try:
+        ssh_dir.mkdir(mode=0o700, exist_ok=True)
+
+        # Check if key already deployed
+        if auth_keys.exists():
+            existing = auth_keys.read_text()
+            if public_key.strip() in existing:
+                return
+
+        # Append key
+        with auth_keys.open("a") as f:
+            f.write(public_key.strip() + "\n")
+        auth_keys.chmod(0o600)
+        logger.info("SSH public key deployed to %s", auth_keys)
+    except Exception as e:
+        logger.warning("Failed to deploy SSH key: %s", e)
+
+
 async def nfc_poll_loop(config: Config, api: APIClient, shared: dict):
 async def nfc_poll_loop(config: Config, api: APIClient, shared: dict):
     """Continuous NFC polling loop — runs in asyncio with blocking reads offloaded."""
     """Continuous NFC polling loop — runs in asyncio with blocking reads offloaded."""
     nfc: NFCReader = shared["nfc"]
     nfc: NFCReader = shared["nfc"]
@@ -226,6 +250,11 @@ async def main():
         config.calibration_factor = reg.get("calibration_factor", config.calibration_factor)
         config.calibration_factor = reg.get("calibration_factor", config.calibration_factor)
         scale.update_calibration(config.tare_offset, config.calibration_factor)
         scale.update_calibration(config.tare_offset, config.calibration_factor)
 
 
+        # Auto-deploy Bambuddy's SSH public key for remote updates
+        ssh_key = reg.get("ssh_public_key")
+        if ssh_key:
+            _deploy_ssh_key(ssh_key)
+
     logger.info("Device registered, starting poll loops")
     logger.info("Device registered, starting poll loops")
 
 
     shared: dict = {"nfc": nfc, "scale": scale, "display": display}
     shared: dict = {"nfc": nfc, "scale": scale, "display": display}

+ 6 - 23
spoolbuddy/install/install.sh

@@ -435,34 +435,17 @@ setup_ssh_key() {
     mkdir -p "$ssh_dir"
     mkdir -p "$ssh_dir"
     chmod 700 "$ssh_dir"
     chmod 700 "$ssh_dir"
 
 
-    # Get the public key from flag, prompt, or skip
-    local pubkey="$SSH_PUBKEY"
-
-    if [[ -z "$pubkey" && "$NON_INTERACTIVE" != "true" ]]; then
-        echo ""
-        echo -e "${BOLD}SSH Public Key for Remote Updates${NC}"
-        echo ""
-        echo "  Bambuddy can update SpoolBuddy remotely over SSH."
-        echo "  To enable this, paste the SSH public key from:"
-        echo "    Bambuddy → SpoolBuddy Settings → SSH Setup"
-        echo ""
-        echo "  (You can also set this up later by re-running the installer"
-        echo "   with --ssh-pubkey or by manually adding the key)"
-        echo ""
-        read -rp "  Paste SSH public key (or press Enter to skip): " pubkey
-    fi
-
-    if [[ -n "$pubkey" ]]; then
-        # Append key if not already present
-        if [[ -f "$auth_keys" ]] && grep -qF "$pubkey" "$auth_keys" 2>/dev/null; then
+    if [[ -n "$SSH_PUBKEY" ]]; then
+        # Manual key provided via --ssh-pubkey flag
+        if [[ -f "$auth_keys" ]] && grep -qF "$SSH_PUBKEY" "$auth_keys" 2>/dev/null; then
             info "SSH key already present in authorized_keys"
             info "SSH key already present in authorized_keys"
         else
         else
-            echo "$pubkey" >> "$auth_keys"
+            echo "$SSH_PUBKEY" >> "$auth_keys"
             success "SSH public key added"
             success "SSH public key added"
         fi
         fi
     else
     else
-        info "No SSH key provided, skipping (can be added later)"
-        # Ensure the file exists even if empty
+        # No manual key — the daemon will auto-deploy it on first registration
+        info "SSH key will be deployed automatically when the daemon connects to Bambuddy"
         touch "$auth_keys"
         touch "$auth_keys"
     fi
     fi