1785 lines
56 KiB
Go
1785 lines
56 KiB
Go
package collector
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
)
|
|
|
|
func TestRedfishConnectorCollect(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
register := func(path string, payload interface{}) {
|
|
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(payload)
|
|
})
|
|
}
|
|
|
|
register("/redfish/v1", map[string]interface{}{"Name": "ServiceRoot"})
|
|
register("/redfish/v1/Systems/1", map[string]interface{}{
|
|
"Manufacturer": "Supermicro",
|
|
"Model": "SYS-TEST",
|
|
"SerialNumber": "SYS123",
|
|
"BiosVersion": "2.1a",
|
|
})
|
|
register("/redfish/v1/Systems/1/Bios", map[string]interface{}{"Version": "2.1a"})
|
|
register("/redfish/v1/Systems/1/SecureBoot", map[string]interface{}{"SecureBootCurrentBoot": "Enabled"})
|
|
register("/redfish/v1/Systems/1/Processors", map[string]interface{}{
|
|
"Members": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Systems/1/Processors/CPU1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/Processors/CPU1", map[string]interface{}{
|
|
"Name": "CPU1",
|
|
"Model": "Xeon Gold",
|
|
"TotalCores": 32,
|
|
"TotalThreads": 64,
|
|
"MaxSpeedMHz": 3600,
|
|
})
|
|
register("/redfish/v1/Systems/1/Memory", map[string]interface{}{
|
|
"Members": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Systems/1/Memory/DIMM1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/Memory/DIMM1", map[string]interface{}{
|
|
"Name": "DIMM A1",
|
|
"CapacityMiB": 32768,
|
|
"MemoryDeviceType": "DDR5",
|
|
"OperatingSpeedMhz": 4800,
|
|
"Status": map[string]interface{}{
|
|
"Health": "OK",
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/Storage", map[string]interface{}{
|
|
"Members": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Systems/1/Storage/1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/Storage/1", map[string]interface{}{
|
|
"Drives": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Systems/1/Storage/1/Drives/1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/Storage/1/Drives/1", map[string]interface{}{
|
|
"Name": "Drive1",
|
|
"Model": "NVMe Test",
|
|
"MediaType": "SSD",
|
|
"Protocol": "NVMe",
|
|
"CapacityGB": 960,
|
|
"SerialNumber": "SN123",
|
|
})
|
|
register("/redfish/v1/Systems/1/PCIeDevices", map[string]interface{}{
|
|
"Members": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Systems/1/PCIeDevices/GPU1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/PCIeDevices/GPU1", map[string]interface{}{
|
|
"Id": "GPU1",
|
|
"Name": "NVIDIA H100",
|
|
"Model": "NVIDIA H100 PCIe",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "GPU-SN-001",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/PCIeDevices/GPU1/PCIeFunctions",
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/PCIeDevices/GPU1/PCIeFunctions", map[string]interface{}{
|
|
"Members": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Systems/1/PCIeFunctions/GPU1F0"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/PCIeFunctions/GPU1F0", map[string]interface{}{
|
|
"FunctionId": "0000:65:00.0",
|
|
"VendorId": "0x10DE",
|
|
"DeviceId": "0x2331",
|
|
"ClassCode": "0x030200",
|
|
"CurrentLinkWidth": 16,
|
|
"CurrentLinkSpeed": "16.0 GT/s",
|
|
"MaxLinkWidth": 16,
|
|
"MaxLinkSpeed": "16.0 GT/s",
|
|
})
|
|
register("/redfish/v1/Chassis/1/NetworkAdapters", map[string]interface{}{
|
|
"Members": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Chassis/1/Power", map[string]interface{}{
|
|
"PowerSupplies": []map[string]interface{}{
|
|
{
|
|
"MemberId": "PSU1",
|
|
"Name": "PSU Slot 1",
|
|
"Model": "PWS-2K01A-1R",
|
|
"Manufacturer": "Delta",
|
|
"PowerCapacityWatts": 2000,
|
|
"PowerInputWatts": 1600,
|
|
"LastPowerOutputWatts": 1200,
|
|
"LineInputVoltage": 230,
|
|
"Status": map[string]interface{}{
|
|
"Health": "OK",
|
|
"State": "Enabled",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
register("/redfish/v1/Chassis/1/NetworkAdapters/1", map[string]interface{}{
|
|
"Name": "Mellanox",
|
|
"Model": "ConnectX-6",
|
|
"SerialNumber": "NIC123",
|
|
})
|
|
register("/redfish/v1/Managers/1", map[string]interface{}{
|
|
"FirmwareVersion": "1.25",
|
|
})
|
|
register("/redfish/v1/Managers/1/NetworkProtocol", map[string]interface{}{
|
|
"Id": "NetworkProtocol",
|
|
})
|
|
|
|
ts := httptest.NewServer(mux)
|
|
defer ts.Close()
|
|
|
|
c := NewRedfishConnector()
|
|
result, err := c.Collect(context.Background(), Request{
|
|
Host: ts.URL,
|
|
Port: 443,
|
|
Protocol: "redfish",
|
|
Username: "admin",
|
|
AuthType: "password",
|
|
Password: "secret",
|
|
TLSMode: "strict",
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("collect failed: %v", err)
|
|
}
|
|
|
|
if result.Hardware == nil {
|
|
t.Fatalf("expected hardware config")
|
|
}
|
|
if result.Hardware.BoardInfo.ProductName != "SYS-TEST" {
|
|
t.Fatalf("unexpected board model: %q", result.Hardware.BoardInfo.ProductName)
|
|
}
|
|
if len(result.Hardware.CPUs) != 1 {
|
|
t.Fatalf("expected one CPU, got %d", len(result.Hardware.CPUs))
|
|
}
|
|
if len(result.Hardware.Memory) != 1 {
|
|
t.Fatalf("expected one DIMM, got %d", len(result.Hardware.Memory))
|
|
}
|
|
if len(result.Hardware.Storage) != 1 {
|
|
t.Fatalf("expected one drive, got %d", len(result.Hardware.Storage))
|
|
}
|
|
if len(result.Hardware.NetworkAdapters) != 1 {
|
|
t.Fatalf("expected one nic, got %d", len(result.Hardware.NetworkAdapters))
|
|
}
|
|
if len(result.Hardware.GPUs) != 1 {
|
|
t.Fatalf("expected one gpu, got %d", len(result.Hardware.GPUs))
|
|
}
|
|
if result.Hardware.GPUs[0].BDF != "0000:65:00.0" {
|
|
t.Fatalf("unexpected gpu BDF: %q", result.Hardware.GPUs[0].BDF)
|
|
}
|
|
if len(result.Hardware.PCIeDevices) != 1 {
|
|
t.Fatalf("expected one pcie device, got %d", len(result.Hardware.PCIeDevices))
|
|
}
|
|
if len(result.Hardware.PowerSupply) != 1 {
|
|
t.Fatalf("expected one psu, got %d", len(result.Hardware.PowerSupply))
|
|
}
|
|
if result.Hardware.PowerSupply[0].WattageW != 2000 {
|
|
t.Fatalf("unexpected psu wattage: %d", result.Hardware.PowerSupply[0].WattageW)
|
|
}
|
|
if len(result.Hardware.Firmware) == 0 {
|
|
t.Fatalf("expected firmware entries")
|
|
}
|
|
if result.RawPayloads == nil {
|
|
t.Fatalf("expected raw payloads")
|
|
}
|
|
treeAny, ok := result.RawPayloads["redfish_tree"]
|
|
if !ok {
|
|
t.Fatalf("expected redfish_tree in raw payloads")
|
|
}
|
|
tree, ok := treeAny.(map[string]interface{})
|
|
if !ok || len(tree) == 0 {
|
|
t.Fatalf("expected non-empty redfish_tree, got %#v", treeAny)
|
|
}
|
|
}
|
|
|
|
func TestParsePCIeDeviceSlot_FromNestedRedfishSlotLocation(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"Id": "NIC1",
|
|
"Slot": map[string]interface{}{
|
|
"Lanes": 16,
|
|
"Location": map[string]interface{}{
|
|
"PartLocation": map[string]interface{}{
|
|
"LocationOrdinalValue": 1,
|
|
"LocationType": "Slot",
|
|
"ServiceLabel": "PCIe Slot 1 (1)",
|
|
},
|
|
},
|
|
"PCIeType": "Gen5",
|
|
"SlotType": "FullLength",
|
|
},
|
|
}
|
|
|
|
got := parsePCIeDevice(doc, nil)
|
|
if got.Slot != "PCIe Slot 1 (1)" {
|
|
t.Fatalf("unexpected slot: %q", got.Slot)
|
|
}
|
|
}
|
|
|
|
func TestParsePCIeDeviceSlot_EmptyMapFallsBackToID(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"Id": "NIC42",
|
|
"Slot": map[string]interface{}{},
|
|
}
|
|
|
|
got := parsePCIeDevice(doc, nil)
|
|
if got.Slot != "NIC42" {
|
|
t.Fatalf("unexpected slot fallback: %q", got.Slot)
|
|
}
|
|
if got.Slot == "map[]" {
|
|
t.Fatalf("slot should not stringify empty map")
|
|
}
|
|
}
|
|
|
|
func TestReplayRedfishFromRawPayloads_FallbackCollectionMembersByPrefix(t *testing.T) {
|
|
raw := map[string]any{
|
|
"redfish_tree": map[string]interface{}{
|
|
"/redfish/v1": map[string]interface{}{
|
|
"Systems": map[string]interface{}{"@odata.id": "/redfish/v1/Systems"},
|
|
"Chassis": map[string]interface{}{"@odata.id": "/redfish/v1/Chassis"},
|
|
"Managers": map[string]interface{}{"@odata.id": "/redfish/v1/Managers"},
|
|
},
|
|
"/redfish/v1/Systems": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1": map[string]interface{}{
|
|
"Manufacturer": "Supermicro",
|
|
"Model": "SYS-TEST",
|
|
"SerialNumber": "SYS123",
|
|
},
|
|
// Intentionally missing /redfish/v1/Systems/1/Processors collection.
|
|
"/redfish/v1/Systems/1/Processors/CPU1": map[string]interface{}{
|
|
"Id": "CPU1",
|
|
"Model": "Xeon Gold",
|
|
"TotalCores": 32,
|
|
"TotalThreads": 64,
|
|
},
|
|
"/redfish/v1/Chassis": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1": map[string]interface{}{
|
|
"Id": "1",
|
|
},
|
|
"/redfish/v1/Managers": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Managers/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Managers/1": map[string]interface{}{
|
|
"Id": "1",
|
|
},
|
|
},
|
|
"redfish_fetch_errors": []map[string]interface{}{
|
|
{"path": "/redfish/v1/Systems/1/Processors", "error": "status 500"},
|
|
},
|
|
}
|
|
|
|
got, err := ReplayRedfishFromRawPayloads(raw, nil)
|
|
if err != nil {
|
|
t.Fatalf("replay failed: %v", err)
|
|
}
|
|
if got.Hardware == nil {
|
|
t.Fatalf("expected hardware")
|
|
}
|
|
if len(got.Hardware.CPUs) != 1 {
|
|
t.Fatalf("expected one CPU via prefix fallback, got %d", len(got.Hardware.CPUs))
|
|
}
|
|
if _, ok := got.RawPayloads["redfish_fetch_errors"]; !ok {
|
|
t.Fatalf("expected raw payloads to preserve redfish_fetch_errors")
|
|
}
|
|
}
|
|
|
|
func TestReplayRedfishFromRawPayloads_ParsesInlineThresholdAndDiscreteSensors(t *testing.T) {
|
|
raw := map[string]any{
|
|
"redfish_tree": map[string]interface{}{
|
|
"/redfish/v1": map[string]interface{}{
|
|
"Systems": map[string]interface{}{"@odata.id": "/redfish/v1/Systems"},
|
|
"Chassis": map[string]interface{}{"@odata.id": "/redfish/v1/Chassis"},
|
|
"Managers": map[string]interface{}{"@odata.id": "/redfish/v1/Managers"},
|
|
},
|
|
"/redfish/v1/Systems": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1": map[string]interface{}{
|
|
"Id": "1",
|
|
"Manufacturer": "Inspur",
|
|
"Model": "NF5688M7",
|
|
"SerialNumber": "23E100051",
|
|
},
|
|
"/redfish/v1/Chassis": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1": map[string]interface{}{
|
|
"Id": "1",
|
|
},
|
|
"/redfish/v1/Managers": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Managers/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Managers/1": map[string]interface{}{
|
|
"Id": "1",
|
|
},
|
|
"/redfish/v1/Chassis/1/ThresholdSensors": map[string]interface{}{
|
|
"Sensors": []interface{}{
|
|
map[string]interface{}{
|
|
"Name": "Inlet_Temp",
|
|
"Reading": 16,
|
|
"ReadingUnits": "deg_c",
|
|
"State": "Enabled",
|
|
},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/DiscreteSensors": map[string]interface{}{
|
|
"Sensors": []interface{}{
|
|
map[string]interface{}{
|
|
"Name": "PSU_Redundant",
|
|
"State": "Disabled",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
got, err := ReplayRedfishFromRawPayloads(raw, nil)
|
|
if err != nil {
|
|
t.Fatalf("replay failed: %v", err)
|
|
}
|
|
if len(got.Sensors) == 0 {
|
|
t.Fatalf("expected sensors from inline ThresholdSensors")
|
|
}
|
|
foundSensor := false
|
|
for _, s := range got.Sensors {
|
|
if s.Name == "Inlet_Temp" {
|
|
foundSensor = true
|
|
break
|
|
}
|
|
}
|
|
if !foundSensor {
|
|
t.Fatalf("expected Inlet_Temp sensor in replay output")
|
|
}
|
|
foundEvent := false
|
|
for _, ev := range got.Events {
|
|
if ev.EventType == "Discrete Sensor Status" && ev.SensorName == "PSU_Redundant" {
|
|
foundEvent = true
|
|
break
|
|
}
|
|
}
|
|
if !foundEvent {
|
|
t.Fatalf("expected discrete sensor warning event from inline DiscreteSensors")
|
|
}
|
|
}
|
|
|
|
func TestReplayRedfishFromRawPayloads_CollectsThermalAndPowerSensors(t *testing.T) {
|
|
raw := map[string]any{
|
|
"redfish_tree": map[string]interface{}{
|
|
"/redfish/v1": map[string]interface{}{
|
|
"Systems": map[string]interface{}{"@odata.id": "/redfish/v1/Systems"},
|
|
"Chassis": map[string]interface{}{"@odata.id": "/redfish/v1/Chassis"},
|
|
"Managers": map[string]interface{}{"@odata.id": "/redfish/v1/Managers"},
|
|
},
|
|
"/redfish/v1/Systems": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1": map[string]interface{}{
|
|
"Id": "1",
|
|
"Manufacturer": "Inspur",
|
|
"Model": "NF5688M7",
|
|
"SerialNumber": "23E100051",
|
|
},
|
|
"/redfish/v1/Chassis": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1": map[string]interface{}{
|
|
"Id": "1",
|
|
},
|
|
"/redfish/v1/Managers": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Managers/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Managers/1": map[string]interface{}{
|
|
"Id": "1",
|
|
},
|
|
"/redfish/v1/Chassis/1/Thermal": map[string]interface{}{
|
|
"Fans": []interface{}{
|
|
map[string]interface{}{
|
|
"Name": "FAN0_F_Speed",
|
|
"Reading": 9279,
|
|
"ReadingUnits": "RPM",
|
|
"Status": map[string]interface{}{
|
|
"Health": "OK",
|
|
"State": "Enabled",
|
|
},
|
|
},
|
|
},
|
|
"Temperatures": []interface{}{
|
|
map[string]interface{}{
|
|
"Name": "CPU0_Temp",
|
|
"ReadingCelsius": 44,
|
|
"Status": map[string]interface{}{
|
|
"Health": "OK",
|
|
"State": "Enabled",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/Power": map[string]interface{}{
|
|
"Oem": map[string]interface{}{
|
|
"Public": map[string]interface{}{
|
|
"TotalPower": 1836,
|
|
"CurrentCPUPowerWatts": 304,
|
|
"CurrentMemoryPowerWatts": 75,
|
|
"CurrentFANPowerWatts": 180,
|
|
},
|
|
},
|
|
"PowerControl": []interface{}{
|
|
map[string]interface{}{
|
|
"Name": "System Power Control 1",
|
|
"PowerConsumedWatts": 1836,
|
|
"Status": map[string]interface{}{
|
|
"Health": "OK",
|
|
"State": "Enabled",
|
|
},
|
|
},
|
|
},
|
|
"PowerSupplies": []interface{}{
|
|
map[string]interface{}{
|
|
"Name": "Power Supply 1",
|
|
"PowerInputWatts": 180,
|
|
"LastPowerOutputWatts": 155,
|
|
"LineInputVoltage": 223.25,
|
|
"Status": map[string]interface{}{
|
|
"Health": "OK",
|
|
"State": "Enabled",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
got, err := ReplayRedfishFromRawPayloads(raw, nil)
|
|
if err != nil {
|
|
t.Fatalf("replay failed: %v", err)
|
|
}
|
|
if len(got.Sensors) == 0 {
|
|
t.Fatalf("expected non-empty sensors")
|
|
}
|
|
expected := map[string]bool{
|
|
"FAN0_F_Speed": false,
|
|
"CPU0_Temp": false,
|
|
"Total_Power": false,
|
|
"System Power Control 1_Consumed": false,
|
|
"Power Supply 1_InputPower": false,
|
|
}
|
|
for _, s := range got.Sensors {
|
|
if _, ok := expected[s.Name]; ok {
|
|
expected[s.Name] = true
|
|
}
|
|
}
|
|
for name, found := range expected {
|
|
if !found {
|
|
t.Fatalf("expected sensor %q in replay output", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnrichNICFromPCIeFunctions(t *testing.T) {
|
|
nic := parseNIC(map[string]interface{}{
|
|
"Id": "1",
|
|
"Model": "MCX75310AAS-NEAT",
|
|
"Manufacturer": "Supermicro",
|
|
"SerialNumber": "NIC-SN-1",
|
|
"Controllers": []interface{}{
|
|
map[string]interface{}{
|
|
"Links": map[string]interface{}{
|
|
"PCIeDevices": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/NIC1"},
|
|
},
|
|
},
|
|
"Location": map[string]interface{}{
|
|
"PartLocation": map[string]interface{}{"ServiceLabel": "PCIe Slot 1 (1)"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
pcieDoc := map[string]interface{}{
|
|
"Id": "NIC1",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/NIC1/PCIeFunctions",
|
|
},
|
|
}
|
|
functionDocs := []map[string]interface{}{
|
|
{
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x1021",
|
|
},
|
|
}
|
|
|
|
enrichNICFromPCIe(&nic, pcieDoc, functionDocs)
|
|
if nic.VendorID != 0x15b3 || nic.DeviceID != 0x1021 {
|
|
t.Fatalf("unexpected NIC IDs: vendor=%#x device=%#x", nic.VendorID, nic.DeviceID)
|
|
}
|
|
if nic.Location != "PCIe Slot 1 (1)" {
|
|
t.Fatalf("unexpected NIC location: %q", nic.Location)
|
|
}
|
|
}
|
|
|
|
func TestParseNIC_PortCountFromControllerCapabilities(t *testing.T) {
|
|
nic := parseNIC(map[string]interface{}{
|
|
"Id": "1",
|
|
"Controllers": []interface{}{
|
|
map[string]interface{}{
|
|
"ControllerCapabilities": map[string]interface{}{
|
|
"NetworkPortCount": 2,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if nic.PortCount != 2 {
|
|
t.Fatalf("expected port_count=2, got %d", nic.PortCount)
|
|
}
|
|
}
|
|
|
|
func TestParseNIC_DropsUnrealisticPortCount(t *testing.T) {
|
|
nic := parseNIC(map[string]interface{}{
|
|
"Id": "1",
|
|
"Controllers": []interface{}{
|
|
map[string]interface{}{
|
|
"ControllerCapabilities": map[string]interface{}{
|
|
"NetworkPortCount": 825307750,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if nic.PortCount != 0 {
|
|
t.Fatalf("expected unrealistic port count to be dropped, got %d", nic.PortCount)
|
|
}
|
|
}
|
|
|
|
func TestParsePCIeDevice_PrefersFunctionClassOverDeviceType(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"Id": "NIC1",
|
|
"DeviceType": "SingleFunction",
|
|
"Model": "MCX75310AAS-NEAT",
|
|
"PartNumber": "MCX75310AAS-NEAT",
|
|
}
|
|
functionDocs := []map[string]interface{}{
|
|
{
|
|
"DeviceClass": "NetworkController",
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x1021",
|
|
},
|
|
}
|
|
|
|
got := parsePCIeDevice(doc, functionDocs)
|
|
if got.DeviceClass == "SingleFunction" {
|
|
t.Fatalf("device class should not keep generic redfish DeviceType")
|
|
}
|
|
if got.DeviceClass == "" {
|
|
t.Fatalf("device class should be resolved")
|
|
}
|
|
}
|
|
|
|
func TestParseComponents_UseNestedSerialNumberFallback(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"Name": "dev0",
|
|
"Id": "dev0",
|
|
"Model": "model0",
|
|
"Manufacturer": "vendor0",
|
|
"SerialNumber": "N/A",
|
|
"Oem": map[string]interface{}{
|
|
"SerialNumber": "SN-OK-001",
|
|
},
|
|
}
|
|
|
|
cpus := parseCPUs([]map[string]interface{}{doc})
|
|
if len(cpus) != 1 || cpus[0].SerialNumber != "SN-OK-001" {
|
|
t.Fatalf("expected CPU serial fallback, got %+v", cpus)
|
|
}
|
|
|
|
dimms := parseMemory([]map[string]interface{}{doc})
|
|
if len(dimms) != 1 || dimms[0].SerialNumber != "SN-OK-001" {
|
|
t.Fatalf("expected DIMM serial fallback, got %+v", dimms)
|
|
}
|
|
|
|
drive := parseDrive(doc)
|
|
if drive.SerialNumber != "SN-OK-001" {
|
|
t.Fatalf("expected drive serial fallback, got %q", drive.SerialNumber)
|
|
}
|
|
|
|
nic := parseNIC(doc)
|
|
if nic.SerialNumber != "SN-OK-001" {
|
|
t.Fatalf("expected NIC serial fallback, got %q", nic.SerialNumber)
|
|
}
|
|
|
|
psu := parsePSU(doc, 1)
|
|
if psu.SerialNumber != "SN-OK-001" {
|
|
t.Fatalf("expected PSU serial fallback, got %q", psu.SerialNumber)
|
|
}
|
|
|
|
pcie := parsePCIeDevice(doc, nil)
|
|
if pcie.SerialNumber != "SN-OK-001" {
|
|
t.Fatalf("expected PCIe device serial fallback, got %q", pcie.SerialNumber)
|
|
}
|
|
|
|
pcieFn := parsePCIeFunction(doc, 1)
|
|
if pcieFn.SerialNumber != "SN-OK-001" {
|
|
t.Fatalf("expected PCIe function serial fallback, got %q", pcieFn.SerialNumber)
|
|
}
|
|
}
|
|
|
|
func TestRedfishCollectionMemberRefs_IncludesOemPublicMembers(t *testing.T) {
|
|
collection := map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/Drives/OB01"},
|
|
},
|
|
"Oem": map[string]interface{}{
|
|
"Public": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/Drives/FP00HDD00"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/Drives/FP00HDD02"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
got := redfishCollectionMemberRefs(collection)
|
|
if len(got) != 3 {
|
|
t.Fatalf("expected 3 member refs, got %d: %v", len(got), got)
|
|
}
|
|
}
|
|
|
|
func TestRecoverCriticalRedfishDocsPlanB_RetriesMembersFromExistingCollection(t *testing.T) {
|
|
t.Setenv("LOGPILE_REDFISH_CRITICAL_COOLDOWN", "0s")
|
|
t.Setenv("LOGPILE_REDFISH_CRITICAL_SLOW_GAP", "0s")
|
|
t.Setenv("LOGPILE_REDFISH_CRITICAL_PLANB_RETRIES", "1")
|
|
t.Setenv("LOGPILE_REDFISH_CRITICAL_RETRIES", "1")
|
|
t.Setenv("LOGPILE_REDFISH_CRITICAL_BACKOFF", "0s")
|
|
|
|
const memberPath = "/redfish/v1/Chassis/1/Drives/FP00HDD00"
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc(memberPath, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"Id": "FP00HDD00",
|
|
"Name": "FP00HDD00",
|
|
"Model": "HDD-TEST",
|
|
"MediaType": "HDD",
|
|
"Protocol": "SAS",
|
|
"CapacityBytes": int64(2000398934016),
|
|
"SerialNumber": "HDD-SN-001",
|
|
})
|
|
})
|
|
ts := httptest.NewServer(mux)
|
|
defer ts.Close()
|
|
|
|
rawTree := map[string]interface{}{
|
|
"/redfish/v1/Chassis/1/Drives": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/Drives/OB01"},
|
|
},
|
|
"Oem": map[string]interface{}{
|
|
"Public": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": memberPath},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
fetchErrs := map[string]string{
|
|
memberPath: "Get \"https://example/redfish/v1/Chassis/1/Drives/FP00HDD00\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)",
|
|
}
|
|
|
|
c := NewRedfishConnector()
|
|
recovered := c.recoverCriticalRedfishDocsPlanB(
|
|
context.Background(),
|
|
ts.Client(),
|
|
Request{},
|
|
ts.URL,
|
|
[]string{"/redfish/v1/Chassis/1/Drives"},
|
|
rawTree,
|
|
fetchErrs,
|
|
nil,
|
|
)
|
|
if recovered == 0 {
|
|
t.Fatalf("expected plan-B to recover at least one document")
|
|
}
|
|
if _, ok := rawTree[memberPath]; !ok {
|
|
t.Fatalf("expected recovered member doc for %s", memberPath)
|
|
}
|
|
if _, ok := fetchErrs[memberPath]; ok {
|
|
t.Fatalf("expected fetch error for %s to be cleared after recovery", memberPath)
|
|
}
|
|
}
|
|
|
|
func TestRecoverCriticalRedfishDocsPlanB_RetriesMembersFromSystemMemoryCollection(t *testing.T) {
|
|
t.Setenv("LOGPILE_REDFISH_CRITICAL_COOLDOWN", "0s")
|
|
t.Setenv("LOGPILE_REDFISH_CRITICAL_SLOW_GAP", "0s")
|
|
t.Setenv("LOGPILE_REDFISH_CRITICAL_PLANB_RETRIES", "1")
|
|
t.Setenv("LOGPILE_REDFISH_CRITICAL_RETRIES", "1")
|
|
t.Setenv("LOGPILE_REDFISH_CRITICAL_BACKOFF", "0s")
|
|
|
|
const systemPath = "/redfish/v1/Systems/1"
|
|
const memoryPath = "/redfish/v1/Systems/1/Memory"
|
|
const dimmPath = "/redfish/v1/Systems/1/Memory/CPU1_C1D1"
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc(dimmPath, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"Id": "CPU1_C1D1",
|
|
"Name": "CPU1_C1D1",
|
|
"DeviceLocator": "CPU1_C1D1",
|
|
"CapacityMiB": 65536,
|
|
"MemoryDeviceType": "DDR5",
|
|
"Status": map[string]interface{}{"State": "Enabled", "Health": "OK"},
|
|
"SerialNumber": "DIMM-SN-001",
|
|
"PartNumber": "DIMM-PN-001",
|
|
})
|
|
})
|
|
ts := httptest.NewServer(mux)
|
|
defer ts.Close()
|
|
|
|
rawTree := map[string]interface{}{
|
|
memoryPath: map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": dimmPath},
|
|
},
|
|
},
|
|
}
|
|
fetchErrs := map[string]string{
|
|
dimmPath: `Get "https://example/redfish/v1/Systems/1/Memory/CPU1_C1D1": context deadline exceeded (Client.Timeout exceeded while awaiting headers)`,
|
|
}
|
|
|
|
criticalPaths := redfishCriticalEndpoints([]string{systemPath}, nil, nil)
|
|
hasMemoryPath := false
|
|
for _, p := range criticalPaths {
|
|
if p == memoryPath {
|
|
hasMemoryPath = true
|
|
break
|
|
}
|
|
}
|
|
if !hasMemoryPath {
|
|
t.Fatalf("expected critical endpoints to include %s", memoryPath)
|
|
}
|
|
|
|
c := NewRedfishConnector()
|
|
recovered := c.recoverCriticalRedfishDocsPlanB(
|
|
context.Background(),
|
|
ts.Client(),
|
|
Request{},
|
|
ts.URL,
|
|
criticalPaths,
|
|
rawTree,
|
|
fetchErrs,
|
|
nil,
|
|
)
|
|
if recovered == 0 {
|
|
t.Fatalf("expected plan-B to recover at least one DIMM document")
|
|
}
|
|
if _, ok := rawTree[dimmPath]; !ok {
|
|
t.Fatalf("expected recovered DIMM doc for %s", dimmPath)
|
|
}
|
|
if _, ok := fetchErrs[dimmPath]; ok {
|
|
t.Fatalf("expected DIMM fetch error for %s to be cleared", dimmPath)
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectStorage_ProbesSupermicroNVMeDiskBayWhenCollectionEmpty(t *testing.T) {
|
|
r := redfishSnapshotReader{tree: map[string]interface{}{
|
|
"/redfish/v1/Systems": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/NVMeSSD"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/NVMeSSD": map[string]interface{}{
|
|
"Drives": map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/NVMeSSD/Drives"},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/NVMeSSD/Drives": map[string]interface{}{
|
|
"Members": []interface{}{},
|
|
},
|
|
"/redfish/v1/Chassis": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane": map[string]interface{}{
|
|
"Drives": map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives"},
|
|
},
|
|
"/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives": map[string]interface{}{
|
|
"Members@odata.count": 0,
|
|
"Members": []interface{}{},
|
|
},
|
|
"/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives/Disk.Bay.0": map[string]interface{}{
|
|
"Id": "Disk.Bay.0",
|
|
"Name": "Disk.Bay.0",
|
|
"Manufacturer": "INTEL",
|
|
"SerialNumber": "BTLJ035203XT1P0FGN",
|
|
"Model": "INTEL SSDPE2KX010T8",
|
|
"CapacityBytes": int64(1000204886016),
|
|
"Protocol": "NVMe",
|
|
"MediaType": "SSD",
|
|
"Status": map[string]interface{}{"State": "Enabled", "Health": "OK"},
|
|
},
|
|
}}
|
|
|
|
got := r.collectStorage("/redfish/v1/Systems/1")
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected one drive from direct Disk.Bay probe, got %d", len(got))
|
|
}
|
|
if got[0].SerialNumber != "BTLJ035203XT1P0FGN" {
|
|
t.Fatalf("unexpected serial: %q", got[0].SerialNumber)
|
|
}
|
|
if got[0].SizeGB == 0 {
|
|
t.Fatalf("expected size to be parsed from CapacityBytes")
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectGPUs_DoesNotCollapseOnPlaceholderSerialAndSkipsNIC(t *testing.T) {
|
|
r := redfishSnapshotReader{tree: map[string]interface{}{
|
|
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/3"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/9"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/7"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/3": map[string]interface{}{
|
|
"Id": "3",
|
|
"Name": "PCIeCard3",
|
|
"Model": "H200-SXM5-141G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "N/A",
|
|
"Oem": map[string]interface{}{
|
|
"Public": map[string]interface{}{
|
|
"DeviceClass": "DisplayController",
|
|
},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/9": map[string]interface{}{
|
|
"Id": "9",
|
|
"Name": "PCIeCard9",
|
|
"Model": "H200-SXM5-141G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "N/A",
|
|
"Oem": map[string]interface{}{
|
|
"Public": map[string]interface{}{
|
|
"DeviceClass": "DisplayController",
|
|
},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/7": map[string]interface{}{
|
|
"Id": "7",
|
|
"Name": "PCIeCard7",
|
|
"Model": "MCX631102AN-ADAT",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "MT2538J00CZE",
|
|
"Oem": map[string]interface{}{
|
|
"Public": map[string]interface{}{
|
|
"DeviceClass": "NetworkController",
|
|
},
|
|
},
|
|
},
|
|
}}
|
|
|
|
got := r.collectGPUs(nil, []string{"/redfish/v1/Chassis/1"})
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected 2 GPUs (two H200 cards), got %d", len(got))
|
|
}
|
|
for _, gpu := range got {
|
|
if gpu.Model == "MCX631102AN-ADAT" {
|
|
t.Fatalf("network adapter should not be classified as GPU")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseBoardInfo_NormalizesNullPlaceholders(t *testing.T) {
|
|
got := parseBoardInfo(map[string]interface{}{
|
|
"Manufacturer": "NULL",
|
|
"Model": "NULL",
|
|
"SerialNumber": "23E100051",
|
|
"PartNumber": "0 ",
|
|
"UUID": "fa403f6f-2ee9-11f0-bab9-346f1104085a",
|
|
})
|
|
if got.Manufacturer != "" {
|
|
t.Fatalf("expected empty manufacturer, got %q", got.Manufacturer)
|
|
}
|
|
if got.ProductName != "" {
|
|
t.Fatalf("expected empty product name, got %q", got.ProductName)
|
|
}
|
|
if got.PartNumber != "" {
|
|
t.Fatalf("expected empty part number, got %q", got.PartNumber)
|
|
}
|
|
if got.SerialNumber != "23E100051" {
|
|
t.Fatalf("unexpected serial number: %q", got.SerialNumber)
|
|
}
|
|
}
|
|
|
|
func TestShouldCrawlPath_SkipsJsonSchemas(t *testing.T) {
|
|
if shouldCrawlPath("/redfish/v1/JsonSchemas") {
|
|
t.Fatalf("expected /JsonSchemas to be skipped")
|
|
}
|
|
if shouldCrawlPath("/redfish/v1/JsonSchemas/ComputerSystem.v1_8_0") {
|
|
t.Fatalf("expected JsonSchemas members to be skipped")
|
|
}
|
|
if !shouldCrawlPath("/redfish/v1/Systems/1") {
|
|
t.Fatalf("expected normal hardware path to be crawled")
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectGPUs_FromGraphicsControllers(t *testing.T) {
|
|
r := redfishSnapshotReader{tree: map[string]interface{}{
|
|
"/redfish/v1/Systems/1/GraphicsControllers": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/GraphicsControllers/GPU0"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/GraphicsControllers/GPU1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/GraphicsControllers/GPU0": map[string]interface{}{
|
|
"Id": "GPU0",
|
|
"Name": "GPU0",
|
|
"Model": "H200-SXM5-141G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "1654225094493",
|
|
"Status": map[string]interface{}{"State": "Enabled", "Health": "OK"},
|
|
},
|
|
"/redfish/v1/Systems/1/GraphicsControllers/GPU1": map[string]interface{}{
|
|
"Id": "GPU1",
|
|
"Name": "GPU1",
|
|
"Model": "H200-SXM5-141G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "1654425002635",
|
|
"Status": map[string]interface{}{"State": "Enabled", "Health": "OK"},
|
|
},
|
|
}}
|
|
|
|
got := r.collectGPUs([]string{"/redfish/v1/Systems/1"}, nil)
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected 2 GPUs from GraphicsControllers, got %d", len(got))
|
|
}
|
|
if got[0].SerialNumber == "" || got[1].SerialNumber == "" {
|
|
t.Fatalf("expected GPU serial numbers from GraphicsControllers")
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectGPUs_DedupUsesRedfishPathBeforeHeuristics(t *testing.T) {
|
|
r := redfishSnapshotReader{tree: map[string]interface{}{
|
|
"/redfish/v1/Systems/1/GraphicsControllers": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/GraphicsControllers/GPU0"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/GraphicsControllers/GPU1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/GraphicsControllers/GPU0": map[string]interface{}{
|
|
"Id": "GPU0",
|
|
"Name": "H100-PCIE-80G",
|
|
"Model": "H100-PCIE-80G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "N/A",
|
|
},
|
|
"/redfish/v1/Systems/1/GraphicsControllers/GPU1": map[string]interface{}{
|
|
"Id": "GPU1",
|
|
"Name": "H100-PCIE-80G",
|
|
"Model": "H100-PCIE-80G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "N/A",
|
|
},
|
|
}}
|
|
|
|
got := r.collectGPUs([]string{"/redfish/v1/Systems/1"}, nil)
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected both GPUs to be kept by unique redfish path, got %d", len(got))
|
|
}
|
|
}
|
|
|
|
func TestParseGPU_UsesNestedOemSerialNumber(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"Id": "GPU4",
|
|
"Name": "H100-PCIE-80G",
|
|
"Model": "H100-PCIE-80G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "N/A",
|
|
"Oem": map[string]interface{}{
|
|
"SerialNumber": "1794024010533",
|
|
},
|
|
}
|
|
|
|
got := parseGPU(doc, nil, 1)
|
|
if got.SerialNumber != "1794024010533" {
|
|
t.Fatalf("expected nested OEM serial number, got %q", got.SerialNumber)
|
|
}
|
|
}
|
|
|
|
func TestParseBoardInfoWithFallback_UsesFRU(t *testing.T) {
|
|
system := map[string]interface{}{
|
|
"Manufacturer": "NULL",
|
|
"Model": "NULL",
|
|
"SerialNumber": "23E100051",
|
|
"PartNumber": "0",
|
|
}
|
|
chassis := map[string]interface{}{
|
|
"Manufacturer": "NULL",
|
|
"Model": "NULL",
|
|
}
|
|
fru := map[string]interface{}{
|
|
"FRUInfo": map[string]interface{}{
|
|
"Board": map[string]interface{}{
|
|
"Manufacturer": "Kaytus",
|
|
"ProductName": "KR4268X2",
|
|
},
|
|
},
|
|
}
|
|
|
|
got := parseBoardInfoWithFallback(system, chassis, fru)
|
|
if got.ProductName != "KR4268X2" {
|
|
t.Fatalf("expected product from FRU, got %q", got.ProductName)
|
|
}
|
|
if got.Manufacturer != "Kaytus" {
|
|
t.Fatalf("expected manufacturer from FRU, got %q", got.Manufacturer)
|
|
}
|
|
if got.SerialNumber != "23E100051" {
|
|
t.Fatalf("expected serial from system, got %q", got.SerialNumber)
|
|
}
|
|
}
|
|
|
|
func TestReplayRedfishFromRawPayloads_AddsMissingServerModelWarning(t *testing.T) {
|
|
raw := map[string]any{
|
|
"redfish_tree": map[string]interface{}{
|
|
"/redfish/v1": map[string]interface{}{
|
|
"Systems": map[string]interface{}{"@odata.id": "/redfish/v1/Systems"},
|
|
"Chassis": map[string]interface{}{"@odata.id": "/redfish/v1/Chassis"},
|
|
"Managers": map[string]interface{}{"@odata.id": "/redfish/v1/Managers"},
|
|
},
|
|
"/redfish/v1/Systems": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1": map[string]interface{}{
|
|
"Manufacturer": "NULL",
|
|
"Model": "NULL",
|
|
"SerialNumber": "23E100051",
|
|
},
|
|
"/redfish/v1/Chassis": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1": map[string]interface{}{
|
|
"Id": "1",
|
|
"Manufacturer": "NULL",
|
|
"Model": "NULL",
|
|
},
|
|
"/redfish/v1/Managers": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Managers/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Managers/1": map[string]interface{}{
|
|
"Id": "1",
|
|
},
|
|
},
|
|
"redfish_fetch_errors": []map[string]interface{}{
|
|
{"path": "/redfish/v1/Systems/1/Oem/Public/FRU", "error": "status 500"},
|
|
},
|
|
}
|
|
|
|
got, err := ReplayRedfishFromRawPayloads(raw, nil)
|
|
if err != nil {
|
|
t.Fatalf("replay failed: %v", err)
|
|
}
|
|
if got.Hardware == nil {
|
|
t.Fatalf("expected hardware")
|
|
}
|
|
if got.Hardware.BoardInfo.ProductName != "" {
|
|
t.Fatalf("expected empty model for warning test, got %q", got.Hardware.BoardInfo.ProductName)
|
|
}
|
|
found := false
|
|
for _, ev := range got.Events {
|
|
if ev.Source == "Redfish" && ev.EventType == "Collection Warning" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected collection warning event about missing server model")
|
|
}
|
|
}
|
|
|
|
func TestReplayRedfishFromRawPayloads_AddsDriveFetchWarning(t *testing.T) {
|
|
raw := map[string]any{
|
|
"redfish_tree": map[string]interface{}{
|
|
"/redfish/v1": map[string]interface{}{
|
|
"Systems": map[string]interface{}{"@odata.id": "/redfish/v1/Systems"},
|
|
"Chassis": map[string]interface{}{"@odata.id": "/redfish/v1/Chassis"},
|
|
"Managers": map[string]interface{}{"@odata.id": "/redfish/v1/Managers"},
|
|
},
|
|
"/redfish/v1/Systems": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1": map[string]interface{}{
|
|
"Manufacturer": "Inspur",
|
|
"Model": "NF5688M7",
|
|
"SerialNumber": "23E100051",
|
|
},
|
|
"/redfish/v1/Chassis": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1": map[string]interface{}{
|
|
"Id": "1",
|
|
"Manufacturer": "Inspur",
|
|
"Model": "NF5688M7",
|
|
},
|
|
"/redfish/v1/Managers": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Managers/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Managers/1": map[string]interface{}{
|
|
"Id": "1",
|
|
},
|
|
},
|
|
"redfish_fetch_errors": []map[string]interface{}{
|
|
{
|
|
"path": "/redfish/v1/Chassis/1/Drives/FP00HDD00",
|
|
"error": `Get "...": context deadline exceeded (Client.Timeout exceeded while awaiting headers)`,
|
|
},
|
|
},
|
|
}
|
|
|
|
got, err := ReplayRedfishFromRawPayloads(raw, nil)
|
|
if err != nil {
|
|
t.Fatalf("replay failed: %v", err)
|
|
}
|
|
|
|
found := false
|
|
for _, ev := range got.Events {
|
|
if ev.Source == "Redfish" &&
|
|
ev.EventType == "Collection Warning" &&
|
|
strings.Contains(strings.ToLower(ev.Description), "drive documents were unavailable") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected collection warning event for drive fetch errors")
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectGPUs_SkipsModelOnlyDuplicateFromGraphicsControllers(t *testing.T) {
|
|
r := redfishSnapshotReader{tree: map[string]interface{}{
|
|
"/redfish/v1/Systems/1/PCIeDevices": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/PCIeDevices/3"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/PCIeDevices/9"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/PCIeDevices/3": map[string]interface{}{
|
|
"Id": "3",
|
|
"Name": "PCIeCard3",
|
|
"Model": "H200-SXM5-141G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "1654225094493",
|
|
},
|
|
"/redfish/v1/Systems/1/PCIeDevices/9": map[string]interface{}{
|
|
"Id": "9",
|
|
"Name": "PCIeCard9",
|
|
"Model": "H200-SXM5-141G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "1654425002635",
|
|
},
|
|
"/redfish/v1/Systems/1/GraphicsControllers": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/GraphicsControllers/GPU0"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/GraphicsControllers/GPU0": map[string]interface{}{
|
|
"Id": "GPU0",
|
|
"Name": "H200-SXM5-141G",
|
|
"Model": "H200-SXM5-141G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "N/A",
|
|
},
|
|
}}
|
|
|
|
got := r.collectGPUs([]string{"/redfish/v1/Systems/1"}, nil)
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected 2 GPUs without generic duplicate, got %d", len(got))
|
|
}
|
|
for _, gpu := range got {
|
|
if gpu.Slot == "H200-SXM5-141G" {
|
|
t.Fatalf("unexpected model-only duplicate GPU row")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestApplyBoardInfoFallbackFromDocs_SkipsComponentProductNames(t *testing.T) {
|
|
board := models.BoardInfo{
|
|
SerialNumber: "23E100051",
|
|
}
|
|
docs := []map[string]interface{}{
|
|
{
|
|
"Model": "DDR5 DIMM",
|
|
"Manufacturer": "DELTA",
|
|
"SerialNumber": "802C1A2507D284B001",
|
|
},
|
|
{
|
|
"PlatformId": "NF5688M7",
|
|
"Manufacturer": "Inspur",
|
|
"PartNumber": "YZMB-00001",
|
|
},
|
|
}
|
|
|
|
applyBoardInfoFallbackFromDocs(&board, docs)
|
|
if board.ProductName != "NF5688M7" {
|
|
t.Fatalf("expected server model from fallback docs, got %q", board.ProductName)
|
|
}
|
|
if board.Manufacturer != "Inspur" {
|
|
t.Fatalf("expected manufacturer from server fallback doc, got %q", board.Manufacturer)
|
|
}
|
|
}
|
|
|
|
func TestDedupeStorage_IgnoresPlaceholderSerial(t *testing.T) {
|
|
in := []models.Storage{
|
|
{Slot: "OB01", Model: "N/A", SerialNumber: "N/A"},
|
|
{Slot: "OB02", Model: "N/A", SerialNumber: "N/A"},
|
|
{Slot: "OB03", Model: "N/A", SerialNumber: "N/A"},
|
|
{Slot: "OB04", Model: "N/A", SerialNumber: "N/A"},
|
|
}
|
|
|
|
out := dedupeStorage(in)
|
|
if len(out) != 4 {
|
|
t.Fatalf("expected all placeholder-serial NVMe drives to be kept by slot key, got %d", len(out))
|
|
}
|
|
}
|
|
|
|
func TestDedupeStorage_MergesPlaceholderSlotsWithRichDrivesByOrder(t *testing.T) {
|
|
in := []models.Storage{
|
|
{Slot: "PCIe8_RAID_Disk_1:0", Type: "SSD", Model: "SOLIDIGM SSDSC2K", SizeGB: 1787, SerialNumber: "S1", Present: true},
|
|
{Slot: "PCIe8_RAID_Disk_1:1", Type: "SSD", Model: "SOLIDIGM SSDSC2K", SizeGB: 1787, SerialNumber: "S2", Present: true},
|
|
{Slot: "PCIe8_RAID_Disk_1:2", Type: "SSD", Model: "SOLIDIGM SSDSC2K", SizeGB: 1787, SerialNumber: "S3", Present: true},
|
|
{Slot: "OB01", Type: "NVMe", Model: "N/A", SerialNumber: "N/A", Present: true},
|
|
{Slot: "OB02", Type: "NVMe", Model: "N/A", SerialNumber: "N/A", Present: true},
|
|
{Slot: "OB03", Type: "NVMe", Model: "N/A", SerialNumber: "N/A", Present: true},
|
|
{Slot: "OB04", Type: "NVMe", Model: "N/A", SerialNumber: "N/A", Present: true},
|
|
{Slot: "FP00HDD00", Type: "NVMe", Model: "INTEL SSDPE2KE032T8", SizeGB: 2980, SerialNumber: "N1", Present: true},
|
|
{Slot: "FP00HDD02", Type: "NVMe", Model: "INTEL SSDPE2KE032T8", SizeGB: 2980, SerialNumber: "N2", Present: true},
|
|
{Slot: "FP00HDD04", Type: "NVMe", Model: "INTEL SSDPE2KE032T8", SizeGB: 2980, SerialNumber: "N3", Present: true},
|
|
{Slot: "FP00HDD06", Type: "NVMe", Model: "INTEL SSDPE2KE032T8", SizeGB: 2980, SerialNumber: "N4", Present: true},
|
|
}
|
|
|
|
out := dedupeStorage(in)
|
|
if len(out) != 7 {
|
|
t.Fatalf("expected 7 rows after placeholder merge, got %d", len(out))
|
|
}
|
|
|
|
bySlot := make(map[string]models.Storage, len(out))
|
|
for _, d := range out {
|
|
bySlot[d.Slot] = d
|
|
if strings.HasPrefix(d.Slot, "FP00HDD") {
|
|
t.Fatalf("expected FP donor slot %q to be absorbed by placeholder slot", d.Slot)
|
|
}
|
|
}
|
|
if bySlot["OB01"].SerialNumber != "N1" || bySlot["OB02"].SerialNumber != "N2" || bySlot["OB03"].SerialNumber != "N3" || bySlot["OB04"].SerialNumber != "N4" {
|
|
t.Fatalf("expected OB slots to be enriched in order, got OB01=%q OB02=%q OB03=%q OB04=%q",
|
|
bySlot["OB01"].SerialNumber, bySlot["OB02"].SerialNumber, bySlot["OB03"].SerialNumber, bySlot["OB04"].SerialNumber)
|
|
}
|
|
if bySlot["OB01"].Model != "INTEL SSDPE2KE032T8" || bySlot["OB01"].SizeGB != 2980 {
|
|
t.Fatalf("expected OB01 to inherit rich model/size, got model=%q size=%d", bySlot["OB01"].Model, bySlot["OB01"].SizeGB)
|
|
}
|
|
}
|
|
|
|
func TestDedupeNetworkAdapters_MergesBySlotAndKeepsRicherData(t *testing.T) {
|
|
in := []models.NetworkAdapter{
|
|
{
|
|
Slot: "NIC-A",
|
|
Model: "N/A",
|
|
Vendor: "",
|
|
Present: true,
|
|
},
|
|
{
|
|
Slot: "NIC-A",
|
|
Model: "ConnectX-7",
|
|
Vendor: "NVIDIA",
|
|
SerialNumber: "NICSN001",
|
|
Firmware: "28.41.2020",
|
|
PortCount: 2,
|
|
MACAddresses: []string{"00:11:22:33:44:55"},
|
|
Present: true,
|
|
},
|
|
}
|
|
|
|
out := dedupeNetworkAdapters(in)
|
|
if len(out) != 1 {
|
|
t.Fatalf("expected merged single NIC row, got %d", len(out))
|
|
}
|
|
if out[0].SerialNumber != "NICSN001" || out[0].Model != "ConnectX-7" || out[0].Vendor != "NVIDIA" {
|
|
t.Fatalf("expected richer NIC fields preserved, got %+v", out[0])
|
|
}
|
|
}
|
|
|
|
func TestDedupePCIeDevices_MergesByLooseKeyAndKeepsBDF(t *testing.T) {
|
|
in := []models.PCIeDevice{
|
|
{
|
|
Slot: "PCIe Slot 3",
|
|
DeviceClass: "Network Controller",
|
|
PartNumber: "MCX75310AAS-NEAT",
|
|
},
|
|
{
|
|
Slot: "PCIe Slot 3",
|
|
DeviceClass: "Network Controller",
|
|
PartNumber: "MCX75310AAS-NEAT",
|
|
BDF: "0000:af:00.0",
|
|
VendorID: 0x15b3,
|
|
DeviceID: 0x1021,
|
|
SerialNumber: "MT000123",
|
|
},
|
|
}
|
|
|
|
out := dedupePCIeDevices(in)
|
|
if len(out) != 1 {
|
|
t.Fatalf("expected merged single PCIe row, got %d", len(out))
|
|
}
|
|
if out[0].BDF != "0000:af:00.0" || out[0].SerialNumber != "MT000123" || out[0].VendorID == 0 || out[0].DeviceID == 0 {
|
|
t.Fatalf("expected richer PCIe fields preserved, got %+v", out[0])
|
|
}
|
|
}
|
|
|
|
func TestAppendPSU_MergesRicherDuplicate(t *testing.T) {
|
|
var out []models.PSU
|
|
seen := make(map[string]int)
|
|
idx := 1
|
|
idx = appendPSU(&out, seen, models.PSU{
|
|
Slot: "PSU1",
|
|
Model: "N/A",
|
|
Present: true,
|
|
}, idx)
|
|
_ = appendPSU(&out, seen, models.PSU{
|
|
Slot: "PSU1",
|
|
Model: "DLG2700BW54C31",
|
|
SerialNumber: "DGPLV2515025L",
|
|
WattageW: 2700,
|
|
Firmware: "00.01.04",
|
|
Present: true,
|
|
}, idx)
|
|
|
|
if len(out) != 1 {
|
|
t.Fatalf("expected PSU duplicate merge, got %d rows", len(out))
|
|
}
|
|
if out[0].SerialNumber != "DGPLV2515025L" || out[0].WattageW != 2700 || out[0].Model != "DLG2700BW54C31" {
|
|
t.Fatalf("expected richer PSU fields preserved, got %+v", out[0])
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectGPUs_DropsModelOnlyPlaceholderWhenConcreteDiscoveredLater(t *testing.T) {
|
|
r := redfishSnapshotReader{tree: map[string]interface{}{
|
|
"/redfish/v1/Systems/1/GraphicsControllers": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/GraphicsControllers/GPU0"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/GraphicsControllers/GPU0": map[string]interface{}{
|
|
"Id": "GPU0",
|
|
"Name": "H200-SXM5-141G",
|
|
"Model": "H200-SXM5-141G",
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/4"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/4": map[string]interface{}{
|
|
"Id": "4",
|
|
"Name": "PCIeCard4",
|
|
"Model": "H200-SXM5-141G",
|
|
"Manufacturer": "NVIDIA",
|
|
"BDF": "0000:0f:00.0",
|
|
},
|
|
}}
|
|
|
|
got := r.collectGPUs([]string{"/redfish/v1/Systems/1"}, []string{"/redfish/v1/Chassis/1"})
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected generic graphics placeholder to be dropped, got %d GPUs", len(got))
|
|
}
|
|
if got[0].Slot != "PCIeCard4" {
|
|
t.Fatalf("expected concrete PCIe GPU to remain, got slot=%q", got[0].Slot)
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectGPUs_MergesGraphicsSerialIntoConcretePCIeGPU(t *testing.T) {
|
|
r := redfishSnapshotReader{tree: map[string]interface{}{
|
|
"/redfish/v1/Systems/1/GraphicsControllers": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/GraphicsControllers/GPU4"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/GraphicsControllers/GPU4": map[string]interface{}{
|
|
"Id": "4",
|
|
"Name": "H100-PCIE-80G",
|
|
"Model": "H100-PCIE-80G",
|
|
"Manufacturer": "NVIDIA",
|
|
"Oem": map[string]interface{}{
|
|
"SerialNumber": "1794024010533",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/8"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/8": map[string]interface{}{
|
|
"Id": "8",
|
|
"Name": "PCIeCard8",
|
|
"Model": "H100-PCIE-80G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "N/A",
|
|
"BDF": "0000:b1:00.0",
|
|
},
|
|
}}
|
|
|
|
got := r.collectGPUs([]string{"/redfish/v1/Systems/1"}, []string{"/redfish/v1/Chassis/1"})
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected merged single GPU row, got %d", len(got))
|
|
}
|
|
if got[0].Slot != "PCIeCard8" {
|
|
t.Fatalf("expected concrete PCIe slot, got %q", got[0].Slot)
|
|
}
|
|
if got[0].SerialNumber != "1794024010533" {
|
|
t.Fatalf("expected merged serial from graphics controller, got %q", got[0].SerialNumber)
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectGPUs_MergesAmbiguousSameModelByOrder(t *testing.T) {
|
|
tree := map[string]interface{}{
|
|
"/redfish/v1/Systems/1/GraphicsControllers": map[string]interface{}{
|
|
"Members": []interface{}{},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
|
|
"Members": []interface{}{},
|
|
},
|
|
}
|
|
|
|
pcieIDs := []int{4, 8, 12, 14, 20, 23, 26, 30}
|
|
serials := []string{
|
|
"1654425002361",
|
|
"1654425004310",
|
|
"1654425004204",
|
|
"1654225097289",
|
|
"1654225095717",
|
|
"1654425002114",
|
|
"1654425002714",
|
|
"1654425002991",
|
|
}
|
|
for i := 0; i < len(pcieIDs); i++ {
|
|
gpuPath := fmt.Sprintf("/redfish/v1/Systems/1/GraphicsControllers/GPU%d", i+1)
|
|
pciePath := fmt.Sprintf("/redfish/v1/Chassis/1/PCIeDevices/%d", pcieIDs[i])
|
|
tree["/redfish/v1/Systems/1/GraphicsControllers"].(map[string]interface{})["Members"] =
|
|
append(tree["/redfish/v1/Systems/1/GraphicsControllers"].(map[string]interface{})["Members"].([]interface{}), map[string]interface{}{"@odata.id": gpuPath})
|
|
tree["/redfish/v1/Chassis/1/PCIeDevices"].(map[string]interface{})["Members"] =
|
|
append(tree["/redfish/v1/Chassis/1/PCIeDevices"].(map[string]interface{})["Members"].([]interface{}), map[string]interface{}{"@odata.id": pciePath})
|
|
|
|
tree[gpuPath] = map[string]interface{}{
|
|
"Id": fmt.Sprintf("GPU%d", i+1),
|
|
"Name": "H200-SXM5-141G",
|
|
"Model": "H200-SXM5-141G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": serials[i],
|
|
}
|
|
tree[pciePath] = map[string]interface{}{
|
|
"Id": fmt.Sprintf("%d", pcieIDs[i]),
|
|
"Name": fmt.Sprintf("PCIeCard%d", pcieIDs[i]),
|
|
"Model": "H200-SXM5-141G",
|
|
"Manufacturer": "NVIDIA",
|
|
"BDF": fmt.Sprintf("0000:%02x:00.0", i+1),
|
|
}
|
|
}
|
|
|
|
r := redfishSnapshotReader{tree: tree}
|
|
got := r.collectGPUs([]string{"/redfish/v1/Systems/1"}, []string{"/redfish/v1/Chassis/1"})
|
|
if len(got) != len(pcieIDs) {
|
|
t.Fatalf("expected %d merged GPUs, got %d", len(pcieIDs), len(got))
|
|
}
|
|
|
|
bySlot := make(map[string]models.GPU, len(got))
|
|
for _, gpu := range got {
|
|
bySlot[gpu.Slot] = gpu
|
|
if strings.EqualFold(strings.TrimSpace(gpu.Slot), strings.TrimSpace(gpu.Model)) {
|
|
t.Fatalf("expected model-only placeholder to be dropped, got slot=%q", gpu.Slot)
|
|
}
|
|
}
|
|
for i, id := range pcieIDs {
|
|
slot := fmt.Sprintf("PCIeCard%d", id)
|
|
gpu, ok := bySlot[slot]
|
|
if !ok {
|
|
t.Fatalf("expected concrete slot %q in output", slot)
|
|
}
|
|
if gpu.SerialNumber != serials[i] {
|
|
t.Fatalf("expected slot %s serial %s, got %s", slot, serials[i], gpu.SerialNumber)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestShouldCrawlPath_MemorySubresourcesAreSkipped(t *testing.T) {
|
|
if !shouldCrawlPath("/redfish/v1/Systems/1/Memory/CPU0_C0D0") {
|
|
t.Fatalf("expected direct DIMM resource to be crawlable")
|
|
}
|
|
if shouldCrawlPath("/redfish/v1/Systems/1/Memory/CPU0_C0D0/Assembly") {
|
|
t.Fatalf("expected DIMM assembly subresource to be skipped")
|
|
}
|
|
if shouldCrawlPath("/redfish/v1/Systems/1/Memory/CPU0_C0D0/MemoryMetrics") {
|
|
t.Fatalf("expected DIMM metrics subresource to be skipped")
|
|
}
|
|
if shouldCrawlPath("/redfish/v1/Chassis/1/PCIeDevices/0/PCIeFunctions/1") {
|
|
t.Fatalf("expected noisy chassis pciefunctions branch to be skipped")
|
|
}
|
|
}
|
|
|
|
func TestIsRedfishMemoryMemberPath(t *testing.T) {
|
|
cases := []struct {
|
|
path string
|
|
want bool
|
|
}{
|
|
{path: "/redfish/v1/Systems/1/Memory", want: false},
|
|
{path: "/redfish/v1/Systems/1/Memory/CPU0_C0D0", want: true},
|
|
{path: "/redfish/v1/Systems/1/Memory/CPU0_C0D0/Assembly", want: false},
|
|
{path: "/redfish/v1/Systems/1/Memory/CPU0_C0D0/MemoryMetrics", want: false},
|
|
{path: "/redfish/v1/Chassis/1/Memory/CPU0_C0D0", want: false},
|
|
}
|
|
for _, tc := range cases {
|
|
got := isRedfishMemoryMemberPath(tc.path)
|
|
if got != tc.want {
|
|
t.Fatalf("isRedfishMemoryMemberPath(%q) = %v, want %v", tc.path, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRedfishSnapshotBranchKey(t *testing.T) {
|
|
cases := []struct {
|
|
path string
|
|
want string
|
|
}{
|
|
{path: "", want: ""},
|
|
{path: "/redfish/v1", want: ""},
|
|
{path: "/redfish/v1/Systems", want: "/redfish/v1/Systems"},
|
|
{path: "/redfish/v1/Systems/1", want: "/redfish/v1/Systems/1"},
|
|
{path: "/redfish/v1/Systems/1/Memory", want: "/redfish/v1/Systems/1/Memory"},
|
|
{path: "/redfish/v1/Systems/1/Memory/CPU0_C0D0", want: "/redfish/v1/Systems/1/Memory"},
|
|
{path: "/redfish/v1/Systems/1/PCIeDevices/GPU1", want: "/redfish/v1/Systems/1/PCIeDevices"},
|
|
{path: "/redfish/v1/Chassis/1/Sensors/1", want: "/redfish/v1/Chassis/1/Sensors"},
|
|
{path: "/redfish/v1/UpdateService/FirmwareInventory/BIOS", want: "/redfish/v1/UpdateService/FirmwareInventory"},
|
|
}
|
|
for _, tc := range cases {
|
|
got := redfishSnapshotBranchKey(tc.path)
|
|
if got != tc.want {
|
|
t.Fatalf("redfishSnapshotBranchKey(%q) = %q, want %q", tc.path, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestShouldPostProbeCollectionPath(t *testing.T) {
|
|
if shouldPostProbeCollectionPath("/redfish/v1/Chassis/1/Sensors") {
|
|
t.Fatalf("expected sensors collection to be skipped by default")
|
|
}
|
|
t.Setenv("LOGPILE_REDFISH_SENSOR_POSTPROBE", "1")
|
|
if !shouldPostProbeCollectionPath("/redfish/v1/Chassis/1/Sensors") {
|
|
t.Fatalf("expected sensors collection to be post-probed when enabled")
|
|
}
|
|
if !shouldPostProbeCollectionPath("/redfish/v1/Systems/1/Storage/RAID/Drives") {
|
|
t.Fatalf("expected drives collection to be post-probed")
|
|
}
|
|
if shouldPostProbeCollectionPath("/redfish/v1/Chassis/1/Boards/BOARD1") {
|
|
t.Fatalf("expected board member resource to be skipped from post-probe")
|
|
}
|
|
if shouldPostProbeCollectionPath("/redfish/v1/Chassis/1/Assembly/Oem/COMMONb/COMMONbAssembly/1") {
|
|
t.Fatalf("expected assembly member resource to be skipped from post-probe")
|
|
}
|
|
}
|
|
|
|
func TestShouldAdaptivePostProbeCollectionPath(t *testing.T) {
|
|
withExplicitNamedMembers := map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/EthernetInterfaces/NIC-0-0"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/EthernetInterfaces/NIC-0-1"},
|
|
},
|
|
}
|
|
if shouldAdaptivePostProbeCollectionPath("/redfish/v1/Systems/1/EthernetInterfaces", withExplicitNamedMembers) {
|
|
t.Fatalf("expected explicit non-numeric members to skip adaptive post-probe")
|
|
}
|
|
|
|
withNumericMembers := map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/1"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/2"},
|
|
},
|
|
}
|
|
if !shouldAdaptivePostProbeCollectionPath("/redfish/v1/Chassis/1/PCIeDevices", withNumericMembers) {
|
|
t.Fatalf("expected numeric members to allow adaptive post-probe")
|
|
}
|
|
|
|
withoutMembers := map[string]interface{}{"Name": "Drives"}
|
|
if !shouldAdaptivePostProbeCollectionPath("/redfish/v1/Chassis/1/Drives", withoutMembers) {
|
|
t.Fatalf("expected missing members to allow adaptive post-probe")
|
|
}
|
|
}
|
|
|
|
func TestShouldAdaptiveNVMeProbe(t *testing.T) {
|
|
withMembers := map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/Drives/OB01"},
|
|
},
|
|
}
|
|
if shouldAdaptiveNVMeProbe(withMembers) {
|
|
t.Fatalf("expected drives collection with explicit members to skip NVMe probe")
|
|
}
|
|
|
|
withoutMembers := map[string]interface{}{"Name": "Drives"}
|
|
if !shouldAdaptiveNVMeProbe(withoutMembers) {
|
|
t.Fatalf("expected drives collection without members to allow NVMe probe")
|
|
}
|
|
}
|
|
|
|
func TestRedfishAdaptivePrefetchTargets(t *testing.T) {
|
|
candidates := []string{
|
|
"/redfish/v1/Systems/1/Memory",
|
|
"/redfish/v1/Systems/1/Processors",
|
|
"/redfish/v1/Systems/1/Storage",
|
|
}
|
|
rawTree := map[string]interface{}{
|
|
"/redfish/v1/Systems/1/Memory": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Memory/DIMM1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/1"},
|
|
},
|
|
},
|
|
}
|
|
fetchErrs := map[string]string{
|
|
"/redfish/v1/Systems/1/Memory/DIMM1": "Get \"https://bmc/redfish/v1/Systems/1/Memory/DIMM1\": context deadline exceeded",
|
|
"/redfish/v1/Systems/1/Storage/1": "status 404 from /redfish/v1/Systems/1/Storage/1: not found",
|
|
"/redfish/v1/Systems/1/Processors": "Get \"https://bmc/redfish/v1/Systems/1/Processors\": context deadline exceeded",
|
|
"/redfish/v1/Systems/1/Storage/Volumes": "status 404 from /redfish/v1/Systems/1/Storage/Volumes: not found",
|
|
}
|
|
|
|
got := redfishAdaptivePrefetchTargets(candidates, rawTree, fetchErrs)
|
|
joined := strings.Join(got, "\n")
|
|
for _, wanted := range []string{
|
|
"/redfish/v1/Systems/1/Memory",
|
|
"/redfish/v1/Systems/1/Processors",
|
|
} {
|
|
if !strings.Contains(joined, wanted) {
|
|
t.Fatalf("expected adaptive prefetch target %q", wanted)
|
|
}
|
|
}
|
|
if strings.Contains(joined, "/redfish/v1/Systems/1/Storage") {
|
|
t.Fatalf("expected storage with only non-retryable missing members to be skipped")
|
|
}
|
|
}
|
|
|
|
func TestRedfishSnapshotPrioritySeeds_DefaultSkipsNoisyBranches(t *testing.T) {
|
|
seeds := redfishSnapshotPrioritySeeds(
|
|
[]string{"/redfish/v1/Systems/1"},
|
|
[]string{"/redfish/v1/Chassis/1"},
|
|
[]string{"/redfish/v1/Managers/1"},
|
|
)
|
|
joined := strings.Join(seeds, "\n")
|
|
for _, noisy := range []string{
|
|
"/redfish/v1/Fabrics",
|
|
"/redfish/v1/Chassis/1/Backplanes",
|
|
"/redfish/v1/Chassis/1/Boards",
|
|
"/redfish/v1/Chassis/1/Sensors",
|
|
"/redfish/v1/Managers/1/LogServices",
|
|
} {
|
|
if strings.Contains(joined, noisy) {
|
|
t.Fatalf("unexpected noisy seed %q", noisy)
|
|
}
|
|
}
|
|
for _, wanted := range []string{
|
|
"/redfish/v1/Systems/1/Memory",
|
|
"/redfish/v1/Systems/1/PCIeDevices",
|
|
"/redfish/v1/Chassis/1/Drives",
|
|
"/redfish/v1/Chassis/1/NetworkAdapters",
|
|
"/redfish/v1/Managers/1/NetworkProtocol",
|
|
} {
|
|
if !strings.Contains(joined, wanted) {
|
|
t.Fatalf("expected seed %q", wanted)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRedfishPrefetchTargets_FilterNoisyBranches(t *testing.T) {
|
|
critical := []string{
|
|
"/redfish/v1/Systems/1",
|
|
"/redfish/v1/Systems/1/Memory",
|
|
"/redfish/v1/Systems/1/Oem/Public/FRU",
|
|
"/redfish/v1/Chassis/1/Drives",
|
|
"/redfish/v1/Chassis/1/Backplanes",
|
|
"/redfish/v1/Chassis/1/Sensors",
|
|
"/redfish/v1/Managers/1/LogServices",
|
|
"/redfish/v1/Managers/1/NetworkProtocol",
|
|
}
|
|
got := redfishPrefetchTargets(critical)
|
|
joined := strings.Join(got, "\n")
|
|
for _, wanted := range []string{
|
|
"/redfish/v1/Systems/1",
|
|
"/redfish/v1/Systems/1/Memory",
|
|
"/redfish/v1/Systems/1/Oem/Public/FRU",
|
|
"/redfish/v1/Chassis/1/Drives",
|
|
"/redfish/v1/Managers/1/NetworkProtocol",
|
|
} {
|
|
if !strings.Contains(joined, wanted) {
|
|
t.Fatalf("expected prefetch target %q", wanted)
|
|
}
|
|
}
|
|
for _, noisy := range []string{
|
|
"/redfish/v1/Chassis/1/Backplanes",
|
|
"/redfish/v1/Chassis/1/Sensors",
|
|
"/redfish/v1/Managers/1/LogServices",
|
|
} {
|
|
if strings.Contains(joined, noisy) {
|
|
t.Fatalf("unexpected noisy prefetch target %q", noisy)
|
|
}
|
|
}
|
|
}
|