703db166e7
gates / gates (push) Successful in 8s
After a rebuild the customer's own drives could not be re-attached: candidates returned initialize:[] attach:[] while both drives sat there, and the deploy refused with 'choose an attached drive from the list' — a list that was empty. Measured live three times. Mechanism: enrolment mounts a drive TWICE, at /mnt/felhom-drives/<name> and at the raw /mnt/<name> it creates on the host. The host survives a guest rebuild; the controller's registry does not. So classifyClaim saw a mount outside the managed prefix and concluded 'claimed by something else' — about our own mount. The fix is CORROBORATED, not a widened prefix: a non-managed mountpoint is forgiven only when the SAME device is also mounted under the managed path, a pairing only our enrolment produces. A disk another system uses — /srv/data, /media/x, even /mnt/someone-elses-disk — has no counterpart and is STILL refused, with its own test and a red-proof showing an over-wide fix offering it for formatting. Read from /proc/mounts deliberately: the lsblk invocation is pinned verbatim in configs/felhom-agent.sudoers, so using the plural MOUNTPOINTS would have coupled this to a sudoers rollout. /proc/mounts is world-readable — no sudo, no new allowlisted command, no config change. Fail-safe: an unreadable mount table corroborates NOTHING, so the device classifies exactly as before. 'Could not corroborate' must never read as 'ours'. 29 packages ok, vet clean, agent gates OK.
103 lines
4.3 KiB
Go
103 lines
4.3 KiB
Go
package storage
|
|
|
|
import "testing"
|
|
|
|
// ── R-220 — A MOUNT FELHOM ITSELF MADE IS NOT "SOMETHING ELSE" ──────────────────────────────────
|
|
//
|
|
// Enrolment mounts a drive twice: at `/mnt/felhom-drives/<name>` and at the raw `/mnt/<name>` it
|
|
// creates on the host. The host survives a guest rebuild; the controller's registry does not. So after
|
|
// a rebuild the customer's own drives read as claimed-by-something-else, `attach` came back empty, and
|
|
// the refusal told them to pick from the empty list. Measured three times live.
|
|
//
|
|
// The fence: a disk genuinely in use elsewhere must STILL be refused. These assert both directions.
|
|
|
|
// ── SCENARIO E — the customer's own drive is offered again after a rebuild ───────────────────────
|
|
//
|
|
// RED-PROOF: drop `&& !f.felhomOwnedMounts[n.mountpoint]` from classifyClaim — the pre-R-220 check —
|
|
// and this FAILS with the drive refused and the list empty again.
|
|
func TestClassifyClaim_R220_FelhomsOwnRawMountIsNotForeign(t *testing.T) {
|
|
f := claimFacts{
|
|
device: "/dev/sdb", wholeDisk: "/dev/sdb", wholeDiskOK: true,
|
|
nodes: []claimNode{{name: "sdb", fstype: "ext4", mountpoint: "/mnt/adatok"}},
|
|
// corroborated: the SAME device is also mounted at the managed path
|
|
felhomOwnedMounts: map[string]bool{"/mnt/adatok": true},
|
|
}
|
|
unclaimed, reason := classifyClaim(f)
|
|
if !unclaimed {
|
|
t.Fatalf("R-220 RETURNED: the customer's own drive is refused after a rebuild — %q", reason)
|
|
}
|
|
}
|
|
|
|
// ── SCENARIO F — a genuinely foreign mount is STILL refused ──────────────────────────────────────
|
|
//
|
|
// RED-PROOF: over-widen the fix to exempt any /mnt/* path (or to skip the mountpoint check entirely)
|
|
// and this FAILS — a disk another system is using would be offered for formatting.
|
|
func TestClassifyClaim_R220_ForeignMountIsStillRefused(t *testing.T) {
|
|
for _, mp := range []string{"/srv/data", "/media/photos", "/mnt/someone-elses-disk", "/var/lib/other"} {
|
|
f := claimFacts{
|
|
device: "/dev/sdb", wholeDisk: "/dev/sdb", wholeDiskOK: true,
|
|
nodes: []claimNode{{name: "sdb", fstype: "ext4", mountpoint: mp}},
|
|
felhomOwnedMounts: nil, // nothing corroborated it as ours
|
|
}
|
|
unclaimed, reason := classifyClaim(f)
|
|
if unclaimed {
|
|
t.Fatalf("THE FENCE BROKE: a disk mounted at %s was offered for formatting", mp)
|
|
}
|
|
if reason == "" {
|
|
t.Fatalf("a refusal must carry a reason (%s)", mp)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The corroboration itself: it must require BOTH mounts of the SAME device, and fail safe.
|
|
func TestFelhomOwnedMounts_RequiresTheManagedCounterpart(t *testing.T) {
|
|
nodes := []claimNode{{name: "sdb"}}
|
|
|
|
t.Run("both mounts present -> the raw one is ours", func(t *testing.T) {
|
|
src := func() ([][2]string, error) {
|
|
return [][2]string{
|
|
{"/dev/sdb", "/mnt/adatok"},
|
|
{"/dev/sdb", "/mnt/felhom-drives/adatok"},
|
|
}, nil
|
|
}
|
|
got := felhomOwnedMounts("/dev/sdb", nodes, src)
|
|
if !got["/mnt/adatok"] {
|
|
t.Fatal("the raw enrolment mount was not recognised as Felhom's own")
|
|
}
|
|
})
|
|
|
|
t.Run("only the raw mount -> corroborates NOTHING", func(t *testing.T) {
|
|
src := func() ([][2]string, error) {
|
|
return [][2]string{{"/dev/sdb", "/mnt/adatok"}}, nil
|
|
}
|
|
if got := felhomOwnedMounts("/dev/sdb", nodes, src); len(got) != 0 {
|
|
t.Fatalf("a lone /mnt/<name> mount must corroborate nothing, got %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("a DIFFERENT device under the managed path does not vouch for this one", func(t *testing.T) {
|
|
src := func() ([][2]string, error) {
|
|
return [][2]string{
|
|
{"/dev/sdb", "/srv/data"},
|
|
{"/dev/sdc", "/mnt/felhom-drives/mentes"}, // someone else's, not sdb's
|
|
}, nil
|
|
}
|
|
if got := felhomOwnedMounts("/dev/sdb", nodes, src); got["/srv/data"] {
|
|
t.Fatal("another device's managed mount vouched for a foreign one")
|
|
}
|
|
})
|
|
|
|
t.Run("an unreadable mount table corroborates NOTHING (fail-safe)", func(t *testing.T) {
|
|
src := func() ([][2]string, error) { return nil, errRead }
|
|
if got := felhomOwnedMounts("/dev/sdb", nodes, src); len(got) != 0 {
|
|
t.Fatalf("an unreadable mount table must corroborate nothing, got %v", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
var errRead = errNoMountTable{}
|
|
|
|
type errNoMountTable struct{}
|
|
|
|
func (errNoMountTable) Error() string { return "mount table unreadable" }
|