at master 13 kB view raw
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 + (if prefix == "." then "" else ".") + 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