Skip to content

Commit 92cb623

Browse files
tas50claude
andcommitted
✨ Add FreeBSD, AIX support and normalize AMD manufacturer
- Add getCpuInfoFreeBSD using sysctl hw.model and kern.smp.cores - Add getCpuInfoAIX using prtconf output parsing - Extract normalizeManufacturer helper: GenuineIntel→Intel, AuthenticAMD→AMD - Apply normalizeManufacturer to Linux and Windows paths - Add tests for AMD Linux, FreeBSD, and AIX Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 10dae7d commit 92cb623

File tree

3 files changed

+212
-12
lines changed

3 files changed

+212
-12
lines changed

providers/os/resources/cpu.go

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ func initMachineCpu(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[
4242
info, err = getCpuInfoMacos(conn)
4343
case pf.IsFamily("windows"):
4444
info, err = getCpuInfoWindows(conn)
45+
case pf.Name == "freebsd":
46+
info, err = getCpuInfoFreeBSD(conn)
47+
case pf.Name == "aix":
48+
info, err = getCpuInfoAIX(conn)
4549
case pf.IsFamily(inventory.FAMILY_UNIX):
4650
info, err = getCpuInfoLinux(conn)
4751
default:
@@ -94,13 +98,8 @@ func getCpuInfoLinux(conn shared.Connection) (*cpuInfo, error) {
9498
seenSockets[p.PhysicalID] = struct{}{}
9599
}
96100

97-
manufacturer := parsed.Processors[0].VendorID
98-
if manufacturer == "GenuineIntel" {
99-
manufacturer = "Intel"
100-
}
101-
102101
info := &cpuInfo{
103-
Manufacturer: manufacturer,
102+
Manufacturer: normalizeManufacturer(parsed.Processors[0].VendorID),
104103
Model: parsed.Processors[0].ModelName,
105104
}
106105

@@ -142,11 +141,8 @@ func getCpuInfoMacos(conn shared.Connection) (*cpuInfo, error) {
142141
return nil, fmt.Errorf("failed to parse hw.physicalcpu: %w", err)
143142
}
144143

145-
// Strip "Apple " prefix from brand string (e.g. "Apple M4 Pro" -> "M4 Pro")
146-
model := strings.TrimPrefix(brandString, "Apple ")
147-
148144
info := &cpuInfo{
149-
Model: model,
145+
Model: brandString,
150146
Cores: physicalCPU,
151147
ProcessorCount: 1, // Macs always have a single CPU package
152148
}
@@ -202,8 +198,103 @@ func getCpuInfoWindows(conn shared.Connection) (*cpuInfo, error) {
202198

203199
return &cpuInfo{
204200
Model: strings.TrimSpace(result.Name),
205-
Manufacturer: strings.TrimSpace(result.Manufacturer),
201+
Manufacturer: normalizeManufacturer(strings.TrimSpace(result.Manufacturer)),
206202
Cores: result.NumberOfCores,
207203
ProcessorCount: result.ProcessorCount,
208204
}, nil
209205
}
206+
207+
func getCpuInfoFreeBSD(conn shared.Connection) (*cpuInfo, error) {
208+
cmd, err := conn.RunCommand("sysctl -n hw.model kern.smp.cores")
209+
if err != nil {
210+
return nil, err
211+
}
212+
if cmd.ExitStatus != 0 {
213+
stderr, _ := io.ReadAll(cmd.Stderr)
214+
return nil, fmt.Errorf("sysctl failed: %s", string(stderr))
215+
}
216+
217+
data, err := io.ReadAll(cmd.Stdout)
218+
if err != nil {
219+
return nil, err
220+
}
221+
222+
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
223+
if len(lines) < 2 {
224+
return nil, fmt.Errorf("unexpected sysctl output: %s", string(data))
225+
}
226+
227+
model := strings.TrimSpace(lines[0])
228+
cores, err := strconv.ParseInt(strings.TrimSpace(lines[1]), 10, 64)
229+
if err != nil {
230+
return nil, fmt.Errorf("failed to parse kern.smp.cores: %w", err)
231+
}
232+
233+
// FreeBSD doesn't expose socket/package count directly; default to 1
234+
info := &cpuInfo{
235+
Model: model,
236+
Cores: cores,
237+
ProcessorCount: 1,
238+
}
239+
240+
// Extract manufacturer from model string
241+
if strings.Contains(model, "Intel") {
242+
info.Manufacturer = "Intel"
243+
} else if strings.Contains(model, "AMD") {
244+
info.Manufacturer = "AMD"
245+
}
246+
247+
return info, nil
248+
}
249+
250+
func getCpuInfoAIX(conn shared.Connection) (*cpuInfo, error) {
251+
// Use prtconf for processor type and count
252+
cmd, err := conn.RunCommand("prtconf")
253+
if err != nil {
254+
return nil, err
255+
}
256+
if cmd.ExitStatus != 0 {
257+
stderr, _ := io.ReadAll(cmd.Stderr)
258+
return nil, fmt.Errorf("prtconf failed: %s", string(stderr))
259+
}
260+
261+
data, err := io.ReadAll(cmd.Stdout)
262+
if err != nil {
263+
return nil, err
264+
}
265+
266+
info := &cpuInfo{
267+
Manufacturer: "IBM",
268+
ProcessorCount: 1,
269+
}
270+
271+
for _, line := range strings.Split(string(data), "\n") {
272+
line = strings.TrimSpace(line)
273+
if k, v, ok := strings.Cut(line, ":"); ok {
274+
k = strings.TrimSpace(k)
275+
v = strings.TrimSpace(v)
276+
switch k {
277+
case "Processor Type":
278+
info.Model = v
279+
case "Number Of Processors":
280+
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
281+
info.Cores = n
282+
}
283+
}
284+
}
285+
}
286+
287+
return info, nil
288+
}
289+
290+
// normalizeManufacturer maps CPUID vendor strings to human-readable names.
291+
func normalizeManufacturer(vendor string) string {
292+
switch vendor {
293+
case "GenuineIntel":
294+
return "Intel"
295+
case "AuthenticAMD":
296+
return "AMD"
297+
default:
298+
return vendor
299+
}
300+
}

providers/os/resources/cpu_test.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func TestGetCpuInfoMacosAppleSilicon(t *testing.T) {
3939
require.NoError(t, err)
4040

4141
assert.Equal(t, "Apple", info.Manufacturer)
42-
assert.Equal(t, "M4 Pro", info.Model)
42+
assert.Equal(t, "Apple M4 Pro", info.Model)
4343
assert.Equal(t, int64(1), info.ProcessorCount)
4444
assert.Equal(t, int64(14), info.Cores)
4545
}
@@ -63,6 +63,61 @@ func TestGetCpuInfoMacosIntel(t *testing.T) {
6363
assert.Equal(t, int64(8), info.Cores)
6464
}
6565

66+
func TestGetCpuInfoLinuxAMD(t *testing.T) {
67+
conn, err := mock.New(0, &inventory.Asset{}, mock.WithPath("./procfs/testdata/cpu-info-amd.toml"))
68+
require.NoError(t, err)
69+
70+
info, err := getCpuInfoLinux(conn)
71+
require.NoError(t, err)
72+
73+
assert.Equal(t, "AMD", info.Manufacturer)
74+
assert.Equal(t, "AMD EPYC 7763 64-Core Processor", info.Model)
75+
assert.Equal(t, int64(1), info.ProcessorCount)
76+
assert.Equal(t, int64(2), info.Cores)
77+
}
78+
79+
func TestGetCpuInfoFreeBSD(t *testing.T) {
80+
conn, err := mock.New(0, &inventory.Asset{}, mock.WithData(&mock.TomlData{
81+
Commands: map[string]*mock.Command{
82+
"sysctl -n hw.model kern.smp.cores": {
83+
Stdout: "Intel(R) Xeon(R) CPU E3-1220 v5 @ 3.00GHz\n4\n",
84+
},
85+
},
86+
}))
87+
require.NoError(t, err)
88+
89+
info, err := getCpuInfoFreeBSD(conn)
90+
require.NoError(t, err)
91+
92+
assert.Equal(t, "Intel", info.Manufacturer)
93+
assert.Equal(t, "Intel(R) Xeon(R) CPU E3-1220 v5 @ 3.00GHz", info.Model)
94+
assert.Equal(t, int64(1), info.ProcessorCount)
95+
assert.Equal(t, int64(4), info.Cores)
96+
}
97+
98+
func TestGetCpuInfoAIX(t *testing.T) {
99+
conn, err := mock.New(0, &inventory.Asset{}, mock.WithData(&mock.TomlData{
100+
Commands: map[string]*mock.Command{
101+
"prtconf": {
102+
Stdout: "System Model: IBM,9009-42A\n" +
103+
"Processor Type: PowerPC_POWER9\n" +
104+
"Number Of Processors: 12\n" +
105+
"Processor Clock Speed: 3800 MHz\n" +
106+
"CPU Type: 64-bit\n",
107+
},
108+
},
109+
}))
110+
require.NoError(t, err)
111+
112+
info, err := getCpuInfoAIX(conn)
113+
require.NoError(t, err)
114+
115+
assert.Equal(t, "IBM", info.Manufacturer)
116+
assert.Equal(t, "PowerPC_POWER9", info.Model)
117+
assert.Equal(t, int64(1), info.ProcessorCount)
118+
assert.Equal(t, int64(12), info.Cores)
119+
}
120+
66121
func TestGetCpuInfoLinuxArm(t *testing.T) {
67122
conn, err := mock.New(0, &inventory.Asset{}, mock.WithPath("./procfs/testdata/cpu-info-aarch64.toml"))
68123
require.NoError(t, err)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
[files."/proc/cpuinfo"]
2+
content = """
3+
processor : 0
4+
vendor_id : AuthenticAMD
5+
cpu family : 25
6+
model : 1
7+
model name : AMD EPYC 7763 64-Core Processor
8+
stepping : 1
9+
cpu MHz : 2450.000
10+
cache size : 512 KB
11+
physical id : 0
12+
siblings : 2
13+
core id : 0
14+
cpu cores : 2
15+
apicid : 0
16+
initial apicid : 0
17+
fpu : yes
18+
fpu_exception : yes
19+
cpuid level : 13
20+
wp : yes
21+
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm rep_good nopl cpuid extd_apicid tsc_known_freq pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm cmp_legacy cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw topoext ssbd ibrs ibpb stibp vmmcall fsgsbase bmi1 avx2 smep bmi2 erms invpcid rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xgetbv1 xsaves
22+
bugs : sysret_ss_attrs spectre_v1 spectre_v2 spec_store_bypass
23+
bogomips : 4890.86
24+
clflush size : 64
25+
cache_alignment : 64
26+
address sizes : 48 bits physical, 48 bits virtual
27+
power management:
28+
29+
processor : 1
30+
vendor_id : AuthenticAMD
31+
cpu family : 25
32+
model : 1
33+
model name : AMD EPYC 7763 64-Core Processor
34+
stepping : 1
35+
cpu MHz : 2450.000
36+
cache size : 512 KB
37+
physical id : 0
38+
siblings : 2
39+
core id : 1
40+
cpu cores : 2
41+
apicid : 1
42+
initial apicid : 1
43+
fpu : yes
44+
fpu_exception : yes
45+
cpuid level : 13
46+
wp : yes
47+
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm rep_good nopl cpuid extd_apicid tsc_known_freq pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm cmp_legacy cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw topoext ssbd ibrs ibpb stibp vmmcall fsgsbase bmi1 avx2 smep bmi2 erms invpcid rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xgetbv1 xsaves
48+
bugs : sysret_ss_attrs spectre_v1 spectre_v2 spec_store_bypass
49+
bogomips : 4890.86
50+
clflush size : 64
51+
cache_alignment : 64
52+
address sizes : 48 bits physical, 48 bits virtual
53+
power management:
54+
"""

0 commit comments

Comments
 (0)