Files
core/internal/ingest/parser_hardware.go
Michael Chus 849235be22 Add PCIe link width and speed fields to hardware ingest
Fixes 400 Bad Request error when ingesting hardware snapshots that include
PCIe link speed information (link_width, link_speed, max_link_width, max_link_speed).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 21:53:44 +03:00

412 lines
12 KiB
Go

package ingest
import (
"encoding/json"
"fmt"
"strings"
)
const (
statusUnknown = "UNKNOWN"
statusEmpty = "EMPTY"
statusWarning = "WARNING"
statusCritical = "CRITICAL"
)
type HardwareIngestRequest struct {
Filename *string `json:"filename"`
SourceType *string `json:"source_type"`
Protocol *string `json:"protocol"`
TargetHost string `json:"target_host"`
CollectedAt string `json:"collected_at"`
Hardware HardwareSnapshot `json:"hardware"`
}
type HardwareSnapshot struct {
Board HardwareBoard `json:"board"`
Firmware []HardwareFirmwareRecord `json:"firmware,omitempty"`
CPUs []HardwareCPU `json:"cpus,omitempty"`
Memory []HardwareMemory `json:"memory,omitempty"`
Storage []HardwareStorage `json:"storage,omitempty"`
PCIeDevices []HardwarePCIeDevice `json:"pcie_devices,omitempty"`
PowerSupplies []HardwarePowerSupply `json:"power_supplies,omitempty"`
}
type HardwareBoard struct {
Manufacturer *string `json:"manufacturer"`
ProductName *string `json:"product_name"`
SerialNumber string `json:"serial_number"`
PartNumber *string `json:"part_number"`
UUID *string `json:"uuid"`
}
type HardwareFirmwareRecord struct {
DeviceName string `json:"device_name"`
Version string `json:"version"`
}
type HardwareCPU struct {
Socket *int `json:"socket"`
Model *string `json:"model"`
Manufacturer *string `json:"manufacturer"`
Status *string `json:"status"`
Present *bool `json:"present"`
SerialNumber *string `json:"serial_number"`
Firmware *string `json:"firmware"`
Cores *int `json:"cores"`
Threads *int `json:"threads"`
FrequencyMHz *int `json:"frequency_mhz"`
MaxFrequencyMHz *int `json:"max_frequency_mhz"`
}
type HardwareMemory struct {
Slot *string `json:"slot"`
Location *string `json:"location"`
Present *bool `json:"present"`
SizeMB *int `json:"size_mb"`
Type *string `json:"type"`
MaxSpeedMHz *int `json:"max_speed_mhz"`
CurrentSpeedMHz *int `json:"current_speed_mhz"`
Manufacturer *string `json:"manufacturer"`
SerialNumber *string `json:"serial_number"`
PartNumber *string `json:"part_number"`
Status *string `json:"status"`
}
type HardwareStorage struct {
Slot *string `json:"slot"`
Type *string `json:"type"`
Model *string `json:"model"`
SizeGB *int `json:"size_gb"`
SerialNumber *string `json:"serial_number"`
Manufacturer *string `json:"manufacturer"`
Firmware *string `json:"firmware"`
Interface *string `json:"interface"`
Present *bool `json:"present"`
Status *string `json:"status"`
}
type HardwarePCIeDevice struct {
Slot *string `json:"slot"`
VendorID *int `json:"vendor_id"`
DeviceID *int `json:"device_id"`
BDF *string `json:"bdf"`
DeviceClass *string `json:"device_class"`
Manufacturer *string `json:"manufacturer"`
Model *string `json:"model"`
LinkWidth *int `json:"link_width"`
LinkSpeed *string `json:"link_speed"`
MaxLinkWidth *int `json:"max_link_width"`
MaxLinkSpeed *string `json:"max_link_speed"`
SerialNumber *string `json:"serial_number"`
Firmware *string `json:"firmware"`
Present *bool `json:"present"`
Status *string `json:"status"`
}
type HardwarePowerSupply struct {
Slot *string `json:"slot"`
Present *bool `json:"present"`
Model *string `json:"model"`
Vendor *string `json:"vendor"`
WattageW *int `json:"wattage_w"`
SerialNumber *string `json:"serial_number"`
PartNumber *string `json:"part_number"`
Firmware *string `json:"firmware"`
Status *string `json:"status"`
InputType *string `json:"input_type"`
InputPowerW *float64 `json:"input_power_w"`
OutputPowerW *float64 `json:"output_power_w"`
InputVoltage *float64 `json:"input_voltage"`
}
type HardwareComponent struct {
ComponentType string `json:"component_type"`
VendorSerial string `json:"vendor_serial"`
Vendor *string `json:"vendor,omitempty"`
Model *string `json:"model,omitempty"`
Firmware *string `json:"firmware,omitempty"`
Status string `json:"status"`
Present *bool `json:"present,omitempty"`
Slot *string `json:"slot,omitempty"`
Attributes map[string]any `json:"attributes,omitempty"`
Telemetry map[string]any `json:"telemetry,omitempty"`
}
func FlattenHardwareComponents(snapshot HardwareSnapshot) ([]HardwareComponent, []HardwareFirmwareRecord) {
boardSerial := strings.TrimSpace(snapshot.Board.SerialNumber)
if boardSerial == "" {
return nil, normalizeFirmwareRecords(snapshot.Firmware)
}
components := make([]HardwareComponent, 0)
components = append(components, flattenCPUs(boardSerial, snapshot.CPUs)...)
components = append(components, flattenMemory(boardSerial, snapshot.Memory)...)
components = append(components, flattenStorage(boardSerial, snapshot.Storage)...)
components = append(components, flattenPCIe(boardSerial, snapshot.PCIeDevices)...)
components = append(components, flattenPSUs(boardSerial, snapshot.PowerSupplies)...)
firmware := normalizeFirmwareRecords(snapshot.Firmware)
return components, firmware
}
func normalizeFirmwareRecords(entries []HardwareFirmwareRecord) []HardwareFirmwareRecord {
normalized := make([]HardwareFirmwareRecord, 0, len(entries))
for _, entry := range entries {
name := strings.TrimSpace(entry.DeviceName)
version := strings.TrimSpace(entry.Version)
if name == "" || version == "" {
continue
}
normalized = append(normalized, HardwareFirmwareRecord{DeviceName: name, Version: version})
}
return normalized
}
func flattenCPUs(boardSerial string, items []HardwareCPU) []HardwareComponent {
result := make([]HardwareComponent, 0)
for _, item := range items {
status := normalizeStatus(item.Status)
present := resolvePresent(item.Present, true)
if !present || status == statusEmpty {
continue
}
serial := normalizeSerial(item.SerialNumber)
if serial == "" {
serial = generateCPUVendorSerial(boardSerial, item.Socket)
}
if serial == "" {
continue
}
comp := HardwareComponent{
ComponentType: "cpu",
VendorSerial: serial,
Vendor: normalizeString(item.Manufacturer),
Model: normalizeString(item.Model),
Firmware: normalizeString(item.Firmware),
Status: status,
Present: boolPtr(present),
Attributes: structToMap(item),
Telemetry: extractTelemetry(structToMap(item), []string{"cores", "threads", "frequency_mhz", "max_frequency_mhz"}),
}
result = append(result, comp)
}
return result
}
func flattenMemory(boardSerial string, items []HardwareMemory) []HardwareComponent {
result := make([]HardwareComponent, 0)
for _, item := range items {
status := normalizeStatus(item.Status)
present := resolvePresent(item.Present, true)
if !present || status == statusEmpty {
continue
}
serial := normalizeSerial(item.SerialNumber)
if serial == "" {
continue
}
comp := HardwareComponent{
ComponentType: "memory",
VendorSerial: serial,
Vendor: normalizeString(item.Manufacturer),
Model: normalizeString(item.PartNumber),
Firmware: nil,
Status: status,
Present: boolPtr(present),
Slot: normalizeString(item.Slot),
Attributes: structToMap(item),
Telemetry: extractTelemetry(structToMap(item), []string{"size_mb", "max_speed_mhz", "current_speed_mhz"}),
}
result = append(result, comp)
}
return result
}
func flattenStorage(boardSerial string, items []HardwareStorage) []HardwareComponent {
result := make([]HardwareComponent, 0)
for _, item := range items {
status := normalizeStatus(item.Status)
present := resolvePresent(item.Present, true)
if !present || status == statusEmpty {
continue
}
serial := normalizeSerial(item.SerialNumber)
if serial == "" {
continue
}
comp := HardwareComponent{
ComponentType: "storage",
VendorSerial: serial,
Vendor: normalizeString(item.Manufacturer),
Model: normalizeString(item.Model),
Firmware: normalizeString(item.Firmware),
Status: status,
Present: boolPtr(present),
Slot: normalizeString(item.Slot),
Attributes: structToMap(item),
Telemetry: extractTelemetry(structToMap(item), []string{"size_gb"}),
}
result = append(result, comp)
}
return result
}
func flattenPCIe(boardSerial string, items []HardwarePCIeDevice) []HardwareComponent {
result := make([]HardwareComponent, 0)
for _, item := range items {
status := normalizeStatus(item.Status)
present := resolvePresent(item.Present, true)
if !present || status == statusEmpty {
continue
}
serial := normalizeSerial(item.SerialNumber)
if serial == "" {
serial = generatePCIEVendorSerial(boardSerial, item.Slot)
}
if serial == "" {
continue
}
comp := HardwareComponent{
ComponentType: "pcie",
VendorSerial: serial,
Vendor: normalizeString(item.Manufacturer),
Model: normalizeString(item.Model),
Firmware: normalizeString(item.Firmware),
Status: status,
Present: boolPtr(present),
Slot: normalizeString(item.Slot),
Attributes: structToMap(item),
Telemetry: extractTelemetry(structToMap(item), []string{"vendor_id", "device_id"}),
}
result = append(result, comp)
}
return result
}
func flattenPSUs(boardSerial string, items []HardwarePowerSupply) []HardwareComponent {
result := make([]HardwareComponent, 0)
for _, item := range items {
status := normalizeStatus(item.Status)
present := resolvePresent(item.Present, true)
if !present || status == statusEmpty {
continue
}
serial := normalizeSerial(item.SerialNumber)
if serial == "" {
continue
}
comp := HardwareComponent{
ComponentType: "psu",
VendorSerial: serial,
Vendor: normalizeString(item.Vendor),
Model: normalizeString(item.Model),
Firmware: normalizeString(item.Firmware),
Status: status,
Present: boolPtr(present),
Slot: normalizeString(item.Slot),
Attributes: structToMap(item),
Telemetry: extractTelemetry(structToMap(item), []string{"wattage_w", "input_power_w", "output_power_w", "input_voltage"}),
}
result = append(result, comp)
}
return result
}
func normalizeSerial(value *string) string {
if value == nil {
return ""
}
trimmed := strings.TrimSpace(*value)
if trimmed == "" {
return ""
}
upper := strings.ToUpper(trimmed)
if upper == "NULL" || upper == "N/A" {
return ""
}
return trimmed
}
func generateCPUVendorSerial(boardSerial string, socket *int) string {
if boardSerial == "" || socket == nil {
return ""
}
return fmt.Sprintf("%s-CPU-%d", boardSerial, *socket)
}
func generatePCIEVendorSerial(boardSerial string, slot *string) string {
if boardSerial == "" || slot == nil {
return ""
}
trimmed := strings.TrimSpace(*slot)
if trimmed == "" {
return ""
}
return fmt.Sprintf("%s-PCIE-%s", boardSerial, trimmed)
}
func resolvePresent(value *bool, defaultValue bool) bool {
if value == nil {
return defaultValue
}
return *value
}
func normalizeStatus(value *string) string {
if value == nil {
return statusUnknown
}
normalized := strings.ToUpper(strings.TrimSpace(*value))
if normalized == "" {
return statusUnknown
}
return normalized
}
func boolPtr(value bool) *bool {
return &value
}
func normalizeString(value *string) *string {
if value == nil {
return nil
}
trimmed := strings.TrimSpace(*value)
if trimmed == "" || strings.EqualFold(trimmed, "NULL") {
return nil
}
return &trimmed
}
func structToMap(value any) map[string]any {
data, err := json.Marshal(value)
if err != nil {
return nil
}
var result map[string]any
if err := json.Unmarshal(data, &result); err != nil {
return nil
}
return result
}
func extractTelemetry(attributes map[string]any, keys []string) map[string]any {
if len(attributes) == 0 {
return nil
}
telemetry := make(map[string]any)
for _, key := range keys {
if v, ok := attributes[key]; ok {
switch v.(type) {
case float64, int, int64, float32:
telemetry[key] = v
}
}
}
if len(telemetry) == 0 {
return nil
}
return telemetry
}