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 reverseList = xs: 257 let l = length xs; in genList (n: elemAt xs (l - n - 1)) l; 258 259 /* Sort a list based on a comparator function which compares two 260 elements and returns true if the first argument is strictly below 261 the second argument. The returned list is sorted in an increasing
··· 256 reverseList = xs: 257 let l = length xs; in genList (n: elemAt xs (l - n - 1)) l; 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 + 339 /* Sort a list based on a comparator function which compares two 340 elements and returns true if the first argument is strictly below 341 the second argument. The returned list is sorted in an increasing
+9
nixos/lib/utils.nix
··· 2 3 rec { 4 5 # Escape a path according to the systemd rules, e.g. /dev/xyzzy 6 # becomes dev-xyzzy. FIXME: slow. 7 escapeSystemdPath = s:
··· 2 3 rec { 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 + 14 # Escape a path according to the systemd rules, e.g. /dev/xyzzy 15 # becomes dev-xyzzy. FIXME: slow. 16 escapeSystemdPath = s:
+1 -1
nixos/modules/security/grsecurity.nix
··· 12 (fs: (fs.neededForBoot 13 || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ]) 14 && fs.fsType == "zfs") 15 - (attrValues config.fileSystems) != []; 16 17 # Ascertain whether NixOS container support is required 18 containerSupportRequired =
··· 12 (fs: (fs.neededForBoot 13 || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ]) 14 && fs.fsType == "zfs") 15 + config.system.build.fileSystems != []; 16 17 # Ascertain whether NixOS container support is required 18 containerSupportRequired =
+9 -19
nixos/modules/system/boot/stage-1.nix
··· 3 # the modules necessary to mount the root file system, then calls the 4 # init in the root file system to start the second boot stage. 5 6 - { config, lib, pkgs, ... }: 7 8 with lib; 9 ··· 23 }; 24 25 26 # Some additional utilities needed in stage 1, like mount, lvm, fsck 27 # etc. We don't want to bring in all of those packages, so we just 28 # copy what we need. Instead of using statically linked binaries, ··· 71 ln -sf kmod $out/bin/modprobe 72 73 # Copy resize2fs if needed. 74 - ${optionalString (any (fs: fs.autoResize) (attrValues config.fileSystems)) '' 75 # We need mke2fs in the initrd. 76 copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/resize2fs 77 ''} ··· 126 127 ${config.boot.initrd.extraUtilsCommandsTest} 128 ''; # */ 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 145 146 udevRules = pkgs.stdenv.mkDerivation { ··· 405 }; 406 407 config = mkIf (!config.boot.isContainer) { 408 - 409 assertions = [ 410 - { assertion = any (fs: fs.mountPoint == "/") (attrValues config.fileSystems); 411 message = "The ‘fileSystems’ option does not specify your root file system."; 412 } 413 { assertion = let inherit (config.boot) resumeDevice; in
··· 3 # the modules necessary to mount the root file system, then calls the 4 # init in the root file system to start the second boot stage. 5 6 + { config, lib, utils, pkgs, ... }: 7 8 with lib; 9 ··· 23 }; 24 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 + 32 # Some additional utilities needed in stage 1, like mount, lvm, fsck 33 # etc. We don't want to bring in all of those packages, so we just 34 # copy what we need. Instead of using statically linked binaries, ··· 77 ln -sf kmod $out/bin/modprobe 78 79 # Copy resize2fs if needed. 80 + ${optionalString (any (fs: fs.autoResize) fileSystems) '' 81 # We need mke2fs in the initrd. 82 copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/resize2fs 83 ''} ··· 132 133 ${config.boot.initrd.extraUtilsCommandsTest} 134 ''; # */ 135 136 137 udevRules = pkgs.stdenv.mkDerivation { ··· 396 }; 397 398 config = mkIf (!config.boot.isContainer) { 399 assertions = [ 400 + { assertion = any (fs: fs.mountPoint == "/") fileSystems; 401 message = "The ‘fileSystems’ option does not specify your root file system."; 402 } 403 { assertion = let inherit (config.boot) resumeDevice; in
+1 -1
nixos/modules/tasks/encrypted-devices.nix
··· 3 with lib; 4 5 let 6 - fileSystems = attrValues config.fileSystems ++ config.swapDevices; 7 encDevs = filter (dev: dev.encrypted.enable) fileSystems; 8 keyedEncDevs = filter (dev: dev.encrypted.keyFile != null) encDevs; 9 keylessEncDevs = filter (dev: dev.encrypted.keyFile == null) encDevs;
··· 3 with lib; 4 5 let 6 + fileSystems = config.system.build.fileSystems ++ config.swapDevices; 7 encDevs = filter (dev: dev.encrypted.enable) fileSystems; 8 keyedEncDevs = filter (dev: dev.encrypted.keyFile != null) encDevs; 9 keylessEncDevs = filter (dev: dev.encrypted.keyFile == null) encDevs;
+30 -9
nixos/modules/tasks/filesystems.nix
··· 5 6 let 7 8 - fileSystems = attrValues config.fileSystems; 9 10 prioOption = prio: optionalString (prio != null) " pri=${toString prio}"; 11 ··· 162 163 config = { 164 165 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems; 166 167 # Add the mount helpers to the system path so that `mount' can find them. ··· 180 # in your /etc/nixos/configuration.nix file. 181 182 # Filesystems. 183 - ${flip concatMapStrings fileSystems (fs: 184 (if fs.device != null then fs.device 185 else if fs.label != null then "/dev/disk/by-label/${fs.label}" 186 else throw "No device specified for mount point ‘${fs.mountPoint}’.") ··· 191 + " " + (if skipCheck fs then "0" else 192 if fs.mountPoint == "/" then "1" else "2") 193 + "\n" 194 - )} 195 196 # Swap devices. 197 ${flip concatMapStrings config.swapDevices (sw: ··· 211 212 formatDevice = fs: 213 let 214 - mountPoint' = escapeSystemdPath fs.mountPoint; 215 - device' = escapeSystemdPath fs.device; 216 in nameValuePair "mkfs-${device'}" 217 { 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" ]; 222 path = [ pkgs.utillinux ] ++ config.system.fsPackages; 223 script = 224 ''
··· 5 6 let 7 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); 18 19 prioOption = prio: optionalString (prio != null) " pri=${toString prio}"; 20 ··· 171 172 config = { 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 + 185 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems; 186 187 # Add the mount helpers to the system path so that `mount' can find them. ··· 200 # in your /etc/nixos/configuration.nix file. 201 202 # Filesystems. 203 + ${concatMapStrings (fs: 204 (if fs.device != null then fs.device 205 else if fs.label != null then "/dev/disk/by-label/${fs.label}" 206 else throw "No device specified for mount point ‘${fs.mountPoint}’.") ··· 211 + " " + (if skipCheck fs then "0" else 212 if fs.mountPoint == "/" then "1" else "2") 213 + "\n" 214 + ) fileSystems} 215 216 # Swap devices. 217 ${flip concatMapStrings config.swapDevices (sw: ··· 231 232 formatDevice = fs: 233 let 234 + mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount"; 235 + device' = escapeSystemdPath fs.device; 236 + device'' = "${device}.device"; 237 in nameValuePair "mkfs-${device'}" 238 { description = "Initialisation of Filesystem ${fs.device}"; 239 + wantedBy = [ mountPoint' ]; 240 + before = [ mountPoint' "systemd-fsck@${device'}.service" ]; 241 + requires = [ device'' ]; 242 + after = [ device'' ]; 243 path = [ pkgs.utillinux ] ++ config.system.fsPackages; 244 script = 245 ''
+3 -5
nixos/modules/tasks/filesystems/zfs.nix
··· 36 37 fsToPool = fs: datasetToPool fs.device; 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" ]; 42 43 allPools = unique ((map fsToPool zfsFilesystems) ++ cfgZfs.extraPools); 44 45 - rootPools = unique (map fsToPool (filter isRoot zfsFilesystems)); 46 47 dataPools = unique (filter (pool: !(elem pool rootPools)) allPools); 48 ··· 277 278 systemd.services = let 279 getPoolFilesystems = pool: 280 - filter (x: x.fsType == "zfs" && (fsToPool x) == pool) (attrValues config.fileSystems); 281 282 getPoolMounts = pool: 283 let
··· 36 37 fsToPool = fs: datasetToPool fs.device; 38 39 + zfsFilesystems = filter (x: x.fsType == "zfs") config.system.build.fileSystems; 40 41 allPools = unique ((map fsToPool zfsFilesystems) ++ cfgZfs.extraPools); 42 43 + rootPools = unique (map fsToPool (filter fsNeededForBoot zfsFilesystems)); 44 45 dataPools = unique (filter (pool: !(elem pool rootPools)) allPools); 46 ··· 275 276 systemd.services = let 277 getPoolFilesystems = pool: 278 + filter (x: x.fsType == "zfs" && (fsToPool x) == pool) config.system.build.fileSystems; 279 280 getPoolMounts = pool: 281 let