Files
logpile/internal/parser/vendors/dell/parser.go
2026-03-15 21:38:28 +03:00

1574 lines
46 KiB
Go

// Package dell provides parser for Dell TSR archives.
package dell
import (
"archive/zip"
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"regexp"
"sort"
"strconv"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser"
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
)
const parserVersion = "3.0"
func init() {
parser.Register(&Parser{})
}
// Parser implements VendorParser for Dell TSR archives.
type Parser struct{}
func (p *Parser) Name() string { return "Dell TSR Parser" }
func (p *Parser) Vendor() string { return "dell" }
func (p *Parser) Version() string { return parserVersion }
func (p *Parser) Detect(files []parser.ExtractedFile) int {
confidence := 0
expanded := expandNestedZipFiles(files)
for _, f := range expanded {
path := strings.ToLower(strings.TrimSpace(f.Path))
switch {
case strings.HasSuffix(path, ".pl.zip"):
confidence += 40
case strings.HasSuffix(path, "tsr/metadata.json") || strings.HasSuffix(path, "/metadata.json"):
confidence += 25
case strings.HasSuffix(path, "sysinfo_dcim_view.xml"):
confidence += 30
case strings.HasSuffix(path, "sysinfo_dcim_softwareidentity.xml"):
confidence += 20
case strings.HasSuffix(path, "curr_lclog.xml"):
confidence += 10
case path == "signature":
confidence += 5
}
if confidence >= 100 {
return 100
}
}
return confidence
}
func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
expanded := expandNestedZipFiles(files)
result := &models.AnalysisResult{
Events: make([]models.Event, 0),
FRU: make([]models.FRUInfo, 0),
Sensors: make([]models.SensorReading, 0),
Hardware: &models.HardwareConfig{
Firmware: make([]models.FirmwareInfo, 0),
CPUs: make([]models.CPU, 0),
Memory: make([]models.MemoryDIMM, 0),
Storage: make([]models.Storage, 0),
Volumes: make([]models.StorageVolume, 0),
PCIeDevices: make([]models.PCIeDevice, 0),
GPUs: make([]models.GPU, 0),
NetworkAdapters: make([]models.NetworkAdapter, 0),
NetworkCards: make([]models.NIC, 0),
PowerSupply: make([]models.PSU, 0),
},
}
if f := findBySuffix(expanded, "tsr/metadata.json", "/metadata.json"); f != nil {
parseMetadataJSON(f.Content, result)
}
if f := findBySuffix(expanded, "sysinfo_dcim_view.xml"); f != nil {
parseDCIMViewXML(f.Content, result)
}
if f := findBySuffix(expanded, "sysinfo_dcim_softwareidentity.xml"); f != nil {
parseSoftwareIdentityXML(f.Content, result)
}
if f := findBySuffix(expanded, "sysinfo_cim_sensor.xml"); f != nil {
parseCIMSensorXML(f.Content, result)
}
if f := findBySuffix(expanded, "curr_lclog.xml"); f != nil {
result.Events = append(result.Events, parseLCEventsXML(f.Content)...)
}
result.Hardware.Storage = dedupeStorage(result.Hardware.Storage)
result.Hardware.Volumes = dedupeVolumes(result.Hardware.Volumes)
result.Hardware.PowerSupply = dedupePSU(result.Hardware.PowerSupply)
result.Hardware.NetworkAdapters = dedupeNetworkAdapters(result.Hardware.NetworkAdapters)
result.Hardware.NetworkCards = nicCardsFromAdapters(result.Hardware.NetworkAdapters)
result.Hardware.GPUs = dedupeGPU(result.Hardware.GPUs)
result.Hardware.PCIeDevices = removePCIeOverlappingWithGPUs(dedupePCIe(result.Hardware.PCIeDevices), result.Hardware.GPUs)
result.Hardware.CPUs = dedupeCPU(result.Hardware.CPUs)
result.Hardware.Memory = dedupeDIMM(result.Hardware.Memory)
result.Hardware.Firmware = dedupeFirmware(result.Hardware.Firmware)
result.Sensors = dedupeSensors(result.Sensors)
return result, nil
}
type metadataPayload struct {
Make string `json:"Make"`
Model string `json:"Model"`
ServiceTag string `json:"ServiceTag"`
FirmwareVersion string `json:"FirmwareVersion"`
CollectionDateTime string `json:"CollectionDateTime"`
}
func parseMetadataJSON(content []byte, result *models.AnalysisResult) {
var m metadataPayload
if err := json.Unmarshal(content, &m); err != nil {
return
}
if result.Hardware == nil {
return
}
setIfEmpty(&result.Hardware.BoardInfo.Manufacturer, strings.TrimSpace(m.Make))
setIfEmpty(&result.Hardware.BoardInfo.ProductName, strings.TrimSpace(m.Model))
setIfEmpty(&result.Hardware.BoardInfo.SerialNumber, strings.TrimSpace(m.ServiceTag))
fw := strings.TrimSpace(m.FirmwareVersion)
if fw != "" {
result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{
DeviceName: "iDRAC",
Version: fw,
})
}
if t := parseDellTSRTime(m.CollectionDateTime); !t.IsZero() {
result.CollectedAt = t.UTC()
}
}
type cimDoc struct {
XMLName xml.Name `xml:"CIM"`
Message struct {
SimpleReq struct {
Named []cimNamedInstance `xml:"VALUE.NAMEDINSTANCE"`
} `xml:"SIMPLEREQ"`
} `xml:"MESSAGE"`
}
type cimNamedInstance struct {
Instance cimInstance `xml:"INSTANCE"`
}
type cimInstance struct {
ClassName string `xml:"CLASSNAME,attr"`
Props []cimProperty `xml:",any"`
}
type cimProperty struct {
XMLName xml.Name
Name string `xml:"NAME,attr"`
Value string `xml:"VALUE"`
DisplayValue string `xml:"DisplayValue"`
ArrayValues []string `xml:"VALUE.ARRAY>VALUE"`
ArrayDisplay []string `xml:"VALUE.ARRAY>DisplayValue"`
}
func parseDCIMViewXML(content []byte, result *models.AnalysisResult) {
var doc cimDoc
if err := xml.Unmarshal(content, &doc); err != nil {
return
}
for _, ni := range doc.Message.SimpleReq.Named {
inst := ni.Instance
if strings.TrimSpace(inst.ClassName) == "" {
continue
}
props := propertyMap(inst.Props)
switch inst.ClassName {
case "DCIM_SystemView":
parseSystemView(props, result)
case "DCIM_CPUView":
parseCPUView(props, result)
case "DCIM_MemoryView":
parseMemoryView(props, result)
case "DCIM_PhysicalDiskView":
parsePhysicalDiskView(props, result)
case "DCIM_VirtualDiskView":
parseVirtualDiskView(props, result)
case "DCIM_PowerSupplyView":
parsePowerSupplyView(props, result)
case "DCIM_PCIDeviceView":
parsePCIeDeviceView(props, result)
case "DCIM_NICView", "DCIM_InfiniBandView":
parseNICView(props, result)
case "DCIM_VideoView":
parseVideoView(props, result)
case "DCIM_ControllerView":
parseControllerView(props, result)
case "DCIM_ControllerBatteryView":
parseControllerBatteryView(props, result)
case "DCIM_EnclosureView":
parseEnclosureView(props, result)
case "DCIM_iDRACCardView":
parseIDRACCardView(props, result)
case "DCIM_FanView":
parseFanView(props, result)
}
}
}
func parseCIMSensorXML(content []byte, result *models.AnalysisResult) {
var doc cimDoc
if err := xml.Unmarshal(content, &doc); err != nil {
return
}
for _, ni := range doc.Message.SimpleReq.Named {
inst := ni.Instance
props := propertyMap(inst.Props)
switch inst.ClassName {
case "DCIM_GPUSensor":
parseGPUCIMSensor(props, result)
default:
parseGenericCIMSensor(props, result)
}
}
}
func parseSoftwareIdentityXML(content []byte, result *models.AnalysisResult) {
var doc cimDoc
if err := xml.Unmarshal(content, &doc); err != nil {
return
}
for _, ni := range doc.Message.SimpleReq.Named {
props := propertyMap(ni.Instance.Props)
version := firstNonEmpty(props["versionstring"], props["revisionstring"])
if strings.TrimSpace(version) == "" {
continue
}
name := nicMACInModelRE.ReplaceAllString(firstNonEmpty(props["elementname"], props["fqdd"], props["instanceid"]), "")
if strings.TrimSpace(name) == "" {
continue
}
result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{
DeviceName: strings.TrimSpace(name),
Version: strings.TrimSpace(version),
Description: strings.TrimSpace(firstNonEmpty(props["fqdd"], props["componenttype"])),
})
}
}
type lcEvents struct {
Events []lcEvent `xml:"Event"`
}
type lcEvent struct {
AgentID string `xml:"AgentID,attr"`
Category string `xml:"Category,attr"`
Severity string `xml:"Severity,attr"`
Timestamp string `xml:"Timestamp,attr"`
Message string `xml:"Message"`
MessageID string `xml:"MessageID"`
FQDD string `xml:"FQDD"`
}
func parseLCEventsXML(content []byte) []models.Event {
var doc lcEvents
if err := xml.Unmarshal(content, &doc); err != nil {
return nil
}
out := make([]models.Event, 0, len(doc.Events))
for _, e := range doc.Events {
ts := parseDellEventTime(e.Timestamp)
desc := strings.TrimSpace(e.Message)
if desc == "" {
continue
}
out = append(out, models.Event{
ID: strings.TrimSpace(e.MessageID),
Timestamp: ts,
Source: firstNonEmpty(strings.TrimSpace(e.AgentID), "iDRAC"),
SensorName: strings.TrimSpace(e.FQDD),
EventType: firstNonEmpty(strings.TrimSpace(e.Category), strings.TrimSpace(e.MessageID)),
Severity: mapDellSeverity(e.Severity),
Description: desc,
})
}
return out
}
func parseSystemView(props map[string]string, result *models.AnalysisResult) {
board := &result.Hardware.BoardInfo
setIfEmpty(&board.Manufacturer, props["manufacturer"])
setIfEmpty(&board.ProductName, props["model"])
setIfEmpty(&board.SerialNumber, props["servicetag"])
setIfEmpty(&board.PartNumber, props["boardpartnumber"])
setIfEmpty(&board.Version, props["biosversionstring"])
setIfEmpty(&board.UUID, props["uuid"])
if strings.TrimSpace(board.Description) == "" {
board.Description = strings.TrimSpace(props["systemgeneration"])
}
addFirmware(result, "BIOS", props["biosversionstring"], "system bios")
addFirmware(result, "Lifecycle Controller", props["lifecyclecontrollerversion"], "idrac lifecycle")
addFirmware(result, "CPLD", props["cpldversion"], "system cpld")
}
func parseCPUView(props map[string]string, result *models.AnalysisResult) {
model := strings.TrimSpace(props["model"])
if model == "" {
return
}
cpu := models.CPU{
Socket: parseSocketFromFQDD(firstNonEmpty(props["fqdd"], props["instanceid"])),
Model: model,
Description: strings.TrimSpace(props["manufacturer"]),
Cores: parseIntLoose(firstNonEmpty(props["numberofenabledcores"], props["numberofprocessorcores"])),
Threads: parseIntLoose(props["numberofenabledthreads"]),
FrequencyMHz: parseIntLoose(props["currentclockspeed"]),
MaxFreqMHz: parseIntLoose(props["maxclockspeed"]),
PPIN: strings.TrimSpace(props["ppin"]),
Status: normalizeStatus(props["primarystatus"]),
}
result.Hardware.CPUs = append(result.Hardware.CPUs, cpu)
}
func parseMemoryView(props map[string]string, result *models.AnalysisResult) {
slot := strings.TrimSpace(firstNonEmpty(props["devicedescription"], props["fqdd"], props["instanceid"]))
if slot == "" {
return
}
status := normalizeStatus(props["primarystatus"])
dimm := models.MemoryDIMM{
Slot: slot,
Location: strings.TrimSpace(firstNonEmpty(props["fqdd"], slot)),
Present: status != "absent",
SizeMB: parseIntLoose(props["size"]),
Type: firstNonEmpty(props["memorytypeextended"], props["memorytype"], "DIMM"),
Technology: strings.TrimSpace(props["memorytechnology"]),
MaxSpeedMHz: parseIntLoose(props["speed"]),
CurrentSpeedMHz: parseIntLoose(props["currentoperatingspeed"]),
Manufacturer: strings.TrimSpace(props["manufacturer"]),
SerialNumber: strings.TrimSpace(props["serialnumber"]),
PartNumber: strings.TrimSpace(props["partnumber"]),
Status: status,
}
result.Hardware.Memory = append(result.Hardware.Memory, dimm)
}
func parsePhysicalDiskView(props map[string]string, result *models.AnalysisResult) {
slot := firstNonEmpty(props["slot"], extractDiskSlotFromFQDD(props["fqdd"]), props["fqdd"])
model := strings.TrimSpace(props["model"])
if strings.TrimSpace(slot) == "" && model == "" {
return
}
st := models.Storage{
Slot: strings.TrimSpace(slot),
Type: inferStorageType(props["mediatype"], props["busprotocol"]),
Model: model,
SizeGB: parseBytesToGB(props["sizeinbytes"]),
SerialNumber: strings.TrimSpace(props["serialnumber"]),
Manufacturer: strings.TrimSpace(props["manufacturer"]),
Firmware: strings.TrimSpace(firstNonEmpty(props["revision"], props["firmwareversion"])),
Interface: strings.TrimSpace(props["busprotocol"]),
Present: true,
Location: strings.TrimSpace(props["devicedescription"]),
Status: normalizeStatus(firstNonEmpty(props["raidstatus"], props["primarystatus"])),
}
if v := strings.TrimSpace(props["remainingratedwriteendurance"]); v != "" {
n := parseIntLoose(v)
st.RemainingEndurancePct = &n
}
result.Hardware.Storage = append(result.Hardware.Storage, st)
}
func parseVirtualDiskView(props map[string]string, result *models.AnalysisResult) {
id := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"]))
if id == "" {
return
}
capacityBytes := parseInt64Loose(props["sizeinbytes"])
sizeGB := 0
if capacityBytes > 0 {
sizeGB = int(capacityBytes / 1_000_000_000)
}
v := models.StorageVolume{
ID: id,
Name: strings.TrimSpace(firstNonEmpty(props["name"], id)),
Controller: extractControllerFromVolumeFQDD(id),
RAIDLevel: normalizeRAIDLevel(firstNonEmpty(props["raidtypes"], props["raidstatus"])),
SizeGB: sizeGB,
CapacityBytes: capacityBytes,
Status: normalizeStatus(firstNonEmpty(props["raidstatus"], props["primarystatus"])),
}
result.Hardware.Volumes = append(result.Hardware.Volumes, v)
}
func parsePowerSupplyView(props map[string]string, result *models.AnalysisResult) {
fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"]))
model := strings.TrimSpace(props["model"])
if fqdd == "" && model == "" {
return
}
slot := normalizePSUSlot(fqdd)
status := normalizeStatus(props["primarystatus"])
psu := models.PSU{
Slot: slot,
Present: status != "absent",
Model: model,
Description: strings.TrimSpace(props["devicedescription"]),
Vendor: strings.TrimSpace(props["manufacturer"]),
WattageW: parseIntLoose(firstNonEmpty(props["totaloutputpower"], props["effectivecapacity"])),
SerialNumber: strings.TrimSpace(props["serialnumber"]),
PartNumber: strings.TrimSpace(props["partnumber"]),
Firmware: strings.TrimSpace(props["firmwareversion"]),
Status: status,
InputVoltage: parseFloatLoose(props["inputvoltage"]),
InputType: strings.TrimSpace(props["type"]),
}
result.Hardware.PowerSupply = append(result.Hardware.PowerSupply, psu)
}
// pcieFQDDNoisePrefix lists FQDD prefixes that represent internal chipset/CPU
// components or devices already captured with richer data elsewhere:
// - HostBridge/P2PBridge/ISABridge/SMBus: AMD EPYC internal fabric, not PCIe slots
// - AHCI.Embedded: AMD FCH SATA, not a slot device
// - Video.Embedded: BMC/iDRAC Matrox graphics chip, not user-visible
// - NIC.Embedded: already parsed from DCIM_NICView with model and MAC addresses
var pcieFQDDNoisePrefix = []string{
"HostBridge.Embedded.",
"P2PBridge.Embedded.",
"ISABridge.Embedded.",
"SMBus.Embedded.",
"AHCI.Embedded.",
"Video.Embedded.",
// All NIC FQDD classes are parsed from DCIM_NICView / DCIM_InfiniBandView into
// NetworkAdapters with model, MAC, firmware, and VendorID/DeviceID. The
// DCIM_PCIDeviceView duplicate carries only DataBusWidth ("Unknown", "16x or x16")
// and no useful extra data, so suppress it here.
"NIC.",
"InfiniBand.",
}
func parsePCIeDeviceView(props map[string]string, result *models.AnalysisResult) {
// "description" is the chip/device model (e.g. "MT28908 Family [ConnectX-6]"); prefer
// it over "devicedescription" which is the location string ("InfiniBand in Slot 1 Port 1").
desc := strings.TrimSpace(firstNonEmpty(props["description"], props["devicedescription"]))
fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"]))
if desc == "" && fqdd == "" {
return
}
for _, prefix := range pcieFQDDNoisePrefix {
if strings.HasPrefix(fqdd, prefix) {
return
}
}
vendorID := parseHexOrDec(firstNonEmpty(props["pcivendorid"], props["vendorid"]))
deviceID := parseHexOrDec(firstNonEmpty(props["pcideviceid"], props["deviceid"]))
manufacturer := strings.TrimSpace(props["manufacturer"])
// General rule: if chip model not found in logs but PCI IDs are known, resolve from pci.ids
if desc == "" && vendorID != 0 && deviceID != 0 {
desc = pciids.DeviceName(vendorID, deviceID)
}
if manufacturer == "" && vendorID != 0 {
manufacturer = pciids.VendorName(vendorID)
}
p := models.PCIeDevice{
Slot: fqdd,
Description: desc,
VendorID: vendorID,
DeviceID: deviceID,
BDF: formatBDF(props["busnumber"], props["devicenumber"], props["functionnumber"]),
Manufacturer: manufacturer,
NUMANode: parseIntLoose(props["cpuaffinity"]),
Status: normalizeStatus(props["primarystatus"]),
}
result.Hardware.PCIeDevices = append(result.Hardware.PCIeDevices, p)
}
func parseNICView(props map[string]string, result *models.AnalysisResult) {
fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"]))
model := nicMACInModelRE.ReplaceAllString(strings.TrimSpace(firstNonEmpty(props["productname"], props["devicedescription"])), "")
if fqdd == "" && model == "" {
return
}
mac := strings.TrimSpace(firstNonEmpty(props["currentmacaddress"], props["permanentmacaddress"]))
vendorID := parseHexOrDec(firstNonEmpty(props["pcivendorid"], props["vendorid"]))
deviceID := parseHexOrDec(firstNonEmpty(props["pcideviceid"], props["deviceid"]))
vendor := strings.TrimSpace(firstNonEmpty(props["vendorname"], props["manufacturer"]))
// Prefer pci.ids chip model over generic ProductName when PCI IDs are available.
// Dell TSR often reports a marketing name (e.g. "Mellanox Network Adapter") while
// pci.ids has the precise chip identifier (e.g. "MT28908 Family [ConnectX-6]").
if vendorID != 0 && deviceID != 0 {
if chipModel := pciids.DeviceName(vendorID, deviceID); chipModel != "" {
model = chipModel
}
if vendor == "" {
vendor = pciids.VendorName(vendorID)
}
}
n := models.NetworkAdapter{
Slot: fqdd,
Location: strings.TrimSpace(firstNonEmpty(props["devicedescription"], fqdd)),
Present: true,
Model: model,
Description: strings.TrimSpace(props["protocol"]),
Vendor: vendor,
VendorID: vendorID,
DeviceID: deviceID,
SerialNumber: strings.TrimSpace(props["serialnumber"]),
PartNumber: strings.TrimSpace(props["partnumber"]),
Firmware: strings.TrimSpace(firstNonEmpty(
props["familyversion"],
props["efiversion"],
props["landriverversion"],
props["controllerbiosversion"],
)),
PortCount: inferPortCountFromFQDD(fqdd),
NUMANode: parseIntLoose(props["cpuaffinity"]),
Status: normalizeStatus(props["primarystatus"]),
}
if mac != "" {
n.MACAddresses = []string{mac}
if n.PortCount == 0 {
n.PortCount = 1
}
}
result.Hardware.NetworkAdapters = append(result.Hardware.NetworkAdapters, n)
}
func parseVideoView(props map[string]string, result *models.AnalysisResult) {
fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"]))
model := strings.TrimSpace(firstNonEmpty(props["marketingname"], props["description"], props["devicedescription"]))
serial := strings.TrimSpace(props["serialnumber"])
if fqdd == "" && model == "" && serial == "" {
return
}
gpu := models.GPU{
Slot: fqdd,
Location: strings.TrimSpace(firstNonEmpty(props["devicedescription"], fqdd)),
Model: model,
Description: strings.TrimSpace(props["description"]),
Manufacturer: strings.TrimSpace(props["manufacturer"]),
VendorID: parseHexOrDec(firstNonEmpty(props["pcivendorid"], props["pcisubvendorid"])),
DeviceID: parseHexOrDec(firstNonEmpty(props["pcideviceid"], props["pcisubdeviceid"])),
BDF: formatBDF(props["busnumber"], props["devicenumber"], props["functionnumber"]),
UUID: strings.TrimSpace(props["gpuguid"]),
SerialNumber: serial,
PartNumber: strings.TrimSpace(firstNonEmpty(props["gpupartnumber"], props["boardpartnumber"])),
Firmware: strings.TrimSpace(props["firmwareversion"]),
Status: normalizeStatus(firstNonEmpty(props["gpuhealth"], props["gpustate"], props["primarystatus"])),
}
result.Hardware.GPUs = append(result.Hardware.GPUs, gpu)
}
func parseControllerView(props map[string]string, result *models.AnalysisResult) {
fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"]))
name := strings.TrimSpace(firstNonEmpty(props["productname"], props["devicedescription"]))
if fqdd == "" && name == "" {
return
}
result.Hardware.PCIeDevices = append(result.Hardware.PCIeDevices, models.PCIeDevice{
Slot: fqdd,
Description: name,
VendorID: parseHexOrDec(firstNonEmpty(props["pcivendorid"], props["pcisubvendorid"])),
DeviceID: parseHexOrDec(firstNonEmpty(props["pcideviceid"], props["pcisubdeviceid"])),
BDF: formatBDF(props["bus"], props["device"], props["function"]),
DeviceClass: "storage-controller",
Manufacturer: strings.TrimSpace(firstNonEmpty(props["devicecardmanufacturer"], props["manufacturer"])),
PartNumber: strings.TrimSpace(firstNonEmpty(props["ppid"], props["boardpartnumber"])),
NUMANode: parseIntLoose(props["cpuaffinity"]),
Status: normalizeStatus(props["primarystatus"]),
})
addFirmware(result, firstNonEmpty(name, fqdd), props["controllerfirmwareversion"], firstNonEmpty(fqdd, "storage controller"))
}
func parseControllerBatteryView(props map[string]string, result *models.AnalysisResult) {
name := strings.TrimSpace(firstNonEmpty(props["devicedescription"], props["fqdd"], props["instanceid"]))
if name == "" {
return
}
result.Sensors = append(result.Sensors, models.SensorReading{
Name: name,
Type: "battery",
RawValue: strings.TrimSpace(firstNonEmpty(props["raidstate"], props["primarystatus"])),
Status: normalizeStatus(firstNonEmpty(props["primarystatus"], props["raidstate"])),
})
}
func parseEnclosureView(props map[string]string, result *models.AnalysisResult) {
desc := strings.TrimSpace(firstNonEmpty(props["devicedescription"], props["productname"]))
if desc == "" {
return
}
result.FRU = append(result.FRU, models.FRUInfo{
DeviceID: strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"])),
Description: desc,
ProductName: strings.TrimSpace(props["productname"]),
SerialNumber: strings.TrimSpace(props["servicetag"]),
AssetTag: strings.TrimSpace(props["assettag"]),
Version: strings.TrimSpace(props["version"]),
})
}
func parseIDRACCardView(props map[string]string, result *models.AnalysisResult) {
addFirmware(result, "iDRAC", props["firmwareversion"], "idrac card")
desc := strings.TrimSpace(firstNonEmpty(props["devicedescription"], "iDRAC"))
result.FRU = append(result.FRU, models.FRUInfo{
DeviceID: strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"])),
Description: desc,
Manufacturer: strings.TrimSpace(result.Hardware.BoardInfo.Manufacturer),
ProductName: strings.TrimSpace(props["model"]),
SerialNumber: strings.TrimSpace(props["guid"]),
Version: strings.TrimSpace(props["firmwareversion"]),
})
}
func parseFanView(props map[string]string, result *models.AnalysisResult) {
name := strings.TrimSpace(firstNonEmpty(props["devicedescription"], props["fqdd"], props["instanceid"]))
if name == "" {
return
}
reading := parseScaledFloat(props["currentreading"], props["unitmodifier"])
result.Sensors = append(result.Sensors, models.SensorReading{
Name: name,
Type: "fan_speed",
Value: reading,
Unit: "RPM",
RawValue: strings.TrimSpace(props["currentreading"]),
Status: normalizeStatus(props["primarystatus"]),
})
}
func parseGPUCIMSensor(props map[string]string, result *models.AnalysisResult) {
deviceID := strings.TrimSpace(props["deviceid"])
primaryTemp := parseScaledFloat(props["primarygputemperature"], "-1")
memTemp := parseScaledFloat(props["memorytemperature"], "-1")
power := parseIntLoose(props["powerconsumption"])
if primaryTemp > 0 {
result.Sensors = append(result.Sensors, models.SensorReading{
Name: firstNonEmpty(deviceID, "GPU"),
Type: "temperature",
Value: primaryTemp,
Unit: "C",
RawValue: strings.TrimSpace(props["primarygputemperature"]),
Status: normalizeCIMHealthStatus(props["thermalalertstatus"]),
})
}
if memTemp > 0 {
result.Sensors = append(result.Sensors, models.SensorReading{
Name: firstNonEmpty(deviceID, "GPU") + " memory",
Type: "temperature",
Value: memTemp,
Unit: "C",
RawValue: strings.TrimSpace(props["memorytemperature"]),
Status: normalizeCIMHealthStatus(props["thermalalertstatus"]),
})
}
for i := range result.Hardware.GPUs {
g := &result.Hardware.GPUs[i]
if deviceID != "" && !strings.EqualFold(strings.TrimSpace(g.Slot), deviceID) {
continue
}
if g.Temperature == 0 && primaryTemp > 0 {
g.Temperature = int(primaryTemp)
}
if g.MemTemperature == 0 && memTemp > 0 {
g.MemTemperature = int(memTemp)
}
if g.Power == 0 && power > 0 {
g.Power = power
}
}
}
func parseGenericCIMSensor(props map[string]string, result *models.AnalysisResult) {
name := strings.TrimSpace(firstNonEmpty(props["elementname"], props["devicedescription"], props["deviceid"]))
if name == "" {
return
}
currentRaw := strings.TrimSpace(props["currentreading"])
if currentRaw == "" {
return
}
value := parseScaledFloat(currentRaw, props["unitmodifier"])
unit, sensorType := mapCIMSensorUnitAndType(props["baseunits"], props["sensortype"], name)
status := normalizeCIMHealthStatus(firstNonEmpty(props["primarystatus"], props["healthstate"]))
result.Sensors = append(result.Sensors, models.SensorReading{
Name: name,
Type: sensorType,
Value: value,
Unit: unit,
RawValue: currentRaw,
Status: status,
})
}
func propertyMap(props []cimProperty) map[string]string {
out := make(map[string]string, len(props))
for _, p := range props {
name := normalizeKey(p.Name)
if name == "" {
continue
}
value := strings.TrimSpace(p.DisplayValue)
if value == "" {
value = strings.TrimSpace(p.Value)
}
if value == "" && len(p.ArrayDisplay) > 0 {
value = strings.Join(nonEmptyStrings(p.ArrayDisplay), ", ")
}
if value == "" && len(p.ArrayValues) > 0 {
value = strings.Join(nonEmptyStrings(p.ArrayValues), ", ")
}
out[name] = strings.TrimSpace(value)
}
return out
}
func findBySuffix(files []parser.ExtractedFile, suffixes ...string) *parser.ExtractedFile {
for i := range files {
path := strings.ToLower(strings.TrimSpace(files[i].Path))
for _, suffix := range suffixes {
if strings.HasSuffix(path, strings.ToLower(suffix)) {
return &files[i]
}
}
}
return nil
}
func expandNestedZipFiles(files []parser.ExtractedFile) []parser.ExtractedFile {
out := make([]parser.ExtractedFile, 0, len(files)+32)
out = append(out, files...)
for _, f := range files {
if !strings.HasSuffix(strings.ToLower(strings.TrimSpace(f.Path)), ".zip") {
continue
}
zr, err := zip.NewReader(bytes.NewReader(f.Content), int64(len(f.Content)))
if err != nil {
continue
}
for _, zf := range zr.File {
if zf.FileInfo().IsDir() {
continue
}
if zf.FileInfo().Size() > 10*1024*1024 {
continue
}
rc, err := zf.Open()
if err != nil {
continue
}
content, err := io.ReadAll(rc)
rc.Close()
if err != nil {
continue
}
out = append(out, parser.ExtractedFile{
Path: zf.Name,
Content: content,
ModTime: zf.Modified,
})
}
}
return out
}
func addFirmware(result *models.AnalysisResult, name, version, desc string) {
name = strings.TrimSpace(name)
version = strings.TrimSpace(version)
if name == "" || version == "" {
return
}
result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{
DeviceName: name,
Version: version,
Description: strings.TrimSpace(desc),
})
}
func parseDellTSRTime(v string) time.Time {
v = strings.TrimSpace(v)
if v == "" {
return time.Time{}
}
layouts := []string{
"2006-01-02 15:04:05.000-0700",
"2006-01-02 15:04:05-0700",
}
for _, layout := range layouts {
if t, err := time.Parse(layout, v); err == nil {
return t
}
}
return time.Time{}
}
func parseDellEventTime(v string) time.Time {
v = strings.TrimSpace(v)
if v == "" {
return time.Time{}
}
layouts := []string{
"2006-01-02T15:04:05-0700",
time.RFC3339,
}
for _, layout := range layouts {
if t, err := time.Parse(layout, v); err == nil {
return t
}
}
return time.Time{}
}
func mapDellSeverity(v string) models.Severity {
switch strings.ToLower(strings.TrimSpace(v)) {
case "critical":
return models.SeverityCritical
case "warning":
return models.SeverityWarning
default:
return models.SeverityInfo
}
}
func normalizeStatus(v string) string {
s := strings.ToLower(strings.TrimSpace(v))
switch {
case s == "", s == "unknown", s == "not applicable":
return ""
case strings.Contains(s, "ok"), strings.Contains(s, "online"), strings.Contains(s, "enabled"), strings.Contains(s, "presence"):
return "ok"
case strings.Contains(s, "warn"), strings.Contains(s, "degrad"):
return "warning"
case strings.Contains(s, "critical"), strings.Contains(s, "fail"), strings.Contains(s, "error"):
return "critical"
case strings.Contains(s, "absent"), strings.Contains(s, "missing"):
return "absent"
default:
return s
}
}
func inferStorageType(mediaType, protocol string) string {
s := strings.ToLower(strings.TrimSpace(mediaType + " " + protocol))
switch {
case strings.Contains(s, "nvme"):
return "nvme"
case strings.Contains(s, "solid state"), strings.Contains(s, "ssd"):
return "ssd"
case strings.Contains(s, "hard disk"), strings.Contains(s, "hdd"), strings.Contains(s, "sata"), strings.Contains(s, "sas"):
return "disk"
default:
return "disk"
}
}
func normalizeRAIDLevel(v string) string {
s := strings.TrimSpace(v)
if s == "" {
return ""
}
u := strings.ToUpper(s)
if strings.HasPrefix(u, "RAID") {
return u
}
if strings.HasPrefix(strings.ToLower(s), "raid") {
return strings.ToUpper(s)
}
if strings.Contains(u, "RAID") {
return strings.ToUpper(s)
}
return s
}
func parseSocketFromFQDD(v string) int {
re := regexp.MustCompile(`(?i)socket\.(\d+)`)
m := re.FindStringSubmatch(strings.TrimSpace(v))
if len(m) != 2 {
return 0
}
return parseIntLoose(m[1])
}
func extractDiskSlotFromFQDD(v string) string {
re := regexp.MustCompile(`(?i)disk\.bay\.(\d+)`)
m := re.FindStringSubmatch(strings.TrimSpace(v))
if len(m) == 2 {
return m[1]
}
return ""
}
func extractControllerFromVolumeFQDD(v string) string {
parts := strings.Split(strings.TrimSpace(v), ":")
if len(parts) < 2 {
return ""
}
return strings.TrimSpace(parts[1])
}
func normalizePSUSlot(v string) string {
re := regexp.MustCompile(`(?i)psu\.slot\.(\d+)`)
m := re.FindStringSubmatch(strings.TrimSpace(v))
if len(m) == 2 {
return "PSU" + m[1]
}
return strings.TrimSpace(v)
}
func inferPortCountFromFQDD(v string) int {
re := regexp.MustCompile(`(?i)-(\d+)-`)
m := re.FindStringSubmatch(v)
if len(m) == 2 {
return parseIntLoose(m[1])
}
return 0
}
func formatBDF(bus, dev, fn string) string {
b := parseIntLoose(bus)
d := parseIntLoose(dev)
f := parseIntLoose(fn)
if b == 0 && d == 0 && f == 0 {
return ""
}
return fmt.Sprintf("%02x:%02x.%x", b, d, f)
}
func parseBytesToGB(v string) int {
n := parseInt64Loose(v)
if n <= 0 {
return 0
}
return int(n / 1_000_000_000)
}
var firstNumberRE = regexp.MustCompile(`[-+]?[0-9]+`)
var firstFloatRE = regexp.MustCompile(`[-+]?[0-9]+(?:\.[0-9]+)?`)
var nicMACInModelRE = regexp.MustCompile(`\s+-\s+([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$`)
func parseIntLoose(v string) int {
v = strings.TrimSpace(v)
if v == "" {
return 0
}
if i := parseHexOrDec(v); i != 0 {
return i
}
num := firstNumberRE.FindString(v)
if num == "" {
return 0
}
n, _ := strconv.Atoi(num)
return n
}
func parseInt64Loose(v string) int64 {
v = strings.TrimSpace(v)
if v == "" {
return 0
}
num := firstNumberRE.FindString(v)
if num == "" {
return 0
}
n, _ := strconv.ParseInt(num, 10, 64)
return n
}
func parseFloatLoose(v string) float64 {
v = strings.TrimSpace(v)
if v == "" {
return 0
}
num := firstFloatRE.FindString(v)
if num == "" {
return 0
}
f, _ := strconv.ParseFloat(num, 64)
return f
}
func parseScaledFloat(valueRaw, unitModRaw string) float64 {
base := parseFloatLoose(valueRaw)
if base == 0 {
return 0
}
mod := parseIntLoose(unitModRaw)
if mod == 0 {
return base
}
scale := 1.0
if mod > 0 {
for i := 0; i < mod; i++ {
scale *= 10
}
return base * scale
}
for i := 0; i < -mod; i++ {
scale *= 10
}
return base / scale
}
func parseHexOrDec(v string) int {
v = strings.TrimSpace(strings.ToLower(v))
if v == "" {
return 0
}
if strings.HasPrefix(v, "0x") {
n, _ := strconv.ParseInt(strings.TrimPrefix(v, "0x"), 16, 32)
return int(n)
}
if n, err := strconv.ParseInt(v, 10, 32); err == nil {
return int(n)
}
if n, err := strconv.ParseInt(v, 16, 32); err == nil {
return int(n)
}
return 0
}
func normalizeKey(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
}
}
return b.String()
}
func mapCIMSensorUnitAndType(baseUnits, sensorTypeRaw, name string) (string, string) {
switch parseIntLoose(baseUnits) {
case 2:
return "C", "temperature"
case 5:
return "V", "voltage"
case 6:
return "A", "current"
case 7:
return "W", "power"
case 19:
return "RPM", "fan_speed"
case 65:
return "%", "utilization"
}
lower := strings.ToLower(name)
switch {
case strings.Contains(lower, "temp"):
return "C", "temperature"
case strings.Contains(lower, "fan"):
return "RPM", "fan_speed"
case strings.Contains(lower, "volt"):
return "V", "voltage"
case strings.Contains(lower, "current"):
return "A", "current"
case strings.Contains(lower, "power"), strings.Contains(lower, "pwr"):
return "W", "power"
}
switch parseIntLoose(sensorTypeRaw) {
case 2:
return "C", "temperature"
case 3:
return "V", "voltage"
case 5:
return "RPM", "fan_speed"
case 13:
return "", "power"
}
return "", "sensor"
}
func normalizeCIMHealthStatus(v string) string {
switch parseIntLoose(v) {
case 1, 2, 5, 15:
return "ok"
case 3, 4, 10:
return "warning"
case 6, 7, 8, 9, 11, 12, 13, 14, 16, 17, 18:
return "critical"
default:
return normalizeStatus(v)
}
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
v = strings.TrimSpace(v)
if v != "" {
return v
}
}
return ""
}
func setIfEmpty(dst *string, value string) {
if strings.TrimSpace(*dst) != "" {
return
}
*dst = strings.TrimSpace(value)
}
func nonEmptyStrings(values []string) []string {
out := make([]string, 0, len(values))
for _, v := range values {
v = strings.TrimSpace(v)
if v != "" {
out = append(out, v)
}
}
return out
}
func dedupeStorage(items []models.Storage) []models.Storage {
out := make([]models.Storage, 0, len(items))
indexByKey := make(map[string]int)
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, item.Slot, item.Model)))
if key == "" {
continue
}
if idx, ok := indexByKey[key]; ok {
mergeStorage(&out[idx], item)
continue
}
indexByKey[key] = len(out)
out = append(out, item)
}
return out
}
func mergeStorage(dst *models.Storage, src models.Storage) {
setIfEmpty(&dst.Slot, src.Slot)
setIfEmpty(&dst.Type, src.Type)
setIfEmpty(&dst.Model, src.Model)
if dst.SizeGB == 0 {
dst.SizeGB = src.SizeGB
}
setIfEmpty(&dst.SerialNumber, src.SerialNumber)
setIfEmpty(&dst.Manufacturer, src.Manufacturer)
setIfEmpty(&dst.Firmware, src.Firmware)
setIfEmpty(&dst.Interface, src.Interface)
if src.Present {
dst.Present = true
}
setIfEmpty(&dst.Location, src.Location)
setIfEmpty(&dst.Status, src.Status)
dst.Details = mergeDellDetails(dst.Details, src.Details)
}
func dedupeVolumes(items []models.StorageVolume) []models.StorageVolume {
out := make([]models.StorageVolume, 0, len(items))
seen := make(map[string]int)
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.ID, item.Name)))
if key == "" {
continue
}
if idx, ok := seen[key]; ok {
mergeVolume(&out[idx], item)
continue
}
seen[key] = len(out)
out = append(out, item)
}
return out
}
func mergeVolume(dst *models.StorageVolume, src models.StorageVolume) {
setIfEmpty(&dst.ID, src.ID)
setIfEmpty(&dst.Name, src.Name)
setIfEmpty(&dst.Controller, src.Controller)
setIfEmpty(&dst.RAIDLevel, src.RAIDLevel)
if dst.SizeGB == 0 {
dst.SizeGB = src.SizeGB
}
if dst.CapacityBytes == 0 {
dst.CapacityBytes = src.CapacityBytes
}
setIfEmpty(&dst.Status, src.Status)
}
func dedupePSU(items []models.PSU) []models.PSU {
out := make([]models.PSU, 0, len(items))
seen := make(map[string]int)
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, item.Slot, item.Model)))
if key == "" {
continue
}
if idx, ok := seen[key]; ok {
mergePSU(&out[idx], item)
continue
}
seen[key] = len(out)
out = append(out, item)
}
return out
}
func mergePSU(dst *models.PSU, src models.PSU) {
setIfEmpty(&dst.Slot, src.Slot)
if src.Present {
dst.Present = true
}
setIfEmpty(&dst.Model, src.Model)
setIfEmpty(&dst.Description, src.Description)
setIfEmpty(&dst.Vendor, src.Vendor)
if dst.WattageW == 0 {
dst.WattageW = src.WattageW
}
setIfEmpty(&dst.SerialNumber, src.SerialNumber)
setIfEmpty(&dst.PartNumber, src.PartNumber)
setIfEmpty(&dst.Firmware, src.Firmware)
setIfEmpty(&dst.Status, src.Status)
if dst.InputVoltage == 0 {
dst.InputVoltage = src.InputVoltage
}
setIfEmpty(&dst.InputType, src.InputType)
dst.Details = mergeDellDetails(dst.Details, src.Details)
}
func mergeDellDetails(primary, secondary map[string]any) map[string]any {
if len(secondary) == 0 {
return primary
}
if primary == nil {
primary = make(map[string]any, len(secondary))
}
for key, value := range secondary {
if _, ok := primary[key]; !ok {
primary[key] = value
}
}
return primary
}
func dedupeNetworkAdapters(items []models.NetworkAdapter) []models.NetworkAdapter {
out := make([]models.NetworkAdapter, 0, len(items))
seen := make(map[string]int)
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, strings.Join(item.MACAddresses, ","), item.Slot)))
if key == "" {
continue
}
if idx, ok := seen[key]; ok {
mergeNetworkAdapter(&out[idx], item)
continue
}
seen[key] = len(out)
out = append(out, item)
}
return out
}
func mergeNetworkAdapter(dst *models.NetworkAdapter, src models.NetworkAdapter) {
setIfEmpty(&dst.Slot, src.Slot)
setIfEmpty(&dst.Location, src.Location)
if src.Present {
dst.Present = true
}
setIfEmpty(&dst.Model, src.Model)
setIfEmpty(&dst.Description, src.Description)
setIfEmpty(&dst.Vendor, src.Vendor)
if dst.VendorID == 0 {
dst.VendorID = src.VendorID
}
if dst.DeviceID == 0 {
dst.DeviceID = src.DeviceID
}
setIfEmpty(&dst.SerialNumber, src.SerialNumber)
setIfEmpty(&dst.PartNumber, src.PartNumber)
setIfEmpty(&dst.Firmware, src.Firmware)
if dst.PortCount == 0 {
dst.PortCount = src.PortCount
}
setIfEmpty(&dst.Status, src.Status)
for _, mac := range src.MACAddresses {
mac = strings.TrimSpace(mac)
if mac == "" {
continue
}
if !containsStringFold(dst.MACAddresses, mac) {
dst.MACAddresses = append(dst.MACAddresses, mac)
}
}
}
func nicCardsFromAdapters(items []models.NetworkAdapter) []models.NIC {
out := make([]models.NIC, 0, len(items))
for _, item := range items {
out = append(out, models.NIC{
Name: firstNonEmpty(item.Slot, item.Location, "NIC"),
Model: item.Model,
Description: item.Description,
MACAddress: firstNonEmpty(item.MACAddresses...),
SerialNumber: item.SerialNumber,
})
}
return out
}
// removePCIeOverlappingWithGPUs drops PCIe entries that duplicate a GPU already
// captured from DCIM_VideoView. Dell TSR lists GPUs in both DCIM_VideoView and
// DCIM_PCIDeviceView; the VideoView record is authoritative (has serial, firmware,
// temperature) so the PCIe duplicate must be removed.
func removePCIeOverlappingWithGPUs(pcie []models.PCIeDevice, gpus []models.GPU) []models.PCIeDevice {
if len(gpus) == 0 {
return pcie
}
gpuSlots := make(map[string]struct{}, len(gpus))
gpuBDFs := make(map[string]struct{}, len(gpus))
for _, g := range gpus {
if s := strings.ToLower(strings.TrimSpace(g.Slot)); s != "" {
gpuSlots[s] = struct{}{}
}
if b := strings.ToLower(strings.TrimSpace(g.BDF)); b != "" {
gpuBDFs[b] = struct{}{}
}
}
out := make([]models.PCIeDevice, 0, len(pcie))
for _, p := range pcie {
slot := strings.ToLower(strings.TrimSpace(p.Slot))
bdf := strings.ToLower(strings.TrimSpace(p.BDF))
if _, ok := gpuSlots[slot]; ok && slot != "" {
continue
}
if _, ok := gpuBDFs[bdf]; ok && bdf != "" {
continue
}
out = append(out, p)
}
return out
}
func dedupePCIe(items []models.PCIeDevice) []models.PCIeDevice {
out := make([]models.PCIeDevice, 0, len(items))
seen := make(map[string]int)
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.BDF, item.Slot, item.Description)))
if key == "" {
continue
}
if idx, ok := seen[key]; ok {
mergePCIe(&out[idx], item)
continue
}
seen[key] = len(out)
out = append(out, item)
}
return out
}
func mergePCIe(dst *models.PCIeDevice, src models.PCIeDevice) {
setIfEmpty(&dst.Slot, src.Slot)
setIfEmpty(&dst.Description, src.Description)
if dst.VendorID == 0 {
dst.VendorID = src.VendorID
}
if dst.DeviceID == 0 {
dst.DeviceID = src.DeviceID
}
setIfEmpty(&dst.BDF, src.BDF)
setIfEmpty(&dst.DeviceClass, src.DeviceClass)
setIfEmpty(&dst.Manufacturer, src.Manufacturer)
setIfEmpty(&dst.Status, src.Status)
}
func dedupeCPU(items []models.CPU) []models.CPU {
out := make([]models.CPU, 0, len(items))
seen := make(map[string]int)
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(firstNonEmpty(fmt.Sprintf("%d", item.Socket), item.PPIN, item.Model)))
if key == "" {
continue
}
if idx, ok := seen[key]; ok {
mergeCPU(&out[idx], item)
continue
}
seen[key] = len(out)
out = append(out, item)
}
return out
}
func dedupeGPU(items []models.GPU) []models.GPU {
out := make([]models.GPU, 0, len(items))
seen := make(map[string]int)
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, item.UUID, item.BDF, item.Slot, item.Model)))
if key == "" {
continue
}
if idx, ok := seen[key]; ok {
mergeGPU(&out[idx], item)
continue
}
seen[key] = len(out)
out = append(out, item)
}
return out
}
func mergeGPU(dst *models.GPU, src models.GPU) {
setIfEmpty(&dst.Slot, src.Slot)
setIfEmpty(&dst.Location, src.Location)
setIfEmpty(&dst.Model, src.Model)
setIfEmpty(&dst.Description, src.Description)
setIfEmpty(&dst.Manufacturer, src.Manufacturer)
if dst.VendorID == 0 {
dst.VendorID = src.VendorID
}
if dst.DeviceID == 0 {
dst.DeviceID = src.DeviceID
}
setIfEmpty(&dst.BDF, src.BDF)
setIfEmpty(&dst.UUID, src.UUID)
setIfEmpty(&dst.SerialNumber, src.SerialNumber)
setIfEmpty(&dst.PartNumber, src.PartNumber)
setIfEmpty(&dst.Firmware, src.Firmware)
setIfEmpty(&dst.Status, src.Status)
}
func mergeCPU(dst *models.CPU, src models.CPU) {
if dst.Socket == 0 {
dst.Socket = src.Socket
}
setIfEmpty(&dst.Model, src.Model)
setIfEmpty(&dst.Description, src.Description)
if dst.Cores == 0 {
dst.Cores = src.Cores
}
if dst.Threads == 0 {
dst.Threads = src.Threads
}
if dst.FrequencyMHz == 0 {
dst.FrequencyMHz = src.FrequencyMHz
}
if dst.MaxFreqMHz == 0 {
dst.MaxFreqMHz = src.MaxFreqMHz
}
setIfEmpty(&dst.PPIN, src.PPIN)
setIfEmpty(&dst.Status, src.Status)
}
func dedupeDIMM(items []models.MemoryDIMM) []models.MemoryDIMM {
out := make([]models.MemoryDIMM, 0, len(items))
seen := make(map[string]int)
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, item.Slot)))
if key == "" {
continue
}
if idx, ok := seen[key]; ok {
mergeDIMM(&out[idx], item)
continue
}
seen[key] = len(out)
out = append(out, item)
}
return out
}
func mergeDIMM(dst *models.MemoryDIMM, src models.MemoryDIMM) {
setIfEmpty(&dst.Slot, src.Slot)
setIfEmpty(&dst.Location, src.Location)
if src.Present {
dst.Present = true
}
if dst.SizeMB == 0 {
dst.SizeMB = src.SizeMB
}
setIfEmpty(&dst.Type, src.Type)
setIfEmpty(&dst.Technology, src.Technology)
if dst.MaxSpeedMHz == 0 {
dst.MaxSpeedMHz = src.MaxSpeedMHz
}
if dst.CurrentSpeedMHz == 0 {
dst.CurrentSpeedMHz = src.CurrentSpeedMHz
}
setIfEmpty(&dst.Manufacturer, src.Manufacturer)
setIfEmpty(&dst.SerialNumber, src.SerialNumber)
setIfEmpty(&dst.PartNumber, src.PartNumber)
setIfEmpty(&dst.Status, src.Status)
}
func dedupeFirmware(items []models.FirmwareInfo) []models.FirmwareInfo {
out := make([]models.FirmwareInfo, 0, len(items))
seen := make(map[string]struct{})
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(item.DeviceName + "|" + item.Version + "|" + item.Description))
if key == "" || strings.TrimSpace(item.Version) == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, item)
}
sort.SliceStable(out, func(i, j int) bool {
return strings.ToLower(out[i].DeviceName) < strings.ToLower(out[j].DeviceName)
})
return out
}
func dedupeSensors(items []models.SensorReading) []models.SensorReading {
out := make([]models.SensorReading, 0, len(items))
seen := make(map[string]int)
for _, item := range items {
name := strings.TrimSpace(item.Name)
if name == "" {
continue
}
key := strings.ToLower(strings.TrimSpace(item.Type + "|" + name))
if idx, ok := seen[key]; ok {
mergeSensor(&out[idx], item)
continue
}
seen[key] = len(out)
out = append(out, item)
}
return out
}
func mergeSensor(dst *models.SensorReading, src models.SensorReading) {
setIfEmpty(&dst.Name, src.Name)
setIfEmpty(&dst.Type, src.Type)
if dst.Value == 0 && src.Value != 0 {
dst.Value = src.Value
}
setIfEmpty(&dst.Unit, src.Unit)
setIfEmpty(&dst.RawValue, src.RawValue)
setIfEmpty(&dst.Status, src.Status)
}
func containsStringFold(items []string, value string) bool {
for _, item := range items {
if strings.EqualFold(strings.TrimSpace(item), strings.TrimSpace(value)) {
return true
}
}
return false
}