vuls: init at 0.27.0
[NixPkgs.git] / nixos / lib / systemd-lib.nix
blobc5d64c3bd4f66abc2023d4bc80a2d8b81a60f7a1
1 { config, lib, pkgs, utils }:
3 let
4   inherit (lib)
5     all
6     attrByPath
7     attrNames
8     concatLists
9     concatMap
10     concatMapStrings
11     concatStrings
12     concatStringsSep
13     const
14     elem
15     elemAt
16     filter
17     filterAttrs
18     flatten
19     flip
20     head
21     isInt
22     isFloat
23     isList
24     isPath
25     isString
26     length
27     makeBinPath
28     makeSearchPathOutput
29     mapAttrs
30     mapAttrsToList
31     mapNullable
32     match
33     mkAfter
34     mkIf
35     optional
36     optionalAttrs
37     optionalString
38     pipe
39     range
40     replaceStrings
41     reverseList
42     splitString
43     stringLength
44     stringToCharacters
45     tail
46     toIntBase10
47     trace
48     types
49     ;
51   inherit (lib.strings) toJSON;
53   cfg = config.systemd;
54   lndir = "${pkgs.buildPackages.xorg.lndir}/bin/lndir";
55   systemd = cfg.package;
56 in rec {
58   shellEscape = s: (replaceStrings [ "\\" ] [ "\\\\" ] s);
60   mkPathSafeName = replaceStrings ["@" ":" "\\" "[" "]"] ["-" "-" "-" "" ""];
62   # a type for options that take a unit name
63   unitNameType = types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)";
65   makeUnit = name: unit:
66     if unit.enable then
67       pkgs.runCommand "unit-${mkPathSafeName name}"
68         { preferLocalBuild = true;
69           allowSubstitutes = false;
70           # unit.text can be null. But variables that are null listed in
71           # passAsFile are ignored by nix, resulting in no file being created,
72           # making the mv operation fail.
73           text = optionalString (unit.text != null) unit.text;
74           passAsFile = [ "text" ];
75         }
76         ''
77           name=${shellEscape name}
78           mkdir -p "$out/$(dirname -- "$name")"
79           mv "$textPath" "$out/$name"
80         ''
81     else
82       pkgs.runCommand "unit-${mkPathSafeName name}-disabled"
83         { preferLocalBuild = true;
84           allowSubstitutes = false;
85         }
86         ''
87           name=${shellEscape name}
88           mkdir -p "$out/$(dirname "$name")"
89           ln -s /dev/null "$out/$name"
90         '';
92   boolValues = [true false "yes" "no"];
94   digits = map toString (range 0 9);
96   isByteFormat = s:
97     let
98       l = reverseList (stringToCharacters s);
99       suffix = head l;
100       nums = tail l;
101     in elem suffix (["K" "M" "G" "T"] ++ digits)
102       && all (num: elem num digits) nums;
104   assertByteFormat = name: group: attr:
105     optional (attr ? ${name} && ! isByteFormat attr.${name})
106       "Systemd ${group} field `${name}' must be in byte format [0-9]+[KMGT].";
108   toIntBaseDetected = value: assert (match "[0-9]+|0x[0-9a-fA-F]+" value) != null; (builtins.fromTOML "v=${value}").v;
110   hexChars = stringToCharacters "0123456789abcdefABCDEF";
112   isMacAddress = s: stringLength s == 17
113     && flip all (splitString ":" s) (bytes:
114       all (byte: elem byte hexChars) (stringToCharacters bytes)
115     );
117   assertMacAddress = name: group: attr:
118     optional (attr ? ${name} && ! isMacAddress attr.${name})
119       "Systemd ${group} field `${name}' must be a valid MAC address.";
121   assertNetdevMacAddress = name: group: attr:
122     optional (attr ? ${name} && (! isMacAddress attr.${name} && attr.${name} != "none"))
123       "Systemd ${group} field `${name}` must be a valid MAC address or the special value `none`.";
125   isNumberOrRangeOf = check: v:
126     if isInt v
127     then check v
128     else let
129       parts = splitString "-" v;
130       lower = toIntBase10 (head parts);
131       upper = if tail parts != [] then toIntBase10 (head (tail parts)) else lower;
132     in
133       length parts <= 2 && lower <= upper && check lower && check upper;
134   isPort = i: i >= 0 && i <= 65535;
135   isPortOrPortRange = isNumberOrRangeOf isPort;
137   assertPort = name: group: attr:
138     optional (attr ? ${name} && ! isPort attr.${name})
139       "Error on the systemd ${group} field `${name}': ${attr.name} is not a valid port number.";
141   assertPortOrPortRange = name: group: attr:
142     optional (attr ? ${name} && ! isPortOrPortRange attr.${name})
143       "Error on the systemd ${group} field `${name}': ${attr.name} is not a valid port number or range of port numbers.";
145   assertValueOneOf = name: values: group: attr:
146     optional (attr ? ${name} && !elem attr.${name} values)
147       "Systemd ${group} field `${name}' cannot have value `${toString attr.${name}}'.";
149   assertValuesSomeOfOr = name: values: default: group: attr:
150     optional (attr ? ${name} && !(all (x: elem x values) (splitString " " attr.${name}) || attr.${name} == default))
151       "Systemd ${group} field `${name}' cannot have value `${toString attr.${name}}'.";
153   assertHasField = name: group: attr:
154     optional (!(attr ? ${name}))
155       "Systemd ${group} field `${name}' must exist.";
157   assertRange = name: min: max: group: attr:
158     optional (attr ? ${name} && !(min <= attr.${name} && max >= attr.${name}))
159       "Systemd ${group} field `${name}' is outside the range [${toString min},${toString max}]";
161   assertRangeOrOneOf = name: min: max: values: group: attr:
162     optional (attr ? ${name} && !(((isInt attr.${name} || isFloat attr.${name}) && min <= attr.${name} && max >= attr.${name}) || elem attr.${name} values))
163       "Systemd ${group} field `${name}' is not a value in range [${toString min},${toString max}], or one of ${toString values}";
165   assertRangeWithOptionalMask = name: min: max: group: attr:
166     if (attr ? ${name}) then
167       if isInt attr.${name} then
168         assertRange name min max group attr
169       else if isString attr.${name} then
170         let
171           fields = match "([0-9]+|0x[0-9a-fA-F]+)(/([0-9]+|0x[0-9a-fA-F]+))?" attr.${name};
172         in if fields == null then ["Systemd ${group} field `${name}' must either be an integer or two integers separated by a slash (/)."]
173         else let
174           value = toIntBaseDetected (elemAt fields 0);
175           mask = mapNullable toIntBaseDetected (elemAt fields 2);
176         in
177           optional (!(min <= value && max >= value)) "Systemd ${group} field `${name}' has main value outside the range [${toString min},${toString max}]."
178           ++ optional (mask != null && !(min <= mask && max >= mask)) "Systemd ${group} field `${name}' has mask outside the range [${toString min},${toString max}]."
179       else ["Systemd ${group} field `${name}' must either be an integer or a string."]
180     else [];
182   assertMinimum = name: min: group: attr:
183     optional (attr ? ${name} && attr.${name} < min)
184       "Systemd ${group} field `${name}' must be greater than or equal to ${toString min}";
186   assertOnlyFields = fields: group: attr:
187     let badFields = filter (name: ! elem name fields) (attrNames attr); in
188     optional (badFields != [ ])
189       "Systemd ${group} has extra fields [${concatStringsSep " " badFields}].";
191   assertInt = name: group: attr:
192     optional (attr ? ${name} && !isInt attr.${name})
193       "Systemd ${group} field `${name}' is not an integer";
195   assertRemoved = name: see: group: attr:
196     optional (attr ? ${name})
197       "Systemd ${group} field `${name}' has been removed. See ${see}";
199   checkUnitConfig = group: checks: attrs: let
200     # We're applied at the top-level type (attrsOf unitOption), so the actual
201     # unit options might contain attributes from mkOverride and mkIf that we need to
202     # convert into single values before checking them.
203     defs = mapAttrs (const (v:
204       if v._type or "" == "override" then v.content
205       else if v._type or "" == "if" then v.content
206       else v
207     )) attrs;
208     errors = concatMap (c: c group defs) checks;
209   in if errors == [] then true
210      else trace (concatStringsSep "\n" errors) false;
212   checkUnitConfigWithLegacyKey = legacyKey: group: checks: attrs:
213     let
214       dump = lib.generators.toPretty { }
215         (lib.generators.withRecursion { depthLimit = 2; throwOnDepthLimit = false; } attrs);
216       attrs' =
217         if legacyKey == null
218           then attrs
219         else if ! attrs?${legacyKey}
220           then attrs
221         else if removeAttrs attrs [ legacyKey ] == {}
222           then attrs.${legacyKey}
223         else throw ''
224           The declaration
226           ${dump}
228           must not mix unit options with the legacy key '${legacyKey}'.
230           This can be fixed by moving all settings from within ${legacyKey}
231           one level up.
232         '';
233     in
234     checkUnitConfig group checks attrs';
236   toOption = x:
237     if x == true then "true"
238     else if x == false then "false"
239     else toString x;
241   attrsToSection = as:
242     concatStrings (concatLists (mapAttrsToList (name: value:
243       map (x: ''
244           ${name}=${toOption x}
245         '')
246         (if isList value then value else [value]))
247         as));
249   generateUnits = { allowCollisions ? true, type, units, upstreamUnits, upstreamWants, packages ? cfg.packages, package ? cfg.package }:
250     let
251       typeDir = ({
252         system = "system";
253         initrd = "system";
254         user = "user";
255         nspawn = "nspawn";
256       }).${type};
257     in pkgs.runCommand "${type}-units"
258       { preferLocalBuild = true;
259         allowSubstitutes = false;
260       } ''
261       mkdir -p $out
263       # Copy the upstream systemd units we're interested in.
264       for i in ${toString upstreamUnits}; do
265         fn=${package}/example/systemd/${typeDir}/$i
266         if ! [ -e $fn ]; then echo "missing $fn"; false; fi
267         if [ -L $fn ]; then
268           target="$(readlink "$fn")"
269           if [ ''${target:0:3} = ../ ]; then
270             ln -s "$(readlink -f "$fn")" $out/
271           else
272             cp -pd $fn $out/
273           fi
274         else
275           ln -s $fn $out/
276         fi
277       done
279       # Copy .wants links, but only those that point to units that
280       # we're interested in.
281       for i in ${toString upstreamWants}; do
282         fn=${package}/example/systemd/${typeDir}/$i
283         if ! [ -e $fn ]; then echo "missing $fn"; false; fi
284         x=$out/$(basename $fn)
285         mkdir $x
286         for i in $fn/*; do
287           y=$x/$(basename $i)
288           cp -pd $i $y
289           if ! [ -e $y ]; then rm $y; fi
290         done
291       done
293       # Symlink all units provided listed in systemd.packages.
294       packages="${toString packages}"
296       # Filter duplicate directories
297       declare -A unique_packages
298       for k in $packages ; do unique_packages[$k]=1 ; done
300       for i in ''${!unique_packages[@]}; do
301         for fn in $i/etc/systemd/${typeDir}/* $i/lib/systemd/${typeDir}/*; do
302           if ! [[ "$fn" =~ .wants$ ]]; then
303             if [[ -d "$fn" ]]; then
304               targetDir="$out/$(basename "$fn")"
305               mkdir -p "$targetDir"
306               ${lndir} "$fn" "$targetDir"
307             else
308               ln -s $fn $out/
309             fi
310           fi
311         done
312       done
314       # Symlink units defined by systemd.units where override strategy
315       # shall be automatically detected. If these are also provided by
316       # systemd or systemd.packages, then add them as
317       # <unit-name>.d/overrides.conf, which makes them extend the
318       # upstream unit.
319       for i in ${toString (mapAttrsToList
320           (n: v: v.unit)
321           (filterAttrs (n: v: (attrByPath [ "overrideStrategy" ] "asDropinIfExists" v) == "asDropinIfExists") units))}; do
322         fn=$(basename $i/*)
323         if [ -e $out/$fn ]; then
324           if [ "$(readlink -f $i/$fn)" = /dev/null ]; then
325             ln -sfn /dev/null $out/$fn
326           else
327             ${if allowCollisions then ''
328               mkdir -p $out/$fn.d
329               ln -s $i/$fn $out/$fn.d/overrides.conf
330             '' else ''
331               echo "Found multiple derivations configuring $fn!"
332               exit 1
333             ''}
334           fi
335        else
336           ln -fs $i/$fn $out/
337         fi
338       done
340       # Symlink units defined by systemd.units which shall be
341       # treated as drop-in file.
342       for i in ${toString (mapAttrsToList
343           (n: v: v.unit)
344           (filterAttrs (n: v: v ? overrideStrategy && v.overrideStrategy == "asDropin") units))}; do
345         fn=$(basename $i/*)
346         mkdir -p $out/$fn.d
347         ln -s $i/$fn $out/$fn.d/overrides.conf
348       done
350       # Create service aliases from aliases option.
351       ${concatStrings (mapAttrsToList (name: unit:
352           concatMapStrings (name2: ''
353             ln -sfn '${name}' $out/'${name2}'
354           '') (unit.aliases or [])) units)}
356       # Create .wants, .upholds and .requires symlinks from the wantedBy, upheldBy and
357       # requiredBy options.
358       ${concatStrings (mapAttrsToList (name: unit:
359           concatMapStrings (name2: ''
360             mkdir -p $out/'${name2}.wants'
361             ln -sfn '../${name}' $out/'${name2}.wants'/
362           '') (unit.wantedBy or [])) units)}
364       ${concatStrings (mapAttrsToList (name: unit:
365           concatMapStrings (name2: ''
366             mkdir -p $out/'${name2}.upholds'
367             ln -sfn '../${name}' $out/'${name2}.upholds'/
368           '') (unit.upheldBy or [])) units)}
370       ${concatStrings (mapAttrsToList (name: unit:
371           concatMapStrings (name2: ''
372             mkdir -p $out/'${name2}.requires'
373             ln -sfn '../${name}' $out/'${name2}.requires'/
374           '') (unit.requiredBy or [])) units)}
376       ${optionalString (type == "system") ''
377         # Stupid misc. symlinks.
378         ln -s ${cfg.defaultUnit} $out/default.target
379         ln -s ${cfg.ctrlAltDelUnit} $out/ctrl-alt-del.target
380         ln -s rescue.target $out/kbrequest.target
382         mkdir -p $out/getty.target.wants/
383         ln -s ../autovt@tty1.service $out/getty.target.wants/
385         ln -s ../remote-fs.target $out/multi-user.target.wants/
386       ''}
387     ''; # */
389   makeJobScript = { name, text, enableStrictShellChecks }:
390     let
391       scriptName = replaceStrings [ "\\" "@" ] [ "-" "_" ] (shellEscape name);
392       out = (
393         if ! enableStrictShellChecks then
394           pkgs.writeShellScriptBin scriptName ''
395             set -e
397             ${text}
398           ''
399         else
400           pkgs.writeShellApplication {
401             name = scriptName;
402             inherit text;
403           }
404       ).overrideAttrs (_: {
405         # The derivation name is different from the script file name
406         # to keep the script file name short to avoid cluttering logs.
407         name = "unit-script-${scriptName}";
408       });
409     in lib.getExe out;
411   unitConfig = { config, name, options, ... }: {
412     config = {
413       unitConfig =
414         optionalAttrs (config.requires != [])
415           { Requires = toString config.requires; }
416         // optionalAttrs (config.wants != [])
417           { Wants = toString config.wants; }
418         // optionalAttrs (config.upholds != [])
419           { Upholds = toString config.upholds; }
420         // optionalAttrs (config.after != [])
421           { After = toString config.after; }
422         // optionalAttrs (config.before != [])
423           { Before = toString config.before; }
424         // optionalAttrs (config.bindsTo != [])
425           { BindsTo = toString config.bindsTo; }
426         // optionalAttrs (config.partOf != [])
427           { PartOf = toString config.partOf; }
428         // optionalAttrs (config.conflicts != [])
429           { Conflicts = toString config.conflicts; }
430         // optionalAttrs (config.requisite != [])
431           { Requisite = toString config.requisite; }
432         // optionalAttrs (config ? restartTriggers && config.restartTriggers != [])
433           { X-Restart-Triggers = "${pkgs.writeText "X-Restart-Triggers-${name}" (pipe config.restartTriggers [
434               flatten
435               (map (x: if isPath x then "${x}" else x))
436               toString
437             ])}"; }
438         // optionalAttrs (config ? reloadTriggers && config.reloadTriggers != [])
439           { X-Reload-Triggers = "${pkgs.writeText "X-Reload-Triggers-${name}" (pipe config.reloadTriggers [
440               flatten
441               (map (x: if isPath x then "${x}" else x))
442               toString
443             ])}"; }
444         // optionalAttrs (config.description != "") {
445           Description = config.description; }
446         // optionalAttrs (config.documentation != []) {
447           Documentation = toString config.documentation; }
448         // optionalAttrs (config.onFailure != []) {
449           OnFailure = toString config.onFailure; }
450         // optionalAttrs (config.onSuccess != []) {
451           OnSuccess = toString config.onSuccess; }
452         // optionalAttrs (options.startLimitIntervalSec.isDefined) {
453           StartLimitIntervalSec = toString config.startLimitIntervalSec;
454         } // optionalAttrs (options.startLimitBurst.isDefined) {
455           StartLimitBurst = toString config.startLimitBurst;
456         };
457     };
458   };
460   serviceConfig =
461   let
462     nixosConfig = config;
463   in
464   { name, lib, config, ... }: {
465     config = {
466       name = "${name}.service";
467       environment.PATH = mkIf (config.path != []) "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}";
469       enableStrictShellChecks = lib.mkOptionDefault nixosConfig.systemd.enableStrictShellChecks;
470     };
471   };
473   pathConfig = { name, config, ... }: {
474     config = {
475       name = "${name}.path";
476     };
477   };
479   socketConfig = { name, config, ... }: {
480     config = {
481       name = "${name}.socket";
482     };
483   };
485   sliceConfig = { name, config, ... }: {
486     config = {
487       name = "${name}.slice";
488     };
489   };
491   targetConfig = { name, config, ... }: {
492     config = {
493       name = "${name}.target";
494     };
495   };
497   timerConfig = { name, config, ... }: {
498     config = {
499       name = "${name}.timer";
500     };
501   };
503   stage2ServiceConfig = {
504     imports = [ serviceConfig ];
505     # Default path for systemd services. Should be quite minimal.
506     config.path = mkAfter [
507       pkgs.coreutils
508       pkgs.findutils
509       pkgs.gnugrep
510       pkgs.gnused
511       systemd
512     ];
513   };
515   stage1ServiceConfig = serviceConfig;
517   mountConfig = { config, ... }: {
518     config = {
519       name = "${utils.escapeSystemdPath config.where}.mount";
520       mountConfig =
521         { What = config.what;
522           Where = config.where;
523         } // optionalAttrs (config.type != "") {
524           Type = config.type;
525         } // optionalAttrs (config.options != "") {
526           Options = config.options;
527         };
528     };
529   };
531   automountConfig = { config, ... }: {
532     config = {
533       name = "${utils.escapeSystemdPath config.where}.automount";
534       automountConfig =
535         { Where = config.where;
536         };
537     };
538   };
540   commonUnitText = def: lines: ''
541       [Unit]
542       ${attrsToSection def.unitConfig}
543     '' + lines + optionalString (def.wantedBy != [ ]) ''
545       [Install]
546       WantedBy=${concatStringsSep " " def.wantedBy}
547     '';
549   targetToUnit = def:
550     { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
551       text =
552         ''
553           [Unit]
554           ${attrsToSection def.unitConfig}
555         '';
556     };
558   serviceToUnit = def:
559     { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
560       text = commonUnitText def (''
561         [Service]
562       '' + (let env = cfg.globalEnvironment // def.environment;
563         in concatMapStrings (n:
564           let s = optionalString (env.${n} != null)
565             "Environment=${toJSON "${n}=${env.${n}}"}\n";
566           # systemd max line length is now 1MiB
567           # https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af
568           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))
569       + (if def ? reloadIfChanged && def.reloadIfChanged then ''
570         X-ReloadIfChanged=true
571       '' else if (def ? restartIfChanged && !def.restartIfChanged) then ''
572         X-RestartIfChanged=false
573       '' else "")
574        + optionalString (def ? stopIfChanged && !def.stopIfChanged) ''
575          X-StopIfChanged=false
576       '' + attrsToSection def.serviceConfig);
577     };
579   socketToUnit = def:
580     { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
581       text = commonUnitText def ''
582         [Socket]
583         ${attrsToSection def.socketConfig}
584         ${concatStringsSep "\n" (map (s: "ListenStream=${s}") def.listenStreams)}
585         ${concatStringsSep "\n" (map (s: "ListenDatagram=${s}") def.listenDatagrams)}
586       '';
587     };
589   timerToUnit = def:
590     { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
591       text = commonUnitText def ''
592         [Timer]
593         ${attrsToSection def.timerConfig}
594       '';
595     };
597   pathToUnit = def:
598     { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
599       text = commonUnitText def ''
600         [Path]
601         ${attrsToSection def.pathConfig}
602       '';
603     };
605   mountToUnit = def:
606     { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
607       text = commonUnitText def ''
608         [Mount]
609         ${attrsToSection def.mountConfig}
610       '';
611     };
613   automountToUnit = def:
614     { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
615       text = commonUnitText def ''
616         [Automount]
617         ${attrsToSection def.automountConfig}
618       '';
619     };
621   sliceToUnit = def:
622     { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
623       text = commonUnitText def ''
624         [Slice]
625         ${attrsToSection def.sliceConfig}
626       '';
627     };
629   # Create a directory that contains systemd definition files from an attrset
630   # that contains the file names as keys and the content as values. The values
631   # in that attrset are determined by the supplied format.
632   definitions = directoryName: format: definitionAttrs:
633     let
634       listOfDefinitions = mapAttrsToList
635         (name: format.generate "${name}.conf")
636         definitionAttrs;
637     in
638     pkgs.runCommand directoryName { } ''
639       mkdir -p $out
640       ${(concatStringsSep "\n"
641         (map (pkg: "cp ${pkg} $out/${pkg.name}") listOfDefinitions)
642       )}
643     '';
645   # The maximum number of characters allowed in a GPT partition label. This
646   # limit is specified by UEFI and enforced by systemd-repart.
647   # Corresponds to GPT_LABEL_MAX from systemd's gpt.h.
648   GPTMaxLabelLength = 36;