looksLikeGPU now falls back to resolving VendorId/DeviceId through the pci.ids database when the BMC leaves Name/Model/Manufacturer/ClassCode empty, so GPUs identifiable only by raw PCI IDs (e.g. NVIDIA H100 SXM5 0x10de/0x2330) are no longer misclassified as generic PCIe devices. The replay pipeline's "backed by canonical NIC" dedup used to trust a PCIeDevice's Links.NetworkDeviceFunctions reference at face value and drop the device, assuming a NetworkAdapters record existed elsewhere. On BMCs that expose resource IDs with characters (parentheses) that 404 on fetch, that canonical NIC never gets captured, so the device carrying its actual hardware identity vanished from the export entirely. hasResolvableLinkedMember now verifies the linked resource is actually present in the snapshot before treating it as authoritative. Also normalize PartNumber through normalizeRedfishIdentityField in the GPU/PCIe parsers so a BMC-supplied literal "null" string doesn't leak into exports verbatim. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4366 lines
150 KiB
Go
4366 lines
150 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)
|
|
}
|
|
|
|
driveWithPCIeFn := parseDriveWithSupplementalDocs(
|
|
map[string]interface{}{
|
|
"Id": "Drive1",
|
|
"SerialNumber": "NVME-001",
|
|
},
|
|
map[string]interface{}{
|
|
"VendorId": "0x144D",
|
|
"DeviceId": "0xA808",
|
|
},
|
|
)
|
|
if driveWithPCIeFn.VendorID != 0x144D || driveWithPCIeFn.DeviceID != 0xA808 {
|
|
t.Fatalf("expected vendor/device id from linked PCIeFunction doc, got vendor=%#x device=%#x", driveWithPCIeFn.VendorID, driveWithPCIeFn.DeviceID)
|
|
}
|
|
|
|
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,
|
|
},
|
|
},
|
|
// The linked NetworkDeviceFunctions resource was actually captured in
|
|
// the snapshot, so the canonical NIC is genuinely available and this
|
|
// PCIe duplicate should be skipped.
|
|
"/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions/Function0": map[string]interface{}{
|
|
"Id": "Function0",
|
|
},
|
|
}}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// TestReplayCollectPCIeDevices_KeepsNICWhenCanonicalLinkIsUnresolvable covers
|
|
// xFusion BMCs (e.g. G5500 V7) that advertise a Links.NetworkDeviceFunctions
|
|
// reference on a PCIeDevice's Function, but whose target resource ID contains
|
|
// characters (parentheses) the BMC 404s on when fetched directly. Because
|
|
// that referenced resource was never actually captured in the snapshot, the
|
|
// PCIe device is the only surviving copy of the NIC's hardware identity and
|
|
// must not be dropped, or the NIC vanishes from the inventory entirely.
|
|
func TestReplayCollectPCIeDevices_KeepsNICWhenCanonicalLinkIsUnresolvable(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/OCPCard1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/OCPCard1": map[string]interface{}{
|
|
"Id": "OCPCard1",
|
|
"Name": "OCPCard1",
|
|
"CardManufacturer": "XFUSION",
|
|
"CardModel": "XC385",
|
|
"Manufacturer": "XFUSION",
|
|
"SerialNumber": "02Y238X6RC000058",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/OCPCard1/Functions",
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/OCPCard1/Functions": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/OCPCard1/Functions/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1/PCIeDevices/OCPCard1/Functions/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/MainboardOCPCard1(XC385)/NetworkDeviceFunctions/1"},
|
|
},
|
|
"NetworkDeviceFunctions@odata.count": 1,
|
|
},
|
|
},
|
|
// Deliberately no tree entry for the linked NetworkDeviceFunctions
|
|
// resource — it 404'd during collection and was never captured.
|
|
}}
|
|
|
|
got := r.collectPCIeDevices(nil, []string{"/redfish/v1/Chassis/1"})
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected the NIC's PCIe device to be kept when its canonical link is unresolvable, got %+v", got)
|
|
}
|
|
if got[0].SerialNumber != "02Y238X6RC000058" {
|
|
t.Fatalf("expected surviving NIC PCIe device, got %+v", got[0])
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
// TestLooksLikeGPU_ResolvesVendorDeviceIDWhenModelTextMissing covers xFusion
|
|
// BMCs (e.g. G5500 V7) whose PCIeDevice doc and linked PCIeFunction leave
|
|
// Name/Model/Manufacturer/ClassCode empty but expose raw VendorId/DeviceId
|
|
// (0x10de/0x2330 = NVIDIA H100 SXM5). Without a pci.ids lookup these devices
|
|
// silently fall through to the generic pcie_devices list instead of gpus.
|
|
func TestLooksLikeGPU_ResolvesVendorDeviceIDWhenModelTextMissing(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"Id": "PCIeCard1",
|
|
"Name": "PCIeCard1",
|
|
"Model": nil,
|
|
"Manufacturer": nil,
|
|
}
|
|
functionDocs := []map[string]interface{}{
|
|
{
|
|
"DeviceClass": "Other",
|
|
"VendorId": "0x10de",
|
|
"DeviceId": "0x2330",
|
|
},
|
|
}
|
|
if !looksLikeGPU(doc, functionDocs) {
|
|
t.Fatal("expected NVIDIA H100 (0x10de/0x2330) to be classified as a GPU via VendorId/DeviceId resolution")
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|