Merge pull request #11484 from oxij/nixos-toposort-filesystems

lib: add toposort, nixos: use toposort for fileSystems to properly support bind and move mounts

authored by

Nikolay Amiantov and committed by
GitHub
3f70fcd4 44289f81

+133 -35
+80
lib/lists.nix
··· 256 256 reverseList = xs: 257 257 let l = length xs; in genList (n: elemAt xs (l - n - 1)) l; 258 258 259 + /* Depth-First Search (DFS) for lists `list != []`. 260 + 261 + `before a b == true` means that `b` depends on `a` (there's an 262 + edge from `b` to `a`). 263 + 264 + Examples: 265 + 266 + listDfs true hasPrefix [ "/home/user" "other" "/" "/home" ] 267 + == { minimal = "/"; # minimal element 268 + visited = [ "/home/user" ]; # seen elements (in reverse order) 269 + rest = [ "/home" "other" ]; # everything else 270 + } 271 + 272 + listDfs true hasPrefix [ "/home/user" "other" "/" "/home" "/" ] 273 + == { cycle = "/"; # cycle encountered at this element 274 + loops = [ "/" ]; # and continues to these elements 275 + visited = [ "/" "/home/user" ]; # elements leading to the cycle (in reverse order) 276 + rest = [ "/home" "other" ]; # everything else 277 + 278 + */ 279 + 280 + listDfs = stopOnCycles: before: list: 281 + let 282 + dfs' = us: visited: rest: 283 + let 284 + c = filter (x: before x us) visited; 285 + b = partition (x: before x us) rest; 286 + in if stopOnCycles && (length c > 0) 287 + then { cycle = us; loops = c; inherit visited rest; } 288 + else if length b.right == 0 289 + then # nothing is before us 290 + { minimal = us; inherit visited rest; } 291 + else # grab the first one before us and continue 292 + dfs' (head b.right) 293 + ([ us ] ++ visited) 294 + (tail b.right ++ b.wrong); 295 + in dfs' (head list) [] (tail list); 296 + 297 + /* Sort a list based on a partial ordering using DFS. This 298 + implementation is O(N^2), if your ordering is linear, use `sort` 299 + instead. 300 + 301 + `before a b == true` means that `b` should be after `a` 302 + in the result. 303 + 304 + Examples: 305 + 306 + toposort hasPrefix [ "/home/user" "other" "/" "/home" ] 307 + == { result = [ "/" "/home" "/home/user" "other" ]; } 308 + 309 + toposort hasPrefix [ "/home/user" "other" "/" "/home" "/" ] 310 + == { cycle = [ "/home/user" "/" "/" ]; # path leading to a cycle 311 + loops = [ "/" ]; } # loops back to these elements 312 + 313 + toposort hasPrefix [ "other" "/home/user" "/home" "/" ] 314 + == { result = [ "other" "/" "/home" "/home/user" ]; } 315 + 316 + toposort (a: b: a < b) [ 3 2 1 ] == { result = [ 1 2 3 ]; } 317 + 318 + */ 319 + 320 + toposort = before: list: 321 + let 322 + dfsthis = listDfs true before list; 323 + toporest = toposort before (dfsthis.visited ++ dfsthis.rest); 324 + in 325 + if length list < 2 326 + then # finish 327 + { result = list; } 328 + else if dfsthis ? "cycle" 329 + then # there's a cycle, starting from the current vertex, return it 330 + { cycle = reverseList ([ dfsthis.cycle ] ++ dfsthis.visited); 331 + inherit (dfsthis) loops; } 332 + else if toporest ? "cycle" 333 + then # there's a cycle somewhere else in the graph, return it 334 + toporest 335 + # Slow, but short. Can be made a bit faster with an explicit stack. 336 + else # there are no cycles 337 + { result = [ dfsthis.minimal ] ++ toporest.result; }; 338 + 259 339 /* Sort a list based on a comparator function which compares two 260 340 elements and returns true if the first argument is strictly below 261 341 the second argument. The returned list is sorted in an increasing
+9
nixos/lib/utils.nix
··· 2 2 3 3 rec { 4 4 5 + # Check whenever fileSystem is needed for boot 6 + fsNeededForBoot = fs: fs.neededForBoot 7 + || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ]; 8 + 9 + # Check whenever `b` depends on `a` as a fileSystem 10 + # FIXME: it's incorrect to simply use hasPrefix here: "/dev/a" is not a parent of "/dev/ab" 11 + fsBefore = a: b: ((any (x: elem x [ "bind" "move" ]) b.options) && (a.mountPoint == b.device)) 12 + || (hasPrefix a.mountPoint b.mountPoint); 13 + 5 14 # Escape a path according to the systemd rules, e.g. /dev/xyzzy 6 15 # becomes dev-xyzzy. FIXME: slow. 7 16 escapeSystemdPath = s:
+1 -1
nixos/modules/security/grsecurity.nix
··· 12 12 (fs: (fs.neededForBoot 13 13 || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ]) 14 14 && fs.fsType == "zfs") 15 - (attrValues config.fileSystems) != []; 15 + config.system.build.fileSystems != []; 16 16 17 17 # Ascertain whether NixOS container support is required 18 18 containerSupportRequired =
+9 -19
nixos/modules/system/boot/stage-1.nix
··· 3 3 # the modules necessary to mount the root file system, then calls the 4 4 # init in the root file system to start the second boot stage. 5 5 6 - { config, lib, pkgs, ... }: 6 + { config, lib, utils, pkgs, ... }: 7 7 8 8 with lib; 9 9 ··· 23 23 }; 24 24 25 25 26 + # The initrd only has to mount `/` or any FS marked as necessary for 27 + # booting (such as the FS containing `/nix/store`, or an FS needed for 28 + # mounting `/`, like `/` on a loopback). 29 + fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems; 30 + 31 + 26 32 # Some additional utilities needed in stage 1, like mount, lvm, fsck 27 33 # etc. We don't want to bring in all of those packages, so we just 28 34 # copy what we need. Instead of using statically linked binaries, ··· 71 77 ln -sf kmod $out/bin/modprobe 72 78 73 79 # Copy resize2fs if needed. 74 - ${optionalString (any (fs: fs.autoResize) (attrValues config.fileSystems)) '' 80 + ${optionalString (any (fs: fs.autoResize) fileSystems) '' 75 81 # We need mke2fs in the initrd. 76 82 copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/resize2fs 77 83 ''} ··· 126 132 127 133 ${config.boot.initrd.extraUtilsCommandsTest} 128 134 ''; # */ 129 - 130 - 131 - # The initrd only has to mount / or any FS marked as necessary for 132 - # booting (such as the FS containing /nix/store, or an FS needed for 133 - # mounting /, like / on a loopback). 134 - # 135 - # We need to guarantee that / is the first filesystem in the list so 136 - # that if and when lustrateRoot is invoked, nothing else is mounted 137 - fileSystems = let 138 - filterNeeded = filter 139 - (fs: fs.mountPoint != "/" && (fs.neededForBoot || elem fs.mountPoint [ "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ])); 140 - filterRoot = filter 141 - (fs: fs.mountPoint == "/"); 142 - allFileSystems = attrValues config.fileSystems; 143 - in (filterRoot allFileSystems) ++ (filterNeeded allFileSystems); 144 135 145 136 146 137 udevRules = pkgs.stdenv.mkDerivation { ··· 405 396 }; 406 397 407 398 config = mkIf (!config.boot.isContainer) { 408 - 409 399 assertions = [ 410 - { assertion = any (fs: fs.mountPoint == "/") (attrValues config.fileSystems); 400 + { assertion = any (fs: fs.mountPoint == "/") fileSystems; 411 401 message = "The ‘fileSystems’ option does not specify your root file system."; 412 402 } 413 403 { assertion = let inherit (config.boot) resumeDevice; in
+1 -1
nixos/modules/tasks/encrypted-devices.nix
··· 3 3 with lib; 4 4 5 5 let 6 - fileSystems = attrValues config.fileSystems ++ config.swapDevices; 6 + fileSystems = config.system.build.fileSystems ++ config.swapDevices; 7 7 encDevs = filter (dev: dev.encrypted.enable) fileSystems; 8 8 keyedEncDevs = filter (dev: dev.encrypted.keyFile != null) encDevs; 9 9 keylessEncDevs = filter (dev: dev.encrypted.keyFile == null) encDevs;
+30 -9
nixos/modules/tasks/filesystems.nix
··· 5 5 6 6 let 7 7 8 - fileSystems = attrValues config.fileSystems; 8 + fileSystems' = toposort fsBefore (attrValues config.fileSystems); 9 + 10 + fileSystems = if fileSystems' ? "result" 11 + then # use topologically sorted fileSystems everywhere 12 + fileSystems'.result 13 + else # the assertion below will catch this, 14 + # but we fall back to the original order 15 + # anyway so that other modules could check 16 + # their assertions too 17 + (attrValues config.fileSystems); 9 18 10 19 prioOption = prio: optionalString (prio != null) " pri=${toString prio}"; 11 20 ··· 162 171 163 172 config = { 164 173 174 + assertions = let 175 + ls = sep: concatMapStringsSep sep (x: x.mountPoint); 176 + in [ 177 + { assertion = ! (fileSystems' ? "cycle"); 178 + message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}"; 179 + } 180 + ]; 181 + 182 + # Export for use in other modules 183 + system.build.fileSystems = fileSystems; 184 + 165 185 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems; 166 186 167 187 # Add the mount helpers to the system path so that `mount' can find them. ··· 180 200 # in your /etc/nixos/configuration.nix file. 181 201 182 202 # Filesystems. 183 - ${flip concatMapStrings fileSystems (fs: 203 + ${concatMapStrings (fs: 184 204 (if fs.device != null then fs.device 185 205 else if fs.label != null then "/dev/disk/by-label/${fs.label}" 186 206 else throw "No device specified for mount point ‘${fs.mountPoint}’.") ··· 191 211 + " " + (if skipCheck fs then "0" else 192 212 if fs.mountPoint == "/" then "1" else "2") 193 213 + "\n" 194 - )} 214 + ) fileSystems} 195 215 196 216 # Swap devices. 197 217 ${flip concatMapStrings config.swapDevices (sw: ··· 211 231 212 232 formatDevice = fs: 213 233 let 214 - mountPoint' = escapeSystemdPath fs.mountPoint; 215 - device' = escapeSystemdPath fs.device; 234 + mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount"; 235 + device' = escapeSystemdPath fs.device; 236 + device'' = "${device}.device"; 216 237 in nameValuePair "mkfs-${device'}" 217 238 { description = "Initialisation of Filesystem ${fs.device}"; 218 - wantedBy = [ "${mountPoint'}.mount" ]; 219 - before = [ "${mountPoint'}.mount" "systemd-fsck@${device'}.service" ]; 220 - requires = [ "${device'}.device" ]; 221 - after = [ "${device'}.device" ]; 239 + wantedBy = [ mountPoint' ]; 240 + before = [ mountPoint' "systemd-fsck@${device'}.service" ]; 241 + requires = [ device'' ]; 242 + after = [ device'' ]; 222 243 path = [ pkgs.utillinux ] ++ config.system.fsPackages; 223 244 script = 224 245 ''
+3 -5
nixos/modules/tasks/filesystems/zfs.nix
··· 36 36 37 37 fsToPool = fs: datasetToPool fs.device; 38 38 39 - zfsFilesystems = filter (x: x.fsType == "zfs") (attrValues config.fileSystems); 40 - 41 - isRoot = fs: fs.neededForBoot || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ]; 39 + zfsFilesystems = filter (x: x.fsType == "zfs") config.system.build.fileSystems; 42 40 43 41 allPools = unique ((map fsToPool zfsFilesystems) ++ cfgZfs.extraPools); 44 42 45 - rootPools = unique (map fsToPool (filter isRoot zfsFilesystems)); 43 + rootPools = unique (map fsToPool (filter fsNeededForBoot zfsFilesystems)); 46 44 47 45 dataPools = unique (filter (pool: !(elem pool rootPools)) allPools); 48 46 ··· 277 275 278 276 systemd.services = let 279 277 getPoolFilesystems = pool: 280 - filter (x: x.fsType == "zfs" && (fsToPool x) == pool) (attrValues config.fileSystems); 278 + filter (x: x.fsType == "zfs" && (fsToPool x) == pool) config.system.build.fileSystems; 281 279 282 280 getPoolMounts = pool: 283 281 let