Sync hardware ingest contract v2.7
This commit is contained in:
@@ -24,7 +24,7 @@ func Run(_ runtimeenv.Mode) schema.HardwareIngestRequest {
|
|||||||
snap.Board = board
|
snap.Board = board
|
||||||
snap.Firmware = append(snap.Firmware, biosFW...)
|
snap.Firmware = append(snap.Firmware, biosFW...)
|
||||||
|
|
||||||
snap.CPUs = collectCPUs(snap.Board.SerialNumber)
|
snap.CPUs = collectCPUs()
|
||||||
|
|
||||||
snap.Memory = collectMemory()
|
snap.Memory = collectMemory()
|
||||||
sensorDoc, err := readSensorsJSONDoc()
|
sensorDoc, err := readSensorsJSONDoc()
|
||||||
@@ -35,7 +35,7 @@ func Run(_ runtimeenv.Mode) schema.HardwareIngestRequest {
|
|||||||
snap.Memory = enrichMemoryWithTelemetry(snap.Memory, sensorDoc)
|
snap.Memory = enrichMemoryWithTelemetry(snap.Memory, sensorDoc)
|
||||||
snap.Storage = collectStorage()
|
snap.Storage = collectStorage()
|
||||||
snap.PCIeDevices = collectPCIe()
|
snap.PCIeDevices = collectPCIe()
|
||||||
snap.PCIeDevices = enrichPCIeWithNVIDIA(snap.PCIeDevices, snap.Board.SerialNumber)
|
snap.PCIeDevices = enrichPCIeWithNVIDIA(snap.PCIeDevices)
|
||||||
snap.PCIeDevices = enrichPCIeWithMellanox(snap.PCIeDevices)
|
snap.PCIeDevices = enrichPCIeWithMellanox(snap.PCIeDevices)
|
||||||
snap.PCIeDevices = enrichPCIeWithNICTelemetry(snap.PCIeDevices)
|
snap.PCIeDevices = enrichPCIeWithNICTelemetry(snap.PCIeDevices)
|
||||||
snap.PCIeDevices = enrichPCIeWithRAIDTelemetry(snap.PCIeDevices)
|
snap.PCIeDevices = enrichPCIeWithRAIDTelemetry(snap.PCIeDevices)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package collector
|
|||||||
import (
|
import (
|
||||||
"bee/audit/internal/schema"
|
"bee/audit/internal/schema"
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -12,14 +11,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// collectCPUs runs dmidecode -t 4 and enriches CPUs with microcode from sysfs.
|
// collectCPUs runs dmidecode -t 4 and enriches CPUs with microcode from sysfs.
|
||||||
func collectCPUs(boardSerial string) []schema.HardwareCPU {
|
func collectCPUs() []schema.HardwareCPU {
|
||||||
out, err := runDmidecode("4")
|
out, err := runDmidecode("4")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("cpu: dmidecode type 4 failed", "err", err)
|
slog.Warn("cpu: dmidecode type 4 failed", "err", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cpus := parseCPUs(out, boardSerial)
|
cpus := parseCPUs(out)
|
||||||
if mc := readMicrocode(); mc != "" {
|
if mc := readMicrocode(); mc != "" {
|
||||||
for i := range cpus {
|
for i := range cpus {
|
||||||
cpus[i].Firmware = &mc
|
cpus[i].Firmware = &mc
|
||||||
@@ -31,12 +30,12 @@ func collectCPUs(boardSerial string) []schema.HardwareCPU {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseCPUs splits dmidecode output into per-processor sections and parses each.
|
// parseCPUs splits dmidecode output into per-processor sections and parses each.
|
||||||
func parseCPUs(output, boardSerial string) []schema.HardwareCPU {
|
func parseCPUs(output string) []schema.HardwareCPU {
|
||||||
sections := splitDMISections(output, "Processor Information")
|
sections := splitDMISections(output, "Processor Information")
|
||||||
cpus := make([]schema.HardwareCPU, 0, len(sections))
|
cpus := make([]schema.HardwareCPU, 0, len(sections))
|
||||||
|
|
||||||
for _, section := range sections {
|
for _, section := range sections {
|
||||||
cpu, ok := parseCPUSection(section, boardSerial)
|
cpu, ok := parseCPUSection(section)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -47,7 +46,7 @@ func parseCPUs(output, boardSerial string) []schema.HardwareCPU {
|
|||||||
|
|
||||||
// parseCPUSection parses one "Processor Information" block into a HardwareCPU.
|
// parseCPUSection parses one "Processor Information" block into a HardwareCPU.
|
||||||
// Returns false if the socket is unpopulated.
|
// Returns false if the socket is unpopulated.
|
||||||
func parseCPUSection(fields map[string]string, boardSerial string) (schema.HardwareCPU, bool) {
|
func parseCPUSection(fields map[string]string) (schema.HardwareCPU, bool) {
|
||||||
status := parseCPUStatus(fields["Status"])
|
status := parseCPUStatus(fields["Status"])
|
||||||
if status == statusEmpty {
|
if status == statusEmpty {
|
||||||
return schema.HardwareCPU{}, false
|
return schema.HardwareCPU{}, false
|
||||||
@@ -70,11 +69,6 @@ func parseCPUSection(fields map[string]string, boardSerial string) (schema.Hardw
|
|||||||
}
|
}
|
||||||
if v := cleanDMIValue(fields["Serial Number"]); v != "" {
|
if v := cleanDMIValue(fields["Serial Number"]); v != "" {
|
||||||
cpu.SerialNumber = &v
|
cpu.SerialNumber = &v
|
||||||
} else if boardSerial != "" && cpu.Socket != nil {
|
|
||||||
// Intel Xeon never exposes serial via DMI — generate stable fallback
|
|
||||||
// matching core's generateCPUVendorSerial() logic
|
|
||||||
fb := fmt.Sprintf("%s-CPU-%d", boardSerial, *cpu.Socket)
|
|
||||||
cpu.SerialNumber = &fb
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if v := parseMHz(fields["Max Speed"]); v > 0 {
|
if v := parseMHz(fields["Max Speed"]); v > 0 {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
func TestParseCPUs_dual_socket(t *testing.T) {
|
func TestParseCPUs_dual_socket(t *testing.T) {
|
||||||
out := mustReadFile(t, "testdata/dmidecode_type4.txt")
|
out := mustReadFile(t, "testdata/dmidecode_type4.txt")
|
||||||
cpus := parseCPUs(out, "CAR315KA0803B90")
|
cpus := parseCPUs(out)
|
||||||
|
|
||||||
if len(cpus) != 2 {
|
if len(cpus) != 2 {
|
||||||
t.Fatalf("expected 2 CPUs, got %d", len(cpus))
|
t.Fatalf("expected 2 CPUs, got %d", len(cpus))
|
||||||
@@ -39,23 +39,22 @@ func TestParseCPUs_dual_socket(t *testing.T) {
|
|||||||
if cpu0.Status == nil || *cpu0.Status != "OK" {
|
if cpu0.Status == nil || *cpu0.Status != "OK" {
|
||||||
t.Errorf("cpu0 status: got %v, want OK", cpu0.Status)
|
t.Errorf("cpu0 status: got %v, want OK", cpu0.Status)
|
||||||
}
|
}
|
||||||
// Intel Xeon serial not available → fallback
|
if cpu0.SerialNumber != nil {
|
||||||
if cpu0.SerialNumber == nil || *cpu0.SerialNumber != "CAR315KA0803B90-CPU-0" {
|
t.Errorf("cpu0 serial should stay nil without source data, got %v", cpu0.SerialNumber)
|
||||||
t.Errorf("cpu0 serial fallback: got %v, want CAR315KA0803B90-CPU-0", cpu0.SerialNumber)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cpu1 := cpus[1]
|
cpu1 := cpus[1]
|
||||||
if cpu1.Socket == nil || *cpu1.Socket != 1 {
|
if cpu1.Socket == nil || *cpu1.Socket != 1 {
|
||||||
t.Errorf("cpu1 socket: got %v, want 1", cpu1.Socket)
|
t.Errorf("cpu1 socket: got %v, want 1", cpu1.Socket)
|
||||||
}
|
}
|
||||||
if cpu1.SerialNumber == nil || *cpu1.SerialNumber != "CAR315KA0803B90-CPU-1" {
|
if cpu1.SerialNumber != nil {
|
||||||
t.Errorf("cpu1 serial fallback: got %v, want CAR315KA0803B90-CPU-1", cpu1.SerialNumber)
|
t.Errorf("cpu1 serial should stay nil without source data, got %v", cpu1.SerialNumber)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseCPUs_unpopulated_skipped(t *testing.T) {
|
func TestParseCPUs_unpopulated_skipped(t *testing.T) {
|
||||||
out := mustReadFile(t, "testdata/dmidecode_type4_disabled.txt")
|
out := mustReadFile(t, "testdata/dmidecode_type4_disabled.txt")
|
||||||
cpus := parseCPUs(out, "BOARD-001")
|
cpus := parseCPUs(out)
|
||||||
|
|
||||||
if len(cpus) != 1 {
|
if len(cpus) != 1 {
|
||||||
t.Fatalf("expected 1 CPU (unpopulated skipped), got %d", len(cpus))
|
t.Fatalf("expected 1 CPU (unpopulated skipped), got %d", len(cpus))
|
||||||
@@ -87,7 +86,7 @@ func TestCollectCPUsSetsFirmwareFromMicrocode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Cleanup(func() { execDmidecode = origRun })
|
t.Cleanup(func() { execDmidecode = origRun })
|
||||||
|
|
||||||
cpus := collectCPUs("CAR315KA0803B90")
|
cpus := collectCPUs()
|
||||||
if len(cpus) != 2 {
|
if len(cpus) != 2 {
|
||||||
t.Fatalf("expected 2 CPUs, got %d", len(cpus))
|
t.Fatalf("expected 2 CPUs, got %d", len(cpus))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package collector
|
package collector
|
||||||
|
|
||||||
import (
|
import "bee/audit/internal/schema"
|
||||||
"bee/audit/internal/schema"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func finalizeSnapshot(snap *schema.HardwareSnapshot, collectedAt string) {
|
func finalizeSnapshot(snap *schema.HardwareSnapshot, collectedAt string) {
|
||||||
snap.Memory = filterMemory(snap.Memory)
|
snap.Memory = filterMemory(snap.Memory)
|
||||||
@@ -11,7 +8,6 @@ func finalizeSnapshot(snap *schema.HardwareSnapshot, collectedAt string) {
|
|||||||
snap.PowerSupplies = filterPSUs(snap.PowerSupplies)
|
snap.PowerSupplies = filterPSUs(snap.PowerSupplies)
|
||||||
|
|
||||||
setComponentStatusMetadata(snap, collectedAt)
|
setComponentStatusMetadata(snap, collectedAt)
|
||||||
deduplicateComponentSerials(snap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterMemory(dimms []schema.HardwareMemory) []schema.HardwareMemory {
|
func filterMemory(dimms []schema.HardwareMemory) []schema.HardwareMemory {
|
||||||
@@ -79,101 +75,3 @@ func setStatusCheckedAt(status *schema.HardwareComponentStatus, collectedAt stri
|
|||||||
status.StatusCheckedAt = &collectedAt
|
status.StatusCheckedAt = &collectedAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deduplicateComponentSerials(snap *schema.HardwareSnapshot) {
|
|
||||||
deduplicateCPUSerials(snap.CPUs)
|
|
||||||
deduplicateMemorySerials(snap.Memory)
|
|
||||||
deduplicateStorageSerials(snap.Storage)
|
|
||||||
deduplicatePCIeSerials(snap.PCIeDevices)
|
|
||||||
deduplicatePSUSerials(snap.PowerSupplies)
|
|
||||||
}
|
|
||||||
|
|
||||||
func deduplicateCPUSerials(items []schema.HardwareCPU) {
|
|
||||||
seen := map[string]int{}
|
|
||||||
seq := 1
|
|
||||||
for i := range items {
|
|
||||||
if items[i].SerialNumber == nil || *items[i].SerialNumber == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
model := derefString(items[i].Model)
|
|
||||||
key := model + "\x00" + *items[i].SerialNumber
|
|
||||||
seen[key]++
|
|
||||||
if seen[key] > 1 {
|
|
||||||
repl := fmt.Sprintf("NO_SN-%08d", seq)
|
|
||||||
seq++
|
|
||||||
items[i].SerialNumber = &repl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func deduplicateMemorySerials(items []schema.HardwareMemory) {
|
|
||||||
seen := map[string]int{}
|
|
||||||
seq := 1
|
|
||||||
for i := range items {
|
|
||||||
if items[i].SerialNumber == nil || *items[i].SerialNumber == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
model := derefString(items[i].PartNumber)
|
|
||||||
key := model + "\x00" + *items[i].SerialNumber
|
|
||||||
seen[key]++
|
|
||||||
if seen[key] > 1 {
|
|
||||||
repl := fmt.Sprintf("NO_SN-%08d", seq)
|
|
||||||
seq++
|
|
||||||
items[i].SerialNumber = &repl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func deduplicateStorageSerials(items []schema.HardwareStorage) {
|
|
||||||
seen := map[string]int{}
|
|
||||||
seq := 1
|
|
||||||
for i := range items {
|
|
||||||
if items[i].SerialNumber == nil || *items[i].SerialNumber == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
model := derefString(items[i].Model)
|
|
||||||
key := model + "\x00" + *items[i].SerialNumber
|
|
||||||
seen[key]++
|
|
||||||
if seen[key] > 1 {
|
|
||||||
repl := fmt.Sprintf("NO_SN-%08d", seq)
|
|
||||||
seq++
|
|
||||||
items[i].SerialNumber = &repl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func deduplicatePCIeSerials(items []schema.HardwarePCIeDevice) {
|
|
||||||
seen := map[string]int{}
|
|
||||||
seq := 1
|
|
||||||
for i := range items {
|
|
||||||
if items[i].SerialNumber == nil || *items[i].SerialNumber == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
model := derefString(items[i].Model)
|
|
||||||
key := model + "\x00" + *items[i].SerialNumber
|
|
||||||
seen[key]++
|
|
||||||
if seen[key] > 1 {
|
|
||||||
repl := fmt.Sprintf("NO_SN-%08d", seq)
|
|
||||||
seq++
|
|
||||||
items[i].SerialNumber = &repl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func deduplicatePSUSerials(items []schema.HardwarePowerSupply) {
|
|
||||||
seen := map[string]int{}
|
|
||||||
seq := 1
|
|
||||||
for i := range items {
|
|
||||||
if items[i].SerialNumber == nil || *items[i].SerialNumber == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
model := derefString(items[i].Model)
|
|
||||||
key := model + "\x00" + *items[i].SerialNumber
|
|
||||||
seen[key]++
|
|
||||||
if seen[key] > 1 {
|
|
||||||
repl := fmt.Sprintf("NO_SN-%08d", seq)
|
|
||||||
seq++
|
|
||||||
items[i].SerialNumber = &repl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func TestFinalizeSnapshotFiltersComponentsWithoutRequiredSerials(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFinalizeSnapshotDeduplicatesSerials(t *testing.T) {
|
func TestFinalizeSnapshotPreservesDuplicateSerials(t *testing.T) {
|
||||||
collectedAt := "2026-03-15T12:00:00Z"
|
collectedAt := "2026-03-15T12:00:00Z"
|
||||||
status := statusOK
|
status := statusOK
|
||||||
model := "Device"
|
model := "Device"
|
||||||
@@ -57,7 +57,7 @@ func TestFinalizeSnapshotDeduplicatesSerials(t *testing.T) {
|
|||||||
if got := *snap.Storage[0].SerialNumber; got != serial {
|
if got := *snap.Storage[0].SerialNumber; got != serial {
|
||||||
t.Fatalf("first serial changed: %q", got)
|
t.Fatalf("first serial changed: %q", got)
|
||||||
}
|
}
|
||||||
if got := *snap.Storage[1].SerialNumber; got != "NO_SN-00000001" {
|
if got := *snap.Storage[1].SerialNumber; got != serial {
|
||||||
t.Fatalf("duplicate serial mismatch: %q", got)
|
t.Fatalf("duplicate serial should stay unchanged: %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,18 +24,17 @@ type nvidiaGPUInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// enrichPCIeWithNVIDIA enriches NVIDIA PCIe devices with data from nvidia-smi.
|
// enrichPCIeWithNVIDIA enriches NVIDIA PCIe devices with data from nvidia-smi.
|
||||||
// If the driver/tool is unavailable, NVIDIA devices get Unknown status and
|
// If the driver/tool is unavailable, NVIDIA devices get Unknown status.
|
||||||
// a stable serial fallback based on board serial + slot.
|
func enrichPCIeWithNVIDIA(devs []schema.HardwarePCIeDevice) []schema.HardwarePCIeDevice {
|
||||||
func enrichPCIeWithNVIDIA(devs []schema.HardwarePCIeDevice, boardSerial string) []schema.HardwarePCIeDevice {
|
|
||||||
if !hasNVIDIADevices(devs) {
|
if !hasNVIDIADevices(devs) {
|
||||||
return devs
|
return devs
|
||||||
}
|
}
|
||||||
gpuByBDF, err := queryNVIDIAGPUs()
|
gpuByBDF, err := queryNVIDIAGPUs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Info("nvidia: enrichment skipped", "err", err)
|
slog.Info("nvidia: enrichment skipped", "err", err)
|
||||||
return enrichPCIeWithNVIDIAData(devs, nil, boardSerial, false)
|
return enrichPCIeWithNVIDIAData(devs, nil, false)
|
||||||
}
|
}
|
||||||
return enrichPCIeWithNVIDIAData(devs, gpuByBDF, boardSerial, true)
|
return enrichPCIeWithNVIDIAData(devs, gpuByBDF, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasNVIDIADevices(devs []schema.HardwarePCIeDevice) bool {
|
func hasNVIDIADevices(devs []schema.HardwarePCIeDevice) bool {
|
||||||
@@ -47,7 +46,7 @@ func hasNVIDIADevices(devs []schema.HardwarePCIeDevice) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func enrichPCIeWithNVIDIAData(devs []schema.HardwarePCIeDevice, gpuByBDF map[string]nvidiaGPUInfo, boardSerial string, driverLoaded bool) []schema.HardwarePCIeDevice {
|
func enrichPCIeWithNVIDIAData(devs []schema.HardwarePCIeDevice, gpuByBDF map[string]nvidiaGPUInfo, driverLoaded bool) []schema.HardwarePCIeDevice {
|
||||||
enriched := 0
|
enriched := 0
|
||||||
for i := range devs {
|
for i := range devs {
|
||||||
if !isNVIDIADevice(devs[i]) {
|
if !isNVIDIADevice(devs[i]) {
|
||||||
@@ -55,7 +54,7 @@ func enrichPCIeWithNVIDIAData(devs []schema.HardwarePCIeDevice, gpuByBDF map[str
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !driverLoaded {
|
if !driverLoaded {
|
||||||
setPCIeFallback(&devs[i], boardSerial)
|
setPCIeFallback(&devs[i])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,14 +64,12 @@ func enrichPCIeWithNVIDIAData(devs []schema.HardwarePCIeDevice, gpuByBDF map[str
|
|||||||
}
|
}
|
||||||
info, ok := gpuByBDF[bdf]
|
info, ok := gpuByBDF[bdf]
|
||||||
if !ok {
|
if !ok {
|
||||||
setPCIeFallback(&devs[i], boardSerial)
|
setPCIeFallback(&devs[i])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if v := strings.TrimSpace(info.Serial); v != "" {
|
if v := strings.TrimSpace(info.Serial); v != "" {
|
||||||
devs[i].SerialNumber = &v
|
devs[i].SerialNumber = &v
|
||||||
} else {
|
|
||||||
setPCIeFallbackSerial(&devs[i], boardSerial)
|
|
||||||
}
|
}
|
||||||
if v := strings.TrimSpace(info.VBIOS); v != "" {
|
if v := strings.TrimSpace(info.VBIOS); v != "" {
|
||||||
devs[i].Firmware = &v
|
devs[i].Firmware = &v
|
||||||
@@ -213,26 +210,11 @@ func isNVIDIADevice(dev schema.HardwarePCIeDevice) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func setPCIeFallback(dev *schema.HardwarePCIeDevice, boardSerial string) {
|
func setPCIeFallback(dev *schema.HardwarePCIeDevice) {
|
||||||
setPCIeFallbackSerial(dev, boardSerial)
|
|
||||||
status := statusUnknown
|
status := statusUnknown
|
||||||
dev.Status = &status
|
dev.Status = &status
|
||||||
}
|
}
|
||||||
|
|
||||||
func setPCIeFallbackSerial(dev *schema.HardwarePCIeDevice, boardSerial string) {
|
|
||||||
if strings.TrimSpace(boardSerial) == "" || dev.SerialNumber != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
slot := "unknown"
|
|
||||||
if dev.BDF != nil && strings.TrimSpace(*dev.BDF) != "" {
|
|
||||||
slot = strings.TrimSpace(*dev.BDF)
|
|
||||||
} else if dev.Slot != nil && strings.TrimSpace(*dev.Slot) != "" {
|
|
||||||
slot = strings.TrimSpace(*dev.Slot)
|
|
||||||
}
|
|
||||||
fb := fmt.Sprintf("%s-PCIE-%s", boardSerial, slot)
|
|
||||||
dev.SerialNumber = &fb
|
|
||||||
}
|
|
||||||
|
|
||||||
func injectNVIDIATelemetry(dev *schema.HardwarePCIeDevice, info nvidiaGPUInfo) {
|
func injectNVIDIATelemetry(dev *schema.HardwarePCIeDevice, info nvidiaGPUInfo) {
|
||||||
if info.TemperatureC != nil {
|
if info.TemperatureC != nil {
|
||||||
dev.TemperatureC = info.TemperatureC
|
dev.TemperatureC = info.TemperatureC
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func TestEnrichPCIeWithNVIDIAData_driverLoaded(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := enrichPCIeWithNVIDIAData(devices, byBDF, "BOARD-001", true)
|
out := enrichPCIeWithNVIDIAData(devices, byBDF, true)
|
||||||
if out[0].SerialNumber == nil || *out[0].SerialNumber != "GPU-ABC" {
|
if out[0].SerialNumber == nil || *out[0].SerialNumber != "GPU-ABC" {
|
||||||
t.Fatalf("serial: got %v", out[0].SerialNumber)
|
t.Fatalf("serial: got %v", out[0].SerialNumber)
|
||||||
}
|
}
|
||||||
@@ -103,9 +103,9 @@ func TestEnrichPCIeWithNVIDIAData_driverMissingFallback(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := enrichPCIeWithNVIDIAData(devices, nil, "BOARD-123", false)
|
out := enrichPCIeWithNVIDIAData(devices, nil, false)
|
||||||
if out[0].SerialNumber == nil || *out[0].SerialNumber != "BOARD-123-PCIE-0000:17:00.0" {
|
if out[0].SerialNumber != nil {
|
||||||
t.Fatalf("fallback serial: got %v", out[0].SerialNumber)
|
t.Fatalf("serial should stay nil without source data, got %v", out[0].SerialNumber)
|
||||||
}
|
}
|
||||||
if out[0].Status == nil || *out[0].Status != statusUnknown {
|
if out[0].Status == nil || *out[0].Status != statusUnknown {
|
||||||
t.Fatalf("fallback status: got %v", out[0].Status)
|
t.Fatalf("fallback status: got %v", out[0].Status)
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ func parseLspciDevice(fields map[string]string) schema.HardwarePCIeDevice {
|
|||||||
|
|
||||||
// Slot is the BDF: "0000:00:02.0"
|
// Slot is the BDF: "0000:00:02.0"
|
||||||
if bdf := fields["Slot"]; bdf != "" {
|
if bdf := fields["Slot"]; bdf != "" {
|
||||||
|
dev.Slot = &bdf
|
||||||
dev.BDF = &bdf
|
dev.BDF = &bdf
|
||||||
// parse vendor_id and device_id from sysfs
|
// parse vendor_id and device_id from sysfs
|
||||||
vendorID, deviceID := readPCIIDs(bdf)
|
vendorID, deviceID := readPCIIDs(bdf)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package collector
|
package collector
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestShouldIncludePCIeDevice(t *testing.T) {
|
func TestShouldIncludePCIeDevice(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -38,6 +42,29 @@ func TestParseLspci_filtersExcludedClasses(t *testing.T) {
|
|||||||
if devs[0].DeviceClass == nil || *devs[0].DeviceClass != "VideoController" {
|
if devs[0].DeviceClass == nil || *devs[0].DeviceClass != "VideoController" {
|
||||||
t.Fatalf("unexpected remaining class: %v", devs[0].DeviceClass)
|
t.Fatalf("unexpected remaining class: %v", devs[0].DeviceClass)
|
||||||
}
|
}
|
||||||
|
if devs[0].Slot == nil || *devs[0].Slot != "0000:65:00.0" {
|
||||||
|
t.Fatalf("slot: got %v", devs[0].Slot)
|
||||||
|
}
|
||||||
|
if devs[0].BDF == nil || *devs[0].BDF != "0000:65:00.0" {
|
||||||
|
t.Fatalf("bdf: got %v", devs[0].BDF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPCIeJSONUsesSlotNotBDF(t *testing.T) {
|
||||||
|
input := "Slot:\t0000:65:00.0\nClass:\tVGA compatible controller\nVendor:\tNVIDIA Corporation\nDevice:\tH100\n\n"
|
||||||
|
|
||||||
|
devs := parseLspci(input)
|
||||||
|
data, err := json.Marshal(devs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
text := string(data)
|
||||||
|
if !strings.Contains(text, `"slot":"0000:65:00.0"`) {
|
||||||
|
t.Fatalf("json missing slot: %s", text)
|
||||||
|
}
|
||||||
|
if strings.Contains(text, `"bdf"`) {
|
||||||
|
t.Fatalf("json should not emit bdf: %s", text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizePCILinkSpeed(t *testing.T) {
|
func TestNormalizePCILinkSpeed(t *testing.T) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type HardwareSnapshot struct {
|
|||||||
PCIeDevices []HardwarePCIeDevice `json:"pcie_devices,omitempty"`
|
PCIeDevices []HardwarePCIeDevice `json:"pcie_devices,omitempty"`
|
||||||
PowerSupplies []HardwarePowerSupply `json:"power_supplies,omitempty"`
|
PowerSupplies []HardwarePowerSupply `json:"power_supplies,omitempty"`
|
||||||
Sensors *HardwareSensors `json:"sensors,omitempty"`
|
Sensors *HardwareSensors `json:"sensors,omitempty"`
|
||||||
|
EventLogs []HardwareEventLog `json:"event_logs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HardwareHealthSummary struct {
|
type HardwareHealthSummary struct {
|
||||||
@@ -148,7 +149,7 @@ type HardwarePCIeDevice struct {
|
|||||||
SFPRXPowerDBM *float64 `json:"sfp_rx_power_dbm,omitempty"`
|
SFPRXPowerDBM *float64 `json:"sfp_rx_power_dbm,omitempty"`
|
||||||
SFPVoltageV *float64 `json:"sfp_voltage_v,omitempty"`
|
SFPVoltageV *float64 `json:"sfp_voltage_v,omitempty"`
|
||||||
SFPBiasMA *float64 `json:"sfp_bias_ma,omitempty"`
|
SFPBiasMA *float64 `json:"sfp_bias_ma,omitempty"`
|
||||||
BDF *string `json:"bdf,omitempty"`
|
BDF *string `json:"-"`
|
||||||
DeviceClass *string `json:"device_class,omitempty"`
|
DeviceClass *string `json:"device_class,omitempty"`
|
||||||
Manufacturer *string `json:"manufacturer,omitempty"`
|
Manufacturer *string `json:"manufacturer,omitempty"`
|
||||||
Model *string `json:"model,omitempty"`
|
Model *string `json:"model,omitempty"`
|
||||||
@@ -183,11 +184,12 @@ type HardwarePowerSupply struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HardwareComponentStatus struct {
|
type HardwareComponentStatus struct {
|
||||||
Status *string `json:"status,omitempty"`
|
Status *string `json:"status,omitempty"`
|
||||||
StatusCheckedAt *string `json:"status_checked_at,omitempty"`
|
StatusCheckedAt *string `json:"status_checked_at,omitempty"`
|
||||||
StatusChangedAt *string `json:"status_changed_at,omitempty"`
|
StatusChangedAt *string `json:"status_changed_at,omitempty"`
|
||||||
StatusHistory []HardwareStatusHistory `json:"status_history,omitempty"`
|
StatusHistory []HardwareStatusHistory `json:"status_history,omitempty"`
|
||||||
ErrorDescription *string `json:"error_description,omitempty"`
|
ErrorDescription *string `json:"error_description,omitempty"`
|
||||||
|
ManufacturedYearWeek *string `json:"manufactured_year_week,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HardwareStatusHistory struct {
|
type HardwareStatusHistory struct {
|
||||||
@@ -235,3 +237,15 @@ type HardwareOtherSensor struct {
|
|||||||
Unit *string `json:"unit,omitempty"`
|
Unit *string `json:"unit,omitempty"`
|
||||||
Status *string `json:"status,omitempty"`
|
Status *string `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HardwareEventLog struct {
|
||||||
|
Source string `json:"source"`
|
||||||
|
EventTime *string `json:"event_time,omitempty"`
|
||||||
|
Severity *string `json:"severity,omitempty"`
|
||||||
|
MessageID *string `json:"message_id,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
ComponentRef *string `json:"component_ref,omitempty"`
|
||||||
|
Fingerprint *string `json:"fingerprint,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
RawPayload map[string]any `json:"raw_payload,omitempty"`
|
||||||
|
}
|
||||||
|
|||||||
46
audit/internal/schema/hardware_test.go
Normal file
46
audit/internal/schema/hardware_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHardwareSnapshotMarshalsNewContractFields(t *testing.T) {
|
||||||
|
week := "2024-W07"
|
||||||
|
eventTime := "2026-03-15T14:03:11Z"
|
||||||
|
message := "Correctable ECC error threshold exceeded"
|
||||||
|
|
||||||
|
payload := HardwareIngestRequest{
|
||||||
|
CollectedAt: "2026-03-15T15:00:00Z",
|
||||||
|
Hardware: HardwareSnapshot{
|
||||||
|
Board: HardwareBoard{SerialNumber: "SRV-001"},
|
||||||
|
CPUs: []HardwareCPU{
|
||||||
|
{
|
||||||
|
HardwareComponentStatus: HardwareComponentStatus{
|
||||||
|
ManufacturedYearWeek: &week,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EventLogs: []HardwareEventLog{
|
||||||
|
{
|
||||||
|
Source: "bmc",
|
||||||
|
EventTime: &eventTime,
|
||||||
|
Message: message,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
text := string(data)
|
||||||
|
if !strings.Contains(text, `"manufactured_year_week":"2024-W07"`) {
|
||||||
|
t.Fatalf("missing manufactured_year_week: %s", text)
|
||||||
|
}
|
||||||
|
if !strings.Contains(text, `"event_logs":[{"source":"bmc","event_time":"2026-03-15T14:03:11Z","message":"Correctable ECC error threshold exceeded"}]`) {
|
||||||
|
t.Fatalf("missing event_logs payload: %s", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Hardware Ingest JSON Contract
|
title: Hardware Ingest JSON Contract
|
||||||
version: "2.1"
|
version: "2.7"
|
||||||
updated: "2026-03-15"
|
updated: "2026-03-15"
|
||||||
maintainer: Reanimator Core
|
maintainer: Reanimator Core
|
||||||
audience: external-integrators, ai-agents
|
audience: external-integrators, ai-agents
|
||||||
@@ -9,7 +9,7 @@ language: ru
|
|||||||
|
|
||||||
# Интеграция с Reanimator: контракт JSON-импорта аппаратного обеспечения
|
# Интеграция с Reanimator: контракт JSON-импорта аппаратного обеспечения
|
||||||
|
|
||||||
Версия: **2.1** · Дата: **2026-03-15**
|
Версия: **2.7** · Дата: **2026-03-15**
|
||||||
|
|
||||||
Документ описывает формат JSON для передачи данных об аппаратном обеспечении серверов в систему **Reanimator** (управление жизненным циклом аппаратного обеспечения).
|
Документ описывает формат JSON для передачи данных об аппаратном обеспечении серверов в систему **Reanimator** (управление жизненным циклом аппаратного обеспечения).
|
||||||
Предназначен для разработчиков смежных систем (Redfish-коллекторов, агентов мониторинга, CMDB-экспортёров) и может быть включён в документацию интегрируемых проектов.
|
Предназначен для разработчиков смежных систем (Redfish-коллекторов, агентов мониторинга, CMDB-экспортёров) и может быть включён в документацию интегрируемых проектов.
|
||||||
@@ -22,6 +22,9 @@ language: ru
|
|||||||
|
|
||||||
| Версия | Дата | Изменения |
|
| Версия | Дата | Изменения |
|
||||||
|--------|------|-----------|
|
|--------|------|-----------|
|
||||||
|
| 2.7 | 2026-03-15 | Явно запрещён синтез данных в `event_logs`; интеграторы не должны придумывать серийные номера компонентов, если источник их не отдал |
|
||||||
|
| 2.6 | 2026-03-15 | Добавлена необязательная секция `event_logs` для dedup/upsert логов `host` / `bmc` / `redfish` вне history timeline |
|
||||||
|
| 2.5 | 2026-03-15 | Добавлено общее необязательное поле `manufactured_year_week` для компонентных секций (`YYYY-Www`) |
|
||||||
| 2.4 | 2026-03-15 | Добавлена первая волна component telemetry: health/life поля для `cpus`, `memory`, `storage`, `pcie_devices`, `power_supplies` |
|
| 2.4 | 2026-03-15 | Добавлена первая волна component telemetry: health/life поля для `cpus`, `memory`, `storage`, `pcie_devices`, `power_supplies` |
|
||||||
| 2.3 | 2026-03-15 | Добавлены component telemetry поля: `pcie_devices.temperature_c`, `pcie_devices.power_w`, `power_supplies.temperature_c` |
|
| 2.3 | 2026-03-15 | Добавлены component telemetry поля: `pcie_devices.temperature_c`, `pcie_devices.power_w`, `power_supplies.temperature_c` |
|
||||||
| 2.2 | 2026-03-15 | Добавлено поле `numa_node` у `pcie_devices` для topology/affinity |
|
| 2.2 | 2026-03-15 | Добавлено поле `numa_node` у `pcie_devices` для topology/affinity |
|
||||||
@@ -38,6 +41,7 @@ language: ru
|
|||||||
3. **Частичность** — можно передавать только те секции, данные по которым доступны. Пустой массив и отсутствие секции эквивалентны.
|
3. **Частичность** — можно передавать только те секции, данные по которым доступны. Пустой массив и отсутствие секции эквивалентны.
|
||||||
4. **Строгая схема** — endpoint использует строгий JSON-декодер; неизвестные поля приводят к `400 Bad Request`.
|
4. **Строгая схема** — endpoint использует строгий JSON-декодер; неизвестные поля приводят к `400 Bad Request`.
|
||||||
5. **Event-driven** — импорт создаёт события в timeline (LOG_COLLECTED, INSTALLED, REMOVED, FIRMWARE_CHANGED и др.).
|
5. **Event-driven** — импорт создаёт события в timeline (LOG_COLLECTED, INSTALLED, REMOVED, FIRMWARE_CHANGED и др.).
|
||||||
|
6. **Без синтеза со стороны интегратора** — сборщик передаёт только фактически собранные значения. Нельзя придумывать `serial_number`, `component_ref`, `message`, `message_id` или другие идентификаторы/атрибуты, если источник их не предоставил или парсер не смог их надёжно извлечь.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -127,7 +131,8 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
"storage": [ ... ],
|
"storage": [ ... ],
|
||||||
"pcie_devices": [ ... ],
|
"pcie_devices": [ ... ],
|
||||||
"power_supplies": [ ... ],
|
"power_supplies": [ ... ],
|
||||||
"sensors": { ... }
|
"sensors": { ... },
|
||||||
|
"event_logs": [ ... ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -157,6 +162,7 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
| `status_changed_at` | string RFC3339 | Время последнего изменения статуса |
|
| `status_changed_at` | string RFC3339 | Время последнего изменения статуса |
|
||||||
| `status_history` | array | История переходов статусов (см. ниже) |
|
| `status_history` | array | История переходов статусов (см. ниже) |
|
||||||
| `error_description` | string | Текст ошибки/диагностики |
|
| `error_description` | string | Текст ошибки/диагностики |
|
||||||
|
| `manufactured_year_week` | string | Дата производства в формате `YYYY-Www`, например `2024-W07` |
|
||||||
|
|
||||||
**Объект `status_history[]`:**
|
**Объект `status_history[]`:**
|
||||||
|
|
||||||
@@ -178,6 +184,7 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
- Если источник хранит историю — передавайте `status_history` отсортированным по `changed_at` по возрастанию.
|
- Если источник хранит историю — передавайте `status_history` отсортированным по `changed_at` по возрастанию.
|
||||||
- Не включайте записи `status_history` без `changed_at`.
|
- Не включайте записи `status_history` без `changed_at`.
|
||||||
- Все даты — RFC3339, рекомендуется UTC (`Z`).
|
- Все даты — RFC3339, рекомендуется UTC (`Z`).
|
||||||
|
- `manufactured_year_week` используйте, когда источник знает только год и неделю производства, без точной календарной даты.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -250,12 +257,14 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
| `life_remaining_pct` | float | нет | Остаточный ресурс / health, % |
|
| `life_remaining_pct` | float | нет | Остаточный ресурс / health, % |
|
||||||
| `life_used_pct` | float | нет | Использованный ресурс / wear, % |
|
| `life_used_pct` | float | нет | Использованный ресурс / wear, % |
|
||||||
| `serial_number` | string | нет | Серийный номер (если доступен) |
|
| `serial_number` | string | нет | Серийный номер (если доступен) |
|
||||||
| `firmware` | string | нет | Версия микрокода |
|
| `firmware` | string | нет | Версия микрокода; если логгер отдает `Microcode level`, передавайте его сюда как есть |
|
||||||
| `present` | bool | нет | Наличие (по умолчанию `true`) |
|
| `present` | bool | нет | Наличие (по умолчанию `true`) |
|
||||||
| + общие поля статуса | | | см. раздел выше |
|
| + общие поля статуса | | | см. раздел выше |
|
||||||
|
|
||||||
**Генерация serial_number при отсутствии:** `{board_serial}-CPU-{socket}`
|
**Генерация serial_number при отсутствии:** `{board_serial}-CPU-{socket}`
|
||||||
|
|
||||||
|
Если источник использует поле/лейбл `Microcode level`, его значение передавайте в `cpus[].firmware` без дополнительного преобразования.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"cpus": [
|
"cpus": [
|
||||||
{
|
{
|
||||||
@@ -282,7 +291,6 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
| Поле | Тип | Обязательно | Описание |
|
| Поле | Тип | Обязательно | Описание |
|
||||||
|------|-----|-------------|----------|
|
|------|-----|-------------|----------|
|
||||||
| `slot` | string | нет | Идентификатор слота |
|
| `slot` | string | нет | Идентификатор слота |
|
||||||
| `location` | string | нет | Физическое расположение |
|
|
||||||
| `present` | bool | нет | Наличие модуля (по умолчанию `true`) |
|
| `present` | bool | нет | Наличие модуля (по умолчанию `true`) |
|
||||||
| `serial_number` | string | нет | Серийный номер |
|
| `serial_number` | string | нет | Серийный номер |
|
||||||
| `part_number` | string | нет | Партномер (используется как модель) |
|
| `part_number` | string | нет | Партномер (используется как модель) |
|
||||||
@@ -328,7 +336,7 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
|
|
||||||
| Поле | Тип | Обязательно | Описание |
|
| Поле | Тип | Обязательно | Описание |
|
||||||
|------|-----|-------------|----------|
|
|------|-----|-------------|----------|
|
||||||
| `slot` | string | нет | Идентификатор слота |
|
| `slot` | string | нет | Канонический адрес установки PCIe-устройства; передавайте BDF (`0000:18:00.0`) |
|
||||||
| `serial_number` | string | нет | Серийный номер |
|
| `serial_number` | string | нет | Серийный номер |
|
||||||
| `model` | string | нет | Модель |
|
| `model` | string | нет | Модель |
|
||||||
| `manufacturer` | string | нет | Производитель |
|
| `manufacturer` | string | нет | Производитель |
|
||||||
@@ -404,7 +412,7 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
| `sfp_rx_power_dbm` | float | нет | RX optical power, dBm |
|
| `sfp_rx_power_dbm` | float | нет | RX optical power, dBm |
|
||||||
| `sfp_voltage_v` | float | нет | Напряжение SFP, В |
|
| `sfp_voltage_v` | float | нет | Напряжение SFP, В |
|
||||||
| `sfp_bias_ma` | float | нет | Bias current SFP, мА |
|
| `sfp_bias_ma` | float | нет | Bias current SFP, мА |
|
||||||
| `bdf` | string | нет | Bus:Device.Function, например `0000:18:00.0` |
|
| `bdf` | string | нет | Deprecated alias для `slot`; при наличии ingest нормализует его в `slot` |
|
||||||
| `device_class` | string | нет | Класс устройства (см. список ниже) |
|
| `device_class` | string | нет | Класс устройства (см. список ниже) |
|
||||||
| `manufacturer` | string | нет | Производитель |
|
| `manufacturer` | string | нет | Производитель |
|
||||||
| `model` | string | нет | Модель |
|
| `model` | string | нет | Модель |
|
||||||
@@ -421,7 +429,9 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
`numa_node` передавайте для NIC / InfiniBand / RAID / GPU, когда источник знает CPU/NUMA affinity. Поле сохраняется в snapshot-атрибутах PCIe-компонента и дублируется в telemetry для topology use cases.
|
`numa_node` передавайте для NIC / InfiniBand / RAID / GPU, когда источник знает CPU/NUMA affinity. Поле сохраняется в snapshot-атрибутах PCIe-компонента и дублируется в telemetry для topology use cases.
|
||||||
Поля `temperature_c` и `power_w` используйте для device-level telemetry GPU / accelerator / smart PCIe devices. Они не влияют на идентификацию компонента.
|
Поля `temperature_c` и `power_w` используйте для device-level telemetry GPU / accelerator / smart PCIe devices. Они не влияют на идентификацию компонента.
|
||||||
|
|
||||||
**Генерация serial_number при отсутствии или `"N/A"`:** `{board_serial}-PCIE-{slot}`
|
**Генерация serial_number при отсутствии или `"N/A"`:** `{board_serial}-PCIE-{slot}`, где `slot` для PCIe равен BDF.
|
||||||
|
|
||||||
|
`slot` — единственный канонический адрес компонента. Для PCIe в `slot` передавайте BDF. Поле `bdf` сохраняется только как переходный alias на входе и не должно использоваться как отдельная координата рядом со `slot`.
|
||||||
|
|
||||||
**Значения `device_class`:**
|
**Значения `device_class`:**
|
||||||
|
|
||||||
@@ -441,7 +451,7 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
```json
|
```json
|
||||||
"pcie_devices": [
|
"pcie_devices": [
|
||||||
{
|
{
|
||||||
"slot": "PCIeCard2",
|
"slot": "0000:3b:00.0",
|
||||||
"vendor_id": 5555,
|
"vendor_id": 5555,
|
||||||
"device_id": 4401,
|
"device_id": 4401,
|
||||||
"numa_node": 0,
|
"numa_node": 0,
|
||||||
@@ -450,7 +460,6 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
"sfp_temperature_c": 36.2,
|
"sfp_temperature_c": 36.2,
|
||||||
"sfp_tx_power_dbm": -1.8,
|
"sfp_tx_power_dbm": -1.8,
|
||||||
"sfp_rx_power_dbm": -2.1,
|
"sfp_rx_power_dbm": -2.1,
|
||||||
"bdf": "0000:3b:00.0",
|
|
||||||
"device_class": "EthernetController",
|
"device_class": "EthernetController",
|
||||||
"manufacturer": "Intel",
|
"manufacturer": "Intel",
|
||||||
"model": "X710 10GbE",
|
"model": "X710 10GbE",
|
||||||
@@ -526,6 +535,58 @@ PSU без `serial_number` игнорируется.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### event_logs
|
||||||
|
|
||||||
|
Нормализованные операционные логи сервера из `host`, `bmc` или `redfish`.
|
||||||
|
|
||||||
|
Эти записи не попадают в history timeline и не создают history events. Они сохраняются в отдельной deduplicated log store и отображаются в отдельном UI-блоке asset logs / host logs.
|
||||||
|
|
||||||
|
| Поле | Тип | Обязательно | Описание |
|
||||||
|
|------|-----|-------------|----------|
|
||||||
|
| `source` | string | **да** | Источник лога: `host`, `bmc`, `redfish` |
|
||||||
|
| `event_time` | string RFC3339 | нет | Время события из источника; если отсутствует, используется время ingest/collection |
|
||||||
|
| `severity` | string | нет | Уровень: `OK`, `Info`, `Warning`, `Critical`, `Unknown` |
|
||||||
|
| `message_id` | string | нет | Идентификатор/код события источника |
|
||||||
|
| `message` | string | **да** | Нормализованный текст события |
|
||||||
|
| `component_ref` | string | нет | Ссылка на компонент/устройство/слот, если извлекается |
|
||||||
|
| `fingerprint` | string | нет | Внешний готовый dedup-key; если не передан, система вычисляет свой |
|
||||||
|
| `is_active` | bool | нет | Признак, что событие всё ещё активно/не погашено, если источник умеет lifecycle |
|
||||||
|
| `raw_payload` | object | нет | Сырой vendor-specific payload для диагностики |
|
||||||
|
|
||||||
|
**Правила event_logs:**
|
||||||
|
- Логи дедуплицируются в рамках asset + source + fingerprint.
|
||||||
|
- Если `fingerprint` не передан, система строит его из нормализованных полей (`source`, `message_id`, `message`, `component_ref`, временная нормализация).
|
||||||
|
- Интегратор/сборщик логов не должен синтезировать содержимое событий: не придумывайте `message`, `message_id`, `component_ref`, serial/device identifiers или иные поля, если они отсутствуют в исходном логе или не были надёжно извлечены.
|
||||||
|
- Повторное получение того же события обновляет `last_seen_at`/счётчик повторов и не должно создавать новый timeline/history event.
|
||||||
|
- `event_logs` используются для отдельного UI-представления логов и не изменяют canonical state компонентов/asset по умолчанию.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"event_logs": [
|
||||||
|
{
|
||||||
|
"source": "bmc",
|
||||||
|
"event_time": "2026-03-15T14:03:11Z",
|
||||||
|
"severity": "Warning",
|
||||||
|
"message_id": "0x000F",
|
||||||
|
"message": "Correctable ECC error threshold exceeded",
|
||||||
|
"component_ref": "CPU0_C0D0",
|
||||||
|
"raw_payload": {
|
||||||
|
"sensor": "DIMM_A1",
|
||||||
|
"sel_record_id": "0042"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "redfish",
|
||||||
|
"event_time": "2026-03-15T14:03:20Z",
|
||||||
|
"severity": "Info",
|
||||||
|
"message_id": "OpenBMC.0.1.SystemReboot",
|
||||||
|
"message": "System reboot requested by administrator",
|
||||||
|
"component_ref": "Mainboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
#### sensors.fans
|
#### sensors.fans
|
||||||
|
|
||||||
| Поле | Тип | Обязательно | Описание |
|
| Поле | Тип | Обязательно | Описание |
|
||||||
@@ -608,10 +669,12 @@ PSU без `serial_number` игнорируется.
|
|||||||
|
|
||||||
## Обработка отсутствующих serial_number
|
## Обработка отсутствующих serial_number
|
||||||
|
|
||||||
|
Общее правило для всех секций: если источник не вернул серийный номер и сборщик не смог его надёжно извлечь, интегратор не должен подставлять вымышленные значения, хеши, локальные placeholder-идентификаторы или серийные номера "по догадке". Разрешены только явно оговорённые ниже server-side fallback-правила ingest.
|
||||||
|
|
||||||
| Тип | Поведение |
|
| Тип | Поведение |
|
||||||
|-----|-----------|
|
|-----|-----------|
|
||||||
| CPU | Генерируется: `{board_serial}-CPU-{socket}` |
|
| CPU | Генерируется: `{board_serial}-CPU-{socket}` |
|
||||||
| PCIe | Генерируется: `{board_serial}-PCIE-{slot}` (если serial = `"N/A"` или пустой) |
|
| PCIe | Генерируется: `{board_serial}-PCIE-{slot}` (если serial = `"N/A"` или пустой; `slot` для PCIe = BDF) |
|
||||||
| Memory | Компонент игнорируется |
|
| Memory | Компонент игнорируется |
|
||||||
| Storage | Компонент игнорируется |
|
| Storage | Компонент игнорируется |
|
||||||
| PSU | Компонент игнорируется |
|
| PSU | Компонент игнорируется |
|
||||||
@@ -687,7 +750,7 @@ PSU без `serial_number` игнорируется.
|
|||||||
],
|
],
|
||||||
"pcie_devices": [
|
"pcie_devices": [
|
||||||
{
|
{
|
||||||
"slot": "PCIeCard1",
|
"slot": "0000:18:00.0",
|
||||||
"device_class": "EthernetController",
|
"device_class": "EthernetController",
|
||||||
"manufacturer": "Intel",
|
"manufacturer": "Intel",
|
||||||
"model": "X710 10GbE",
|
"model": "X710 10GbE",
|
||||||
|
|||||||
Reference in New Issue
Block a user