export: align reanimator contract v2.7
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -206,6 +207,177 @@ func TestRedfishConnectorCollect(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if !got.PowerControlAvailable {
|
||||
t.Fatalf("expected power control available")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureHostPowerForCollection_WaitsForStablePowerOn(t *testing.T) {
|
||||
t.Setenv("LOGPILE_REDFISH_POWERON_STABILIZATION", "1ms")
|
||||
|
||||
powerState := "Off"
|
||||
resetCalls := 0
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/redfish/v1/Systems/1", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1",
|
||||
"PowerState": powerState,
|
||||
"Actions": map[string]interface{}{
|
||||
"#ComputerSystem.Reset": map[string]interface{}{
|
||||
"target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
|
||||
"ResetType@Redfish.AllowableValues": []interface{}{"On"},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", func(w http.ResponseWriter, r *http.Request) {
|
||||
resetCalls++
|
||||
powerState = "On"
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
ts := httptest.NewTLSServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.Parse(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse server url: %v", err)
|
||||
}
|
||||
port := 443
|
||||
if u.Port() != "" {
|
||||
fmt.Sscanf(u.Port(), "%d", &port)
|
||||
}
|
||||
|
||||
c := NewRedfishConnector()
|
||||
hostOn, changed := c.ensureHostPowerForCollection(context.Background(), c.httpClientWithTimeout(Request{TLSMode: "insecure"}, 5*time.Second), Request{
|
||||
Host: u.Hostname(),
|
||||
Protocol: "redfish",
|
||||
Port: port,
|
||||
Username: "admin",
|
||||
AuthType: "password",
|
||||
Password: "secret",
|
||||
TLSMode: "insecure",
|
||||
PowerOnIfHostOff: true,
|
||||
}, ts.URL, "/redfish/v1/Systems/1", nil)
|
||||
if !hostOn || !changed {
|
||||
t.Fatalf("expected stable power-on result, got hostOn=%v changed=%v", hostOn, changed)
|
||||
}
|
||||
if resetCalls != 1 {
|
||||
t.Fatalf("expected one reset call, got %d", resetCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureHostPowerForCollection_FailsIfHostDoesNotStayOnAfterStabilization(t *testing.T) {
|
||||
t.Setenv("LOGPILE_REDFISH_POWERON_STABILIZATION", "1ms")
|
||||
|
||||
powerState := "Off"
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/redfish/v1/Systems/1", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
current := powerState
|
||||
if powerState == "On" {
|
||||
powerState = "Off"
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1",
|
||||
"PowerState": current,
|
||||
"Actions": map[string]interface{}{
|
||||
"#ComputerSystem.Reset": map[string]interface{}{
|
||||
"target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
|
||||
"ResetType@Redfish.AllowableValues": []interface{}{"On"},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", func(w http.ResponseWriter, r *http.Request) {
|
||||
powerState = "On"
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
ts := httptest.NewTLSServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.Parse(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse server url: %v", err)
|
||||
}
|
||||
port := 443
|
||||
if u.Port() != "" {
|
||||
fmt.Sscanf(u.Port(), "%d", &port)
|
||||
}
|
||||
|
||||
c := NewRedfishConnector()
|
||||
hostOn, changed := c.ensureHostPowerForCollection(context.Background(), c.httpClientWithTimeout(Request{TLSMode: "insecure"}, 5*time.Second), Request{
|
||||
Host: u.Hostname(),
|
||||
Protocol: "redfish",
|
||||
Port: port,
|
||||
Username: "admin",
|
||||
AuthType: "password",
|
||||
Password: "secret",
|
||||
TLSMode: "insecure",
|
||||
PowerOnIfHostOff: true,
|
||||
}, ts.URL, "/redfish/v1/Systems/1", nil)
|
||||
if hostOn || changed {
|
||||
t.Fatalf("expected unstable power-on result to fail, got hostOn=%v changed=%v", hostOn, changed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePCIeDeviceSlot_FromNestedRedfishSlotLocation(t *testing.T) {
|
||||
doc := map[string]interface{}{
|
||||
"Id": "NIC1",
|
||||
@@ -629,6 +801,42 @@ func TestParseNIC_PortCountFromControllerCapabilities(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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_DropsUnrealisticPortCount(t *testing.T) {
|
||||
nic := parseNIC(map[string]interface{}{
|
||||
"Id": "1",
|
||||
@@ -670,6 +878,48 @@ func TestParsePCIeDevice_PrefersFunctionClassOverDeviceType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
@@ -727,9 +977,9 @@ func TestParseCPUAndMemory_CollectOemDetails(t *testing.T) {
|
||||
"TemperatureCelsius": 63,
|
||||
"Oem": map[string]interface{}{
|
||||
"VendorX": map[string]interface{}{
|
||||
"MicrocodeVersion": "0x2b000643",
|
||||
"MicrocodeVersion": "0x2b000643",
|
||||
"UncorrectableErrors": 1,
|
||||
"ThermalThrottled": true,
|
||||
"ThermalThrottled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -749,18 +999,18 @@ func TestParseCPUAndMemory_CollectOemDetails(t *testing.T) {
|
||||
|
||||
dimms := parseMemory([]map[string]interface{}{
|
||||
{
|
||||
"Id": "DIMM0",
|
||||
"Id": "DIMM0",
|
||||
"DeviceLocator": "CPU0_C0D0",
|
||||
"CapacityMiB": 32768,
|
||||
"SerialNumber": "DIMM-001",
|
||||
"CapacityMiB": 32768,
|
||||
"SerialNumber": "DIMM-001",
|
||||
"Oem": map[string]interface{}{
|
||||
"VendorX": map[string]interface{}{
|
||||
"CorrectableECCErrorCount": 12,
|
||||
"UncorrectableECCErrorCount": 2,
|
||||
"TemperatureC": 41.5,
|
||||
"CorrectableECCErrorCount": 12,
|
||||
"UncorrectableECCErrorCount": 2,
|
||||
"TemperatureC": 41.5,
|
||||
"SpareBlocksRemainingPercent": 88,
|
||||
"PerformanceDegraded": true,
|
||||
"DataLossDetected": false,
|
||||
"PerformanceDegraded": true,
|
||||
"DataLossDetected": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -831,9 +1081,9 @@ func TestReplayRedfishFromRawPayloads_UsesProcessorAndMemoryMetrics(t *testing.T
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Memory/DIMM0/MemoryMetrics": map[string]interface{}{
|
||||
"CorrectableECCErrorCount": 14,
|
||||
"TemperatureCelsius": 42,
|
||||
"PerformanceDegraded": true,
|
||||
"CorrectableECCErrorCount": 14,
|
||||
"TemperatureCelsius": 42,
|
||||
"PerformanceDegraded": true,
|
||||
"SpareBlocksRemainingPercent": 91,
|
||||
},
|
||||
},
|
||||
@@ -897,10 +1147,10 @@ func TestReplayRedfishFromRawPayloads_UsesDriveMetrics(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Storage/RAID1/Drives/Drive0/DriveMetrics": map[string]interface{}{
|
||||
"PowerOnHours": 1001,
|
||||
"MediaErrors": 3,
|
||||
"PowerOnHours": 1001,
|
||||
"MediaErrors": 3,
|
||||
"AvailableSparePercent": 92,
|
||||
"TemperatureCelsius": 37,
|
||||
"TemperatureCelsius": 37,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -978,9 +1228,9 @@ func TestParseDriveAndPSU_CollectComponentMetricsIntoDetails(t *testing.T) {
|
||||
"SerialNumber": "SSD-002",
|
||||
"Oem": map[string]interface{}{
|
||||
"Public": map[string]interface{}{
|
||||
"temperature": 19,
|
||||
"temperature": 19,
|
||||
"PercentAvailableSpare": 93,
|
||||
"PercentageUsed": 7,
|
||||
"PercentageUsed": 7,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1076,9 +1326,9 @@ func TestParseComponentDetails_UseLinkedSupplementalMetrics(t *testing.T) {
|
||||
"SerialNumber": "SSD-001",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"PowerOnHours": 5001,
|
||||
"MediaErrors": 2,
|
||||
"TemperatureC": 39.5,
|
||||
"PowerOnHours": 5001,
|
||||
"MediaErrors": 2,
|
||||
"TemperatureC": 39.5,
|
||||
"LifeUsedPercent": 9,
|
||||
},
|
||||
)
|
||||
@@ -1093,7 +1343,7 @@ func TestParseComponentDetails_UseLinkedSupplementalMetrics(t *testing.T) {
|
||||
},
|
||||
1,
|
||||
map[string]interface{}{
|
||||
"Temperature": 44,
|
||||
"Temperature": 44,
|
||||
"LifeRemainingPercent": 97,
|
||||
},
|
||||
)
|
||||
@@ -2206,6 +2456,48 @@ func TestLooksLikeGPU_NVSwitchExcluded(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
@@ -2222,6 +2514,12 @@ func TestShouldCrawlPath_MemoryAndProcessorMetricsAreAllowed(t *testing.T) {
|
||||
if shouldCrawlPath("/redfish/v1/Chassis/1/PCIeDevices/0/PCIeFunctions/1") {
|
||||
t.Fatalf("expected noisy chassis pciefunctions branch to be skipped")
|
||||
}
|
||||
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) {
|
||||
@@ -2531,3 +2829,19 @@ func TestNVMePostProbeSkipsNonStorageChassis(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user