Introduce canonical hardware.devices repository and align UI/Reanimator exports
This commit is contained in:
+127
-164
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user