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