735 lines
20 KiB
Go
735 lines
20 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
)
|
|
|
|
type slotFirmwareInfo struct {
|
|
Model string
|
|
Version string
|
|
Category string
|
|
}
|
|
|
|
var (
|
|
psuFirmwareRe = regexp.MustCompile(`(?i)^PSU\s*([0-9A-Za-z_-]+)\s*(?:\(([^)]+)\))?$`)
|
|
nicFirmwareRe = regexp.MustCompile(`(?i)^NIC\s+([^()]+?)\s*(?:\(([^)]+)\))?$`)
|
|
gpuFirmwareRe = regexp.MustCompile(`(?i)^GPU\s+([^()]+?)\s*(?:\(([^)]+)\))?$`)
|
|
nvsFirmwareRe = regexp.MustCompile(`(?i)^NVSwitch\s+([^()]+?)\s*(?:\(([^)]+)\))?$`)
|
|
)
|
|
|
|
func BuildHardwareDevices(hw *models.HardwareConfig) []models.HardwareDevice {
|
|
if hw == nil {
|
|
return nil
|
|
}
|
|
|
|
all := make([]models.HardwareDevice, 0, 1+len(hw.CPUs)+len(hw.Memory)+len(hw.Storage)+len(hw.PCIeDevices)+len(hw.GPUs)+len(hw.NetworkAdapters)+len(hw.PowerSupply))
|
|
fwBySlot := buildFirmwareBySlot(hw.Firmware)
|
|
nextID := 0
|
|
add := func(d models.HardwareDevice) {
|
|
d.ID = fmt.Sprintf("%s:%d", d.Kind, nextID)
|
|
nextID++
|
|
all = append(all, d)
|
|
}
|
|
|
|
add(models.HardwareDevice{
|
|
Kind: models.DeviceKindBoard,
|
|
Source: "board",
|
|
Slot: "board",
|
|
Model: strings.TrimSpace(hw.BoardInfo.ProductName),
|
|
PartNumber: strings.TrimSpace(hw.BoardInfo.PartNumber),
|
|
Manufacturer: strings.TrimSpace(hw.BoardInfo.Manufacturer),
|
|
SerialNumber: strings.TrimSpace(hw.BoardInfo.SerialNumber),
|
|
Details: map[string]any{
|
|
"description": strings.TrimSpace(hw.BoardInfo.Description),
|
|
"version": strings.TrimSpace(hw.BoardInfo.Version),
|
|
"uuid": strings.TrimSpace(hw.BoardInfo.UUID),
|
|
},
|
|
})
|
|
|
|
for _, cpu := range hw.CPUs {
|
|
add(models.HardwareDevice{
|
|
Kind: models.DeviceKindCPU,
|
|
Source: "cpus",
|
|
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{
|
|
"description": cpu.Description,
|
|
"socket": cpu.Socket,
|
|
"l1_cache_kb": cpu.L1CacheKB,
|
|
"l2_cache_kb": cpu.L2CacheKB,
|
|
"l3_cache_kb": cpu.L3CacheKB,
|
|
"tdp_w": cpu.TDP,
|
|
"ppin": cpu.PPIN,
|
|
},
|
|
})
|
|
}
|
|
|
|
for _, mem := range hw.Memory {
|
|
if !mem.Present || mem.SizeMB == 0 {
|
|
continue
|
|
}
|
|
present := mem.Present
|
|
add(models.HardwareDevice{
|
|
Kind: models.DeviceKindMemory,
|
|
Source: "memory",
|
|
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{
|
|
"description": mem.Description,
|
|
"technology": mem.Technology,
|
|
"max_speed_mhz": mem.MaxSpeedMHz,
|
|
"current_speed_mhz": mem.CurrentSpeedMHz,
|
|
"ranks": mem.Ranks,
|
|
},
|
|
})
|
|
}
|
|
|
|
for _, stor := range hw.Storage {
|
|
if !stor.Present {
|
|
continue
|
|
}
|
|
present := stor.Present
|
|
add(models.HardwareDevice{
|
|
Kind: models.DeviceKindStorage,
|
|
Source: "storage",
|
|
Slot: stor.Slot,
|
|
Location: stor.Location,
|
|
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,
|
|
Details: map[string]any{
|
|
"description": stor.Description,
|
|
"backplane_id": stor.BackplaneID,
|
|
},
|
|
})
|
|
}
|
|
|
|
for _, p := range hw.PCIeDevices {
|
|
if isEmptyPCIeDevice(p) {
|
|
continue
|
|
}
|
|
slotKey := normalizeSlotKey(p.Slot)
|
|
fwInfo := fwBySlot[slotKey]
|
|
model := strings.TrimSpace(p.PartNumber)
|
|
if model == "" {
|
|
model = strings.TrimSpace(p.DeviceClass)
|
|
}
|
|
if model == "" {
|
|
model = strings.TrimSpace(p.Description)
|
|
}
|
|
if model == "" && fwInfo.Model != "" {
|
|
model = fwInfo.Model
|
|
}
|
|
add(models.HardwareDevice{
|
|
Kind: models.DeviceKindPCIe,
|
|
Source: "pcie_devices",
|
|
Slot: p.Slot,
|
|
BDF: p.BDF,
|
|
DeviceClass: p.DeviceClass,
|
|
VendorID: p.VendorID,
|
|
DeviceID: p.DeviceID,
|
|
Model: model,
|
|
PartNumber: p.PartNumber,
|
|
Manufacturer: p.Manufacturer,
|
|
SerialNumber: p.SerialNumber,
|
|
Firmware: fwInfo.Version,
|
|
MACAddresses: p.MACAddresses,
|
|
LinkWidth: p.LinkWidth,
|
|
LinkSpeed: p.LinkSpeed,
|
|
MaxLinkWidth: p.MaxLinkWidth,
|
|
MaxLinkSpeed: p.MaxLinkSpeed,
|
|
Status: p.Status,
|
|
StatusCheckedAt: p.StatusCheckedAt,
|
|
StatusChangedAt: p.StatusChangedAt,
|
|
StatusAtCollect: p.StatusAtCollect,
|
|
StatusHistory: p.StatusHistory,
|
|
ErrorDescription: p.ErrorDescription,
|
|
Details: map[string]any{
|
|
"description": p.Description,
|
|
"fw_category": fwInfo.Category,
|
|
},
|
|
})
|
|
}
|
|
|
|
for _, gpu := range hw.GPUs {
|
|
add(models.HardwareDevice{
|
|
Kind: models.DeviceKindGPU,
|
|
Source: "gpus",
|
|
Slot: gpu.Slot,
|
|
Location: gpu.Location,
|
|
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,
|
|
Details: map[string]any{
|
|
"description": gpu.Description,
|
|
"uuid": gpu.UUID,
|
|
"video_bios": gpu.VideoBIOS,
|
|
"irq": gpu.IRQ,
|
|
"bus_type": gpu.BusType,
|
|
"dma_size": gpu.DMASize,
|
|
"dma_mask": gpu.DMAMask,
|
|
"device_minor": gpu.DeviceMinor,
|
|
"temperature": gpu.Temperature,
|
|
"mem_temperature": gpu.MemTemperature,
|
|
"power": gpu.Power,
|
|
"max_power": gpu.MaxPower,
|
|
"clock_speed": gpu.ClockSpeed,
|
|
},
|
|
})
|
|
}
|
|
|
|
for _, nic := range hw.NetworkAdapters {
|
|
if !nic.Present {
|
|
continue
|
|
}
|
|
present := nic.Present
|
|
add(models.HardwareDevice{
|
|
Kind: models.DeviceKindNetwork,
|
|
Source: "network_adapters",
|
|
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,
|
|
Details: map[string]any{
|
|
"description": nic.Description,
|
|
},
|
|
})
|
|
}
|
|
|
|
for _, psu := range hw.PowerSupply {
|
|
if !psu.Present {
|
|
continue
|
|
}
|
|
present := psu.Present
|
|
add(models.HardwareDevice{
|
|
Kind: models.DeviceKindPSU,
|
|
Source: "power_supplies",
|
|
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,
|
|
Details: map[string]any{
|
|
"description": psu.Description,
|
|
"output_voltage": psu.OutputVoltage,
|
|
},
|
|
})
|
|
}
|
|
|
|
return dedupeDevices(all)
|
|
}
|
|
|
|
func isEmptyPCIeDevice(p models.PCIeDevice) bool {
|
|
if isNumericSlot(strings.TrimSpace(p.Slot)) &&
|
|
strings.TrimSpace(p.BDF) == "" &&
|
|
p.VendorID == 0 &&
|
|
p.DeviceID == 0 &&
|
|
normalizedSerial(p.SerialNumber) == "" &&
|
|
!hasMeaningfulText(p.PartNumber) &&
|
|
!hasMeaningfulText(p.Manufacturer) &&
|
|
!hasMeaningfulText(p.Description) &&
|
|
len(p.MACAddresses) == 0 &&
|
|
p.LinkWidth == 0 &&
|
|
p.MaxLinkWidth == 0 {
|
|
return true
|
|
}
|
|
|
|
if strings.TrimSpace(p.BDF) != "" {
|
|
return false
|
|
}
|
|
if p.VendorID != 0 || p.DeviceID != 0 {
|
|
return false
|
|
}
|
|
if normalizedSerial(p.SerialNumber) != "" {
|
|
return false
|
|
}
|
|
if hasMeaningfulText(p.PartNumber) {
|
|
return false
|
|
}
|
|
if hasMeaningfulText(p.Manufacturer) {
|
|
return false
|
|
}
|
|
if hasMeaningfulText(p.Description) {
|
|
return false
|
|
}
|
|
if strings.TrimSpace(p.DeviceClass) != "" {
|
|
class := strings.ToLower(strings.TrimSpace(p.DeviceClass))
|
|
if class != "unknown" && class != "other" && class != "pcie device" {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isNumericSlot(slot string) bool {
|
|
if slot == "" {
|
|
return false
|
|
}
|
|
for _, r := range slot {
|
|
if r < '0' || r > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func hasMeaningfulText(v string) bool {
|
|
s := strings.ToLower(strings.TrimSpace(v))
|
|
if s == "" {
|
|
return false
|
|
}
|
|
switch s {
|
|
case "-", "n/a", "na", "none", "null", "unknown":
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func dedupeDevices(items []models.HardwareDevice) []models.HardwareDevice {
|
|
if len(items) < 2 {
|
|
return items
|
|
}
|
|
parent := make([]int, len(items))
|
|
for i := range parent {
|
|
parent[i] = i
|
|
}
|
|
find := func(x int) int {
|
|
for parent[x] != x {
|
|
parent[x] = parent[parent[x]]
|
|
x = parent[x]
|
|
}
|
|
return x
|
|
}
|
|
union := func(a, b int) {
|
|
ra := find(a)
|
|
rb := find(b)
|
|
if ra != rb {
|
|
parent[rb] = ra
|
|
}
|
|
}
|
|
|
|
for i := 0; i < len(items); i++ {
|
|
for j := i + 1; j < len(items); j++ {
|
|
if shouldMergeDevices(items[i], items[j]) {
|
|
union(i, j)
|
|
}
|
|
}
|
|
}
|
|
|
|
groups := make(map[int][]int, len(items))
|
|
order := make([]int, 0, len(items))
|
|
for i := range items {
|
|
root := find(i)
|
|
if _, ok := groups[root]; !ok {
|
|
order = append(order, root)
|
|
}
|
|
groups[root] = append(groups[root], i)
|
|
}
|
|
|
|
out := make([]models.HardwareDevice, 0, len(order))
|
|
for _, root := range order {
|
|
indices := groups[root]
|
|
bestIdx := indices[0]
|
|
bestScore := qualityScore(items[bestIdx])
|
|
for _, idx := range indices[1:] {
|
|
if s := qualityScore(items[idx]); s > bestScore {
|
|
bestIdx = idx
|
|
bestScore = s
|
|
}
|
|
}
|
|
merged := items[bestIdx]
|
|
for _, idx := range indices {
|
|
if idx == bestIdx {
|
|
continue
|
|
}
|
|
merged = mergeDevices(merged, items[idx])
|
|
}
|
|
out = append(out, merged)
|
|
}
|
|
|
|
for i := range out {
|
|
out[i].ID = out[i].Kind + ":" + strconv.Itoa(i)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func shouldMergeDevices(a, b models.HardwareDevice) bool {
|
|
aSN := strings.ToLower(normalizedSerial(a.SerialNumber))
|
|
bSN := strings.ToLower(normalizedSerial(b.SerialNumber))
|
|
aBDF := strings.ToLower(strings.TrimSpace(a.BDF))
|
|
bBDF := strings.ToLower(strings.TrimSpace(b.BDF))
|
|
|
|
// Hard conflicts.
|
|
if aSN != "" && bSN != "" && aSN == bSN {
|
|
return true
|
|
}
|
|
if aSN != "" && bSN != "" && aSN != bSN {
|
|
return false
|
|
}
|
|
if aBDF != "" && bBDF != "" && aBDF != bBDF {
|
|
return false
|
|
}
|
|
|
|
// Strong identities.
|
|
if aBDF != "" && aBDF == bBDF {
|
|
return true
|
|
}
|
|
|
|
// If both have no strong IDs, be conservative.
|
|
if aSN == "" && bSN == "" && aBDF == "" && bBDF == "" {
|
|
if hasMACOverlap(a.MACAddresses, b.MACAddresses) {
|
|
return true
|
|
}
|
|
if normalizeSlot(a.Slot) != "" && normalizeSlot(a.Slot) == normalizeSlot(b.Slot) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
score := 0
|
|
if samePCIID(a, b) {
|
|
score += 4
|
|
}
|
|
if sameModel(a, b) {
|
|
score += 3
|
|
}
|
|
if sameManufacturer(a, b) {
|
|
score += 2
|
|
}
|
|
if normalizeSlot(a.Slot) != "" && normalizeSlot(a.Slot) == normalizeSlot(b.Slot) {
|
|
score += 2
|
|
}
|
|
if hasMACOverlap(a.MACAddresses, b.MACAddresses) {
|
|
score += 2
|
|
}
|
|
if sameKindFamily(a.Kind, b.Kind) {
|
|
score++
|
|
}
|
|
if samePCIID(a, b) && ((aBDF != "" && bBDF == "") || (aBDF == "" && bBDF != "")) {
|
|
score += 2
|
|
}
|
|
|
|
return score >= 7
|
|
}
|
|
|
|
func mergeDevices(primary, secondary models.HardwareDevice) models.HardwareDevice {
|
|
fillString := func(dst *string, src string) {
|
|
if strings.TrimSpace(*dst) == "" && strings.TrimSpace(src) != "" {
|
|
*dst = src
|
|
}
|
|
}
|
|
fillInt := func(dst *int, src int) {
|
|
if *dst == 0 && src != 0 {
|
|
*dst = src
|
|
}
|
|
}
|
|
fillFloat := func(dst *float64, src float64) {
|
|
if *dst == 0 && src != 0 {
|
|
*dst = src
|
|
}
|
|
}
|
|
|
|
fillString(&primary.ID, secondary.ID)
|
|
fillString(&primary.Kind, secondary.Kind)
|
|
fillString(&primary.Source, secondary.Source)
|
|
fillString(&primary.Slot, secondary.Slot)
|
|
fillString(&primary.Location, secondary.Location)
|
|
fillString(&primary.BDF, secondary.BDF)
|
|
fillString(&primary.DeviceClass, secondary.DeviceClass)
|
|
fillInt(&primary.VendorID, secondary.VendorID)
|
|
fillInt(&primary.DeviceID, secondary.DeviceID)
|
|
fillString(&primary.Model, secondary.Model)
|
|
fillString(&primary.PartNumber, secondary.PartNumber)
|
|
fillString(&primary.Manufacturer, secondary.Manufacturer)
|
|
fillString(&primary.SerialNumber, secondary.SerialNumber)
|
|
fillString(&primary.Firmware, secondary.Firmware)
|
|
fillString(&primary.Type, secondary.Type)
|
|
fillString(&primary.Interface, secondary.Interface)
|
|
if primary.Present == nil && secondary.Present != nil {
|
|
primary.Present = secondary.Present
|
|
}
|
|
fillInt(&primary.SizeMB, secondary.SizeMB)
|
|
fillInt(&primary.SizeGB, secondary.SizeGB)
|
|
fillInt(&primary.Cores, secondary.Cores)
|
|
fillInt(&primary.Threads, secondary.Threads)
|
|
fillInt(&primary.FrequencyMHz, secondary.FrequencyMHz)
|
|
fillInt(&primary.MaxFreqMHz, secondary.MaxFreqMHz)
|
|
fillInt(&primary.PortCount, secondary.PortCount)
|
|
fillString(&primary.PortType, secondary.PortType)
|
|
if len(primary.MACAddresses) == 0 && len(secondary.MACAddresses) > 0 {
|
|
primary.MACAddresses = secondary.MACAddresses
|
|
}
|
|
fillInt(&primary.LinkWidth, secondary.LinkWidth)
|
|
fillString(&primary.LinkSpeed, secondary.LinkSpeed)
|
|
fillInt(&primary.MaxLinkWidth, secondary.MaxLinkWidth)
|
|
fillString(&primary.MaxLinkSpeed, secondary.MaxLinkSpeed)
|
|
fillInt(&primary.WattageW, secondary.WattageW)
|
|
fillString(&primary.InputType, secondary.InputType)
|
|
fillInt(&primary.InputPowerW, secondary.InputPowerW)
|
|
fillInt(&primary.OutputPowerW, secondary.OutputPowerW)
|
|
fillFloat(&primary.InputVoltage, secondary.InputVoltage)
|
|
fillInt(&primary.TemperatureC, secondary.TemperatureC)
|
|
fillString(&primary.Status, secondary.Status)
|
|
if primary.StatusCheckedAt.IsZero() && !secondary.StatusCheckedAt.IsZero() {
|
|
primary.StatusCheckedAt = secondary.StatusCheckedAt
|
|
}
|
|
if primary.StatusChangedAt.IsZero() && !secondary.StatusChangedAt.IsZero() {
|
|
primary.StatusChangedAt = secondary.StatusChangedAt
|
|
}
|
|
if primary.StatusAtCollect == nil && secondary.StatusAtCollect != nil {
|
|
primary.StatusAtCollect = secondary.StatusAtCollect
|
|
}
|
|
if len(primary.StatusHistory) == 0 && len(secondary.StatusHistory) > 0 {
|
|
primary.StatusHistory = secondary.StatusHistory
|
|
}
|
|
fillString(&primary.ErrorDescription, secondary.ErrorDescription)
|
|
if primary.Details == nil && secondary.Details != nil {
|
|
primary.Details = secondary.Details
|
|
}
|
|
return primary
|
|
}
|
|
|
|
func samePCIID(a, b models.HardwareDevice) bool {
|
|
if (a.VendorID == 0 && a.DeviceID == 0) || (b.VendorID == 0 && b.DeviceID == 0) {
|
|
return false
|
|
}
|
|
return a.VendorID == b.VendorID && a.DeviceID == b.DeviceID
|
|
}
|
|
|
|
func sameModel(a, b models.HardwareDevice) bool {
|
|
am := normalizeText(coalesce(a.Model, a.PartNumber, a.DeviceClass))
|
|
bm := normalizeText(coalesce(b.Model, b.PartNumber, b.DeviceClass))
|
|
return am != "" && am == bm
|
|
}
|
|
|
|
func sameManufacturer(a, b models.HardwareDevice) bool {
|
|
am := normalizeText(a.Manufacturer)
|
|
bm := normalizeText(b.Manufacturer)
|
|
return am != "" && am == bm
|
|
}
|
|
|
|
func hasMACOverlap(a, b []string) bool {
|
|
if len(a) == 0 || len(b) == 0 {
|
|
return false
|
|
}
|
|
set := make(map[string]struct{}, len(a))
|
|
for _, mac := range a {
|
|
key := normalizeText(mac)
|
|
if key != "" {
|
|
set[key] = struct{}{}
|
|
}
|
|
}
|
|
for _, mac := range b {
|
|
if _, ok := set[normalizeText(mac)]; ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func sameKindFamily(a, b string) bool {
|
|
if a == b {
|
|
return true
|
|
}
|
|
family := map[string]bool{
|
|
models.DeviceKindPCIe: true,
|
|
models.DeviceKindGPU: true,
|
|
models.DeviceKindNetwork: true,
|
|
}
|
|
return family[a] && family[b]
|
|
}
|
|
|
|
func normalizeText(v string) string {
|
|
s := strings.ToLower(strings.TrimSpace(v))
|
|
s = strings.ReplaceAll(s, " ", "")
|
|
s = strings.ReplaceAll(s, "_", "")
|
|
s = strings.ReplaceAll(s, "-", "")
|
|
return s
|
|
}
|
|
|
|
func normalizeSlot(slot string) string {
|
|
return normalizeText(slot)
|
|
}
|
|
|
|
func qualityScore(d models.HardwareDevice) int {
|
|
score := 0
|
|
if normalizedSerial(d.SerialNumber) != "" {
|
|
score += 6
|
|
}
|
|
if strings.TrimSpace(d.BDF) != "" {
|
|
score += 4
|
|
}
|
|
if strings.TrimSpace(d.Model) != "" {
|
|
score += 3
|
|
}
|
|
if strings.TrimSpace(d.Firmware) != "" {
|
|
score += 2
|
|
}
|
|
if strings.TrimSpace(d.Status) != "" {
|
|
score++
|
|
}
|
|
return score
|
|
}
|
|
|
|
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 buildFirmwareBySlot(firmware []models.FirmwareInfo) map[string]slotFirmwareInfo {
|
|
out := make(map[string]slotFirmwareInfo)
|
|
add := func(slot, model, version, category string) {
|
|
key := normalizeSlotKey(slot)
|
|
if key == "" || strings.TrimSpace(version) == "" {
|
|
return
|
|
}
|
|
existing, ok := out[key]
|
|
if ok && strings.TrimSpace(existing.Model) != "" {
|
|
return
|
|
}
|
|
out[key] = slotFirmwareInfo{
|
|
Model: strings.TrimSpace(model),
|
|
Version: strings.TrimSpace(version),
|
|
Category: category,
|
|
}
|
|
}
|
|
|
|
for _, fw := range firmware {
|
|
name := strings.TrimSpace(fw.DeviceName)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
if m := psuFirmwareRe.FindStringSubmatch(name); len(m) == 3 {
|
|
model := strings.TrimSpace(m[2])
|
|
if model == "" {
|
|
model = "PSU"
|
|
}
|
|
add(m[1], model, fw.Version, "psu")
|
|
continue
|
|
}
|
|
if m := nicFirmwareRe.FindStringSubmatch(name); len(m) == 3 {
|
|
model := strings.TrimSpace(m[2])
|
|
if model == "" {
|
|
model = "NIC"
|
|
}
|
|
add(m[1], model, fw.Version, "nic")
|
|
continue
|
|
}
|
|
if m := gpuFirmwareRe.FindStringSubmatch(name); len(m) == 3 {
|
|
model := strings.TrimSpace(m[2])
|
|
if model == "" {
|
|
model = "GPU"
|
|
}
|
|
add(m[1], model, fw.Version, "gpu")
|
|
continue
|
|
}
|
|
if m := nvsFirmwareRe.FindStringSubmatch(name); len(m) == 3 {
|
|
model := strings.TrimSpace(m[2])
|
|
if model == "" {
|
|
model = "NVSwitch"
|
|
}
|
|
add(m[1], model, fw.Version, "nvswitch")
|
|
continue
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func normalizeSlotKey(slot string) string {
|
|
return strings.ToLower(strings.TrimSpace(slot))
|
|
}
|