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>
412 lines
12 KiB
Go
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
|
|
}
|