1574 lines
46 KiB
Go
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
|
|
}
|