424 lines
13 KiB
Go
424 lines
13 KiB
Go
package collector
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestRedfishConnectorCollect(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
register := func(path string, payload interface{}) {
|
|
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(payload)
|
|
})
|
|
}
|
|
|
|
register("/redfish/v1", map[string]interface{}{"Name": "ServiceRoot"})
|
|
register("/redfish/v1/Systems/1", map[string]interface{}{
|
|
"Manufacturer": "Supermicro",
|
|
"Model": "SYS-TEST",
|
|
"SerialNumber": "SYS123",
|
|
"BiosVersion": "2.1a",
|
|
})
|
|
register("/redfish/v1/Systems/1/Bios", map[string]interface{}{"Version": "2.1a"})
|
|
register("/redfish/v1/Systems/1/SecureBoot", map[string]interface{}{"SecureBootCurrentBoot": "Enabled"})
|
|
register("/redfish/v1/Systems/1/Processors", map[string]interface{}{
|
|
"Members": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Systems/1/Processors/CPU1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/Processors/CPU1", map[string]interface{}{
|
|
"Name": "CPU1",
|
|
"Model": "Xeon Gold",
|
|
"TotalCores": 32,
|
|
"TotalThreads": 64,
|
|
"MaxSpeedMHz": 3600,
|
|
})
|
|
register("/redfish/v1/Systems/1/Memory", map[string]interface{}{
|
|
"Members": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Systems/1/Memory/DIMM1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/Memory/DIMM1", map[string]interface{}{
|
|
"Name": "DIMM A1",
|
|
"CapacityMiB": 32768,
|
|
"MemoryDeviceType": "DDR5",
|
|
"OperatingSpeedMhz": 4800,
|
|
"Status": map[string]interface{}{
|
|
"Health": "OK",
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/Storage", map[string]interface{}{
|
|
"Members": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Systems/1/Storage/1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/Storage/1", map[string]interface{}{
|
|
"Drives": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Systems/1/Storage/1/Drives/1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/Storage/1/Drives/1", map[string]interface{}{
|
|
"Name": "Drive1",
|
|
"Model": "NVMe Test",
|
|
"MediaType": "SSD",
|
|
"Protocol": "NVMe",
|
|
"CapacityGB": 960,
|
|
"SerialNumber": "SN123",
|
|
})
|
|
register("/redfish/v1/Systems/1/PCIeDevices", map[string]interface{}{
|
|
"Members": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Systems/1/PCIeDevices/GPU1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/PCIeDevices/GPU1", map[string]interface{}{
|
|
"Id": "GPU1",
|
|
"Name": "NVIDIA H100",
|
|
"Model": "NVIDIA H100 PCIe",
|
|
"Manufacturer": "NVIDIA",
|
|
"SerialNumber": "GPU-SN-001",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Systems/1/PCIeDevices/GPU1/PCIeFunctions",
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/PCIeDevices/GPU1/PCIeFunctions", map[string]interface{}{
|
|
"Members": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Systems/1/PCIeFunctions/GPU1F0"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Systems/1/PCIeFunctions/GPU1F0", map[string]interface{}{
|
|
"FunctionId": "0000:65:00.0",
|
|
"VendorId": "0x10DE",
|
|
"DeviceId": "0x2331",
|
|
"ClassCode": "0x030200",
|
|
"CurrentLinkWidth": 16,
|
|
"CurrentLinkSpeed": "16.0 GT/s",
|
|
"MaxLinkWidth": 16,
|
|
"MaxLinkSpeed": "16.0 GT/s",
|
|
})
|
|
register("/redfish/v1/Chassis/1/NetworkAdapters", map[string]interface{}{
|
|
"Members": []map[string]string{
|
|
{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/1"},
|
|
},
|
|
})
|
|
register("/redfish/v1/Chassis/1/Power", map[string]interface{}{
|
|
"PowerSupplies": []map[string]interface{}{
|
|
{
|
|
"MemberId": "PSU1",
|
|
"Name": "PSU Slot 1",
|
|
"Model": "PWS-2K01A-1R",
|
|
"Manufacturer": "Delta",
|
|
"PowerCapacityWatts": 2000,
|
|
"PowerInputWatts": 1600,
|
|
"LastPowerOutputWatts": 1200,
|
|
"LineInputVoltage": 230,
|
|
"Status": map[string]interface{}{
|
|
"Health": "OK",
|
|
"State": "Enabled",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
register("/redfish/v1/Chassis/1/NetworkAdapters/1", map[string]interface{}{
|
|
"Name": "Mellanox",
|
|
"Model": "ConnectX-6",
|
|
"SerialNumber": "NIC123",
|
|
})
|
|
register("/redfish/v1/Managers/1", map[string]interface{}{
|
|
"FirmwareVersion": "1.25",
|
|
})
|
|
register("/redfish/v1/Managers/1/NetworkProtocol", map[string]interface{}{
|
|
"Id": "NetworkProtocol",
|
|
})
|
|
|
|
ts := httptest.NewServer(mux)
|
|
defer ts.Close()
|
|
|
|
c := NewRedfishConnector()
|
|
result, err := c.Collect(context.Background(), Request{
|
|
Host: ts.URL,
|
|
Port: 443,
|
|
Protocol: "redfish",
|
|
Username: "admin",
|
|
AuthType: "password",
|
|
Password: "secret",
|
|
TLSMode: "strict",
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("collect failed: %v", err)
|
|
}
|
|
|
|
if result.Hardware == nil {
|
|
t.Fatalf("expected hardware config")
|
|
}
|
|
if result.Hardware.BoardInfo.ProductName != "SYS-TEST" {
|
|
t.Fatalf("unexpected board model: %q", result.Hardware.BoardInfo.ProductName)
|
|
}
|
|
if len(result.Hardware.CPUs) != 1 {
|
|
t.Fatalf("expected one CPU, got %d", len(result.Hardware.CPUs))
|
|
}
|
|
if len(result.Hardware.Memory) != 1 {
|
|
t.Fatalf("expected one DIMM, got %d", len(result.Hardware.Memory))
|
|
}
|
|
if len(result.Hardware.Storage) != 1 {
|
|
t.Fatalf("expected one drive, got %d", len(result.Hardware.Storage))
|
|
}
|
|
if len(result.Hardware.NetworkAdapters) != 1 {
|
|
t.Fatalf("expected one nic, got %d", len(result.Hardware.NetworkAdapters))
|
|
}
|
|
if len(result.Hardware.GPUs) != 1 {
|
|
t.Fatalf("expected one gpu, got %d", len(result.Hardware.GPUs))
|
|
}
|
|
if result.Hardware.GPUs[0].BDF != "0000:65:00.0" {
|
|
t.Fatalf("unexpected gpu BDF: %q", result.Hardware.GPUs[0].BDF)
|
|
}
|
|
if len(result.Hardware.PCIeDevices) != 1 {
|
|
t.Fatalf("expected one pcie device, got %d", len(result.Hardware.PCIeDevices))
|
|
}
|
|
if len(result.Hardware.PowerSupply) != 1 {
|
|
t.Fatalf("expected one psu, got %d", len(result.Hardware.PowerSupply))
|
|
}
|
|
if result.Hardware.PowerSupply[0].WattageW != 2000 {
|
|
t.Fatalf("unexpected psu wattage: %d", result.Hardware.PowerSupply[0].WattageW)
|
|
}
|
|
if len(result.Hardware.Firmware) == 0 {
|
|
t.Fatalf("expected firmware entries")
|
|
}
|
|
if result.RawPayloads == nil {
|
|
t.Fatalf("expected raw payloads")
|
|
}
|
|
treeAny, ok := result.RawPayloads["redfish_tree"]
|
|
if !ok {
|
|
t.Fatalf("expected redfish_tree in raw payloads")
|
|
}
|
|
tree, ok := treeAny.(map[string]interface{})
|
|
if !ok || len(tree) == 0 {
|
|
t.Fatalf("expected non-empty redfish_tree, got %#v", treeAny)
|
|
}
|
|
}
|
|
|
|
func TestParsePCIeDeviceSlot_FromNestedRedfishSlotLocation(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"Id": "NIC1",
|
|
"Slot": map[string]interface{}{
|
|
"Lanes": 16,
|
|
"Location": map[string]interface{}{
|
|
"PartLocation": map[string]interface{}{
|
|
"LocationOrdinalValue": 1,
|
|
"LocationType": "Slot",
|
|
"ServiceLabel": "PCIe Slot 1 (1)",
|
|
},
|
|
},
|
|
"PCIeType": "Gen5",
|
|
"SlotType": "FullLength",
|
|
},
|
|
}
|
|
|
|
got := parsePCIeDevice(doc, nil)
|
|
if got.Slot != "PCIe Slot 1 (1)" {
|
|
t.Fatalf("unexpected slot: %q", got.Slot)
|
|
}
|
|
}
|
|
|
|
func TestParsePCIeDeviceSlot_EmptyMapFallsBackToID(t *testing.T) {
|
|
doc := map[string]interface{}{
|
|
"Id": "NIC42",
|
|
"Slot": map[string]interface{}{},
|
|
}
|
|
|
|
got := parsePCIeDevice(doc, nil)
|
|
if got.Slot != "NIC42" {
|
|
t.Fatalf("unexpected slot fallback: %q", got.Slot)
|
|
}
|
|
if got.Slot == "map[]" {
|
|
t.Fatalf("slot should not stringify empty map")
|
|
}
|
|
}
|
|
|
|
func TestReplayRedfishFromRawPayloads_FallbackCollectionMembersByPrefix(t *testing.T) {
|
|
raw := map[string]any{
|
|
"redfish_tree": map[string]interface{}{
|
|
"/redfish/v1": map[string]interface{}{
|
|
"Systems": map[string]interface{}{"@odata.id": "/redfish/v1/Systems"},
|
|
"Chassis": map[string]interface{}{"@odata.id": "/redfish/v1/Chassis"},
|
|
"Managers": map[string]interface{}{"@odata.id": "/redfish/v1/Managers"},
|
|
},
|
|
"/redfish/v1/Systems": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1": map[string]interface{}{
|
|
"Manufacturer": "Supermicro",
|
|
"Model": "SYS-TEST",
|
|
"SerialNumber": "SYS123",
|
|
},
|
|
// Intentionally missing /redfish/v1/Systems/1/Processors collection.
|
|
"/redfish/v1/Systems/1/Processors/CPU1": map[string]interface{}{
|
|
"Id": "CPU1",
|
|
"Model": "Xeon Gold",
|
|
"TotalCores": 32,
|
|
"TotalThreads": 64,
|
|
},
|
|
"/redfish/v1/Chassis": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/1": map[string]interface{}{
|
|
"Id": "1",
|
|
},
|
|
"/redfish/v1/Managers": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Managers/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Managers/1": map[string]interface{}{
|
|
"Id": "1",
|
|
},
|
|
},
|
|
"redfish_fetch_errors": []map[string]interface{}{
|
|
{"path": "/redfish/v1/Systems/1/Processors", "error": "status 500"},
|
|
},
|
|
}
|
|
|
|
got, err := ReplayRedfishFromRawPayloads(raw, nil)
|
|
if err != nil {
|
|
t.Fatalf("replay failed: %v", err)
|
|
}
|
|
if got.Hardware == nil {
|
|
t.Fatalf("expected hardware")
|
|
}
|
|
if len(got.Hardware.CPUs) != 1 {
|
|
t.Fatalf("expected one CPU via prefix fallback, got %d", len(got.Hardware.CPUs))
|
|
}
|
|
if _, ok := got.RawPayloads["redfish_fetch_errors"]; !ok {
|
|
t.Fatalf("expected raw payloads to preserve redfish_fetch_errors")
|
|
}
|
|
}
|
|
|
|
func TestEnrichNICFromPCIeFunctions(t *testing.T) {
|
|
nic := parseNIC(map[string]interface{}{
|
|
"Id": "1",
|
|
"Model": "MCX75310AAS-NEAT",
|
|
"Manufacturer": "Supermicro",
|
|
"SerialNumber": "NIC-SN-1",
|
|
"Controllers": []interface{}{
|
|
map[string]interface{}{
|
|
"Links": map[string]interface{}{
|
|
"PCIeDevices": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/NIC1"},
|
|
},
|
|
},
|
|
"Location": map[string]interface{}{
|
|
"PartLocation": map[string]interface{}{"ServiceLabel": "PCIe Slot 1 (1)"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
pcieDoc := map[string]interface{}{
|
|
"Id": "NIC1",
|
|
"PCIeFunctions": map[string]interface{}{
|
|
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/NIC1/PCIeFunctions",
|
|
},
|
|
}
|
|
functionDocs := []map[string]interface{}{
|
|
{
|
|
"VendorId": "0x15b3",
|
|
"DeviceId": "0x1021",
|
|
},
|
|
}
|
|
|
|
enrichNICFromPCIe(&nic, pcieDoc, functionDocs)
|
|
if nic.VendorID != 0x15b3 || nic.DeviceID != 0x1021 {
|
|
t.Fatalf("unexpected NIC IDs: vendor=%#x device=%#x", nic.VendorID, nic.DeviceID)
|
|
}
|
|
if nic.Location != "PCIe Slot 1 (1)" {
|
|
t.Fatalf("unexpected NIC location: %q", nic.Location)
|
|
}
|
|
}
|
|
|
|
func 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 TestReplayCollectStorage_ProbesSupermicroNVMeDiskBayWhenCollectionEmpty(t *testing.T) {
|
|
r := redfishSnapshotReader{tree: map[string]interface{}{
|
|
"/redfish/v1/Systems": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/NVMeSSD"},
|
|
},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/NVMeSSD": map[string]interface{}{
|
|
"Drives": map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/NVMeSSD/Drives"},
|
|
},
|
|
"/redfish/v1/Systems/1/Storage/NVMeSSD/Drives": map[string]interface{}{
|
|
"Members": []interface{}{},
|
|
},
|
|
"/redfish/v1/Chassis": map[string]interface{}{
|
|
"Members": []interface{}{
|
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane"},
|
|
},
|
|
},
|
|
"/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane": map[string]interface{}{
|
|
"Drives": map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives"},
|
|
},
|
|
"/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives": map[string]interface{}{
|
|
"Members@odata.count": 0,
|
|
"Members": []interface{}{},
|
|
},
|
|
"/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives/Disk.Bay.0": map[string]interface{}{
|
|
"Id": "Disk.Bay.0",
|
|
"Name": "Disk.Bay.0",
|
|
"Manufacturer": "INTEL",
|
|
"SerialNumber": "BTLJ035203XT1P0FGN",
|
|
"Model": "INTEL SSDPE2KX010T8",
|
|
"CapacityBytes": int64(1000204886016),
|
|
"Protocol": "NVMe",
|
|
"MediaType": "SSD",
|
|
"Status": map[string]interface{}{"State": "Enabled", "Health": "OK"},
|
|
},
|
|
}}
|
|
|
|
got := r.collectStorage("/redfish/v1/Systems/1")
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected one drive from direct Disk.Bay probe, got %d", len(got))
|
|
}
|
|
if got[0].SerialNumber != "BTLJ035203XT1P0FGN" {
|
|
t.Fatalf("unexpected serial: %q", got[0].SerialNumber)
|
|
}
|
|
if got[0].SizeGB == 0 {
|
|
t.Fatalf("expected size to be parsed from CapacityBytes")
|
|
}
|
|
}
|