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

lib: add toposort, nixos: use toposort for fileSystems to properly support bind and move mounts
This commit is contained in:
Nikolay Amiantov 2016-08-27 14:34:55 +04:00 committed by GitHub
commit 3f70fcd4c1
7 changed files with 133 additions and 35 deletions

View File

@ -256,6 +256,86 @@ rec {
reverseList = xs: reverseList = xs:
let l = length xs; in genList (n: elemAt xs (l - n - 1)) l; let l = length xs; in genList (n: elemAt xs (l - n - 1)) l;
/* Depth-First Search (DFS) for lists `list != []`.
`before a b == true` means that `b` depends on `a` (there's an
edge from `b` to `a`).
Examples:
listDfs true hasPrefix [ "/home/user" "other" "/" "/home" ]
== { minimal = "/"; # minimal element
visited = [ "/home/user" ]; # seen elements (in reverse order)
rest = [ "/home" "other" ]; # everything else
}
listDfs true hasPrefix [ "/home/user" "other" "/" "/home" "/" ]
== { cycle = "/"; # cycle encountered at this element
loops = [ "/" ]; # and continues to these elements
visited = [ "/" "/home/user" ]; # elements leading to the cycle (in reverse order)
rest = [ "/home" "other" ]; # everything else
*/
listDfs = stopOnCycles: before: list:
let
dfs' = us: visited: rest:
let
c = filter (x: before x us) visited;
b = partition (x: before x us) rest;
in if stopOnCycles && (length c > 0)
then { cycle = us; loops = c; inherit visited rest; }
else if length b.right == 0
then # nothing is before us
{ minimal = us; inherit visited rest; }
else # grab the first one before us and continue
dfs' (head b.right)
([ us ] ++ visited)
(tail b.right ++ b.wrong);
in dfs' (head list) [] (tail list);
/* Sort a list based on a partial ordering using DFS. This
implementation is O(N^2), if your ordering is linear, use `sort`
instead.
`before a b == true` means that `b` should be after `a`
in the result.
Examples:
toposort hasPrefix [ "/home/user" "other" "/" "/home" ]
== { result = [ "/" "/home" "/home/user" "other" ]; }
toposort hasPrefix [ "/home/user" "other" "/" "/home" "/" ]
== { cycle = [ "/home/user" "/" "/" ]; # path leading to a cycle
loops = [ "/" ]; } # loops back to these elements
toposort hasPrefix [ "other" "/home/user" "/home" "/" ]
== { result = [ "other" "/" "/home" "/home/user" ]; }
toposort (a: b: a < b) [ 3 2 1 ] == { result = [ 1 2 3 ]; }
*/
toposort = before: list:
let
dfsthis = listDfs true before list;
toporest = toposort before (dfsthis.visited ++ dfsthis.rest);
in
if length list < 2
then # finish
{ result = list; }
else if dfsthis ? "cycle"
then # there's a cycle, starting from the current vertex, return it
{ cycle = reverseList ([ dfsthis.cycle ] ++ dfsthis.visited);
inherit (dfsthis) loops; }
else if toporest ? "cycle"
then # there's a cycle somewhere else in the graph, return it
toporest
# Slow, but short. Can be made a bit faster with an explicit stack.
else # there are no cycles
{ result = [ dfsthis.minimal ] ++ toporest.result; };
/* Sort a list based on a comparator function which compares two /* Sort a list based on a comparator function which compares two
elements and returns true if the first argument is strictly below elements and returns true if the first argument is strictly below
the second argument. The returned list is sorted in an increasing the second argument. The returned list is sorted in an increasing

View File

@ -2,6 +2,15 @@ pkgs: with pkgs.lib;
rec { rec {
# Check whenever fileSystem is needed for boot
fsNeededForBoot = fs: fs.neededForBoot
|| elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ];
# Check whenever `b` depends on `a` as a fileSystem
# FIXME: it's incorrect to simply use hasPrefix here: "/dev/a" is not a parent of "/dev/ab"
fsBefore = a: b: ((any (x: elem x [ "bind" "move" ]) b.options) && (a.mountPoint == b.device))
|| (hasPrefix a.mountPoint b.mountPoint);
# Escape a path according to the systemd rules, e.g. /dev/xyzzy # Escape a path according to the systemd rules, e.g. /dev/xyzzy
# becomes dev-xyzzy. FIXME: slow. # becomes dev-xyzzy. FIXME: slow.
escapeSystemdPath = s: escapeSystemdPath = s:

View File

@ -12,7 +12,7 @@ let
(fs: (fs.neededForBoot (fs: (fs.neededForBoot
|| elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ]) || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ])
&& fs.fsType == "zfs") && fs.fsType == "zfs")
(attrValues config.fileSystems) != []; config.system.build.fileSystems != [];
# Ascertain whether NixOS container support is required # Ascertain whether NixOS container support is required
containerSupportRequired = containerSupportRequired =

View File

@ -3,7 +3,7 @@
# the modules necessary to mount the root file system, then calls the # the modules necessary to mount the root file system, then calls the
# init in the root file system to start the second boot stage. # init in the root file system to start the second boot stage.
{ config, lib, pkgs, ... }: { config, lib, utils, pkgs, ... }:
with lib; with lib;
@ -23,6 +23,12 @@ let
}; };
# The initrd only has to mount `/` or any FS marked as necessary for
# booting (such as the FS containing `/nix/store`, or an FS needed for
# mounting `/`, like `/` on a loopback).
fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems;
# Some additional utilities needed in stage 1, like mount, lvm, fsck # Some additional utilities needed in stage 1, like mount, lvm, fsck
# etc. We don't want to bring in all of those packages, so we just # etc. We don't want to bring in all of those packages, so we just
# copy what we need. Instead of using statically linked binaries, # copy what we need. Instead of using statically linked binaries,
@ -71,7 +77,7 @@ let
ln -sf kmod $out/bin/modprobe ln -sf kmod $out/bin/modprobe
# Copy resize2fs if needed. # Copy resize2fs if needed.
${optionalString (any (fs: fs.autoResize) (attrValues config.fileSystems)) '' ${optionalString (any (fs: fs.autoResize) fileSystems) ''
# We need mke2fs in the initrd. # We need mke2fs in the initrd.
copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/resize2fs copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/resize2fs
''} ''}
@ -128,21 +134,6 @@ let
''; # */ ''; # */
# The initrd only has to mount / or any FS marked as necessary for
# booting (such as the FS containing /nix/store, or an FS needed for
# mounting /, like / on a loopback).
#
# We need to guarantee that / is the first filesystem in the list so
# that if and when lustrateRoot is invoked, nothing else is mounted
fileSystems = let
filterNeeded = filter
(fs: fs.mountPoint != "/" && (fs.neededForBoot || elem fs.mountPoint [ "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ]));
filterRoot = filter
(fs: fs.mountPoint == "/");
allFileSystems = attrValues config.fileSystems;
in (filterRoot allFileSystems) ++ (filterNeeded allFileSystems);
udevRules = pkgs.stdenv.mkDerivation { udevRules = pkgs.stdenv.mkDerivation {
name = "udev-rules"; name = "udev-rules";
allowedReferences = [ extraUtils ]; allowedReferences = [ extraUtils ];
@ -405,9 +396,8 @@ in
}; };
config = mkIf (!config.boot.isContainer) { config = mkIf (!config.boot.isContainer) {
assertions = [ assertions = [
{ assertion = any (fs: fs.mountPoint == "/") (attrValues config.fileSystems); { assertion = any (fs: fs.mountPoint == "/") fileSystems;
message = "The fileSystems option does not specify your root file system."; message = "The fileSystems option does not specify your root file system.";
} }
{ assertion = let inherit (config.boot) resumeDevice; in { assertion = let inherit (config.boot) resumeDevice; in

View File

@ -3,7 +3,7 @@
with lib; with lib;
let let
fileSystems = attrValues config.fileSystems ++ config.swapDevices; fileSystems = config.system.build.fileSystems ++ config.swapDevices;
encDevs = filter (dev: dev.encrypted.enable) fileSystems; encDevs = filter (dev: dev.encrypted.enable) fileSystems;
keyedEncDevs = filter (dev: dev.encrypted.keyFile != null) encDevs; keyedEncDevs = filter (dev: dev.encrypted.keyFile != null) encDevs;
keylessEncDevs = filter (dev: dev.encrypted.keyFile == null) encDevs; keylessEncDevs = filter (dev: dev.encrypted.keyFile == null) encDevs;

View File

@ -5,7 +5,16 @@ with utils;
let let
fileSystems = attrValues config.fileSystems; fileSystems' = toposort fsBefore (attrValues config.fileSystems);
fileSystems = if fileSystems' ? "result"
then # use topologically sorted fileSystems everywhere
fileSystems'.result
else # the assertion below will catch this,
# but we fall back to the original order
# anyway so that other modules could check
# their assertions too
(attrValues config.fileSystems);
prioOption = prio: optionalString (prio != null) " pri=${toString prio}"; prioOption = prio: optionalString (prio != null) " pri=${toString prio}";
@ -162,6 +171,17 @@ in
config = { config = {
assertions = let
ls = sep: concatMapStringsSep sep (x: x.mountPoint);
in [
{ assertion = ! (fileSystems' ? "cycle");
message = "The fileSystems option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}";
}
];
# Export for use in other modules
system.build.fileSystems = fileSystems;
boot.supportedFilesystems = map (fs: fs.fsType) fileSystems; boot.supportedFilesystems = map (fs: fs.fsType) fileSystems;
# Add the mount helpers to the system path so that `mount' can find them. # Add the mount helpers to the system path so that `mount' can find them.
@ -180,7 +200,7 @@ in
# in your /etc/nixos/configuration.nix file. # in your /etc/nixos/configuration.nix file.
# Filesystems. # Filesystems.
${flip concatMapStrings fileSystems (fs: ${concatMapStrings (fs:
(if fs.device != null then fs.device (if fs.device != null then fs.device
else if fs.label != null then "/dev/disk/by-label/${fs.label}" else if fs.label != null then "/dev/disk/by-label/${fs.label}"
else throw "No device specified for mount point ${fs.mountPoint}.") else throw "No device specified for mount point ${fs.mountPoint}.")
@ -191,7 +211,7 @@ in
+ " " + (if skipCheck fs then "0" else + " " + (if skipCheck fs then "0" else
if fs.mountPoint == "/" then "1" else "2") if fs.mountPoint == "/" then "1" else "2")
+ "\n" + "\n"
)} ) fileSystems}
# Swap devices. # Swap devices.
${flip concatMapStrings config.swapDevices (sw: ${flip concatMapStrings config.swapDevices (sw:
@ -211,14 +231,15 @@ in
formatDevice = fs: formatDevice = fs:
let let
mountPoint' = escapeSystemdPath fs.mountPoint; mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount";
device' = escapeSystemdPath fs.device; device' = escapeSystemdPath fs.device;
device'' = "${device}.device";
in nameValuePair "mkfs-${device'}" in nameValuePair "mkfs-${device'}"
{ description = "Initialisation of Filesystem ${fs.device}"; { description = "Initialisation of Filesystem ${fs.device}";
wantedBy = [ "${mountPoint'}.mount" ]; wantedBy = [ mountPoint' ];
before = [ "${mountPoint'}.mount" "systemd-fsck@${device'}.service" ]; before = [ mountPoint' "systemd-fsck@${device'}.service" ];
requires = [ "${device'}.device" ]; requires = [ device'' ];
after = [ "${device'}.device" ]; after = [ device'' ];
path = [ pkgs.utillinux ] ++ config.system.fsPackages; path = [ pkgs.utillinux ] ++ config.system.fsPackages;
script = script =
'' ''

View File

@ -36,13 +36,11 @@ let
fsToPool = fs: datasetToPool fs.device; fsToPool = fs: datasetToPool fs.device;
zfsFilesystems = filter (x: x.fsType == "zfs") (attrValues config.fileSystems); zfsFilesystems = filter (x: x.fsType == "zfs") config.system.build.fileSystems;
isRoot = fs: fs.neededForBoot || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ];
allPools = unique ((map fsToPool zfsFilesystems) ++ cfgZfs.extraPools); allPools = unique ((map fsToPool zfsFilesystems) ++ cfgZfs.extraPools);
rootPools = unique (map fsToPool (filter isRoot zfsFilesystems)); rootPools = unique (map fsToPool (filter fsNeededForBoot zfsFilesystems));
dataPools = unique (filter (pool: !(elem pool rootPools)) allPools); dataPools = unique (filter (pool: !(elem pool rootPools)) allPools);
@ -277,7 +275,7 @@ in
systemd.services = let systemd.services = let
getPoolFilesystems = pool: getPoolFilesystems = pool:
filter (x: x.fsType == "zfs" && (fsToPool x) == pool) (attrValues config.fileSystems); filter (x: x.fsType == "zfs" && (fsToPool x) == pool) config.system.build.fileSystems;
getPoolMounts = pool: getPoolMounts = pool:
let let