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
+40
View File
@@ -9,6 +9,7 @@ import (
"strings"
"sync"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
@@ -380,6 +381,7 @@ type fakeGuestAttacher struct {
vmid int
slot, where string
}
reboots []int
}
func (f *fakeGuestAttacher) AttachBind(_ context.Context, vmid int, mountKey, where string) error {
@@ -393,6 +395,14 @@ func (f *fakeGuestAttacher) AttachBind(_ context.Context, vmid int, mountKey, wh
}
func (f *fakeGuestAttacher) count() int { f.mu.Lock(); defer f.mu.Unlock(); return len(f.calls) }
func (f *fakeGuestAttacher) RebootGuest(_ context.Context, vmid int) error {
f.mu.Lock()
defer f.mu.Unlock()
f.reboots = append(f.reboots, vmid)
return nil
}
func (f *fakeGuestAttacher) rebootCount() int { f.mu.Lock(); defer f.mu.Unlock(); return len(f.reboots) }
func newAttachServer(t *testing.T, ga GuestAttacher, mounts map[int]map[string]string) http.Handler {
t.Helper()
srv, err := NewServer(Options{
@@ -462,6 +472,36 @@ func TestGuestAttach_NotConfigured(t *testing.T) {
}
}
// Guest reboot (activation) returns 202 and triggers RebootGuest for the token's vmid (detached).
func TestGuestReboot_Accepted(t *testing.T) {
ga := &fakeGuestAttacher{}
h := newAttachServer(t, ga, map[int]map[string]string{8200: {}})
w := do(t, h, "POST", "/guest/reboot", "A", "")
if w.Code != http.StatusAccepted {
t.Fatalf("reboot: got %d want 202 (%s)", w.Code, w.Body.String())
}
// The reboot runs in a goroutine; give it a moment to record the call.
for i := 0; i < 100 && ga.rebootCount() == 0; i++ {
time.Sleep(time.Millisecond)
}
if ga.rebootCount() != 1 || ga.reboots[0] != 8200 {
t.Fatalf("RebootGuest not invoked for vmid 8200: %+v", ga.reboots)
}
}
// A cross-guest reboot (body vmid != token's) is refused 403, no reboot.
func TestGuestReboot_CrossGuest403(t *testing.T) {
ga := &fakeGuestAttacher{}
h := newAttachServer(t, ga, map[int]map[string]string{8200: {}})
if w := do(t, h, "POST", "/guest/reboot", "A", `{"vmid":9300}`); w.Code != http.StatusForbidden {
t.Fatalf("cross-guest reboot: got %d want 403", w.Code)
}
time.Sleep(5 * time.Millisecond)
if ga.rebootCount() != 0 {
t.Fatal("reboot triggered for a cross-guest request")
}
}
// ---- auth / config ----------------------------------------------------------------------
func TestDisks_CrossGuest403(t *testing.T) {