systemd: add a name option to all systemd units

This allows us to set things like dependencies in a way that we can
catch typos at eval time.
So instead of
```nix
systemd.services.foo.wants = [ "bar.service" ];
```
we can write
```nix
systemd.services.foo.wants = [ config.systemd.services.bar.name ];
```
which will throw an error if no such service has been defined.

Not all cases can be done like this (eg template services), but in a lot
of cases this will allow to avoid typos.

There is a matching option on the unit option
(`systemd.units."foo.service".name`) as well.
This commit is contained in:
r-vdp 2024-03-21 14:52:12 +01:00
parent b432281d97
commit 9258f57625
No known key found for this signature in database
8 changed files with 124 additions and 63 deletions

View File

@ -1,4 +1,4 @@
{ config, lib, pkgs }: { config, lib, pkgs, utils }:
let let
inherit (lib) inherit (lib)
@ -381,8 +381,41 @@ in rec {
}; };
}; };
serviceConfig = { config, ... }: { serviceConfig = { name, config, ... }: {
config.environment.PATH = mkIf (config.path != []) "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}"; config = {
name = "${name}.service";
environment.PATH = mkIf (config.path != []) "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}";
};
};
pathConfig = { name, config, ... }: {
config = {
name = "${name}.path";
};
};
socketConfig = { name, config, ... }: {
config = {
name = "${name}.socket";
};
};
sliceConfig = { name, config, ... }: {
config = {
name = "${name}.slice";
};
};
targetConfig = { name, config, ... }: {
config = {
name = "${name}.target";
};
};
timerConfig = { name, config, ... }: {
config = {
name = "${name}.timer";
};
}; };
stage2ServiceConfig = { stage2ServiceConfig = {
@ -401,6 +434,7 @@ in rec {
mountConfig = { config, ... }: { mountConfig = { config, ... }: {
config = { config = {
name = "${utils.escapeSystemdPath config.where}.mount";
mountConfig = mountConfig =
{ What = config.what; { What = config.what;
Where = config.where; Where = config.where;
@ -414,6 +448,7 @@ in rec {
automountConfig = { config, ... }: { automountConfig = { config, ... }: {
config = { config = {
name = "${utils.escapeSystemdPath config.where}.automount";
automountConfig = automountConfig =
{ Where = config.where; { Where = config.where;
}; };
@ -429,8 +464,8 @@ in rec {
WantedBy=${concatStringsSep " " def.wantedBy} WantedBy=${concatStringsSep " " def.wantedBy}
''; '';
targetToUnit = name: def: targetToUnit = def:
{ inherit (def) aliases wantedBy requiredBy upheldBy enable overrideStrategy; { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
text = text =
'' ''
[Unit] [Unit]
@ -438,8 +473,8 @@ in rec {
''; '';
}; };
serviceToUnit = name: def: serviceToUnit = def:
{ inherit (def) aliases wantedBy requiredBy upheldBy enable overrideStrategy; { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
text = commonUnitText def ('' text = commonUnitText def (''
[Service] [Service]
'' + (let env = cfg.globalEnvironment // def.environment; '' + (let env = cfg.globalEnvironment // def.environment;
@ -448,7 +483,7 @@ in rec {
"Environment=${toJSON "${n}=${env.${n}}"}\n"; "Environment=${toJSON "${n}=${env.${n}}"}\n";
# systemd max line length is now 1MiB # systemd max line length is now 1MiB
# https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af # https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af
in if stringLength s >= 1048576 then throw "The value of the environment variable ${n} in systemd service ${name}.service is too long." else s) (attrNames env)) in if stringLength s >= 1048576 then throw "The value of the environment variable ${n} in systemd service ${def.name}.service is too long." else s) (attrNames env))
+ (if def ? reloadIfChanged && def.reloadIfChanged then '' + (if def ? reloadIfChanged && def.reloadIfChanged then ''
X-ReloadIfChanged=true X-ReloadIfChanged=true
'' else if (def ? restartIfChanged && !def.restartIfChanged) then '' '' else if (def ? restartIfChanged && !def.restartIfChanged) then ''
@ -459,8 +494,8 @@ in rec {
'' + attrsToSection def.serviceConfig); '' + attrsToSection def.serviceConfig);
}; };
socketToUnit = name: def: socketToUnit = def:
{ inherit (def) aliases wantedBy requiredBy upheldBy enable overrideStrategy; { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
text = commonUnitText def '' text = commonUnitText def ''
[Socket] [Socket]
${attrsToSection def.socketConfig} ${attrsToSection def.socketConfig}
@ -469,40 +504,40 @@ in rec {
''; '';
}; };
timerToUnit = name: def: timerToUnit = def:
{ inherit (def) aliases wantedBy requiredBy upheldBy enable overrideStrategy; { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
text = commonUnitText def '' text = commonUnitText def ''
[Timer] [Timer]
${attrsToSection def.timerConfig} ${attrsToSection def.timerConfig}
''; '';
}; };
pathToUnit = name: def: pathToUnit = def:
{ inherit (def) aliases wantedBy requiredBy upheldBy enable overrideStrategy; { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
text = commonUnitText def '' text = commonUnitText def ''
[Path] [Path]
${attrsToSection def.pathConfig} ${attrsToSection def.pathConfig}
''; '';
}; };
mountToUnit = name: def: mountToUnit = def:
{ inherit (def) aliases wantedBy requiredBy upheldBy enable overrideStrategy; { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
text = commonUnitText def '' text = commonUnitText def ''
[Mount] [Mount]
${attrsToSection def.mountConfig} ${attrsToSection def.mountConfig}
''; '';
}; };
automountToUnit = name: def: automountToUnit = def:
{ inherit (def) aliases wantedBy requiredBy upheldBy enable overrideStrategy; { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
text = commonUnitText def '' text = commonUnitText def ''
[Automount] [Automount]
${attrsToSection def.automountConfig} ${attrsToSection def.automountConfig}
''; '';
}; };
sliceToUnit = name: def: sliceToUnit = def:
{ inherit (def) aliases wantedBy requiredBy upheldBy enable overrideStrategy; { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
text = commonUnitText def '' text = commonUnitText def ''
[Slice] [Slice]
${attrsToSection def.sliceConfig} ${attrsToSection def.sliceConfig}

View File

@ -5,8 +5,13 @@ let
automountConfig automountConfig
makeUnit makeUnit
mountConfig mountConfig
pathConfig
sliceConfig
socketConfig
stage1ServiceConfig stage1ServiceConfig
stage2ServiceConfig stage2ServiceConfig
targetConfig
timerConfig
unitConfig unitConfig
; ;
@ -48,29 +53,32 @@ let
; ;
in in
rec { {
units = attrsOf (submodule ({ name, config, ... }: { units = attrsOf (submodule ({ name, config, ... }: {
options = concreteUnitOptions; options = concreteUnitOptions;
config = { unit = mkDefault (makeUnit name config); }; config = {
name = mkDefault name;
unit = mkDefault (makeUnit name config);
};
})); }));
services = attrsOf (submodule [ stage2ServiceOptions unitConfig stage2ServiceConfig ]); services = attrsOf (submodule [ stage2ServiceOptions unitConfig stage2ServiceConfig ]);
initrdServices = attrsOf (submodule [ stage1ServiceOptions unitConfig stage1ServiceConfig ]); initrdServices = attrsOf (submodule [ stage1ServiceOptions unitConfig stage1ServiceConfig ]);
targets = attrsOf (submodule [ stage2CommonUnitOptions unitConfig ]); targets = attrsOf (submodule [ stage2CommonUnitOptions unitConfig targetConfig ]);
initrdTargets = attrsOf (submodule [ stage1CommonUnitOptions unitConfig ]); initrdTargets = attrsOf (submodule [ stage1CommonUnitOptions unitConfig targetConfig ]);
sockets = attrsOf (submodule [ stage2SocketOptions unitConfig ]); sockets = attrsOf (submodule [ stage2SocketOptions unitConfig socketConfig]);
initrdSockets = attrsOf (submodule [ stage1SocketOptions unitConfig ]); initrdSockets = attrsOf (submodule [ stage1SocketOptions unitConfig socketConfig ]);
timers = attrsOf (submodule [ stage2TimerOptions unitConfig ]); timers = attrsOf (submodule [ stage2TimerOptions unitConfig timerConfig ]);
initrdTimers = attrsOf (submodule [ stage1TimerOptions unitConfig ]); initrdTimers = attrsOf (submodule [ stage1TimerOptions unitConfig timerConfig ]);
paths = attrsOf (submodule [ stage2PathOptions unitConfig ]); paths = attrsOf (submodule [ stage2PathOptions unitConfig pathConfig ]);
initrdPaths = attrsOf (submodule [ stage1PathOptions unitConfig ]); initrdPaths = attrsOf (submodule [ stage1PathOptions unitConfig pathConfig ]);
slices = attrsOf (submodule [ stage2SliceOptions unitConfig ]); slices = attrsOf (submodule [ stage2SliceOptions unitConfig sliceConfig ]);
initrdSlices = attrsOf (submodule [ stage1SliceOptions unitConfig ]); initrdSlices = attrsOf (submodule [ stage1SliceOptions unitConfig sliceConfig ]);
mounts = listOf (submodule [ stage2MountOptions unitConfig mountConfig ]); mounts = listOf (submodule [ stage2MountOptions unitConfig mountConfig ]);
initrdMounts = listOf (submodule [ stage1MountOptions unitConfig mountConfig ]); initrdMounts = listOf (submodule [ stage1MountOptions unitConfig mountConfig ]);

View File

@ -65,6 +65,14 @@ in rec {
''; '';
}; };
name = lib.mkOption {
type = lib.types.str;
description = ''
The name of this systemd unit, including its extension.
This can be used to refer to this unit from other systemd units.
'';
};
overrideStrategy = mkOption { overrideStrategy = mkOption {
default = "asDropinIfExists"; default = "asDropinIfExists";
type = types.enum [ "asDropinIfExists" "asDropin" ]; type = types.enum [ "asDropinIfExists" "asDropin" ];

View File

@ -35,7 +35,8 @@ let
inherit (lib.strings) toJSON normalizePath escapeC; inherit (lib.strings) toJSON normalizePath escapeC;
in in
rec { let
utils = rec {
# Copy configuration files to avoid having the entire sources in the system closure # Copy configuration files to avoid having the entire sources in the system closure
copyFile = filePath: pkgs.runCommand (builtins.unsafeDiscardStringContext (baseNameOf filePath)) {} '' copyFile = filePath: pkgs.runCommand (builtins.unsafeDiscardStringContext (baseNameOf filePath)) {} ''
@ -262,11 +263,12 @@ rec {
filter (x: !(elem (getName x) namesToRemove)) packages; filter (x: !(elem (getName x) namesToRemove)) packages;
systemdUtils = { systemdUtils = {
lib = import ./systemd-lib.nix { inherit lib config pkgs; }; lib = import ./systemd-lib.nix { inherit lib config pkgs utils; };
unitOptions = import ./systemd-unit-options.nix { inherit lib systemdUtils; }; unitOptions = import ./systemd-unit-options.nix { inherit lib systemdUtils; };
types = import ./systemd-types.nix { inherit lib systemdUtils pkgs; }; types = import ./systemd-types.nix { inherit lib systemdUtils pkgs; };
network = { network = {
units = import ./systemd-network-units.nix { inherit lib systemdUtils; }; units = import ./systemd-network-units.nix { inherit lib systemdUtils; };
}; };
}; };
} };
in utils

View File

@ -595,18 +595,17 @@ in
}; };
systemd.units = systemd.units =
mapAttrs' (n: v: nameValuePair "${n}.path" (pathToUnit n v)) cfg.paths let
// mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services withName = cfgToUnit: cfg: lib.nameValuePair cfg.name (cfgToUnit cfg);
// mapAttrs' (n: v: nameValuePair "${n}.slice" (sliceToUnit n v)) cfg.slices in
// mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit n v)) cfg.sockets mapAttrs' (_: withName pathToUnit) cfg.paths
// mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit n v)) cfg.targets // mapAttrs' (_: withName serviceToUnit) cfg.services
// mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit n v)) cfg.timers // mapAttrs' (_: withName sliceToUnit) cfg.slices
// listToAttrs (map // mapAttrs' (_: withName socketToUnit) cfg.sockets
(v: let n = escapeSystemdPath v.where; // mapAttrs' (_: withName targetToUnit) cfg.targets
in nameValuePair "${n}.mount" (mountToUnit n v)) cfg.mounts) // mapAttrs' (_: withName timerToUnit) cfg.timers
// listToAttrs (map // listToAttrs (map (withName mountToUnit) cfg.mounts)
(v: let n = escapeSystemdPath v.where; // listToAttrs (map (withName automountToUnit) cfg.automounts);
in nameValuePair "${n}.automount" (automountToUnit n v)) cfg.automounts);
# Environment of PID 1 # Environment of PID 1
systemd.managerEnvironment = { systemd.managerEnvironment = {

View File

@ -490,18 +490,18 @@ in {
targets.initrd.aliases = ["default.target"]; targets.initrd.aliases = ["default.target"];
units = units =
mapAttrs' (n: v: nameValuePair "${n}.path" (pathToUnit n v)) cfg.paths mapAttrs' (n: v: nameValuePair "${n}.path" (pathToUnit v)) cfg.paths
// mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit v)) cfg.services
// mapAttrs' (n: v: nameValuePair "${n}.slice" (sliceToUnit n v)) cfg.slices // mapAttrs' (n: v: nameValuePair "${n}.slice" (sliceToUnit v)) cfg.slices
// mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit n v)) cfg.sockets // mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit v)) cfg.sockets
// mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit n v)) cfg.targets // mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit v)) cfg.targets
// mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit n v)) cfg.timers // mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit v)) cfg.timers
// listToAttrs (map // listToAttrs (map
(v: let n = escapeSystemdPath v.where; (v: let n = escapeSystemdPath v.where;
in nameValuePair "${n}.mount" (mountToUnit n v)) cfg.mounts) in nameValuePair "${n}.mount" (mountToUnit v)) cfg.mounts)
// listToAttrs (map // listToAttrs (map
(v: let n = escapeSystemdPath v.where; (v: let n = escapeSystemdPath v.where;
in nameValuePair "${n}.automount" (automountToUnit n v)) cfg.automounts); in nameValuePair "${n}.automount" (automountToUnit v)) cfg.automounts);
# make sure all the /dev nodes are set up # make sure all the /dev nodes are set up
services.systemd-tmpfiles-setup-dev.wantedBy = ["sysinit.target"]; services.systemd-tmpfiles-setup-dev.wantedBy = ["sysinit.target"];

View File

@ -175,12 +175,12 @@ in {
}; };
systemd.user.units = systemd.user.units =
mapAttrs' (n: v: nameValuePair "${n}.path" (pathToUnit n v)) cfg.paths mapAttrs' (n: v: nameValuePair "${n}.path" (pathToUnit v)) cfg.paths
// mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit v)) cfg.services
// mapAttrs' (n: v: nameValuePair "${n}.slice" (sliceToUnit n v)) cfg.slices // mapAttrs' (n: v: nameValuePair "${n}.slice" (sliceToUnit v)) cfg.slices
// mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit n v)) cfg.sockets // mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit v)) cfg.sockets
// mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit n v)) cfg.targets // mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit v)) cfg.targets
// mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit n v)) cfg.timers; // mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit v)) cfg.timers;
# Generate timer units for all services that have a startAt value. # Generate timer units for all services that have a startAt value.
systemd.user.timers = systemd.user.timers =

View File

@ -1,7 +1,7 @@
import ./make-test-python.nix ({ pkgs, ... }: { import ./make-test-python.nix ({ pkgs, ... }: {
name = "systemd"; name = "systemd";
nodes.machine = { lib, ... }: { nodes.machine = { config, lib, ... }: {
imports = [ common/user-account.nix common/x11.nix ]; imports = [ common/user-account.nix common/x11.nix ];
virtualisation.emptyDiskImages = [ 512 512 ]; virtualisation.emptyDiskImages = [ 512 512 ];
@ -38,9 +38,18 @@ import ./make-test-python.nix ({ pkgs, ... }: {
script = "true"; script = "true";
}; };
systemd.services.testDependency1 = {
description = "Test Dependency 1";
wantedBy = [ config.systemd.services."testservice1".name ];
serviceConfig.Type = "oneshot";
script = ''
true
'';
};
systemd.services.testservice1 = { systemd.services.testservice1 = {
description = "Test Service 1"; description = "Test Service 1";
wantedBy = [ "multi-user.target" ]; wantedBy = [ config.systemd.targets.multi-user.name ];
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
script = '' script = ''
if [ "$XXX_SYSTEM" = foo ]; then if [ "$XXX_SYSTEM" = foo ]; then