diff --git a/internal/hostagent/unraid.go b/internal/hostagent/unraid.go index 446074e7d..20c77ef3f 100644 --- a/internal/hostagent/unraid.go +++ b/internal/hostagent/unraid.go @@ -522,10 +522,10 @@ func isUnraidEmptySlot(disk agentshost.UnraidDisk) bool { // Unraid names every configured slot (for example disk6 or parity2), even // when it has never been assigned. A slot label is therefore topology, not // membership evidence. Preserve DISK_NP members only when native identity, - // device, filesystem, or size evidence shows that a disk was assigned. + // device, a concrete filesystem, or size evidence shows that a disk was assigned. return strings.TrimSpace(disk.Device) == "" && !unraidstatus.HasMeaningfulIdentity(disk.Model, disk.Serial) && - strings.TrimSpace(disk.Filesystem) == "" && + !unraidstatus.HasMeaningfulFilesystem(disk.Filesystem) && disk.SizeBytes == 0 } diff --git a/internal/hostagent/unraid_test.go b/internal/hostagent/unraid_test.go index 2f5d92b57..a5fb58621 100644 --- a/internal/hostagent/unraid_test.go +++ b/internal/hostagent/unraid_test.go @@ -154,6 +154,7 @@ diskNumber.5=5 diskName.5=disk5 diskSize.5=0 diskId.5=ata-_ +diskFsType.5=auto rdevStatus.5=DISK_NP rdevName.5= rdevId.5=ata-_ @@ -161,6 +162,7 @@ diskNumber.29=29 diskName.29=parity2 diskSize.29=0 diskId.29=ata-_ +diskFsType.29=auto rdevStatus.29=DISK_NP_DSBL rdevName.29= rdevId.29=ata-_ @@ -286,6 +288,7 @@ id="ata-_" size="0" status="DISK_NP" type="Data" +fsType="auto" ["parity2"] idx="29" name="parity2" @@ -294,6 +297,7 @@ id="ata-_" size="0" status="DISK_NP_DSBL" type="Parity" +fsType="auto" ` disks := parseUnraidDisksINI(input) diff --git a/internal/monitoring/monitor_agents.go b/internal/monitoring/monitor_agents.go index 7c099846f..56a5cf1da 100644 --- a/internal/monitoring/monitor_agents.go +++ b/internal/monitoring/monitor_agents.go @@ -4147,7 +4147,7 @@ func isLegacyUnraidEmptySlot(disk agentshost.UnraidDisk, normalizedStatus string } return strings.TrimSpace(disk.Device) == "" && !unraidstatus.HasMeaningfulIdentity(disk.Model, disk.Serial) && - strings.TrimSpace(disk.Filesystem) == "" && + !unraidstatus.HasMeaningfulFilesystem(disk.Filesystem) && disk.SizeBytes == 0 } diff --git a/internal/monitoring/monitor_host_agents_test.go b/internal/monitoring/monitor_host_agents_test.go index e2ce84fa3..95c0d8c3b 100644 --- a/internal/monitoring/monitor_host_agents_test.go +++ b/internal/monitoring/monitor_host_agents_test.go @@ -2588,8 +2588,8 @@ func TestApplyHostReportFiltersLegacyUnraidEmptySlots(t *testing.T) { Disks: []agentshost.UnraidDisk{ {Name: "parity", Device: "/dev/sdb", Role: "parity", RawStatus: "DISK_OK", SizeBytes: 5860522532}, {Name: "disk1", Device: "/dev/sde", Role: "data", RawStatus: "DISK_OK", SizeBytes: 5860522532}, - {Name: "disk6", Role: "data", RawStatus: "DISK_NP", Model: "ata -", Serial: "ata-_", Slot: 6}, - {Name: "parity2", Role: "parity", RawStatus: "DISK_NP_DSBL", Model: "ata -", Serial: "ata-_", Slot: 29}, + {Name: "disk6", Role: "data", Status: "missing", RawStatus: "DISK_NP", Model: "ata -", Serial: "ata-_", Filesystem: "auto", Slot: 6}, + {Name: "parity2", Role: "parity", Status: "missing", RawStatus: "DISK_NP_DSBL", Model: "ata -", Serial: "ata-_", Filesystem: "auto", Slot: 29}, }, }, Timestamp: time.Now().UTC(), diff --git a/internal/unraid/status.go b/internal/unraid/status.go index 2ea54e51e..4836caf14 100644 --- a/internal/unraid/status.go +++ b/internal/unraid/status.go @@ -31,6 +31,15 @@ func HasMeaningfulIdentity(model, serial string) bool { return NormalizeNativeIdentity(model) != "" || NormalizeNativeIdentity(serial) != "" } +// HasMeaningfulFilesystem reports whether a native filesystem field is +// evidence that a slot has a disk assigned. Unraid emits "auto" for configured +// but empty DISK_NP slots, so presence of that value alone cannot establish +// membership. +func HasMeaningfulFilesystem(filesystem string) bool { + filesystem = strings.TrimSpace(filesystem) + return filesystem != "" && !strings.EqualFold(filesystem, "auto") +} + // IsExplicitMissingMember reports Unraid's provider-owned status for a slot // that was assigned but whose device is no longer present. Plain DISK_NP means // no device is assigned and must not be treated as equivalent. diff --git a/internal/unraid/status_test.go b/internal/unraid/status_test.go index 8c5be0c36..c97202727 100644 --- a/internal/unraid/status_test.go +++ b/internal/unraid/status_test.go @@ -35,3 +35,18 @@ func TestIsExplicitMissingMember(t *testing.T) { } } } + +func TestHasMeaningfulFilesystem(t *testing.T) { + t.Parallel() + + for _, filesystem := range []string{"", "auto", " AUTO "} { + if HasMeaningfulFilesystem(filesystem) { + t.Errorf("%q must not establish Unraid disk assignment", filesystem) + } + } + for _, filesystem := range []string{"xfs", "btrfs", "luks:xfs"} { + if !HasMeaningfulFilesystem(filesystem) { + t.Errorf("%q must establish Unraid disk assignment", filesystem) + } + } +}