v0.318.0: the internal data place, partitions, an honest whole-box backup offer (R-1084), sys_drive never offered (R-1082)
gates / gates (push) Successful in 46s
gates / gates (push) Successful in 46s
felhom.eu architecture/16 §4-§6 (build B). MinAgent 0.157.0 — the internal place and the partitions appear only when agent >= 0.158.0 reports them. Tests red-proved; four parity fixtures refreshed (only the new lines). Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
)
|
||||
|
||||
// Partitions as places (felhom.eu architecture/16 §6, v0.318.0): the free part of a disk whose other part is a
|
||||
// registered drive stays on offer; the whole disk never does — in the list AND on the server.
|
||||
|
||||
const partMounts = `/dev/mapper/pve-root /mnt/felhom-drives ext4 rw,relatime 0 0
|
||||
/dev/sdb1 /mnt/felhom-drives/hdd_1 ext4 rw,relatime 0 0
|
||||
/dev/mapper/pve-vm--9201--disk--1 /mnt/sys_drive ext4 rw,relatime 0 0
|
||||
`
|
||||
|
||||
func diskWithParts() agentapi.DiskCandidate {
|
||||
return agentapi.DiskCandidate{Device: "/dev/sdb", MountSource: "/dev/sdb1", FSType: "ext4", DataBearing: true,
|
||||
Partitions: []agentapi.CandidatePartition{
|
||||
{Device: "/dev/sdb1", Number: 1, FSType: "ext4", State: agentapi.PartInUse, MountedAt: "/mnt/felhom-drives/hdd_1"},
|
||||
{Device: "/dev/sdb2", Number: 2, State: agentapi.PartEmpty},
|
||||
{Device: "/dev/sdb3", Number: 3, FSType: "ext4", State: agentapi.PartFilesystem, Mountable: true, DurableID: "uuid:3333"},
|
||||
}}
|
||||
}
|
||||
|
||||
func TestPartitions_FreePartsOfAnInUseDiskStayOffered(t *testing.T) {
|
||||
reg := map[string]bool{"/mnt/felhom-drives/hdd_1": true}
|
||||
got := dropRegisteredDrives(agentapi.CandidatesResult{Initialize: []agentapi.DiskCandidate{diskWithParts()}}, partMounts, reg)
|
||||
if len(got.Initialize) != 1 {
|
||||
t.Fatalf("the disk must stay listed for its free parts, got %+v", got.Initialize)
|
||||
}
|
||||
c := got.Initialize[0]
|
||||
if !c.WholeDiskInUse {
|
||||
t.Fatalf("the whole disk must be marked in use (no whole-disk action): %+v", c)
|
||||
}
|
||||
if len(c.Partitions) != 2 || c.Partitions[0].Device != "/dev/sdb2" || c.Partitions[1].Device != "/dev/sdb3" {
|
||||
t.Fatalf("only the free parts may be offered, got %+v", c.Partitions)
|
||||
}
|
||||
}
|
||||
|
||||
// R-542 unchanged: a disk with nothing free is still dropped.
|
||||
func TestPartitions_InUseDiskWithNoFreePartIsDropped(t *testing.T) {
|
||||
d := diskWithParts()
|
||||
d.Partitions = d.Partitions[:1]
|
||||
got := dropRegisteredDrives(agentapi.CandidatesResult{Initialize: []agentapi.DiskCandidate{d}}, partMounts, map[string]bool{"/mnt/felhom-drives/hdd_1": true})
|
||||
if len(got.Initialize) != 0 {
|
||||
t.Fatalf("a disk with no free part must not be offered, got %+v", got.Initialize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartitions_ServerRefusesTheWholeDiskButNotTheFreePart(t *testing.T) {
|
||||
reg := map[string]bool{"/mnt/felhom-drives/hdd_1": true}
|
||||
for dev, want := range map[string]bool{"/dev/sdb": true, "/dev/sdb1": true, "/dev/sdb2": false, "/dev/sdc": false} {
|
||||
if got := deviceBacksRegistered(dev, partMounts, reg); got != want {
|
||||
t.Errorf("%s: backs a registered drive = %v, want %v", dev, got, want)
|
||||
}
|
||||
}
|
||||
if !deviceBacksRegistered("/dev/sdc", "", reg) {
|
||||
t.Errorf("an unreadable mount table must refuse (fail-safe)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartitions_AFormattedPartsUUIDIsFound(t *testing.T) {
|
||||
ag := &mockAgent{candidates: agentapi.CandidatesResult{Initialize: []agentapi.DiskCandidate{diskWithParts()}}}
|
||||
if got := resolveEnrollUUID(context.Background(), ag, "/dev/sdb3"); got != "3333" {
|
||||
t.Fatalf("the part's new filesystem must be found for the mount, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The handler asks before it starts anything: the whole disk under a registered part → 409, never a format job.
|
||||
// Positive control: the free part passes the guard and reaches the agent step (503 here: no agent configured).
|
||||
func TestPartitions_InitHandlerRefusesTheWholeDisk(t *testing.T) {
|
||||
s := testServer(t)
|
||||
s.mountsFn = func() string { return partMounts }
|
||||
if err := s.settings.AddStoragePath(settingsPath("/mnt/felhom-drives/hdd_1")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
post := func(dev string) int {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/api/storage/init", strings.NewReader(`{"device":"`+dev+`","fstype":"ext4","mount_name":"films"}`))
|
||||
s.handleStorageInit(w, r)
|
||||
return w.Code
|
||||
}
|
||||
if c := post("/dev/sdb"); c != http.StatusConflict {
|
||||
t.Fatalf("the whole disk under a registered part: want 409, got %d", c)
|
||||
}
|
||||
if c := post("/dev/sdb2"); c == http.StatusConflict {
|
||||
t.Fatalf("positive control: the free part must pass the guard, got %d", c)
|
||||
}
|
||||
}
|
||||
|
||||
func settingsPath(p string) settings.StoragePath {
|
||||
return settings.StoragePath{Path: p, Schedulable: true}
|
||||
}
|
||||
Reference in New Issue
Block a user