Skip to content

Commit 668b7ed

Browse files
feat: add CPU micro architecture support (#1628)
allows troubleshoot to collect and analyze CPU micro architecture. this is an usage example: ```yaml apiVersion: troubleshoot.sh/v1beta2 kind: HostPreflight metadata: name: ec-cluster-preflight spec: collectors: - cpu: {} analyzers: - cpu: checkName: CPU outcomes: - pass: when: 'supports x86-64-v2' message: CPU supports x86-64-v2 - fail: message: CPU does not support x86-64-v2 ```
1 parent 2bb611c commit 668b7ed

File tree

4 files changed

+79
-9
lines changed

4 files changed

+79
-9
lines changed

pkg/analyze/host_cpu.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package analyzer
22

33
import (
44
"encoding/json"
5+
"slices"
56
"strconv"
67
"strings"
78

@@ -10,6 +11,18 @@ import (
1011
"github.com/replicatedhq/troubleshoot/pkg/collect"
1112
)
1213

14+
// microarchs holds a list of features present in each microarchitecture.
15+
// ref: https://gitlab.com/x86-psABIs/x86-64-ABI
16+
// ref: https://developers.redhat.com/blog/2021/01/05/building-red-hat-enterprise-linux-9-for-the-x86-64-v2-microarchitecture-level
17+
var microarchs = map[string][]string{
18+
"x86-64-v2": {"cx16", "lahf_lm", "popcnt", "ssse3", "sse4_1", "sse4_2", "ssse3"},
19+
"x86-64-v3": {"avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "lzcnt", "movbe", "xsave"},
20+
"x86-64-v4": {"avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"},
21+
}
22+
23+
// x8664BaseFeatures are the features that are present in all x86-64 microarchitectures.
24+
var x8664BaseFeatures = []string{"cmov", "cx8", "fpu", "fxsr", "mmx", "syscall", "sse", "sse2"}
25+
1326
type AnalyzeHostCPU struct {
1427
hostAnalyzer *troubleshootv1beta2.CPUAnalyze
1528
}
@@ -52,7 +65,7 @@ func (a *AnalyzeHostCPU) Analyze(
5265
return []*AnalyzeResult{&result}, nil
5366
}
5467

55-
isMatch, err := compareHostCPUConditionalToActual(outcome.Fail.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount)
68+
isMatch, err := compareHostCPUConditionalToActual(outcome.Fail.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags)
5669
if err != nil {
5770
return nil, errors.Wrap(err, "failed to compare")
5871
}
@@ -73,7 +86,7 @@ func (a *AnalyzeHostCPU) Analyze(
7386
return []*AnalyzeResult{&result}, nil
7487
}
7588

76-
isMatch, err := compareHostCPUConditionalToActual(outcome.Warn.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount)
89+
isMatch, err := compareHostCPUConditionalToActual(outcome.Warn.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags)
7790
if err != nil {
7891
return nil, errors.Wrap(err, "failed to compare")
7992
}
@@ -94,7 +107,7 @@ func (a *AnalyzeHostCPU) Analyze(
94107
return []*AnalyzeResult{&result}, nil
95108
}
96109

97-
isMatch, err := compareHostCPUConditionalToActual(outcome.Pass.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount)
110+
isMatch, err := compareHostCPUConditionalToActual(outcome.Pass.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags)
98111
if err != nil {
99112
return nil, errors.Wrap(err, "failed to compare")
100113
}
@@ -112,7 +125,25 @@ func (a *AnalyzeHostCPU) Analyze(
112125
return []*AnalyzeResult{&result}, nil
113126
}
114127

115-
func compareHostCPUConditionalToActual(conditional string, logicalCount int, physicalCount int) (res bool, err error) {
128+
func doCompareHostCPUMicroArchitecture(microarch string, flags []string) (res bool, err error) {
129+
specifics, ok := microarchs[microarch]
130+
if !ok && microarch != "x86-64" {
131+
return false, errors.Errorf("troubleshoot does not yet support microarchitecture %q", microarch)
132+
}
133+
expectedFlags := x8664BaseFeatures
134+
if len(specifics) > 0 {
135+
expectedFlags = append(expectedFlags, specifics...)
136+
}
137+
for _, flag := range expectedFlags {
138+
if slices.Contains(flags, flag) {
139+
continue
140+
}
141+
return false, nil
142+
}
143+
return true, nil
144+
}
145+
146+
func compareHostCPUConditionalToActual(conditional string, logicalCount int, physicalCount int, flags []string) (res bool, err error) {
116147
compareLogical := false
117148
comparePhysical := false
118149
compareUnspecified := false
@@ -137,6 +168,11 @@ func compareHostCPUConditionalToActual(conditional string, logicalCount int, phy
137168
desired = parts[1]
138169
}
139170

171+
// analyze if the cpu supports a specific set of features, aka as micrarchitecture.
172+
if strings.ToLower(comparator) == "supports" {
173+
return doCompareHostCPUMicroArchitecture(desired, flags)
174+
}
175+
140176
if !compareLogical && !comparePhysical && !compareUnspecified {
141177
return false, errors.New("unable to parse conditional")
142178
}

pkg/analyze/host_cpu_test.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ func Test_compareHostCPUConditionalToActual(t *testing.T) {
8181
when string
8282
logicalCount int
8383
physicalCount int
84+
flags []string
8485
expected bool
8586
}{
8687
{
@@ -139,12 +140,24 @@ func Test_compareHostCPUConditionalToActual(t *testing.T) {
139140
physicalCount: 4,
140141
expected: true,
141142
},
143+
{
144+
name: "supports x86-64-v2 microarchitecture",
145+
when: "supports x86-64-v2",
146+
flags: []string{""},
147+
expected: false,
148+
},
149+
{
150+
name: "supports x86-64-v2 microarchitecture",
151+
when: "supports x86-64-v2",
152+
flags: []string{"cmov", "cx8", "fpu", "fxsr", "mmx", "syscall", "sse", "sse2", "cx16", "lahf_lm", "popcnt", "ssse3", "sse4_1", "sse4_2", "ssse3"},
153+
expected: true,
154+
},
142155
}
143156
for _, test := range tests {
144157
t.Run(test.name, func(t *testing.T) {
145158
req := require.New(t)
146159

147-
actual, err := compareHostCPUConditionalToActual(test.when, test.logicalCount, test.physicalCount)
160+
actual, err := compareHostCPUConditionalToActual(test.when, test.logicalCount, test.physicalCount, test.flags)
148161
req.NoError(err)
149162

150163
assert.Equal(t, test.expected, actual)

pkg/collect/host_cpu.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import (
1010
)
1111

1212
type CPUInfo struct {
13-
LogicalCount int `json:"logicalCount"`
14-
PhysicalCount int `json:"physicalCount"`
13+
LogicalCount int `json:"logicalCount"`
14+
PhysicalCount int `json:"physicalCount"`
15+
Flags []string `json:"flags"`
1516
}
1617

1718
const HostCPUPath = `host-collectors/system/cpu.json`
@@ -44,6 +45,25 @@ func (c *CollectHostCPU) Collect(progressChan chan<- interface{}) (map[string][]
4445
}
4546
cpuInfo.PhysicalCount = physicalCount
4647

48+
// XXX even though the cpu.Info() returns a slice per CPU it is way
49+
// common to have the same flags for all CPUs. We consolidate them here
50+
// so the output is a list of all different flags present in all CPUs.
51+
info, err := cpu.Info()
52+
if err != nil {
53+
return nil, errors.Wrap(err, "failed to get cpu info")
54+
}
55+
56+
seen := make(map[string]bool)
57+
for _, infoForCPU := range info {
58+
for _, flag := range infoForCPU.Flags {
59+
if seen[flag] {
60+
continue
61+
}
62+
seen[flag] = true
63+
cpuInfo.Flags = append(cpuInfo.Flags, flag)
64+
}
65+
}
66+
4767
b, err := json.Marshal(cpuInfo)
4868
if err != nil {
4969
return nil, errors.Wrap(err, "failed to marshal cpu info")

pkg/collect/host_cpu_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ func TestCollectHostCPU_Collect(t *testing.T) {
2020
require.Contains(t, got, "host-collectors/system/cpu.json")
2121
values := got["host-collectors/system/cpu.json"]
2222

23-
var m map[string]int
23+
var m map[string]interface{}
2424
err = json.Unmarshal(values, &m)
2525
require.NoError(t, err)
2626

2727
// Check if values exist. They will be different on different machines.
28-
assert.Equal(t, 2, len(m))
28+
assert.Equal(t, 3, len(m))
2929
assert.Contains(t, m, "logicalCount")
3030
assert.Contains(t, m, "physicalCount")
31+
assert.Contains(t, m, "flags")
3132
}

0 commit comments

Comments
 (0)