Add systemd resource budgets with TOML profiles
This commit is contained in:
commit
be8749a384
1 changed files with 2591 additions and 0 deletions
242
internal/budget/config_test.go
Normal file
242
internal/budget/config_test.go
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
package budget
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func testConfig() config {
|
||||
return config{
|
||||
Profiles: map[string]profile{
|
||||
"default": {"400%", "6G", "8G", "2G"},
|
||||
"compute": {"800%", "12G", "16G", "2G"},
|
||||
"bulk": {"400%", "6G", "8G", "2G"},
|
||||
"search": {"200%", "512M", "1G", "0"},
|
||||
"unrestricted": {"", "infinity", "infinity", "infinity"},
|
||||
},
|
||||
Devices: []device{{"/dev/mock", map[string]ioLimit{
|
||||
"default": {"100M", "100M", "4000", "4000"},
|
||||
"compute": {"500M", "500M", "20000", "20000"},
|
||||
"bulk": {"500M", "500M", "10000", "10000"},
|
||||
"search": {"50M", "5M", "1000", "1000"},
|
||||
}}},
|
||||
Apps: map[string][]string{
|
||||
"unrestricted": {"org.example.Terminal", "org.example.Console"},
|
||||
"compute": {"game", "org.example.Game"},
|
||||
"bulk": {"org.example.Download"},
|
||||
},
|
||||
Units: map[string][]string{"unrestricted": {"containerd.service", "worker.service"}},
|
||||
Prefixes: map[string][]string{"unrestricted": {"app-terminal-.scope.d"}},
|
||||
Commands: map[string]string{"rg": "search", "java": "compute", "rsync": "bulk"},
|
||||
Python: "compute",
|
||||
Terminals: []string{"app-terminal-", "app-org.example.Terminal", "app-org.example.Console"},
|
||||
Shared: map[string]string{"editor": "default", "editor-uri-handler": "default"},
|
||||
}
|
||||
}
|
||||
|
||||
func values(properties []property) map[string]dbus.Variant {
|
||||
result := map[string]dbus.Variant{}
|
||||
for _, p := range properties {
|
||||
result[p.Name] = p.Value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestParseSize(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
value string
|
||||
base uint64
|
||||
want uint64
|
||||
}{
|
||||
{"8G", 1024, 8589934592}, {"12G", 1024, 12884901888},
|
||||
{"16G", 1024, 17179869184}, {"512M", 1024, 536870912},
|
||||
{"50M", 1000, 50000000}, {"5K", 1000, 5000},
|
||||
{"1000", 1000, 1000}, {"0", 1024, 0}, {"infinity", 1024, math.MaxUint64},
|
||||
} {
|
||||
t.Run(tc.value, func(t *testing.T) {
|
||||
got, err := parseSize(tc.value, tc.base)
|
||||
if err != nil || got != tc.want {
|
||||
t.Fatalf("got %d, %v; want %d", got, err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, input := range []string{"", "-1", "abc", "1T"} {
|
||||
if _, err := parseSize(input, 1024); err == nil {
|
||||
t.Errorf("accepted %q", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeProps(t *testing.T) {
|
||||
props, err := testConfig().properties("compute")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := values(props)
|
||||
for name, want := range map[string]uint64{
|
||||
"CPUQuotaPerSecUSec": 8000000, "MemoryHigh": 12884901888,
|
||||
"MemoryMax": 17179869184, "MemorySwapMax": 2147483648,
|
||||
} {
|
||||
if got[name].Signature().String() != "t" || got[name].Value() != want {
|
||||
t.Errorf("%s: %v", name, got[name])
|
||||
}
|
||||
}
|
||||
if got["IOReadBandwidthMax"].Signature().String() != "a(st)" {
|
||||
t.Fatal("wrong I/O wire signature")
|
||||
}
|
||||
want := []diskLimit{{"/dev/mock", 500000000}}
|
||||
if !reflect.DeepEqual(got["IOReadBandwidthMax"].Value(), want) {
|
||||
t.Fatal(got["IOReadBandwidthMax"])
|
||||
}
|
||||
if dbus.SignatureOf(props).String() != "a(sv)" {
|
||||
t.Fatal("wrong property wire signature")
|
||||
}
|
||||
if dbus.SignatureOf([]auxiliary{}).String() != "a(sa(sv))" {
|
||||
t.Fatal("wrong auxiliary wire signature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnrestricted(t *testing.T) {
|
||||
c := testConfig()
|
||||
props, err := c.properties("unrestricted")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := values(props)
|
||||
for _, name := range []string{"CPUQuotaPerSecUSec", "MemoryHigh", "MemoryMax", "MemorySwapMax"} {
|
||||
if got[name].Value() != uint64(math.MaxUint64) {
|
||||
t.Errorf("%s wasn't cleared", name)
|
||||
}
|
||||
}
|
||||
for _, field := range ioFields {
|
||||
if got[field.name].Signature().String() != "a(st)" || len(got[field.name].Value().([]diskLimit)) != 0 {
|
||||
t.Errorf("%s wasn't cleared", field.name)
|
||||
}
|
||||
}
|
||||
assignments, err := c.assignments("unrestricted")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{"CPUQuota=", "MemoryMax=infinity", "IOReadBandwidthMax=", "IOWriteIOPSMax="} {
|
||||
if !strings.Contains(strings.Join(assignments, "\n"), want+"\n") && assignments[len(assignments)-1] != want {
|
||||
t.Errorf("missing reset %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileErrors(t *testing.T) {
|
||||
c := testConfig()
|
||||
if _, err := c.properties("missing"); err == nil {
|
||||
t.Fatal("unknown profile accepted")
|
||||
}
|
||||
if _, err := c.assignments("missing"); err == nil {
|
||||
t.Fatal("unknown profile accepted")
|
||||
}
|
||||
for _, p := range []profile{{"bad%", "6G", "8G", "2G"}, {"400%", "bad", "8G", "2G"}} {
|
||||
c.Profiles["bad"] = p
|
||||
if _, err := c.properties("bad"); err == nil {
|
||||
t.Fatal("invalid size accepted")
|
||||
}
|
||||
}
|
||||
c.Devices[0].Limits["default"] = ioLimit{"bad", "100M", "4000", "4000"}
|
||||
if _, err := c.properties("default"); err == nil {
|
||||
t.Fatal("invalid disk rate accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "profiles.toml")
|
||||
data := `python = "compute"
|
||||
terminals = ["app-terminal-", "app-org.example.Console"]
|
||||
|
||||
[profiles.compute]
|
||||
cpu = "800%"
|
||||
high = "12G"
|
||||
memory = "16G"
|
||||
swap = "2G"
|
||||
|
||||
[[devices]]
|
||||
path = '/dev/mock'
|
||||
[devices.limits.compute]
|
||||
read = "500M"
|
||||
write = "500M"
|
||||
riops = "20000"
|
||||
wiops = "20000"
|
||||
|
||||
[apps]
|
||||
compute = ["game", "org.example.Game"]
|
||||
[units]
|
||||
unrestricted = ["containerd.service"]
|
||||
[prefixes]
|
||||
unrestricted = ["app-terminal-.scope.d"]
|
||||
[commands]
|
||||
java = "compute"
|
||||
[shared]
|
||||
"editor-uri-handler" = "default"
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(data), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := loadConfig(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := testConfig()
|
||||
want.Profiles = map[string]profile{"compute": want.Profiles["compute"]}
|
||||
want.Devices[0].Limits = map[string]ioLimit{"compute": want.Devices[0].Limits["compute"]}
|
||||
want.Apps = map[string][]string{"compute": want.Apps["compute"]}
|
||||
want.Units["unrestricted"] = []string{"containerd.service"}
|
||||
want.Commands = map[string]string{"java": "compute"}
|
||||
want.Terminals = []string{"app-terminal-", "app-org.example.Console"}
|
||||
want.Shared = map[string]string{"editor-uri-handler": "default"}
|
||||
if !reflect.DeepEqual(c, want) {
|
||||
t.Fatalf("got %#v, want %#v", c, want)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte("{"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := loadConfig(path); err == nil {
|
||||
t.Fatal("invalid TOML accepted")
|
||||
}
|
||||
if _, err := loadConfig(path + "-missing"); !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigRoundTrip(t *testing.T) {
|
||||
want := testConfig()
|
||||
var out bytes.Buffer
|
||||
if err := toml.NewEncoder(&out).Encode(want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got config
|
||||
if _, err := toml.Decode(out.String(), &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExample(t *testing.T) {
|
||||
c, err := loadConfig(filepath.Join("..", "..", "profiles.example.toml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := c.render(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name := range c.Profiles {
|
||||
if _, err := c.properties(name); err != nil {
|
||||
t.Fatal(name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue