diff --git a/agent/probes/inventory.go b/agent/probes/inventory.go index 7832c90..6baaf5a 100644 --- a/agent/probes/inventory.go +++ b/agent/probes/inventory.go @@ -522,38 +522,7 @@ func GPUs(log Logger) []spec.GPUSpec { } var gpus []spec.GPUSpec for _, line := range strings.Split(string(out), "\n") { - low := strings.ToLower(line) - if !strings.Contains(low, "vga compatible controller") && - !strings.Contains(low, "3d controller") { - continue - } - // With -D the first quoted field is the PCI address. - fields := splitQuoted(line) - if len(fields) < 3 { - continue - } - // -D -mm emits the address as the first non-quoted token. - // splitQuoted strips unquoted whitespace, so the address is - // indexed at 0 and the quoted class/vendor/device follow. - // On older lspci the first field is the slot in quotes instead; - // cover both: if fields[0] doesn't look like an address, strip - // it from the space-separated prefix. - addr := "" - if pciAddrRE.MatchString(fields[0]) { - addr = fields[0] - } else if i := strings.IndexByte(line, '"'); i > 0 { - prefix := strings.TrimSpace(line[:i]) - if pciAddrRE.MatchString(prefix) { - addr = prefix - } - } - model := stripPCIID(fields[2]) - if !pciAddrRE.MatchString(fields[0]) { - // With no -D support the device is at [1], model at [2]: same - // as old code path. Keep existing behavior. - model = stripPCIID(fields[2]) - } - model = sanitizeASCII(model) + addr, model := parseLspciMMLine(line) if model == "" { continue } @@ -572,6 +541,46 @@ func GPUs(log Logger) []spec.GPUSpec { var pciAddrRE = regexp.MustCompile(`^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$`) +// parseLspciMMLine extracts the PCI address and sanitized device-model +// string from one line of `lspci -D -mm -nn` output. +// +// With -D the line begins with a bare PCI address token followed by +// three quoted fields in the order class / vendor / device, so after +// splitQuoted the address is at fields[0] and the device name lives at +// fields[3]. Older lspci without -D emits only the three quoted +// fields: class / vendor / device, putting the device at fields[2]. +// Both layouts are accepted; VGA and 3D-controller classes are the +// only ones returned (keeps non-GPU PCI entries from slipping in). +// +// Returns ("", "") for lines that don't match a GPU class or whose +// model sanitizes to empty. +func parseLspciMMLine(line string) (addr, model string) { + low := strings.ToLower(line) + if !strings.Contains(low, "vga compatible controller") && + !strings.Contains(low, "3d controller") { + return "", "" + } + fields := splitQuoted(line) + if len(fields) < 3 { + return "", "" + } + modelIdx := 2 + if pciAddrRE.MatchString(fields[0]) { + addr = fields[0] + modelIdx = 3 + } else if i := strings.IndexByte(line, '"'); i > 0 { + prefix := strings.TrimSpace(line[:i]) + if pciAddrRE.MatchString(prefix) { + addr = prefix + } + } + if modelIdx >= len(fields) { + return "", "" + } + model = sanitizeASCII(stripPCIID(fields[modelIdx])) + return addr, model +} + // parseLspciVerbose pulls the largest prefetchable memory region (VRAM) // in GiB and the "Kernel driver in use" name from an lspci -vv block. // The memory line looks like: "Memory at ... (64-bit, prefetchable) [size=8G]" diff --git a/agent/probes/inventory_test.go b/agent/probes/inventory_test.go index 95d15a5..997f51e 100644 --- a/agent/probes/inventory_test.go +++ b/agent/probes/inventory_test.go @@ -153,6 +153,57 @@ Memory Device } } +func TestParseLspciMMLine(t *testing.T) { + cases := []struct { + name string + line string + wantAddr string + wantModel string + wantSkipped bool + }{ + { + name: "with -D, N95 integrated GPU", + line: `0000:00:02.0 "VGA compatible controller [0300]" "Intel Corporation [8086]" "Alder Lake-N [UHD Graphics] [46d0]" -r1c "Intel Corporation [8086]" "Device [7270]"`, + wantAddr: "0000:00:02.0", + wantModel: "Alder Lake-N [UHD Graphics]", + }, + { + name: "without -D, legacy three-quoted layout", + line: `"VGA compatible controller" "NVIDIA Corporation" "GP104 [GeForce GTX 1080]"`, + wantAddr: "", + wantModel: "GP104 [GeForce GTX 1080]", + }, + { + name: "3D controller class also picked up", + line: `0000:01:00.0 "3D controller [0302]" "NVIDIA Corporation [10de]" "TU104GL [Tesla T4] [1eb8]"`, + wantAddr: "0000:01:00.0", + wantModel: "TU104GL [Tesla T4]", + }, + { + name: "non-GPU class is skipped", + line: `0000:01:00.0 "Ethernet controller [0200]" "Intel Corporation [8086]" "I350 [1521]"`, + wantSkipped: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + addr, model := parseLspciMMLine(tc.line) + if tc.wantSkipped { + if model != "" { + t.Fatalf("expected skip, got addr=%q model=%q", addr, model) + } + return + } + if addr != tc.wantAddr { + t.Errorf("addr: got %q want %q", addr, tc.wantAddr) + } + if model != tc.wantModel { + t.Errorf("model: got %q want %q", model, tc.wantModel) + } + }) + } +} + func TestParseLspciVerbose(t *testing.T) { sample := `01:00.0 VGA compatible controller: NVIDIA Corporation Subsystem: ASUSTeK