1{
2 lib,
3 config,
4 pkgs,
5}:
6
7let
8 inherit (lib)
9 any
10 attrNames
11 concatMapStringsSep
12 concatStringsSep
13 elem
14 escapeShellArg
15 filter
16 flatten
17 getName
18 hasPrefix
19 hasSuffix
20 imap0
21 imap1
22 isAttrs
23 isDerivation
24 isFloat
25 isInt
26 isList
27 isPath
28 isString
29 listToAttrs
30 mapAttrs
31 nameValuePair
32 optionalString
33 removePrefix
34 removeSuffix
35 replaceStrings
36 stringToCharacters
37 types
38 ;
39
40 inherit (lib.strings) toJSON normalizePath escapeC;
41in
42
43let
44 utils = rec {
45
46 # Copy configuration files to avoid having the entire sources in the system closure
47 copyFile =
48 filePath:
49 pkgs.runCommand (builtins.unsafeDiscardStringContext (baseNameOf filePath)) { } ''
50 cp ${filePath} $out
51 '';
52
53 # Check whenever fileSystem is needed for boot. NOTE: Make sure
54 # pathsNeededForBoot is closed under the parent relationship, i.e. if /a/b/c
55 # is in the list, put /a and /a/b in as well.
56 pathsNeededForBoot = [
57 "/"
58 "/nix"
59 "/nix/store"
60 "/var"
61 "/var/log"
62 "/var/lib"
63 "/var/lib/nixos"
64 "/etc"
65 "/usr"
66 ];
67 fsNeededForBoot = fs: fs.neededForBoot || elem fs.mountPoint pathsNeededForBoot;
68
69 # Check whenever `b` depends on `a` as a fileSystem
70 fsBefore =
71 a: b:
72 let
73 # normalisePath adds a slash at the end of the path if it didn't already
74 # have one.
75 #
76 # The reason slashes are added at the end of each path is to prevent `b`
77 # from accidentally depending on `a` in cases like
78 # a = { mountPoint = "/aaa"; ... }
79 # b = { device = "/aaaa"; ... }
80 # Here a.mountPoint *is* a prefix of b.device even though a.mountPoint is
81 # *not* a parent of b.device. If we add a slash at the end of each string,
82 # though, this is not a problem: "/aaa/" is not a prefix of "/aaaa/".
83 normalisePath = path: "${path}${optionalString (!(hasSuffix "/" path)) "/"}";
84 normalise =
85 mount:
86 mount
87 // {
88 device = normalisePath (toString mount.device);
89 mountPoint = normalisePath mount.mountPoint;
90 depends = map normalisePath mount.depends;
91 };
92
93 a' = normalise a;
94 b' = normalise b;
95
96 in
97 hasPrefix a'.mountPoint b'.device
98 || hasPrefix a'.mountPoint b'.mountPoint
99 || any (hasPrefix a'.mountPoint) b'.depends;
100
101 # Escape a path according to the systemd rules. FIXME: slow
102 # The rules are described in systemd.unit(5) as follows:
103 # The escaping algorithm operates as follows: given a string, any "/" character is replaced by "-", and all other characters which are not ASCII alphanumerics, ":", "_" or "." are replaced by C-style "\x2d" escapes. In addition, "." is replaced with such a C-style escape when it would appear as the first character in the escaped string.
104 # When the input qualifies as absolute file system path, this algorithm is extended slightly: the path to the root directory "/" is encoded as single dash "-". In addition, any leading, trailing or duplicate "/" characters are removed from the string before transformation. Example: /foo//bar/baz/ becomes "foo-bar-baz".
105 escapeSystemdPath =
106 s:
107 let
108 replacePrefix =
109 p: r: s:
110 (if (hasPrefix p s) then r + (removePrefix p s) else s);
111 trim = s: removeSuffix "/" (removePrefix "/" s);
112 normalizedPath = normalizePath s;
113 in
114 replaceStrings [ "/" ] [ "-" ] (
115 replacePrefix "." (escapeC [ "." ] ".") (
116 escapeC (stringToCharacters " !\"#$%&'()*+,;<=>=@[\\]^`{|}~-") (
117 if normalizedPath == "/" then normalizedPath else trim normalizedPath
118 )
119 )
120 );
121
122 # Quotes an argument for use in Exec* service lines.
123 # systemd accepts "-quoted strings with escape sequences, toJSON produces
124 # a subset of these.
125 # Additionally we escape % to disallow expansion of % specifiers. Any lone ;
126 # in the input will be turned it ";" and thus lose its special meaning.
127 # Every $ is escaped to $$, this makes it unnecessary to disable environment
128 # substitution for the directive.
129 escapeSystemdExecArg =
130 arg:
131 let
132 s =
133 if isPath arg then
134 "${arg}"
135 else if isString arg then
136 arg
137 else if isInt arg || isFloat arg || isDerivation arg then
138 toString arg
139 else
140 throw "escapeSystemdExecArg only allows strings, paths, numbers and derivations";
141 in
142 replaceStrings [ "%" "$" ] [ "%%" "$$" ] (toJSON s);
143
144 # Quotes a list of arguments into a single string for use in a Exec*
145 # line.
146 escapeSystemdExecArgs = concatMapStringsSep " " escapeSystemdExecArg;
147
148 # Returns a system path for a given shell package
149 toShellPath =
150 shell:
151 if types.shellPackage.check shell then
152 "/run/current-system/sw${shell.shellPath}"
153 else if types.package.check shell then
154 throw "${shell} is not a shell package"
155 else
156 shell;
157
158 /*
159 Recurse into a list or an attrset, searching for attrs named like
160 the value of the "attr" parameter, and return an attrset where the
161 names are the corresponding jq path where the attrs were found and
162 the values are the values of the attrs.
163
164 Example:
165 recursiveGetAttrWithJqPrefix {
166 example = [
167 {
168 irrelevant = "not interesting";
169 }
170 {
171 ignored = "ignored attr";
172 relevant = {
173 secret = {
174 _secret = "/path/to/secret";
175 };
176 };
177 }
178 ];
179 } "_secret" -> { ".example[1].relevant.secret" = "/path/to/secret"; }
180 */
181 recursiveGetAttrWithJqPrefix =
182 item: attr: mapAttrs (_name: set: set.${attr}) (recursiveGetAttrsetWithJqPrefix item attr);
183
184 /*
185 Similar to `recursiveGetAttrWithJqPrefix`, but returns the whole
186 attribute set containing `attr` instead of the value of `attr` in
187 the set.
188
189 Example:
190 recursiveGetAttrsetWithJqPrefix {
191 example = [
192 {
193 irrelevant = "not interesting";
194 }
195 {
196 ignored = "ignored attr";
197 relevant = {
198 secret = {
199 _secret = "/path/to/secret";
200 quote = true;
201 };
202 };
203 }
204 ];
205 } "_secret" -> { ".example[1].relevant.secret" = { _secret = "/path/to/secret"; quote = true; }; }
206 */
207 recursiveGetAttrsetWithJqPrefix =
208 item: attr:
209 let
210 recurse =
211 prefix: item:
212 if item ? ${attr} then
213 nameValuePair prefix item
214 else if isDerivation item then
215 [ ]
216 else if isAttrs item then
217 map (
218 name:
219 let
220 escapedName = ''"${replaceStrings [ ''"'' "\\" ] [ ''\"'' "\\\\" ] name}"'';
221 in
222 recurse (prefix + "." + escapedName) item.${name}
223 ) (attrNames item)
224 else if isList item then
225 imap0 (index: item: recurse (prefix + "[${toString index}]") item) item
226 else
227 [ ];
228 in
229 listToAttrs (flatten (recurse "" item));
230
231 /*
232 Takes an attrset and a file path and generates a bash snippet that
233 outputs a JSON file at the file path with all instances of
234
235 { _secret = "/path/to/secret" }
236
237 in the attrset replaced with the contents of the file
238 "/path/to/secret" in the output JSON.
239
240 When a configuration option accepts an attrset that is finally
241 converted to JSON, this makes it possible to let the user define
242 arbitrary secret values.
243
244 Example:
245 If the file "/path/to/secret" contains the string
246 "topsecretpassword1234",
247
248 genJqSecretsReplacementSnippet {
249 example = [
250 {
251 irrelevant = "not interesting";
252 }
253 {
254 ignored = "ignored attr";
255 relevant = {
256 secret = {
257 _secret = "/path/to/secret";
258 };
259 };
260 }
261 ];
262 } "/path/to/output.json"
263
264 would generate a snippet that, when run, outputs the following
265 JSON file at "/path/to/output.json":
266
267 {
268 "example": [
269 {
270 "irrelevant": "not interesting"
271 },
272 {
273 "ignored": "ignored attr",
274 "relevant": {
275 "secret": "topsecretpassword1234"
276 }
277 }
278 ]
279 }
280
281 The attribute set { _secret = "/path/to/secret"; } can contain extra
282 options, currently it accepts the `quote = true|false` option.
283
284 If `quote = true` (default behavior), the content of the secret file will
285 be quoted as a string and embedded. Otherwise, if `quote = false`, the
286 content of the secret file will be parsed to JSON and then embedded.
287
288 Example:
289 If the file "/path/to/secret" contains the JSON document:
290
291 [
292 { "a": "topsecretpassword1234" },
293 { "b": "topsecretpassword5678" }
294 ]
295
296 genJqSecretsReplacementSnippet {
297 example = [
298 {
299 irrelevant = "not interesting";
300 }
301 {
302 ignored = "ignored attr";
303 relevant = {
304 secret = {
305 _secret = "/path/to/secret";
306 quote = false;
307 };
308 };
309 }
310 ];
311 } "/path/to/output.json"
312
313 would generate a snippet that, when run, outputs the following
314 JSON file at "/path/to/output.json":
315
316 {
317 "example": [
318 {
319 "irrelevant": "not interesting"
320 },
321 {
322 "ignored": "ignored attr",
323 "relevant": {
324 "secret": [
325 { "a": "topsecretpassword1234" },
326 { "b": "topsecretpassword5678" }
327 ]
328 }
329 }
330 ]
331 }
332 */
333 genJqSecretsReplacementSnippet = genJqSecretsReplacementSnippet' "_secret";
334
335 # Like genJqSecretsReplacementSnippet, but allows the name of the
336 # attr which identifies the secret to be changed.
337 genJqSecretsReplacementSnippet' =
338 attr: set: output:
339 let
340 secretsRaw = recursiveGetAttrsetWithJqPrefix set attr;
341 # Set default option values
342 secrets = mapAttrs (
343 _name: set:
344 {
345 quote = true;
346 }
347 // set
348 ) secretsRaw;
349 stringOrDefault = str: def: if str == "" then def else str;
350 in
351 ''
352 if [[ -h '${output}' ]]; then
353 rm '${output}'
354 fi
355
356 inherit_errexit_enabled=0
357 shopt -pq inherit_errexit && inherit_errexit_enabled=1
358 shopt -s inherit_errexit
359 ''
360 + concatStringsSep "\n" (
361 imap1 (index: name: ''
362 secret${toString index}=$(<'${secrets.${name}.${attr}}')
363 export secret${toString index}
364 '') (attrNames secrets)
365 )
366 + "\n"
367 + "${pkgs.jq}/bin/jq >'${output}' "
368 + escapeShellArg (
369 stringOrDefault (concatStringsSep " | " (
370 imap1 (
371 index: name:
372 ''${name} = ($ENV.secret${toString index}${optionalString (!secrets.${name}.quote) " | fromjson"})''
373 ) (attrNames secrets)
374 )) "."
375 )
376 + ''
377 <<'EOF'
378 ${toJSON set}
379 EOF
380 (( ! inherit_errexit_enabled )) && shopt -u inherit_errexit
381 '';
382
383 /*
384 Remove packages of packagesToRemove from packages, based on their names.
385 Relies on package names and has quadratic complexity so use with caution!
386
387 Type:
388 removePackagesByName :: [package] -> [package] -> [package]
389
390 Example:
391 removePackagesByName [ nautilus file-roller ] [ file-roller totem ]
392 => [ nautilus ]
393 */
394 removePackagesByName =
395 packages: packagesToRemove:
396 let
397 namesToRemove = map getName packagesToRemove;
398 in
399 filter (x: !(elem (getName x) namesToRemove)) packages;
400
401 /*
402 Returns false if a package with the same name as the `package` is present in `packagesToDisable`.
403
404 Type:
405 disablePackageByName :: package -> [package] -> bool
406
407 Example:
408 disablePackageByName file-roller [ file-roller totem ]
409 => false
410
411 Example:
412 disablePackageByName nautilus [ file-roller totem ]
413 => true
414 */
415 disablePackageByName =
416 package: packagesToDisable:
417 let
418 namesToDisable = map getName packagesToDisable;
419 in
420 !elem (getName package) namesToDisable;
421
422 systemdUtils = {
423 lib = import ./systemd-lib.nix {
424 inherit
425 lib
426 config
427 pkgs
428 utils
429 ;
430 };
431 unitOptions = import ./systemd-unit-options.nix { inherit lib systemdUtils; };
432 types = import ./systemd-types.nix { inherit lib systemdUtils pkgs; };
433 network = {
434 units = import ./systemd-network-units.nix { inherit lib systemdUtils; };
435 };
436 };
437 };
438in
439utils