nixos/preload: init
[NixPkgs.git] / nixos / lib / systemd-lib.nix
blob820ccbcbf72a1785bd5725c4aa87d60709f739e3
1 { config, lib, pkgs }:
3 with lib;
5 let
6   cfg = config.systemd;
7   lndir = "${pkgs.buildPackages.xorg.lndir}/bin/lndir";
8   systemd = cfg.package;
9 in rec {
11   shellEscape = s: (replaceStrings [ "\\" ] [ "\\\\" ] s);
13   mkPathSafeName = lib.replaceStrings ["@" ":" "\\" "[" "]"] ["-" "-" "-" "" ""];
15   # a type for options that take a unit name
16   unitNameType = types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)";
18   makeUnit = name: unit:
19     if unit.enable then
20       pkgs.runCommand "unit-${mkPathSafeName name}"
21         { preferLocalBuild = true;
22           allowSubstitutes = false;
23           # unit.text can be null. But variables that are null listed in
24           # passAsFile are ignored by nix, resulting in no file being created,
25           # making the mv operation fail.
26           text = optionalString (unit.text != null) unit.text;
27           passAsFile = [ "text" ];
28         }
29         ''
30           name=${shellEscape name}
31           mkdir -p "$out/$(dirname -- "$name")"
32           mv "$textPath" "$out/$name"
33         ''
34     else
35       pkgs.runCommand "unit-${mkPathSafeName name}-disabled"
36         { preferLocalBuild = true;
37           allowSubstitutes = false;
38         }
39         ''
40           name=${shellEscape name}
41           mkdir -p "$out/$(dirname "$name")"
42           ln -s /dev/null "$out/$name"
43         '';
45   boolValues = [true false "yes" "no"];
47   digits = map toString (range 0 9);
49   isByteFormat = s:
50     let
51       l = reverseList (stringToCharacters s);
52       suffix = head l;
53       nums = tail l;
54     in elem suffix (["K" "M" "G" "T"] ++ digits)
55       && all (num: elem num digits) nums;
57   assertByteFormat = name: group: attr:
58     optional (attr ? ${name} && ! isByteFormat attr.${name})
59       "Systemd ${group} field `${name}' must be in byte format [0-9]+[KMGT].";
61   hexChars = stringToCharacters "0123456789abcdefABCDEF";
63   isMacAddress = s: stringLength s == 17
64     && flip all (splitString ":" s) (bytes:
65       all (byte: elem byte hexChars) (stringToCharacters bytes)
66     );
68   assertMacAddress = name: group: attr:
69     optional (attr ? ${name} && ! isMacAddress attr.${name})
70       "Systemd ${group} field `${name}' must be a valid MAC address.";
72   assertNetdevMacAddress = name: group: attr:
73     optional (attr ? ${name} && (! isMacAddress attr.${name} && attr.${name} != "none"))
74       "Systemd ${group} field `${name}` must be a valid MAC address or the special value `none`.";
77   isPort = i: i >= 0 && i <= 65535;
79   assertPort = name: group: attr:
80     optional (attr ? ${name} && ! isPort attr.${name})
81       "Error on the systemd ${group} field `${name}': ${attr.name} is not a valid port number.";
83   assertValueOneOf = name: values: group: attr:
84     optional (attr ? ${name} && !elem attr.${name} values)
85       "Systemd ${group} field `${name}' cannot have value `${toString attr.${name}}'.";
87   assertValuesSomeOfOr = name: values: default: group: attr:
88     optional (attr ? ${name} && !(all (x: elem x values) (splitString " " attr.${name}) || attr.${name} == default))
89       "Systemd ${group} field `${name}' cannot have value `${toString attr.${name}}'.";
91   assertHasField = name: group: attr:
92     optional (!(attr ? ${name}))
93       "Systemd ${group} field `${name}' must exist.";
95   assertRange = name: min: max: group: attr:
96     optional (attr ? ${name} && !(min <= attr.${name} && max >= attr.${name}))
97       "Systemd ${group} field `${name}' is outside the range [${toString min},${toString max}]";
99   assertMinimum = name: min: group: attr:
100     optional (attr ? ${name} && attr.${name} < min)
101       "Systemd ${group} field `${name}' must be greater than or equal to ${toString min}";
103   assertOnlyFields = fields: group: attr:
104     let badFields = filter (name: ! elem name fields) (attrNames attr); in
105     optional (badFields != [ ])
106       "Systemd ${group} has extra fields [${concatStringsSep " " badFields}].";
108   assertInt = name: group: attr:
109     optional (attr ? ${name} && !isInt attr.${name})
110       "Systemd ${group} field `${name}' is not an integer";
112   checkUnitConfig = group: checks: attrs: let
113     # We're applied at the top-level type (attrsOf unitOption), so the actual
114     # unit options might contain attributes from mkOverride and mkIf that we need to
115     # convert into single values before checking them.
116     defs = mapAttrs (const (v:
117       if v._type or "" == "override" then v.content
118       else if v._type or "" == "if" then v.content
119       else v
120     )) attrs;
121     errors = concatMap (c: c group defs) checks;
122   in if errors == [] then true
123      else builtins.trace (concatStringsSep "\n" errors) false;
125   toOption = x:
126     if x == true then "true"
127     else if x == false then "false"
128     else toString x;
130   attrsToSection = as:
131     concatStrings (concatLists (mapAttrsToList (name: value:
132       map (x: ''
133           ${name}=${toOption x}
134         '')
135         (if isList value then value else [value]))
136         as));
138   generateUnits = { allowCollisions ? true, type, units, upstreamUnits, upstreamWants, packages ? cfg.packages, package ? cfg.package }:
139     let
140       typeDir = ({
141         system = "system";
142         initrd = "system";
143         user = "user";
144         nspawn = "nspawn";
145       }).${type};
146     in pkgs.runCommand "${type}-units"
147       { preferLocalBuild = true;
148         allowSubstitutes = false;
149       } ''
150       mkdir -p $out
152       # Copy the upstream systemd units we're interested in.
153       for i in ${toString upstreamUnits}; do
154         fn=${package}/example/systemd/${typeDir}/$i
155         if ! [ -e $fn ]; then echo "missing $fn"; false; fi
156         if [ -L $fn ]; then
157           target="$(readlink "$fn")"
158           if [ ''${target:0:3} = ../ ]; then
159             ln -s "$(readlink -f "$fn")" $out/
160           else
161             cp -pd $fn $out/
162           fi
163         else
164           ln -s $fn $out/
165         fi
166       done
168       # Copy .wants links, but only those that point to units that
169       # we're interested in.
170       for i in ${toString upstreamWants}; do
171         fn=${package}/example/systemd/${typeDir}/$i
172         if ! [ -e $fn ]; then echo "missing $fn"; false; fi
173         x=$out/$(basename $fn)
174         mkdir $x
175         for i in $fn/*; do
176           y=$x/$(basename $i)
177           cp -pd $i $y
178           if ! [ -e $y ]; then rm $y; fi
179         done
180       done
182       # Symlink all units provided listed in systemd.packages.
183       packages="${toString packages}"
185       # Filter duplicate directories
186       declare -A unique_packages
187       for k in $packages ; do unique_packages[$k]=1 ; done
189       for i in ''${!unique_packages[@]}; do
190         for fn in $i/etc/systemd/${typeDir}/* $i/lib/systemd/${typeDir}/*; do
191           if ! [[ "$fn" =~ .wants$ ]]; then
192             if [[ -d "$fn" ]]; then
193               targetDir="$out/$(basename "$fn")"
194               mkdir -p "$targetDir"
195               ${lndir} "$fn" "$targetDir"
196             else
197               ln -s $fn $out/
198             fi
199           fi
200         done
201       done
203       # Symlink units defined by systemd.units where override strategy
204       # shall be automatically detected. If these are also provided by
205       # systemd or systemd.packages, then add them as
206       # <unit-name>.d/overrides.conf, which makes them extend the
207       # upstream unit.
208       for i in ${toString (mapAttrsToList
209           (n: v: v.unit)
210           (lib.filterAttrs (n: v: (attrByPath [ "overrideStrategy" ] "asDropinIfExists" v) == "asDropinIfExists") units))}; do
211         fn=$(basename $i/*)
212         if [ -e $out/$fn ]; then
213           if [ "$(readlink -f $i/$fn)" = /dev/null ]; then
214             ln -sfn /dev/null $out/$fn
215           else
216             ${if allowCollisions then ''
217               mkdir -p $out/$fn.d
218               ln -s $i/$fn $out/$fn.d/overrides.conf
219             '' else ''
220               echo "Found multiple derivations configuring $fn!"
221               exit 1
222             ''}
223           fi
224        else
225           ln -fs $i/$fn $out/
226         fi
227       done
229       # Symlink units defined by systemd.units which shall be
230       # treated as drop-in file.
231       for i in ${toString (mapAttrsToList
232           (n: v: v.unit)
233           (lib.filterAttrs (n: v: v ? overrideStrategy && v.overrideStrategy == "asDropin") units))}; do
234         fn=$(basename $i/*)
235         mkdir -p $out/$fn.d
236         ln -s $i/$fn $out/$fn.d/overrides.conf
237       done
239       # Create service aliases from aliases option.
240       ${concatStrings (mapAttrsToList (name: unit:
241           concatMapStrings (name2: ''
242             ln -sfn '${name}' $out/'${name2}'
243           '') (unit.aliases or [])) units)}
245       # Create .wants and .requires symlinks from the wantedBy and
246       # requiredBy options.
247       ${concatStrings (mapAttrsToList (name: unit:
248           concatMapStrings (name2: ''
249             mkdir -p $out/'${name2}.wants'
250             ln -sfn '../${name}' $out/'${name2}.wants'/
251           '') (unit.wantedBy or [])) units)}
253       ${concatStrings (mapAttrsToList (name: unit:
254           concatMapStrings (name2: ''
255             mkdir -p $out/'${name2}.requires'
256             ln -sfn '../${name}' $out/'${name2}.requires'/
257           '') (unit.requiredBy or [])) units)}
259       ${optionalString (type == "system") ''
260         # Stupid misc. symlinks.
261         ln -s ${cfg.defaultUnit} $out/default.target
262         ln -s ${cfg.ctrlAltDelUnit} $out/ctrl-alt-del.target
263         ln -s rescue.target $out/kbrequest.target
265         mkdir -p $out/getty.target.wants/
266         ln -s ../autovt@tty1.service $out/getty.target.wants/
268         ln -s ../remote-fs.target $out/multi-user.target.wants/
269       ''}
270     ''; # */
272   makeJobScript = name: text:
273     let
274       scriptName = replaceStrings [ "\\" "@" ] [ "-" "_" ] (shellEscape name);
275       out = (pkgs.writeShellScriptBin scriptName ''
276         set -e
277         ${text}
278       '').overrideAttrs (_: {
279         # The derivation name is different from the script file name
280         # to keep the script file name short to avoid cluttering logs.
281         name = "unit-script-${scriptName}";
282       });
283     in "${out}/bin/${scriptName}";
285   unitConfig = { config, name, options, ... }: {
286     config = {
287       unitConfig =
288         optionalAttrs (config.requires != [])
289           { Requires = toString config.requires; }
290         // optionalAttrs (config.wants != [])
291           { Wants = toString config.wants; }
292         // optionalAttrs (config.after != [])
293           { After = toString config.after; }
294         // optionalAttrs (config.before != [])
295           { Before = toString config.before; }
296         // optionalAttrs (config.bindsTo != [])
297           { BindsTo = toString config.bindsTo; }
298         // optionalAttrs (config.partOf != [])
299           { PartOf = toString config.partOf; }
300         // optionalAttrs (config.conflicts != [])
301           { Conflicts = toString config.conflicts; }
302         // optionalAttrs (config.requisite != [])
303           { Requisite = toString config.requisite; }
304         // optionalAttrs (config ? restartTriggers && config.restartTriggers != [])
305           { X-Restart-Triggers = "${pkgs.writeText "X-Restart-Triggers-${name}" (toString config.restartTriggers)}"; }
306         // optionalAttrs (config ? reloadTriggers && config.reloadTriggers != [])
307           { X-Reload-Triggers = "${pkgs.writeText "X-Reload-Triggers-${name}" (toString config.reloadTriggers)}"; }
308         // optionalAttrs (config.description != "") {
309           Description = config.description; }
310         // optionalAttrs (config.documentation != []) {
311           Documentation = toString config.documentation; }
312         // optionalAttrs (config.onFailure != []) {
313           OnFailure = toString config.onFailure; }
314         // optionalAttrs (config.onSuccess != []) {
315           OnSuccess = toString config.onSuccess; }
316         // optionalAttrs (options.startLimitIntervalSec.isDefined) {
317           StartLimitIntervalSec = toString config.startLimitIntervalSec;
318         } // optionalAttrs (options.startLimitBurst.isDefined) {
319           StartLimitBurst = toString config.startLimitBurst;
320         };
321     };
322   };
324   serviceConfig = { config, ... }: {
325     config.environment.PATH = mkIf (config.path != []) "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}";
326   };
328   stage2ServiceConfig = {
329     imports = [ serviceConfig ];
330     # Default path for systemd services. Should be quite minimal.
331     config.path = mkAfter [
332       pkgs.coreutils
333       pkgs.findutils
334       pkgs.gnugrep
335       pkgs.gnused
336       systemd
337     ];
338   };
340   stage1ServiceConfig = serviceConfig;
342   mountConfig = { config, ... }: {
343     config = {
344       mountConfig =
345         { What = config.what;
346           Where = config.where;
347         } // optionalAttrs (config.type != "") {
348           Type = config.type;
349         } // optionalAttrs (config.options != "") {
350           Options = config.options;
351         };
352     };
353   };
355   automountConfig = { config, ... }: {
356     config = {
357       automountConfig =
358         { Where = config.where;
359         };
360     };
361   };
363   commonUnitText = def: ''
364       [Unit]
365       ${attrsToSection def.unitConfig}
366     '';
368   targetToUnit = name: def:
369     { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
370       text =
371         ''
372           [Unit]
373           ${attrsToSection def.unitConfig}
374         '';
375     };
377   serviceToUnit = name: def:
378     { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
379       text = commonUnitText def + ''
380         [Service]
381       '' + (let env = cfg.globalEnvironment // def.environment;
382         in concatMapStrings (n:
383           let s = optionalString (env.${n} != null)
384             "Environment=${builtins.toJSON "${n}=${env.${n}}"}\n";
385           # systemd max line length is now 1MiB
386           # https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af
387           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))
388       + (if def ? reloadIfChanged && def.reloadIfChanged then ''
389         X-ReloadIfChanged=true
390       '' else if (def ? restartIfChanged && !def.restartIfChanged) then ''
391         X-RestartIfChanged=false
392       '' else "")
393        + optionalString (def ? stopIfChanged && !def.stopIfChanged) ''
394          X-StopIfChanged=false
395       '' + attrsToSection def.serviceConfig;
396     };
398   socketToUnit = name: def:
399     { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
400       text = commonUnitText def +
401         ''
402           [Socket]
403           ${attrsToSection def.socketConfig}
404           ${concatStringsSep "\n" (map (s: "ListenStream=${s}") def.listenStreams)}
405           ${concatStringsSep "\n" (map (s: "ListenDatagram=${s}") def.listenDatagrams)}
406         '';
407     };
409   timerToUnit = name: def:
410     { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
411       text = commonUnitText def +
412         ''
413           [Timer]
414           ${attrsToSection def.timerConfig}
415         '';
416     };
418   pathToUnit = name: def:
419     { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
420       text = commonUnitText def +
421         ''
422           [Path]
423           ${attrsToSection def.pathConfig}
424         '';
425     };
427   mountToUnit = name: def:
428     { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
429       text = commonUnitText def +
430         ''
431           [Mount]
432           ${attrsToSection def.mountConfig}
433         '';
434     };
436   automountToUnit = name: def:
437     { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
438       text = commonUnitText def +
439         ''
440           [Automount]
441           ${attrsToSection def.automountConfig}
442         '';
443     };
445   sliceToUnit = name: def:
446     { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
447       text = commonUnitText def +
448         ''
449           [Slice]
450           ${attrsToSection def.sliceConfig}
451         '';
452     };
454   # Create a directory that contains systemd definition files from an attrset
455   # that contains the file names as keys and the content as values. The values
456   # in that attrset are determined by the supplied format.
457   definitions = directoryName: format: definitionAttrs:
458     let
459       listOfDefinitions = lib.mapAttrsToList
460         (name: format.generate "${name}.conf")
461         definitionAttrs;
462     in
463     pkgs.runCommand directoryName { } ''
464       mkdir -p $out
465       ${(lib.concatStringsSep "\n"
466         (map (pkg: "cp ${pkg} $out/${pkg.name}") listOfDefinitions)
467       )}
468     '';