fix(inventory): read GPU model from device field, not vendor field
`lspci -D -mm -nn` prefixes every line with the PCI address as a bare
token before the three quoted class/vendor/device fields, so the
device name sits at fields[3] — not fields[2], which is the vendor.
The probe was indexing [2] and recording every GPU's model as its
vendor string ("Intel Corporation" instead of "Alder Lake-N [UHD
Graphics]"), which made every SpecValidate mismatch on real hosts
once the expected spec named the device.
Extract the per-line parse into parseLspciMMLine, handle both the
modern -D layout (addr + class/vendor/device) and the legacy
layout without an address prefix (class/vendor/device), and cover
both paths plus the non-GPU-class skip in inventory_test.go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+41
-32
@@ -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]"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user