Improve Multillect Redfish replay and power detection
This commit is contained in:
@@ -270,6 +270,71 @@ func TestRedfishConnectorProbe(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if !got.PowerControlAvailable {
|
||||
t.Fatalf("expected power control available")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureHostPowerForCollection_WaitsForStablePowerOn(t *testing.T) {
|
||||
t.Setenv("LOGPILE_REDFISH_POWERON_STABILIZATION", "1ms")
|
||||
t.Setenv("LOGPILE_REDFISH_BMC_READY_WAITS", "1ms,1ms")
|
||||
@@ -388,6 +453,104 @@ func TestEnsureHostPowerForCollection_FailsIfHostDoesNotStayOnAfterStabilization
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureHostPowerForCollection_UsesPowerSummaryState(t *testing.T) {
|
||||
t.Setenv("LOGPILE_REDFISH_POWERON_STABILIZATION", "1ms")
|
||||
t.Setenv("LOGPILE_REDFISH_BMC_READY_WAITS", "1ms,1ms")
|
||||
|
||||
powerState := "On"
|
||||
|
||||
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",
|
||||
"PowerSummary": map[string]interface{}{
|
||||
"PowerState": powerState,
|
||||
},
|
||||
"MemorySummary": map[string]interface{}{
|
||||
"TotalSystemMemoryGiB": 128,
|
||||
},
|
||||
"Actions": map[string]interface{}{
|
||||
"#ComputerSystem.Reset": map[string]interface{}{
|
||||
"target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
|
||||
"ResetType@Redfish.AllowableValues": []interface{}{"On"},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
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 already-on host from PowerSummary, got hostOn=%v changed=%v", hostOn, changed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForHostPowerState_UsesPowerSummaryState(t *testing.T) {
|
||||
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 == "Off" {
|
||||
powerState = "On"
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1",
|
||||
"PowerSummary": map[string]interface{}{
|
||||
"PowerState": current,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
ok := c.waitForHostPowerState(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",
|
||||
}, ts.URL, "/redfish/v1/Systems/1", true, 3*time.Second)
|
||||
if !ok {
|
||||
t.Fatalf("expected waitForHostPowerState to use PowerSummary")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePCIeDeviceSlot_FromNestedRedfishSlotLocation(t *testing.T) {
|
||||
doc := map[string]interface{}{
|
||||
"Id": "NIC1",
|
||||
@@ -488,6 +651,271 @@ func TestReplayRedfishFromRawPayloads_FallbackCollectionMembersByPrefix(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
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{}{
|
||||
|
||||
Reference in New Issue
Block a user