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)) }