Skip to content

Commit ed1c31c

Browse files
authored
Merge pull request #471 from rpardini/devicetree-fallback
linux: fall back to DeviceTree when DMI is unavailable
2 parents 5c7a65f + 51adb07 commit ed1c31c

14 files changed

Lines changed: 770 additions & 0 deletions

pkg/baseboard/baseboard_linux.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@ import (
99
"context"
1010

1111
"github.com/jaypipes/ghw/pkg/linuxdmi"
12+
"github.com/jaypipes/ghw/pkg/linuxdt"
13+
"github.com/jaypipes/ghw/pkg/util"
1214
)
1315

1416
func (i *Info) load(ctx context.Context) error {
17+
if !linuxdmi.Available(ctx) && linuxdt.Available(ctx) {
18+
return i.loadDeviceTree(ctx)
19+
}
20+
1521
i.AssetTag = linuxdmi.Item(ctx, "board_asset_tag")
1622
i.SerialNumber = linuxdmi.Item(ctx, "board_serial")
1723
i.Vendor = linuxdmi.Item(ctx, "board_vendor")
@@ -20,3 +26,16 @@ func (i *Info) load(ctx context.Context) error {
2026

2127
return nil
2228
}
29+
30+
// loadDeviceTree populates baseboard information from the DeviceTree on systems
31+
// without DMI/SMBIOS. The DeviceTree carries the model, vendor and serial
32+
// number, so the asset tag and version stay unknown.
33+
func (i *Info) loadDeviceTree(ctx context.Context) error {
34+
i.AssetTag = util.UNKNOWN
35+
i.SerialNumber = linuxdt.SerialNumber(ctx)
36+
i.Vendor = linuxdt.Vendor(ctx)
37+
i.Version = util.UNKNOWN
38+
i.Product = linuxdt.Model(ctx)
39+
40+
return nil
41+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Use and distribution licensed under the Apache license version 2.
2+
//
3+
// See the COPYING file in the root project directory for full text.
4+
//
5+
6+
package baseboard_test
7+
8+
import (
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
13+
"github.com/jaypipes/ghw"
14+
"github.com/jaypipes/ghw/pkg/baseboard"
15+
"github.com/jaypipes/ghw/pkg/util"
16+
)
17+
18+
func deviceTreeChroot(t *testing.T, props map[string]string) string {
19+
t.Helper()
20+
root := t.TempDir()
21+
base := filepath.Join(root, "sys", "firmware", "devicetree", "base")
22+
for name, content := range props {
23+
path := filepath.Join(base, name)
24+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
25+
t.Fatal(err)
26+
}
27+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
28+
t.Fatal(err)
29+
}
30+
}
31+
return root
32+
}
33+
34+
func TestBaseboardDeviceTree(t *testing.T) {
35+
// Mixtile Blade 3: compatible prefix "rockchip" despite model "Mixtile Blade 3".
36+
root := deviceTreeChroot(t, map[string]string{
37+
"model": "Mixtile Blade 3 v1.0.1\x00",
38+
"compatible": "rockchip,rk3588-blade3-v101-linux\x00rockchip,rk3588\x00",
39+
"serial-number": "6bb18c7ac387ae18\x00",
40+
})
41+
42+
info, err := baseboard.New(ghw.WithChroot(root))
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
if got, want := info.Product, "Mixtile Blade 3 v1.0.1"; got != want {
47+
t.Errorf("product: got %q, want %q", got, want)
48+
}
49+
// Vendor is the compatible prefix, taken verbatim (a downstream-kernel quirk).
50+
if got, want := info.Vendor, "rockchip"; got != want {
51+
t.Errorf("vendor: got %q, want %q", got, want)
52+
}
53+
if got, want := info.SerialNumber, "6bb18c7ac387ae18"; got != want {
54+
t.Errorf("serial: got %q, want %q", got, want)
55+
}
56+
if got := info.AssetTag; got != util.UNKNOWN {
57+
t.Errorf("asset tag: got %q, want %q", got, util.UNKNOWN)
58+
}
59+
if got := info.Version; got != util.UNKNOWN {
60+
t.Errorf("version: got %q, want %q", got, util.UNKNOWN)
61+
}
62+
}

