From 5e71717185a45ec0a7017539feeccb37dd88a61d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C4=8Cerm=C3=A1k?= Date: Fri, 28 Feb 2025 14:12:48 +0100 Subject: [PATCH] Add DBus node for swap size and swappiness config (#222) * Add DBus node for swap size and swappiness config Add /io/hass/os/Config/Swap object that exposes interface with properties controlling the swap size and swappiness configuration added in [1] and [2]. There are no checks whether this supported on the target system (while swappiness would probably have effect also on Supervised installs, swap size can't be controlled there). These checks should be implemented on upper layer (i.e. Supervisor). [1] https://github.com/home-assistant/operating-system/pull/3882 [2] https://github.com/home-assistant/operating-system/pull/3884 * Read default/current swappiness from procfs --- config/swap/swap.go | 179 ++++++++++++++++++++++++++++ config/{ => timesyncd}/timesyncd.go | 2 +- main.go | 6 +- 3 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 config/swap/swap.go rename config/{ => timesyncd}/timesyncd.go (99%) diff --git a/config/swap/swap.go b/config/swap/swap.go new file mode 100644 index 0000000..547e1eb --- /dev/null +++ b/config/swap/swap.go @@ -0,0 +1,179 @@ +package swap + +import ( + "fmt" + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "github.com/godbus/dbus/v5/prop" + "github.com/home-assistant/os-agent/utils/lineinfile" + logging "github.com/home-assistant/os-agent/utils/log" + "os" + "regexp" + "strconv" + "strings" +) + +const ( + objectPath = "/io/hass/os/Config/Swap" + ifaceName = "io.hass.os.Config.Swap" + swapPath = "/etc/default/haos-swapfile" + swappinessPath = "/etc/sysctl.d/15-swappiness.conf" +) + +var ( + optSwapSize string + optSwappiness int + swapFileEditor = lineinfile.LineInFile{FilePath: swapPath} + swappinessEditor = lineinfile.LineInFile{FilePath: swappinessPath} +) + +type swap struct { + conn *dbus.Conn + props *prop.Properties +} + +// Read swappiness from kernel procfs. If it fails, log errors and return 60 +// as it's usual kernel default. +func readKernelSwappiness() int { + content, err := os.ReadFile("/proc/sys/vm/swappiness") + if err != nil { + logging.Error.Printf("Failed to read kernel swappiness: %s", err) + return 60 + } + + swappiness, err := strconv.Atoi(strings.TrimSpace(string(content))) + if err != nil { + logging.Error.Printf("Failed to parse kernel swappiness: %s", err) + return 60 + } + + return swappiness +} + +func getSwapSize() string { + found, err := swapFileEditor.Find(`^SWAPSIZE=`, "", true) + if found == nil || err != nil { + return "" + } + + matches := regexp.MustCompile(`^SWAPSIZE=(.*)`).FindStringSubmatch(*found) + if len(matches) > 1 { + return matches[1] + } + + return "" +} + +func getSwappiness() int { + found, err := swappinessEditor.Find(`^vm.swappiness\s*=`, "", true) + if found == nil || err != nil { + return readKernelSwappiness() + } + + matches := regexp.MustCompile(`^vm.swappiness\s*=\s*(\d+)`).FindStringSubmatch(*found) + if len(matches) > 1 { + if swappiness, err := strconv.Atoi(matches[1]); err == nil { + return swappiness + } + } + + return readKernelSwappiness() +} + +func setSwapSize(c *prop.Change) *dbus.Error { + swapSize, ok := c.Value.(string) + if !ok { + return dbus.MakeFailedError(fmt.Errorf("invalid type for swap size")) + } + + re := regexp.MustCompile(`^\d+([KMG]?(i?B)?)?$`) + if !re.MatchString(swapSize) { + return dbus.MakeFailedError(fmt.Errorf("invalid swap size format")) + } + + params := lineinfile.NewPresentParams(fmt.Sprintf("SWAPSIZE=%s", swapSize)) + params.Regexp, _ = regexp.Compile(`^[#\s]*SWAPSIZE=`) + + if err := swapFileEditor.Present(params); err != nil { + return dbus.MakeFailedError(fmt.Errorf("failed to set swap size: %w", err)) + } + + return nil +} + +func setSwappiness(c *prop.Change) *dbus.Error { + swappiness, ok := c.Value.(int32) + if !ok { + return dbus.MakeFailedError(fmt.Errorf("swappiness must be int32, got %T", c.Value)) + } + + if swappiness < 0 || swappiness > 100 { + return dbus.MakeFailedError(fmt.Errorf("swappiness must be between 0 and 100")) + } + + params := lineinfile.NewPresentParams(fmt.Sprintf("vm.swappiness=%d", swappiness)) + params.Regexp, _ = regexp.Compile(`^[#\s]*vm.swappiness\s*=`) + + if err := swappinessEditor.Present(params); err != nil { + return dbus.MakeFailedError(fmt.Errorf("failed to set swappiness: %w", err)) + } + + return nil +} + +func InitializeDBus(conn *dbus.Conn) { + d := swap{ + conn: conn, + } + + optSwapSize = getSwapSize() + optSwappiness = getSwappiness() + + propsSpec := map[string]map[string]*prop.Prop{ + ifaceName: { + "SwapSize": { + Value: optSwapSize, + Writable: true, + Emit: prop.EmitTrue, + Callback: setSwapSize, + }, + "Swappiness": { + Value: optSwappiness, + Writable: true, + Emit: prop.EmitTrue, + Callback: setSwappiness, + }, + }, + } + + props, err := prop.Export(conn, objectPath, propsSpec) + if err != nil { + logging.Critical.Panic(err) + } + d.props = props + + err = conn.Export(d, objectPath, ifaceName) + if err != nil { + logging.Critical.Panic(err) + } + + node := &introspect.Node{ + Name: objectPath, + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + prop.IntrospectData, + { + Name: ifaceName, + Methods: introspect.Methods(d), + Properties: props.Introspection(ifaceName), + }, + }, + } + + err = conn.Export(introspect.NewIntrospectable(node), objectPath, "org.freedesktop.DBus.Introspectable") + if err != nil { + logging.Critical.Panic(err) + } + + logging.Info.Printf("Exposing object %s with interface %s ...", objectPath, ifaceName) +} diff --git a/config/timesyncd.go b/config/timesyncd/timesyncd.go similarity index 99% rename from config/timesyncd.go rename to config/timesyncd/timesyncd.go index e6319c7..adfa8ff 100644 --- a/config/timesyncd.go +++ b/config/timesyncd/timesyncd.go @@ -1,4 +1,4 @@ -package config +package timesyncd import ( "fmt" diff --git a/main.go b/main.go index c5c0dbb..8302f1b 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,8 @@ import ( "github.com/home-assistant/os-agent/apparmor" "github.com/home-assistant/os-agent/boards" "github.com/home-assistant/os-agent/cgroup" - "github.com/home-assistant/os-agent/config" + "github.com/home-assistant/os-agent/config/swap" + "github.com/home-assistant/os-agent/config/timesyncd" "github.com/home-assistant/os-agent/datadisk" "github.com/home-assistant/os-agent/system" logging "github.com/home-assistant/os-agent/utils/log" @@ -71,7 +72,8 @@ func main() { apparmor.InitializeDBus(conn) cgroup.InitializeDBus(conn) boards.InitializeDBus(conn, board) - config.InitializeDBus(conn) + swap.InitializeDBus(conn) + timesyncd.InitializeDBus(conn) _, err = daemon.SdNotify(false, daemon.SdNotifyReady) if err != nil {