collector/redfish: expand endpoint coverage and timestamp collect logs
This commit is contained in:
@@ -974,6 +974,12 @@ func redfishCriticalEndpoints(systemPaths, chassisPaths, managerPaths []string)
|
|||||||
add(joinPath(p, "/Power"))
|
add(joinPath(p, "/Power"))
|
||||||
add(joinPath(p, "/Thermal"))
|
add(joinPath(p, "/Thermal"))
|
||||||
add(joinPath(p, "/Sensors"))
|
add(joinPath(p, "/Sensors"))
|
||||||
|
add(joinPath(p, "/HealthSummary"))
|
||||||
|
add(joinPath(p, "/ThresholdSensors"))
|
||||||
|
add(joinPath(p, "/DiscreteSensors"))
|
||||||
|
add(joinPath(p, "/Boards"))
|
||||||
|
add(joinPath(p, "/Backplanes"))
|
||||||
|
add(joinPath(p, "/Assembly"))
|
||||||
add(joinPath(p, "/NetworkAdapters"))
|
add(joinPath(p, "/NetworkAdapters"))
|
||||||
add(joinPath(p, "/PCIeDevices"))
|
add(joinPath(p, "/PCIeDevices"))
|
||||||
add(joinPath(p, "/Accelerators"))
|
add(joinPath(p, "/Accelerators"))
|
||||||
@@ -1796,6 +1802,9 @@ func parseGPU(doc map[string]interface{}, functionDocs []map[string]interface{},
|
|||||||
if bdf := asString(doc["BDF"]); bdf != "" {
|
if bdf := asString(doc["BDF"]); bdf != "" {
|
||||||
gpu.BDF = bdf
|
gpu.BDF = bdf
|
||||||
}
|
}
|
||||||
|
if gpu.BDF == "" {
|
||||||
|
gpu.BDF = buildBDFfromOemPublic(doc)
|
||||||
|
}
|
||||||
if gpu.VendorID == 0 {
|
if gpu.VendorID == 0 {
|
||||||
gpu.VendorID = asHexOrInt(doc["VendorId"])
|
gpu.VendorID = asHexOrInt(doc["VendorId"])
|
||||||
}
|
}
|
||||||
@@ -1850,6 +1859,9 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter
|
|||||||
VendorID: asHexOrInt(doc["VendorId"]),
|
VendorID: asHexOrInt(doc["VendorId"]),
|
||||||
DeviceID: asHexOrInt(doc["DeviceId"]),
|
DeviceID: asHexOrInt(doc["DeviceId"]),
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(dev.BDF) == "" {
|
||||||
|
dev.BDF = buildBDFfromOemPublic(doc)
|
||||||
|
}
|
||||||
|
|
||||||
for _, fn := range functionDocs {
|
for _, fn := range functionDocs {
|
||||||
if dev.BDF == "" {
|
if dev.BDF == "" {
|
||||||
@@ -1969,6 +1981,36 @@ func isGenericPCIeClassLabel(v string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildBDFfromOemPublic(doc map[string]interface{}) string {
|
||||||
|
if len(doc) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
oem, ok := doc["Oem"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
public, ok := oem["Public"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
bus := asHexOrInt(public["BusNumber"])
|
||||||
|
dev := asHexOrInt(public["DeviceNumber"])
|
||||||
|
fn := asHexOrInt(public["FunctionNumber"])
|
||||||
|
if bus < 0 || dev < 0 || fn < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
segment := asHexOrInt(public["Segment"])
|
||||||
|
if segment < 0 {
|
||||||
|
segment = 0
|
||||||
|
}
|
||||||
|
// Require at least bus + dev numbers to avoid inventing meaningless BDFs.
|
||||||
|
if bus == 0 && dev == 0 && fn == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%04x:%02x:%02x.%x", segment, bus, dev, fn)
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeRedfishIdentityField(v string) string {
|
func normalizeRedfishIdentityField(v string) string {
|
||||||
v = strings.TrimSpace(v)
|
v = strings.TrimSpace(v)
|
||||||
if v == "" {
|
if v == "" {
|
||||||
@@ -2514,6 +2556,7 @@ func redfishSnapshotPrioritySeeds(systemPaths, chassisPaths, managerPaths []stri
|
|||||||
add(joinPath(p, "/PCIeDevices"))
|
add(joinPath(p, "/PCIeDevices"))
|
||||||
add(joinPath(p, "/PCIeFunctions"))
|
add(joinPath(p, "/PCIeFunctions"))
|
||||||
add(joinPath(p, "/Accelerators"))
|
add(joinPath(p, "/Accelerators"))
|
||||||
|
add(joinPath(p, "/GraphicsControllers"))
|
||||||
add(joinPath(p, "/Storage"))
|
add(joinPath(p, "/Storage"))
|
||||||
add(joinPath(p, "/SimpleStorage"))
|
add(joinPath(p, "/SimpleStorage"))
|
||||||
add(joinPath(p, "/Storage/IntelVROC"))
|
add(joinPath(p, "/Storage/IntelVROC"))
|
||||||
@@ -2524,6 +2567,12 @@ func redfishSnapshotPrioritySeeds(systemPaths, chassisPaths, managerPaths []stri
|
|||||||
add(p)
|
add(p)
|
||||||
add(joinPath(p, "/Oem/Public/FRU"))
|
add(joinPath(p, "/Oem/Public/FRU"))
|
||||||
add(joinPath(p, "/Sensors"))
|
add(joinPath(p, "/Sensors"))
|
||||||
|
add(joinPath(p, "/HealthSummary"))
|
||||||
|
add(joinPath(p, "/ThresholdSensors"))
|
||||||
|
add(joinPath(p, "/DiscreteSensors"))
|
||||||
|
add(joinPath(p, "/Boards"))
|
||||||
|
add(joinPath(p, "/Backplanes"))
|
||||||
|
add(joinPath(p, "/Assembly"))
|
||||||
add(joinPath(p, "/Thermal"))
|
add(joinPath(p, "/Thermal"))
|
||||||
add(joinPath(p, "/EnvironmentMetrics"))
|
add(joinPath(p, "/EnvironmentMetrics"))
|
||||||
add(joinPath(p, "/PCIeDevices"))
|
add(joinPath(p, "/PCIeDevices"))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package collector
|
package collector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -55,6 +56,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
if len(fruDoc) == 0 {
|
if len(fruDoc) == 0 {
|
||||||
fruDoc = chassisFRUDoc
|
fruDoc = chassisFRUDoc
|
||||||
}
|
}
|
||||||
|
boardFallbackDocs := r.collectBoardFallbackDocs(chassisPaths)
|
||||||
|
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{Status: "running", Progress: 55, Message: "Redfish snapshot: replay CPU/RAM/Storage..."})
|
emit(Progress{Status: "running", Progress: 55, Message: "Redfish snapshot: replay CPU/RAM/Storage..."})
|
||||||
@@ -71,16 +73,24 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
pcieDevices := r.collectPCIeDevices(systemPaths, chassisPaths)
|
pcieDevices := r.collectPCIeDevices(systemPaths, chassisPaths)
|
||||||
gpus := r.collectGPUs(systemPaths, chassisPaths)
|
gpus := r.collectGPUs(systemPaths, chassisPaths)
|
||||||
nics := r.collectNICs(chassisPaths)
|
nics := r.collectNICs(chassisPaths)
|
||||||
|
r.enrichNICsFromNetworkInterfaces(&nics, systemPaths)
|
||||||
|
thresholdSensors := r.collectThresholdSensors(chassisPaths)
|
||||||
|
discreteEvents := r.collectDiscreteSensorEvents(chassisPaths)
|
||||||
|
healthEvents := r.collectHealthSummaryEvents(chassisPaths)
|
||||||
managerDoc, _ := r.getJSON(primaryManager)
|
managerDoc, _ := r.getJSON(primaryManager)
|
||||||
networkProtocolDoc, _ := r.getJSON(joinPath(primaryManager, "/NetworkProtocol"))
|
networkProtocolDoc, _ := r.getJSON(joinPath(primaryManager, "/NetworkProtocol"))
|
||||||
|
firmware := parseFirmware(systemDoc, biosDoc, managerDoc, secureBootDoc, networkProtocolDoc)
|
||||||
|
firmware = dedupeFirmwareInfo(append(firmware, r.collectFirmwareInventory()...))
|
||||||
|
boardInfo := parseBoardInfoWithFallback(systemDoc, chassisDoc, fruDoc)
|
||||||
|
applyBoardInfoFallbackFromDocs(&boardInfo, boardFallbackDocs)
|
||||||
|
|
||||||
result := &models.AnalysisResult{
|
result := &models.AnalysisResult{
|
||||||
Events: make([]models.Event, 0),
|
Events: append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+1), healthEvents...), discreteEvents...),
|
||||||
FRU: make([]models.FRUInfo, 0),
|
FRU: make([]models.FRUInfo, 0),
|
||||||
Sensors: make([]models.SensorReading, 0),
|
Sensors: thresholdSensors,
|
||||||
RawPayloads: cloneRawPayloads(rawPayloads),
|
RawPayloads: cloneRawPayloads(rawPayloads),
|
||||||
Hardware: &models.HardwareConfig{
|
Hardware: &models.HardwareConfig{
|
||||||
BoardInfo: parseBoardInfoWithFallback(systemDoc, chassisDoc, fruDoc),
|
BoardInfo: boardInfo,
|
||||||
CPUs: parseCPUs(processors),
|
CPUs: parseCPUs(processors),
|
||||||
Memory: parseMemory(memory),
|
Memory: parseMemory(memory),
|
||||||
Storage: storageDevices,
|
Storage: storageDevices,
|
||||||
@@ -89,7 +99,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
GPUs: gpus,
|
GPUs: gpus,
|
||||||
PowerSupply: psus,
|
PowerSupply: psus,
|
||||||
NetworkAdapters: nics,
|
NetworkAdapters: nics,
|
||||||
Firmware: parseFirmware(systemDoc, biosDoc, managerDoc, secureBootDoc, networkProtocolDoc),
|
Firmware: firmware,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
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"))
|
||||||
@@ -158,6 +168,324 @@ func redfishFetchErrorsFromRawPayloads(rawPayloads map[string]any) map[string]st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r redfishSnapshotReader) collectFirmwareInventory() []models.FirmwareInfo {
|
||||||
|
docs, err := r.getCollectionMembers("/redfish/v1/UpdateService/FirmwareInventory")
|
||||||
|
if err != nil || len(docs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]models.FirmwareInfo, 0, len(docs))
|
||||||
|
for _, doc := range docs {
|
||||||
|
version := firstNonEmpty(
|
||||||
|
asString(doc["Version"]),
|
||||||
|
asString(doc["FirmwareVersion"]),
|
||||||
|
asString(doc["SoftwareVersion"]),
|
||||||
|
)
|
||||||
|
if strings.TrimSpace(version) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := firstNonEmpty(
|
||||||
|
asString(doc["DeviceName"]),
|
||||||
|
asString(doc["Name"]),
|
||||||
|
asString(doc["Id"]),
|
||||||
|
)
|
||||||
|
if strings.TrimSpace(name) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, models.FirmwareInfo{DeviceName: name, Version: version})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func dedupeFirmwareInfo(items []models.FirmwareInfo) []models.FirmwareInfo {
|
||||||
|
seen := make(map[string]struct{}, len(items))
|
||||||
|
out := make([]models.FirmwareInfo, 0, len(items))
|
||||||
|
for _, fw := range items {
|
||||||
|
name := strings.TrimSpace(fw.DeviceName)
|
||||||
|
ver := strings.TrimSpace(fw.Version)
|
||||||
|
if name == "" || ver == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToLower(name + "|" + ver)
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
out = append(out, models.FirmwareInfo{DeviceName: name, Version: ver})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redfishSnapshotReader) collectThresholdSensors(chassisPaths []string) []models.SensorReading {
|
||||||
|
out := make([]models.SensorReading, 0)
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for _, chassisPath := range chassisPaths {
|
||||||
|
docs, err := r.getCollectionMembers(joinPath(chassisPath, "/ThresholdSensors"))
|
||||||
|
if err != nil || len(docs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, doc := range docs {
|
||||||
|
sensor, ok := parseThresholdSensor(doc)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToLower(strings.TrimSpace(sensor.Name))
|
||||||
|
if key == "" {
|
||||||
|
key = strings.ToLower(strings.TrimSpace(sensor.Type) + "|" + strings.TrimSpace(sensor.RawValue))
|
||||||
|
}
|
||||||
|
if _, exists := seen[key]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
out = append(out, sensor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseThresholdSensor(doc map[string]interface{}) (models.SensorReading, bool) {
|
||||||
|
if len(doc) == 0 {
|
||||||
|
return models.SensorReading{}, false
|
||||||
|
}
|
||||||
|
name := firstNonEmpty(asString(doc["Name"]), asString(doc["Id"]))
|
||||||
|
status := mapStatus(doc["Status"])
|
||||||
|
if status == "" {
|
||||||
|
status = firstNonEmpty(asString(doc["Health"]), asString(doc["State"]))
|
||||||
|
}
|
||||||
|
reading := 0.0
|
||||||
|
unit := ""
|
||||||
|
rawValue := ""
|
||||||
|
switch {
|
||||||
|
case asString(doc["ReadingCelsius"]) != "":
|
||||||
|
reading = asFloat(doc["ReadingCelsius"])
|
||||||
|
unit = "C"
|
||||||
|
rawValue = asString(doc["ReadingCelsius"])
|
||||||
|
case asString(doc["ReadingVolts"]) != "":
|
||||||
|
reading = asFloat(doc["ReadingVolts"])
|
||||||
|
unit = "V"
|
||||||
|
rawValue = asString(doc["ReadingVolts"])
|
||||||
|
case asString(doc["ReadingAmps"]) != "":
|
||||||
|
reading = asFloat(doc["ReadingAmps"])
|
||||||
|
unit = "A"
|
||||||
|
rawValue = asString(doc["ReadingAmps"])
|
||||||
|
case asString(doc["ReadingWatts"]) != "":
|
||||||
|
reading = asFloat(doc["ReadingWatts"])
|
||||||
|
unit = "W"
|
||||||
|
rawValue = asString(doc["ReadingWatts"])
|
||||||
|
case asString(doc["Reading"]) != "":
|
||||||
|
reading = asFloat(doc["Reading"])
|
||||||
|
unit = asString(doc["ReadingUnits"])
|
||||||
|
rawValue = asString(doc["Reading"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" && rawValue == "" && status == "" {
|
||||||
|
return models.SensorReading{}, false
|
||||||
|
}
|
||||||
|
return models.SensorReading{
|
||||||
|
Name: firstNonEmpty(name, "threshold-sensor"),
|
||||||
|
Type: firstNonEmpty(asString(doc["ReadingType"]), asString(doc["SensorType"]), "threshold"),
|
||||||
|
Value: reading,
|
||||||
|
Unit: unit,
|
||||||
|
RawValue: rawValue,
|
||||||
|
Status: status,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redfishSnapshotReader) collectDiscreteSensorEvents(chassisPaths []string) []models.Event {
|
||||||
|
out := make([]models.Event, 0)
|
||||||
|
for _, chassisPath := range chassisPaths {
|
||||||
|
docs, err := r.getCollectionMembers(joinPath(chassisPath, "/DiscreteSensors"))
|
||||||
|
if err != nil || len(docs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, doc := range docs {
|
||||||
|
name := firstNonEmpty(asString(doc["Name"]), asString(doc["Id"]))
|
||||||
|
status := mapStatus(doc["Status"])
|
||||||
|
if status == "" {
|
||||||
|
status = firstNonEmpty(asString(doc["Health"]), asString(doc["State"]))
|
||||||
|
}
|
||||||
|
if name == "" || status == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(status))
|
||||||
|
if normalized == "ok" || normalized == "enabled" || normalized == "normal" || normalized == "present" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, models.Event{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Source: "Redfish",
|
||||||
|
SensorName: name,
|
||||||
|
EventType: "Discrete Sensor Status",
|
||||||
|
Severity: models.SeverityWarning,
|
||||||
|
Description: fmt.Sprintf("%s reports %s", name, status),
|
||||||
|
RawData: firstNonEmpty(asString(doc["Description"]), status),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redfishSnapshotReader) collectHealthSummaryEvents(chassisPaths []string) []models.Event {
|
||||||
|
out := make([]models.Event, 0)
|
||||||
|
for _, chassisPath := range chassisPaths {
|
||||||
|
doc, err := r.getJSON(joinPath(chassisPath, "/HealthSummary"))
|
||||||
|
if err != nil || len(doc) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
health := firstNonEmpty(
|
||||||
|
mapStatus(doc["Status"]),
|
||||||
|
asString(doc["Health"]),
|
||||||
|
asString(doc["HealthRollup"]),
|
||||||
|
findFirstNormalizedStringByKeys(doc, "Health", "HealthRollup", "OverallHealth"),
|
||||||
|
)
|
||||||
|
if health == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.EqualFold(health, "OK") || strings.EqualFold(health, "Normal") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
raw, _ := json.Marshal(doc)
|
||||||
|
out = append(out, models.Event{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Source: "Redfish",
|
||||||
|
EventType: "Health Summary",
|
||||||
|
Severity: models.SeverityWarning,
|
||||||
|
Description: fmt.Sprintf("Chassis health summary reports %s", health),
|
||||||
|
RawData: string(raw),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redfishSnapshotReader) enrichNICsFromNetworkInterfaces(nics *[]models.NetworkAdapter, systemPaths []string) {
|
||||||
|
if nics == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bySlot := make(map[string]int, len(*nics))
|
||||||
|
for i, nic := range *nics {
|
||||||
|
bySlot[strings.ToLower(strings.TrimSpace(nic.Slot))] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, systemPath := range systemPaths {
|
||||||
|
ifaces, err := r.getCollectionMembers(joinPath(systemPath, "/NetworkInterfaces"))
|
||||||
|
if err != nil || len(ifaces) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
slot := firstNonEmpty(asString(iface["Id"]), asString(iface["Name"]))
|
||||||
|
if strings.TrimSpace(slot) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idx, ok := bySlot[strings.ToLower(strings.TrimSpace(slot))]
|
||||||
|
if !ok {
|
||||||
|
*nics = append(*nics, models.NetworkAdapter{
|
||||||
|
Slot: slot,
|
||||||
|
Present: true,
|
||||||
|
Model: firstNonEmpty(asString(iface["Model"]), asString(iface["Name"])),
|
||||||
|
Status: mapStatus(iface["Status"]),
|
||||||
|
})
|
||||||
|
idx = len(*nics) - 1
|
||||||
|
bySlot[strings.ToLower(strings.TrimSpace(slot))] = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
portsPath := redfishLinkedPath(iface, "NetworkPorts")
|
||||||
|
if portsPath == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
portDocs, err := r.getCollectionMembers(portsPath)
|
||||||
|
if err != nil || len(portDocs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
macs := append([]string{}, (*nics)[idx].MACAddresses...)
|
||||||
|
for _, p := range portDocs {
|
||||||
|
macs = append(macs, collectNetworkPortMACs(p)...)
|
||||||
|
}
|
||||||
|
(*nics)[idx].MACAddresses = dedupeStrings(macs)
|
||||||
|
if (*nics)[idx].PortCount == 0 {
|
||||||
|
(*nics)[idx].PortCount = len(portDocs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectNetworkPortMACs(doc map[string]interface{}) []string {
|
||||||
|
if len(doc) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]string, 0, 4)
|
||||||
|
if list, ok := doc["AssociatedNetworkAddresses"].([]interface{}); ok {
|
||||||
|
for _, item := range list {
|
||||||
|
if s := strings.TrimSpace(asString(item)); s != "" {
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, key := range []string{"MACAddress", "PermanentMACAddress", "CurrentMACAddress"} {
|
||||||
|
if s := strings.TrimSpace(asString(doc[key])); s != "" {
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func dedupeStrings(items []string) []string {
|
||||||
|
seen := make(map[string]struct{}, len(items))
|
||||||
|
out := make([]string, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
s := strings.TrimSpace(item)
|
||||||
|
if s == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToLower(s)
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redfishSnapshotReader) collectBoardFallbackDocs(chassisPaths []string) []map[string]interface{} {
|
||||||
|
out := make([]map[string]interface{}, 0)
|
||||||
|
for _, chassisPath := range chassisPaths {
|
||||||
|
for _, suffix := range []string{"/Boards", "/Backplanes", "/Assembly"} {
|
||||||
|
path := joinPath(chassisPath, suffix)
|
||||||
|
if docs, err := r.getCollectionMembers(path); err == nil && len(docs) > 0 {
|
||||||
|
out = append(out, docs...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if doc, err := r.getJSON(path); err == nil && len(doc) > 0 {
|
||||||
|
out = append(out, doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyBoardInfoFallbackFromDocs(board *models.BoardInfo, docs []map[string]interface{}) {
|
||||||
|
if board == nil || len(docs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, doc := range docs {
|
||||||
|
candidate := parseBoardInfoFromFRUDoc(doc)
|
||||||
|
if board.Manufacturer == "" {
|
||||||
|
board.Manufacturer = candidate.Manufacturer
|
||||||
|
}
|
||||||
|
if board.ProductName == "" {
|
||||||
|
board.ProductName = candidate.ProductName
|
||||||
|
}
|
||||||
|
if board.SerialNumber == "" {
|
||||||
|
board.SerialNumber = candidate.SerialNumber
|
||||||
|
}
|
||||||
|
if board.PartNumber == "" {
|
||||||
|
board.PartNumber = candidate.PartNumber
|
||||||
|
}
|
||||||
|
if board.Manufacturer != "" && board.ProductName != "" && board.SerialNumber != "" && board.PartNumber != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type redfishSnapshotReader struct {
|
type redfishSnapshotReader struct {
|
||||||
tree map[string]interface{}
|
tree map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -23,7 +24,7 @@ func (m *JobManager) CreateJob(req CollectRequest) *Job {
|
|||||||
ID: generateJobID(),
|
ID: generateJobID(),
|
||||||
Status: CollectStatusQueued,
|
Status: CollectStatusQueued,
|
||||||
Progress: 0,
|
Progress: 0,
|
||||||
Logs: []string{"Задача поставлена в очередь"},
|
Logs: []string{formatCollectLogLine(now, "Задача поставлена в очередь")},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
RequestMeta: CollectRequestMeta{
|
RequestMeta: CollectRequestMeta{
|
||||||
@@ -65,7 +66,7 @@ func (m *JobManager) CancelJob(id string) (*Job, bool) {
|
|||||||
job.Status = CollectStatusCanceled
|
job.Status = CollectStatusCanceled
|
||||||
job.Error = ""
|
job.Error = ""
|
||||||
job.UpdatedAt = time.Now().UTC()
|
job.UpdatedAt = time.Now().UTC()
|
||||||
job.Logs = append(job.Logs, "Сбор отменен пользователем")
|
job.Logs = append(job.Logs, formatCollectLogLine(job.UpdatedAt, "Сбор отменен пользователем"))
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelFn := job.cancel
|
cancelFn := job.cancel
|
||||||
@@ -120,6 +121,7 @@ func (m *JobManager) AppendJobLog(id, message string) (*Job, bool) {
|
|||||||
|
|
||||||
job.Logs = append(job.Logs, message)
|
job.Logs = append(job.Logs, message)
|
||||||
job.UpdatedAt = time.Now().UTC()
|
job.UpdatedAt = time.Now().UTC()
|
||||||
|
job.Logs[len(job.Logs)-1] = formatCollectLogLine(job.UpdatedAt, message)
|
||||||
|
|
||||||
cloned := cloneJob(job)
|
cloned := cloneJob(job)
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
@@ -157,6 +159,17 @@ func normalizeProgress(progress int) int {
|
|||||||
return progress
|
return progress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatCollectLogLine(ts time.Time, message string) string {
|
||||||
|
msg := message
|
||||||
|
if msg == "" {
|
||||||
|
msg = "-"
|
||||||
|
}
|
||||||
|
if ts.IsZero() {
|
||||||
|
ts = time.Now().UTC()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s", ts.Format(time.RFC3339Nano), msg)
|
||||||
|
}
|
||||||
|
|
||||||
func cloneJob(job *Job) *Job {
|
func cloneJob(job *Job) *Job {
|
||||||
if job == nil {
|
if job == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user