fix(redfish): trim MSI replay noise and unify NIC classes
This commit is contained in:
@@ -1045,3 +1045,52 @@ logical volumes.
|
||||
- HPE PCIe inventory gets better slot labels like `OCP 3.0 Slot 15` plus concrete classes such as
|
||||
`LOM/NIC` or `SAS/SATA Storage Controller`.
|
||||
- `part_number` remains available separately for model identity, without polluting the class field.
|
||||
|
||||
---
|
||||
|
||||
## ADL-041 — Redfish replay drops topology-only PCIe noise classes from canonical inventory
|
||||
|
||||
**Date:** 2026-04-01
|
||||
**Context:** Some Redfish BMCs, especially MSI/AMI GPU systems, expose a very wide PCIe topology
|
||||
tree under `Chassis/*/PCIeDevices/*`. Besides real endpoint devices, the replay sees bridge stages,
|
||||
CPU-side helper functions, IMC/mesh signal-processing nodes, USB/SPI side controllers, and GPU
|
||||
display-function duplicates reported as generic `Display Device`. Keeping all of them in
|
||||
`hardware.pcie_devices` pollutes downstream exports such as Reanimator and hides the actual
|
||||
endpoint inventory signal.
|
||||
|
||||
**Decision:**
|
||||
- Filter topology-only PCIe records during Redfish replay, not in the UI layer.
|
||||
- Drop PCIe entries with replay-resolved classes:
|
||||
- `Bridge`
|
||||
- `Processor`
|
||||
- `SignalProcessingController`
|
||||
- `SerialBusController`
|
||||
- Drop `DisplayController` entries when the source Redfish PCIe document is the generic MSI-style
|
||||
`Description: "Display Device"` duplicate.
|
||||
- Drop PCIe network endpoints when their PCIe functions already link to `NetworkDeviceFunctions`,
|
||||
because those devices are represented canonically in `hardware.network_adapters`.
|
||||
- When `Systems/*/NetworkInterfaces/*` links back to a chassis `NetworkAdapter`, match against the
|
||||
fully enriched chassis NIC identity to avoid creating a second ghost NIC row with the raw
|
||||
`NetworkAdapter_*` slot/name.
|
||||
- Treat generic Redfish object names such as `NetworkAdapter_*` and `PCIeDevice_*` as placeholder
|
||||
models and replace them from PCI IDs when a concrete vendor/device match exists.
|
||||
- Drop MSI-style storage service PCIe endpoints whose resolved device names are only
|
||||
`Volume Management Device NVMe RAID Controller` or `PCIe Switch management endpoint`; storage
|
||||
inventory already comes from the Redfish storage tree.
|
||||
- Normalize Ethernet-class NICs into the single exported class `NetworkController`; do not split
|
||||
`EthernetController` into a separate top-level inventory section.
|
||||
- Keep endpoint classes such as `NetworkController`, `MassStorageController`, and dedicated GPU
|
||||
inventory coming from `hardware.gpus`.
|
||||
|
||||
**Consequences:**
|
||||
- `hardware.pcie_devices` becomes closer to real endpoint inventory instead of raw PCIe topology.
|
||||
- Reanimator exports stop showing MSI bridge/processor/display duplicate noise.
|
||||
- Reanimator exports no longer duplicate the same MSI NIC as both `PCIeDevice_*` and
|
||||
`NetworkAdapter_*`.
|
||||
- Replay no longer creates extra NIC rows from `Systems/NetworkInterfaces` when the same adapter
|
||||
was already normalized from `Chassis/NetworkAdapters`.
|
||||
- MSI VMD / PCIe switch storage service endpoints no longer pollute PCIe inventory.
|
||||
- UI/Reanimator group all Ethernet NICs under the same `NETWORKCONTROLLER` section.
|
||||
- Canonical NIC inventory prefers resolved PCI product names over generic Redfish placeholder names.
|
||||
- The raw Redfish snapshot still remains available in `raw_payloads.redfish_tree` for low-level
|
||||
troubleshooting if topology details are ever needed.
|
||||
|
||||
@@ -4793,6 +4793,9 @@ func isMissingOrRawPCIModel(model string) bool {
|
||||
if l == "unknown" || l == "n/a" || l == "na" || l == "none" {
|
||||
return true
|
||||
}
|
||||
if isGenericRedfishInventoryName(l) {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(l, "0x") && len(l) <= 6 {
|
||||
return true
|
||||
}
|
||||
@@ -4811,6 +4814,26 @@ func isMissingOrRawPCIModel(model string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func isGenericRedfishInventoryName(value string) bool {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
switch {
|
||||
case value == "":
|
||||
return false
|
||||
case value == "networkadapter", strings.HasPrefix(value, "networkadapter_"), strings.HasPrefix(value, "networkadapter "):
|
||||
return true
|
||||
case value == "pciedevice", strings.HasPrefix(value, "pciedevice_"), strings.HasPrefix(value, "pciedevice "):
|
||||
return true
|
||||
case value == "pciefunction", strings.HasPrefix(value, "pciefunction_"), strings.HasPrefix(value, "pciefunction "):
|
||||
return true
|
||||
case value == "ethernetinterface", strings.HasPrefix(value, "ethernetinterface_"), strings.HasPrefix(value, "ethernetinterface "):
|
||||
return true
|
||||
case value == "networkport", strings.HasPrefix(value, "networkport_"), strings.HasPrefix(value, "networkport "):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// isUnidentifiablePCIeDevice returns true for PCIe topology entries that carry no
|
||||
// useful inventory information: generic class (SingleFunction/MultiFunction), no
|
||||
// resolved model or serial, and no PCI vendor/device IDs for future resolution.
|
||||
@@ -5650,6 +5673,9 @@ func normalizeNetworkAdapterModel(nic models.NetworkAdapter) string {
|
||||
if model == "" {
|
||||
return ""
|
||||
}
|
||||
if isMissingOrRawPCIModel(model) {
|
||||
return ""
|
||||
}
|
||||
slot := strings.TrimSpace(nic.Slot)
|
||||
if slot != "" && strings.EqualFold(slot, model) {
|
||||
return ""
|
||||
|
||||
@@ -31,7 +31,7 @@ func (r redfishSnapshotReader) enrichNICsFromNetworkInterfaces(nics *[]models.Ne
|
||||
// the real NIC that came from Chassis/NetworkAdapters (e.g. "RISER 5
|
||||
// slot 1 (7)"). Try to find the real NIC via the Links.NetworkAdapter
|
||||
// cross-reference before creating a ghost entry.
|
||||
if linkedIdx := r.findNICIndexByLinkedNetworkAdapter(iface, bySlot); linkedIdx >= 0 {
|
||||
if linkedIdx := r.findNICIndexByLinkedNetworkAdapter(iface, *nics, bySlot); linkedIdx >= 0 {
|
||||
idx = linkedIdx
|
||||
ok = true
|
||||
}
|
||||
@@ -75,33 +75,37 @@ func (r redfishSnapshotReader) collectNICs(chassisPaths []string) []models.Netwo
|
||||
continue
|
||||
}
|
||||
for _, doc := range adapterDocs {
|
||||
nic := parseNIC(doc)
|
||||
adapterFunctionDocs := r.getNetworkAdapterFunctionDocs(doc)
|
||||
for _, pciePath := range networkAdapterPCIeDevicePaths(doc) {
|
||||
pcieDoc, err := r.getJSON(pciePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
functionDocs := r.getLinkedPCIeFunctions(pcieDoc)
|
||||
for _, adapterFnDoc := range adapterFunctionDocs {
|
||||
functionDocs = append(functionDocs, r.getLinkedPCIeFunctions(adapterFnDoc)...)
|
||||
}
|
||||
functionDocs = dedupeJSONDocsByPath(functionDocs)
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(pcieDoc, "EnvironmentMetrics", "Metrics")
|
||||
for _, fn := range functionDocs {
|
||||
supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
|
||||
}
|
||||
enrichNICFromPCIe(&nic, pcieDoc, functionDocs, supplementalDocs)
|
||||
}
|
||||
if len(nic.MACAddresses) == 0 {
|
||||
r.enrichNICMACsFromNetworkDeviceFunctions(&nic, doc)
|
||||
}
|
||||
nics = append(nics, nic)
|
||||
nics = append(nics, r.buildNICFromAdapterDoc(doc))
|
||||
}
|
||||
}
|
||||
return dedupeNetworkAdapters(nics)
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) buildNICFromAdapterDoc(adapterDoc map[string]interface{}) models.NetworkAdapter {
|
||||
nic := parseNIC(adapterDoc)
|
||||
adapterFunctionDocs := r.getNetworkAdapterFunctionDocs(adapterDoc)
|
||||
for _, pciePath := range networkAdapterPCIeDevicePaths(adapterDoc) {
|
||||
pcieDoc, err := r.getJSON(pciePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
functionDocs := r.getLinkedPCIeFunctions(pcieDoc)
|
||||
for _, adapterFnDoc := range adapterFunctionDocs {
|
||||
functionDocs = append(functionDocs, r.getLinkedPCIeFunctions(adapterFnDoc)...)
|
||||
}
|
||||
functionDocs = dedupeJSONDocsByPath(functionDocs)
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(pcieDoc, "EnvironmentMetrics", "Metrics")
|
||||
for _, fn := range functionDocs {
|
||||
supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
|
||||
}
|
||||
enrichNICFromPCIe(&nic, pcieDoc, functionDocs, supplementalDocs)
|
||||
}
|
||||
if len(nic.MACAddresses) == 0 {
|
||||
r.enrichNICMACsFromNetworkDeviceFunctions(&nic, adapterDoc)
|
||||
}
|
||||
return nic
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) getNetworkAdapterFunctionDocs(adapterDoc map[string]interface{}) []map[string]interface{} {
|
||||
ndfCol, ok := adapterDoc["NetworkDeviceFunctions"].(map[string]interface{})
|
||||
if !ok {
|
||||
@@ -137,13 +141,16 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st
|
||||
if looksLikeGPU(doc, functionDocs) {
|
||||
continue
|
||||
}
|
||||
if replayPCIeDeviceBackedByCanonicalNIC(doc, functionDocs) {
|
||||
continue
|
||||
}
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
|
||||
supplementalDocs = append(supplementalDocs, r.getChassisScopedPCIeSupplementalDocs(doc)...)
|
||||
for _, fn := range functionDocs {
|
||||
supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
|
||||
}
|
||||
dev := parsePCIeDeviceWithSupplementalDocs(doc, functionDocs, supplementalDocs)
|
||||
if isUnidentifiablePCIeDevice(dev) {
|
||||
if shouldSkipReplayPCIeDevice(doc, dev) {
|
||||
continue
|
||||
}
|
||||
out = append(out, dev)
|
||||
@@ -157,12 +164,134 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st
|
||||
for idx, fn := range functionDocs {
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")
|
||||
dev := parsePCIeFunctionWithSupplementalDocs(fn, supplementalDocs, idx+1)
|
||||
if shouldSkipReplayPCIeDevice(fn, dev) {
|
||||
continue
|
||||
}
|
||||
out = append(out, dev)
|
||||
}
|
||||
}
|
||||
return dedupePCIeDevices(out)
|
||||
}
|
||||
|
||||
func shouldSkipReplayPCIeDevice(doc map[string]interface{}, dev models.PCIeDevice) bool {
|
||||
if isUnidentifiablePCIeDevice(dev) {
|
||||
return true
|
||||
}
|
||||
if replayNetworkFunctionBackedByCanonicalNIC(doc, dev) {
|
||||
return true
|
||||
}
|
||||
if isReplayStorageServiceEndpoint(doc, dev) {
|
||||
return true
|
||||
}
|
||||
if isReplayNoisePCIeClass(dev.DeviceClass) {
|
||||
return true
|
||||
}
|
||||
if isReplayDisplayDeviceDuplicate(doc, dev) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func replayPCIeDeviceBackedByCanonicalNIC(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
|
||||
if !looksLikeReplayNetworkPCIeDevice(doc, functionDocs) {
|
||||
return false
|
||||
}
|
||||
for _, fn := range functionDocs {
|
||||
if hasRedfishLinkedMember(fn, "NetworkDeviceFunctions") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func replayNetworkFunctionBackedByCanonicalNIC(doc map[string]interface{}, dev models.PCIeDevice) bool {
|
||||
if !looksLikeReplayNetworkClass(dev.DeviceClass) {
|
||||
return false
|
||||
}
|
||||
return hasRedfishLinkedMember(doc, "NetworkDeviceFunctions")
|
||||
}
|
||||
|
||||
func looksLikeReplayNetworkPCIeDevice(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
|
||||
for _, fn := range functionDocs {
|
||||
if looksLikeReplayNetworkClass(asString(fn["DeviceClass"])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
joined := strings.ToLower(strings.TrimSpace(strings.Join([]string{
|
||||
asString(doc["DeviceType"]),
|
||||
asString(doc["Description"]),
|
||||
asString(doc["Name"]),
|
||||
asString(doc["Model"]),
|
||||
}, " ")))
|
||||
return strings.Contains(joined, "network")
|
||||
}
|
||||
|
||||
func looksLikeReplayNetworkClass(class string) bool {
|
||||
class = strings.ToLower(strings.TrimSpace(class))
|
||||
return strings.Contains(class, "network") || strings.Contains(class, "ethernet")
|
||||
}
|
||||
|
||||
func isReplayStorageServiceEndpoint(doc map[string]interface{}, dev models.PCIeDevice) bool {
|
||||
class := strings.ToLower(strings.TrimSpace(dev.DeviceClass))
|
||||
if class != "massstoragecontroller" && class != "mass storage controller" {
|
||||
return false
|
||||
}
|
||||
name := strings.ToLower(strings.TrimSpace(firstNonEmpty(
|
||||
dev.PartNumber,
|
||||
asString(doc["PartNumber"]),
|
||||
asString(doc["Description"]),
|
||||
)))
|
||||
if strings.Contains(name, "pcie switch management endpoint") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(name, "volume management device nvme raid controller") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasRedfishLinkedMember(doc map[string]interface{}, key string) bool {
|
||||
links, ok := doc["Links"].(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if asInt(links[key+"@odata.count"]) > 0 {
|
||||
return true
|
||||
}
|
||||
linked, ok := links[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
switch v := linked.(type) {
|
||||
case []interface{}:
|
||||
return len(v) > 0
|
||||
case map[string]interface{}:
|
||||
if asString(v["@odata.id"]) != "" {
|
||||
return true
|
||||
}
|
||||
return len(v) > 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isReplayNoisePCIeClass(class string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(class)) {
|
||||
case "bridge", "processor", "signalprocessingcontroller", "signal processing controller", "serialbuscontroller", "serial bus controller":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isReplayDisplayDeviceDuplicate(doc map[string]interface{}, dev models.PCIeDevice) bool {
|
||||
class := strings.ToLower(strings.TrimSpace(dev.DeviceClass))
|
||||
if class != "displaycontroller" && class != "display controller" {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(asString(doc["Description"])), "Display Device")
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) getChassisScopedPCIeSupplementalDocs(doc map[string]interface{}) []map[string]interface{} {
|
||||
docPath := normalizeRedfishPath(asString(doc["@odata.id"]))
|
||||
chassisPath := chassisPathForPCIeDoc(docPath)
|
||||
@@ -362,8 +491,9 @@ func redfishManagerInterfaceScore(summary map[string]any) int {
|
||||
|
||||
// findNICIndexByLinkedNetworkAdapter resolves a NetworkInterface document to an
|
||||
// existing NIC in bySlot by following Links.NetworkAdapter → the Chassis
|
||||
// NetworkAdapter doc → its slot label. Returns -1 if no match is found.
|
||||
func (r redfishSnapshotReader) findNICIndexByLinkedNetworkAdapter(iface map[string]interface{}, bySlot map[string]int) int {
|
||||
// NetworkAdapter doc and reconstructing the canonical NIC identity. Returns -1
|
||||
// if no match is found.
|
||||
func (r redfishSnapshotReader) findNICIndexByLinkedNetworkAdapter(iface map[string]interface{}, existing []models.NetworkAdapter, bySlot map[string]int) int {
|
||||
links, ok := iface["Links"].(map[string]interface{})
|
||||
if !ok {
|
||||
return -1
|
||||
@@ -380,15 +510,58 @@ func (r redfishSnapshotReader) findNICIndexByLinkedNetworkAdapter(iface map[stri
|
||||
if err != nil || len(adapterDoc) == 0 {
|
||||
return -1
|
||||
}
|
||||
adapterNIC := parseNIC(adapterDoc)
|
||||
adapterNIC := r.buildNICFromAdapterDoc(adapterDoc)
|
||||
if serial := normalizeRedfishIdentityField(adapterNIC.SerialNumber); serial != "" {
|
||||
for idx, nic := range existing {
|
||||
if strings.EqualFold(normalizeRedfishIdentityField(nic.SerialNumber), serial) {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
}
|
||||
if bdf := strings.TrimSpace(adapterNIC.BDF); bdf != "" {
|
||||
for idx, nic := range existing {
|
||||
if strings.EqualFold(strings.TrimSpace(nic.BDF), bdf) {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
}
|
||||
if slot := strings.ToLower(strings.TrimSpace(adapterNIC.Slot)); slot != "" {
|
||||
if idx, ok := bySlot[slot]; ok {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
for idx, nic := range existing {
|
||||
if networkAdaptersShareMACs(nic, adapterNIC) {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func networkAdaptersShareMACs(a, b models.NetworkAdapter) bool {
|
||||
if len(a.MACAddresses) == 0 || len(b.MACAddresses) == 0 {
|
||||
return false
|
||||
}
|
||||
seen := make(map[string]struct{}, len(a.MACAddresses))
|
||||
for _, mac := range a.MACAddresses {
|
||||
normalized := strings.ToUpper(strings.TrimSpace(mac))
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
seen[normalized] = struct{}{}
|
||||
}
|
||||
for _, mac := range b.MACAddresses {
|
||||
normalized := strings.ToUpper(strings.TrimSpace(mac))
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[normalized]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// enrichNICMACsFromNetworkDeviceFunctions reads the NetworkDeviceFunctions
|
||||
// collection linked from a NetworkAdapter document and populates the NIC's
|
||||
// MACAddresses from each function's Ethernet.PermanentMACAddress / MACAddress.
|
||||
|
||||
@@ -1366,6 +1366,148 @@ func TestReplayCollectNICs_UsesNetworkDeviceFunctionPCIeFunctionLink(t *testing.
|
||||
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) {
|
||||
@@ -2469,6 +2611,279 @@ func TestReplayCollectGPUs_DoesNotCollapseOnPlaceholderSerialAndSkipsNIC(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayCollectPCIeDevices_SkipsMSITopologyNoiseClasses(t *testing.T) {
|
||||
r := redfishSnapshotReader{tree: map[string]interface{}{
|
||||
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/bridge"},
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/processor"},
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/signal"},
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/serial"},
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/display"},
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/network"},
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/storage"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/bridge": map[string]interface{}{
|
||||
"Id": "bridge",
|
||||
"Name": "Bridge",
|
||||
"Description": "Bridge Device",
|
||||
"Manufacturer": "Intel Corporation",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/bridge/PCIeFunctions",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/bridge/PCIeFunctions": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/bridge/PCIeFunctions/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/bridge/PCIeFunctions/1": map[string]interface{}{
|
||||
"DeviceClass": "Bridge",
|
||||
"VendorId": "0x8086",
|
||||
"DeviceId": "0x0db0",
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/processor": map[string]interface{}{
|
||||
"Id": "processor",
|
||||
"Name": "Processor",
|
||||
"Description": "Processor Device",
|
||||
"Manufacturer": "Intel Corporation",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/processor/PCIeFunctions",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/processor/PCIeFunctions": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/processor/PCIeFunctions/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/processor/PCIeFunctions/1": map[string]interface{}{
|
||||
"DeviceClass": "Processor",
|
||||
"VendorId": "0x8086",
|
||||
"DeviceId": "0x4944",
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/signal": map[string]interface{}{
|
||||
"Id": "signal",
|
||||
"Name": "Signal",
|
||||
"Manufacturer": "Intel Corporation",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/signal/PCIeFunctions",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/signal/PCIeFunctions": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/signal/PCIeFunctions/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/signal/PCIeFunctions/1": map[string]interface{}{
|
||||
"DeviceClass": "SignalProcessingController",
|
||||
"VendorId": "0x8086",
|
||||
"DeviceId": "0x3254",
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/serial": map[string]interface{}{
|
||||
"Id": "serial",
|
||||
"Name": "Serial",
|
||||
"Manufacturer": "Renesas",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/serial/PCIeFunctions",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/serial/PCIeFunctions": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/serial/PCIeFunctions/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/serial/PCIeFunctions/1": map[string]interface{}{
|
||||
"DeviceClass": "SerialBusController",
|
||||
"VendorId": "0x1912",
|
||||
"DeviceId": "0x0014",
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/display": map[string]interface{}{
|
||||
"Id": "display",
|
||||
"Name": "Display",
|
||||
"Description": "Display Device",
|
||||
"Manufacturer": "NVIDIA Corporation",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/display/PCIeFunctions",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/display/PCIeFunctions": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/display/PCIeFunctions/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/display/PCIeFunctions/1": map[string]interface{}{
|
||||
"DeviceClass": "DisplayController",
|
||||
"VendorId": "0x10de",
|
||||
"DeviceId": "0x233b",
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/network": map[string]interface{}{
|
||||
"Id": "network",
|
||||
"Name": "NIC",
|
||||
"Description": "Network Device",
|
||||
"Manufacturer": "Mellanox Technologies",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/network/PCIeFunctions",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/network/PCIeFunctions": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/network/PCIeFunctions/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/network/PCIeFunctions/1": map[string]interface{}{
|
||||
"DeviceClass": "NetworkController",
|
||||
"VendorId": "0x15b3",
|
||||
"DeviceId": "0x101f",
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/storage": map[string]interface{}{
|
||||
"Id": "storage",
|
||||
"Name": "Storage",
|
||||
"Description": "Storage Device",
|
||||
"Manufacturer": "Intel Corporation",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/storage/PCIeFunctions",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/storage/PCIeFunctions": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/storage/PCIeFunctions/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/storage/PCIeFunctions/1": map[string]interface{}{
|
||||
"DeviceClass": "MassStorageController",
|
||||
"VendorId": "0x1234",
|
||||
"DeviceId": "0x5678",
|
||||
},
|
||||
}}
|
||||
|
||||
got := r.collectPCIeDevices(nil, []string{"/redfish/v1/Chassis/1"})
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected only endpoint PCIe devices to remain, got %d: %+v", len(got), got)
|
||||
}
|
||||
classes := map[string]bool{}
|
||||
for _, dev := range got {
|
||||
classes[dev.DeviceClass] = true
|
||||
}
|
||||
if !classes["NetworkController"] || !classes["MassStorageController"] {
|
||||
t.Fatalf("expected network and storage PCIe devices to remain, got %+v", got)
|
||||
}
|
||||
if classes["Bridge"] || classes["Processor"] || classes["SignalProcessingController"] || classes["SerialBusController"] || classes["DisplayController"] {
|
||||
t.Fatalf("expected MSI topology noise classes to be filtered, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayCollectPCIeDevices_SkipsNICsAlreadyRepresentedAsNetworkAdapters(t *testing.T) {
|
||||
r := redfishSnapshotReader{tree: map[string]interface{}{
|
||||
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/nic"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/nic": map[string]interface{}{
|
||||
"Id": "nic",
|
||||
"Name": "PCIeDevice_00_39_00",
|
||||
"Description": "Network Device",
|
||||
"Manufacturer": "Mellanox Technologies",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/nic/PCIeFunctions",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/nic/PCIeFunctions": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/nic/PCIeFunctions/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/nic/PCIeFunctions/1": map[string]interface{}{
|
||||
"DeviceClass": "NetworkController",
|
||||
"VendorId": "0x15b3",
|
||||
"DeviceId": "0x101f",
|
||||
"Links": map[string]interface{}{
|
||||
"NetworkDeviceFunctions": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions/Function0"},
|
||||
},
|
||||
"NetworkDeviceFunctions@odata.count": 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
got := r.collectPCIeDevices(nil, []string{"/redfish/v1/Chassis/1"})
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("expected network-backed PCIe duplicate to be skipped, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayCollectPCIeDevices_SkipsStorageServiceEndpoints(t *testing.T) {
|
||||
r := redfishSnapshotReader{tree: map[string]interface{}{
|
||||
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/vmd"},
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/switch-mgmt"},
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/hba"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/vmd": map[string]interface{}{
|
||||
"Id": "vmd",
|
||||
"Description": "Storage Device",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/vmd/PCIeFunctions",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/vmd/PCIeFunctions": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/vmd/PCIeFunctions/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/vmd/PCIeFunctions/1": map[string]interface{}{
|
||||
"DeviceClass": "MassStorageController",
|
||||
"VendorId": "0x8086",
|
||||
"DeviceId": "0x28c0",
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/switch-mgmt": map[string]interface{}{
|
||||
"Id": "switch-mgmt",
|
||||
"Description": "Storage Device",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/switch-mgmt/PCIeFunctions",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/switch-mgmt/PCIeFunctions": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/switch-mgmt/PCIeFunctions/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/switch-mgmt/PCIeFunctions/1": map[string]interface{}{
|
||||
"DeviceClass": "MassStorageController",
|
||||
"VendorId": "0x1000",
|
||||
"DeviceId": "0x00b2",
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/hba": map[string]interface{}{
|
||||
"Id": "hba",
|
||||
"Description": "Storage Device",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/hba/PCIeFunctions",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/hba/PCIeFunctions": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/hba/PCIeFunctions/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/hba/PCIeFunctions/1": map[string]interface{}{
|
||||
"DeviceClass": "MassStorageController",
|
||||
"VendorId": "0x1234",
|
||||
"DeviceId": "0x5678",
|
||||
},
|
||||
}}
|
||||
|
||||
got := r.collectPCIeDevices(nil, []string{"/redfish/v1/Chassis/1"})
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected only non-service storage controller to remain, got %+v", got)
|
||||
}
|
||||
if got[0].VendorID != 0x1234 || got[0].DeviceID != 0x5678 {
|
||||
t.Fatalf("expected generic HBA to remain, got %+v", got[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBoardInfo_NormalizesNullPlaceholders(t *testing.T) {
|
||||
got := parseBoardInfo(map[string]interface{}{
|
||||
"Manufacturer": "NULL",
|
||||
|
||||
@@ -2246,10 +2246,8 @@ func normalizePCIeDeviceClass(d models.HardwareDevice) string {
|
||||
|
||||
func normalizeLegacyPCIeDeviceClass(deviceClass string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(deviceClass)) {
|
||||
case "", "network", "network controller", "networkcontroller":
|
||||
case "", "network", "network controller", "networkcontroller", "ethernet", "ethernet controller", "ethernetcontroller":
|
||||
return "NetworkController"
|
||||
case "ethernet", "ethernet controller", "ethernetcontroller":
|
||||
return "EthernetController"
|
||||
case "fibre channel", "fibre channel controller", "fibrechannelcontroller", "fc":
|
||||
return "FibreChannelController"
|
||||
case "display", "displaycontroller", "display controller", "vga":
|
||||
@@ -2270,8 +2268,6 @@ func normalizeLegacyPCIeDeviceClass(deviceClass string) string {
|
||||
func normalizeNetworkDeviceClass(portType, model, description string) string {
|
||||
joined := strings.ToLower(strings.TrimSpace(strings.Join([]string{portType, model, description}, " ")))
|
||||
switch {
|
||||
case strings.Contains(joined, "ethernet"):
|
||||
return "EthernetController"
|
||||
case strings.Contains(joined, "fibre channel") || strings.Contains(joined, " fibrechannel") || strings.Contains(joined, "fc "):
|
||||
return "FibreChannelController"
|
||||
default:
|
||||
|
||||
@@ -1733,6 +1733,43 @@ func TestConvertToReanimator_ExportsContractV24Telemetry(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_UnifiesEthernetAndNetworkControllers(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
|
||||
Devices: []models.HardwareDevice{
|
||||
{
|
||||
Kind: models.DeviceKindPCIe,
|
||||
Slot: "PCIe1",
|
||||
DeviceClass: "EthernetController",
|
||||
Present: boolPtr(true),
|
||||
SerialNumber: "ETH-001",
|
||||
},
|
||||
{
|
||||
Kind: models.DeviceKindNetwork,
|
||||
Slot: "NIC1",
|
||||
Model: "Ethernet Adapter",
|
||||
Present: boolPtr(true),
|
||||
SerialNumber: "NIC-001",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.PCIeDevices) != 2 {
|
||||
t.Fatalf("expected two pcie-class exports, got %d", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
for _, dev := range out.Hardware.PCIeDevices {
|
||||
if dev.DeviceClass != "NetworkController" {
|
||||
t.Fatalf("expected unified NetworkController class, got %+v", dev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_PreservesLegacyStorageAndPSUDetails(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "legacy-details.json",
|
||||
|
||||
@@ -3,6 +3,8 @@ package server
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -29,7 +31,17 @@ func TestCollectProbe(t *testing.T) {
|
||||
_, ts := newCollectTestServer()
|
||||
defer ts.Close()
|
||||
|
||||
body := `{"host":"bmc-off.local","protocol":"redfish","port":443,"username":"admin","auth_type":"password","password":"secret","tls_mode":"strict"}`
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen probe target: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
addr, ok := ln.Addr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected listener address type: %T", ln.Addr())
|
||||
}
|
||||
|
||||
body := fmt.Sprintf(`{"host":"127.0.0.1","protocol":"redfish","port":%d,"username":"admin-off","auth_type":"password","password":"secret","tls_mode":"strict"}`, addr.Port)
|
||||
resp, err := http.Post(ts.URL+"/api/collect/probe", "application/json", bytes.NewBufferString(body))
|
||||
if err != nil {
|
||||
t.Fatalf("post collect probe failed: %v", err)
|
||||
|
||||
@@ -21,11 +21,15 @@ func (c *mockConnector) Probe(ctx context.Context, req collector.Request) (*coll
|
||||
if strings.Contains(strings.ToLower(req.Host), "fail") {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
hostPoweredOn := true
|
||||
if strings.Contains(strings.ToLower(req.Host), "off") || strings.Contains(strings.ToLower(req.Username), "off") {
|
||||
hostPoweredOn = false
|
||||
}
|
||||
return &collector.ProbeResult{
|
||||
Reachable: true,
|
||||
Protocol: c.protocol,
|
||||
HostPowerState: map[bool]string{true: "On", false: "Off"}[!strings.Contains(strings.ToLower(req.Host), "off")],
|
||||
HostPoweredOn: !strings.Contains(strings.ToLower(req.Host), "off"),
|
||||
HostPowerState: map[bool]string{true: "On", false: "Off"}[hostPoweredOn],
|
||||
HostPoweredOn: hostPoweredOn,
|
||||
PowerControlAvailable: true,
|
||||
SystemPath: "/redfish/v1/Systems/1",
|
||||
}, nil
|
||||
|
||||
@@ -243,6 +243,7 @@ func BuildHardwareDevices(hw *models.HardwareConfig) []models.HardwareDevice {
|
||||
Source: "network_adapters",
|
||||
Slot: nic.Slot,
|
||||
Location: nic.Location,
|
||||
DeviceClass: "NetworkController",
|
||||
VendorID: nic.VendorID,
|
||||
DeviceID: nic.DeviceID,
|
||||
Model: nic.Model,
|
||||
|
||||
@@ -223,6 +223,31 @@ func TestBuildHardwareDevices_SkipsFirmwareOnlyNumericSlots(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHardwareDevices_NetworkDevicesUseUnifiedControllerClass(t *testing.T) {
|
||||
hw := &models.HardwareConfig{
|
||||
NetworkAdapters: []models.NetworkAdapter{
|
||||
{
|
||||
Slot: "NIC1",
|
||||
Model: "Ethernet Adapter",
|
||||
Vendor: "Intel",
|
||||
Present: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
devices := BuildHardwareDevices(hw)
|
||||
for _, d := range devices {
|
||||
if d.Kind != models.DeviceKindNetwork {
|
||||
continue
|
||||
}
|
||||
if d.DeviceClass != "NetworkController" {
|
||||
t.Fatalf("expected unified network controller class, got %+v", d)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatalf("expected one canonical network device")
|
||||
}
|
||||
|
||||
func TestHandleGetConfig_ReturnsCanonicalHardware(t *testing.T) {
|
||||
srv := &Server{}
|
||||
srv.SetResult(&models.AnalysisResult{
|
||||
|
||||
Reference in New Issue
Block a user