mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
478 lines
14 KiB
Go
478 lines
14 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
)
|
|
|
|
// --- mergeHostAgentSMARTIntoDisks ---
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_EmptyInputs(t *testing.T) {
|
|
// All combinations of empty inputs should return disks unchanged
|
|
disks := []models.PhysicalDisk{{ID: "d1"}}
|
|
if got := mergeHostAgentSMARTIntoDisks(nil, nil, nil); got != nil {
|
|
t.Error("nil disks should return nil")
|
|
}
|
|
if got := mergeHostAgentSMARTIntoDisks(disks, nil, nil); len(got) != 1 {
|
|
t.Error("nil nodes/hosts should return disks unchanged")
|
|
}
|
|
if got := mergeHostAgentSMARTIntoDisks(disks, []models.Node{}, nil); len(got) != 1 {
|
|
t.Error("empty nodes should return disks unchanged")
|
|
}
|
|
if got := mergeHostAgentSMARTIntoDisks(disks, []models.Node{{Name: "n1"}}, nil); len(got) != 1 {
|
|
t.Error("nil hosts should return disks unchanged")
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_MatchByWWN(t *testing.T) {
|
|
disks := []models.PhysicalDisk{
|
|
{ID: "d1", Node: "pve1", DevPath: "/dev/sda", WWN: "0x5001234", Temperature: 0},
|
|
}
|
|
nodes := []models.Node{
|
|
{Name: "pve1", LinkedAgentID: "host-1"},
|
|
}
|
|
hosts := []models.Host{
|
|
{
|
|
ID: "host-1",
|
|
Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{
|
|
{Device: "/dev/sda", WWN: "0x5001234", Temperature: 42},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if result[0].Temperature != 42 {
|
|
t.Errorf("expected temp 42 via WWN match, got %d", result[0].Temperature)
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_MatchBySerial(t *testing.T) {
|
|
disks := []models.PhysicalDisk{
|
|
{ID: "d1", Node: "pve1", Serial: "SERIAL123", Temperature: 0},
|
|
}
|
|
nodes := []models.Node{
|
|
{Name: "pve1", LinkedAgentID: "host-1"},
|
|
}
|
|
hosts := []models.Host{
|
|
{
|
|
ID: "host-1",
|
|
Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{
|
|
{Device: "/dev/sdb", Serial: "serial123", Temperature: 38},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if result[0].Temperature != 38 {
|
|
t.Errorf("expected temp 38 via serial match (case-insensitive), got %d", result[0].Temperature)
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_MatchByDevPath(t *testing.T) {
|
|
disks := []models.PhysicalDisk{
|
|
{ID: "d1", Node: "pve1", DevPath: "/dev/nvme0n1", Temperature: 0},
|
|
}
|
|
nodes := []models.Node{
|
|
{Name: "pve1", LinkedAgentID: "host-1"},
|
|
}
|
|
hosts := []models.Host{
|
|
{
|
|
ID: "host-1",
|
|
Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{
|
|
{Device: "nvme0n1", Temperature: 35},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if result[0].Temperature != 35 {
|
|
t.Errorf("expected temp 35 via device path match (strip /dev/), got %d", result[0].Temperature)
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_SkipsStandby(t *testing.T) {
|
|
disks := []models.PhysicalDisk{
|
|
{ID: "d1", Node: "pve1", Serial: "SER1", Temperature: 0},
|
|
}
|
|
nodes := []models.Node{
|
|
{Name: "pve1", LinkedAgentID: "host-1"},
|
|
}
|
|
hosts := []models.Host{
|
|
{
|
|
ID: "host-1",
|
|
Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{
|
|
{Device: "/dev/sda", Serial: "SER1", Temperature: 40, Standby: true},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if result[0].Temperature != 0 {
|
|
t.Error("standby disk should not have its temperature merged")
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_MergesSMARTAttributes(t *testing.T) {
|
|
disks := []models.PhysicalDisk{
|
|
{ID: "d1", Node: "pve1", Serial: "SER1", Temperature: 0},
|
|
}
|
|
nodes := []models.Node{
|
|
{Name: "pve1", LinkedAgentID: "host-1"},
|
|
}
|
|
attrs := &models.SMARTAttributes{}
|
|
poh := int64(5000)
|
|
attrs.PowerOnHours = &poh
|
|
hosts := []models.Host{
|
|
{
|
|
ID: "host-1",
|
|
Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{
|
|
{Device: "/dev/sda", Serial: "SER1", Temperature: 41, Attributes: attrs},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if result[0].SmartAttributes == nil {
|
|
t.Fatal("SMART attributes should be merged")
|
|
}
|
|
if *result[0].SmartAttributes.PowerOnHours != 5000 {
|
|
t.Error("PowerOnHours should be 5000")
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_DerivesWearoutFromSMARTAttributes(t *testing.T) {
|
|
disks := []models.PhysicalDisk{
|
|
{ID: "d1", Node: "pve1", Serial: "SER1", Wearout: -1},
|
|
}
|
|
nodes := []models.Node{
|
|
{Name: "pve1", LinkedAgentID: "host-1"},
|
|
}
|
|
used := 6
|
|
hosts := []models.Host{
|
|
{
|
|
ID: "host-1",
|
|
Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{
|
|
{
|
|
Device: "/dev/sda [megaraid,7]",
|
|
Serial: "SER1",
|
|
Health: "PASSED",
|
|
Attributes: &models.SMARTAttributes{
|
|
PercentageUsed: &used,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if result[0].Wearout != 94 {
|
|
t.Fatalf("expected wearout derived from PercentageUsed, got %d", result[0].Wearout)
|
|
}
|
|
if result[0].SmartAttributes == nil || result[0].SmartAttributes.PercentageUsed == nil || *result[0].SmartAttributes.PercentageUsed != 6 {
|
|
t.Fatalf("expected merged PercentageUsed, got %+v", result[0].SmartAttributes)
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_SMARTFailureAndWearoutOverrideCoarsePVEValues(t *testing.T) {
|
|
disks := []models.PhysicalDisk{{
|
|
ID: "d1",
|
|
Node: "pve1",
|
|
Serial: "SER1",
|
|
Type: "ssd",
|
|
Health: "PASSED",
|
|
Wearout: 100,
|
|
}}
|
|
nodes := []models.Node{{Name: "pve1", LinkedAgentID: "host-1"}}
|
|
used := 95
|
|
hosts := []models.Host{{
|
|
ID: "host-1",
|
|
Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{{
|
|
Device: "/dev/sda",
|
|
Serial: "SER1",
|
|
Health: "FAILED",
|
|
Attributes: &models.SMARTAttributes{
|
|
PercentageUsed: &used,
|
|
},
|
|
}},
|
|
},
|
|
}}
|
|
|
|
result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if result[0].Health != "FAILED" {
|
|
t.Fatalf("SMART failure was suppressed by PVE health: %+v", result[0])
|
|
}
|
|
if result[0].Wearout != 5 {
|
|
t.Fatalf("SMART wearout did not override contradictory PVE value: %+v", result[0])
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_DoesNotReplaceFailureWithPassed(t *testing.T) {
|
|
disks := []models.PhysicalDisk{{ID: "d1", Node: "pve1", Serial: "SER1", Health: "FAILED"}}
|
|
nodes := []models.Node{{Name: "pve1", LinkedAgentID: "host-1"}}
|
|
hosts := []models.Host{{
|
|
ID: "host-1",
|
|
Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{{Serial: "SER1", Health: "PASSED"}},
|
|
},
|
|
}}
|
|
|
|
if got := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)[0].Health; got != "FAILED" {
|
|
t.Fatalf("PASSED replaced an existing failure: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_DuplicateIdentityRequiresUniqueTopology(t *testing.T) {
|
|
disks := []models.PhysicalDisk{{
|
|
ID: "d1",
|
|
Node: "pve1",
|
|
DevPath: "/dev/sg2",
|
|
Serial: "DUPLICATE-SERIAL",
|
|
Controller: "sssraid0",
|
|
Target: "sssraid,0,2",
|
|
Health: "UNKNOWN",
|
|
}}
|
|
nodes := []models.Node{{Name: "pve1", LinkedAgentID: "host-1"}}
|
|
hosts := []models.Host{{
|
|
ID: "host-1",
|
|
Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{
|
|
{
|
|
Device: "/dev/sg2",
|
|
Serial: "DUPLICATE-SERIAL",
|
|
Controller: "sssraid0",
|
|
Target: "sssraid,0,1",
|
|
Temperature: 31,
|
|
},
|
|
{
|
|
Device: "/dev/sg2",
|
|
Serial: "DUPLICATE-SERIAL",
|
|
Controller: "sssraid0",
|
|
Target: "sssraid,0,2",
|
|
Temperature: 47,
|
|
Health: "PASSED",
|
|
},
|
|
},
|
|
},
|
|
}}
|
|
|
|
result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if result[0].Temperature != 47 || result[0].Health != "PASSED" {
|
|
t.Fatalf("topology-specific SMART evidence was not selected: %+v", result[0])
|
|
}
|
|
|
|
disks[0].Controller = ""
|
|
disks[0].Target = ""
|
|
disks[0].DevPath = "/dev/unknown"
|
|
result = mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if result[0].Temperature != 0 || result[0].Health != "UNKNOWN" {
|
|
t.Fatalf("ambiguous SMART identity was guessed: %+v", result[0])
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_FillsMissingHealthFromHostSMART(t *testing.T) {
|
|
disks := []models.PhysicalDisk{
|
|
{ID: "d1", Node: "pve1", Serial: "SER1", Health: "UNKNOWN"},
|
|
}
|
|
nodes := []models.Node{
|
|
{Name: "pve1", LinkedAgentID: "host-1"},
|
|
}
|
|
hosts := []models.Host{
|
|
{
|
|
ID: "host-1",
|
|
Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{
|
|
{Device: "/dev/sda", Serial: "SER1", Health: "FAILED"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if result[0].Health != "FAILED" {
|
|
t.Fatalf("expected host SMART health to fill unknown disk health, got %q", result[0].Health)
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_NoAgentLink(t *testing.T) {
|
|
disks := []models.PhysicalDisk{
|
|
{ID: "d1", Node: "pve1", Serial: "SER1", Temperature: 0},
|
|
}
|
|
nodes := []models.Node{
|
|
{Name: "pve1"}, // No LinkedAgentID
|
|
}
|
|
hosts := []models.Host{
|
|
{
|
|
ID: "host-1",
|
|
Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{
|
|
{Serial: "SER1", Temperature: 40},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if result[0].Temperature != 0 {
|
|
t.Error("unlinked nodes should not get host agent temperatures")
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_PreservesExistingTemp(t *testing.T) {
|
|
disks := []models.PhysicalDisk{
|
|
{ID: "d1", Node: "pve1", Serial: "SER1", Temperature: 50}, // Already has temp
|
|
}
|
|
nodes := []models.Node{
|
|
{Name: "pve1", LinkedAgentID: "host-1"},
|
|
}
|
|
hosts := []models.Host{
|
|
{
|
|
ID: "host-1",
|
|
Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{
|
|
{Serial: "SER1", Temperature: 40},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if result[0].Temperature != 50 {
|
|
t.Errorf("existing temperature should be preserved, got %d", result[0].Temperature)
|
|
}
|
|
}
|
|
|
|
func TestMergeHostAgentSMARTIntoDisks_OriginalUnchanged(t *testing.T) {
|
|
disks := []models.PhysicalDisk{
|
|
{ID: "d1", Node: "pve1", Serial: "SER1", Temperature: 0},
|
|
}
|
|
nodes := []models.Node{{Name: "pve1", LinkedAgentID: "host-1"}}
|
|
hosts := []models.Host{
|
|
{ID: "host-1", Sensors: models.HostSensorSummary{
|
|
SMART: []models.HostDiskSMART{{Serial: "SER1", Temperature: 42}},
|
|
}},
|
|
}
|
|
result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts)
|
|
if disks[0].Temperature != 0 {
|
|
t.Error("original disk slice should not be mutated")
|
|
}
|
|
if result[0].Temperature != 42 {
|
|
t.Error("returned slice should have the merged temp")
|
|
}
|
|
}
|
|
|
|
// --- smartAttributesFromUnifiedMeta ---
|
|
|
|
func TestSmartAttributesFromUnifiedMeta_Nil(t *testing.T) {
|
|
if smartAttributesFromUnifiedMeta(nil) != nil {
|
|
t.Error("nil input should return nil")
|
|
}
|
|
}
|
|
|
|
func TestSmartAttributesFromUnifiedMeta_AllZero(t *testing.T) {
|
|
// No reported fields should return nil.
|
|
if smartAttributesFromUnifiedMeta(&unifiedresources.SMARTMeta{}) != nil {
|
|
t.Error("SMARTMeta with no reported fields should return nil")
|
|
}
|
|
}
|
|
|
|
func TestSmartAttributesFromUnifiedMeta_PopulatesFields(t *testing.T) {
|
|
meta := &unifiedresources.SMARTMeta{
|
|
PowerOnHours: smartInt64Pointer(12345),
|
|
PowerCycles: smartInt64Pointer(500),
|
|
ReallocatedSectors: smartInt64Pointer(0),
|
|
PendingSectors: smartInt64Pointer(2),
|
|
OfflineUncorrectable: smartInt64Pointer(1),
|
|
UDMACRCErrors: smartInt64Pointer(3),
|
|
PercentageUsed: smartIntPointer(15),
|
|
AvailableSpare: smartIntPointer(85),
|
|
MediaErrors: smartInt64Pointer(0),
|
|
UnsafeShutdowns: smartInt64Pointer(10),
|
|
}
|
|
result := smartAttributesFromUnifiedMeta(meta)
|
|
if result == nil {
|
|
t.Fatal("non-zero meta should produce non-nil result")
|
|
}
|
|
if result.PowerOnHours == nil || *result.PowerOnHours != 12345 {
|
|
t.Error("PowerOnHours should be 12345")
|
|
}
|
|
if result.PowerCycles == nil || *result.PowerCycles != 500 {
|
|
t.Error("PowerCycles should be 500")
|
|
}
|
|
if result.ReallocatedSectors == nil || *result.ReallocatedSectors != 0 {
|
|
t.Error("reported zero ReallocatedSectors should survive projection")
|
|
}
|
|
if result.PendingSectors == nil || *result.PendingSectors != 2 {
|
|
t.Error("PendingSectors should be 2")
|
|
}
|
|
if result.PercentageUsed == nil || *result.PercentageUsed != 15 {
|
|
t.Error("PercentageUsed should be 15")
|
|
}
|
|
if result.MediaErrors == nil || *result.MediaErrors != 0 {
|
|
t.Error("reported zero MediaErrors should survive projection")
|
|
}
|
|
if result.UnsafeShutdowns == nil || *result.UnsafeShutdowns != 10 {
|
|
t.Error("UnsafeShutdowns should be 10")
|
|
}
|
|
}
|
|
|
|
func smartIntPointer(value int) *int { return &value }
|
|
func smartInt64Pointer(value int64) *int64 { return &value }
|
|
|
|
// --- getNodeDisplayName ---
|
|
|
|
func TestGetNodeDisplayName_NilInstance(t *testing.T) {
|
|
got := getNodeDisplayName(nil, "pve1")
|
|
if got != "pve1" {
|
|
t.Errorf("expected 'pve1', got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestGetNodeDisplayName_EmptyNodeName(t *testing.T) {
|
|
got := getNodeDisplayName(nil, "")
|
|
if got != "unknown-node" {
|
|
t.Errorf("expected 'unknown-node', got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestGetNodeDisplayName_FriendlyName(t *testing.T) {
|
|
inst := &config.PVEInstance{Name: "My Server"}
|
|
got := getNodeDisplayName(inst, "pve1")
|
|
if got != "My Server" {
|
|
t.Errorf("expected friendly name 'My Server', got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestGetNodeDisplayName_ClusterUsesNodeName(t *testing.T) {
|
|
inst := &config.PVEInstance{Name: "cluster1", IsCluster: true}
|
|
got := getNodeDisplayName(inst, "node1")
|
|
if got != "node1" {
|
|
t.Errorf("cluster mode should fall back to the native node name, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestGetNodeDisplayName_ClusterWithEndpointLabel(t *testing.T) {
|
|
inst := &config.PVEInstance{
|
|
Name: "cluster1",
|
|
IsCluster: true,
|
|
ClusterEndpoints: []config.ClusterEndpoint{
|
|
{NodeName: "node1", NodeID: "node/node1", NodeIdentity: "cluster1-node1"},
|
|
},
|
|
ClusterNodeIdentities: []config.PVEClusterNodeIdentity{{
|
|
ID: "cluster1-node1", NativeName: "node1", DisplayName: "Compute one",
|
|
}},
|
|
}
|
|
got := getNodeDisplayName(inst, "node1")
|
|
if got != "Compute one" {
|
|
t.Errorf("expected persisted cluster-node display name, got %q", got)
|
|
}
|
|
}
|