From 05c1fde233948c7104c83b30f0938aa65b3aa197 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Sun, 12 Apr 2026 12:42:17 +0300 Subject: [PATCH] Warn on PCIe link speed degradation and collect lspci -vvv in techdump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - collector/pcie: add applyPCIeLinkSpeedWarning that sets status=Warning and ErrorDescription when current link speed is below maximum negotiated speed (e.g. Gen1 running on a Gen5 slot) - collector/pcie: add pcieLinkSpeedRank helper for Gen string comparison - collector/pcie_filter_test: cover degraded and healthy link speed cases - platform/techdump: collect lspci -vvv → lspci-vvv.txt for LnkCap/LnkSta Co-Authored-By: Claude Sonnet 4.6 --- audit/internal/collector/pcie.go | 39 ++++++++++ audit/internal/collector/pcie_filter_test.go | 75 ++++++++++++++++++++ audit/internal/platform/techdump.go | 1 + 3 files changed, 115 insertions(+) diff --git a/audit/internal/collector/pcie.go b/audit/internal/collector/pcie.go index 2db1510..c1473c8 100644 --- a/audit/internal/collector/pcie.go +++ b/audit/internal/collector/pcie.go @@ -2,6 +2,7 @@ package collector import ( "bee/audit/internal/schema" + "fmt" "log/slog" "os/exec" "strconv" @@ -172,6 +173,9 @@ func parseLspciDevice(fields map[string]string) schema.HardwarePCIeDevice { // SVendor/SDevice available but not in schema — skip + // Warn if PCIe link is running below its maximum negotiated speed. + applyPCIeLinkSpeedWarning(&dev) + return dev } @@ -241,6 +245,41 @@ func readPCIStringAttribute(bdf, attribute string) (string, bool) { return value, true } +// applyPCIeLinkSpeedWarning sets the device status to Warning if the current PCIe link +// speed is below the maximum negotiated speed supported by both ends. +func applyPCIeLinkSpeedWarning(dev *schema.HardwarePCIeDevice) { + if dev.LinkSpeed == nil || dev.MaxLinkSpeed == nil { + return + } + if pcieLinkSpeedRank(*dev.LinkSpeed) < pcieLinkSpeedRank(*dev.MaxLinkSpeed) { + warn := statusWarning + dev.Status = &warn + desc := fmt.Sprintf("PCIe link speed degraded: running at %s, capable of %s", *dev.LinkSpeed, *dev.MaxLinkSpeed) + dev.ErrorDescription = &desc + } +} + +// pcieLinkSpeedRank returns a numeric rank for a normalized Gen string (e.g. "Gen4" → 4). +// Returns 0 for unrecognised values so comparisons fail safe. +func pcieLinkSpeedRank(gen string) int { + switch gen { + case "Gen1": + return 1 + case "Gen2": + return 2 + case "Gen3": + return 3 + case "Gen4": + return 4 + case "Gen5": + return 5 + case "Gen6": + return 6 + default: + return 0 + } +} + func normalizePCILinkSpeed(raw string) string { raw = strings.TrimSpace(strings.ToLower(raw)) switch { diff --git a/audit/internal/collector/pcie_filter_test.go b/audit/internal/collector/pcie_filter_test.go index 8d2d898..184d59f 100644 --- a/audit/internal/collector/pcie_filter_test.go +++ b/audit/internal/collector/pcie_filter_test.go @@ -1,6 +1,7 @@ package collector import ( + "bee/audit/internal/schema" "encoding/json" "strings" "testing" @@ -141,3 +142,77 @@ func TestNormalizePCILinkSpeed(t *testing.T) { } } } + +func TestApplyPCIeLinkSpeedWarning(t *testing.T) { + ptr := func(s string) *string { return &s } + + tests := []struct { + name string + linkSpeed *string + maxSpeed *string + wantWarning bool + wantGenIn string // substring expected in ErrorDescription when warning + }{ + { + name: "degraded Gen1 vs Gen5", + linkSpeed: ptr("Gen1"), + maxSpeed: ptr("Gen5"), + wantWarning: true, + wantGenIn: "Gen1", + }, + { + name: "at max Gen5", + linkSpeed: ptr("Gen5"), + maxSpeed: ptr("Gen5"), + wantWarning: false, + }, + { + name: "degraded Gen4 vs Gen5", + linkSpeed: ptr("Gen4"), + maxSpeed: ptr("Gen5"), + wantWarning: true, + wantGenIn: "Gen4", + }, + { + name: "missing current speed — no warning", + linkSpeed: nil, + maxSpeed: ptr("Gen5"), + wantWarning: false, + }, + { + name: "missing max speed — no warning", + linkSpeed: ptr("Gen1"), + maxSpeed: nil, + wantWarning: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dev := schema.HardwarePCIeDevice{} + ok := statusOK + dev.Status = &ok + dev.LinkSpeed = tt.linkSpeed + dev.MaxLinkSpeed = tt.maxSpeed + + applyPCIeLinkSpeedWarning(&dev) + + gotWarn := dev.Status != nil && *dev.Status == statusWarning + if gotWarn != tt.wantWarning { + t.Fatalf("wantWarning=%v gotWarning=%v (status=%v)", tt.wantWarning, gotWarn, dev.Status) + } + if tt.wantWarning { + if dev.ErrorDescription == nil { + t.Fatal("expected ErrorDescription to be set") + } + if !strings.Contains(*dev.ErrorDescription, tt.wantGenIn) { + t.Fatalf("ErrorDescription %q does not contain %q", *dev.ErrorDescription, tt.wantGenIn) + } + } else { + if dev.ErrorDescription != nil { + t.Fatalf("unexpected ErrorDescription: %s", *dev.ErrorDescription) + } + } + }) + } +} diff --git a/audit/internal/platform/techdump.go b/audit/internal/platform/techdump.go index be18f58..f79f928 100644 --- a/audit/internal/platform/techdump.go +++ b/audit/internal/platform/techdump.go @@ -20,6 +20,7 @@ var techDumpFixedCommands = []struct { {Name: "dmidecode", Args: []string{"-t", "4"}, File: "dmidecode-type4.txt"}, {Name: "dmidecode", Args: []string{"-t", "17"}, File: "dmidecode-type17.txt"}, {Name: "lspci", Args: []string{"-vmm", "-D"}, File: "lspci-vmm.txt"}, + {Name: "lspci", Args: []string{"-vvv"}, File: "lspci-vvv.txt"}, {Name: "lsblk", Args: []string{"-J", "-d", "-o", "NAME,TYPE,SIZE,SERIAL,MODEL,TRAN,HCTL"}, File: "lsblk.json"}, {Name: "sensors", Args: []string{"-j"}, File: "sensors.json"}, {Name: "ipmitool", Args: []string{"fru", "print"}, File: "ipmitool-fru.txt"},