Add raw export reanalyze flow for Redfish snapshots

This commit is contained in:
Mikhail Chusavitin
2026-02-24 17:23:26 +03:00
parent 5d9e9d73de
commit 810c4b5ff9
7 changed files with 783 additions and 23 deletions

View File

@@ -542,15 +542,15 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
enqueue(ref)
}
}
n := atomic.AddInt32(&processed, 1)
if err != nil {
c.debugSnapshotf("worker=%d fetch error path=%s err=%v", workerID, current, err)
if emit != nil {
emit(Progress{
Status: "running",
Progress: 92 + int(minInt32(n/200, 6)),
Message: fmt.Sprintf("Redfish snapshot: ошибка на %s", compactProgressPath(current)),
})
n := atomic.AddInt32(&processed, 1)
if err != nil {
c.debugSnapshotf("worker=%d fetch error path=%s err=%v", workerID, current, err)
if emit != nil && shouldReportSnapshotFetchError(err) {
emit(Progress{
Status: "running",
Progress: 92 + int(minInt32(n/200, 6)),
Message: fmt.Sprintf("Redfish snapshot: ошибка на %s", compactProgressPath(current)),
})
}
}
if emit != nil && n%40 == 0 {
@@ -894,14 +894,20 @@ func parsePSU(doc map[string]interface{}, idx int) models.PSU {
}
func parseGPU(doc map[string]interface{}, functionDocs []map[string]interface{}, idx int) models.GPU {
slot := firstNonEmpty(asString(doc["Slot"]), asString(doc["Name"]), asString(doc["Id"]))
slot := firstNonEmpty(
redfishLocationLabel(doc["Slot"]),
redfishLocationLabel(doc["Location"]),
redfishLocationLabel(doc["PhysicalLocation"]),
asString(doc["Name"]),
asString(doc["Id"]),
)
if slot == "" {
slot = fmt.Sprintf("GPU%d", idx)
}
gpu := models.GPU{
Slot: slot,
Location: firstNonEmpty(asString(doc["Location"]), asString(doc["PhysicalLocation"])),
Location: firstNonEmpty(redfishLocationLabel(doc["Location"]), redfishLocationLabel(doc["PhysicalLocation"])),
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
Manufacturer: asString(doc["Manufacturer"]),
SerialNumber: asString(doc["SerialNumber"]),
@@ -958,7 +964,7 @@ func parseGPU(doc map[string]interface{}, functionDocs []map[string]interface{},
func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]interface{}) models.PCIeDevice {
dev := models.PCIeDevice{
Slot: firstNonEmpty(asString(doc["Slot"]), asString(doc["Name"]), asString(doc["Id"])),
Slot: firstNonEmpty(redfishLocationLabel(doc["Slot"]), redfishLocationLabel(doc["Location"]), asString(doc["Name"]), asString(doc["Id"])),
BDF: asString(doc["BDF"]),
DeviceClass: firstNonEmpty(asString(doc["DeviceType"]), asString(doc["PCIeType"])),
Manufacturer: asString(doc["Manufacturer"]),
@@ -1013,7 +1019,7 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter
}
func parsePCIeFunction(doc map[string]interface{}, idx int) models.PCIeDevice {
slot := firstNonEmpty(asString(doc["Id"]), asString(doc["Name"]))
slot := firstNonEmpty(redfishLocationLabel(doc["Location"]), asString(doc["Id"]), asString(doc["Name"]))
if slot == "" {
slot = fmt.Sprintf("PCIeFn%d", idx)
}
@@ -1367,6 +1373,9 @@ func normalizeRedfishPath(raw string) string {
if raw == "" {
return ""
}
if i := strings.Index(raw, "#"); i >= 0 {
raw = raw[:i]
}
if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") {
u, err := url.Parse(raw)
@@ -1444,6 +1453,45 @@ func topRoots(counts map[string]int, limit int) []string {
return out
}
func redfishLocationLabel(v interface{}) string {
switch typed := v.(type) {
case nil:
return ""
case string:
return strings.TrimSpace(typed)
case map[string]interface{}:
// Common shapes:
// Slot.Location.PartLocation.ServiceLabel
// Location.PartLocation.ServiceLabel
// PartLocation.ServiceLabel
if nested := redfishLocationLabel(typed["Location"]); nested != "" {
return nested
}
if nested := redfishLocationLabel(typed["PartLocation"]); nested != "" {
return nested
}
serviceLabel := asString(typed["ServiceLabel"])
locationType := asString(typed["LocationType"])
ordinal := asString(typed["LocationOrdinalValue"])
if serviceLabel != "" {
return serviceLabel
}
if locationType != "" && ordinal != "" {
return fmt.Sprintf("%s %s", locationType, ordinal)
}
if locationType != "" {
return locationType
}
if ordinal != "" {
return "Slot " + ordinal
}
return ""
default:
// Avoid fmt.Sprint(map[]) style garbage for complex objects in UI/export.
return ""
}
}
func compactProgressPath(p string) string {
const maxLen = 72
if len(p) <= maxLen {
@@ -1452,6 +1500,20 @@ func compactProgressPath(p string) string {
return "..." + p[len(p)-maxLen+3:]
}
func shouldReportSnapshotFetchError(err error) bool {
if err == nil {
return false
}
msg := err.Error()
if strings.HasPrefix(msg, "status 404 ") ||
strings.HasPrefix(msg, "status 405 ") ||
strings.HasPrefix(msg, "status 410 ") ||
strings.HasPrefix(msg, "status 501 ") {
return false
}
return true
}
func minInt32(a, b int32) int32 {
if a < b {
return a