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