agent v0.34.0: intermediary mount model — shared parent + host-side attach/detach + reconcile
Replaces the per-drive 'pct set -mpN' bind with ONE permanent parent bind /mnt/felhom-drives plus host-side felhom-data swaps underneath it (propagates into the running guest live, no pct, no reboot; C1-immune; confined; fail-closed when absent). EnsureSharedParent installs a boot unit ordered Before=pve-guests. ReassertGuestBinds is now a pure host-side reconcile. /disks reports GuestPath + BoundUnderParent for the controller repoint+gate. Non-hollow tests + companions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package localapi
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -394,6 +395,10 @@ type fakeGuestAttacher struct {
|
||||
vmid int
|
||||
slot string
|
||||
}
|
||||
attachDrives []string // where passed to AttachDrive (intermediary model)
|
||||
detachDrives []string // where passed to DetachDrive
|
||||
ensureParentN int // EnsureSharedParent call count
|
||||
attachDriveFail bool // when set, AttachDrive returns an error
|
||||
}
|
||||
|
||||
func (f *fakeGuestAttacher) AttachBind(_ context.Context, vmid int, mountKey, where string) error {
|
||||
@@ -407,6 +412,35 @@ 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) AttachDrive(_ context.Context, where string) (string, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.attachDriveFail {
|
||||
return "", fmt.Errorf("fake attach-drive failure")
|
||||
}
|
||||
f.attachDrives = append(f.attachDrives, where)
|
||||
return StablePathForRaw(where), nil
|
||||
}
|
||||
func (f *fakeGuestAttacher) attachDriveCount() int {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return len(f.attachDrives)
|
||||
}
|
||||
|
||||
func (f *fakeGuestAttacher) DetachDrive(_ context.Context, where string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.detachDrives = append(f.detachDrives, where)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeGuestAttacher) EnsureSharedParent(_ context.Context) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.ensureParentN++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeGuestAttacher) DetachBind(_ context.Context, vmid int, mountKey string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
@@ -444,8 +478,12 @@ func newAttachServer(t *testing.T, ga GuestAttacher, mounts map[int]map[string]s
|
||||
return srv.Handler()
|
||||
}
|
||||
|
||||
// A first attach picks the lowest free slot (mp0; mp9 bootstrap is taken) and calls the binder.
|
||||
func TestGuestAttach_PicksFreeSlotAndBinds(t *testing.T) {
|
||||
// Intermediary model: a guest-attach binds the drive's felhom-data under the shared parent (host-side)
|
||||
// and returns its STABLE guest path — no pct slot. EnsureSharedParent runs first.
|
||||
//
|
||||
// COMPANION GUARD: the legacy `pct set -mpN` AttachBind must NOT be used in the intermediary model — a
|
||||
// pre-fix impl that still called AttachBind would trip ga.count()!=0 here.
|
||||
func TestGuestAttach_BindsUnderParent(t *testing.T) {
|
||||
ga := &fakeGuestAttacher{}
|
||||
h := newAttachServer(t, ga, map[int]map[string]string{
|
||||
8200: {"mp9": "/var/lib/.../bootstrap,mp=/etc/felhom-bootstrap,ro=1"},
|
||||
@@ -454,26 +492,27 @@ func TestGuestAttach_PicksFreeSlotAndBinds(t *testing.T) {
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("attach: got %d want 200 (%s)", w.Code, w.Body.String())
|
||||
}
|
||||
if ga.count() != 1 || ga.calls[0].slot != "mp0" || ga.calls[0].where != "/mnt/felhom-usb" || ga.calls[0].vmid != 8200 {
|
||||
t.Fatalf("AttachBind not called with mp0/where/vmid: %+v", ga.calls)
|
||||
if ga.attachDriveCount() != 1 || ga.attachDrives[0] != "/mnt/felhom-usb" {
|
||||
t.Fatalf("AttachDrive not called with /mnt/felhom-usb: %+v", ga.attachDrives)
|
||||
}
|
||||
if ga.ensureParentN < 1 {
|
||||
t.Fatalf("EnsureSharedParent must run before binding the drive")
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), `"guest_path":"/mnt/felhom-drives/felhom-usb"`) {
|
||||
t.Fatalf("response missing the stable guest_path: %s", w.Body.String())
|
||||
}
|
||||
if ga.count() != 0 {
|
||||
t.Fatalf("legacy pct AttachBind called (%d) — intermediary model must use host-side AttachDrive", ga.count())
|
||||
}
|
||||
}
|
||||
|
||||
// An already-bound drive is idempotent: returns the existing slot, binder NOT called again.
|
||||
func TestGuestAttach_Idempotent(t *testing.T) {
|
||||
ga := &fakeGuestAttacher{}
|
||||
h := newAttachServer(t, ga, map[int]map[string]string{
|
||||
8200: {"mp0": "/mnt/felhom-usb/felhom-data,mp=/mnt/felhom-usb"},
|
||||
})
|
||||
// An AttachDrive failure surfaces as 502 (no silent success).
|
||||
func TestGuestAttach_AttachDriveFailure(t *testing.T) {
|
||||
ga := &fakeGuestAttacher{attachDriveFail: true}
|
||||
h := newAttachServer(t, ga, map[int]map[string]string{8200: {}})
|
||||
w := do(t, h, "POST", "/disks/guest-attach", "A", `{"where":"/mnt/felhom-usb"}`)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("idempotent attach: got %d want 200 (%s)", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), `"already":true`) {
|
||||
t.Fatalf("expected already:true: %s", w.Body.String())
|
||||
}
|
||||
if ga.count() != 0 {
|
||||
t.Fatalf("AttachBind must NOT be called for an already-bound drive: %+v", ga.calls)
|
||||
if w.Code != http.StatusBadGateway {
|
||||
t.Fatalf("attach failure: got %d want 502 (%s)", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user