agent v0.26.0: slice 10 P2 activation — POST /guest/reboot (user-triggered)

Self-scoped guest reboot (pct reboot, detached, 202) so an enrolled-into-running-
guest drive's persisted bind activates at next boot. Tests: accepted + cross-guest
403. Pairs with controller v0.49.0 pending-drive detection + "Újraindítás most".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 17:19:12 +02:00
parent 7336a87514
commit bb1692cbfb
6 changed files with 112 additions and 2 deletions
+43 -1
View File
@@ -5,6 +5,7 @@ import (
"net/http"
"strconv"
"strings"
"time"
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
@@ -62,9 +63,11 @@ type GuestLister interface {
}
// GuestAttacher binds an enrolled user-data drive's felhom-data namespace into a guest as an RW bind
// mount (slice 10 P2, Model A). Satisfied by *GuestBinder. The handler picks the slot + dedups.
// mount (slice 10 P2, Model A) and reboots the guest to activate persisted-but-inactive binds (the
// host-side live inject is blocked on unprivileged guests). Satisfied by *GuestBinder.
type GuestAttacher interface {
AttachBind(ctx context.Context, vmid int, mountKey, where string) error
RebootGuest(ctx context.Context, vmid int) error
}
// ---- handlers ---------------------------------------------------------------------------
@@ -266,6 +269,45 @@ func (s *Server) handleDiskGuestAttach(w http.ResponseWriter, r *http.Request, v
writeOK(w, map[string]any{"vmid": vmid, "attached": where, "slot": slot})
}
type guestRebootRequest struct {
VMID int `json:"vmid"`
}
// handleGuestReboot reboots THIS guest (self-scoped) to activate persisted-but-inactive mountpoint
// binds (slice 10 P2 activation). It runs the reboot DETACHED and returns 202 immediately, so the
// calling controller gets a clean response before the reboot takes it (and the agent — host-side —
// survives the guest reboot). User-triggered ("Újraindítás most"); the controller batches all pending
// drives into one restart.
func (s *Server) handleGuestReboot(w http.ResponseWriter, r *http.Request, vmid int) {
if s.guestAttach == nil {
writeErr(w, http.StatusServiceUnavailable, "guest passthrough not configured on this host")
return
}
if r.ContentLength != 0 {
var req guestRebootRequest
if !decodeBody(w, r, &req) {
return
}
if !s.scopedFromBody(w, req.VMID, vmid, r.URL.Path) {
return
}
}
base := s.baseCtx
if base == nil {
base = context.Background()
}
go func() {
// Detached: pct reboot blocks ~30s until the guest is back; don't tie it to the request ctx.
rebootCtx, cancel := context.WithTimeout(base, 5*time.Minute)
defer cancel()
if err := s.guestAttach.RebootGuest(rebootCtx, vmid); err != nil {
s.logger.Error("local-api: guest-reboot failed", "vmid", vmid, "err", err)
}
}()
s.logger.Warn("local-api: guest reboot requested (activating pending drive binds)", "vmid", vmid)
writeStatus(w, http.StatusAccepted, true, map[string]any{"vmid": vmid, "rebooting": true}, "")
}
// validGuestMountPath accepts an absolute /mnt/<name> path with no traversal (the enroll convention
// root). Mirrors the controller's mount-name discipline so a hostile `where` can't escape /mnt.
func validGuestMountPath(p string) bool {