168 lines
4 KiB
Go
168 lines
4 KiB
Go
|
|
package budget
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"maps"
|
||
|
|
"math"
|
||
|
|
"slices"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/BurntSushi/toml"
|
||
|
|
"github.com/godbus/dbus/v5"
|
||
|
|
)
|
||
|
|
|
||
|
|
type profile struct {
|
||
|
|
CPU string `toml:"cpu"`
|
||
|
|
High string `toml:"high"`
|
||
|
|
Memory string `toml:"memory"`
|
||
|
|
Swap string `toml:"swap"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type ioLimit struct {
|
||
|
|
Read string `toml:"read"`
|
||
|
|
Write string `toml:"write"`
|
||
|
|
RIOPS string `toml:"riops"`
|
||
|
|
WIOPS string `toml:"wiops"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type device struct {
|
||
|
|
Path string `toml:"path"`
|
||
|
|
Limits map[string]ioLimit `toml:"limits"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type config struct {
|
||
|
|
Profiles map[string]profile `toml:"profiles"`
|
||
|
|
Devices []device `toml:"devices"`
|
||
|
|
Apps map[string][]string `toml:"apps"`
|
||
|
|
Units map[string][]string `toml:"units"`
|
||
|
|
Prefixes map[string][]string `toml:"prefixes"`
|
||
|
|
Commands map[string]string `toml:"commands"`
|
||
|
|
Python string `toml:"python"`
|
||
|
|
Terminals []string `toml:"terminals"`
|
||
|
|
Shared map[string]string `toml:"shared"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type property struct {
|
||
|
|
Name string
|
||
|
|
Value dbus.Variant
|
||
|
|
}
|
||
|
|
|
||
|
|
type diskLimit struct {
|
||
|
|
Path string
|
||
|
|
Limit uint64
|
||
|
|
}
|
||
|
|
|
||
|
|
type ioField struct {
|
||
|
|
name string
|
||
|
|
get func(ioLimit) string
|
||
|
|
}
|
||
|
|
|
||
|
|
var ioFields = []ioField{
|
||
|
|
{"IOReadBandwidthMax", func(l ioLimit) string { return l.Read }},
|
||
|
|
{"IOWriteBandwidthMax", func(l ioLimit) string { return l.Write }},
|
||
|
|
{"IOReadIOPSMax", func(l ioLimit) string { return l.RIOPS }},
|
||
|
|
{"IOWriteIOPSMax", func(l ioLimit) string { return l.WIOPS }},
|
||
|
|
}
|
||
|
|
|
||
|
|
func keys[V any](m map[string]V) []string {
|
||
|
|
return slices.Sorted(maps.Keys(m))
|
||
|
|
}
|
||
|
|
|
||
|
|
func loadConfig(path string) (config, error) {
|
||
|
|
var c config
|
||
|
|
_, err := toml.DecodeFile(path, &c)
|
||
|
|
return c, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func parseSize(value string, base uint64) (uint64, error) {
|
||
|
|
if value == "infinity" {
|
||
|
|
return math.MaxUint64, nil
|
||
|
|
}
|
||
|
|
factor := uint64(1)
|
||
|
|
for i, suffix := range []string{"K", "M", "G"} {
|
||
|
|
if strings.HasSuffix(value, suffix) {
|
||
|
|
value = strings.TrimSuffix(value, suffix)
|
||
|
|
for range i + 1 {
|
||
|
|
factor *= base
|
||
|
|
}
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
number, err := strconv.ParseUint(value, 10, 64)
|
||
|
|
return number * factor, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func prop(name string, value any) property {
|
||
|
|
return property{Name: name, Value: dbus.MakeVariant(value)}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c config) getProfile(name string) (profile, error) {
|
||
|
|
p, ok := c.Profiles[name]
|
||
|
|
if !ok {
|
||
|
|
return profile{}, fmt.Errorf("unknown profile %q", name)
|
||
|
|
}
|
||
|
|
return p, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c config) properties(name string) ([]property, error) {
|
||
|
|
p, err := c.getProfile(name)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
quota := uint64(math.MaxUint64)
|
||
|
|
if p.CPU != "" {
|
||
|
|
quota, err = strconv.ParseUint(strings.TrimSuffix(p.CPU, "%"), 10, 64)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("CPUQuota: %w", err)
|
||
|
|
}
|
||
|
|
quota *= 10000
|
||
|
|
}
|
||
|
|
result := []property{prop("CPUQuotaPerSecUSec", quota)}
|
||
|
|
for _, field := range []struct{ name, value string }{
|
||
|
|
{"MemoryHigh", p.High}, {"MemoryMax", p.Memory}, {"MemorySwapMax", p.Swap},
|
||
|
|
} {
|
||
|
|
value, err := parseSize(field.value, 1024)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("%s: %w", field.name, err)
|
||
|
|
}
|
||
|
|
result = append(result, prop(field.name, value))
|
||
|
|
}
|
||
|
|
for _, field := range ioFields {
|
||
|
|
limits := make([]diskLimit, 0, len(c.Devices))
|
||
|
|
for _, d := range c.Devices {
|
||
|
|
limit, ok := d.Limits[name]
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
value, err := parseSize(field.get(limit), 1000)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("%s: %w", field.name, err)
|
||
|
|
}
|
||
|
|
limits = append(limits, diskLimit{Path: d.Path, Limit: value})
|
||
|
|
}
|
||
|
|
result = append(result, prop(field.name, limits))
|
||
|
|
}
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c config) assignments(name string) ([]string, error) {
|
||
|
|
p, err := c.getProfile(name)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
result := []string{"CPUQuota=" + p.CPU, "MemoryHigh=" + p.High, "MemoryMax=" + p.Memory, "MemorySwapMax=" + p.Swap}
|
||
|
|
for _, field := range ioFields {
|
||
|
|
found := false
|
||
|
|
for _, d := range c.Devices {
|
||
|
|
if limit, ok := d.Limits[name]; ok {
|
||
|
|
result = append(result, field.name+"="+d.Path+" "+field.get(limit))
|
||
|
|
found = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !found {
|
||
|
|
result = append(result, field.name+"=")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return result, nil
|
||
|
|
}
|