diff --git a/nixos/modules/config/sysfs.nix b/nixos/modules/config/sysfs.nix new file mode 100644 index 000000000000..7be2070684b2 --- /dev/null +++ b/nixos/modules/config/sysfs.nix @@ -0,0 +1,266 @@ +{ + lib, + config, + utils, + pkgs, + ... +}: + +let + inherit (lib) + all + any + concatLines + concatStringsSep + escapeShellArg + flatten + floatToString + foldl' + head + isAttrs + isDerivation + isFloat + isList + length + listToAttrs + match + mapAttrsToList + nameValuePair + removePrefix + tail + throwIf + ; + + inherit (lib.options) + showDefs + showOption + ; + + inherit (lib.strings) + escapeC + isConvertibleWithToString + ; + + inherit (lib.path.subpath) join; + + inherit (utils) escapeSystemdPath; + + cfg = config.boot.kernel.sysfs; + + sysfsAttrs = with lib.types; nullOr (either sysfsValue (attrsOf sysfsAttrs)); + sysfsValue = lib.mkOptionType { + name = "sysfs value"; + description = "sysfs attribute value"; + descriptionClass = "noun"; + check = v: isConvertibleWithToString v; + merge = + loc: defs: + if length defs == 1 then + (head defs).value + else + (foldl' ( + first: def: + # merge definitions if they produce the same value string + throwIf (mkValueString first.value != mkValueString def.value) + "The option \"${showOption loc}\" has conflicting definition values:${ + showDefs [ + first + def + ] + }" + first + ) (head defs) (tail defs)).value; + }; + + mapAttrsToListRecursive = + fn: set: + let + recurse = + p: v: + if isAttrs v && !isDerivation v then mapAttrsToList (n: v: recurse (p ++ [ n ]) v) v else fn p v; + in + flatten (recurse [ ] set); + + mkPath = p: "/sys" + removePrefix "." (join p); + hasGlob = p: any (n: match ''(.*[^\\])?[*?[].*'' n != null) p; + + mkValueString = + v: + # true will be converted to "1" by toString, saving one branch + if v == false then + "0" + else if isFloat v then + floatToString v # warn about loss of precision + else if isList v then + concatStringsSep "," (map mkValueString v) + else + toString v; + + # escape whitespace and linebreaks, as well as the escape character itself, + # to ensure that field boundaries are always preserved + escapeTmpfiles = escapeC [ + "\t" + "\n" + "\r" + " " + "\\" + ]; + + tmpfiles = pkgs.runCommand "nixos-sysfs-tmpfiles.d" { } ( + '' + mkdir "$out" + '' + + concatLines ( + mapAttrsToListRecursive ( + p: v: + let + path = mkPath p; + in + if v == null then + [ ] + else + '' + printf 'w %s - - - - %s\n' \ + ${escapeShellArg (escapeTmpfiles path)} \ + ${escapeShellArg (escapeTmpfiles (mkValueString v))} \ + >"$out"/${escapeShellArg (escapeSystemdPath path)}.conf + '' + ) cfg + ) + ); +in +{ + options = { + boot.kernel.sysfs = lib.mkOption { + type = lib.types.submodule { + freeformType = lib.types.attrsOf sysfsAttrs // { + description = "nested attribute set of null or sysfs attribute values"; + }; + }; + + description = '' + sysfs attributes to be set as soon as they become available. + + Attribute names represent path components in the sysfs filesystem and + cannot be `.` or `..` nor contain any slash character (`/`). + + Names may contain shell‐style glob patterns (`*`, `?` and `[…]`) + matching a single path component, these should however be used with + caution, as they may produce unexpected results if attribute paths + overlap. + + Values will be converted to strings, with list elements concatenated + with commata and booleans converted to numeric values (`0` or `1`). + + `null` values are ignored, allowing removal of values defined in other + modules, as are empty attribute sets. + + List values defined in different modules will _not_ be concatenated. + + This option may only be used for attributes which can be set + idempotently, as the configured values might be written more than once. + ''; + + default = { }; + + example = lib.literalExpression '' + { + # enable transparent hugepages with deferred defragmentaion + kernel.mm.transparent_hugepage = { + enabled = "always"; + defrag = "defer"; + shmem_enabled = "within_size"; + }; + + devices.system.cpu = { + # configure powesave frequency governor for all CPUs + # the [0-9]* glob pattern ensures that other paths + # like cpufreq or cpuidle are not matched + "cpu[0-9]*" = { + scaling_governor = "powersave"; + energy_performance_preference = 8; + }; + + # disable frequency boost + intel_pstate.no_turbo = true; + }; + } + ''; + }; + }; + + config = lib.mkIf (cfg != { }) { + systemd = { + paths = + { + "nixos-sysfs@" = { + description = "/%I attribute watcher"; + pathConfig.PathExistsGlob = "/%I"; + unitConfig.DefaultDependencies = false; + }; + } + // listToAttrs ( + mapAttrsToListRecursive ( + p: v: + if v == null then + [ ] + else + nameValuePair "nixos-sysfs@${escapeSystemdPath (mkPath p)}" { + overrideStrategy = "asDropin"; + wantedBy = [ "sysinit.target" ]; + before = [ "sysinit.target" ]; + } + ) cfg + ); + + services."nixos-sysfs@" = { + description = "/%I attribute setter"; + + unitConfig = { + DefaultDependencies = false; + AssertPathIsMountPoint = "/sys"; + AssertPathExistsGlob = "/%I"; + }; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + + # while we could be tempted to use simple shell script to set the + # sysfs attributes specified by the path or glob pattern, it is + # almost impossible to properly escape a glob pattern so that it + # can be used safely in a shell script + ExecStart = "${lib.getExe' config.systemd.package "systemd-tmpfiles"} --prefix=/sys --create ${tmpfiles}/%i.conf"; + + # hardening may be overkill for such a simple and short‐lived + # service, the following settings would however be suitable to deny + # access to anything but /sys + #ProtectProc = "noaccess"; + #ProcSubset = "pid"; + #ProtectSystem = "strict"; + #PrivateDevices = true; + #SystemCallErrorNumber = "EPERM"; + #SystemCallFilter = [ + # "@basic-io" + # "@file-system" + #]; + }; + }; + }; + + warnings = mapAttrsToListRecursive ( + p: v: + if hasGlob p then + "Attribute path \"${concatStringsSep "." p}\" contains glob patterns. Please ensure that it does not overlap with other paths." + else + [ ] + ) cfg; + + assertions = mapAttrsToListRecursive (p: v: { + assertion = all (n: match ''(\.\.?|.*/.*)'' n == null) p; + message = "Attribute path \"${concatStringsSep "." p}\" has invalid components."; + }) cfg; + }; + + meta.maintainers = with lib.maintainers; [ mvs ]; +} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 7f0ac3970cd1..22205eee8396 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -31,6 +31,7 @@ ./config/stub-ld.nix ./config/swap.nix ./config/sysctl.nix + ./config/sysfs.nix ./config/system-environment.nix ./config/system-path.nix ./config/terminfo.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index bbc66e837e56..fd52943f093e 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1394,6 +1394,7 @@ in syncthing-folders = runTest ./syncthing-folders.nix; syncthing-relay = runTest ./syncthing-relay.nix; sysinit-reactivation = runTest ./sysinit-reactivation.nix; + sysfs = runTest ./sysfs.nix; systemd = runTest ./systemd.nix; systemd-analyze = runTest ./systemd-analyze.nix; systemd-binfmt = handleTestOn [ "x86_64-linux" ] ./systemd-binfmt.nix { }; diff --git a/nixos/tests/sysfs.nix b/nixos/tests/sysfs.nix new file mode 100644 index 000000000000..c8d0bdb884f8 --- /dev/null +++ b/nixos/tests/sysfs.nix @@ -0,0 +1,37 @@ +{ lib, ... }: + +{ + name = "sysfs"; + meta.maintainers = with lib.maintainers; [ mvs ]; + + nodes.machine = { + boot.kernel.sysfs = { + kernel.mm.transparent_hugepage = { + enabled = "always"; + defrag = "defer"; + shmem_enabled = "within_size"; + }; + + block."*".queue.scheduler = "none"; + }; + }; + + testScript = + { nodes, ... }: + let + inherit (nodes.machine.boot.kernel) sysfs; + in + '' + from shlex import quote + + def check(filename, contents): + machine.succeed('grep -F -q {} {}'.format(quote(contents), quote(filename))) + + check('/sys/kernel/mm/transparent_hugepage/enabled', + '[${sysfs.kernel.mm.transparent_hugepage.enabled}]') + check('/sys/kernel/mm/transparent_hugepage/defrag', + '[${sysfs.kernel.mm.transparent_hugepage.defrag}]') + check('/sys/kernel/mm/transparent_hugepage/shmem_enabled', + '[${sysfs.kernel.mm.transparent_hugepage.shmem_enabled}]') + ''; +}