Files
troubleshoot/pkg/analyze/host_cpu.go
Ash ea900a1881 chore: Refactor host cpu analyzer for remote collection (#1664)
* Refactor host cpu analyzer for remote collection

---------

Co-authored-by: Gerard Nguyen <gerard@replicated.com>
2024-11-06 14:43:27 +11:00

209 lines
6.1 KiB
Go

package analyzer
import (
"encoding/json"
"fmt"
"slices"
"strconv"
"strings"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
// microarchs holds a list of features present in each microarchitecture.
// ref: https://gitlab.com/x86-psABIs/x86-64-ABI
// ref: https://developers.redhat.com/blog/2021/01/05/building-red-hat-enterprise-linux-9-for-the-x86-64-v2-microarchitecture-level
var microarchs = map[string][]string{
"x86-64": {"cmov", "cx8", "fpu", "fxsr", "mmx", "syscall", "sse", "sse2"},
"x86-64-v2": {"cx16", "lahf_lm", "popcnt", "ssse3", "sse4_1", "sse4_2", "ssse3"},
"x86-64-v3": {"avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "lzcnt", "movbe", "xsave"},
"x86-64-v4": {"avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"},
}
type AnalyzeHostCPU struct {
hostAnalyzer *troubleshootv1beta2.CPUAnalyze
}
func (a *AnalyzeHostCPU) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Number of CPUs")
}
func (a *AnalyzeHostCPU) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostCPU) CheckCondition(when string, data []byte) (bool, error) {
cpuInfo := collect.CPUInfo{}
if err := json.Unmarshal(data, &cpuInfo); err != nil {
return false, fmt.Errorf("failed to unmarshal data into CPUInfo: %v", err)
}
return compareHostCPUConditionalToActual(when, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags, cpuInfo.MachineArch)
}
func (a *AnalyzeHostCPU) Analyze(
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
) ([]*AnalyzeResult, error) {
result := AnalyzeResult{Title: a.Title()}
// Use the generic function to collect both local and remote data
collectedContents, err := retrieveCollectedContents(
getCollectedFileContents,
collect.HostCPUPath, // Local path
collect.NodeInfoBaseDir, // Remote base directory
collect.HostCPUFileName, // Remote file name
)
if err != nil {
return []*AnalyzeResult{&result}, err
}
results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
if err != nil {
return nil, errors.Wrap(err, "failed to analyze OS version")
}
return results, nil
}
func doCompareHostCPUMicroArchitecture(microarch string, flags []string) (res bool, err error) {
specifics := make([]string, 0)
switch microarch {
case "x86-64-v4":
specifics = append(specifics, microarchs["x86-64-v4"]...)
fallthrough
case "x86-64-v3":
specifics = append(specifics, microarchs["x86-64-v3"]...)
fallthrough
case "x86-64-v2":
specifics = append(specifics, microarchs["x86-64-v2"]...)
fallthrough
case "x86-64":
specifics = append(specifics, microarchs["x86-64"]...)
default:
return false, errors.Errorf("troubleshoot does not yet support microarchitecture %q", microarch)
}
for _, flag := range specifics {
if slices.Contains(flags, flag) {
continue
}
return false, nil
}
return true, nil
}
func doCompareHostCPUFlags(expected string, flags []string) (res bool, err error) {
expectedFlags := strings.Split(expected, ",")
if len(expectedFlags) == 0 {
return false, errors.New("expected flags cannot be empty")
}
for _, flag := range expectedFlags {
if slices.Contains(flags, flag) {
continue
}
return false, nil
}
return true, nil
}
func compareHostCPUConditionalToActual(conditional string, logicalCount int, physicalCount int, flags []string, machineArch string) (res bool, err error) {
compareLogical := false
comparePhysical := false
compareUnspecified := false
compareMachineArch := false
comparator := ""
desired := ""
/* When the conditional is in the format of "logical <comparator> <desired>"
example: when: "count < 2"
*/
parts := strings.Split(conditional, " ")
if len(parts) == 3 {
comparator = parts[1]
desired = parts[2]
if strings.ToLower(parts[0]) == "logical" {
compareLogical = true
} else if strings.ToLower(parts[0]) == "physical" {
comparePhysical = true
} else if strings.ToLower(parts[0]) == "count" {
compareUnspecified = true
} else if strings.ToLower(parts[0]) == "machinearch" {
compareMachineArch = true
}
} else if len(parts) == 2 {
compareUnspecified = true
comparator = parts[0]
desired = parts[1]
}
// analyze if the cpu supports a specific set of features, aka as micrarchitecture.
if strings.ToLower(comparator) == "supports" {
return doCompareHostCPUMicroArchitecture(desired, flags)
}
// hasFlags allows users to query for specific flags on the CPU.
if strings.ToLower(comparator) == "hasflags" {
return doCompareHostCPUFlags(desired, flags)
}
if !compareLogical && !comparePhysical && !compareUnspecified && !compareMachineArch {
return false, errors.New("unable to parse conditional")
}
if compareLogical {
return doCompareHostCPU(comparator, desired, logicalCount)
} else if comparePhysical {
return doCompareHostCPU(comparator, desired, physicalCount)
} else if compareMachineArch {
return doCompareMachineArch(comparator, desired, machineArch)
} else {
actual := logicalCount
if physicalCount > logicalCount {
actual = physicalCount
}
return doCompareHostCPU(comparator, desired, actual)
}
}
func doCompareMachineArch(operator string, desired string, actual string) (bool, error) {
switch operator {
case "=", "==", "===":
return actual == desired, nil
case "!=", "!==":
return actual != desired, nil
}
return false, errors.New("unknown operator")
}
func doCompareHostCPU(operator string, desired string, actual int) (bool, error) {
desiredInt, err := strconv.ParseInt(desired, 10, 64)
if err != nil {
return false, errors.Wrap(err, "failed to parse")
}
switch operator {
case "<":
return actual < int(desiredInt), nil
case "<=":
return actual <= int(desiredInt), nil
case ">":
return actual > int(desiredInt), nil
case ">=":
return actual >= int(desiredInt), nil
case "=", "==", "===":
return actual == int(desiredInt), nil
case "!=", "!==":
return actual != int(desiredInt), nil
}
return false, errors.New("unknown operator")
}