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