Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 265 additions & 0 deletions nixos/modules/config/sysfs.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
{
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 ];
}
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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
Expand Down
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,7 @@ in
syncthing-many-devices = handleTest ./syncthing-many-devices.nix { };
syncthing-no-settings = handleTest ./syncthing-no-settings.nix { };
syncthing-relay = handleTest ./syncthing-relay.nix { };
sysfs = runTest ./sysfs.nix;
sysinit-reactivation = runTest ./sysinit-reactivation.nix;
systemd = handleTest ./systemd.nix { };
systemd-analyze = handleTest ./systemd-analyze.nix { };
Expand Down
37 changes: 37 additions & 0 deletions nixos/tests/sysfs.nix
Original file line number Diff line number Diff line change
@@ -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}]')
'';
}
Loading