parseGPUWithSupplementalDocs did not read PCIeInterface from the device doc, only from function docs. xFusion GPU PCIeCard entries carry link width/speed in PCIeInterface (LanesInUse/Maxlanes/PCIeType/MaxPCIeType) so GPU link width was always empty for xFusion servers. Also apply the xFusion OEM function-level fallback for GPU function docs, consistent with the NIC and PCIeDevice paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4268 lines
146 KiB
Go
4268 lines
146 KiB
Go
package collector
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/collector/redfishprofile"
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
)
|
|
|
|
func testAnalysisPlan(d redfishprofile.AnalysisDirectives) redfishprofile.ResolvedAnalysisPlan {
|
|
return redfishprofile.ResolvedAnalysisPlan{Directives: d}
|
|
}
|
|
|
|
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 TestRedfishConnectorProbe(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{}{
|
|
"@odata.id": "/redfish/v1/Systems/1",
|
|
"PowerState": "Off",
|
|
"Actions": map[string]interface{}{
|
|
"#ComputerSystem.Reset": map[string]interface{}{
|
|
"target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
|
|
"ResetType@Redfish.AllowableValues": []interface{}{"On", "ForceOff"},
|
|
},
|
|
},
|
|
})
|
|
ts := httptest.NewTLSServer(mux)
|
|
defer ts.Close()
|
|
|
|
connector := NewRedfishConnector()
|
|
port := 443
|
|
host := ""
|
|
if u, err := url.Parse(ts.URL); err == nil {
|
|
host = u.Hostname()
|
|
if p := u.Port(); p != "" {
|
|
fmt.Sscanf(p, "%d", &port)
|
|
}
|
|
}
|
|
got, err := connector.Probe(context.Background(), Request{
|
|
Host: host,
|
|
Protocol: "redfish",
|
|
Port: port,
|
|
Username: "admin",
|
|
AuthType: "password",
|
|
Password: "secret",
|
|
TLSMode: "insecure",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("probe failed: %v", err)
|
|
}
|
|
if got == nil || !got.Reachable {
|
|
t.Fatalf("expected reachable probe result, got %+v", got)
|
|
}
|
|
if got.HostPoweredOn {
|
|
t.Fatalf("expected powered off host")
|
|
}
|
|
if got.HostPowerState != "Off" {
|
|
t.Fatalf("expected power state Off, got %q", got.HostPowerState)
|
|
}
|
|
}
|
|
|
|
func TestRedfishConnectorProbe_FallsBackToPowerSummary(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", map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1", map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1",
|
|
"PowerSummary": map[string]interface{}{
|
|
"PowerState": "On",
|
|
},
|
|
"Actions": map[string]interface{}{
|
|
"#ComputerSystem.Reset": map[string]interface{}{
|
|
"target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
|
|
"ResetType@Redfish.AllowableValues": []interface{}{"On", "ForceOff"},
|
|
},
|
|
},
|
|
})
|
|
ts := httptest.NewTLSServer(mux)
|
|
defer ts.Close()
|
|
|
|
connector := NewRedfishConnector()
|
|
port := 443
|
|
host := ""
|
|
if u, err := url.Parse(ts.URL); err == nil {
|
|
host = u.Hostname()
|
|
if p := u.Port(); p != "" {
|
|
fmt.Sscanf(p, "%d", &port)
|
|
}
|
|
}
|
|
got, err := connector.Probe(context.Background(), Request{
|
|
Host: host,
|
|
Protocol: "redfish",
|
|
Port: port,
|
|
Username: "admin",
|
|
AuthType: "password",
|
|
Password: "secret",
|
|
TLSMode: "insecure",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("probe failed: %v", err)
|
|
}
|
|
if got == nil || !got.Reachable {
|
|
t.Fatalf("expected reachable probe result, got %+v", got)
|
|
}
|
|
if !got.HostPoweredOn {
|
|
t.Fatalf("expected powered on host from PowerSummary")
|
|
}
|
|
if got.HostPowerState != "On" {
|
|
t.Fatalf("expected power state On, got %q", got.HostPowerState)
|
|
}
|
|
}
|
|
|
|
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_FallbackCollectionMembersSkipsPlaceholderNumericDocs(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": "Multillect",
|
|
"Model": "MLT-S06",
|
|
"SerialNumber": "430044262001626",
|
|
"Storage": map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage"},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/Storage",
|
|
"Members": []interface{}{},
|
|
"Members@odata.count": 0,
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/1": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/Storage/1",
|
|
"@odata.type": "#Storage.v1_7_1.Storage",
|
|
"Drives": []interface{}{},
|
|
"Drives@odata.count": "0",
|
|
"LogicalDisk": []interface{}{},
|
|
"PhysicalDisk": []interface{}{},
|
|
},
|
|
"/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",
|
|
"NetworkAdapters": map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters"},
|
|
"PCIeDevices": map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices"},
|
|
},
|
|
"/redfish/v1/Chassis/1/NetworkAdapters": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters",
|
|
"Members": []interface{}{},
|
|
"Members@odata.count": 0,
|
|
},
|
|
"/redfish/v1/Chassis/1/NetworkAdapters/1": map[string]interface{}{
|
|
"@odata.context": "/redfish/v1/$metadata#Chassis/Members/1/NetworkAdapters/Members/$entity",
|
|
"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/1",
|
|
"@odata.type": "#NetworkAdapter.v1_0_0.Networkadapter",
|
|
"Id": "1",
|
|
"Name": "1",
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices",
|
|
"Members": []interface{}{},
|
|
"Members@odata.count": 0,
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/1": map[string]interface{}{
|
|
"@odata.context": "/redfish/v1/$metadata#PCIeDevice.PCIeDevice",
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/1",
|
|
"@odata.type": "#PCIeDevice.v1_4_0.PCIeDevice",
|
|
"Id": "1",
|
|
"Name": "PCIe Device",
|
|
},
|
|
"/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",
|
|
},
|
|
},
|
|
}
|
|
|
|
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.NetworkAdapters) != 0 {
|
|
t.Fatalf("expected placeholder network adapters to be skipped, got %d", len(got.Hardware.NetworkAdapters))
|
|
}
|
|
if len(got.Hardware.PCIeDevices) != 0 {
|
|
t.Fatalf("expected placeholder PCIe devices to be skipped, got %d", len(got.Hardware.PCIeDevices))
|
|
}
|
|
if len(got.Hardware.Storage) != 0 {
|
|
t.Fatalf("expected placeholder storage members to be skipped, got %d", len(got.Hardware.Storage))
|
|
}
|
|
}
|
|
|
|
func TestReplayRedfishFromRawPayloads_PrefersActiveBMCInterfaceForBoardMAC(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": "Multillect",
|
|
"Model": "MLT-S06",
|
|
"SerialNumber": "430044262001626",
|
|
},
|
|
"/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/Managers/1/EthernetInterfaces": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Managers/1/EthernetInterfaces/eth0"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Managers/1/EthernetInterfaces/eth1"},
|
|
},
|
|
"Oem": map[string]interface{}{
|
|
"Public": map[string]interface{}{
|
|
"NcsiEnabled": true,
|
|
"LLDP": map[string]interface{}{
|
|
"LLDPMode": "Rx",
|
|
"Members": []interface{}{
|
|
map[string]interface{}{
|
|
"EthIndex": "eth1",
|
|
"ChassisName": "castor.netwell.local",
|
|
"PortDesc": "ge-0/0/17",
|
|
"PortId": "531",
|
|
"VlanId": 20,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"/redfish/v1/Managers/1/EthernetInterfaces/eth0": map[string]interface{}{
|
|
"Id": "eth0",
|
|
"MACAddress": "00:25:6c:70:00:13",
|
|
"LinkStatus": "NoLink",
|
|
"SpeedMbps": 65535,
|
|
},
|
|
"/redfish/v1/Managers/1/EthernetInterfaces/eth1": map[string]interface{}{
|
|
"Id": "eth1",
|
|
"MACAddress": "00:25:6c:70:00:12",
|
|
"LinkStatus": "LinkActive",
|
|
"SpeedMbps": 1000,
|
|
"IPv4Addresses": []interface{}{
|
|
map[string]interface{}{
|
|
"Address": "172.16.41.42",
|
|
"Gateway": "172.16.41.1",
|
|
"SubnetMask": "255.255.255.0",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
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.BMCMACAddress != "00:25:6C:70:00:12" {
|
|
t.Fatalf("expected active BMC MAC from eth1, got %q", got.Hardware.BoardInfo.BMCMACAddress)
|
|
}
|
|
summary, ok := got.RawPayloads["redfish_bmc_network_summary"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected redfish_bmc_network_summary")
|
|
}
|
|
if summary["interface_id"] != "eth1" {
|
|
t.Fatalf("expected eth1 summary, got %#v", summary["interface_id"])
|
|
}
|
|
}
|
|
|
|
func TestReplayRedfishFromRawPayloads_AddsSensorsListHintSummary(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": "Multillect",
|
|
"Model": "MLT-S06",
|
|
"SerialNumber": "430044262001626",
|
|
},
|
|
"/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/Chassis/1/SensorsList": map[string]interface{}{
|
|
"SensorsList": []interface{}{
|
|
map[string]interface{}{"SensorName": "DIMM000_Status", "SensorType": "Memory", "Status": "OK"},
|
|
map[string]interface{}{"SensorName": "DIMM001_Status", "SensorType": "Memory", "Status": "nop"},
|
|
map[string]interface{}{"SensorName": "DIMM100_Status", "SensorType": "Memory", "Status": "OK"},
|
|
map[string]interface{}{"SensorName": "HDD0_F_Status", "SensorType": "Drive Slot", "Status": "nop"},
|
|
map[string]interface{}{"SensorName": "NVME0_F_Status", "SensorType": "Drive Slot", "Status": "nop"},
|
|
map[string]interface{}{"SensorName": "Logical_Drive", "SensorType": "Drive Slot", "Status": "OK"},
|
|
},
|
|
},
|
|
"/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"},
|
|
},
|
|
}
|
|
|
|
got, err := ReplayRedfishFromRawPayloads(raw, nil)
|
|
if err != nil {
|
|
t.Fatalf("replay failed: %v", err)
|
|
}
|
|
hints, ok := got.RawPayloads["redfish_sensor_hints"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected redfish_sensor_hints")
|
|
}
|
|
memHints, ok := hints["memory_slots"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected memory_slots hint")
|
|
}
|
|
if asInt(memHints["present_count"]) != 2 {
|
|
t.Fatalf("expected 2 present memory slot hints, got %#v", memHints["present_count"])
|
|
}
|
|
driveHints, ok := hints["drive_slots"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected drive_slots hint")
|
|
}
|
|
if asInt(driveHints["physical_total"]) != 2 {
|
|
t.Fatalf("expected 2 physical drive slots, got %#v", driveHints["physical_total"])
|
|
}
|
|
if driveHints["logical_drive_status"] != "OK" {
|
|
t.Fatalf("expected logical drive status OK, got %#v", driveHints["logical_drive_status"])
|
|
}
|
|
foundMemoryEvent := false
|
|
foundDriveEvent := false
|
|
for _, ev := range got.Events {
|
|
if strings.Contains(ev.Description, "Memory slot sensors report 2 populated positions out of 3") {
|
|
foundMemoryEvent = true
|
|
}
|
|
if strings.Contains(ev.Description, "Drive slot sensors report 0 active physical slots out of 2") {
|
|
foundDriveEvent = true
|
|
}
|
|
}
|
|
if !foundMemoryEvent {
|
|
t.Fatalf("expected memory slot hint event")
|
|
}
|
|
if !foundDriveEvent {
|
|
t.Fatalf("expected drive slot hint event")
|
|
}
|
|
}
|
|
|
|
func TestReplayRedfishFromRawPayloads_PreservesSourceTimezoneAndUTCCollectedAt(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"},
|
|
"/redfish/v1/Managers": map[string]interface{}{
|
|
"Members": []interface{}{map[string]interface{}{"@odata.id": "/redfish/v1/Managers/1"}},
|
|
},
|
|
"/redfish/v1/Managers/1": map[string]interface{}{
|
|
"DateTime": "2026-02-28T04:18:18+08:00",
|
|
"DateTimeLocalOffset": "+08:00",
|
|
},
|
|
},
|
|
}
|
|
|
|
got, err := ReplayRedfishFromRawPayloads(raw, nil)
|
|
if err != nil {
|
|
t.Fatalf("replay failed: %v", err)
|
|
}
|
|
if got.SourceTimezone != "+08:00" {
|
|
t.Fatalf("expected source_timezone +08:00, got %q", got.SourceTimezone)
|
|
}
|
|
wantCollectedAt := time.Date(2026, 2, 27, 20, 18, 18, 0, time.UTC)
|
|
if !got.CollectedAt.Equal(wantCollectedAt) {
|
|
t.Fatalf("expected collected_at %s, got %s", wantCollectedAt, got.CollectedAt)
|
|
}
|
|
if got.RawPayloads["source_timezone"] != "+08:00" {
|
|
t.Fatalf("expected source_timezone in raw payloads, got %#v", got.RawPayloads["source_timezone"])
|
|
}
|
|
}
|
|
|
|
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{}{
|
|
{
|
|
"FunctionId": "0000:17:00.0",
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x1021",
|
|
"SerialNumber": "MT-SN-0001",
|
|
"PartNumber": "MCX623106AC-CDAT",
|
|
"CurrentLinkWidth": 16,
|
|
"CurrentLinkSpeedGTs": "32 GT/s",
|
|
"MaxLinkWidth": 16,
|
|
"MaxLinkSpeedGTs": "32 GT/s",
|
|
},
|
|
}
|
|
|
|
enrichNICFromPCIe(&nic, pcieDoc, functionDocs, nil)
|
|
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)
|
|
}
|
|
if nic.BDF != "0000:17:00.0" {
|
|
t.Fatalf("unexpected NIC BDF: %q", nic.BDF)
|
|
}
|
|
if nic.SerialNumber != "NIC-SN-1" {
|
|
t.Fatalf("expected existing NIC serial to be preserved, got %q", nic.SerialNumber)
|
|
}
|
|
if nic.PartNumber != "MCX623106AC-CDAT" {
|
|
t.Fatalf("expected NIC part number from PCIe function, got %q", nic.PartNumber)
|
|
}
|
|
if nic.LinkWidth != 16 || nic.MaxLinkWidth != 16 {
|
|
t.Fatalf("unexpected NIC link width state: current=%d max=%d", nic.LinkWidth, nic.MaxLinkWidth)
|
|
}
|
|
if nic.LinkSpeed != "32 GT/s" || nic.MaxLinkSpeed != "32 GT/s" {
|
|
t.Fatalf("unexpected NIC link speed state: current=%q max=%q", nic.LinkSpeed, nic.MaxLinkSpeed)
|
|
}
|
|
}
|
|
|
|
func TestEnrichNICFromPCIeFunctions_FillsMissingIdentityFromFunctionDoc(t *testing.T) {
|
|
nic := parseNIC(map[string]interface{}{
|
|
"Id": "DevType7_NIC1",
|
|
"Controllers": []interface{}{
|
|
map[string]interface{}{
|
|
"ControllerCapabilities": map[string]interface{}{
|
|
"NetworkPortCount": 1,
|
|
},
|
|
},
|
|
map[string]interface{}{
|
|
"ControllerCapabilities": map[string]interface{}{
|
|
"NetworkPortCount": 1,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
pcieDoc := map[string]interface{}{
|
|
"Slot": map[string]interface{}{
|
|
"Location": map[string]interface{}{
|
|
"PartLocation": map[string]interface{}{
|
|
"ServiceLabel": "RISER4",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
functionDocs := []map[string]interface{}{
|
|
{
|
|
"FunctionId": "0000:0f:00.0",
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x101f",
|
|
"SerialNumber": "MT2412X00001",
|
|
"PartNumber": "MCX623432AC-GDA_Ax",
|
|
},
|
|
}
|
|
|
|
enrichNICFromPCIe(&nic, pcieDoc, functionDocs, nil)
|
|
if nic.Slot != "RISER4" {
|
|
t.Fatalf("expected slot from PCIe slot label, got %q", nic.Slot)
|
|
}
|
|
if nic.Location != "RISER4" {
|
|
t.Fatalf("expected location from PCIe slot label, got %q", nic.Location)
|
|
}
|
|
if nic.PortCount != 2 {
|
|
t.Fatalf("expected combined port count from controllers, got %d", nic.PortCount)
|
|
}
|
|
if nic.SerialNumber != "MT2412X00001" {
|
|
t.Fatalf("expected serial from PCIe function, got %q", nic.SerialNumber)
|
|
}
|
|
if nic.PartNumber != "MCX623432AC-GDA_Ax" {
|
|
t.Fatalf("expected part number from PCIe function, got %q", nic.PartNumber)
|
|
}
|
|
if nic.BDF != "0000:0f:00.0" {
|
|
t.Fatalf("expected BDF from PCIe function, got %q", nic.BDF)
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectNICs_UsesNetworkDeviceFunctionPCIeFunctionLink(t *testing.T) {
|
|
tree := map[string]interface{}{
|
|
"/redfish/v1/Chassis/1/NetworkAdapters": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/NIC1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/NetworkAdapters/NIC1": map[string]interface{}{
|
|
"Id": "DevType7_NIC1",
|
|
"Name": "NetworkAdapter_1",
|
|
"Controllers": []interface{}{
|
|
map[string]interface{}{
|
|
"ControllerCapabilities": map[string]interface{}{
|
|
"NetworkPortCount": 2,
|
|
},
|
|
"Links": map[string]interface{}{
|
|
"PCIeDevices": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/00_0F_00"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"NetworkDeviceFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions/Function0"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions/Function0": map[string]interface{}{
|
|
"Id": "Function0",
|
|
"Links": map[string]interface{}{
|
|
"PCIeFunction": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/00_0F_00/PCIeFunctions/Function0",
|
|
},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/00_0F_00": map[string]interface{}{
|
|
"Id": "00_0F_00",
|
|
"Name": "PCIeDevice_00_0F_00",
|
|
"Manufacturer": "Mellanox Technologies",
|
|
"FirmwareVersion": "26.43.25.66",
|
|
"Slot": map[string]interface{}{
|
|
"Location": map[string]interface{}{
|
|
"PartLocation": map[string]interface{}{
|
|
"ServiceLabel": "RISER4",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/00_0F_00/PCIeFunctions/Function0": map[string]interface{}{
|
|
"Id": "Function0",
|
|
"FunctionId": "0000:0f:00.0",
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x101f",
|
|
"SerialNumber": "MT2412X00001",
|
|
"PartNumber": "MCX623432AC-GDA_Ax",
|
|
},
|
|
}
|
|
|
|
r := redfishSnapshotReader{tree: tree}
|
|
nics := r.collectNICs([]string{"/redfish/v1/Chassis/1"})
|
|
if len(nics) != 1 {
|
|
t.Fatalf("expected one NIC, got %d", len(nics))
|
|
}
|
|
if nics[0].Slot != "RISER4" {
|
|
t.Fatalf("expected slot from PCIe device, got %q", nics[0].Slot)
|
|
}
|
|
if nics[0].SerialNumber != "MT2412X00001" {
|
|
t.Fatalf("expected serial from NetworkDeviceFunction PCIeFunction link, got %q", nics[0].SerialNumber)
|
|
}
|
|
if nics[0].PartNumber != "MCX623432AC-GDA_Ax" {
|
|
t.Fatalf("expected part number from linked PCIeFunction, got %q", nics[0].PartNumber)
|
|
}
|
|
if nics[0].BDF != "0000:0f:00.0" {
|
|
t.Fatalf("expected BDF from linked PCIeFunction, got %q", nics[0].BDF)
|
|
}
|
|
if nics[0].Model != "MT2894 Family [ConnectX-6 Lx]" {
|
|
t.Fatalf("expected model resolved from PCI IDs, got %q", nics[0].Model)
|
|
}
|
|
}
|
|
|
|
func TestReplayEnrichNICsFromNetworkInterfaces_DoesNotCreateGhostForLinkedAdapter(t *testing.T) {
|
|
tree := map[string]interface{}{
|
|
"/redfish/v1/Chassis/1/NetworkAdapters": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/NIC1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/NetworkAdapters/NIC1": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/NIC1",
|
|
"Id": "DevType7_NIC1",
|
|
"Name": "NetworkAdapter_1",
|
|
"Controllers": []interface{}{
|
|
map[string]interface{}{
|
|
"ControllerCapabilities": map[string]interface{}{
|
|
"NetworkPortCount": 1,
|
|
},
|
|
"Links": map[string]interface{}{
|
|
"PCIeDevices": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/00_0F_00"},
|
|
},
|
|
},
|
|
},
|
|
map[string]interface{}{
|
|
"ControllerCapabilities": map[string]interface{}{
|
|
"NetworkPortCount": 1,
|
|
},
|
|
"Links": map[string]interface{}{
|
|
"PCIeDevices": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/00_0F_00"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"NetworkDeviceFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions/Function0"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions/Function1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions/Function0": map[string]interface{}{
|
|
"Id": "Function0",
|
|
"Ethernet": map[string]interface{}{
|
|
"MACAddress": "CC:40:F3:D6:9E:DE",
|
|
"PermanentMACAddress": "CC:40:F3:D6:9E:DE",
|
|
},
|
|
"Links": map[string]interface{}{
|
|
"PCIeFunction": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/00_0F_00/PCIeFunctions/Function0",
|
|
},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions/Function1": map[string]interface{}{
|
|
"Id": "Function1",
|
|
"Ethernet": map[string]interface{}{
|
|
"MACAddress": "CC:40:F3:D6:9E:DF",
|
|
"PermanentMACAddress": "CC:40:F3:D6:9E:DF",
|
|
},
|
|
"Links": map[string]interface{}{
|
|
"PCIeFunction": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/00_0F_00/PCIeFunctions/Function1",
|
|
},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/00_0F_00": map[string]interface{}{
|
|
"Id": "00_0F_00",
|
|
"Name": "PCIeDevice_00_0F_00",
|
|
"Manufacturer": "Mellanox Technologies",
|
|
"FirmwareVersion": "26.43.25.66",
|
|
"Slot": map[string]interface{}{
|
|
"Location": map[string]interface{}{
|
|
"PartLocation": map[string]interface{}{
|
|
"ServiceLabel": "RISER4",
|
|
},
|
|
},
|
|
},
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/00_0F_00/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/00_0F_00/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/00_0F_00/PCIeFunctions/Function0"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/00_0F_00/PCIeFunctions/Function1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/00_0F_00/PCIeFunctions/Function0": map[string]interface{}{
|
|
"FunctionId": "0000:0f:00.0",
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x101f",
|
|
"DeviceClass": "NetworkController",
|
|
"SerialNumber": "N/A",
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/00_0F_00/PCIeFunctions/Function1": map[string]interface{}{
|
|
"FunctionId": "0000:0f:00.1",
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x101f",
|
|
"DeviceClass": "NetworkController",
|
|
},
|
|
"/redfish/v1/Systems/1/NetworkInterfaces": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/NetworkInterfaces/DevType7_NIC1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/NetworkInterfaces/DevType7_NIC1": map[string]interface{}{
|
|
"Id": "DevType7_NIC1",
|
|
"Name": "NetworkAdapter_1",
|
|
"Links": map[string]interface{}{
|
|
"NetworkAdapter": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/NIC1",
|
|
},
|
|
},
|
|
"Status": map[string]interface{}{
|
|
"Health": "OK",
|
|
"State": "Disabled",
|
|
},
|
|
},
|
|
}
|
|
|
|
r := redfishSnapshotReader{tree: tree}
|
|
nics := r.collectNICs([]string{"/redfish/v1/Chassis/1"})
|
|
r.enrichNICsFromNetworkInterfaces(&nics, []string{"/redfish/v1/Systems/1"})
|
|
if len(nics) != 1 {
|
|
t.Fatalf("expected linked network interface to reuse existing NIC, got %d: %+v", len(nics), nics)
|
|
}
|
|
if nics[0].Slot != "RISER4" {
|
|
t.Fatalf("expected enriched slot to stay canonical, got %q", nics[0].Slot)
|
|
}
|
|
if nics[0].Model != "MT2894 Family [ConnectX-6 Lx]" {
|
|
t.Fatalf("expected resolved Mellanox model, got %q", nics[0].Model)
|
|
}
|
|
if len(nics[0].MACAddresses) != 2 {
|
|
t.Fatalf("expected both MACs to stay on one NIC, got %+v", nics[0].MACAddresses)
|
|
}
|
|
}
|
|
|
|
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_PrefersControllerSlotLabelAndPCIeInterface(t *testing.T) {
|
|
nic := parseNIC(map[string]interface{}{
|
|
"Id": "1",
|
|
"Model": "MCX75310AAS-NEAT",
|
|
"Manufacturer": "Supermicro",
|
|
"Controllers": []interface{}{
|
|
map[string]interface{}{
|
|
"Location": map[string]interface{}{
|
|
"PartLocation": map[string]interface{}{
|
|
"ServiceLabel": "PCIe Slot 1 (1)",
|
|
},
|
|
},
|
|
"PCIeInterface": map[string]interface{}{
|
|
"LanesInUse": 16,
|
|
"MaxLanes": 16,
|
|
"PCIeType": "Gen5",
|
|
"MaxPCIeType": "Gen5",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if nic.Slot != "PCIe Slot 1 (1)" {
|
|
t.Fatalf("expected slot from controller location, got %q", nic.Slot)
|
|
}
|
|
if nic.Location != "PCIe Slot 1 (1)" {
|
|
t.Fatalf("expected location from controller location, got %q", nic.Location)
|
|
}
|
|
if nic.LinkWidth != 16 || nic.MaxLinkWidth != 16 {
|
|
t.Fatalf("expected link widths from controller PCIeInterface, got current=%d max=%d", nic.LinkWidth, nic.MaxLinkWidth)
|
|
}
|
|
if nic.LinkSpeed != "Gen5" || nic.MaxLinkSpeed != "Gen5" {
|
|
t.Fatalf("expected link speeds from controller PCIeInterface, got current=%q max=%q", nic.LinkSpeed, nic.MaxLinkSpeed)
|
|
}
|
|
}
|
|
|
|
func TestParseNIC_xFusionMaxlanesAndOEMLinkWidth(t *testing.T) {
|
|
// xFusion uses "Maxlanes" (lowercase 'l') in PCIeInterface, not "MaxLanes".
|
|
// xFusion also stores per-function link width as Oem.xFusion.LinkWidth = "X8".
|
|
nic := parseNIC(map[string]interface{}{
|
|
"Id": "OCPCard1",
|
|
"Model": "ConnectX-6 Lx",
|
|
"Controllers": []interface{}{
|
|
map[string]interface{}{
|
|
"PCIeInterface": map[string]interface{}{
|
|
"LanesInUse": 8,
|
|
"Maxlanes": 8, // xFusion uses lowercase 'l'
|
|
"PCIeType": "Gen4",
|
|
"MaxPCIeType": "Gen4",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if nic.LinkWidth != 8 || nic.MaxLinkWidth != 8 {
|
|
t.Fatalf("expected link widths 8/8 from xFusion Maxlanes, got current=%d max=%d", nic.LinkWidth, nic.MaxLinkWidth)
|
|
}
|
|
|
|
// enrichNICFromPCIe: OEM xFusion LinkWidth on a PCIeFunction doc.
|
|
nic2 := models.NetworkAdapter{}
|
|
fnDoc := map[string]interface{}{
|
|
"Oem": map[string]interface{}{
|
|
"xFusion": map[string]interface{}{
|
|
"LinkWidth": "X8",
|
|
"LinkWidthAbility": "X8",
|
|
"LinkSpeed": "Gen4 (16.0GT/s)",
|
|
"LinkSpeedAbility": "Gen4 (16.0GT/s)",
|
|
},
|
|
},
|
|
}
|
|
enrichNICFromPCIe(&nic2, map[string]interface{}{}, []map[string]interface{}{fnDoc}, nil)
|
|
if nic2.LinkWidth != 8 || nic2.MaxLinkWidth != 8 {
|
|
t.Fatalf("expected link width 8 from xFusion OEM LinkWidth, got current=%d max=%d", nic2.LinkWidth, nic2.MaxLinkWidth)
|
|
}
|
|
if nic2.LinkSpeed != "Gen4 (16.0GT/s)" || nic2.MaxLinkSpeed != "Gen4 (16.0GT/s)" {
|
|
t.Fatalf("expected link speed from xFusion OEM LinkSpeed, got current=%q max=%q", nic2.LinkSpeed, nic2.MaxLinkSpeed)
|
|
}
|
|
}
|
|
|
|
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 TestParsePCIeDevice_DoesNotPromotePartNumberToDeviceClass(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"Id": "NIC1",
|
|
"DeviceType": "SingleFunction",
|
|
"Model": "MCX75310AAS-NEAT",
|
|
"PartNumber": "MCX75310AAS-NEAT",
|
|
}
|
|
|
|
got := parsePCIeDevice(doc, nil)
|
|
if got.DeviceClass != "PCIe device" {
|
|
t.Fatalf("expected generic PCIe class fallback, got %q", got.DeviceClass)
|
|
}
|
|
if got.PartNumber != "MCX75310AAS-NEAT" {
|
|
t.Fatalf("expected part number to stay intact, got %q", got.PartNumber)
|
|
}
|
|
}
|
|
|
|
func TestParsePCIeComponents_DoNotTreatNumericFunctionIDAsBDF(t *testing.T) {
|
|
pcieFn := parsePCIeFunction(map[string]interface{}{
|
|
"Id": "1",
|
|
"FunctionId": "1",
|
|
"DeviceClass": "NetworkController",
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x1021",
|
|
}, 1)
|
|
if pcieFn.BDF != "" {
|
|
t.Fatalf("expected empty BDF for numeric FunctionId, got %q", pcieFn.BDF)
|
|
}
|
|
|
|
gpu := parseGPU(map[string]interface{}{
|
|
"Id": "GPU1",
|
|
"Name": "GPU1",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions",
|
|
},
|
|
}, []map[string]interface{}{
|
|
{
|
|
"FunctionId": "1",
|
|
"VendorId": "0x10de",
|
|
"DeviceId": "0x2331",
|
|
},
|
|
}, 1)
|
|
if gpu.BDF != "" {
|
|
t.Fatalf("expected GPU BDF to stay empty when only numeric FunctionId exists, got %q", gpu.BDF)
|
|
}
|
|
|
|
nic := parseNIC(map[string]interface{}{"Id": "1"})
|
|
enrichNICFromPCIe(&nic, map[string]interface{}{}, []map[string]interface{}{
|
|
{
|
|
"FunctionId": "1",
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x1021",
|
|
},
|
|
}, nil)
|
|
if nic.BDF != "" {
|
|
t.Fatalf("expected NIC BDF to stay empty when only numeric FunctionId exists, got %q", nic.BDF)
|
|
}
|
|
}
|
|
|
|
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{}{
|
|
"VendorX": 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 TestParseCPU_UsesPublicSerialAsPPINAndCurrentSpeedMHz(t *testing.T) {
|
|
cpus := parseCPUs([]map[string]interface{}{
|
|
{
|
|
"Id": "CPU0",
|
|
"Model": "Intel Xeon",
|
|
"TotalCores": 48,
|
|
"TotalThreads": 96,
|
|
"MaxSpeedMHz": 4000,
|
|
"OperatingSpeedMHz": 0,
|
|
"Oem": map[string]interface{}{
|
|
"Public": map[string]interface{}{
|
|
"SerialNumber": "6FB5241E81CECDFD",
|
|
"CurrentSpeedMHz": 2700,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if len(cpus) != 1 {
|
|
t.Fatalf("expected one CPU, got %d", len(cpus))
|
|
}
|
|
if cpus[0].PPIN != "6FB5241E81CECDFD" {
|
|
t.Fatalf("expected PPIN from Oem.Public.SerialNumber, got %+v", cpus[0])
|
|
}
|
|
if cpus[0].SerialNumber != "" {
|
|
t.Fatalf("expected empty CPU serial number when only Public serial exists, got %+v", cpus[0])
|
|
}
|
|
if cpus[0].FrequencyMHz != 2700 {
|
|
t.Fatalf("expected CPU frequency from Oem.Public.CurrentSpeedMHz, got %+v", cpus[0])
|
|
}
|
|
}
|
|
|
|
func TestParseCPUAndMemory_CollectOemDetails(t *testing.T) {
|
|
cpus := parseCPUs([]map[string]interface{}{
|
|
{
|
|
"Id": "CPU0",
|
|
"Model": "Intel Xeon",
|
|
"CorrectableErrors": 7,
|
|
"TemperatureCelsius": 63,
|
|
"Oem": map[string]interface{}{
|
|
"VendorX": map[string]interface{}{
|
|
"MicrocodeVersion": "0x2b000643",
|
|
"UncorrectableErrors": 1,
|
|
"ThermalThrottled": true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if len(cpus) != 1 || cpus[0].Details == nil {
|
|
t.Fatalf("expected CPU details, got %+v", cpus)
|
|
}
|
|
if cpus[0].Details["microcode"] != "0x2b000643" {
|
|
t.Fatalf("expected CPU microcode detail, got %#v", cpus[0].Details)
|
|
}
|
|
if cpus[0].Details["correctable_error_count"] != int64(7) || cpus[0].Details["uncorrectable_error_count"] != int64(1) {
|
|
t.Fatalf("expected CPU error counters, got %#v", cpus[0].Details)
|
|
}
|
|
if cpus[0].Details["throttled"] != true || cpus[0].Details["temperature_c"] != 63.0 {
|
|
t.Fatalf("expected CPU thermal details, got %#v", cpus[0].Details)
|
|
}
|
|
|
|
dimms := parseMemory([]map[string]interface{}{
|
|
{
|
|
"Id": "DIMM0",
|
|
"DeviceLocator": "CPU0_C0D0",
|
|
"CapacityMiB": 32768,
|
|
"SerialNumber": "DIMM-001",
|
|
"Oem": map[string]interface{}{
|
|
"VendorX": map[string]interface{}{
|
|
"CorrectableECCErrorCount": 12,
|
|
"UncorrectableECCErrorCount": 2,
|
|
"TemperatureC": 41.5,
|
|
"SpareBlocksRemainingPercent": 88,
|
|
"PerformanceDegraded": true,
|
|
"DataLossDetected": false,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if len(dimms) != 1 || dimms[0].Details == nil {
|
|
t.Fatalf("expected DIMM details, got %+v", dimms)
|
|
}
|
|
if dimms[0].Details["correctable_ecc_error_count"] != int64(12) || dimms[0].Details["uncorrectable_ecc_error_count"] != int64(2) {
|
|
t.Fatalf("expected DIMM ECC counters, got %#v", dimms[0].Details)
|
|
}
|
|
if dimms[0].Details["temperature_c"] != 41.5 || dimms[0].Details["spare_blocks_remaining_pct"] != 88.0 {
|
|
t.Fatalf("expected DIMM telemetry details, got %#v", dimms[0].Details)
|
|
}
|
|
if dimms[0].Details["performance_degraded"] != true || dimms[0].Details["data_loss_detected"] != false {
|
|
t.Fatalf("expected DIMM boolean health details, got %#v", dimms[0].Details)
|
|
}
|
|
}
|
|
|
|
func TestReplayRedfishFromRawPayloads_UsesProcessorAndMemoryMetrics(t *testing.T) {
|
|
rawPayloads := map[string]any{
|
|
"redfish_tree": map[string]interface{}{
|
|
"/redfish/v1": map[string]interface{}{},
|
|
"/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",
|
|
"Processors": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/Processors",
|
|
},
|
|
"Memory": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/Memory",
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Processors": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Processors/CPU0"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Processors/CPU0": map[string]interface{}{
|
|
"Id": "CPU0",
|
|
"ProcessorType": "CPU",
|
|
"Model": "Intel Xeon",
|
|
"ProcessorMetrics": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/Processors/CPU0/ProcessorMetrics",
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Processors/CPU0/ProcessorMetrics": map[string]interface{}{
|
|
"CorrectableErrors": 10,
|
|
"ThermalThrottled": true,
|
|
"MicrocodeVersion": "0x2b000643",
|
|
"TemperatureCelsius": 66,
|
|
},
|
|
"/redfish/v1/Systems/1/Memory": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Memory/DIMM0"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Memory/DIMM0": map[string]interface{}{
|
|
"Id": "DIMM0",
|
|
"DeviceLocator": "CPU0_C0D0",
|
|
"CapacityMiB": 32768,
|
|
"SerialNumber": "DIMM-001",
|
|
"MemoryMetrics": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/Memory/DIMM0/MemoryMetrics",
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Memory/DIMM0/MemoryMetrics": map[string]interface{}{
|
|
"CorrectableECCErrorCount": 14,
|
|
"TemperatureCelsius": 42,
|
|
"PerformanceDegraded": true,
|
|
"SpareBlocksRemainingPercent": 91,
|
|
},
|
|
},
|
|
}
|
|
|
|
result, err := ReplayRedfishFromRawPayloads(rawPayloads, nil)
|
|
if err != nil {
|
|
t.Fatalf("ReplayRedfishFromRawPayloads() failed: %v", err)
|
|
}
|
|
if len(result.Hardware.CPUs) != 1 || result.Hardware.CPUs[0].Details == nil {
|
|
t.Fatalf("expected CPU details from replay metrics, got %+v", result.Hardware.CPUs)
|
|
}
|
|
if result.Hardware.CPUs[0].Details["correctable_error_count"] != int64(10) || result.Hardware.CPUs[0].Details["microcode"] != "0x2b000643" {
|
|
t.Fatalf("expected CPU replay metrics details, got %#v", result.Hardware.CPUs[0].Details)
|
|
}
|
|
if len(result.Hardware.Memory) != 1 || result.Hardware.Memory[0].Details == nil {
|
|
t.Fatalf("expected memory details from replay metrics, got %+v", result.Hardware.Memory)
|
|
}
|
|
if result.Hardware.Memory[0].Details["correctable_ecc_error_count"] != int64(14) || result.Hardware.Memory[0].Details["performance_degraded"] != true {
|
|
t.Fatalf("expected DIMM replay metrics details, got %#v", result.Hardware.Memory[0].Details)
|
|
}
|
|
}
|
|
|
|
func TestReplayRedfishFromRawPayloads_UsesDriveMetrics(t *testing.T) {
|
|
rawPayloads := map[string]any{
|
|
"redfish_tree": map[string]interface{}{
|
|
"/redfish/v1": map[string]interface{}{},
|
|
"/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",
|
|
"Storage": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/Storage",
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/RAID1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/RAID1": map[string]interface{}{
|
|
"Id": "RAID1",
|
|
"Drives": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/Storage/RAID1/Drives",
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/RAID1/Drives": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/RAID1/Drives/Drive0"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/RAID1/Drives/Drive0": map[string]interface{}{
|
|
"Id": "Drive0",
|
|
"Model": "NVMe SSD",
|
|
"SerialNumber": "SSD-001",
|
|
"DriveMetrics": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/Storage/RAID1/Drives/Drive0/DriveMetrics",
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/RAID1/Drives/Drive0/DriveMetrics": map[string]interface{}{
|
|
"PowerOnHours": 1001,
|
|
"MediaErrors": 3,
|
|
"AvailableSparePercent": 92,
|
|
"TemperatureCelsius": 37,
|
|
},
|
|
},
|
|
}
|
|
|
|
result, err := ReplayRedfishFromRawPayloads(rawPayloads, nil)
|
|
if err != nil {
|
|
t.Fatalf("ReplayRedfishFromRawPayloads() failed: %v", err)
|
|
}
|
|
if len(result.Hardware.Storage) != 1 || result.Hardware.Storage[0].Details == nil {
|
|
t.Fatalf("expected storage details from replay drive metrics, got %+v", result.Hardware.Storage)
|
|
}
|
|
if result.Hardware.Storage[0].Details["power_on_hours"] != int64(1001) || result.Hardware.Storage[0].Details["media_errors"] != int64(3) {
|
|
t.Fatalf("expected drive metrics counters, got %#v", result.Hardware.Storage[0].Details)
|
|
}
|
|
if result.Hardware.Storage[0].Details["available_spare_pct"] != 92.0 || result.Hardware.Storage[0].Details["temperature_c"] != 37.0 {
|
|
t.Fatalf("expected drive metrics telemetry, got %#v", result.Hardware.Storage[0].Details)
|
|
}
|
|
}
|
|
|
|
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 TestParseDriveAndPSU_CollectComponentMetricsIntoDetails(t *testing.T) {
|
|
drive := parseDrive(map[string]interface{}{
|
|
"Id": "Drive0",
|
|
"Model": "NVMe SSD",
|
|
"SerialNumber": "SSD-001",
|
|
"TemperatureCelsius": 38.5,
|
|
"PowerOnHours": 12450,
|
|
"UnsafeShutdowns": 3,
|
|
"PredictedMediaLifeLeftPercent": 91,
|
|
"Oem": map[string]interface{}{
|
|
"Public": map[string]interface{}{
|
|
"AvailableSparePercent": 87,
|
|
},
|
|
},
|
|
})
|
|
if drive.Details == nil {
|
|
t.Fatalf("expected drive details to be populated")
|
|
}
|
|
if got := drive.Details["temperature_c"]; got != 38.5 {
|
|
t.Fatalf("expected drive temperature detail 38.5, got %#v", got)
|
|
}
|
|
if got := drive.Details["power_on_hours"]; got != int64(12450) {
|
|
t.Fatalf("expected drive power_on_hours detail, got %#v", got)
|
|
}
|
|
if got := drive.Details["life_remaining_pct"]; got != 91.0 {
|
|
t.Fatalf("expected drive life_remaining_pct detail, got %#v", got)
|
|
}
|
|
if got := drive.Details["available_spare_pct"]; got != 87.0 {
|
|
t.Fatalf("expected drive available_spare_pct detail from Oem/Public, got %#v", got)
|
|
}
|
|
|
|
driveOEM := parseDrive(map[string]interface{}{
|
|
"Id": "Drive1",
|
|
"Model": "NVMe SSD 2",
|
|
"SerialNumber": "SSD-002",
|
|
"Oem": map[string]interface{}{
|
|
"Public": map[string]interface{}{
|
|
"temperature": 19,
|
|
"PercentAvailableSpare": 93,
|
|
"PercentageUsed": 7,
|
|
},
|
|
},
|
|
})
|
|
if driveOEM.Details == nil {
|
|
t.Fatalf("expected oem drive details to be populated")
|
|
}
|
|
if got := driveOEM.Details["temperature_c"]; got != 19.0 {
|
|
t.Fatalf("expected lowercase OEM drive temperature 19, got %#v", got)
|
|
}
|
|
if got := driveOEM.Details["available_spare_pct"]; got != 93.0 {
|
|
t.Fatalf("expected OEM available_spare_pct 93, got %#v", got)
|
|
}
|
|
if got := driveOEM.Details["life_used_pct"]; got != 7.0 {
|
|
t.Fatalf("expected OEM life_used_pct 7, got %#v", got)
|
|
}
|
|
|
|
psu := parsePSU(map[string]interface{}{
|
|
"MemberId": "PSU0",
|
|
"SerialNumber": "PSU-001",
|
|
"TemperatureCelsius": 41,
|
|
"Oem": map[string]interface{}{
|
|
"Public": map[string]interface{}{
|
|
"LifeRemainingPercent": 96,
|
|
},
|
|
},
|
|
}, 1)
|
|
if psu.Details == nil {
|
|
t.Fatalf("expected psu details to be populated")
|
|
}
|
|
if got := psu.Details["temperature_c"]; got != 41.0 {
|
|
t.Fatalf("expected psu temperature detail 41, got %#v", got)
|
|
}
|
|
if got := psu.Details["life_remaining_pct"]; got != 96.0 {
|
|
t.Fatalf("expected psu life_remaining_pct detail, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestParseGPUPCIeAndNIC_CollectComponentMetricsIntoDetails(t *testing.T) {
|
|
functionDocs := []map[string]interface{}{
|
|
{
|
|
"FunctionId": "0000:17:00.0",
|
|
"VendorId": "0x10de",
|
|
"DeviceId": "0x2331",
|
|
"TemperatureCelsius": 48.5,
|
|
"PowerConsumedWatts": 315.0,
|
|
"ECCCorrectedTotal": 12,
|
|
"BatteryHealthPercent": 87,
|
|
"SFPTemperatureCelsius": 36.2,
|
|
},
|
|
}
|
|
gpu := parseGPU(map[string]interface{}{
|
|
"Id": "GPU0",
|
|
"Model": "NVIDIA H100",
|
|
"Manufacturer": "NVIDIA",
|
|
}, functionDocs, 1)
|
|
if gpu.Details == nil || gpu.Details["temperature_c"] != 48.5 || gpu.Details["power_w"] != 315.0 {
|
|
t.Fatalf("expected gpu details from function docs, got %#v", gpu.Details)
|
|
}
|
|
|
|
pcie := parsePCIeDevice(map[string]interface{}{
|
|
"Id": "NIC1",
|
|
}, []map[string]interface{}{
|
|
{
|
|
"FunctionId": "0000:18:00.0",
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x1021",
|
|
"SFPTXPowerDBm": -1.8,
|
|
"SFPRXPowerDBm": -2.1,
|
|
"SFPBiasMA": 5.5,
|
|
"BatteryReplaceRequired": true,
|
|
},
|
|
})
|
|
if pcie.Details == nil || pcie.Details["sfp_tx_power_dbm"] != -1.8 || pcie.Details["battery_replace_required"] != true {
|
|
t.Fatalf("expected pcie details from function docs, got %#v", pcie.Details)
|
|
}
|
|
|
|
nic := parseNIC(map[string]interface{}{"Id": "1"})
|
|
enrichNICFromPCIe(&nic, map[string]interface{}{}, []map[string]interface{}{
|
|
{
|
|
"FunctionId": "0000:19:00.0",
|
|
"SFPTemperatureCelsius": 34.0,
|
|
},
|
|
}, nil)
|
|
if nic.Details == nil || nic.Details["sfp_temperature_c"] != 34.0 {
|
|
t.Fatalf("expected nic details from linked pcie function, got %#v", nic.Details)
|
|
}
|
|
}
|
|
|
|
func TestParseComponentDetails_UseLinkedSupplementalMetrics(t *testing.T) {
|
|
drive := parseDriveWithSupplementalDocs(
|
|
map[string]interface{}{
|
|
"Id": "Drive0",
|
|
"SerialNumber": "SSD-001",
|
|
},
|
|
map[string]interface{}{
|
|
"PowerOnHours": 5001,
|
|
"MediaErrors": 2,
|
|
"TemperatureC": 39.5,
|
|
"LifeUsedPercent": 9,
|
|
},
|
|
)
|
|
if drive.Details == nil || drive.Details["power_on_hours"] != int64(5001) || drive.Details["temperature_c"] != 39.5 {
|
|
t.Fatalf("expected drive details from supplemental metrics, got %#v", drive.Details)
|
|
}
|
|
|
|
psu := parsePSUWithSupplementalDocs(
|
|
map[string]interface{}{
|
|
"MemberId": "PSU0",
|
|
"SerialNumber": "PSU-001",
|
|
},
|
|
1,
|
|
map[string]interface{}{
|
|
"Temperature": 44,
|
|
"LifeRemainingPercent": 97,
|
|
},
|
|
)
|
|
if psu.Details == nil || psu.Details["temperature_c"] != 44.0 || psu.Details["life_remaining_pct"] != 97.0 {
|
|
t.Fatalf("expected psu details from supplemental metrics, got %#v", psu.Details)
|
|
}
|
|
|
|
gpu := parseGPUWithSupplementalDocs(
|
|
map[string]interface{}{
|
|
"Id": "GPU0",
|
|
"Model": "NVIDIA H100",
|
|
"Manufacturer": "NVIDIA",
|
|
},
|
|
nil,
|
|
[]map[string]interface{}{
|
|
{
|
|
"PowerConsumptionWatts": 305.0,
|
|
"HWSlowdown": true,
|
|
},
|
|
},
|
|
1,
|
|
)
|
|
if gpu.Details == nil || gpu.Details["power_w"] != 305.0 || gpu.Details["hw_slowdown"] != true {
|
|
t.Fatalf("expected gpu details from supplemental metrics, got %#v", gpu.Details)
|
|
}
|
|
}
|
|
|
|
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,
|
|
redfishprofile.AcquisitionTuning{
|
|
RecoveryPolicy: redfishprofile.AcquisitionRecoveryPolicy{
|
|
EnableCriticalCollectionMemberRetry: true,
|
|
EnableCriticalSlowProbe: true,
|
|
},
|
|
},
|
|
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)`,
|
|
}
|
|
|
|
plan := redfishprofile.BuildAcquisitionPlan(redfishprofile.MatchSignals{})
|
|
match := redfishprofile.MatchProfiles(redfishprofile.MatchSignals{})
|
|
resolved := redfishprofile.ResolveAcquisitionPlan(match, plan, redfishprofile.DiscoveredResources{
|
|
SystemPaths: []string{systemPath},
|
|
}, redfishprofile.MatchSignals{})
|
|
criticalPaths := resolved.CriticalPaths
|
|
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,
|
|
redfishprofile.AcquisitionTuning{
|
|
RecoveryPolicy: redfishprofile.AcquisitionRecoveryPolicy{
|
|
EnableCriticalCollectionMemberRetry: true,
|
|
EnableCriticalSlowProbe: true,
|
|
},
|
|
},
|
|
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 TestRecoverCriticalRedfishDocsPlanB_SkipsMemberRetryWithoutRecoveryPolicy(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 memoryPath = "/redfish/v1/Systems/1/Memory"
|
|
const dimmPath = "/redfish/v1/Systems/1/Memory/CPU1_C1D1"
|
|
|
|
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)`,
|
|
}
|
|
|
|
c := NewRedfishConnector()
|
|
recovered := c.recoverCriticalRedfishDocsPlanB(
|
|
context.Background(),
|
|
http.DefaultClient,
|
|
Request{},
|
|
"https://example",
|
|
[]string{memoryPath},
|
|
rawTree,
|
|
fetchErrs,
|
|
redfishprofile.AcquisitionTuning{},
|
|
nil,
|
|
)
|
|
if recovered != 0 {
|
|
t.Fatalf("expected no recovery without recovery policy, got %d", recovered)
|
|
}
|
|
if _, ok := rawTree[dimmPath]; ok {
|
|
t.Fatalf("did not expect recovered DIMM doc for %s", dimmPath)
|
|
}
|
|
if _, ok := fetchErrs[dimmPath]; !ok {
|
|
t.Fatalf("expected DIMM fetch error for %s to remain", 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", testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableSupermicroNVMeBackplane: true}))
|
|
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 TestReplayCollectStorage_SkipsEnclosureRecoveryWhenDirectiveDisabled(t *testing.T) {
|
|
r := redfishSnapshotReader{tree: map[string]interface{}{
|
|
"/redfish/v1/Systems/1/Storage": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/1": map[string]interface{}{
|
|
"Links": map[string]interface{}{
|
|
"Enclosures": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Enclosures/1"},
|
|
},
|
|
},
|
|
},
|
|
"/redfish/v1/Enclosures/1/Drives": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Enclosures/1/Drives/Drive1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Enclosures/1/Drives/Drive1": map[string]interface{}{
|
|
"Id": "Drive1",
|
|
"Name": "Drive1",
|
|
"Model": "INTEL SSD",
|
|
"SerialNumber": "ENCLOSURE-DRIVE-001",
|
|
"Protocol": "SATA",
|
|
"MediaType": "SSD",
|
|
},
|
|
}}
|
|
|
|
got := r.collectStorage("/redfish/v1/Systems/1", testAnalysisPlan(redfishprofile.AnalysisDirectives{}))
|
|
if len(got) != 0 {
|
|
t.Fatalf("expected no enclosure recovery when directive is off, got %d", len(got))
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectStorage_UsesKnownControllerRecoveryWhenEnabled(t *testing.T) {
|
|
r := redfishSnapshotReader{tree: map[string]interface{}{
|
|
"/redfish/v1/Systems/1/Storage/IntelVROC/Drives": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/IntelVROC/Drives/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/IntelVROC/Drives/1": map[string]interface{}{
|
|
"Id": "1",
|
|
"Name": "Drive1",
|
|
"Model": "VROC SSD",
|
|
"SerialNumber": "VROC-001",
|
|
"Protocol": "NVMe",
|
|
"MediaType": "SSD",
|
|
},
|
|
}}
|
|
|
|
got := r.collectStorage("/redfish/v1/Systems/1", redfishprofile.ResolvedAnalysisPlan{
|
|
Directives: redfishprofile.AnalysisDirectives{EnableKnownStorageControllerRecovery: true},
|
|
KnownStorageDriveCollections: []string{"/Storage/IntelVROC/Drives"},
|
|
})
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected one drive from known controller recovery, got %d", len(got))
|
|
}
|
|
if got[0].SerialNumber != "VROC-001" {
|
|
t.Fatalf("unexpected serial %q", got[0].SerialNumber)
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectStorageVolumes_SkipsVolumeCapabilitiesFallbackMember(t *testing.T) {
|
|
r := redfishSnapshotReader{tree: map[string]interface{}{
|
|
"/redfish/v1/Systems/1/Storage": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/DE00A000"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/DE00A000": map[string]interface{}{
|
|
"Id": "DE00A000",
|
|
"Volumes": map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/DE00A000/Volumes"},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/DE00A000/Volumes": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/Storage/DE00A000/Volumes",
|
|
"@odata.type": "#VolumeCollection.VolumeCollection",
|
|
"Members": []interface{}{},
|
|
"Members@odata.count": 0,
|
|
"Name": "MR Volume Collection",
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/DE00A000/Volumes/Capabilities": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/Storage/DE00A000/Volumes/Capabilities",
|
|
"@odata.type": "#Volume.v1_9_0.Volume",
|
|
"Id": "Capabilities",
|
|
"Name": "Capabilities for VolumeCollection",
|
|
},
|
|
}}
|
|
|
|
got := r.collectStorageVolumes("/redfish/v1/Systems/1", testAnalysisPlan(redfishprofile.AnalysisDirectives{}))
|
|
if len(got) != 0 {
|
|
t.Fatalf("expected capabilities-only volume collection to stay empty, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectPCIeDevices_UsesChassisDeviceSupplementalDocs(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/2"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/2": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/2",
|
|
"Name": "BCM 5719 1Gb 4p BASE-T OCP Adptr",
|
|
"Model": "P51183-001",
|
|
"PartNumber": "P51183-001",
|
|
"Manufacturer": "Broadcom",
|
|
"SerialNumber": "1CH0150001",
|
|
"DeviceType": "SingleFunction",
|
|
},
|
|
"/redfish/v1/Chassis/1/Devices": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/Devices/2"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/Devices/4"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/Devices/2": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/Devices/2",
|
|
"Name": "BCM 5719 1Gb 4p BASE-T OCP Adptr",
|
|
"DeviceType": "LOM/NIC",
|
|
"Manufacturer": "Broadcom",
|
|
"PartNumber": "BCM95719N1905HC",
|
|
"ProductPartNumber": "P51183-001",
|
|
"SerialNumber": "1CH0150001",
|
|
"Location": "OCP 3.0 Slot 15",
|
|
},
|
|
"/redfish/v1/Chassis/1/Devices/4": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/Devices/4",
|
|
"Name": "Empty slot 2",
|
|
"DeviceType": "Unknown",
|
|
"Location": "PCI-E Slot 2",
|
|
"SerialNumber": "",
|
|
},
|
|
}}
|
|
|
|
got := r.collectPCIeDevices(nil, []string{"/redfish/v1/Chassis/1"})
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected one PCIe device, got %d", len(got))
|
|
}
|
|
if got[0].Slot != "OCP 3.0 Slot 15" {
|
|
t.Fatalf("expected chassis device location to override weak slot label, got %+v", got[0])
|
|
}
|
|
if got[0].DeviceClass != "LOM/NIC" {
|
|
t.Fatalf("expected chassis device type to enrich class, got %+v", got[0])
|
|
}
|
|
if got[0].DeviceClass == "P51183-001" {
|
|
t.Fatalf("device class should not degrade into part number: %+v", got[0])
|
|
}
|
|
}
|
|
|
|
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"}, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableGenericGraphicsControllerDedup: true}))
|
|
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 TestReplayCollectPCIeDevices_SkipsMSITopologyNoiseClasses(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/bridge"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/processor"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/signal"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/serial"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/display"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/network"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/storage"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/bridge": map[string]interface{}{
|
|
"Id": "bridge",
|
|
"Name": "Bridge",
|
|
"Description": "Bridge Device",
|
|
"Manufacturer": "Intel Corporation",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/bridge/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/bridge/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/bridge/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/bridge/PCIeFunctions/1": map[string]interface{}{
|
|
"DeviceClass": "Bridge",
|
|
"VendorId": "0x8086",
|
|
"DeviceId": "0x0db0",
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/processor": map[string]interface{}{
|
|
"Id": "processor",
|
|
"Name": "Processor",
|
|
"Description": "Processor Device",
|
|
"Manufacturer": "Intel Corporation",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/processor/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/processor/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/processor/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/processor/PCIeFunctions/1": map[string]interface{}{
|
|
"DeviceClass": "Processor",
|
|
"VendorId": "0x8086",
|
|
"DeviceId": "0x4944",
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/signal": map[string]interface{}{
|
|
"Id": "signal",
|
|
"Name": "Signal",
|
|
"Manufacturer": "Intel Corporation",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/signal/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/signal/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/signal/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/signal/PCIeFunctions/1": map[string]interface{}{
|
|
"DeviceClass": "SignalProcessingController",
|
|
"VendorId": "0x8086",
|
|
"DeviceId": "0x3254",
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/serial": map[string]interface{}{
|
|
"Id": "serial",
|
|
"Name": "Serial",
|
|
"Manufacturer": "Renesas",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/serial/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/serial/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/serial/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/serial/PCIeFunctions/1": map[string]interface{}{
|
|
"DeviceClass": "SerialBusController",
|
|
"VendorId": "0x1912",
|
|
"DeviceId": "0x0014",
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/display": map[string]interface{}{
|
|
"Id": "display",
|
|
"Name": "Display",
|
|
"Description": "Display Device",
|
|
"Manufacturer": "NVIDIA Corporation",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/display/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/display/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/display/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/display/PCIeFunctions/1": map[string]interface{}{
|
|
"DeviceClass": "DisplayController",
|
|
"VendorId": "0x10de",
|
|
"DeviceId": "0x233b",
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/network": map[string]interface{}{
|
|
"Id": "network",
|
|
"Name": "NIC",
|
|
"Description": "Network Device",
|
|
"Manufacturer": "Mellanox Technologies",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/network/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/network/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/network/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/network/PCIeFunctions/1": map[string]interface{}{
|
|
"DeviceClass": "NetworkController",
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x101f",
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/storage": map[string]interface{}{
|
|
"Id": "storage",
|
|
"Name": "Storage",
|
|
"Description": "Storage Device",
|
|
"Manufacturer": "Intel Corporation",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/storage/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/storage/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/storage/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/storage/PCIeFunctions/1": map[string]interface{}{
|
|
"DeviceClass": "MassStorageController",
|
|
"VendorId": "0x1234",
|
|
"DeviceId": "0x5678",
|
|
},
|
|
}}
|
|
|
|
got := r.collectPCIeDevices(nil, []string{"/redfish/v1/Chassis/1"})
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected only endpoint PCIe devices to remain, got %d: %+v", len(got), got)
|
|
}
|
|
classes := map[string]bool{}
|
|
for _, dev := range got {
|
|
classes[dev.DeviceClass] = true
|
|
}
|
|
if !classes["NetworkController"] || !classes["MassStorageController"] {
|
|
t.Fatalf("expected network and storage PCIe devices to remain, got %+v", got)
|
|
}
|
|
if classes["Bridge"] || classes["Processor"] || classes["SignalProcessingController"] || classes["SerialBusController"] || classes["DisplayController"] {
|
|
t.Fatalf("expected MSI topology noise classes to be filtered, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectPCIeDevices_SkipsNICsAlreadyRepresentedAsNetworkAdapters(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/nic"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/nic": map[string]interface{}{
|
|
"Id": "nic",
|
|
"Name": "PCIeDevice_00_39_00",
|
|
"Description": "Network Device",
|
|
"Manufacturer": "Mellanox Technologies",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/nic/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/nic/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/nic/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/nic/PCIeFunctions/1": map[string]interface{}{
|
|
"DeviceClass": "NetworkController",
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x101f",
|
|
"Links": map[string]interface{}{
|
|
"NetworkDeviceFunctions": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions/Function0"},
|
|
},
|
|
"NetworkDeviceFunctions@odata.count": 1,
|
|
},
|
|
},
|
|
}}
|
|
|
|
got := r.collectPCIeDevices(nil, []string{"/redfish/v1/Chassis/1"})
|
|
if len(got) != 0 {
|
|
t.Fatalf("expected network-backed PCIe duplicate to be skipped, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestReplayCollectPCIeDevices_SkipsStorageServiceEndpoints(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/vmd"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/switch-mgmt"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/hba"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/vmd": map[string]interface{}{
|
|
"Id": "vmd",
|
|
"Description": "Storage Device",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/vmd/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/vmd/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/vmd/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/vmd/PCIeFunctions/1": map[string]interface{}{
|
|
"DeviceClass": "MassStorageController",
|
|
"VendorId": "0x8086",
|
|
"DeviceId": "0x28c0",
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/switch-mgmt": map[string]interface{}{
|
|
"Id": "switch-mgmt",
|
|
"Description": "Storage Device",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/switch-mgmt/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/switch-mgmt/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/switch-mgmt/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/switch-mgmt/PCIeFunctions/1": map[string]interface{}{
|
|
"DeviceClass": "MassStorageController",
|
|
"VendorId": "0x1000",
|
|
"DeviceId": "0x00b2",
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/hba": map[string]interface{}{
|
|
"Id": "hba",
|
|
"Description": "Storage Device",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/hba/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/hba/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/hba/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/hba/PCIeFunctions/1": map[string]interface{}{
|
|
"DeviceClass": "MassStorageController",
|
|
"VendorId": "0x1234",
|
|
"DeviceId": "0x5678",
|
|
},
|
|
}}
|
|
|
|
got := r.collectPCIeDevices(nil, []string{"/redfish/v1/Chassis/1"})
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected only non-service storage controller to remain, got %+v", got)
|
|
}
|
|
if got[0].VendorID != 0x1234 || got[0].DeviceID != 0x5678 {
|
|
t.Fatalf("expected generic HBA to remain, got %+v", got[0])
|
|
}
|
|
}
|
|
|
|
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 TestParseBoardInfo_UsesSKUAsPartNumberFallback(t *testing.T) {
|
|
got := parseBoardInfo(map[string]interface{}{
|
|
"Manufacturer": "HPE",
|
|
"Model": "ProLiant DL380 Gen11",
|
|
"SerialNumber": "CZ2D1X0GS4",
|
|
"SKU": "P52560-421",
|
|
})
|
|
if got.PartNumber != "P52560-421" {
|
|
t.Fatalf("expected SKU to populate part number, got %q", got.PartNumber)
|
|
}
|
|
}
|
|
|
|
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, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableGenericGraphicsControllerDedup: true}))
|
|
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, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableGenericGraphicsControllerDedup: true}))
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected both GPUs to be kept by unique redfish path, got %d", len(got))
|
|
}
|
|
}
|
|
|
|
func TestParseGPU_xFusionPCIeInterfaceMaxlanes(t *testing.T) {
|
|
// xFusion GPU PCIeDevices (PCIeCard1..N) carry link width in PCIeInterface
|
|
// with "Maxlanes" (lowercase 'l') rather than "MaxLanes".
|
|
doc := map[string]interface{}{
|
|
"Id": "PCIeCard1",
|
|
"Model": "RTX PRO 6000",
|
|
"PCIeInterface": map[string]interface{}{
|
|
"LanesInUse": 16,
|
|
"Maxlanes": 16,
|
|
"PCIeType": "Gen5",
|
|
"MaxPCIeType": "Gen5",
|
|
},
|
|
}
|
|
gpu := parseGPU(doc, nil, 1)
|
|
if gpu.CurrentLinkWidth != 16 || gpu.MaxLinkWidth != 16 {
|
|
t.Fatalf("expected link widths 16/16 from PCIeInterface, got current=%d max=%d", gpu.CurrentLinkWidth, gpu.MaxLinkWidth)
|
|
}
|
|
if gpu.CurrentLinkSpeed != "Gen5" || gpu.MaxLinkSpeed != "Gen5" {
|
|
t.Fatalf("expected link speeds Gen5/Gen5 from PCIeInterface, got current=%q max=%q", gpu.CurrentLinkSpeed, gpu.MaxLinkSpeed)
|
|
}
|
|
}
|
|
|
|
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_StoresAnalysisProfilesMetadata(t *testing.T) {
|
|
raw := map[string]any{
|
|
"redfish_tree": map[string]interface{}{
|
|
"/redfish/v1": map[string]interface{}{
|
|
"Vendor": "AMI",
|
|
"Product": "AMI Redfish",
|
|
"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": "Micro-Star International Co., Ltd.",
|
|
"Model": "CG290",
|
|
},
|
|
"/redfish/v1/Chassis": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1": map[string]interface{}{
|
|
"Manufacturer": "Micro-Star International Co., Ltd.",
|
|
"Model": "CG290",
|
|
},
|
|
"/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",
|
|
},
|
|
},
|
|
}
|
|
|
|
got, err := ReplayRedfishFromRawPayloads(raw, nil)
|
|
if err != nil {
|
|
t.Fatalf("replay failed: %v", err)
|
|
}
|
|
meta, ok := got.RawPayloads["redfish_analysis_profiles"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected redfish_analysis_profiles metadata")
|
|
}
|
|
if meta["mode"] != redfishprofile.ModeMatched {
|
|
t.Fatalf("expected matched mode, got %#v", meta["mode"])
|
|
}
|
|
profiles, ok := meta["profiles"].([]string)
|
|
if !ok {
|
|
t.Fatalf("expected []string profiles, got %T", meta["profiles"])
|
|
}
|
|
foundMSI := false
|
|
for _, profile := range profiles {
|
|
if profile == "msi" {
|
|
foundMSI = true
|
|
break
|
|
}
|
|
}
|
|
if !foundMSI {
|
|
t.Fatalf("expected msi in applied profiles, got %v", profiles)
|
|
}
|
|
planMeta, ok := got.RawPayloads["redfish_analysis_plan"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected redfish_analysis_plan metadata")
|
|
}
|
|
directives, ok := planMeta["directives"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected directives map in redfish_analysis_plan")
|
|
}
|
|
if directives["generic_graphics_controller_dedup"] != true {
|
|
t.Fatalf("expected generic_graphics_controller_dedup directive, got %#v", directives["generic_graphics_controller_dedup"])
|
|
}
|
|
}
|
|
|
|
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, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableGenericGraphicsControllerDedup: true}))
|
|
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 TestReplayCollectGPUs_KeepsModelOnlyGraphicsDuplicateWhenDirectiveDisabled(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/4"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/9"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/4": map[string]interface{}{
|
|
"Id": "4",
|
|
"Name": "PCIeCard4",
|
|
"Model": "H200-SXM5-141G",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "1654225094493",
|
|
},
|
|
"/redfish/v1/Chassis/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"}, []string{"/redfish/v1/Chassis/1"}, testAnalysisPlan(redfishprofile.AnalysisDirectives{}))
|
|
if len(got) != 3 {
|
|
t.Fatalf("expected model-only graphics duplicate to remain when directive is off, got %d", len(got))
|
|
}
|
|
}
|
|
|
|
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 TestRedfishPSUNominalWattage_PrefersInputRangeOutputWattage(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"PowerCapacityWatts": 22600,
|
|
"InputRanges": []interface{}{
|
|
map[string]interface{}{"OutputWattage": 2700},
|
|
map[string]interface{}{"OutputWattage": 3200},
|
|
},
|
|
}
|
|
|
|
if got := redfishPSUNominalWattage(doc); got != 3200 {
|
|
t.Fatalf("redfishPSUNominalWattage() = %d, want 3200", got)
|
|
}
|
|
}
|
|
|
|
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"}, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableGenericGraphicsControllerDedup: true}))
|
|
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"}, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableGenericGraphicsControllerDedup: true}))
|
|
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"}, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableGenericGraphicsControllerDedup: true}))
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCollectGPUsFromProcessors_SupermicroHGX verifies that GPU-type Processor
|
|
// entries (Supermicro HGX: HGX_Baseboard_0/Processors/GPU_SXM_N) are not added
|
|
// as duplicates when the same GPU is already present via Chassis PCIeDevices.
|
|
// The processor doc carries SerialNumber directly; the chassis ID ("HGX_GPU_SXM_1")
|
|
// does NOT match the processor Id ("GPU_SXM_1"), so chassis-based serial lookup
|
|
// fails and the dedup must fall back to the processor doc's own SerialNumber.
|
|
func TestCollectGPUsFromProcessors_SupermicroHGX(t *testing.T) {
|
|
tree := map[string]interface{}{
|
|
// Main chassis PCIeDevices — GPU1 and GPU2 with serials.
|
|
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU2"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/GPU1": map[string]interface{}{
|
|
"Id": "GPU1",
|
|
"Name": "GPU1",
|
|
"Model": "NVIDIA H200",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "SN001",
|
|
"FirmwareVersion": "96.00.D9.00.02",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions/1": map[string]interface{}{
|
|
"FunctionId": "1",
|
|
"ClassCode": "0x030200",
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/GPU2": map[string]interface{}{
|
|
"Id": "GPU2",
|
|
"Name": "GPU2",
|
|
"Model": "NVIDIA H200",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "SN002",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU2/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/GPU2/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU2/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/GPU2/PCIeFunctions/1": map[string]interface{}{
|
|
"FunctionId": "2",
|
|
"ClassCode": "0x030200",
|
|
},
|
|
// HGX GPU chassis — named HGX_GPU_SXM_N (NOT GPU_SXM_N), so chassis-ID lookup
|
|
// by processor Id "GPU_SXM_1" will NOT find them.
|
|
"/redfish/v1/Chassis/HGX_GPU_SXM_1": map[string]interface{}{
|
|
"Id": "HGX_GPU_SXM_1",
|
|
},
|
|
"/redfish/v1/Chassis/HGX_GPU_SXM_2": map[string]interface{}{
|
|
"Id": "HGX_GPU_SXM_2",
|
|
},
|
|
// HGX Baseboard system with GPU-type Processors carrying the same serials.
|
|
"/redfish/v1/Systems/HGX_Baseboard_0/Processors": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/HGX_Baseboard_0/Processors/GPU_SXM_1"},
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/HGX_Baseboard_0/Processors/GPU_SXM_2"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/HGX_Baseboard_0/Processors/GPU_SXM_1": map[string]interface{}{
|
|
"Id": "GPU_SXM_1",
|
|
"Name": "Processor",
|
|
"ProcessorType": "GPU",
|
|
"Model": "NVIDIA H200",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "SN001",
|
|
"UUID": "aaaaaaaa-0000-0000-0000-000000000001",
|
|
"Location": map[string]interface{}{
|
|
"PartLocation": map[string]interface{}{
|
|
"ServiceLabel": "SXM1",
|
|
},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/HGX_Baseboard_0/Processors/GPU_SXM_2": map[string]interface{}{
|
|
"Id": "GPU_SXM_2",
|
|
"Name": "Processor",
|
|
"ProcessorType": "GPU",
|
|
"Model": "NVIDIA H200",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "SN002",
|
|
"UUID": "aaaaaaaa-0000-0000-0000-000000000002",
|
|
"Location": map[string]interface{}{
|
|
"PartLocation": map[string]interface{}{
|
|
"ServiceLabel": "SXM2",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
r := redfishSnapshotReader{tree: tree}
|
|
chassisPaths := []string{
|
|
"/redfish/v1/Chassis/1",
|
|
"/redfish/v1/Chassis/HGX_GPU_SXM_1",
|
|
"/redfish/v1/Chassis/HGX_GPU_SXM_2",
|
|
}
|
|
systemPaths := []string{"/redfish/v1/Systems/HGX_Baseboard_0"}
|
|
|
|
gpus := r.collectGPUs(systemPaths, chassisPaths, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableGenericGraphicsControllerDedup: true}))
|
|
gpus = r.collectGPUsFromProcessors(systemPaths, chassisPaths, gpus, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableProcessorGPUFallback: true}))
|
|
|
|
if len(gpus) != 2 {
|
|
var slots []string
|
|
for _, g := range gpus {
|
|
slots = append(slots, fmt.Sprintf("%s(sn=%s)", g.Slot, g.SerialNumber))
|
|
}
|
|
t.Fatalf("expected 2 GPUs (no duplicates), got %d: %v", len(gpus), slots)
|
|
}
|
|
}
|
|
|
|
func TestCollectGPUsFromProcessors_SupermicroHGXUsesChassisAliasSerial(t *testing.T) {
|
|
tree := map[string]interface{}{
|
|
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/GPU1": map[string]interface{}{
|
|
"Id": "GPU1",
|
|
"Name": "GPU1",
|
|
"Model": "NVIDIA H200",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "SN-ALIAS-001",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions/1": map[string]interface{}{
|
|
"FunctionId": "1",
|
|
"ClassCode": "0x030200",
|
|
},
|
|
"/redfish/v1/Chassis/HGX_GPU_SXM_1": map[string]interface{}{
|
|
"Id": "HGX_GPU_SXM_1",
|
|
"SerialNumber": "SN-ALIAS-001",
|
|
},
|
|
"/redfish/v1/Systems/HGX_Baseboard_0/Processors": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/HGX_Baseboard_0/Processors/GPU_SXM_1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/HGX_Baseboard_0/Processors/GPU_SXM_1": map[string]interface{}{
|
|
"Id": "GPU_SXM_1",
|
|
"Name": "Processor",
|
|
"ProcessorType": "GPU",
|
|
"Model": "NVIDIA H200",
|
|
"Manufacturer": "NVIDIA",
|
|
},
|
|
}
|
|
|
|
r := redfishSnapshotReader{tree: tree}
|
|
chassisPaths := []string{
|
|
"/redfish/v1/Chassis/1",
|
|
"/redfish/v1/Chassis/HGX_GPU_SXM_1",
|
|
}
|
|
systemPaths := []string{"/redfish/v1/Systems/HGX_Baseboard_0"}
|
|
|
|
gpus := r.collectGPUs(systemPaths, chassisPaths, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableGenericGraphicsControllerDedup: true}))
|
|
gpus = r.collectGPUsFromProcessors(systemPaths, chassisPaths, gpus, redfishprofile.ResolvedAnalysisPlan{
|
|
Directives: redfishprofile.AnalysisDirectives{EnableProcessorGPUFallback: true, EnableProcessorGPUChassisAlias: true},
|
|
ProcessorGPUChassisLookupModes: []string{"hgx-alias"},
|
|
})
|
|
|
|
if len(gpus) != 1 {
|
|
t.Fatalf("expected alias serial dedupe to keep 1 gpu, got %d", len(gpus))
|
|
}
|
|
if gpus[0].SerialNumber != "SN-ALIAS-001" {
|
|
t.Fatalf("expected serial from aliased chassis, got %q", gpus[0].SerialNumber)
|
|
}
|
|
}
|
|
|
|
func TestCollectGPUsFromProcessors_MSIUsesIndexedChassisLookup(t *testing.T) {
|
|
tree := map[string]interface{}{
|
|
"/redfish/v1/Chassis/GPU1": map[string]interface{}{
|
|
"Id": "GPU1",
|
|
"SerialNumber": "MSI-SN-001",
|
|
},
|
|
"/redfish/v1/Systems/1/Processors": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Processors/GPU_SXM_1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Processors/GPU_SXM_1": map[string]interface{}{
|
|
"Id": "GPU_SXM_1",
|
|
"Name": "Processor",
|
|
"ProcessorType": "GPU",
|
|
"Model": "NVIDIA RTX PRO 6000 Blackwell",
|
|
"Manufacturer": "NVIDIA",
|
|
},
|
|
}
|
|
|
|
r := redfishSnapshotReader{tree: tree}
|
|
gpus := r.collectGPUsFromProcessors(
|
|
[]string{"/redfish/v1/Systems/1"},
|
|
[]string{"/redfish/v1/Chassis/GPU1"},
|
|
nil,
|
|
redfishprofile.ResolvedAnalysisPlan{
|
|
Directives: redfishprofile.AnalysisDirectives{EnableProcessorGPUFallback: true, EnableMSIProcessorGPUChassisLookup: true},
|
|
ProcessorGPUChassisLookupModes: []string{"msi-index"},
|
|
},
|
|
)
|
|
|
|
if len(gpus) != 1 {
|
|
t.Fatalf("expected one gpu, got %d", len(gpus))
|
|
}
|
|
if gpus[0].SerialNumber != "MSI-SN-001" {
|
|
t.Fatalf("expected serial from MSI indexed chassis lookup, got %q", gpus[0].SerialNumber)
|
|
}
|
|
}
|
|
|
|
// TestReplayCollectGPUs_DedupCrossChassisSerial verifies that the same GPU
|
|
// appearing under two Chassis PCIeDevice trees (e.g. Chassis/1/PCIeDevices/GPU1
|
|
// and Chassis/HGX_GPU_SXM_1/PCIeDevices/GPU_SXM_1) is deduplicated to one entry
|
|
// when both expose the same serial number.
|
|
func TestReplayCollectGPUs_DedupCrossChassisSerial(t *testing.T) {
|
|
tree := map[string]interface{}{
|
|
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/GPU1": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1",
|
|
"Id": "GPU1",
|
|
"Name": "GPU1",
|
|
"Model": "NVIDIA H200",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "SN-CROSSTEST-001",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions/1": map[string]interface{}{
|
|
"FunctionId": "1",
|
|
"ClassCode": "0x030200",
|
|
},
|
|
// Same GPU exposed via dedicated HGX chassis — same serial, different path.
|
|
"/redfish/v1/Chassis/HGX_GPU_SXM_1/PCIeDevices": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/HGX_GPU_SXM_1/PCIeDevices/GPU_SXM_1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/HGX_GPU_SXM_1/PCIeDevices/GPU_SXM_1": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/HGX_GPU_SXM_1/PCIeDevices/GPU_SXM_1",
|
|
"Id": "GPU_SXM_1",
|
|
"Name": "PCIe Device",
|
|
"Model": "NVIDIA H200",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "SN-CROSSTEST-001",
|
|
"UUID": "deadbeef-0000-0000-0000-000000000001",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/HGX_GPU_SXM_1/PCIeDevices/GPU_SXM_1/PCIeFunctions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/HGX_GPU_SXM_1/PCIeDevices/GPU_SXM_1/PCIeFunctions": map[string]interface{}{
|
|
"Members": []interface{}{},
|
|
},
|
|
}
|
|
|
|
r := redfishSnapshotReader{tree: tree}
|
|
got := r.collectGPUs(nil, []string{
|
|
"/redfish/v1/Chassis/1",
|
|
"/redfish/v1/Chassis/HGX_GPU_SXM_1",
|
|
}, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableGenericGraphicsControllerDedup: true}))
|
|
if len(got) != 1 {
|
|
var slots []string
|
|
for _, g := range got {
|
|
slots = append(slots, fmt.Sprintf("%s(sn=%s)", g.Slot, g.SerialNumber))
|
|
}
|
|
t.Fatalf("expected 1 GPU (cross-chassis serial dedup), got %d: %v", len(got), slots)
|
|
}
|
|
if got[0].SerialNumber != "SN-CROSSTEST-001" {
|
|
t.Fatalf("unexpected serial %q", got[0].SerialNumber)
|
|
}
|
|
}
|
|
|
|
// TestLooksLikeGPU_NVSwitchExcluded verifies that NVSwitch PCIe devices
|
|
// are not classified as GPUs even though their manufacturer is NVIDIA.
|
|
func TestLooksLikeGPU_NVSwitchExcluded(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"Id": "NVSwitch_0",
|
|
"Name": "PCIe Device",
|
|
"Model": "NVSwitch",
|
|
"Manufacturer": "NVIDIA",
|
|
"DeviceType": "SingleFunction",
|
|
}
|
|
if looksLikeGPU(doc, nil) {
|
|
t.Fatal("NVSwitch should not be classified as a GPU")
|
|
}
|
|
}
|
|
|
|
func TestFirmwareInventoryDeviceName_PrefersIDForGenericSoftwareInventory(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"Id": "HGX_FW_NVSwitch_0",
|
|
"Name": "Software Inventory",
|
|
"Version": "96.10.73.00.01",
|
|
}
|
|
|
|
got := firmwareInventoryDeviceName(doc)
|
|
if got != "HGX_FW_NVSwitch_0" {
|
|
t.Fatalf("expected firmware inventory id to be used, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestParsePCIeDeviceWithSupplementalDocs_NVSwitchThermalMetrics(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/HGX_NVSwitch_0/PCIeDevices/NVSwitch_0",
|
|
"Id": "NVSwitch_0",
|
|
"Model": "NVSwitch",
|
|
"Manufacturer": "NVIDIA",
|
|
"DeviceType": "SingleFunction",
|
|
}
|
|
|
|
supplementalDocs := []map[string]interface{}{
|
|
{
|
|
"TemperatureReadingsCelsius": []interface{}{
|
|
map[string]interface{}{
|
|
"DeviceName": "NVSwitch_0",
|
|
"Reading": "31.593750",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
got := parsePCIeDeviceWithSupplementalDocs(doc, nil, supplementalDocs)
|
|
if got.Details == nil {
|
|
t.Fatalf("expected NVSwitch details to be populated")
|
|
}
|
|
if temp := got.Details["temperature_c"]; temp != 31.59375 {
|
|
t.Fatalf("expected NVSwitch thermal metric, got %#v", got.Details)
|
|
}
|
|
}
|
|
|
|
func TestShouldCrawlPath_MemoryAndProcessorMetricsAreAllowed(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 crawlable")
|
|
}
|
|
if !shouldCrawlPath("/redfish/v1/Systems/1/Processors/CPU0/ProcessorMetrics") {
|
|
t.Fatalf("expected CPU metrics subresource to be crawlable")
|
|
}
|
|
if shouldCrawlPath("/redfish/v1/Chassis/1/PCIeDevices/0/PCIeFunctions") {
|
|
t.Fatalf("expected broad chassis PCIeFunctions collection to be skipped")
|
|
}
|
|
if !shouldCrawlPath("/redfish/v1/Chassis/1/PCIeDevices/0/PCIeFunctions/1") {
|
|
t.Fatalf("expected direct chassis PCIeFunction member to remain crawlable")
|
|
}
|
|
if !shouldCrawlPath("/redfish/v1/Fabrics/HGX_NVLinkFabric_0/Switches/NVSwitch_0") {
|
|
t.Fatalf("expected NVSwitch fabric resource to be crawlable")
|
|
}
|
|
if !shouldCrawlPath("/redfish/v1/Fabrics/HGX_NVLinkFabric_0/Switches/NVSwitch_0/Ports/NVLink_0/Metrics") {
|
|
t.Fatalf("expected NVLink port metrics to be crawlable")
|
|
}
|
|
}
|
|
|
|
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) {
|
|
var tuning redfishprofile.AcquisitionTuning
|
|
if shouldPostProbeCollectionPath("/redfish/v1/Chassis/1/Sensors", tuning) {
|
|
t.Fatalf("expected sensors collection to be skipped by default")
|
|
}
|
|
if shouldPostProbeCollectionPath("/redfish/v1/Systems/1/Storage/RAID/Drives", tuning) {
|
|
t.Fatalf("expected drives collection to be skipped without profile policy")
|
|
}
|
|
tuning.PostProbePolicy.EnableNumericCollectionProbe = true
|
|
t.Setenv("LOGPILE_REDFISH_SENSOR_POSTPROBE", "1")
|
|
if !shouldPostProbeCollectionPath("/redfish/v1/Chassis/1/Sensors", tuning) {
|
|
t.Fatalf("expected sensors collection to be post-probed when enabled")
|
|
}
|
|
if !shouldPostProbeCollectionPath("/redfish/v1/Systems/1/Storage/RAID/Drives", tuning) {
|
|
t.Fatalf("expected drives collection to be post-probed")
|
|
}
|
|
if shouldPostProbeCollectionPath("/redfish/v1/Chassis/1/Boards/BOARD1", tuning) {
|
|
t.Fatalf("expected board member resource to be skipped from post-probe")
|
|
}
|
|
if shouldPostProbeCollectionPath("/redfish/v1/Chassis/1/Assembly/Oem/COMMONb/COMMONbAssembly/1", tuning) {
|
|
t.Fatalf("expected assembly member resource to be skipped from post-probe")
|
|
}
|
|
}
|
|
|
|
func TestShouldAdaptivePostProbeCollectionPath(t *testing.T) {
|
|
tuning := redfishprofile.AcquisitionTuning{
|
|
PostProbePolicy: redfishprofile.AcquisitionPostProbePolicy{
|
|
EnableNumericCollectionProbe: true,
|
|
},
|
|
}
|
|
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, tuning) {
|
|
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, tuning) {
|
|
t.Fatalf("expected numeric members to allow adaptive post-probe")
|
|
}
|
|
|
|
withoutMembers := map[string]interface{}{"Name": "Drives"}
|
|
if !shouldAdaptivePostProbeCollectionPath("/redfish/v1/Chassis/1/Drives", withoutMembers, tuning) {
|
|
t.Fatalf("expected missing members to allow adaptive post-probe")
|
|
}
|
|
|
|
if shouldAdaptivePostProbeCollectionPath("/redfish/v1/Chassis/1/Drives", withoutMembers, redfishprofile.AcquisitionTuning{}) {
|
|
t.Fatalf("expected post-probe to stay disabled without profile policy")
|
|
}
|
|
}
|
|
|
|
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) {
|
|
tuning := redfishprofile.AcquisitionTuning{
|
|
PrefetchPolicy: redfishprofile.AcquisitionPrefetchPolicy{
|
|
IncludeSuffixes: []string{
|
|
"/Memory",
|
|
"/Processors",
|
|
"/Storage",
|
|
},
|
|
},
|
|
}
|
|
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(redfishPrefetchTargets(candidates, tuning), 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 TestResolveAcquisitionPlan_DefaultSkipsNoisyBranches(t *testing.T) {
|
|
signals := redfishprofile.MatchSignals{}
|
|
match := redfishprofile.MatchProfiles(signals)
|
|
plan := redfishprofile.BuildAcquisitionPlan(signals)
|
|
resolved := redfishprofile.ResolveAcquisitionPlan(match, plan, redfishprofile.DiscoveredResources{
|
|
SystemPaths: []string{"/redfish/v1/Systems/1"},
|
|
ChassisPaths: []string{"/redfish/v1/Chassis/1"},
|
|
ManagerPaths: []string{"/redfish/v1/Managers/1"},
|
|
}, signals)
|
|
seeds := resolved.SeedPaths
|
|
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 TestShouldPrefetchCriticalPath_UsesPrefetchPolicy(t *testing.T) {
|
|
tuning := redfishprofile.AcquisitionTuning{
|
|
PrefetchPolicy: redfishprofile.AcquisitionPrefetchPolicy{
|
|
IncludeSuffixes: []string{"/Storage", "/Oem/Public"},
|
|
ExcludeContains: []string{"/Assembly"},
|
|
},
|
|
}
|
|
if !shouldPrefetchCriticalPath("/redfish/v1/Systems/1/Storage", tuning) {
|
|
t.Fatal("expected storage path to be prefetched when included by policy")
|
|
}
|
|
if !shouldPrefetchCriticalPath("/redfish/v1/Systems/1/Oem/Public", tuning) {
|
|
t.Fatal("expected OEM public path to be prefetched when included by policy")
|
|
}
|
|
if shouldPrefetchCriticalPath("/redfish/v1/Chassis/1/Assembly", tuning) {
|
|
t.Fatal("expected excluded path to skip prefetch")
|
|
}
|
|
if shouldPrefetchCriticalPath("/redfish/v1/Chassis/1/Power", redfishprofile.AcquisitionTuning{}) {
|
|
t.Fatal("expected empty prefetch policy to disable suffix-based prefetch")
|
|
}
|
|
}
|
|
|
|
func TestRedfishPrefetchTargets_FilterNoisyBranches(t *testing.T) {
|
|
tuning := redfishprofile.AcquisitionTuning{
|
|
PrefetchPolicy: redfishprofile.AcquisitionPrefetchPolicy{
|
|
IncludeSuffixes: []string{
|
|
"/Memory",
|
|
"/Oem/Public/FRU",
|
|
"/Drives",
|
|
"/NetworkProtocol",
|
|
},
|
|
ExcludeContains: []string{
|
|
"/Backplanes",
|
|
"/Sensors",
|
|
"/LogServices",
|
|
},
|
|
},
|
|
}
|
|
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, tuning)
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestChassisTypeCanHaveNVMe verifies that non-storage chassis types (GPU modules,
|
|
// RoT components, fabric zones) are excluded from NVMe bay probing, while storage
|
|
// and unclassified chassis types are kept.
|
|
//
|
|
// Regression guard: on Supermicro HGX (SYS-A21GE-NBRT) all 35 sub-chassis (GPUs,
|
|
// NVSwitches, PCIeRetimers, ERoT/IRoT, BMC, FPGA) have ChassisType=Module/Component/Zone
|
|
// and expose empty /Drives collections. Without this filter each chassis triggered
|
|
// 384 HTTP requests → ~22 minutes wasted per collection. (2026-03-12)
|
|
func TestChassisTypeCanHaveNVMe(t *testing.T) {
|
|
cases := []struct {
|
|
chassisType string
|
|
want bool
|
|
}{
|
|
// Non-storage sub-module types — must return false
|
|
{"Module", false}, // GPU SXM, PCIeRetimer, NVLinkManagementNIC
|
|
{"module", false}, // case-insensitive
|
|
{"Component", false}, // ERoT, IRoT, BMC, FPGA sub-chassis
|
|
{"component", false},
|
|
{"Zone", false}, // HGX_Chassis_0 fabric zone
|
|
{"zone", false},
|
|
// Storage-capable and generic types — must return true
|
|
{"Enclosure", true}, // NVMe StorageBackplane
|
|
{"RackMount", true}, // main server chassis
|
|
{"Blade", true}, // blade server chassis
|
|
{"StandAlone", true}, // standalone server
|
|
{"", true}, // unknown type — probe to be safe
|
|
}
|
|
for _, tc := range cases {
|
|
got := chassisTypeCanHaveNVMe(tc.chassisType)
|
|
if got != tc.want {
|
|
t.Errorf("chassisTypeCanHaveNVMe(%q) = %v, want %v", tc.chassisType, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestNVMePostProbeSkipsNonStorageChassis verifies that the NVMe bay probe candidate
|
|
// selection skips chassis whose ChassisType indicates they cannot hold NVMe drives.
|
|
//
|
|
// Simulates an HGX topology: one GPU chassis (Module) and one NVMe backplane
|
|
// (Enclosure), both with empty /Drives collections. Only the backplane must be
|
|
// selected as a probe candidate.
|
|
func TestNVMePostProbeSkipsNonStorageChassis(t *testing.T) {
|
|
// Build the out map as collectRawRedfishTree would produce it
|
|
out := map[string]interface{}{
|
|
// GPU chassis — Module type, empty Drives: should be skipped
|
|
"/redfish/v1/Chassis/HGX_GPU_SXM_1": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/HGX_GPU_SXM_1",
|
|
"ChassisType": "Module",
|
|
"Name": "HGX_GPU_SXM_1",
|
|
},
|
|
"/redfish/v1/Chassis/HGX_GPU_SXM_1/Drives": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/HGX_GPU_SXM_1/Drives",
|
|
"Members": []interface{}{},
|
|
"Members@odata.count": 0,
|
|
},
|
|
// NVMe backplane — Enclosure type, empty Drives: must be selected
|
|
"/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane",
|
|
"ChassisType": "Enclosure",
|
|
"Name": "Backplane",
|
|
},
|
|
"/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives",
|
|
"Members": []interface{}{},
|
|
"Members@odata.count": 0,
|
|
},
|
|
}
|
|
|
|
// Replicate the candidate selection logic from collectRawRedfishTree
|
|
var selected []string
|
|
for path, docAny := range out {
|
|
normalized := normalizeRedfishPath(path)
|
|
if !strings.HasSuffix(normalized, "/Drives") {
|
|
continue
|
|
}
|
|
doc, _ := docAny.(map[string]interface{})
|
|
if !shouldAdaptiveNVMeProbe(doc) {
|
|
continue
|
|
}
|
|
chassisPath := strings.TrimSuffix(normalized, "/Drives")
|
|
if chassisDocAny, ok := out[chassisPath]; ok {
|
|
if chassisDoc, ok := chassisDocAny.(map[string]interface{}); ok {
|
|
if !chassisTypeCanHaveNVMe(asString(chassisDoc["ChassisType"])) {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
selected = append(selected, normalized)
|
|
}
|
|
|
|
if len(selected) != 1 {
|
|
t.Fatalf("expected 1 NVMe probe candidate (backplane), got %d: %v", len(selected), selected)
|
|
}
|
|
if !strings.Contains(selected[0], "StorageBackplane") {
|
|
t.Fatalf("expected StorageBackplane to be selected, got %q", selected[0])
|
|
}
|
|
}
|
|
|
|
func TestIsVirtualStorageDrive_AMIVirtualMedia(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"Id": "USB_Device1_Port4",
|
|
"Name": "Virtual Cdrom Device",
|
|
"Model": "Virtual Cdrom Device",
|
|
"Manufacturer": "American Megatrends Inc.",
|
|
"Protocol": "USB",
|
|
"CapacityBytes": 0,
|
|
"SerialNumber": "AAAABBBBCCCC1",
|
|
}
|
|
|
|
if !isVirtualStorageDrive(doc) {
|
|
t.Fatalf("expected AMI virtual media to be filtered")
|
|
}
|
|
}
|