feat(hpe): improve inventory extraction and export fidelity

This commit is contained in:
Mikhail Chusavitin
2026-03-30 15:04:17 +03:00
parent d8c3256e41
commit c47c34fd11
12 changed files with 989 additions and 59 deletions
+152 -14
View File
@@ -1513,16 +1513,13 @@ func (c *RedfishConnector) collectPCIeDevices(ctx context.Context, client *http.
}
func (c *RedfishConnector) getChassisScopedPCIeSupplementalDocs(ctx context.Context, client *http.Client, req Request, baseURL string, doc map[string]interface{}) []map[string]interface{} {
if !looksLikeNVSwitchPCIeDoc(doc) {
return nil
}
docPath := normalizeRedfishPath(asString(doc["@odata.id"]))
chassisPath := chassisPathForPCIeDoc(docPath)
if chassisPath == "" {
return nil
}
out := make([]map[string]interface{}, 0, 4)
out := make([]map[string]interface{}, 0, 6)
seen := make(map[string]struct{})
add := func(path string) {
path = normalizeRedfishPath(path)
@@ -1540,8 +1537,19 @@ func (c *RedfishConnector) getChassisScopedPCIeSupplementalDocs(ctx context.Cont
out = append(out, supplementalDoc)
}
add(joinPath(chassisPath, "/EnvironmentMetrics"))
add(joinPath(chassisPath, "/ThermalSubsystem/ThermalMetrics"))
if looksLikeNVSwitchPCIeDoc(doc) {
add(joinPath(chassisPath, "/EnvironmentMetrics"))
add(joinPath(chassisPath, "/ThermalSubsystem/ThermalMetrics"))
}
deviceDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(chassisPath, "/Devices"))
if err == nil {
for _, deviceDoc := range deviceDocs {
if !redfishPCIeMatchesChassisDeviceDoc(doc, deviceDoc) {
continue
}
add(asString(deviceDoc["@odata.id"]))
}
}
return out
}
@@ -3434,8 +3442,11 @@ func parseBoardInfo(system map[string]interface{}) models.BoardInfo {
asString(system["Name"]),
)),
SerialNumber: normalizeRedfishIdentityField(asString(system["SerialNumber"])),
PartNumber: normalizeRedfishIdentityField(asString(system["PartNumber"])),
UUID: normalizeRedfishIdentityField(asString(system["UUID"])),
PartNumber: normalizeRedfishIdentityField(firstNonEmpty(
asString(system["PartNumber"]),
asString(system["SKU"]),
)),
UUID: normalizeRedfishIdentityField(asString(system["UUID"])),
}
}
@@ -3818,6 +3829,22 @@ func parseStorageVolume(doc map[string]interface{}, controller string) models.St
}
}
func redfishVolumeCapabilitiesDoc(doc map[string]interface{}) bool {
if len(doc) == 0 {
return false
}
if strings.Contains(strings.ToLower(strings.TrimSpace(asString(doc["@odata.type"]))), "collectioncapabilities") {
return true
}
path := strings.ToLower(normalizeRedfishPath(asString(doc["@odata.id"])))
if strings.HasSuffix(path, "/volumes/capabilities") {
return true
}
id := strings.TrimSpace(asString(doc["Id"]))
name := strings.ToLower(strings.TrimSpace(asString(doc["Name"])))
return strings.EqualFold(id, "Capabilities") || strings.Contains(name, "capabilities for volumecollection")
}
func parseNIC(doc map[string]interface{}) models.NetworkAdapter {
vendorID := asHexOrInt(doc["VendorId"])
deviceID := asHexOrInt(doc["DeviceId"])
@@ -4356,6 +4383,39 @@ func redfishFirstBoolAcrossDocs(docs []map[string]interface{}, keys ...string) *
return nil
}
func redfishFirstString(doc map[string]interface{}, keys ...string) string {
for _, key := range keys {
if v, ok := redfishLookupValue(doc, key); ok {
if s := strings.TrimSpace(asString(v)); s != "" {
return s
}
}
}
return ""
}
func redfishFirstStringAcrossDocs(docs []map[string]interface{}, keys ...string) string {
for _, doc := range docs {
if v := redfishFirstString(doc, keys...); v != "" {
return v
}
}
return ""
}
func redfishFirstLocationAcrossDocs(docs []map[string]interface{}, keys ...string) string {
for _, doc := range docs {
for _, key := range keys {
if v, ok := redfishLookupValue(doc, key); ok {
if loc := redfishLocationLabel(v); loc != "" {
return loc
}
}
}
}
return ""
}
func redfishLookupValue(doc map[string]interface{}, key string) (any, bool) {
if doc == nil || strings.TrimSpace(key) == "" {
return nil, false
@@ -4537,8 +4597,9 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter
}
func parsePCIeDeviceWithSupplementalDocs(doc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}) models.PCIeDevice {
supplementalSlot := redfishFirstLocationAcrossDocs(supplementalDocs, "Slot", "Location", "PhysicalLocation")
dev := models.PCIeDevice{
Slot: firstNonEmpty(redfishLocationLabel(doc["Slot"]), redfishLocationLabel(doc["Location"]), asString(doc["Name"]), asString(doc["Id"])),
Slot: firstNonEmpty(redfishLocationLabel(doc["Slot"]), redfishLocationLabel(doc["Location"]), supplementalSlot, asString(doc["Name"]), asString(doc["Id"])),
BDF: sanitizeRedfishBDF(asString(doc["BDF"])),
DeviceClass: asString(doc["DeviceType"]),
Manufacturer: asString(doc["Manufacturer"]),
@@ -4578,6 +4639,9 @@ func parsePCIeDeviceWithSupplementalDocs(doc map[string]interface{}, functionDoc
dev.MaxLinkSpeed = firstNonEmpty(asString(fn["MaxLinkSpeedGTs"]), asString(fn["MaxLinkSpeed"]))
}
}
if dev.DeviceClass == "" || isGenericPCIeClassLabel(dev.DeviceClass) {
dev.DeviceClass = firstNonEmpty(redfishFirstStringAcrossDocs(supplementalDocs, "DeviceType"), dev.DeviceClass)
}
if dev.DeviceClass == "" {
dev.DeviceClass = "PCIe device"
@@ -4588,15 +4652,22 @@ func parsePCIeDeviceWithSupplementalDocs(doc map[string]interface{}, functionDoc
}
}
if isGenericPCIeClassLabel(dev.DeviceClass) {
// Redfish DeviceType (e.g. MultiFunction/Simulated) is a topology attribute,
// not a user-facing device name. Prefer model/part labels when class cannot be resolved.
dev.DeviceClass = firstNonEmpty(asString(doc["Model"]), dev.PartNumber, dev.DeviceClass)
dev.DeviceClass = "PCIe device"
}
if strings.TrimSpace(dev.Manufacturer) == "" {
dev.Manufacturer = pciids.VendorName(dev.VendorID)
dev.Manufacturer = firstNonEmpty(
redfishFirstStringAcrossDocs(supplementalDocs, "Manufacturer"),
pciids.VendorName(dev.VendorID),
)
}
if strings.TrimSpace(dev.PartNumber) == "" {
dev.PartNumber = pciids.DeviceName(dev.VendorID, dev.DeviceID)
dev.PartNumber = firstNonEmpty(
redfishFirstStringAcrossDocs(supplementalDocs, "ProductPartNumber", "PartNumber"),
pciids.DeviceName(dev.VendorID, dev.DeviceID),
)
}
if normalizeRedfishIdentityField(dev.SerialNumber) == "" {
dev.SerialNumber = redfishFirstStringAcrossDocs(supplementalDocs, "SerialNumber")
}
return dev
}
@@ -4699,6 +4770,70 @@ func isGenericPCIeClassLabel(v string) bool {
}
}
func redfishPCIeMatchesChassisDeviceDoc(doc, deviceDoc map[string]interface{}) bool {
if len(doc) == 0 || len(deviceDoc) == 0 || redfishChassisDeviceDocLooksEmpty(deviceDoc) {
return false
}
docSerial := normalizeRedfishIdentityField(findFirstNormalizedStringByKeys(doc, "SerialNumber"))
deviceSerial := normalizeRedfishIdentityField(findFirstNormalizedStringByKeys(deviceDoc, "SerialNumber"))
if docSerial != "" && deviceSerial != "" && strings.EqualFold(docSerial, deviceSerial) {
return true
}
docTokens := redfishPCIeMatchTokens(doc)
deviceTokens := redfishPCIeMatchTokens(deviceDoc)
if len(docTokens) == 0 || len(deviceTokens) == 0 {
return false
}
for _, token := range docTokens {
for _, candidate := range deviceTokens {
if strings.EqualFold(token, candidate) {
return true
}
}
}
return false
}
func redfishPCIeMatchTokens(doc map[string]interface{}) []string {
if len(doc) == 0 {
return nil
}
rawValues := []string{
asString(doc["Name"]),
asString(doc["Model"]),
asString(doc["PartNumber"]),
asString(doc["ProductPartNumber"]),
}
out := make([]string, 0, len(rawValues))
seen := make(map[string]struct{}, len(rawValues))
for _, raw := range rawValues {
value := normalizeRedfishIdentityField(raw)
if value == "" {
continue
}
key := strings.ToLower(value)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, value)
}
return out
}
func redfishChassisDeviceDocLooksEmpty(doc map[string]interface{}) bool {
name := strings.ToLower(strings.TrimSpace(asString(doc["Name"])))
if strings.HasPrefix(name, "empty slot") {
return true
}
if strings.ToLower(strings.TrimSpace(asString(doc["DeviceType"]))) != "unknown" {
return false
}
return normalizeRedfishIdentityField(asString(doc["PartNumber"])) == "" &&
normalizeRedfishIdentityField(asString(doc["ProductPartNumber"])) == "" &&
normalizeRedfishIdentityField(findFirstNormalizedStringByKeys(doc, "SerialNumber")) == ""
}
func buildBDFfromOemPublic(doc map[string]interface{}) string {
if len(doc) == 0 {
return ""
@@ -5126,6 +5261,9 @@ func classifyStorageType(doc map[string]interface{}) string {
}
func looksLikeVolume(doc map[string]interface{}) bool {
if redfishVolumeCapabilitiesDoc(doc) {
return false
}
if asString(doc["RAIDType"]) != "" || asString(doc["VolumeType"]) != "" {
return true
}