Improve Multillect Redfish replay and power detection
This commit is contained in:
@@ -110,7 +110,7 @@ func (c *RedfishConnector) Probe(ctx context.Context, req Request) (*ProbeResult
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("redfish system: %w", err)
|
return nil, fmt.Errorf("redfish system: %w", err)
|
||||||
}
|
}
|
||||||
powerState := strings.TrimSpace(asString(systemDoc["PowerState"]))
|
powerState := redfishSystemPowerState(systemDoc)
|
||||||
return &ProbeResult{
|
return &ProbeResult{
|
||||||
Reachable: true,
|
Reachable: true,
|
||||||
Protocol: "redfish",
|
Protocol: "redfish",
|
||||||
@@ -494,7 +494,7 @@ func (c *RedfishConnector) ensureHostPowerForCollection(ctx context.Context, cli
|
|||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
powerState := strings.TrimSpace(asString(systemDoc["PowerState"]))
|
powerState := redfishSystemPowerState(systemDoc)
|
||||||
if isRedfishHostPoweredOn(powerState) {
|
if isRedfishHostPoweredOn(powerState) {
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{Status: "running", Progress: 18, Message: fmt.Sprintf("Redfish: host включен (%s)", firstNonEmpty(powerState, "On"))})
|
emit(Progress{Status: "running", Progress: 18, Message: fmt.Sprintf("Redfish: host включен (%s)", firstNonEmpty(powerState, "On"))})
|
||||||
@@ -753,7 +753,7 @@ func (c *RedfishConnector) waitForHostPowerState(ctx context.Context, client *ht
|
|||||||
for {
|
for {
|
||||||
systemDoc, err := c.getJSON(ctx, client, req, baseURL, systemPath)
|
systemDoc, err := c.getJSON(ctx, client, req, baseURL, systemPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if isRedfishHostPoweredOn(strings.TrimSpace(asString(systemDoc["PowerState"]))) == wantOn {
|
if isRedfishHostPoweredOn(redfishSystemPowerState(systemDoc)) == wantOn {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -786,6 +786,19 @@ func isRedfishHostPoweredOn(state string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func redfishSystemPowerState(systemDoc map[string]interface{}) string {
|
||||||
|
if len(systemDoc) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if state := strings.TrimSpace(asString(systemDoc["PowerState"])); state != "" {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
if summary, ok := systemDoc["PowerSummary"].(map[string]interface{}); ok {
|
||||||
|
return strings.TrimSpace(asString(summary["PowerState"]))
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func redfishResetActionTarget(systemDoc map[string]interface{}) string {
|
func redfishResetActionTarget(systemDoc map[string]interface{}) string {
|
||||||
if systemDoc == nil {
|
if systemDoc == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -96,17 +96,23 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
networkProtocolDoc, _ := r.getJSON(joinPath(primaryManager, "/NetworkProtocol"))
|
networkProtocolDoc, _ := r.getJSON(joinPath(primaryManager, "/NetworkProtocol"))
|
||||||
firmware := parseFirmware(systemDoc, biosDoc, managerDoc, networkProtocolDoc)
|
firmware := parseFirmware(systemDoc, biosDoc, managerDoc, networkProtocolDoc)
|
||||||
firmware = dedupeFirmwareInfo(append(firmware, r.collectFirmwareInventory()...))
|
firmware = dedupeFirmwareInfo(append(firmware, r.collectFirmwareInventory()...))
|
||||||
boardInfo.BMCMACAddress = r.collectBMCMAC(managerPaths)
|
bmcManagementSummary := r.collectBMCManagementSummary(managerPaths)
|
||||||
|
boardInfo.BMCMACAddress = strings.TrimSpace(firstNonEmpty(
|
||||||
|
asString(bmcManagementSummary["mac_address"]),
|
||||||
|
r.collectBMCMAC(managerPaths),
|
||||||
|
))
|
||||||
assemblyFRU := r.collectAssemblyFRU(chassisPaths)
|
assemblyFRU := r.collectAssemblyFRU(chassisPaths)
|
||||||
collectedAt, sourceTimezone := inferRedfishCollectionTime(managerDoc, rawPayloads)
|
collectedAt, sourceTimezone := inferRedfishCollectionTime(managerDoc, rawPayloads)
|
||||||
inventoryLastModifiedAt := inferInventoryLastModifiedTime(r.tree)
|
inventoryLastModifiedAt := inferInventoryLastModifiedTime(r.tree)
|
||||||
logEntryEvents := parseRedfishLogEntries(rawPayloads, collectedAt)
|
logEntryEvents := parseRedfishLogEntries(rawPayloads, collectedAt)
|
||||||
|
sensorHintSummary, sensorHintEvents := r.collectSensorsListHints(chassisPaths, collectedAt)
|
||||||
|
bmcManagementEvent := buildBMCManagementSummaryEvent(bmcManagementSummary, collectedAt)
|
||||||
|
|
||||||
result := &models.AnalysisResult{
|
result := &models.AnalysisResult{
|
||||||
CollectedAt: collectedAt,
|
CollectedAt: collectedAt,
|
||||||
InventoryLastModifiedAt: inventoryLastModifiedAt,
|
InventoryLastModifiedAt: inventoryLastModifiedAt,
|
||||||
SourceTimezone: sourceTimezone,
|
SourceTimezone: sourceTimezone,
|
||||||
Events: append(append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+len(logEntryEvents)+1), healthEvents...), discreteEvents...), driveFetchWarningEvents...), logEntryEvents...),
|
Events: append(append(append(append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+len(logEntryEvents)+len(sensorHintEvents)+2), healthEvents...), discreteEvents...), driveFetchWarningEvents...), logEntryEvents...), sensorHintEvents...), bmcManagementEvent...),
|
||||||
FRU: assemblyFRU,
|
FRU: assemblyFRU,
|
||||||
Sensors: dedupeSensorReadings(append(append(thresholdSensors, thermalSensors...), powerSensors...)),
|
Sensors: dedupeSensorReadings(append(append(thresholdSensors, thermalSensors...), powerSensors...)),
|
||||||
RawPayloads: cloneRawPayloads(rawPayloads),
|
RawPayloads: cloneRawPayloads(rawPayloads),
|
||||||
@@ -155,6 +161,12 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
if strings.TrimSpace(sourceTimezone) != "" {
|
if strings.TrimSpace(sourceTimezone) != "" {
|
||||||
result.RawPayloads["source_timezone"] = sourceTimezone
|
result.RawPayloads["source_timezone"] = sourceTimezone
|
||||||
}
|
}
|
||||||
|
if len(sensorHintSummary) > 0 {
|
||||||
|
result.RawPayloads["redfish_sensor_hints"] = sensorHintSummary
|
||||||
|
}
|
||||||
|
if len(bmcManagementSummary) > 0 {
|
||||||
|
result.RawPayloads["redfish_bmc_network_summary"] = bmcManagementSummary
|
||||||
|
}
|
||||||
appendMissingServerModelWarning(result, systemDoc, joinPath(primarySystem, "/Oem/Public/FRU"), joinPath(primaryChassis, "/Oem/Public/FRU"))
|
appendMissingServerModelWarning(result, systemDoc, joinPath(primarySystem, "/Oem/Public/FRU"), joinPath(primaryChassis, "/Oem/Public/FRU"))
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@@ -324,6 +336,153 @@ func buildDriveFetchWarningEvents(rawPayloads map[string]any) []models.Event {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r redfishSnapshotReader) collectSensorsListHints(chassisPaths []string, collectedAt time.Time) (map[string]any, []models.Event) {
|
||||||
|
summary := make(map[string]any)
|
||||||
|
var events []models.Event
|
||||||
|
var presentDIMMs []string
|
||||||
|
dimmTotal := 0
|
||||||
|
dimmPresent := 0
|
||||||
|
physicalDriveSlots := 0
|
||||||
|
activePhysicalDriveSlots := 0
|
||||||
|
logicalDriveStatus := ""
|
||||||
|
|
||||||
|
for _, chassisPath := range chassisPaths {
|
||||||
|
doc, err := r.getJSON(joinPath(chassisPath, "/SensorsList"))
|
||||||
|
if err != nil || len(doc) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sensors, ok := doc["SensorsList"].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, item := range sensors {
|
||||||
|
sensor, ok := item.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(asString(sensor["SensorName"]))
|
||||||
|
sensorType := strings.TrimSpace(asString(sensor["SensorType"]))
|
||||||
|
status := strings.TrimSpace(asString(sensor["Status"]))
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(name, "DIMM") && strings.HasSuffix(name, "_Status") && strings.EqualFold(sensorType, "Memory"):
|
||||||
|
dimmTotal++
|
||||||
|
if redfishSlotStatusLooksPresent(status) {
|
||||||
|
dimmPresent++
|
||||||
|
presentDIMMs = append(presentDIMMs, strings.TrimSuffix(name, "_Status"))
|
||||||
|
}
|
||||||
|
case strings.EqualFold(sensorType, "Drive Slot"):
|
||||||
|
if strings.EqualFold(name, "Logical_Drive") {
|
||||||
|
logicalDriveStatus = firstNonEmpty(logicalDriveStatus, status)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
physicalDriveSlots++
|
||||||
|
if redfishSlotStatusLooksPresent(status) {
|
||||||
|
activePhysicalDriveSlots++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dimmTotal > 0 {
|
||||||
|
sort.Strings(presentDIMMs)
|
||||||
|
summary["memory_slots"] = map[string]any{
|
||||||
|
"total": dimmTotal,
|
||||||
|
"present_count": dimmPresent,
|
||||||
|
"present_slots": presentDIMMs,
|
||||||
|
"source": "SensorsList",
|
||||||
|
}
|
||||||
|
events = append(events, models.Event{
|
||||||
|
Timestamp: replayEventTimestamp(collectedAt),
|
||||||
|
Source: "Redfish",
|
||||||
|
EventType: "Collection Info",
|
||||||
|
Severity: models.SeverityInfo,
|
||||||
|
Description: fmt.Sprintf("Memory slot sensors report %d populated positions out of %d", dimmPresent, dimmTotal),
|
||||||
|
RawData: firstNonEmpty(strings.Join(presentDIMMs, ", "), "no populated DIMM slots reported"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if physicalDriveSlots > 0 || logicalDriveStatus != "" {
|
||||||
|
summary["drive_slots"] = map[string]any{
|
||||||
|
"physical_total": physicalDriveSlots,
|
||||||
|
"physical_active_count": activePhysicalDriveSlots,
|
||||||
|
"logical_drive_status": logicalDriveStatus,
|
||||||
|
"source": "SensorsList",
|
||||||
|
}
|
||||||
|
rawParts := []string{
|
||||||
|
fmt.Sprintf("physical_active=%d/%d", activePhysicalDriveSlots, physicalDriveSlots),
|
||||||
|
}
|
||||||
|
if logicalDriveStatus != "" {
|
||||||
|
rawParts = append(rawParts, "logical_drive="+logicalDriveStatus)
|
||||||
|
}
|
||||||
|
events = append(events, models.Event{
|
||||||
|
Timestamp: replayEventTimestamp(collectedAt),
|
||||||
|
Source: "Redfish",
|
||||||
|
EventType: "Collection Info",
|
||||||
|
Severity: models.SeverityInfo,
|
||||||
|
Description: fmt.Sprintf("Drive slot sensors report %d active physical slots out of %d", activePhysicalDriveSlots, physicalDriveSlots),
|
||||||
|
RawData: strings.Join(rawParts, "; "),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary, events
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBMCManagementSummaryEvent(summary map[string]any, collectedAt time.Time) []models.Event {
|
||||||
|
if len(summary) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
desc := fmt.Sprintf(
|
||||||
|
"BMC management interface %s link=%s ip=%s",
|
||||||
|
firstNonEmpty(asString(summary["interface_id"]), "unknown"),
|
||||||
|
firstNonEmpty(asString(summary["link_status"]), "unknown"),
|
||||||
|
firstNonEmpty(asString(summary["ipv4_address"]), "n/a"),
|
||||||
|
)
|
||||||
|
rawParts := make([]string, 0, 8)
|
||||||
|
for _, part := range []string{
|
||||||
|
"mac_address=" + strings.TrimSpace(asString(summary["mac_address"])),
|
||||||
|
"speed_mbps=" + strings.TrimSpace(asString(summary["speed_mbps"])),
|
||||||
|
"lldp_chassis_name=" + strings.TrimSpace(asString(summary["lldp_chassis_name"])),
|
||||||
|
"lldp_port_desc=" + strings.TrimSpace(asString(summary["lldp_port_desc"])),
|
||||||
|
"lldp_port_id=" + strings.TrimSpace(asString(summary["lldp_port_id"])),
|
||||||
|
"ipv4_gateway=" + strings.TrimSpace(asString(summary["ipv4_gateway"])),
|
||||||
|
} {
|
||||||
|
if !strings.HasSuffix(part, "=") {
|
||||||
|
rawParts = append(rawParts, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if vlan := asInt(summary["lldp_vlan_id"]); vlan > 0 {
|
||||||
|
rawParts = append(rawParts, fmt.Sprintf("lldp_vlan_id=%d", vlan))
|
||||||
|
}
|
||||||
|
if asBool(summary["ncsi_enabled"]) {
|
||||||
|
rawParts = append(rawParts, "ncsi_enabled=true")
|
||||||
|
}
|
||||||
|
return []models.Event{
|
||||||
|
{
|
||||||
|
Timestamp: replayEventTimestamp(collectedAt),
|
||||||
|
Source: "Redfish",
|
||||||
|
EventType: "Collection Info",
|
||||||
|
Severity: models.SeverityInfo,
|
||||||
|
Description: desc,
|
||||||
|
RawData: strings.Join(rawParts, "; "),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func redfishSlotStatusLooksPresent(status string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||||
|
case "ok", "enabled", "present", "warning", "critical":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func replayEventTimestamp(collectedAt time.Time) time.Time {
|
||||||
|
if !collectedAt.IsZero() {
|
||||||
|
return collectedAt
|
||||||
|
}
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
func (r redfishSnapshotReader) collectFirmwareInventory() []models.FirmwareInfo {
|
func (r redfishSnapshotReader) collectFirmwareInventory() []models.FirmwareInfo {
|
||||||
docs, err := r.getCollectionMembers("/redfish/v1/UpdateService/FirmwareInventory")
|
docs, err := r.getCollectionMembers("/redfish/v1/UpdateService/FirmwareInventory")
|
||||||
if err != nil || len(docs) == 0 {
|
if err != nil || len(docs) == 0 {
|
||||||
@@ -856,6 +1015,9 @@ func (r redfishSnapshotReader) fallbackCollectionMembers(collectionPath string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if redfishFallbackMemberLooksLikePlaceholder(collectionPath, doc) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if strings.TrimSpace(asString(doc["@odata.id"])) == "" {
|
if strings.TrimSpace(asString(doc["@odata.id"])) == "" {
|
||||||
doc["@odata.id"] = normalizeRedfishPath(p)
|
doc["@odata.id"] = normalizeRedfishPath(p)
|
||||||
}
|
}
|
||||||
@@ -864,6 +1026,135 @@ func (r redfishSnapshotReader) fallbackCollectionMembers(collectionPath string,
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func redfishFallbackMemberLooksLikePlaceholder(collectionPath string, doc map[string]interface{}) bool {
|
||||||
|
if len(doc) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
path := normalizeRedfishPath(collectionPath)
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(path, "/NetworkAdapters"):
|
||||||
|
return redfishNetworkAdapterPlaceholderDoc(doc)
|
||||||
|
case strings.HasSuffix(path, "/PCIeDevices"):
|
||||||
|
return redfishPCIePlaceholderDoc(doc)
|
||||||
|
case strings.Contains(path, "/Storage"):
|
||||||
|
return redfishStoragePlaceholderDoc(doc)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func redfishNetworkAdapterPlaceholderDoc(doc map[string]interface{}) bool {
|
||||||
|
if normalizeRedfishIdentityField(asString(doc["Model"])) != "" ||
|
||||||
|
normalizeRedfishIdentityField(asString(doc["Manufacturer"])) != "" ||
|
||||||
|
normalizeRedfishIdentityField(asString(doc["SerialNumber"])) != "" ||
|
||||||
|
normalizeRedfishIdentityField(asString(doc["PartNumber"])) != "" ||
|
||||||
|
normalizeRedfishIdentityField(asString(doc["BDF"])) != "" ||
|
||||||
|
asHexOrInt(doc["VendorId"]) != 0 ||
|
||||||
|
asHexOrInt(doc["DeviceId"]) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return redfishDocHasOnlyAllowedKeys(doc,
|
||||||
|
"@odata.context",
|
||||||
|
"@odata.id",
|
||||||
|
"@odata.type",
|
||||||
|
"Id",
|
||||||
|
"Name",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func redfishPCIePlaceholderDoc(doc map[string]interface{}) bool {
|
||||||
|
if normalizeRedfishIdentityField(asString(doc["Model"])) != "" ||
|
||||||
|
normalizeRedfishIdentityField(asString(doc["Manufacturer"])) != "" ||
|
||||||
|
normalizeRedfishIdentityField(asString(doc["SerialNumber"])) != "" ||
|
||||||
|
normalizeRedfishIdentityField(asString(doc["PartNumber"])) != "" ||
|
||||||
|
normalizeRedfishIdentityField(asString(doc["BDF"])) != "" ||
|
||||||
|
asHexOrInt(doc["VendorId"]) != 0 ||
|
||||||
|
asHexOrInt(doc["DeviceId"]) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return redfishDocHasOnlyAllowedKeys(doc,
|
||||||
|
"@odata.context",
|
||||||
|
"@odata.id",
|
||||||
|
"@odata.type",
|
||||||
|
"Id",
|
||||||
|
"Name",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func redfishStoragePlaceholderDoc(doc map[string]interface{}) bool {
|
||||||
|
if normalizeRedfishIdentityField(asString(doc["Model"])) != "" ||
|
||||||
|
normalizeRedfishIdentityField(asString(doc["Manufacturer"])) != "" ||
|
||||||
|
normalizeRedfishIdentityField(asString(doc["SerialNumber"])) != "" ||
|
||||||
|
normalizeRedfishIdentityField(asString(doc["PartNumber"])) != "" ||
|
||||||
|
normalizeRedfishIdentityField(asString(doc["BDF"])) != "" ||
|
||||||
|
asHexOrInt(doc["VendorId"]) != 0 ||
|
||||||
|
asHexOrInt(doc["DeviceId"]) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !redfishDocHasOnlyAllowedKeys(doc,
|
||||||
|
"@odata.id",
|
||||||
|
"@odata.type",
|
||||||
|
"Drives",
|
||||||
|
"Drives@odata.count",
|
||||||
|
"LogicalDisk",
|
||||||
|
"PhysicalDisk",
|
||||||
|
"Name",
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return redfishFieldIsEmptyCollection(doc["Drives"]) &&
|
||||||
|
redfishFieldIsZeroLike(doc["Drives@odata.count"]) &&
|
||||||
|
redfishFieldIsEmptyCollection(doc["LogicalDisk"]) &&
|
||||||
|
redfishFieldIsEmptyCollection(doc["PhysicalDisk"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func redfishDocHasOnlyAllowedKeys(doc map[string]interface{}, allowed ...string) bool {
|
||||||
|
if len(doc) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
allowedSet := make(map[string]struct{}, len(allowed))
|
||||||
|
for _, key := range allowed {
|
||||||
|
allowedSet[key] = struct{}{}
|
||||||
|
}
|
||||||
|
for key := range doc {
|
||||||
|
if _, ok := allowedSet[key]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func redfishFieldIsEmptyCollection(v any) bool {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case nil:
|
||||||
|
return true
|
||||||
|
case []interface{}:
|
||||||
|
return len(x) == 0
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func redfishFieldIsZeroLike(v any) bool {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case nil:
|
||||||
|
return true
|
||||||
|
case int:
|
||||||
|
return x == 0
|
||||||
|
case int32:
|
||||||
|
return x == 0
|
||||||
|
case int64:
|
||||||
|
return x == 0
|
||||||
|
case float64:
|
||||||
|
return x == 0
|
||||||
|
case string:
|
||||||
|
x = strings.TrimSpace(x)
|
||||||
|
return x == "" || x == "0"
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func cloneRawPayloads(src map[string]any) map[string]any {
|
func cloneRawPayloads(src map[string]any) map[string]any {
|
||||||
if len(src) == 0 {
|
if len(src) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -165,12 +165,25 @@ func (r redfishSnapshotReader) getChassisScopedPCIeSupplementalDocs(doc map[stri
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectBMCMAC returns the MAC address of the first active BMC management
|
// collectBMCMAC returns the MAC address of the best BMC management interface
|
||||||
// interface found in Managers/*/EthernetInterfaces. Returns empty string if
|
// found in Managers/*/EthernetInterfaces. Prefer an active link with an IP
|
||||||
// no MAC is available.
|
// address over a passive sideband interface.
|
||||||
func (r redfishSnapshotReader) collectBMCMAC(managerPaths []string) string {
|
func (r redfishSnapshotReader) collectBMCMAC(managerPaths []string) string {
|
||||||
|
summary := r.collectBMCManagementSummary(managerPaths)
|
||||||
|
if len(summary) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.ToUpper(strings.TrimSpace(asString(summary["mac_address"])))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redfishSnapshotReader) collectBMCManagementSummary(managerPaths []string) map[string]any {
|
||||||
|
bestScore := -1
|
||||||
|
var best map[string]any
|
||||||
for _, managerPath := range managerPaths {
|
for _, managerPath := range managerPaths {
|
||||||
members, err := r.getCollectionMembers(joinPath(managerPath, "/EthernetInterfaces"))
|
collectionPath := joinPath(managerPath, "/EthernetInterfaces")
|
||||||
|
collectionDoc, _ := r.getJSON(collectionPath)
|
||||||
|
ncsiEnabled, lldpMode, lldpByEth := redfishManagerEthernetCollectionHints(collectionDoc)
|
||||||
|
members, err := r.getCollectionMembers(collectionPath)
|
||||||
if err != nil || len(members) == 0 {
|
if err != nil || len(members) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -182,12 +195,141 @@ func (r redfishSnapshotReader) collectBMCMAC(managerPaths []string) string {
|
|||||||
if mac == "" || strings.EqualFold(mac, "00:00:00:00:00:00") {
|
if mac == "" || strings.EqualFold(mac, "00:00:00:00:00:00") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return strings.ToUpper(mac)
|
ifaceID := strings.TrimSpace(firstNonEmpty(asString(doc["Id"]), asString(doc["Name"])))
|
||||||
|
summary := map[string]any{
|
||||||
|
"manager_path": managerPath,
|
||||||
|
"interface_id": ifaceID,
|
||||||
|
"hostname": strings.TrimSpace(asString(doc["HostName"])),
|
||||||
|
"fqdn": strings.TrimSpace(asString(doc["FQDN"])),
|
||||||
|
"mac_address": strings.ToUpper(mac),
|
||||||
|
"link_status": strings.TrimSpace(asString(doc["LinkStatus"])),
|
||||||
|
"speed_mbps": asInt(doc["SpeedMbps"]),
|
||||||
|
"interface_name": strings.TrimSpace(asString(doc["Name"])),
|
||||||
|
"interface_desc": strings.TrimSpace(asString(doc["Description"])),
|
||||||
|
"ncsi_enabled": ncsiEnabled,
|
||||||
|
"lldp_mode": lldpMode,
|
||||||
|
"ipv4_address": redfishManagerIPv4Field(doc, "Address"),
|
||||||
|
"ipv4_gateway": redfishManagerIPv4Field(doc, "Gateway"),
|
||||||
|
"ipv4_subnet": redfishManagerIPv4Field(doc, "SubnetMask"),
|
||||||
|
"ipv6_address": redfishManagerIPv6Field(doc, "Address"),
|
||||||
|
"link_is_active": strings.EqualFold(strings.TrimSpace(asString(doc["LinkStatus"])), "LinkActive"),
|
||||||
|
"interface_score": 0,
|
||||||
|
}
|
||||||
|
if lldp, ok := lldpByEth[strings.ToLower(ifaceID)]; ok {
|
||||||
|
summary["lldp_chassis_name"] = lldp["ChassisName"]
|
||||||
|
summary["lldp_port_desc"] = lldp["PortDesc"]
|
||||||
|
summary["lldp_port_id"] = lldp["PortId"]
|
||||||
|
if vlan := asInt(lldp["VlanId"]); vlan > 0 {
|
||||||
|
summary["lldp_vlan_id"] = vlan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score := redfishManagerInterfaceScore(summary)
|
||||||
|
summary["interface_score"] = score
|
||||||
|
if score > bestScore {
|
||||||
|
bestScore = score
|
||||||
|
best = summary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
func redfishManagerEthernetCollectionHints(collectionDoc map[string]interface{}) (bool, string, map[string]map[string]interface{}) {
|
||||||
|
lldpByEth := make(map[string]map[string]interface{})
|
||||||
|
if len(collectionDoc) == 0 {
|
||||||
|
return false, "", lldpByEth
|
||||||
|
}
|
||||||
|
oem, _ := collectionDoc["Oem"].(map[string]interface{})
|
||||||
|
public, _ := oem["Public"].(map[string]interface{})
|
||||||
|
ncsiEnabled := asBool(public["NcsiEnabled"])
|
||||||
|
lldp, _ := public["LLDP"].(map[string]interface{})
|
||||||
|
lldpMode := strings.TrimSpace(asString(lldp["LLDPMode"]))
|
||||||
|
if members, ok := lldp["Members"].([]interface{}); ok {
|
||||||
|
for _, item := range members {
|
||||||
|
member, ok := item.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ethIndex := strings.ToLower(strings.TrimSpace(asString(member["EthIndex"])))
|
||||||
|
if ethIndex == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lldpByEth[ethIndex] = member
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ncsiEnabled, lldpMode, lldpByEth
|
||||||
|
}
|
||||||
|
|
||||||
|
func redfishManagerIPv4Field(doc map[string]interface{}, key string) string {
|
||||||
|
if len(doc) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, field := range []string{"IPv4Addresses", "IPv4StaticAddresses"} {
|
||||||
|
list, ok := doc[field].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, item := range list {
|
||||||
|
entry, ok := item.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value := strings.TrimSpace(asString(entry[key]))
|
||||||
|
if value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func redfishManagerIPv6Field(doc map[string]interface{}, key string) string {
|
||||||
|
if len(doc) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
list, ok := doc["IPv6Addresses"].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, item := range list {
|
||||||
|
entry, ok := item.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value := strings.TrimSpace(asString(entry[key]))
|
||||||
|
if value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func redfishManagerInterfaceScore(summary map[string]any) int {
|
||||||
|
score := 0
|
||||||
|
if strings.EqualFold(strings.TrimSpace(asString(summary["link_status"])), "LinkActive") {
|
||||||
|
score += 100
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(asString(summary["ipv4_address"])) != "" {
|
||||||
|
score += 40
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(asString(summary["ipv6_address"])) != "" {
|
||||||
|
score += 10
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(asString(summary["mac_address"])) != "" {
|
||||||
|
score += 10
|
||||||
|
}
|
||||||
|
if asInt(summary["speed_mbps"]) > 0 {
|
||||||
|
score += 5
|
||||||
|
}
|
||||||
|
if ifaceID := strings.ToLower(strings.TrimSpace(asString(summary["interface_id"]))); ifaceID != "" && !strings.HasPrefix(ifaceID, "usb") {
|
||||||
|
score += 3
|
||||||
|
}
|
||||||
|
if asBool(summary["ncsi_enabled"]) {
|
||||||
|
score += 1
|
||||||
|
}
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
// findNICIndexByLinkedNetworkAdapter resolves a NetworkInterface document to an
|
// findNICIndexByLinkedNetworkAdapter resolves a NetworkInterface document to an
|
||||||
// existing NIC in bySlot by following Links.NetworkAdapter → the Chassis
|
// existing NIC in bySlot by following Links.NetworkAdapter → the Chassis
|
||||||
// NetworkAdapter doc → its slot label. Returns -1 if no match is found.
|
// NetworkAdapter doc → its slot label. Returns -1 if no match is found.
|
||||||
|
|||||||
@@ -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) {
|
func TestEnsureHostPowerForCollection_WaitsForStablePowerOn(t *testing.T) {
|
||||||
t.Setenv("LOGPILE_REDFISH_POWERON_STABILIZATION", "1ms")
|
t.Setenv("LOGPILE_REDFISH_POWERON_STABILIZATION", "1ms")
|
||||||
t.Setenv("LOGPILE_REDFISH_BMC_READY_WAITS", "1ms,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) {
|
func TestParsePCIeDeviceSlot_FromNestedRedfishSlotLocation(t *testing.T) {
|
||||||
doc := map[string]interface{}{
|
doc := map[string]interface{}{
|
||||||
"Id": "NIC1",
|
"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) {
|
func TestReplayRedfishFromRawPayloads_PreservesSourceTimezoneAndUTCCollectedAt(t *testing.T) {
|
||||||
raw := map[string]any{
|
raw := map[string]any{
|
||||||
"redfish_tree": map[string]interface{}{
|
"redfish_tree": map[string]interface{}{
|
||||||
|
|||||||
Reference in New Issue
Block a user