capping funciton now returns which zones were sucessfully capped and which zones could not be capped. This information is now returned to the caller of the HTTP api.

This commit is contained in:
Renan DelValle 2020-01-18 20:11:44 -08:00
parent bcd25c805a
commit 5e4ba5a933
No known key found for this signature in database
GPG key ID: 3895800E03F17676
4 changed files with 86 additions and 21 deletions

View file

@ -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.

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)