Hardware: a Motherboard card
The board, the chassis and the firmware on it, which the System card was reducing to two lines. Maker, model, version, serial and asset tag; chassis and where the board sits in it; the firmware's vendor, version, revision, date, ROM size and whether the machine boots UEFI — that last one is a line in SMBIOS's characteristics list rather than a field, so the parser now keeps those lists too. Expansion slots come with it, with the device found at each occupied slot's PCI address rather than the address alone. They are only shown when the firmware tracks occupancy at all: core's NucBox lists five PCIe x1 slots it does not physically have, all "Available", copied out of a reference design. grid's board reports real addresses and real occupancy, which is the case worth printing. A machine with no SMBIOS gets a card that says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
38039d1adc
commit
03e0761f3e
+124
-23
@@ -26,15 +26,44 @@ import (
|
||||
)
|
||||
|
||||
type System struct {
|
||||
Model string `json:"model,omitempty"` // "GMKtec NucBox G3"
|
||||
Serial string `json:"serial,omitempty"`
|
||||
Board string `json:"board,omitempty"` // motherboard, when it differs from the model
|
||||
Chassis string `json:"chassis,omitempty"`
|
||||
BIOS string `json:"bios,omitempty"`
|
||||
BIOSDate string `json:"bios_date,omitempty"`
|
||||
Kernel string `json:"kernel,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
Virt string `json:"virt,omitempty"` // "kvm", "lxc", … when this is not real hardware
|
||||
Model string `json:"model,omitempty"` // "GMKtec NucBox G3"
|
||||
Serial string `json:"serial,omitempty"`
|
||||
Family string `json:"family,omitempty"`
|
||||
Kernel string `json:"kernel,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
Virt string `json:"virt,omitempty"` // "kvm", "lxc", … when this is not real hardware
|
||||
}
|
||||
|
||||
// Slot is an expansion slot as the firmware describes it. Worth treating with suspicion on
|
||||
// a mini PC, where the table is often copied from a reference design and lists slots the
|
||||
// case has no room for.
|
||||
type Slot struct {
|
||||
Designation string `json:"designation"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Usage string `json:"usage,omitempty"` // Available | In Use | Unknown
|
||||
Length string `json:"length,omitempty"`
|
||||
Width string `json:"width,omitempty"`
|
||||
Bus string `json:"bus,omitempty"` // PCI address of what is in it
|
||||
Device string `json:"device,omitempty"` // and what that turns out to be
|
||||
}
|
||||
|
||||
// Board is the motherboard and the firmware on it.
|
||||
type Board struct {
|
||||
Maker string `json:"maker,omitempty"`
|
||||
Product string `json:"product,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Serial string `json:"serial,omitempty"`
|
||||
AssetTag string `json:"asset_tag,omitempty"`
|
||||
Type string `json:"type,omitempty"` // "Motherboard", "Processor Module", …
|
||||
Location string `json:"location,omitempty"` // where it sits in the chassis
|
||||
Chassis string `json:"chassis,omitempty"`
|
||||
ChassisMaker string `json:"chassis_maker,omitempty"`
|
||||
BIOS string `json:"bios,omitempty"` // vendor and version
|
||||
BIOSDate string `json:"bios_date,omitempty"`
|
||||
BIOSRevision string `json:"bios_revision,omitempty"`
|
||||
ROMSize string `json:"rom_size,omitempty"`
|
||||
UEFI bool `json:"uefi,omitempty"`
|
||||
Slots []Slot `json:"slots,omitempty"`
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
@@ -114,6 +143,7 @@ type GPU struct {
|
||||
type Report struct {
|
||||
At time.Time `json:"at"`
|
||||
System System `json:"system"`
|
||||
Board Board `json:"board"`
|
||||
CPU CPU `json:"cpu"`
|
||||
Memory Memory `json:"memory"`
|
||||
Drives []Drive `json:"drives"`
|
||||
@@ -159,6 +189,7 @@ func collect(ctx context.Context) *Report {
|
||||
r.Notes = append(r.Notes, note)
|
||||
}
|
||||
r.System = system(ctx, dmi)
|
||||
r.Board = board(dmi, pciNames(ctx))
|
||||
r.CPU = cpu(ctx, dmi)
|
||||
r.Memory = memory(dmi)
|
||||
if len(r.Memory.Modules) == 0 && note == "" {
|
||||
@@ -180,6 +211,10 @@ type record struct {
|
||||
V map[string]string
|
||||
}
|
||||
|
||||
// listKey holds every doubly-indented line of a record — dmidecode's lists, which have no
|
||||
// key of their own: BIOS characteristics, board features, slot characteristics.
|
||||
const listKey = "\x00list"
|
||||
|
||||
var handleRe = regexp.MustCompile(`^Handle 0x[0-9A-Fa-f]+, DMI type (\d+)`)
|
||||
|
||||
// readDMI runs dmidecode once for every type the page wants and parses the lot. It needs
|
||||
@@ -193,7 +228,7 @@ func readDMI(ctx context.Context) ([]record, string) {
|
||||
}
|
||||
tool = "/usr/sbin/dmidecode"
|
||||
}
|
||||
args := []string{tool, "-t", "bios", "-t", "system", "-t", "baseboard", "-t", "chassis", "-t", "processor", "-t", "memory"}
|
||||
args := []string{tool, "-t", "bios", "-t", "system", "-t", "baseboard", "-t", "chassis", "-t", "slot", "-t", "processor", "-t", "memory"}
|
||||
if os.Geteuid() != 0 {
|
||||
if !priv.Available() {
|
||||
return nil, "the board and the memory modules come from the firmware's table, which only root may read"
|
||||
@@ -223,12 +258,16 @@ func parse(text string) []record {
|
||||
cur = &recs[len(recs)-1]
|
||||
continue
|
||||
}
|
||||
if cur == nil || !strings.HasPrefix(line, "\t") || strings.HasPrefix(line, "\t\t") {
|
||||
if cur == nil || !strings.HasPrefix(line, "\t") {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
cur = nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "\t\t") { // a list under the field above it, e.g. BIOS characteristics
|
||||
cur.V[listKey] += strings.TrimSpace(line) + "\n"
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(strings.TrimSpace(line), ":")
|
||||
if !ok {
|
||||
continue
|
||||
@@ -280,19 +319,12 @@ func all(recs []record, t int) []record {
|
||||
// ---- system ----
|
||||
|
||||
func system(ctx context.Context, dmi []record) System {
|
||||
sys, board, bios, chassis := first(dmi, 1), first(dmi, 2), first(dmi, 0), first(dmi, 3)
|
||||
sys := first(dmi, 1)
|
||||
s := System{
|
||||
Model: join(sys.get("Manufacturer"), sys.get("Product Name")),
|
||||
Serial: sys.get("Serial Number"),
|
||||
Board: join(board.get("Manufacturer"), board.get("Product Name")),
|
||||
Chassis: chassis.get("Type"),
|
||||
BIOS: join(bios.get("Vendor"), bios.get("Version")),
|
||||
BIOSDate: bios.get("Release Date"),
|
||||
Arch: runtimeArch(),
|
||||
}
|
||||
// a mini PC names its board after itself — "GMKtec" under "GMKtec NucBox G3" says nothing
|
||||
if s.Board != "" && (s.Board == s.Model || strings.Contains(strings.ToLower(s.Model), strings.ToLower(s.Board))) {
|
||||
s.Board = ""
|
||||
Model: join(sys.get("Manufacturer"), sys.get("Product Name")),
|
||||
Serial: sys.get("Serial Number"),
|
||||
Family: sys.get("Family"),
|
||||
Arch: runtimeArch(),
|
||||
}
|
||||
s.Kernel = readFile("/proc/sys/kernel/osrelease")
|
||||
if s.Model == "" { // no SMBIOS: a Pi, a Mac, anything with a device tree
|
||||
@@ -306,6 +338,53 @@ func system(ctx context.Context, dmi []record) System {
|
||||
return s
|
||||
}
|
||||
|
||||
// board reads the motherboard, the chassis it sits in, the firmware on it and the
|
||||
// expansion slots the firmware claims. All of it is SMBIOS, so a machine without one — a
|
||||
// Mac, a Pi — gets an empty board, and the page says so rather than inventing one.
|
||||
func board(dmi []record, pci map[string]string) Board {
|
||||
b, ch, bios := first(dmi, 2), first(dmi, 3), first(dmi, 0)
|
||||
out := Board{
|
||||
Maker: b.get("Manufacturer"),
|
||||
Product: b.get("Product Name"),
|
||||
Version: b.get("Version"),
|
||||
Serial: b.get("Serial Number"),
|
||||
AssetTag: b.get("Asset Tag"),
|
||||
Type: b.get("Type"),
|
||||
Location: b.get("Location In Chassis"),
|
||||
Chassis: ch.get("Type"),
|
||||
ChassisMaker: ch.get("Manufacturer"),
|
||||
BIOS: join(bios.get("Vendor"), bios.get("Version")),
|
||||
BIOSDate: bios.get("Release Date"),
|
||||
BIOSRevision: bios.get("BIOS Revision"),
|
||||
}
|
||||
// "ROM Size: 0 MB" is a board that never filled the field in, not a board with no ROM
|
||||
if rom := bios.get("ROM Size"); rom != "" && !strings.HasPrefix(rom, "0 ") {
|
||||
out.ROMSize = rom
|
||||
}
|
||||
if out.Product == out.Maker {
|
||||
out.Product = "" // a board named after its maker says nothing twice
|
||||
}
|
||||
out.UEFI = strings.Contains(bios.V[listKey], "UEFI") // a characteristics line, not a field
|
||||
for _, sl := range all(dmi, 9) {
|
||||
s := Slot{
|
||||
Designation: sl.get("Designation"),
|
||||
Type: sl.get("Type"),
|
||||
Usage: sl.get("Current Usage"),
|
||||
Length: sl.get("Length"),
|
||||
Width: sl.get("Data Bus Width"),
|
||||
}
|
||||
// every slot on some boards claims 0000:00:00.0, which is the host bridge and
|
||||
// certainly not what is plugged into that slot
|
||||
if bus := sl.get("Bus Address"); bus != "" && bus != "0000:00:00.0" {
|
||||
s.Bus, s.Device = bus, pci[bus]
|
||||
}
|
||||
if s.Designation != "" {
|
||||
out.Slots = append(out.Slots, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func join(parts ...string) string {
|
||||
var keep []string
|
||||
for _, p := range parts {
|
||||
@@ -667,6 +746,28 @@ func gpus(ctx context.Context) []GPU {
|
||||
return out
|
||||
}
|
||||
|
||||
// pciNames maps a PCI address to a readable name, so a slot the firmware says is in use
|
||||
// can say what is in it.
|
||||
func pciNames(ctx context.Context) map[string]string {
|
||||
m := map[string]string{}
|
||||
if _, err := exec.LookPath("lspci"); err != nil {
|
||||
return m
|
||||
}
|
||||
out, err := exec.CommandContext(ctx, "lspci", "-D", "-mm", "-nn").Output()
|
||||
if err != nil {
|
||||
return m
|
||||
}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
slot, _, _ := strings.Cut(line, " ")
|
||||
f := quoted.FindAllStringSubmatch(line, -1)
|
||||
if slot == "" || len(f) < 3 {
|
||||
continue
|
||||
}
|
||||
m[slot] = strings.TrimSpace(stripID(f[1][1]) + " " + stripID(f[2][1]))
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func displayClass(c string) string {
|
||||
switch {
|
||||
case strings.Contains(c, "[0300]"):
|
||||
|
||||
Reference in New Issue
Block a user