pkg/bios/bios_linux.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,34 @@ import (
99
"context"
1010

1111
"github.com/jaypipes/ghw/pkg/linuxdmi"
12+
"github.com/jaypipes/ghw/pkg/linuxdt"
13+
"github.com/jaypipes/ghw/pkg/util"
1214
)
1315

1416
func (i *Info) load(ctx context.Context) error {
17+
if !linuxdmi.Available(ctx) && linuxdt.Available(ctx) {
18+
return i.loadDeviceTree(ctx)
19+
}
20+
1521
i.Vendor = linuxdmi.Item(ctx, "bios_vendor")
1622
i.Version = linuxdmi.Item(ctx, "bios_version")
1723
i.Date = linuxdmi.Item(ctx, "bios_date")
1824

1925
return nil
2026
}
27+
28+
// loadDeviceTree populates BIOS/firmware information from the DeviceTree on
29+
// systems without DMI/SMBIOS. U-Boot exposes its version under the "chosen"
30+
// node; when present we report it as a "U-Boot" firmware. The DeviceTree has no
31+
// firmware date, so that stays unknown.
32+
func (i *Info) loadDeviceTree(ctx context.Context) error {
33+
i.Vendor = util.UNKNOWN
34+
i.Version = util.UNKNOWN
35+
i.Date = util.UNKNOWN
36+
if version := linuxdt.UBootVersion(ctx); version != util.UNKNOWN {
37+
i.Vendor = "U-Boot"
38+
i.Version = version
39+
}
40+
41+
return nil
42+
}

pkg/bios/bios_linux_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Use and distribution licensed under the Apache license version 2.
2+
//
3+
// See the COPYING file in the root project directory for full text.
4+
//
5+
6+
package bios_test
7+
8+
import (
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
13+
"github.com/jaypipes/ghw"
14+
"github.com/jaypipes/ghw/pkg/bios"
15+
"github.com/jaypipes/ghw/pkg/util"
16+
)
17+
18+
func deviceTreeChroot(t *testing.T, props map[string]string) string {
19+
t.Helper()
20+
root := t.TempDir()
21+
base := filepath.Join(root, "sys", "firmware", "devicetree", "base")
22+
for name, content := range props {
23+
path := filepath.Join(base, name)
24+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
25+
t.Fatal(err)
26+
}
27+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
28+
t.Fatal(err)
29+
}
30+
}
31+
return root
32+
}
33+
34+
func TestBIOSDeviceTreeUBoot(t *testing.T) {
35+
root := deviceTreeChroot(t, map[string]string{
36+
"compatible": "radxa,nio-12l\x00mediatek,mt8395\x00",
37+
"chosen/u-boot,version": "2022.10_armbian-2022.10-S40c5\x00",
38+
})
39+
40+
info, err := bios.New(ghw.WithChroot(root))
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
if got, want := info.Vendor, "U-Boot"; got != want {
45+
t.Errorf("vendor: got %q, want %q", got, want)
46+
}
47+
if got, want := info.Version, "2022.10_armbian-2022.10-S40c5"; got != want {
48+
t.Errorf("version: got %q, want %q", got, want)
49+
}
50+
if got := info.Date; got != util.UNKNOWN {
51+
t.Errorf("date: got %q, want %q", got, util.UNKNOWN)
52+
}
53+
}
54+
55+
func TestBIOSDeviceTreeNoUBoot(t *testing.T) {
56+
// Raspberry Pi: no chosen/u-boot,version (uses its own bootloader).
57+
root := deviceTreeChroot(t, map[string]string{
58+
"compatible": "raspberrypi,4-model-b\x00brcm,bcm2711\x00",
59+
})
60+
61+
info, err := bios.New(ghw.WithChroot(root))
62+
if err != nil {
63+
t.Fatal(err)
64+
}
65+
for name, got := range map[string]string{
66+
"vendor": info.Vendor, "version": info.Version, "date": info.Date,
67+
} {
68+
if got != util.UNKNOWN {
69+
t.Errorf("%s: got %q, want %q", name, got, util.UNKNOWN)
70+
}
71+
}
72+
}

