diff --git a/rapl-daemon/README.md b/rapl-daemon/README.md index e557b9c..415a31c 100644 --- a/rapl-daemon/README.md +++ b/rapl-daemon/README.md @@ -11,3 +11,29 @@ on all worker nodes. --data '{"percentage":75}' \ http://localhost:9090/powercap ``` + +### Payload + +```json +{ + "percentage":75 +} +``` + +### Response + +The daemon will respond with a json payload containing zones that were +successfully capped as well as the zones that were not capped. + +```json +{ + "cappedZones": null, + "failedZones": [ + "intel-rapl:0", + "intel-rapl:1" + ], + "error": "some zones were not able to be powercapped" +} +``` + +Field error will not exist if failed zones is empty. \ No newline at end of file diff --git a/rapl-daemon/main.go b/rapl-daemon/main.go index f1b2c96..e88d3eb 100644 --- a/rapl-daemon/main.go +++ b/rapl-daemon/main.go @@ -15,6 +15,13 @@ type Cap struct { Percentage int } +// CapResponse is the payload sent with information about the capping call +type CapResponse struct { + CappedZones []string `json:"cappedZones"` + FailedZones []string `json:"failedZones"` + Error *string `json:"error"` +} + func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Unsupported endpoint %s", html.EscapeString(r.URL.Path)) @@ -26,19 +33,28 @@ func main() { // Handler for the powercapping HTTP API endpoint. func powercapEndpoint(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var payload Cap + var response CapResponse + decoder := json.NewDecoder(r.Body) err := decoder.Decode(&payload) if err != nil { - http.Error(w, "error parsing payload: "+err.Error(), 400) + errorMsg := "error parsing payload: " + err.Error() + response.Error = &errorMsg + json.NewEncoder(w).Encode(response) return } - err = capNode(powercapDir, payload.Percentage) + cappedZones, failedZones, err := capNode(powercapDir, payload.Percentage) if err != nil { - http.Error(w, err.Error(), 400) - return + errorMsg := err.Error() + response.Error = &errorMsg } - fmt.Fprintf(w, "capped node at %d percent", payload.Percentage) + response.CappedZones = cappedZones + response.FailedZones = failedZones + + json.NewEncoder(w).Encode(response) } diff --git a/rapl-daemon/util.go b/rapl-daemon/util.go index 1d86483..daf8dff 100644 --- a/rapl-daemon/util.go +++ b/rapl-daemon/util.go @@ -19,19 +19,19 @@ const powerLimitFileLongWindow = "constraint_0_power_limit_uw" // capNode uses pseudo files made available by the Linux kernel // in order to capNode CPU power. More information is available at: // https://www.kernel.org/doc/html/latest/power/powercap/powercap.html -func capNode(base string, percentage int) error { +func capNode(base string, percentage int) ([]string, []string, error) { if percentage <= 0 || percentage > 100 { - return fmt.Errorf("cap percentage must be between (0, 100]: %d", percentage) + return nil, nil, fmt.Errorf("cap percentage must be between 0 (non-inclusive) and 100 (inclusive): %d", percentage) } files, err := ioutil.ReadDir(base) if err != nil { - return err + return nil, nil, err } + var capped, failed []string for _, file := range files { - fields := strings.Split(file.Name(), ":") // Fields should be in the form intel-rapl:X where X is the power zone @@ -43,22 +43,28 @@ func capNode(base string, percentage int) error { if fields[0] == raplPrefixCPU { maxPower, err := maxPower(filepath.Join(base, file.Name(), maxPowerFileLongWindow)) if err != nil { + failed = append(failed, file.Name()) fmt.Println("unable to retreive max power for zone ", err) continue } - // We use floats to mitigate the possibility of an integer overflows. + // We use floats to mitigate the possibility of an integer overflow. powercap := uint64(math.Ceil(float64(maxPower) * (float64(percentage) / 100))) - err = capZone(filepath.Join(base, file.Name(), powerLimitFileLongWindow), powercap) - if err != nil { + if err := capZone(filepath.Join(base, file.Name(), powerLimitFileLongWindow), powercap); err != nil { + failed = append(failed, file.Name()) fmt.Println("unable to write powercap value: ", err) continue } + capped = append(capped, file.Name()) } } - return nil + if len(failed) > 0 { + return capped, failed, fmt.Errorf("some zones were not able to be powercapped") + } + + return capped, nil, nil } // maxPower returns the value in float of the maximum watts a power zone can use. @@ -88,3 +94,17 @@ func capZone(limitFile string, value uint64) error { } return nil } + +func currentCap(limit string) (uint64, error) { + powercap, err := ioutil.ReadFile(limit) + if err != nil { + return 0, err + } + + powercapuW, err := strconv.ParseUint(strings.TrimSpace(string(powercap)), 10, 64) + if err != nil { + return 0, err + } + + return powercapuW, nil +} diff --git a/rapl-daemon/util_test.go b/rapl-daemon/util_test.go index 5e26faa..58065bd 100644 --- a/rapl-daemon/util_test.go +++ b/rapl-daemon/util_test.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "strconv" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -48,18 +47,25 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +// TODO(rdelvalle): Add tests where capping fails func TestCapNode(t *testing.T) { - err := capNode(raplDir, 95) + capped, failed, err := capNode(raplDir, 95) assert.NoError(t, err) + assert.Len(t, capped, 1) + assert.Nil(t, failed) t.Run("bad-percentage", func(t *testing.T) { - err := capNode(raplDir, 1000) + capped, failed, err := capNode(raplDir, 1000) assert.Error(t, err) + assert.Nil(t, capped) + assert.Nil(t, failed) }) t.Run("zero-percent", func(t *testing.T) { - err := capNode(raplDir, 0) + capped, failed, err := capNode(raplDir, 0) assert.Error(t, err) + assert.Nil(t, capped) + assert.Nil(t, failed) }) } @@ -84,10 +90,7 @@ func TestCapZone(t *testing.T) { err := capZone(limitFile, powercap) assert.NoError(t, err) - newCapBytes, err := ioutil.ReadFile(limitFile) - assert.NoError(t, err) - - newCap, err := strconv.ParseUint(strings.TrimSpace(string(newCapBytes)), 10, 64) + newCap, err := currentCap(limitFile) assert.NoError(t, err) assert.Equal(t, powercap, newCap)