1383 lines
38 KiB
Go
1383 lines
38 KiB
Go
package exporter
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
)
|
|
|
|
var cpuMicrocodeFirmwareRegex = regexp.MustCompile(`(?i)^cpu\d+\s+microcode$`)
|
|
|
|
// ConvertToReanimator converts AnalysisResult to Reanimator export format
|
|
func ConvertToReanimator(result *models.AnalysisResult) (*ReanimatorExport, error) {
|
|
if result == nil {
|
|
return nil, fmt.Errorf("no data available for export")
|
|
}
|
|
|
|
if result.Hardware == nil {
|
|
return nil, fmt.Errorf("no hardware data available for export")
|
|
}
|
|
|
|
if result.Hardware.BoardInfo.SerialNumber == "" {
|
|
return nil, fmt.Errorf("board serial_number is required for Reanimator export")
|
|
}
|
|
|
|
// Determine target host (optional field)
|
|
targetHost := inferTargetHost(result.TargetHost, result.Filename)
|
|
|
|
collectedAt := formatRFC3339(result.CollectedAt)
|
|
devices := canonicalDevicesForExport(result.Hardware)
|
|
|
|
export := &ReanimatorExport{
|
|
Filename: result.Filename,
|
|
SourceType: normalizeSourceType(result.SourceType),
|
|
Protocol: normalizeProtocol(result.Protocol),
|
|
TargetHost: targetHost,
|
|
CollectedAt: collectedAt,
|
|
Hardware: ReanimatorHardware{
|
|
Board: convertBoard(result.Hardware.BoardInfo),
|
|
Firmware: dedupeFirmware(convertFirmware(result.Hardware.Firmware)),
|
|
CPUs: convertCPUsFromDevices(devices, collectedAt),
|
|
Memory: convertMemoryFromDevices(devices, collectedAt),
|
|
Storage: convertStorageFromDevices(devices, collectedAt),
|
|
PCIeDevices: convertPCIeFromDevices(devices, collectedAt),
|
|
PowerSupplies: convertPSUsFromDevices(devices, collectedAt),
|
|
},
|
|
}
|
|
|
|
return export, nil
|
|
}
|
|
|
|
// formatRFC3339 formats time in RFC3339 format, returns current time if zero
|
|
func formatRFC3339(t time.Time) string {
|
|
if t.IsZero() {
|
|
return time.Now().UTC().Format(time.RFC3339)
|
|
}
|
|
return t.UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
// convertBoard converts BoardInfo to Reanimator format
|
|
func convertBoard(board models.BoardInfo) ReanimatorBoard {
|
|
return ReanimatorBoard{
|
|
Manufacturer: normalizeNullableString(board.Manufacturer),
|
|
ProductName: normalizeNullableString(board.ProductName),
|
|
SerialNumber: board.SerialNumber,
|
|
PartNumber: board.PartNumber,
|
|
UUID: board.UUID,
|
|
}
|
|
}
|
|
|
|
func canonicalDevicesForExport(hw *models.HardwareConfig) []models.HardwareDevice {
|
|
if hw == nil {
|
|
return nil
|
|
}
|
|
if len(hw.Devices) > 0 {
|
|
return hw.Devices
|
|
}
|
|
hw.Devices = buildDevicesFromLegacy(hw)
|
|
return hw.Devices
|
|
}
|
|
|
|
func buildDevicesFromLegacy(hw *models.HardwareConfig) []models.HardwareDevice {
|
|
if hw == nil {
|
|
return nil
|
|
}
|
|
all := make([]models.HardwareDevice, 0, len(hw.CPUs)+len(hw.Memory)+len(hw.Storage)+len(hw.PCIeDevices)+len(hw.GPUs)+len(hw.NetworkAdapters)+len(hw.PowerSupply))
|
|
appendDevice := func(d models.HardwareDevice) {
|
|
all = append(all, d)
|
|
}
|
|
|
|
for _, cpu := range hw.CPUs {
|
|
appendDevice(models.HardwareDevice{
|
|
Kind: models.DeviceKindCPU,
|
|
Slot: fmt.Sprintf("CPU%d", cpu.Socket),
|
|
Model: cpu.Model,
|
|
SerialNumber: cpu.SerialNumber,
|
|
Cores: cpu.Cores,
|
|
Threads: cpu.Threads,
|
|
FrequencyMHz: cpu.FrequencyMHz,
|
|
MaxFreqMHz: cpu.MaxFreqMHz,
|
|
Status: cpu.Status,
|
|
StatusCheckedAt: cpu.StatusCheckedAt,
|
|
StatusChangedAt: cpu.StatusChangedAt,
|
|
StatusAtCollect: cpu.StatusAtCollect,
|
|
StatusHistory: cpu.StatusHistory,
|
|
ErrorDescription: cpu.ErrorDescription,
|
|
Details: map[string]any{
|
|
"socket": cpu.Socket,
|
|
},
|
|
})
|
|
}
|
|
for _, mem := range hw.Memory {
|
|
if !mem.Present || mem.SizeMB == 0 {
|
|
continue
|
|
}
|
|
present := mem.Present
|
|
appendDevice(models.HardwareDevice{
|
|
Kind: models.DeviceKindMemory,
|
|
Slot: mem.Slot,
|
|
Location: mem.Location,
|
|
Manufacturer: mem.Manufacturer,
|
|
SerialNumber: mem.SerialNumber,
|
|
PartNumber: mem.PartNumber,
|
|
Type: mem.Type,
|
|
Present: &present,
|
|
SizeMB: mem.SizeMB,
|
|
Status: mem.Status,
|
|
StatusCheckedAt: mem.StatusCheckedAt,
|
|
StatusChangedAt: mem.StatusChangedAt,
|
|
StatusAtCollect: mem.StatusAtCollect,
|
|
StatusHistory: mem.StatusHistory,
|
|
ErrorDescription: mem.ErrorDescription,
|
|
Details: map[string]any{
|
|
"max_speed_mhz": mem.MaxSpeedMHz,
|
|
"current_speed_mhz": mem.CurrentSpeedMHz,
|
|
},
|
|
})
|
|
}
|
|
for _, stor := range hw.Storage {
|
|
if !stor.Present {
|
|
continue
|
|
}
|
|
present := stor.Present
|
|
appendDevice(models.HardwareDevice{
|
|
Kind: models.DeviceKindStorage,
|
|
Slot: stor.Slot,
|
|
Model: stor.Model,
|
|
Manufacturer: stor.Manufacturer,
|
|
SerialNumber: stor.SerialNumber,
|
|
Firmware: stor.Firmware,
|
|
Type: stor.Type,
|
|
Interface: stor.Interface,
|
|
Present: &present,
|
|
SizeGB: stor.SizeGB,
|
|
Status: stor.Status,
|
|
StatusCheckedAt: stor.StatusCheckedAt,
|
|
StatusChangedAt: stor.StatusChangedAt,
|
|
StatusAtCollect: stor.StatusAtCollect,
|
|
StatusHistory: stor.StatusHistory,
|
|
ErrorDescription: stor.ErrorDescription,
|
|
})
|
|
}
|
|
for _, pcie := range hw.PCIeDevices {
|
|
appendDevice(models.HardwareDevice{
|
|
Kind: models.DeviceKindPCIe,
|
|
Slot: pcie.Slot,
|
|
BDF: pcie.BDF,
|
|
DeviceClass: pcie.DeviceClass,
|
|
VendorID: pcie.VendorID,
|
|
DeviceID: pcie.DeviceID,
|
|
Model: pcie.PartNumber,
|
|
PartNumber: pcie.PartNumber,
|
|
Manufacturer: pcie.Manufacturer,
|
|
SerialNumber: pcie.SerialNumber,
|
|
LinkWidth: pcie.LinkWidth,
|
|
LinkSpeed: pcie.LinkSpeed,
|
|
MaxLinkWidth: pcie.MaxLinkWidth,
|
|
MaxLinkSpeed: pcie.MaxLinkSpeed,
|
|
Status: pcie.Status,
|
|
StatusCheckedAt: pcie.StatusCheckedAt,
|
|
StatusChangedAt: pcie.StatusChangedAt,
|
|
StatusAtCollect: pcie.StatusAtCollect,
|
|
StatusHistory: pcie.StatusHistory,
|
|
ErrorDescription: pcie.ErrorDescription,
|
|
})
|
|
}
|
|
for _, gpu := range hw.GPUs {
|
|
appendDevice(models.HardwareDevice{
|
|
Kind: models.DeviceKindGPU,
|
|
Slot: gpu.Slot,
|
|
BDF: gpu.BDF,
|
|
DeviceClass: "DisplayController",
|
|
VendorID: gpu.VendorID,
|
|
DeviceID: gpu.DeviceID,
|
|
Model: gpu.Model,
|
|
PartNumber: gpu.PartNumber,
|
|
Manufacturer: gpu.Manufacturer,
|
|
SerialNumber: gpu.SerialNumber,
|
|
Firmware: gpu.Firmware,
|
|
LinkWidth: gpu.CurrentLinkWidth,
|
|
LinkSpeed: gpu.CurrentLinkSpeed,
|
|
MaxLinkWidth: gpu.MaxLinkWidth,
|
|
MaxLinkSpeed: gpu.MaxLinkSpeed,
|
|
Status: gpu.Status,
|
|
StatusCheckedAt: gpu.StatusCheckedAt,
|
|
StatusChangedAt: gpu.StatusChangedAt,
|
|
StatusAtCollect: gpu.StatusAtCollect,
|
|
StatusHistory: gpu.StatusHistory,
|
|
ErrorDescription: gpu.ErrorDescription,
|
|
})
|
|
}
|
|
for _, nic := range hw.NetworkAdapters {
|
|
if !nic.Present {
|
|
continue
|
|
}
|
|
present := nic.Present
|
|
appendDevice(models.HardwareDevice{
|
|
Kind: models.DeviceKindNetwork,
|
|
Slot: nic.Slot,
|
|
Location: nic.Location,
|
|
VendorID: nic.VendorID,
|
|
DeviceID: nic.DeviceID,
|
|
Model: nic.Model,
|
|
PartNumber: nic.PartNumber,
|
|
Manufacturer: nic.Vendor,
|
|
SerialNumber: nic.SerialNumber,
|
|
Firmware: nic.Firmware,
|
|
PortCount: nic.PortCount,
|
|
PortType: nic.PortType,
|
|
MACAddresses: nic.MACAddresses,
|
|
Present: &present,
|
|
Status: nic.Status,
|
|
StatusCheckedAt: nic.StatusCheckedAt,
|
|
StatusChangedAt: nic.StatusChangedAt,
|
|
StatusAtCollect: nic.StatusAtCollect,
|
|
StatusHistory: nic.StatusHistory,
|
|
ErrorDescription: nic.ErrorDescription,
|
|
})
|
|
}
|
|
for _, psu := range hw.PowerSupply {
|
|
present := psu.Present
|
|
appendDevice(models.HardwareDevice{
|
|
Kind: models.DeviceKindPSU,
|
|
Slot: psu.Slot,
|
|
Model: psu.Model,
|
|
PartNumber: psu.PartNumber,
|
|
Manufacturer: psu.Vendor,
|
|
SerialNumber: psu.SerialNumber,
|
|
Firmware: psu.Firmware,
|
|
Present: &present,
|
|
WattageW: psu.WattageW,
|
|
InputType: psu.InputType,
|
|
InputPowerW: psu.InputPowerW,
|
|
OutputPowerW: psu.OutputPowerW,
|
|
InputVoltage: psu.InputVoltage,
|
|
TemperatureC: psu.TemperatureC,
|
|
Status: psu.Status,
|
|
StatusCheckedAt: psu.StatusCheckedAt,
|
|
StatusChangedAt: psu.StatusChangedAt,
|
|
StatusAtCollect: psu.StatusAtCollect,
|
|
StatusHistory: psu.StatusHistory,
|
|
ErrorDescription: psu.ErrorDescription,
|
|
})
|
|
}
|
|
return dedupeCanonicalDevices(all)
|
|
}
|
|
|
|
func dedupeCanonicalDevices(items []models.HardwareDevice) []models.HardwareDevice {
|
|
type scored struct {
|
|
item models.HardwareDevice
|
|
score int
|
|
}
|
|
byKey := make(map[string]scored, len(items))
|
|
order := make([]string, 0, len(items))
|
|
noKey := make([]models.HardwareDevice, 0)
|
|
for _, item := range items {
|
|
key := canonicalKey(item)
|
|
if key == "" {
|
|
noKey = append(noKey, item)
|
|
continue
|
|
}
|
|
curr := scored{item: item, score: canonicalScore(item)}
|
|
prev, ok := byKey[key]
|
|
if !ok {
|
|
byKey[key] = curr
|
|
order = append(order, key)
|
|
continue
|
|
}
|
|
if curr.score > prev.score {
|
|
byKey[key] = curr
|
|
}
|
|
}
|
|
out := make([]models.HardwareDevice, 0, len(order)+len(noKey))
|
|
for _, key := range order {
|
|
out = append(out, byKey[key].item)
|
|
}
|
|
out = append(out, noKey...)
|
|
for i := range out {
|
|
out[i].ID = out[i].Kind + ":" + strconv.Itoa(i)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func canonicalKey(item models.HardwareDevice) string {
|
|
if sn := normalizedSerial(item.SerialNumber); sn != "" {
|
|
return "sn:" + strings.ToLower(sn)
|
|
}
|
|
if bdf := strings.ToLower(strings.TrimSpace(item.BDF)); bdf != "" {
|
|
return "bdf:" + bdf
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func canonicalScore(item models.HardwareDevice) int {
|
|
score := 0
|
|
if normalizedSerial(item.SerialNumber) != "" {
|
|
score += 6
|
|
}
|
|
if strings.TrimSpace(item.BDF) != "" {
|
|
score += 4
|
|
}
|
|
if strings.TrimSpace(item.Model) != "" {
|
|
score += 3
|
|
}
|
|
if strings.TrimSpace(item.Firmware) != "" {
|
|
score += 2
|
|
}
|
|
if strings.TrimSpace(item.Status) != "" {
|
|
score++
|
|
}
|
|
return score
|
|
}
|
|
|
|
// convertFirmware converts firmware information to Reanimator format
|
|
func convertFirmware(firmware []models.FirmwareInfo) []ReanimatorFirmware {
|
|
if len(firmware) == 0 {
|
|
return nil
|
|
}
|
|
|
|
result := make([]ReanimatorFirmware, 0, len(firmware))
|
|
for _, fw := range firmware {
|
|
if isDeviceBoundFirmwareName(fw.DeviceName) {
|
|
continue
|
|
}
|
|
result = append(result, ReanimatorFirmware{
|
|
DeviceName: fw.DeviceName,
|
|
Version: fw.Version,
|
|
})
|
|
}
|
|
if len(result) == 0 {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func convertCPUsFromDevices(devices []models.HardwareDevice, collectedAt string) []ReanimatorCPU {
|
|
result := make([]ReanimatorCPU, 0)
|
|
for _, d := range devices {
|
|
if d.Kind != models.DeviceKindCPU {
|
|
continue
|
|
}
|
|
socket := parseSocketFromSlot(d.Slot)
|
|
if v, ok := d.Details["socket"].(int); ok {
|
|
socket = v
|
|
}
|
|
cpuStatus := normalizeStatus(d.Status, false)
|
|
if strings.TrimSpace(d.Status) == "" {
|
|
cpuStatus = "Unknown"
|
|
}
|
|
meta := buildStatusMeta(cpuStatus, d.StatusCheckedAt, d.StatusChangedAt, d.StatusAtCollect, d.StatusHistory, d.ErrorDescription, collectedAt)
|
|
result = append(result, ReanimatorCPU{
|
|
Socket: socket,
|
|
Model: d.Model,
|
|
Cores: d.Cores,
|
|
Threads: d.Threads,
|
|
FrequencyMHz: d.FrequencyMHz,
|
|
MaxFrequencyMHz: d.MaxFreqMHz,
|
|
Manufacturer: inferCPUManufacturer(d.Model),
|
|
Status: cpuStatus,
|
|
StatusCheckedAt: meta.StatusCheckedAt,
|
|
StatusChangedAt: meta.StatusChangedAt,
|
|
StatusAtCollect: meta.StatusAtCollection,
|
|
StatusHistory: meta.StatusHistory,
|
|
ErrorDescription: meta.ErrorDescription,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func convertMemoryFromDevices(devices []models.HardwareDevice, collectedAt string) []ReanimatorMemory {
|
|
result := make([]ReanimatorMemory, 0)
|
|
for _, d := range devices {
|
|
if d.Kind != models.DeviceKindMemory {
|
|
continue
|
|
}
|
|
present := d.Present != nil && *d.Present
|
|
if !present || d.SizeMB == 0 {
|
|
continue
|
|
}
|
|
status := normalizeStatus(d.Status, true)
|
|
if strings.TrimSpace(d.Status) == "" {
|
|
if present {
|
|
status = "OK"
|
|
} else {
|
|
status = "Empty"
|
|
}
|
|
}
|
|
meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusAtCollect, d.StatusHistory, d.ErrorDescription, collectedAt)
|
|
result = append(result, ReanimatorMemory{
|
|
Slot: d.Slot,
|
|
Location: d.Location,
|
|
Present: present,
|
|
SizeMB: d.SizeMB,
|
|
Type: d.Type,
|
|
MaxSpeedMHz: intFromDetailMap(d.Details, "max_speed_mhz"),
|
|
CurrentSpeedMHz: intFromDetailMap(d.Details, "current_speed_mhz"),
|
|
Manufacturer: d.Manufacturer,
|
|
SerialNumber: d.SerialNumber,
|
|
PartNumber: d.PartNumber,
|
|
Status: status,
|
|
StatusCheckedAt: meta.StatusCheckedAt,
|
|
StatusChangedAt: meta.StatusChangedAt,
|
|
StatusAtCollect: meta.StatusAtCollection,
|
|
StatusHistory: meta.StatusHistory,
|
|
ErrorDescription: meta.ErrorDescription,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func convertStorageFromDevices(devices []models.HardwareDevice, collectedAt string) []ReanimatorStorage {
|
|
result := make([]ReanimatorStorage, 0)
|
|
for _, d := range devices {
|
|
if d.Kind != models.DeviceKindStorage {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(d.SerialNumber) == "" {
|
|
continue
|
|
}
|
|
present := d.Present == nil || *d.Present
|
|
status := inferStorageStatus(models.Storage{Present: present})
|
|
if strings.TrimSpace(d.Status) != "" {
|
|
status = normalizeStatus(d.Status, false)
|
|
}
|
|
meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusAtCollect, d.StatusHistory, d.ErrorDescription, collectedAt)
|
|
result = append(result, ReanimatorStorage{
|
|
Slot: d.Slot,
|
|
Type: d.Type,
|
|
Model: d.Model,
|
|
SizeGB: d.SizeGB,
|
|
SerialNumber: d.SerialNumber,
|
|
Manufacturer: d.Manufacturer,
|
|
Firmware: d.Firmware,
|
|
Interface: d.Interface,
|
|
Present: present,
|
|
Status: status,
|
|
StatusCheckedAt: meta.StatusCheckedAt,
|
|
StatusChangedAt: meta.StatusChangedAt,
|
|
StatusAtCollect: meta.StatusAtCollection,
|
|
StatusHistory: meta.StatusHistory,
|
|
ErrorDescription: meta.ErrorDescription,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func convertPCIeFromDevices(devices []models.HardwareDevice, collectedAt string) []ReanimatorPCIe {
|
|
result := make([]ReanimatorPCIe, 0)
|
|
for _, d := range devices {
|
|
if d.Kind != models.DeviceKindPCIe && d.Kind != models.DeviceKindGPU && d.Kind != models.DeviceKindNetwork {
|
|
continue
|
|
}
|
|
deviceClass := d.DeviceClass
|
|
if d.Kind == models.DeviceKindGPU && strings.TrimSpace(deviceClass) == "" {
|
|
deviceClass = "DisplayController"
|
|
}
|
|
model := d.Model
|
|
if model == "" {
|
|
model = d.PartNumber
|
|
}
|
|
status := normalizeStatus(d.Status, false)
|
|
meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusAtCollect, d.StatusHistory, d.ErrorDescription, collectedAt)
|
|
result = append(result, ReanimatorPCIe{
|
|
Slot: d.Slot,
|
|
VendorID: d.VendorID,
|
|
DeviceID: d.DeviceID,
|
|
BDF: d.BDF,
|
|
DeviceClass: deviceClass,
|
|
Manufacturer: d.Manufacturer,
|
|
Model: model,
|
|
LinkWidth: d.LinkWidth,
|
|
LinkSpeed: d.LinkSpeed,
|
|
MaxLinkWidth: d.MaxLinkWidth,
|
|
MaxLinkSpeed: d.MaxLinkSpeed,
|
|
SerialNumber: normalizedSerial(d.SerialNumber),
|
|
Firmware: d.Firmware,
|
|
Status: status,
|
|
StatusCheckedAt: meta.StatusCheckedAt,
|
|
StatusChangedAt: meta.StatusChangedAt,
|
|
StatusAtCollect: meta.StatusAtCollection,
|
|
StatusHistory: meta.StatusHistory,
|
|
ErrorDescription: meta.ErrorDescription,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func convertPSUsFromDevices(devices []models.HardwareDevice, collectedAt string) []ReanimatorPSU {
|
|
result := make([]ReanimatorPSU, 0)
|
|
for _, d := range devices {
|
|
if d.Kind != models.DeviceKindPSU {
|
|
continue
|
|
}
|
|
present := d.Present != nil && *d.Present
|
|
if !present || strings.TrimSpace(d.SerialNumber) == "" {
|
|
continue
|
|
}
|
|
status := normalizeStatus(d.Status, false)
|
|
meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusAtCollect, d.StatusHistory, d.ErrorDescription, collectedAt)
|
|
result = append(result, ReanimatorPSU{
|
|
Slot: d.Slot,
|
|
Present: present,
|
|
Model: d.Model,
|
|
Vendor: d.Manufacturer,
|
|
WattageW: d.WattageW,
|
|
SerialNumber: d.SerialNumber,
|
|
PartNumber: d.PartNumber,
|
|
Firmware: d.Firmware,
|
|
Status: status,
|
|
InputType: d.InputType,
|
|
InputPowerW: d.InputPowerW,
|
|
OutputPowerW: d.OutputPowerW,
|
|
InputVoltage: d.InputVoltage,
|
|
StatusCheckedAt: meta.StatusCheckedAt,
|
|
StatusChangedAt: meta.StatusChangedAt,
|
|
StatusAtCollect: meta.StatusAtCollection,
|
|
StatusHistory: meta.StatusHistory,
|
|
ErrorDescription: meta.ErrorDescription,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func isDeviceBoundFirmwareName(name string) bool {
|
|
n := strings.TrimSpace(strings.ToLower(name))
|
|
if n == "" {
|
|
return false
|
|
}
|
|
|
|
if strings.HasPrefix(n, "gpu ") ||
|
|
strings.HasPrefix(n, "nvswitch ") ||
|
|
strings.HasPrefix(n, "nic ") ||
|
|
strings.HasPrefix(n, "hdd ") ||
|
|
strings.HasPrefix(n, "ssd ") ||
|
|
strings.HasPrefix(n, "nvme ") ||
|
|
strings.HasPrefix(n, "psu") {
|
|
return true
|
|
}
|
|
|
|
return cpuMicrocodeFirmwareRegex.MatchString(strings.TrimSpace(name))
|
|
}
|
|
|
|
// convertCPUs converts CPU information to Reanimator format
|
|
func convertCPUs(cpus []models.CPU, collectedAt string) []ReanimatorCPU {
|
|
if len(cpus) == 0 {
|
|
return nil
|
|
}
|
|
|
|
result := make([]ReanimatorCPU, 0, len(cpus))
|
|
for _, cpu := range cpus {
|
|
manufacturer := inferCPUManufacturer(cpu.Model)
|
|
|
|
cpuStatus := normalizeStatus(cpu.Status, false)
|
|
if strings.TrimSpace(cpu.Status) == "" {
|
|
cpuStatus = "Unknown"
|
|
}
|
|
meta := buildStatusMeta(
|
|
cpuStatus,
|
|
cpu.StatusCheckedAt,
|
|
cpu.StatusChangedAt,
|
|
cpu.StatusAtCollect,
|
|
cpu.StatusHistory,
|
|
cpu.ErrorDescription,
|
|
collectedAt,
|
|
)
|
|
|
|
result = append(result, ReanimatorCPU{
|
|
Socket: cpu.Socket,
|
|
Model: cpu.Model,
|
|
Cores: cpu.Cores,
|
|
Threads: cpu.Threads,
|
|
FrequencyMHz: cpu.FrequencyMHz,
|
|
MaxFrequencyMHz: cpu.MaxFreqMHz,
|
|
Manufacturer: manufacturer,
|
|
Status: cpuStatus,
|
|
StatusCheckedAt: meta.StatusCheckedAt,
|
|
StatusChangedAt: meta.StatusChangedAt,
|
|
StatusAtCollect: meta.StatusAtCollection,
|
|
StatusHistory: meta.StatusHistory,
|
|
ErrorDescription: meta.ErrorDescription,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
// convertMemory converts memory modules to Reanimator format
|
|
func convertMemory(memory []models.MemoryDIMM, collectedAt string) []ReanimatorMemory {
|
|
if len(memory) == 0 {
|
|
return nil
|
|
}
|
|
|
|
result := make([]ReanimatorMemory, 0, len(memory))
|
|
for _, mem := range memory {
|
|
status := normalizeStatus(mem.Status, true)
|
|
if strings.TrimSpace(mem.Status) == "" {
|
|
if mem.Present {
|
|
status = "OK"
|
|
} else {
|
|
status = "Empty"
|
|
}
|
|
}
|
|
|
|
meta := buildStatusMeta(
|
|
status,
|
|
mem.StatusCheckedAt,
|
|
mem.StatusChangedAt,
|
|
mem.StatusAtCollect,
|
|
mem.StatusHistory,
|
|
mem.ErrorDescription,
|
|
collectedAt,
|
|
)
|
|
|
|
result = append(result, ReanimatorMemory{
|
|
Slot: mem.Slot,
|
|
Location: mem.Location,
|
|
Present: mem.Present,
|
|
SizeMB: mem.SizeMB,
|
|
Type: mem.Type,
|
|
MaxSpeedMHz: mem.MaxSpeedMHz,
|
|
CurrentSpeedMHz: mem.CurrentSpeedMHz,
|
|
Manufacturer: mem.Manufacturer,
|
|
SerialNumber: mem.SerialNumber,
|
|
PartNumber: mem.PartNumber,
|
|
Status: status,
|
|
StatusCheckedAt: meta.StatusCheckedAt,
|
|
StatusChangedAt: meta.StatusChangedAt,
|
|
StatusAtCollect: meta.StatusAtCollection,
|
|
StatusHistory: meta.StatusHistory,
|
|
ErrorDescription: meta.ErrorDescription,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
// convertStorage converts storage devices to Reanimator format
|
|
func convertStorage(storage []models.Storage, collectedAt string) []ReanimatorStorage {
|
|
if len(storage) == 0 {
|
|
return nil
|
|
}
|
|
|
|
result := make([]ReanimatorStorage, 0, len(storage))
|
|
for _, stor := range storage {
|
|
// Skip storage without serial number
|
|
if stor.SerialNumber == "" {
|
|
continue
|
|
}
|
|
|
|
status := inferStorageStatus(stor)
|
|
if strings.TrimSpace(stor.Status) != "" {
|
|
status = normalizeStatus(stor.Status, false)
|
|
}
|
|
meta := buildStatusMeta(
|
|
status,
|
|
stor.StatusCheckedAt,
|
|
stor.StatusChangedAt,
|
|
stor.StatusAtCollect,
|
|
stor.StatusHistory,
|
|
stor.ErrorDescription,
|
|
collectedAt,
|
|
)
|
|
|
|
result = append(result, ReanimatorStorage{
|
|
Slot: stor.Slot,
|
|
Type: stor.Type,
|
|
Model: stor.Model,
|
|
SizeGB: stor.SizeGB,
|
|
SerialNumber: stor.SerialNumber,
|
|
Manufacturer: stor.Manufacturer,
|
|
Firmware: stor.Firmware,
|
|
Interface: stor.Interface,
|
|
Present: stor.Present,
|
|
Status: status,
|
|
StatusCheckedAt: meta.StatusCheckedAt,
|
|
StatusChangedAt: meta.StatusChangedAt,
|
|
StatusAtCollect: meta.StatusAtCollection,
|
|
StatusHistory: meta.StatusHistory,
|
|
ErrorDescription: meta.ErrorDescription,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
// convertPCIeDevices converts PCIe devices, GPUs, and network adapters to Reanimator format
|
|
func convertPCIeDevices(hw *models.HardwareConfig, collectedAt string) []ReanimatorPCIe {
|
|
result := make([]ReanimatorPCIe, 0)
|
|
gpuSlots := make(map[string]struct{}, len(hw.GPUs))
|
|
nvswitchFirmwareBySlot := buildNVSwitchFirmwareBySlot(hw.Firmware)
|
|
for _, gpu := range hw.GPUs {
|
|
slot := strings.ToLower(strings.TrimSpace(gpu.Slot))
|
|
if slot != "" {
|
|
gpuSlots[slot] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Convert regular PCIe devices
|
|
for _, pcie := range hw.PCIeDevices {
|
|
slot := strings.ToLower(strings.TrimSpace(pcie.Slot))
|
|
if _, isDedicatedGPU := gpuSlots[slot]; isDedicatedGPU || isDisplayClass(pcie.DeviceClass) {
|
|
// Skip GPU-like PCIe entries to avoid duplicates:
|
|
// dedicated GPUs are exported from hw.GPUs with richer metadata.
|
|
continue
|
|
}
|
|
|
|
serialNumber := normalizedSerial(pcie.SerialNumber)
|
|
|
|
// Determine model (prefer PartNumber, fallback to DeviceClass)
|
|
model := pcie.PartNumber
|
|
if model == "" {
|
|
model = pcie.DeviceClass
|
|
}
|
|
|
|
status := normalizeStatus(pcie.Status, false)
|
|
firmware := ""
|
|
if isNVSwitchPCIeDevice(pcie) {
|
|
firmware = nvswitchFirmwareBySlot[normalizeNVSwitchSlotForLookup(pcie.Slot)]
|
|
}
|
|
meta := buildStatusMeta(
|
|
status,
|
|
pcie.StatusCheckedAt,
|
|
pcie.StatusChangedAt,
|
|
pcie.StatusAtCollect,
|
|
pcie.StatusHistory,
|
|
pcie.ErrorDescription,
|
|
collectedAt,
|
|
)
|
|
|
|
result = append(result, ReanimatorPCIe{
|
|
Slot: pcie.Slot,
|
|
VendorID: pcie.VendorID,
|
|
DeviceID: pcie.DeviceID,
|
|
BDF: pcie.BDF,
|
|
DeviceClass: pcie.DeviceClass,
|
|
Manufacturer: pcie.Manufacturer,
|
|
Model: model,
|
|
LinkWidth: pcie.LinkWidth,
|
|
LinkSpeed: pcie.LinkSpeed,
|
|
MaxLinkWidth: pcie.MaxLinkWidth,
|
|
MaxLinkSpeed: pcie.MaxLinkSpeed,
|
|
SerialNumber: serialNumber,
|
|
Firmware: firmware,
|
|
Status: status,
|
|
StatusCheckedAt: meta.StatusCheckedAt,
|
|
StatusChangedAt: meta.StatusChangedAt,
|
|
StatusAtCollect: meta.StatusAtCollection,
|
|
StatusHistory: meta.StatusHistory,
|
|
ErrorDescription: meta.ErrorDescription,
|
|
})
|
|
}
|
|
|
|
// Convert GPUs as PCIe devices
|
|
for _, gpu := range hw.GPUs {
|
|
serialNumber := normalizedSerial(gpu.SerialNumber)
|
|
|
|
// Determine device class
|
|
deviceClass := "DisplayController"
|
|
|
|
status := normalizeStatus(gpu.Status, false)
|
|
meta := buildStatusMeta(
|
|
status,
|
|
gpu.StatusCheckedAt,
|
|
gpu.StatusChangedAt,
|
|
gpu.StatusAtCollect,
|
|
gpu.StatusHistory,
|
|
gpu.ErrorDescription,
|
|
collectedAt,
|
|
)
|
|
|
|
result = append(result, ReanimatorPCIe{
|
|
Slot: gpu.Slot,
|
|
VendorID: gpu.VendorID,
|
|
DeviceID: gpu.DeviceID,
|
|
BDF: gpu.BDF,
|
|
DeviceClass: deviceClass,
|
|
Manufacturer: gpu.Manufacturer,
|
|
Model: gpu.Model,
|
|
LinkWidth: gpu.CurrentLinkWidth,
|
|
LinkSpeed: gpu.CurrentLinkSpeed,
|
|
MaxLinkWidth: gpu.MaxLinkWidth,
|
|
MaxLinkSpeed: gpu.MaxLinkSpeed,
|
|
SerialNumber: serialNumber,
|
|
Firmware: gpu.Firmware,
|
|
Status: status,
|
|
StatusCheckedAt: meta.StatusCheckedAt,
|
|
StatusChangedAt: meta.StatusChangedAt,
|
|
StatusAtCollect: meta.StatusAtCollection,
|
|
StatusHistory: meta.StatusHistory,
|
|
ErrorDescription: meta.ErrorDescription,
|
|
})
|
|
}
|
|
|
|
// Convert network adapters as PCIe devices
|
|
for _, nic := range hw.NetworkAdapters {
|
|
if !nic.Present {
|
|
continue
|
|
}
|
|
|
|
serialNumber := normalizedSerial(nic.SerialNumber)
|
|
|
|
status := normalizeStatus(nic.Status, false)
|
|
meta := buildStatusMeta(
|
|
status,
|
|
nic.StatusCheckedAt,
|
|
nic.StatusChangedAt,
|
|
nic.StatusAtCollect,
|
|
nic.StatusHistory,
|
|
nic.ErrorDescription,
|
|
collectedAt,
|
|
)
|
|
|
|
result = append(result, ReanimatorPCIe{
|
|
Slot: nic.Slot,
|
|
VendorID: nic.VendorID,
|
|
DeviceID: nic.DeviceID,
|
|
BDF: "",
|
|
DeviceClass: "NetworkController",
|
|
Manufacturer: nic.Vendor,
|
|
Model: nic.Model,
|
|
LinkWidth: 0,
|
|
LinkSpeed: "",
|
|
MaxLinkWidth: 0,
|
|
MaxLinkSpeed: "",
|
|
SerialNumber: serialNumber,
|
|
Firmware: nic.Firmware,
|
|
Status: status,
|
|
StatusCheckedAt: meta.StatusCheckedAt,
|
|
StatusChangedAt: meta.StatusChangedAt,
|
|
StatusAtCollect: meta.StatusAtCollection,
|
|
StatusHistory: meta.StatusHistory,
|
|
ErrorDescription: meta.ErrorDescription,
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func isNVSwitchPCIeDevice(pcie models.PCIeDevice) bool {
|
|
deviceClass := strings.TrimSpace(pcie.DeviceClass)
|
|
if strings.EqualFold(deviceClass, "NVSwitch") {
|
|
return true
|
|
}
|
|
slot := normalizeNVSwitchSlotForLookup(pcie.Slot)
|
|
return strings.HasPrefix(slot, "NVSWITCH")
|
|
}
|
|
|
|
func buildNVSwitchFirmwareBySlot(firmware []models.FirmwareInfo) map[string]string {
|
|
result := make(map[string]string)
|
|
for _, fw := range firmware {
|
|
name := strings.TrimSpace(fw.DeviceName)
|
|
if !strings.HasPrefix(strings.ToUpper(name), "NVSWITCH ") {
|
|
continue
|
|
}
|
|
|
|
rest := strings.TrimSpace(name[len("NVSwitch "):])
|
|
if rest == "" {
|
|
continue
|
|
}
|
|
|
|
slot := rest
|
|
if idx := strings.Index(rest, " ("); idx > 0 {
|
|
slot = strings.TrimSpace(rest[:idx])
|
|
}
|
|
slot = normalizeNVSwitchSlotForLookup(slot)
|
|
if slot == "" {
|
|
continue
|
|
}
|
|
|
|
if _, exists := result[slot]; exists {
|
|
continue
|
|
}
|
|
version := strings.TrimSpace(fw.Version)
|
|
if version == "" {
|
|
continue
|
|
}
|
|
result[slot] = version
|
|
}
|
|
return result
|
|
}
|
|
|
|
func normalizeNVSwitchSlotForLookup(slot string) string {
|
|
normalized := strings.ToUpper(strings.TrimSpace(slot))
|
|
if strings.HasPrefix(normalized, "NVSWITCHNVSWITCH") {
|
|
return "NVSWITCH" + strings.TrimPrefix(normalized, "NVSWITCHNVSWITCH")
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func isDisplayClass(deviceClass string) bool {
|
|
class := strings.ToLower(strings.TrimSpace(deviceClass))
|
|
return strings.Contains(class, "display") ||
|
|
strings.Contains(class, "vga") ||
|
|
strings.Contains(class, "3d controller")
|
|
}
|
|
|
|
// convertPowerSupplies converts power supplies to Reanimator format
|
|
func convertPowerSupplies(psus []models.PSU, collectedAt string) []ReanimatorPSU {
|
|
if len(psus) == 0 {
|
|
return nil
|
|
}
|
|
|
|
result := make([]ReanimatorPSU, 0, len(psus))
|
|
for _, psu := range psus {
|
|
// Skip PSUs without serial number (if not present)
|
|
if !psu.Present || psu.SerialNumber == "" {
|
|
continue
|
|
}
|
|
|
|
status := normalizeStatus(psu.Status, false)
|
|
meta := buildStatusMeta(
|
|
status,
|
|
psu.StatusCheckedAt,
|
|
psu.StatusChangedAt,
|
|
psu.StatusAtCollect,
|
|
psu.StatusHistory,
|
|
psu.ErrorDescription,
|
|
collectedAt,
|
|
)
|
|
|
|
result = append(result, ReanimatorPSU{
|
|
Slot: psu.Slot,
|
|
Present: psu.Present,
|
|
Model: psu.Model,
|
|
Vendor: psu.Vendor,
|
|
WattageW: psu.WattageW,
|
|
SerialNumber: psu.SerialNumber,
|
|
PartNumber: psu.PartNumber,
|
|
Firmware: psu.Firmware,
|
|
Status: status,
|
|
InputType: psu.InputType,
|
|
InputPowerW: psu.InputPowerW,
|
|
OutputPowerW: psu.OutputPowerW,
|
|
InputVoltage: psu.InputVoltage,
|
|
StatusCheckedAt: meta.StatusCheckedAt,
|
|
StatusChangedAt: meta.StatusChangedAt,
|
|
StatusAtCollect: meta.StatusAtCollection,
|
|
StatusHistory: meta.StatusHistory,
|
|
ErrorDescription: meta.ErrorDescription,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
type convertedStatusMeta struct {
|
|
StatusCheckedAt string
|
|
StatusChangedAt string
|
|
StatusAtCollection *ReanimatorStatusAtCollection
|
|
StatusHistory []ReanimatorStatusHistoryEntry
|
|
ErrorDescription string
|
|
}
|
|
|
|
func buildStatusMeta(
|
|
currentStatus string,
|
|
checkedAt time.Time,
|
|
changedAt time.Time,
|
|
statusAtCollection *models.StatusAtCollection,
|
|
history []models.StatusHistoryEntry,
|
|
errorDescription string,
|
|
collectedAt string,
|
|
) convertedStatusMeta {
|
|
meta := convertedStatusMeta{
|
|
StatusCheckedAt: formatOptionalRFC3339(checkedAt),
|
|
StatusChangedAt: formatOptionalRFC3339(changedAt),
|
|
ErrorDescription: strings.TrimSpace(errorDescription),
|
|
}
|
|
|
|
convertedHistory := make([]ReanimatorStatusHistoryEntry, 0, len(history))
|
|
for _, h := range history {
|
|
changed := formatOptionalRFC3339(h.ChangedAt)
|
|
if changed == "" {
|
|
continue
|
|
}
|
|
convertedHistory = append(convertedHistory, ReanimatorStatusHistoryEntry{
|
|
Status: normalizeStatus(h.Status, true),
|
|
ChangedAt: changed,
|
|
Details: strings.TrimSpace(h.Details),
|
|
})
|
|
}
|
|
sort.Slice(convertedHistory, func(i, j int) bool {
|
|
return convertedHistory[i].ChangedAt < convertedHistory[j].ChangedAt
|
|
})
|
|
if len(convertedHistory) > 0 {
|
|
meta.StatusHistory = convertedHistory
|
|
if meta.StatusChangedAt == "" {
|
|
meta.StatusChangedAt = convertedHistory[len(convertedHistory)-1].ChangedAt
|
|
}
|
|
}
|
|
|
|
if statusAtCollection != nil {
|
|
at := formatOptionalRFC3339(statusAtCollection.At)
|
|
if at != "" && strings.TrimSpace(statusAtCollection.Status) != "" {
|
|
meta.StatusAtCollection = &ReanimatorStatusAtCollection{
|
|
Status: normalizeStatus(statusAtCollection.Status, true),
|
|
At: at,
|
|
}
|
|
}
|
|
}
|
|
if meta.StatusAtCollection == nil && strings.TrimSpace(currentStatus) != "" && collectedAt != "" {
|
|
meta.StatusAtCollection = &ReanimatorStatusAtCollection{
|
|
Status: currentStatus,
|
|
At: collectedAt,
|
|
}
|
|
}
|
|
|
|
if meta.StatusCheckedAt == "" && len(meta.StatusHistory) > 0 {
|
|
meta.StatusCheckedAt = meta.StatusHistory[len(meta.StatusHistory)-1].ChangedAt
|
|
}
|
|
if meta.StatusCheckedAt == "" && strings.TrimSpace(currentStatus) != "" && collectedAt != "" {
|
|
meta.StatusCheckedAt = collectedAt
|
|
}
|
|
|
|
return meta
|
|
}
|
|
|
|
func formatOptionalRFC3339(t time.Time) string {
|
|
if t.IsZero() {
|
|
return ""
|
|
}
|
|
return t.UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
func dedupeFirmware(items []ReanimatorFirmware) []ReanimatorFirmware {
|
|
if len(items) < 2 {
|
|
return items
|
|
}
|
|
seen := make(map[string]struct{}, len(items))
|
|
result := make([]ReanimatorFirmware, 0, len(items))
|
|
for _, item := range items {
|
|
key := strings.ToLower(strings.TrimSpace(item.DeviceName))
|
|
if key == "" {
|
|
key = strings.ToLower(strings.TrimSpace(item.Version))
|
|
}
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
result = append(result, item)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func dedupeCPUs(items []ReanimatorCPU) []ReanimatorCPU {
|
|
if len(items) < 2 {
|
|
return items
|
|
}
|
|
seen := make(map[int]struct{}, len(items))
|
|
result := make([]ReanimatorCPU, 0, len(items))
|
|
for _, item := range items {
|
|
if _, ok := seen[item.Socket]; ok {
|
|
continue
|
|
}
|
|
seen[item.Socket] = struct{}{}
|
|
result = append(result, item)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func dedupeMemory(items []ReanimatorMemory) []ReanimatorMemory {
|
|
if len(items) < 2 {
|
|
return items
|
|
}
|
|
seen := make(map[string]struct{}, len(items))
|
|
result := make([]ReanimatorMemory, 0, len(items))
|
|
for _, item := range items {
|
|
key := strings.ToLower(strings.TrimSpace(item.Slot))
|
|
if key == "" {
|
|
key = strings.ToLower(strings.TrimSpace(item.Location))
|
|
}
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
result = append(result, item)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func dedupeStorage(items []ReanimatorStorage) []ReanimatorStorage {
|
|
if len(items) < 2 {
|
|
return items
|
|
}
|
|
seen := make(map[string]struct{}, len(items))
|
|
result := make([]ReanimatorStorage, 0, len(items))
|
|
for _, item := range items {
|
|
key := strings.ToLower(strings.TrimSpace(item.SerialNumber))
|
|
if key == "" {
|
|
key = "slot:" + strings.ToLower(strings.TrimSpace(item.Slot))
|
|
}
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
result = append(result, item)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func dedupePSUs(items []ReanimatorPSU) []ReanimatorPSU {
|
|
if len(items) < 2 {
|
|
return items
|
|
}
|
|
seen := make(map[string]struct{}, len(items))
|
|
result := make([]ReanimatorPSU, 0, len(items))
|
|
for _, item := range items {
|
|
key := strings.ToLower(strings.TrimSpace(item.SerialNumber))
|
|
if key == "" {
|
|
key = "slot:" + strings.ToLower(strings.TrimSpace(item.Slot))
|
|
}
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
result = append(result, item)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func dedupePCIe(items []ReanimatorPCIe) []ReanimatorPCIe {
|
|
if len(items) < 2 {
|
|
return items
|
|
}
|
|
type scored struct {
|
|
item ReanimatorPCIe
|
|
score int
|
|
idx int
|
|
}
|
|
byKey := make(map[string]scored, len(items))
|
|
order := make([]string, 0, len(items))
|
|
for i, item := range items {
|
|
key := pcieDedupKey(item)
|
|
curr := scored{item: item, score: pcieQualityScore(item), idx: i}
|
|
existing, ok := byKey[key]
|
|
if !ok {
|
|
byKey[key] = curr
|
|
order = append(order, key)
|
|
continue
|
|
}
|
|
if curr.score > existing.score {
|
|
byKey[key] = curr
|
|
}
|
|
}
|
|
result := make([]ReanimatorPCIe, 0, len(byKey))
|
|
for _, key := range order {
|
|
result = append(result, byKey[key].item)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func pcieDedupKey(item ReanimatorPCIe) string {
|
|
slot := strings.ToLower(strings.TrimSpace(item.Slot))
|
|
serial := strings.ToLower(strings.TrimSpace(item.SerialNumber))
|
|
bdf := strings.ToLower(strings.TrimSpace(item.BDF))
|
|
if slot != "" {
|
|
return "slot:" + slot
|
|
}
|
|
if serial != "" {
|
|
return "sn:" + serial
|
|
}
|
|
if bdf != "" {
|
|
return "bdf:" + bdf
|
|
}
|
|
return strings.ToLower(strings.TrimSpace(item.DeviceClass)) + "|" + strings.ToLower(strings.TrimSpace(item.Model))
|
|
}
|
|
|
|
func pcieQualityScore(item ReanimatorPCIe) int {
|
|
score := 0
|
|
if strings.TrimSpace(item.SerialNumber) != "" {
|
|
score += 4
|
|
}
|
|
if strings.TrimSpace(item.Model) != "" && !isGenericPCIeModel(item.Model) {
|
|
score += 3
|
|
}
|
|
status := strings.ToLower(strings.TrimSpace(item.Status))
|
|
if status == "ok" || status == "warning" || status == "critical" {
|
|
score += 2
|
|
}
|
|
if strings.TrimSpace(item.BDF) != "" {
|
|
score++
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(item.DeviceClass), "DisplayController") {
|
|
score++
|
|
}
|
|
return score
|
|
}
|
|
|
|
func isGenericPCIeModel(model string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(model)) {
|
|
case "", "unknown", "vga", "3d controller", "display controller":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// inferCPUManufacturer determines CPU manufacturer from model string
|
|
func inferCPUManufacturer(model string) string {
|
|
upper := strings.ToUpper(model)
|
|
|
|
// Intel patterns
|
|
if strings.Contains(upper, "INTEL") ||
|
|
strings.Contains(upper, "XEON") ||
|
|
strings.Contains(upper, "CORE I") {
|
|
return "Intel"
|
|
}
|
|
|
|
// AMD patterns
|
|
if strings.Contains(upper, "AMD") ||
|
|
strings.Contains(upper, "EPYC") ||
|
|
strings.Contains(upper, "RYZEN") ||
|
|
strings.Contains(upper, "THREADRIPPER") {
|
|
return "AMD"
|
|
}
|
|
|
|
// ARM patterns
|
|
if strings.Contains(upper, "ARM") ||
|
|
strings.Contains(upper, "CORTEX") {
|
|
return "ARM"
|
|
}
|
|
|
|
// Ampere patterns
|
|
if strings.Contains(upper, "AMPERE") ||
|
|
strings.Contains(upper, "ALTRA") {
|
|
return "Ampere"
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func normalizedSerial(serial string) string {
|
|
s := strings.TrimSpace(serial)
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
switch strings.ToUpper(s) {
|
|
case "N/A", "NA", "NONE", "NULL", "UNKNOWN", "-":
|
|
return ""
|
|
default:
|
|
return s
|
|
}
|
|
}
|
|
|
|
func parseSocketFromSlot(slot string) int {
|
|
s := strings.TrimSpace(strings.ToUpper(slot))
|
|
s = strings.TrimPrefix(s, "CPU")
|
|
if s == "" {
|
|
return 0
|
|
}
|
|
v, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return v
|
|
}
|
|
|
|
func intFromDetailMap(details map[string]any, key string) int {
|
|
if details == nil {
|
|
return 0
|
|
}
|
|
v, ok := details[key]
|
|
if !ok {
|
|
return 0
|
|
}
|
|
switch n := v.(type) {
|
|
case int:
|
|
return n
|
|
case float64:
|
|
return int(n)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// inferStorageStatus determines storage device status
|
|
func inferStorageStatus(stor models.Storage) string {
|
|
if !stor.Present {
|
|
return "Unknown"
|
|
}
|
|
return "Unknown"
|
|
}
|
|
|
|
func normalizeSourceType(sourceType string) string {
|
|
normalized := strings.ToLower(strings.TrimSpace(sourceType))
|
|
switch normalized {
|
|
case "api", "logfile", "manual":
|
|
return normalized
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func normalizeProtocol(protocol string) string {
|
|
normalized := strings.ToLower(strings.TrimSpace(protocol))
|
|
switch normalized {
|
|
case "redfish", "ipmi", "snmp", "ssh":
|
|
return normalized
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func normalizeNullableString(v string) string {
|
|
trimmed := strings.TrimSpace(v)
|
|
if strings.EqualFold(trimmed, "NULL") {
|
|
return ""
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
func normalizeStatus(status string, allowEmpty bool) string {
|
|
switch strings.ToLower(strings.TrimSpace(status)) {
|
|
case "ok":
|
|
return "OK"
|
|
case "pass":
|
|
return "OK"
|
|
case "warning":
|
|
return "Warning"
|
|
case "critical":
|
|
return "Critical"
|
|
case "fail":
|
|
return "Critical"
|
|
case "unknown":
|
|
return "Unknown"
|
|
case "empty":
|
|
if allowEmpty {
|
|
return "Empty"
|
|
}
|
|
return "Unknown"
|
|
default:
|
|
if allowEmpty {
|
|
return "Unknown"
|
|
}
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
var (
|
|
ipv4Regex = regexp.MustCompile(`(?:^|[^0-9])((?:\d{1,3}\.){3}\d{1,3})(?:[^0-9]|$)`)
|
|
)
|
|
|
|
func inferTargetHost(targetHost, filename string) string {
|
|
if trimmed := strings.TrimSpace(targetHost); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
|
|
candidate := strings.TrimSpace(filename)
|
|
if candidate == "" {
|
|
return ""
|
|
}
|
|
|
|
if parsed, err := url.Parse(candidate); err == nil && parsed.Hostname() != "" {
|
|
return parsed.Hostname()
|
|
}
|
|
|
|
if submatches := ipv4Regex.FindStringSubmatch(candidate); len(submatches) > 1 {
|
|
return submatches[1]
|
|
}
|
|
|
|
return ""
|
|
}
|