feat(hpe): improve inventory extraction and export fidelity
This commit is contained in:
455
internal/parser/vendors/hpe_ilo_ahs/parser.go
vendored
455
internal/parser/vendors/hpe_ilo_ahs/parser.go
vendored
@@ -25,12 +25,17 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
partNumberPattern = regexp.MustCompile(`(?i)^[a-z0-9]{1,4}\d{4,6}-[a-z0-9]{2,4}$`)
|
||||
serverSerialRE = regexp.MustCompile(`(?i)(?:^|[_-])([a-z0-9]{10})(?:[_-]|\.)`)
|
||||
dimmSlotRE = regexp.MustCompile(`^PROC\s+(\d+)\s+DIMM\s+(\d+)$`)
|
||||
procSlotRE = regexp.MustCompile(`^Proc\s+(\d+)$`)
|
||||
psuSlotRE = regexp.MustCompile(`^Power Supply\s+(\d+)$`)
|
||||
eventTimeRE = regexp.MustCompile(`^\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}$`)
|
||||
partNumberPattern = regexp.MustCompile(`(?i)^[a-z0-9]{1,4}\d{4,6}-[a-z0-9]{2,4}$`)
|
||||
serverSerialRE = regexp.MustCompile(`(?i)(?:^|[_-])([a-z0-9]{10})(?:[_-]|\.)`)
|
||||
dimmSlotRE = regexp.MustCompile(`^PROC\s+(\d+)\s+DIMM\s+(\d+)$`)
|
||||
procSlotRE = regexp.MustCompile(`^Proc\s+(\d+)$`)
|
||||
psuSlotRE = regexp.MustCompile(`^Power Supply\s+(\d+)$`)
|
||||
eventTimeRE = regexp.MustCompile(`^\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}$`)
|
||||
psuXMLRE = regexp.MustCompile(`(?s)<PowerSupplySlot id="(\d+)">(.*?)</PowerSupplySlot>`)
|
||||
firmwareLockdownRE = regexp.MustCompile(`(?s)<FirmwareLockdown>(.*?)</FirmwareLockdown>`)
|
||||
xmlFieldRE = regexp.MustCompile(`(?s)<([A-Za-z0-9_-]+)>([^<]*)</[A-Za-z0-9_-]+>`)
|
||||
psuLogRE = regexp.MustCompile(`Update bay (\d+) (SPN|Serial Number|Model Number|fw ver\.), value = ([A-Za-z0-9._-]+)`)
|
||||
versionFragmentRE = regexp.MustCompile(`\d+(?:\.\d+)+`)
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -129,6 +134,13 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
result.Hardware.NetworkAdapters = dedupeNetworkAdapters(parseNetworkAdapters(tokens))
|
||||
result.Hardware.Firmware = dedupeFirmware(parseFirmware(tokens))
|
||||
|
||||
psuSupplements := parsePSUSupplements(entries)
|
||||
result.Hardware.PowerSupply = dedupePSUs(mergePSUs(result.Hardware.PowerSupply, psuSupplements))
|
||||
|
||||
lockdownFW, nicFirmwareByVendor := parseBCertFirmware(entries)
|
||||
result.Hardware.NetworkAdapters = dedupeNetworkAdapters(enrichNetworkAdapters(result.Hardware.NetworkAdapters, nicFirmwareByVendor))
|
||||
result.Hardware.Firmware = dedupeFirmware(append(result.Hardware.Firmware, lockdownFW...))
|
||||
|
||||
storage, volumes, controllerDevices, controllerFW := parseRedfishStorage(redfishDocs)
|
||||
result.Hardware.Storage = dedupeStorage(storage)
|
||||
result.Hardware.Volumes = volumes
|
||||
@@ -446,22 +458,37 @@ func parseDIMMs(tokens []string) []models.MemoryDIMM {
|
||||
|
||||
func parsePSUs(tokens []string) []models.PSU {
|
||||
out := make([]models.PSU, 0, 4)
|
||||
for i := 0; i+2 < len(tokens); i++ {
|
||||
for i := 0; i < len(tokens); i++ {
|
||||
match := psuSlotRE.FindStringSubmatch(tokens[i])
|
||||
if len(match) != 2 {
|
||||
continue
|
||||
}
|
||||
slot := "PSU " + match[1]
|
||||
serial := tokens[i+1]
|
||||
partNumber := tokens[i+2]
|
||||
if isUnavailable(serial) && isUnavailable(partNumber) {
|
||||
vendor := ""
|
||||
serial := ""
|
||||
partNumber := ""
|
||||
for j := i + 1; j < len(tokens) && j <= i+5; j++ {
|
||||
field := strings.TrimSpace(tokens[j])
|
||||
if strings.HasPrefix(field, "PciRoot(") || psuSlotRE.MatchString(field) || dimmSlotRE.MatchString(field) || procSlotRE.MatchString(field) || eventTimeRE.MatchString(field) {
|
||||
break
|
||||
}
|
||||
switch {
|
||||
case vendor == "" && looksLikePSUVendor(field):
|
||||
vendor = field
|
||||
case partNumber == "" && looksLikePartNumber(field):
|
||||
partNumber = field
|
||||
case serial == "" && isLikelySerial(field):
|
||||
serial = field
|
||||
}
|
||||
}
|
||||
if serial == "" && partNumber == "" {
|
||||
continue
|
||||
}
|
||||
psu := models.PSU{
|
||||
Slot: slot,
|
||||
Present: true,
|
||||
Model: valueOr(partNumber, "Power Supply"),
|
||||
Vendor: "HPE",
|
||||
Vendor: valueOr(cleanUnavailable(vendor), "HPE"),
|
||||
SerialNumber: cleanUnavailable(serial),
|
||||
PartNumber: cleanUnavailable(partNumber),
|
||||
Status: "ok",
|
||||
@@ -471,6 +498,80 @@ func parsePSUs(tokens []string) []models.PSU {
|
||||
return out
|
||||
}
|
||||
|
||||
func parsePSUSupplements(entries []ahsEntry) []models.PSU {
|
||||
bySlot := make(map[string]models.PSU)
|
||||
|
||||
for _, entry := range entries {
|
||||
text := string(entry.Content)
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.EqualFold(entry.Name, "bcert.pkg") {
|
||||
for _, match := range psuXMLRE.FindAllStringSubmatch(text, -1) {
|
||||
slotNum, _ := strconv.Atoi(match[1])
|
||||
slot := fmt.Sprintf("PSU %d", slotNum+1)
|
||||
fields := parseXMLFields(match[2])
|
||||
item := bySlot[slot]
|
||||
item.Slot = slot
|
||||
item.Present = strings.EqualFold(fields["Present"], "Yes") || item.Present
|
||||
if serial := strings.TrimSpace(fields["SerialNumber"]); serial != "" {
|
||||
item.SerialNumber = serial
|
||||
}
|
||||
if fw := strings.TrimSpace(fields["FirmwareVersion"]); fw != "" {
|
||||
item.Firmware = fw
|
||||
}
|
||||
if spare := strings.TrimSpace(fields["SparePartNumber"]); spare != "" {
|
||||
if item.Details == nil {
|
||||
item.Details = make(map[string]any)
|
||||
}
|
||||
item.Details["spare_part_number"] = spare
|
||||
}
|
||||
bySlot[slot] = item
|
||||
}
|
||||
}
|
||||
|
||||
for _, match := range psuLogRE.FindAllStringSubmatch(text, -1) {
|
||||
slotNum, _ := strconv.Atoi(match[1])
|
||||
slot := fmt.Sprintf("PSU %d", slotNum+1)
|
||||
item := bySlot[slot]
|
||||
item.Slot = slot
|
||||
item.Present = true
|
||||
value := strings.TrimSpace(match[3])
|
||||
switch match[2] {
|
||||
case "SPN":
|
||||
if item.Details == nil {
|
||||
item.Details = make(map[string]any)
|
||||
}
|
||||
item.Details["spare_part_number"] = value
|
||||
case "Serial Number":
|
||||
item.SerialNumber = value
|
||||
case "Model Number":
|
||||
item.Model = value
|
||||
item.PartNumber = value
|
||||
case "fw ver.":
|
||||
item.Firmware = normalizeLooseVersion(value)
|
||||
}
|
||||
bySlot[slot] = item
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]models.PSU, 0, len(bySlot))
|
||||
for _, item := range bySlot {
|
||||
if item.Slot == "" {
|
||||
continue
|
||||
}
|
||||
item.Vendor = valueOr(item.Vendor, "HPE")
|
||||
item.Status = valueOr(item.Status, "ok")
|
||||
if item.Model == "" {
|
||||
item.Model = valueOr(item.PartNumber, "Power Supply")
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Slot < out[j].Slot })
|
||||
return out
|
||||
}
|
||||
|
||||
type pcieSequence struct {
|
||||
UEFIPath string
|
||||
Code string
|
||||
@@ -621,13 +722,53 @@ func parseRedfishStorage(docs map[string]map[string]any) ([]models.Storage, []mo
|
||||
|
||||
storage := make([]models.Storage, 0, 8)
|
||||
volumes := make([]models.StorageVolume, 0, 4)
|
||||
devices := make([]models.HardwareDevice, 0, 4)
|
||||
firmware := make([]models.FirmwareInfo, 0, 4)
|
||||
devices := make([]models.HardwareDevice, 0, 6)
|
||||
firmware := make([]models.FirmwareInfo, 0, 8)
|
||||
fabricNames := make(map[string]string)
|
||||
fabricTypes := make(map[string]string)
|
||||
|
||||
for _, path := range paths {
|
||||
doc := docs[path]
|
||||
docType := asString(doc["@odata.type"])
|
||||
switch {
|
||||
case strings.Contains(docType, "#Fabric."):
|
||||
fabricID := redfishID(path)
|
||||
fabricNames[fabricID] = strings.TrimSpace(asString(doc["Name"]))
|
||||
fabricTypes[fabricID] = strings.TrimSpace(asString(doc["FabricType"]))
|
||||
|
||||
case strings.Contains(docType, "#Switch."):
|
||||
fabricID := fabricIDFromPath(path)
|
||||
name := valueOr(fabricNames[fabricID], strings.TrimSpace(asString(doc["Name"])))
|
||||
model := strings.TrimSpace(asString(doc["Model"]))
|
||||
fw := strings.TrimSpace(asString(doc["FirmwareVersion"]))
|
||||
device := models.HardwareDevice{
|
||||
ID: "hpe-fabric-" + redfishID(path),
|
||||
Kind: models.DeviceKindStorage,
|
||||
Source: "redfish",
|
||||
Slot: valueOr(fabricID, redfishID(path)),
|
||||
DeviceClass: "storage_backplane",
|
||||
Model: valueOr(name, model),
|
||||
PartNumber: model,
|
||||
Firmware: fw,
|
||||
Status: redfishStatus(doc["Status"]),
|
||||
Details: map[string]any{
|
||||
"odata_id": path,
|
||||
"fabric_type": valueOr(fabricTypes[fabricID], strings.TrimSpace(asString(doc["FabricType"]))),
|
||||
"switch_type": strings.TrimSpace(asString(doc["SwitchType"])),
|
||||
"supported_protocols": stringSlice(doc["SupportedProtocols"]),
|
||||
"domain_id": asInt64(doc["DomainID"]),
|
||||
"fabric_name": fabricNames[fabricID],
|
||||
"connected_chassis_id": asString(nested(doc, "Links", "Chassis", "@odata.id")),
|
||||
},
|
||||
}
|
||||
devices = append(devices, device)
|
||||
if fw != "" {
|
||||
firmware = append(firmware, models.FirmwareInfo{
|
||||
DeviceName: valueOr(name, model),
|
||||
Version: fw,
|
||||
})
|
||||
}
|
||||
|
||||
case strings.Contains(docType, "#StorageController."):
|
||||
slot := redfishServiceLabel(doc, "Location", "PartLocation", "ServiceLabel")
|
||||
model := valueOr(asString(doc["Model"]), asString(doc["Name"]))
|
||||
@@ -649,9 +790,16 @@ func parseRedfishStorage(docs map[string]map[string]any) ([]models.Storage, []mo
|
||||
Firmware: fw,
|
||||
Status: redfishStatus(doc["Status"]),
|
||||
Details: map[string]any{
|
||||
"odata_id": path,
|
||||
"part_number": partNumber,
|
||||
"sku": sku,
|
||||
"odata_id": path,
|
||||
"part_number": partNumber,
|
||||
"sku": sku,
|
||||
"speed_gbps": asFloat64(doc["SpeedGbps"]),
|
||||
"supported_controller_protocols": stringSlice(doc["SupportedControllerProtocols"]),
|
||||
"supported_device_protocols": stringSlice(doc["SupportedDeviceProtocols"]),
|
||||
"supported_raid_types": stringSlice(doc["SupportedRAIDTypes"]),
|
||||
"cache_total_mib": asInt64(nested(doc, "CacheSummary", "TotalCacheSizeMiB")),
|
||||
"persistent_cache_mib": asInt64(nested(doc, "CacheSummary", "PersistentCacheSizeMiB")),
|
||||
"durable_name": firstDurableName(doc),
|
||||
},
|
||||
}
|
||||
if width := asInt(doc, "PCIeInterface", "LanesInUse"); width > 0 {
|
||||
@@ -692,8 +840,12 @@ func parseRedfishStorage(docs map[string]map[string]any) ([]models.Storage, []mo
|
||||
RemainingEndurancePct: endurance,
|
||||
Status: redfishStatus(doc["Status"]),
|
||||
Details: map[string]any{
|
||||
"odata_id": path,
|
||||
"capacity_bytes": capacity,
|
||||
"odata_id": path,
|
||||
"capacity_bytes": capacity,
|
||||
"failure_predicted": asBool(doc["FailurePredicted"]),
|
||||
"negotiated_speed_gbps": asFloat64(doc["NegotiatedSpeedGbs"]),
|
||||
"capable_speed_gbps": asFloat64(doc["CapableSpeedGbs"]),
|
||||
"location_indicator_active": asBool(doc["LocationIndicatorActive"]),
|
||||
},
|
||||
}
|
||||
storage = append(storage, entry)
|
||||
@@ -1005,6 +1157,16 @@ func isHPEManufacturer(v string) bool {
|
||||
return v == "HPE" || v == "HP"
|
||||
}
|
||||
|
||||
func looksLikePSUVendor(v string) bool {
|
||||
v = strings.TrimSpace(strings.ToUpper(v))
|
||||
switch v {
|
||||
case "HPE", "HP", "DELTA", "LITEON", "LTEON":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func looksLikeServerModel(v string) bool {
|
||||
v = sanitizeModel(v)
|
||||
if v == "" {
|
||||
@@ -1115,6 +1277,163 @@ func inferVendor(model string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func mergePSUs(base, extra []models.PSU) []models.PSU {
|
||||
merged := make(map[string]models.PSU)
|
||||
order := make([]string, 0, len(base)+len(extra))
|
||||
mergeOne := func(item models.PSU) {
|
||||
key := strings.ToLower(strings.TrimSpace(item.Slot))
|
||||
if key == "" {
|
||||
key = strings.ToLower(strings.TrimSpace(valueOr(item.SerialNumber, item.Model+"|"+item.PartNumber)))
|
||||
}
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
current, exists := merged[key]
|
||||
if !exists {
|
||||
merged[key] = item
|
||||
order = append(order, key)
|
||||
return
|
||||
}
|
||||
if current.Slot == "" {
|
||||
current.Slot = item.Slot
|
||||
}
|
||||
current.Present = current.Present || item.Present
|
||||
current.Model = valueOr(current.Model, item.Model)
|
||||
current.Description = valueOr(current.Description, item.Description)
|
||||
current.Vendor = valueOr(current.Vendor, item.Vendor)
|
||||
if current.WattageW == 0 {
|
||||
current.WattageW = item.WattageW
|
||||
}
|
||||
current.SerialNumber = valueOr(current.SerialNumber, item.SerialNumber)
|
||||
current.PartNumber = valueOr(current.PartNumber, item.PartNumber)
|
||||
current.Firmware = valueOr(current.Firmware, item.Firmware)
|
||||
current.Status = valueOr(current.Status, item.Status)
|
||||
current.InputType = valueOr(current.InputType, item.InputType)
|
||||
if current.InputPowerW == 0 {
|
||||
current.InputPowerW = item.InputPowerW
|
||||
}
|
||||
if current.OutputPowerW == 0 {
|
||||
current.OutputPowerW = item.OutputPowerW
|
||||
}
|
||||
if current.InputVoltage == 0 {
|
||||
current.InputVoltage = item.InputVoltage
|
||||
}
|
||||
if current.OutputVoltage == 0 {
|
||||
current.OutputVoltage = item.OutputVoltage
|
||||
}
|
||||
if current.TemperatureC == 0 {
|
||||
current.TemperatureC = item.TemperatureC
|
||||
}
|
||||
current.Details = mergeDetailMaps(current.Details, item.Details)
|
||||
merged[key] = current
|
||||
}
|
||||
for _, item := range base {
|
||||
mergeOne(item)
|
||||
}
|
||||
for _, item := range extra {
|
||||
mergeOne(item)
|
||||
}
|
||||
out := make([]models.PSU, 0, len(order))
|
||||
for _, key := range order {
|
||||
out = append(out, merged[key])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func enrichNetworkAdapters(items []models.NetworkAdapter, firmwareByVendor map[string]string) []models.NetworkAdapter {
|
||||
out := make([]models.NetworkAdapter, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Firmware == "" {
|
||||
if fw := firmwareByVendor[strings.ToLower(strings.TrimSpace(item.Vendor))]; fw != "" {
|
||||
item.Firmware = fw
|
||||
}
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseBCertFirmware(entries []ahsEntry) ([]models.FirmwareInfo, map[string]string) {
|
||||
out := make([]models.FirmwareInfo, 0, 8)
|
||||
nicFirmwareByVendor := make(map[string]string)
|
||||
seen := make(map[string]bool)
|
||||
|
||||
tagNames := map[string]string{
|
||||
"SystemProgrammableLogicDevice": "System Programmable Logic Device",
|
||||
"ServerPlatformServicesSPSFirmware": "Server Platform Services (SPS) Firmware",
|
||||
"STMicroGen11TPM": "TPM Firmware",
|
||||
"PrimaryR012U3x16slotsriserx8-x16-x8": "PCIe Riser 1 Programmable Logic Device",
|
||||
"HPEMR408i-oGen11": "HPE MR408i-o Gen11",
|
||||
"UBM3": "8 SFF 24G x1NVMe/SAS UBM3 BC BP",
|
||||
"BCM57191Gb4pBASE-T": "BCM 5719 1Gb 4p BASE-T OCP Adptr",
|
||||
"BCM57191Gb4pBASE-TOCP3": "BCM 5719 1Gb 4p BASE-T OCP Adptr",
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !strings.EqualFold(entry.Name, "bcert.pkg") {
|
||||
continue
|
||||
}
|
||||
text := string(entry.Content)
|
||||
for _, match := range firmwareLockdownRE.FindAllStringSubmatch(text, -1) {
|
||||
fields := parseXMLFields(match[1])
|
||||
for tag, value := range fields {
|
||||
name := tagNames[tag]
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
version := normalizeBCertVersion(tag, value)
|
||||
if version == "" {
|
||||
continue
|
||||
}
|
||||
appendFirmware(&out, seen, models.FirmwareInfo{
|
||||
DeviceName: name,
|
||||
Version: version,
|
||||
})
|
||||
if strings.Contains(name, "BCM 5719") {
|
||||
nicFirmwareByVendor["broadcom"] = version
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out, nicFirmwareByVendor
|
||||
}
|
||||
|
||||
func parseXMLFields(block string) map[string]string {
|
||||
out := make(map[string]string)
|
||||
for _, match := range xmlFieldRE.FindAllStringSubmatch(block, -1) {
|
||||
out[match[1]] = strings.TrimSpace(match[2])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeBCertVersion(tag, value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || strings.EqualFold(value, "NA") {
|
||||
return ""
|
||||
}
|
||||
switch tag {
|
||||
case "UBM3":
|
||||
if idx := strings.LastIndex(value, "/"); idx >= 0 && idx+1 < len(value) {
|
||||
return strings.TrimSpace(value[idx+1:])
|
||||
}
|
||||
case "IntegratedLights-OutVI":
|
||||
if idx := strings.Index(value, " - "); idx > 0 {
|
||||
return strings.TrimSpace(value[:idx])
|
||||
}
|
||||
case "U54":
|
||||
return value
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeLooseVersion(value string) string {
|
||||
if match := versionFragmentRE.FindString(strings.TrimSpace(value)); match != "" {
|
||||
return match
|
||||
}
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func slotLabelFromCode(code string) string {
|
||||
parts := strings.Split(code, ".")
|
||||
if len(parts) < 3 {
|
||||
@@ -1132,6 +1451,16 @@ func slotLabelFromCode(code string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func fabricIDFromPath(path string) string {
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
for i := 0; i+1 < len(parts); i++ {
|
||||
if parts[i] == "Fabrics" {
|
||||
return parts[i+1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func inferSeverity(message string) models.Severity {
|
||||
lower := strings.ToLower(message)
|
||||
switch {
|
||||
@@ -1261,6 +1590,24 @@ func asInt64(v any) int64 {
|
||||
}
|
||||
}
|
||||
|
||||
func asFloat64(v any) float64 {
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
return t
|
||||
case float32:
|
||||
return float64(t)
|
||||
case int:
|
||||
return float64(t)
|
||||
case int64:
|
||||
return float64(t)
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func asOptionalInt(v any) *int {
|
||||
switch value := v.(type) {
|
||||
case float64:
|
||||
@@ -1274,6 +1621,11 @@ func asOptionalInt(v any) *int {
|
||||
}
|
||||
}
|
||||
|
||||
func asBool(v any) bool {
|
||||
b, ok := v.(bool)
|
||||
return ok && b
|
||||
}
|
||||
|
||||
func valueOr(v, fallback string) string {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return strings.TrimSpace(v)
|
||||
@@ -1281,6 +1633,73 @@ func valueOr(v, fallback string) string {
|
||||
return strings.TrimSpace(fallback)
|
||||
}
|
||||
|
||||
func stringSlice(v any) []string {
|
||||
items, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
value := strings.TrimSpace(asString(item))
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func firstDurableName(doc map[string]any) string {
|
||||
items, ok := doc["Identifiers"].([]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
for _, item := range items {
|
||||
entry, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if value := strings.TrimSpace(asString(entry["DurableName"])); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func mergeDetailMaps(base, extra map[string]any) map[string]any {
|
||||
if len(extra) == 0 {
|
||||
return base
|
||||
}
|
||||
if base == nil {
|
||||
base = make(map[string]any, len(extra))
|
||||
}
|
||||
for key, value := range extra {
|
||||
if _, exists := base[key]; !exists || isZeroValue(base[key]) {
|
||||
base[key] = value
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func isZeroValue(v any) bool {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return true
|
||||
case string:
|
||||
return strings.TrimSpace(t) == ""
|
||||
case int:
|
||||
return t == 0
|
||||
case int64:
|
||||
return t == 0
|
||||
case float64:
|
||||
return t == 0
|
||||
case bool:
|
||||
return !t
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func boolPtr(v bool) *bool {
|
||||
out := v
|
||||
return &out
|
||||
|
||||
Reference in New Issue
Block a user