agent v0.36.2: eject keeps raw mounted (reconnectable, like decommission)
Eject now DetachDrive's the bind under the parent but leaves the raw /mnt/<name> mounted, so disconnect->reconnect re-binds on a non-removable drive. Tests updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -288,20 +288,35 @@ func TestEject_UnmountAndDependents(t *testing.T) {
|
||||
sv := fakeStorage{targets: []hub.StorageTarget{{Name: "bulk", Type: hub.StorageTypeUSB, BackingDevice: "/dev/sdb1", MountPath: "/mnt/bulk"}}}
|
||||
// guest 8200 mounts storage "bulk"; guest 9300 does not
|
||||
gl := fakeGuestList{guests: []proxmox.Guest{{VMID: 8200}, {VMID: 9300}}}
|
||||
h := newDiskServerWithGuestConfigs(t, d, sv, gl, map[int]map[string]string{
|
||||
8200: {"mp0": "bulk:200,mp=/mnt/media,backup=0"},
|
||||
9300: {"mp0": "local-lvm:8,mp=/var,backup=1"},
|
||||
ga := &fakeGuestAttacher{}
|
||||
srv, err := NewServer(Options{
|
||||
ListenAddr: "127.0.0.1:0", Guests: &fakeGuestsCfg{mounts: map[int]map[string]string{
|
||||
8200: {"mp0": "bulk:200,mp=/mnt/media,backup=0"},
|
||||
9300: {"mp0": "local-lvm:8,mp=/var,backup=1"},
|
||||
}}, Backups: &fakeBackups{}, Store: &fakeStore{}, Storage: sv,
|
||||
Tokens: staticTokens{"A": 8200, "B": 9300}, Disks: d, DiskGate: &fakeGate{}, Guests2: gl,
|
||||
GuestAttach: ga, HostReader: sysOnSDA(), Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv.baseCtx = context.Background()
|
||||
h := srv.Handler()
|
||||
|
||||
w := do(t, h, "POST", "/disks/eject", "A", `{"where":"/mnt/bulk"}`)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("eject: got %d (%s)", w.Code, w.Body.String())
|
||||
}
|
||||
// Intermediary model: eject DETACHES the bind under the parent but does NOT unmount the raw drive
|
||||
// (so a reconnect re-binds it). COMPANION GUARD: the pre-fix eject called Unmount → orphaned the drive.
|
||||
if len(ga.detachDrives) != 1 || ga.detachDrives[0] != "/mnt/bulk" {
|
||||
t.Fatalf("DetachDrive not called for /mnt/bulk: %v", ga.detachDrives)
|
||||
}
|
||||
d.mu.Lock()
|
||||
unmounts := append([]string(nil), d.unmountCalls...)
|
||||
d.mu.Unlock()
|
||||
if len(unmounts) != 1 || unmounts[0] != "/mnt/bulk" {
|
||||
t.Fatalf("Unmount not called: %v", unmounts)
|
||||
if len(unmounts) != 0 {
|
||||
t.Fatalf("raw drive must NOT be unmounted on eject (reconnectable); got %v", unmounts)
|
||||
}
|
||||
var resp struct {
|
||||
Data struct {
|
||||
@@ -357,15 +372,16 @@ func TestEject_RoleGated(t *testing.T) {
|
||||
}
|
||||
d2.mu.Unlock()
|
||||
|
||||
// user-data mount → ejects (Unmount called once).
|
||||
// user-data mount → PERMITTED (200, not 403). The bind-detach happens via DetachDrive (asserted in
|
||||
// TestEject_UnmountAndDependents); the role-gate's job here is to NOT refuse it + NOT unmount the raw.
|
||||
d3 := &fakeDiskOps{}
|
||||
h3 := newDiskServer(t, d3, &fakeGate{}, sv, nil)
|
||||
if w := do(t, h3, "POST", "/disks/eject", "A", `{"where":"/mnt/bulk"}`); w.Code != http.StatusOK {
|
||||
t.Fatalf("eject user-data mount: got %d want 200 (%s)", w.Code, w.Body.String())
|
||||
}
|
||||
d3.mu.Lock()
|
||||
if len(d3.unmountCalls) != 1 || d3.unmountCalls[0] != "/mnt/bulk" {
|
||||
t.Fatalf("user-data eject did not Unmount /mnt/bulk: %v", d3.unmountCalls)
|
||||
if len(d3.unmountCalls) != 0 {
|
||||
t.Fatalf("user-data eject must NOT unmount the raw drive (reconnectable): %v", d3.unmountCalls)
|
||||
}
|
||||
d3.mu.Unlock()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user