pkg/chassis/chassis_linux.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@ import (
99
"context"
1010

1111
"github.com/jaypipes/ghw/pkg/linuxdmi"
12+
"github.com/jaypipes/ghw/pkg/linuxdt"
1213
"github.com/jaypipes/ghw/pkg/util"
1314
)
1415

1516
func (i *Info) load(ctx context.Context) error {
17+
if !linuxdmi.Available(ctx) && linuxdt.Available(ctx) {
18+
return i.loadDeviceTree(ctx)
19+
}
20+
1621
i.AssetTag = linuxdmi.Item(ctx, "chassis_asset_tag")
1722
i.SerialNumber = linuxdmi.Item(ctx, "chassis_serial")
1823
i.Type = linuxdmi.Item(ctx, "chassis_type")
@@ -26,3 +31,22 @@ func (i *Info) load(ctx context.Context) error {
2631

2732
return nil
2833
}
34+
35+
// loadDeviceTree populates chassis information from the DeviceTree on systems
36+
// without DMI/SMBIOS. The DeviceTree has no asset tag concept, and only some
37+
// boards expose a chassis-type, so several fields remain unknown.
38+
func (i *Info) loadDeviceTree(ctx context.Context) error {
39+
i.AssetTag = util.UNKNOWN
40+
i.SerialNumber = linuxdt.SerialNumber(ctx)
41+
i.Vendor = linuxdt.Vendor(ctx)
42+
i.Version = linuxdt.Model(ctx)
43+
44+
i.Type = linuxdt.ChassisType(ctx)
45+
typeDesc, found := chassisTypeDescriptions[i.Type]
46+
if !found {
47+
typeDesc = util.UNKNOWN
48+
}
49+
i.TypeDescription = typeDesc
50+
51+
return nil
52+
}

pkg/chassis/chassis_linux_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Use and distribution licensed under the Apache license version 2.
2+
//
3+
// See the COPYING file in the root project directory for full text.
4+
//
5+
6+
package chassis_test
7+
8+
import (
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
13+
"github.com/jaypipes/ghw"
14+
"github.com/jaypipes/ghw/pkg/chassis"
15+
"github.com/jaypipes/ghw/pkg/util"
16+
)
17+
18+
// writeFiles writes name->content files (names may be nested subpaths) rooted at
19+
// dir, creating parent directories as needed.
20+
func writeFiles(t *testing.T, dir string, files map[string]string) {
21+
t.Helper()
22+
for name, content := range files {
23+
path := filepath.Join(dir, name)
24+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
25+
t.Fatal(err)
26+
}
27+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
28+
t.Fatal(err)
29+
}
30+
}
31+
}
32+
33+
// deviceTreeChroot builds a chroot whose DeviceTree base holds the given
34+
// properties and which has no DMI, so the DeviceTree fallback is exercised.
35+
func deviceTreeChroot(t *testing.T, props map[string]string) string {
36+
t.Helper()
37+
root := t.TempDir()
38+
writeFiles(t, filepath.Join(root, "sys", "firmware", "devicetree", "base"), props)
39+
return root
40+
}
41+
42+
func TestChassisDeviceTreeRPi(t *testing.T) {
43+
root := deviceTreeChroot(t, map[string]string{
44+
"model": "Raspberry Pi 4 Model B Rev 1.4\x00",
45+
"compatible": "raspberrypi,4-model-b\x00brcm,bcm2711\x00",
46+
"serial-number": "10000000196f8c53\x00",
47+
})
48+
49+
info, err := chassis.New(ghw.WithChroot(root))
50+
if err != nil {
51+
t.Fatal(err)
52+
}
53+
if got, want := info.Vendor, "raspberrypi"; got != want {
54+
t.Errorf("vendor: got %q, want %q", got, want)
55+
}
56+
if got, want := info.Version, "Raspberry Pi 4 Model B Rev 1.4"; got != want {
57+
t.Errorf("version: got %q, want %q", got, want)
58+
}
59+
if got, want := info.SerialNumber, "10000000196f8c53"; got != want {
60+
t.Errorf("serial: got %q, want %q", got, want)
61+
}
62+
if got := info.AssetTag; got != util.UNKNOWN {
63+
t.Errorf("asset tag: got %q, want %q", got, util.UNKNOWN)
64+
}
65+
// No chassis-type on the Pi -> Type/TypeDescription unknown.
66+
if got := info.Type; got != util.UNKNOWN {
67+
t.Errorf("type: got %q, want %q", got, util.UNKNOWN)
68+
}
69+
if got := info.TypeDescription; got != util.UNKNOWN {
70+
t.Errorf("type description: got %q, want %q", got, util.UNKNOWN)
71+
}
72+
}
73+
74+
func TestChassisDeviceTreeChassisType(t *testing.T) {
75+
// radxa NIO 12L: has chassis-type, no serial-number.
76+
root := deviceTreeChroot(t, map[string]string{
77+
"model": "Radxa NIO 12L\x00",
78+
"compatible": "radxa,nio-12l\x00mediatek,mt8395\x00",
79+
"chassis-type": "embedded\x00",
80+
})
81+
82+
info, err := chassis.New(ghw.WithChroot(root))
83+
if err != nil {
84+
t.Fatal(err)
85+
}
86+
if got, want := info.Vendor, "radxa"; got != want {
87+
t.Errorf("vendor: got %q, want %q", got, want)
88+
}
89+
if got, want := info.Type, "34"; got != want {
90+
t.Errorf("type: got %q, want %q", got, want)
91+
}
92+
if got, want := info.TypeDescription, "Embedded PC"; got != want {
93+
t.Errorf("type description: got %q, want %q", got, want)
94+
}
95+
if got := info.SerialNumber; got != util.UNKNOWN {
96+
t.Errorf("serial: got %q, want %q", got, util.UNKNOWN)
97+
}
98+
}
99+
100+
func TestChassisDMITakesPrecedence(t *testing.T) {
101+
root := t.TempDir()
102+
// Both DMI and DeviceTree present: DMI must win.
103+
writeFiles(t, filepath.Join(root, "sys", "class", "dmi", "id"), map[string]string{
104+
"chassis_vendor": "Acme Corp\n",
105+
"chassis_type": "3\n",
106+
"chassis_serial": "DMI-SERIAL\n",
107+
"chassis_version": "1.0\n",
108+
})
109+
writeFiles(t, filepath.Join(root, "sys", "firmware", "devicetree", "base"), map[string]string{
110+
"compatible": "raspberrypi,4-model-b\x00",
111+
"serial-number": "DT-SERIAL\x00",
112+
})
113+
114+
info, err := chassis.New(ghw.WithChroot(root))
115+
if err != nil {
116+
t.Fatal(err)
117+
}
118+
if got, want := info.Vendor, "Acme Corp"; got != want {
119+
t.Errorf("vendor: got %q, want %q (DMI should take precedence)", got, want)
120+
}
121+
if got, want := info.SerialNumber, "DMI-SERIAL"; got != want {
122+
t.Errorf("serial: got %q, want %q (DMI should take precedence)", got, want)
123+
}
124+
if got, want := info.TypeDescription, "Desktop"; got != want {
125+
t.Errorf("type description: got %q, want %q", got, want)
126+
}
127+
}

pkg/cpu/cpu_linux.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"strings"
1919

2020
"github.com/jaypipes/ghw/internal/log"
21+
"github.com/jaypipes/ghw/pkg/linuxdt"
2122
"github.com/jaypipes/ghw/pkg/linuxpath"
2223
"github.com/jaypipes/ghw/pkg/util"
2324
)
@@ -118,6 +119,14 @@ func processorsGet(ctx context.Context) []*Processor {
118119
} else if len(lp.Attrs["machine"]) != 0 {
119120
proc.Model = lp.Attrs["machine"]
120121
}
122+
// Last resort: many ARM/RISC-V boards expose no useful model in
123+
// /proc/cpuinfo. Fall back to the DeviceTree SoC compatible, e.g.
124+
// "rockchip,rk3576".
125+
if proc.Model == "" && linuxdt.Available(ctx) {
126+
if soc := linuxdt.SoC(ctx); soc != util.UNKNOWN {
127+
proc.Model = soc
128+
}
129+
}
121130
// 3. Vendor Detection
122131
if len(lp.Attrs["vendor_id"]) != 0 {
123132
proc.Vendor = lp.Attrs["vendor_id"]

0 commit comments

Comments
 (0)