Introduce canonical hardware.devices repository and align UI/Reanimator exports

This commit is contained in:
2026-02-17 19:07:18 +03:00
parent a82b55b144
commit de5521a4e5
11 changed files with 1944 additions and 319 deletions
+127 -164
View File
@@ -163,10 +163,14 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
return
}
// Build specification summary
spec := buildSpecification(result)
devices := canonicalDevices(result.Hardware)
spec := buildSpecification(result.Hardware)
response["hardware"] = result.Hardware
response["hardware"] = map[string]any{
"board": result.Hardware.BoardInfo,
"firmware": result.Hardware.Firmware,
"devices": devices,
}
response["specification"] = spec
jsonResponse(w, response)
}
@@ -178,17 +182,28 @@ type SpecLine struct {
Quantity int `json:"quantity"`
}
func buildSpecification(result *models.AnalysisResult) []SpecLine {
func canonicalDevices(hw *models.HardwareConfig) []models.HardwareDevice {
if hw == nil {
return nil
}
hw.Devices = BuildHardwareDevices(hw)
return hw.Devices
}
func buildSpecification(hw *models.HardwareConfig) []SpecLine {
var spec []SpecLine
hw := result.Hardware
if hw == nil {
return spec
}
devices := canonicalDevices(hw)
// CPUs - group by model
cpuGroups := make(map[string]int)
cpuDetails := make(map[string]models.CPU)
for _, cpu := range hw.CPUs {
cpuDetails := make(map[string]models.HardwareDevice)
for _, cpu := range devices {
if cpu.Kind != models.DeviceKindCPU {
continue
}
cpuGroups[cpu.Model]++
cpuDetails[cpu.Model] = cpu
}
@@ -198,21 +213,26 @@ func buildSpecification(result *models.AnalysisResult) []SpecLine {
model,
float64(cpu.FrequencyMHz)/1000,
cpu.Cores,
cpu.TDP)
intFromDetails(cpu.Details, "tdp_w"))
spec = append(spec, SpecLine{Category: "Процессор", Name: name, Quantity: count})
}
// Memory - group by size, type and frequency (only installed modules)
memGroups := make(map[string]int)
for _, mem := range hw.Memory {
for _, mem := range devices {
if mem.Kind != models.DeviceKindMemory {
continue
}
present := mem.Present != nil && *mem.Present
// Skip empty slots (not present or 0 size)
if !mem.Present || mem.SizeMB == 0 {
if !present || mem.SizeMB == 0 {
continue
}
// Include frequency if available
key := ""
if mem.CurrentSpeedMHz > 0 {
key = fmt.Sprintf("%s %dGB %dMHz", mem.Type, mem.SizeMB/1024, mem.CurrentSpeedMHz)
currentSpeed := intFromDetails(mem.Details, "current_speed_mhz")
if currentSpeed > 0 {
key = fmt.Sprintf("%s %dGB %dMHz", mem.Type, mem.SizeMB/1024, currentSpeed)
} else {
key = fmt.Sprintf("%s %dGB", mem.Type, mem.SizeMB/1024)
}
@@ -224,7 +244,10 @@ func buildSpecification(result *models.AnalysisResult) []SpecLine {
// Storage - group by type and capacity
storGroups := make(map[string]int)
for _, stor := range hw.Storage {
for _, stor := range devices {
if stor.Kind != models.DeviceKindStorage {
continue
}
var key string
if stor.SizeGB >= 1000 {
key = fmt.Sprintf("%s %s %.2fTB", stor.Type, stor.Interface, float64(stor.SizeGB)/1000)
@@ -239,8 +262,11 @@ func buildSpecification(result *models.AnalysisResult) []SpecLine {
// PCIe devices - group by device class/name and manufacturer
pcieGroups := make(map[string]int)
pcieDetails := make(map[string]models.PCIeDevice)
for _, pcie := range hw.PCIeDevices {
pcieDetails := make(map[string]models.HardwareDevice)
for _, pcie := range devices {
if pcie.Kind != models.DeviceKindPCIe && pcie.Kind != models.DeviceKindGPU && pcie.Kind != models.DeviceKindNetwork {
continue
}
// Create unique key from manufacturer + device class/name
key := pcie.DeviceClass
if pcie.Manufacturer != "" {
@@ -259,7 +285,7 @@ func buildSpecification(result *models.AnalysisResult) []SpecLine {
// Determine category based on device class or known GPU names
deviceClass := pcie.DeviceClass
isGPU := isGPUDevice(deviceClass)
isGPU := pcie.Kind == models.DeviceKindGPU || isGPUDevice(deviceClass)
isNetwork := deviceClass == "Network" || strings.Contains(deviceClass, "ConnectX")
if isGPU {
@@ -275,7 +301,10 @@ func buildSpecification(result *models.AnalysisResult) []SpecLine {
// Power supplies - group by model/wattage
psuGroups := make(map[string]int)
for _, psu := range hw.PowerSupply {
for _, psu := range devices {
if psu.Kind != models.DeviceKindPSU {
continue
}
key := psu.Model
if key == "" && psu.WattageW > 0 {
key = fmt.Sprintf("%dW", psu.WattageW)
@@ -309,23 +338,6 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
}
var serials []SerialEntry
seenByLocationSerial := make(map[string]bool)
markSeen := func(location, serial string) {
loc := strings.ToLower(strings.TrimSpace(location))
sn := strings.ToLower(strings.TrimSpace(serial))
if loc == "" || sn == "" {
return
}
seenByLocationSerial[loc+"|"+sn] = true
}
alreadySeen := func(location, serial string) bool {
loc := strings.ToLower(strings.TrimSpace(location))
sn := strings.ToLower(strings.TrimSpace(serial))
if loc == "" || sn == "" {
return false
}
return seenByLocationSerial[loc+"|"+sn]
}
// From FRU
for _, fru := range result.FRU {
@@ -345,132 +357,18 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
})
}
// From Hardware
if result.Hardware != nil {
// Board
if hasUsableSerial(result.Hardware.BoardInfo.SerialNumber) {
serials = append(serials, SerialEntry{
Component: result.Hardware.BoardInfo.ProductName,
SerialNumber: strings.TrimSpace(result.Hardware.BoardInfo.SerialNumber),
Manufacturer: result.Hardware.BoardInfo.Manufacturer,
PartNumber: result.Hardware.BoardInfo.PartNumber,
Category: "Board",
})
}
// CPUs
for _, cpu := range result.Hardware.CPUs {
if !hasUsableSerial(cpu.SerialNumber) {
for _, d := range canonicalDevices(result.Hardware) {
if !hasUsableSerial(d.SerialNumber) {
continue
}
serials = append(serials, SerialEntry{
Component: cpu.Model,
Location: fmt.Sprintf("CPU%d", cpu.Socket),
SerialNumber: strings.TrimSpace(cpu.SerialNumber),
Category: "CPU",
})
}
// Memory DIMMs
for _, mem := range result.Hardware.Memory {
if !hasUsableSerial(mem.SerialNumber) {
continue
}
location := mem.Location
if location == "" {
location = mem.Slot
}
serials = append(serials, SerialEntry{
Component: mem.PartNumber,
Location: location,
SerialNumber: strings.TrimSpace(mem.SerialNumber),
Manufacturer: mem.Manufacturer,
PartNumber: mem.PartNumber,
Category: "Memory",
})
}
// Storage
for _, stor := range result.Hardware.Storage {
if !hasUsableSerial(stor.SerialNumber) {
continue
}
serials = append(serials, SerialEntry{
Component: stor.Model,
Location: stor.Slot,
SerialNumber: strings.TrimSpace(stor.SerialNumber),
Manufacturer: stor.Manufacturer,
Category: "Storage",
})
}
// GPUs
for _, gpu := range result.Hardware.GPUs {
if !hasUsableSerial(gpu.SerialNumber) {
continue
}
model := gpu.Model
if model == "" {
model = "GPU"
}
serials = append(serials, SerialEntry{
Component: model,
Location: gpu.Slot,
SerialNumber: strings.TrimSpace(gpu.SerialNumber),
Manufacturer: gpu.Manufacturer,
Category: "GPU",
})
markSeen(gpu.Slot, gpu.SerialNumber)
}
// PCIe devices
for _, pcie := range result.Hardware.PCIeDevices {
if !hasUsableSerial(pcie.SerialNumber) {
continue
}
if alreadySeen(pcie.Slot, pcie.SerialNumber) {
continue
}
component := normalizePCIeSerialComponentName(pcie)
if strings.EqualFold(strings.TrimSpace(pcie.DeviceClass), "NVSwitch") && strings.TrimSpace(pcie.PartNumber) != "" {
component = strings.TrimSpace(pcie.PartNumber)
}
serials = append(serials, SerialEntry{
Component: component,
Location: pcie.Slot,
SerialNumber: strings.TrimSpace(pcie.SerialNumber),
Manufacturer: pcie.Manufacturer,
PartNumber: pcie.PartNumber,
Category: "PCIe",
})
markSeen(pcie.Slot, pcie.SerialNumber)
}
// Network cards
for _, nic := range result.Hardware.NetworkCards {
if !hasUsableSerial(nic.SerialNumber) {
continue
}
serials = append(serials, SerialEntry{
Component: nic.Model,
Location: nic.Name,
SerialNumber: strings.TrimSpace(nic.SerialNumber),
Category: "Network",
})
markSeen(nic.Name, nic.SerialNumber)
}
// Power supplies
for _, psu := range result.Hardware.PowerSupply {
if !hasUsableSerial(psu.SerialNumber) {
continue
}
serials = append(serials, SerialEntry{
Component: psu.Model,
Location: psu.Slot,
SerialNumber: strings.TrimSpace(psu.SerialNumber),
Manufacturer: psu.Vendor,
Category: "PSU",
Component: serialComponent(d),
Location: strings.TrimSpace(coalesce(d.Location, d.Slot)),
SerialNumber: strings.TrimSpace(d.SerialNumber),
Manufacturer: strings.TrimSpace(d.Manufacturer),
PartNumber: strings.TrimSpace(d.PartNumber),
Category: serialCategory(d.Kind),
})
}
}
@@ -566,26 +464,91 @@ func buildFirmwareEntries(hw *models.HardwareConfig) []firmwareEntry {
appendEntry(component, model, fw.Version)
}
// Fallback for parsers that fill GPU firmware on device inventory only
// (e.g. runtime enrichment from redis/HGX) without explicit Hardware.Firmware entries.
for _, gpu := range hw.GPUs {
version := strings.TrimSpace(gpu.Firmware)
for _, d := range canonicalDevices(hw) {
version := strings.TrimSpace(d.Firmware)
if version == "" {
continue
}
model := strings.TrimSpace(gpu.PartNumber)
model := strings.TrimSpace(d.PartNumber)
if model == "" {
model = strings.TrimSpace(gpu.Model)
model = strings.TrimSpace(d.Model)
}
if model == "" {
model = strings.TrimSpace(gpu.Slot)
model = strings.TrimSpace(d.Slot)
}
appendEntry("GPU", model, version)
appendEntry(serialCategory(d.Kind), model, version)
}
return deduplicated
}
func serialComponent(d models.HardwareDevice) string {
if strings.TrimSpace(d.Model) != "" {
return strings.TrimSpace(d.Model)
}
if strings.TrimSpace(d.PartNumber) != "" {
return strings.TrimSpace(d.PartNumber)
}
if d.Kind == models.DeviceKindPCIe {
return normalizePCIeSerialComponentName(models.PCIeDevice{
DeviceClass: d.DeviceClass,
PartNumber: d.PartNumber,
})
}
if strings.TrimSpace(d.DeviceClass) != "" {
return strings.TrimSpace(d.DeviceClass)
}
return strings.ToUpper(d.Kind)
}
func serialCategory(kind string) string {
switch kind {
case models.DeviceKindBoard:
return "Board"
case models.DeviceKindCPU:
return "CPU"
case models.DeviceKindMemory:
return "Memory"
case models.DeviceKindStorage:
return "Storage"
case models.DeviceKindGPU:
return "GPU"
case models.DeviceKindNetwork:
return "Network"
case models.DeviceKindPSU:
return "PSU"
default:
return "PCIe"
}
}
func intFromDetails(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
}
}
func coalesce(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
// extractFirmwareComponentAndModel extracts the component type and model from firmware device name
func extractFirmwareComponentAndModel(deviceName string) (component, model string) {
// Parse different firmware name formats and extract component + model