diff --git a/client/fingerprint/cpu.go b/client/fingerprint/cpu.go index 421614824..0c9b4c25d 100644 --- a/client/fingerprint/cpu.go +++ b/client/fingerprint/cpu.go @@ -31,39 +31,48 @@ func (f *CPUFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bo } if err := stats.Init(); err != nil { - err := fmt.Errorf("Unable to obtain CPU information: %v", err) - - if cfg.CpuCompute != 0 { - f.logger.Printf("[DEBUG] fingerprint.cpu: %v. Using specified cpu compute %d", err, cfg.CpuCompute) - setResources(cfg.CpuCompute) - return true, nil - } - - f.logger.Printf("[ERR] fingerprint.cpu: %v", err) - f.logger.Printf("[INFO] fingerprint.cpu: cpu compute may be set manually"+ - " using the client config option %q on machines where cpu information"+ - " can not be automatically detected.", "cpu_total_compute") - - return false, err + f.logger.Printf("[WARN] fingerprint.cpu: %v", err) } - modelName := stats.CPUModelName() - if modelName != "" { + if cfg.CpuCompute != 0 { + setResources(cfg.CpuCompute) + return true, nil + } + + if modelName := stats.CPUModelName(); modelName != "" { node.Attributes["cpu.modelname"] = modelName } - mhz := stats.CPUMHzPerCore() - node.Attributes["cpu.frequency"] = fmt.Sprintf("%.0f", mhz) - f.logger.Printf("[DEBUG] fingerprint.cpu: frequency: %.0f MHz", mhz) + if mhz := stats.CPUMHzPerCore(); mhz > 0 { + node.Attributes["cpu.frequency"] = fmt.Sprintf("%.0f", mhz) + f.logger.Printf("[DEBUG] fingerprint.cpu: frequency: %.0f MHz", mhz) + } - numCores := stats.CPUNumCores() - node.Attributes["cpu.numcores"] = fmt.Sprintf("%d", numCores) - f.logger.Printf("[DEBUG] fingerprint.cpu: core count: %d", numCores) + if numCores := stats.CPUNumCores(); numCores > 0 { + node.Attributes["cpu.numcores"] = fmt.Sprintf("%d", numCores) + f.logger.Printf("[DEBUG] fingerprint.cpu: core count: %d", numCores) + } - tt := stats.TotalTicksAvailable() + tt := int(stats.TotalTicksAvailable()) + if cfg.CpuCompute > 0 { + f.logger.Printf("[DEBUG] fingerprint.cpu: Using specified cpu compute %d", cfg.CpuCompute) + tt = cfg.CpuCompute + } - node.Attributes["cpu.totalcompute"] = fmt.Sprintf("%.0f", tt) + // Return an error if no cpu was detected or explicitly set as this + // node would be unable to receive any allocations. + if tt == 0 { + return false, fmt.Errorf("cannot detect cpu total compute. "+ + "CPU compute must be set manually using the client config option %q", + "cpu_total_compute") + } - setResources(int(tt)) + node.Attributes["cpu.totalcompute"] = fmt.Sprintf("%d", tt) + + if node.Resources == nil { + node.Resources = &structs.Resources{} + } + + node.Resources.CPU = tt return true, nil } diff --git a/client/fingerprint/cpu_test.go b/client/fingerprint/cpu_test.go index 55c1e1008..238d55770 100644 --- a/client/fingerprint/cpu_test.go +++ b/client/fingerprint/cpu_test.go @@ -40,3 +40,39 @@ func TestCPUFingerprint(t *testing.T) { } } + +// TestCPUFingerprint_OverrideCompute asserts that setting cpu_total_compute in +// the client config overrides the detected CPU freq (if any). +func TestCPUFingerprint_OverrideCompute(t *testing.T) { + f := NewCPUFingerprint(testLogger()) + node := &structs.Node{ + Attributes: make(map[string]string), + } + cfg := &config.Config{} + ok, err := f.Fingerprint(cfg, node) + if err != nil { + t.Fatalf("err: %v", err) + } + if !ok { + t.Fatalf("should apply") + } + + // Get actual system CPU + origCPU := node.Resources.CPU + + // Override it with a setting + cfg.CpuCompute = origCPU + 123 + + // Make sure the Fingerprinter applies the override + ok, err = f.Fingerprint(cfg, node) + if err != nil { + t.Fatalf("err: %v", err) + } + if !ok { + t.Fatalf("should apply") + } + + if node.Resources.CPU != cfg.CpuCompute { + t.Fatalf("expected override cpu of %d but found %d", cfg.CpuCompute, node.Resources.CPU) + } +} diff --git a/helper/stats/cpu.go b/helper/stats/cpu.go index 7326deda6..e6d3bf26c 100644 --- a/helper/stats/cpu.go +++ b/helper/stats/cpu.go @@ -5,6 +5,7 @@ import ( "math" "sync" + multierror "github.com/hashicorp/go-multierror" "github.com/shirou/gopsutil/cpu" ) @@ -20,15 +21,15 @@ var ( func Init() error { onceLer.Do(func() { - if cpuNumCores, initErr = cpu.Counts(true); initErr != nil { - initErr = fmt.Errorf("Unable to determine the number of CPU cores available: %v", initErr) - return + var merrs *multierror.Error + var err error + if cpuNumCores, err = cpu.Counts(true); err != nil { + merrs = multierror.Append(merrs, fmt.Errorf("Unable to determine the number of CPU cores available: %v", err)) } var cpuInfo []cpu.InfoStat - if cpuInfo, initErr = cpu.Info(); initErr != nil { - initErr = fmt.Errorf("Unable to obtain CPU information: %v", initErr) - return + if cpuInfo, err = cpu.Info(); err != nil { + merrs = multierror.Append(merrs, fmt.Errorf("Unable to obtain CPU information: %v", initErr)) } for _, cpu := range cpuInfo { @@ -41,6 +42,9 @@ func Init() error { // node to fall into a unique computed node class cpuMhzPerCore = math.Floor(cpuMhzPerCore) cpuTotalTicks = math.Floor(float64(cpuNumCores) * cpuMhzPerCore) + + // Set any errors that occurred + initErr = merrs.ErrorOrNil() }) return initErr } @@ -60,8 +64,7 @@ func CPUModelName() string { return cpuModelName } -// TotalTicksAvailable calculates the total frequency available across all -// cores +// TotalTicksAvailable calculates the total Mhz available across all cores func TotalTicksAvailable() float64 { return cpuTotalTicks } diff --git a/website/source/docs/runtime/interpolation.html.md.erb b/website/source/docs/runtime/interpolation.html.md.erb index b5f9cb6b8..b414e9158 100644 --- a/website/source/docs/runtime/interpolation.html.md.erb +++ b/website/source/docs/runtime/interpolation.html.md.erb @@ -114,14 +114,20 @@ Below is a table documenting common node properties: