3c174bc6f2
Operator ruling 2026-07-13: walk the root's backing device through /sys/block/<dev>/slaves recursively down to physical disks (dm AND md; topology, never VG names); those + any mounted-ESP holder are system; the all-system fail-safe returns to being the WALK-FAILURE error case only. SAFETY DIRECTION: a root-backing disk classified candidate is made impossible — per-branch conservatism (any unresolvable slave fails the WHOLE walk -> ok=false -> the unchanged all-system path). - physicalDisksOf/walkSlaves in role.go (symlink canon -> wholeDiskOf fast path -> recursive slaves walk; cycle/depth guard; non-/dev sources unwalkable) - HostReader.BlockSlaves(name) — the ONE new seam method; ProcHostReader reads /sys/block/<name>/slaves; all four test fakes mirror it - role_walk_test.go: signature table (root-backing disk ALWAYS system across legacy-LVM / md-raid / EFI+raw / EFI+LVM / nested dm-on-md — NEVER weaken) + dead-wizard-lives + dangling-slave fail-safe (real sysKnown=false path) + cycle + empty-slaves; red-proofs A/B/D run->fail->revert (recorded in REPORT) - §3 spike transcripts (drill legacy: dm-1->sda3->sda; felhom-pve: ESP+walk agree on sda -> byte-identical regression); caller audit: none relied on all-system as a feature - format/mkfs paths, data-bearing guards, wizard UI untouched
199 lines
8.0 KiB
Go
199 lines
8.0 KiB
Go
package storage
|
|
|
|
import "testing"
|
|
|
|
// v0.87.0 — SystemDisks device-mapper/md walk (IA finding 2, operator ruling 2026-07-13).
|
|
// The direction that must be IMPOSSIBLE is a root-backing disk classified as a candidate;
|
|
// the direction being fixed is the legacy-boot all-system over-protection (dead wizard).
|
|
//
|
|
// Fixtures mirror the §3 live transcripts: on the drill host / → /dev/mapper/pve-root →
|
|
// dm-1 → slaves sda3 → parent sda; physical disks carry an EMPTY slaves dir (hasDir=true).
|
|
// COMPANION red-proofs (run → fail → revert, recorded in REPORT):
|
|
// B: walk returns only the dm node (pre-fix shape) → TestSystemDisks_WalkTopologies legacy
|
|
// fixture fails its system-set assertion.
|
|
// A: pre-fix resolver (no walk) → TestSystemDisks_LegacyBoot_WizardLives fails
|
|
// (reproduces the dead-wizard live shape).
|
|
// D: per-branch conservatism removed (skip unresolvable slaves) → the dangling fixture
|
|
// fails on "scratch classified candidate while walk incomplete".
|
|
|
|
// walkTopology is one fixture: the host's mounts + sysfs slaves tree, with the disks that
|
|
// MUST be system and a scratch disk that must stay outside the set.
|
|
type walkTopology struct {
|
|
name string
|
|
mounts []Mount
|
|
slaves map[string][]string
|
|
wantSystem []string // every entry MUST be in the resolved set (signature assertion B)
|
|
scratch string // must NOT be in the set (wizard-eligible)
|
|
}
|
|
|
|
func walkTopologies() []walkTopology {
|
|
return []walkTopology{
|
|
{
|
|
// The drill host's exact shape (§3 transcript): legacy boot, LVM root, no ESP mount.
|
|
name: "legacy LVM root (dm -> partition -> disk)",
|
|
mounts: []Mount{{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}},
|
|
slaves: map[string][]string{
|
|
"pve-root": {"sda3"},
|
|
"sda": {}, // physical disk: slaves dir exists, empty (live-probed)
|
|
},
|
|
wantSystem: []string{"/dev/sda"},
|
|
scratch: "/dev/sdd",
|
|
},
|
|
{
|
|
// md-raid root: BOTH member disks are system.
|
|
name: "md-raid root (md -> 2 disks)",
|
|
mounts: []Mount{{Device: "/dev/md0", MountPoint: "/", FSType: "ext4"}},
|
|
slaves: map[string][]string{
|
|
"md0": {"sda1", "sdb1"},
|
|
},
|
|
wantSystem: []string{"/dev/sda", "/dev/sdb"},
|
|
scratch: "/dev/sdd",
|
|
},
|
|
{
|
|
// Plain EFI + raw partitions (the pre-walk demo shape) — no virtual layer at all.
|
|
name: "EFI + raw partitions",
|
|
mounts: []Mount{
|
|
{Device: "/dev/sda2", MountPoint: "/", FSType: "ext4"},
|
|
{Device: "/dev/sda1", MountPoint: "/boot/efi", FSType: "vfat"},
|
|
},
|
|
slaves: map[string][]string{},
|
|
wantSystem: []string{"/dev/sda"},
|
|
scratch: "/dev/sdd",
|
|
},
|
|
{
|
|
// felhom-pve's exact shape (§3 baseline): EFI mount AND LVM root — the walk and the
|
|
// ESP must agree on the same disk (scenario C: output identical pre/post).
|
|
name: "EFI + LVM root (felhom-pve)",
|
|
mounts: []Mount{
|
|
{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"},
|
|
{Device: "/dev/sda2", MountPoint: "/boot/efi", FSType: "vfat"},
|
|
},
|
|
slaves: map[string][]string{
|
|
"pve-root": {"sda3"},
|
|
},
|
|
wantSystem: []string{"/dev/sda"},
|
|
scratch: "/dev/sdd",
|
|
},
|
|
{
|
|
// Nested virtual layers: dm on md on partitions of two disks.
|
|
name: "nested dm-on-md",
|
|
mounts: []Mount{{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}},
|
|
slaves: map[string][]string{
|
|
"pve-root": {"md0"},
|
|
"md0": {"sda2", "sdb2"},
|
|
},
|
|
wantSystem: []string{"/dev/sda", "/dev/sdb"},
|
|
scratch: "/dev/sdd",
|
|
},
|
|
}
|
|
}
|
|
|
|
// TestSystemDisks_WalkTopologies is the SIGNATURE test (scenario B): in every topology the
|
|
// walk resolves, the root-backing physical disk(s) are ALWAYS in the system set. This
|
|
// assertion may never be weakened.
|
|
func TestSystemDisks_WalkTopologies(t *testing.T) {
|
|
for _, tc := range walkTopologies() {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
host := &fakeHostReader{mounts: tc.mounts, slaves: tc.slaves}
|
|
set, ok := SystemDisks(host)
|
|
if !ok {
|
|
t.Fatalf("SystemDisks must resolve this topology, got ok=false (set=%v)", set)
|
|
}
|
|
for _, d := range tc.wantSystem {
|
|
if !set[d] {
|
|
t.Errorf("SIGNATURE VIOLATION: root-backing disk %s missing from system set %v", d, set)
|
|
}
|
|
}
|
|
if set[tc.scratch] {
|
|
t.Errorf("scratch disk %s wrongly in the system set %v", tc.scratch, set)
|
|
}
|
|
// And the classification consequences: the system disk is never user-data, the
|
|
// scratch disk is never system.
|
|
if RoleForRawDevice(tc.wantSystem[0], set, ok) != RoleSystem {
|
|
t.Errorf("root-backing disk %s must classify system", tc.wantSystem[0])
|
|
}
|
|
if RoleForRawDevice(tc.scratch, set, ok) != RoleUserData {
|
|
t.Errorf("scratch disk %s must classify user-data (wizard-eligible)", tc.scratch)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSystemDisks_LegacyBoot_WizardLives (scenario A): the drill-box shape resolves — the
|
|
// scratch disk is wizard-eligible instead of the pre-fix all-system dead end.
|
|
// RED-PROOF companion: with the pre-fix resolver (wholeDiskOf only, no walk) this fails on
|
|
// ok=false — the exact dead-wizard live shape from the IA report.
|
|
func TestSystemDisks_LegacyBoot_WizardLives(t *testing.T) {
|
|
host := &fakeHostReader{
|
|
mounts: []Mount{{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}},
|
|
slaves: map[string][]string{"pve-root": {"sda3"}},
|
|
}
|
|
set, ok := SystemDisks(host)
|
|
if !ok {
|
|
t.Fatal("legacy-boot topology must resolve (the all-system fail-safe is the ERROR case, not the legacy-boot case)")
|
|
}
|
|
if !set["/dev/sda"] {
|
|
t.Fatalf("root parent disk missing: %v", set)
|
|
}
|
|
if isSystemBacked("/dev/sdd", set, ok) {
|
|
t.Fatal("hot-added scratch disk still classified system — the wizard stays dead")
|
|
}
|
|
}
|
|
|
|
// TestSystemDisks_DanglingSlave_FailSafe (scenario D): an unresolvable slave fails the WHOLE
|
|
// walk and callers see the all-system behavior through the SAME code path as today
|
|
// (sysKnown=false → isSystemBacked true for everything).
|
|
// RED-PROOF companion: treating an unresolved slave as skippable makes ok=true here → the
|
|
// scratch disk classifies candidate while the walk is incomplete → this test fails.
|
|
func TestSystemDisks_DanglingSlave_FailSafe(t *testing.T) {
|
|
host := &fakeHostReader{
|
|
mounts: []Mount{{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}},
|
|
slaves: map[string][]string{
|
|
// One branch resolves (sda3 → sda), one is DANGLING (dm-9 has no /sys/block entry).
|
|
// The resolvable branch is load-bearing for the red-proof: a "skip the unresolved
|
|
// slave" mutation would yield a plausible non-empty set — exactly the partial
|
|
// topology the conservatism rule forbids.
|
|
"pve-root": {"sda3", "dm-9"},
|
|
},
|
|
}
|
|
set, ok := SystemDisks(host)
|
|
if ok {
|
|
t.Fatalf("a dangling slave must fail the whole walk, got ok=true set=%v", set)
|
|
}
|
|
// The fail-safe path itself (not a lookalike): sysKnown=false forces system for EVERY
|
|
// device, exactly as the pre-walk legacy behavior did.
|
|
if !isSystemBacked("/dev/sdd", set, ok) {
|
|
t.Fatal("fail-safe violated: scratch disk classified candidate while the walk is incomplete")
|
|
}
|
|
if RoleForRawDevice("/dev/sdd", set, ok) != RoleSystem {
|
|
t.Fatal("fail-safe violated at role level")
|
|
}
|
|
}
|
|
|
|
// TestSystemDisks_CycleGuard: a slaves cycle (corrupt sysfs / hostile fixture) terminates and
|
|
// fails safe instead of recursing forever.
|
|
func TestSystemDisks_CycleGuard(t *testing.T) {
|
|
host := &fakeHostReader{
|
|
mounts: []Mount{{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}},
|
|
slaves: map[string][]string{
|
|
"pve-root": {"dm-1"},
|
|
"dm-1": {"pve-root"},
|
|
},
|
|
}
|
|
if _, ok := SystemDisks(host); ok {
|
|
t.Fatal("a slaves cycle must fail safe (ok=false)")
|
|
}
|
|
}
|
|
|
|
// TestSystemDisks_VirtualWithEmptySlaves: a virtual root whose slaves dir is EMPTY (nothing to
|
|
// ground on) fails safe — hasDir alone is not resolution.
|
|
func TestSystemDisks_VirtualWithEmptySlaves(t *testing.T) {
|
|
host := &fakeHostReader{
|
|
mounts: []Mount{{Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}},
|
|
slaves: map[string][]string{"pve-root": {}},
|
|
}
|
|
if _, ok := SystemDisks(host); ok {
|
|
t.Fatal("virtual device with no slaves must fail safe")
|
|
}
|
|
}
|