nixos/config/sysfs: init module

This commit is contained in:
Mikael Voss 2025-03-19 18:13:03 +01:00 committed by Masum Reza
parent b6e09db847
commit ba04f97d4e
4 changed files with 305 additions and 0 deletions

View File

@ -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 shellstyle 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 shortlived
# 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 ];
}

View File

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

View File

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

37
nixos/tests/sysfs.nix Normal file
View File

@ -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}]')
'';
}