diff --git a/example/zfs-with-vdevs.nix b/example/zfs-with-vdevs.nix index 2161f5cb..985e082c 100644 --- a/example/zfs-with-vdevs.nix +++ b/example/zfs-with-vdevs.nix @@ -1,9 +1,9 @@ { disko.devices = { disk = { - x = { + data1 = { type = "disk"; - device = "/dev/sdx"; + device = "/dev/vda"; content = { type = "gpt"; partitions = { @@ -27,9 +27,9 @@ }; }; }; - y = { + data2 = { type = "disk"; - device = "/dev/sdy"; + device = "/dev/vdb"; content = { type = "gpt"; partitions = { @@ -43,9 +43,169 @@ }; }; }; - z = { + data3 = { type = "disk"; - device = "/dev/sdz"; + device = "/dev/vdc"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + spare = { + type = "disk"; + device = "/dev/vdd"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + log1 = { + type = "disk"; + device = "/dev/vde"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + log2 = { + type = "disk"; + device = "/dev/vdf"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + log3 = { + type = "disk"; + device = "/dev/vdg"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + dedup1 = { + type = "disk"; + device = "/dev/vdh"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + dedup2 = { + type = "disk"; + device = "/dev/vdi"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + dedup3 = { + type = "disk"; + device = "/dev/vdj"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + special1 = { + type = "disk"; + device = "/dev/vdk"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + special2 = { + type = "disk"; + device = "/dev/vdl"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + special3 = { + type = "disk"; + device = "/dev/vdm"; content = { type = "gpt"; partitions = { @@ -61,7 +221,7 @@ }; cache = { type = "disk"; - device = "/dev/vdc"; + device = "/dev/vdn"; content = { type = "gpt"; partitions = { @@ -85,15 +245,40 @@ vdev = [ { mode = "mirror"; - members = [ - "x" - "y" - ]; + members = [ "data1" "data2" ]; + } + { + members = [ "data3" ]; + } + ]; + spare = [ "spare" ]; + log = [ + { + mode = "mirror"; + members = [ "log1" "log2" ]; + } + { + members = [ "log3" ]; + } + ]; + dedup = [ + { + mode = "mirror"; + members = [ "dedup1" "dedup2" ]; + } + { + members = [ "dedup3" ]; + } + ]; + special = [ + { + mode = "mirror"; + members = [ "special1" "special2" ]; + } + { + members = [ "special3" ]; } ]; - special = { - members = [ "z" ]; - }; cache = [ "cache" ]; }; }; diff --git a/lib/tests.nix b/lib/tests.nix index 25b6a593..2d7fcd3b 100644 --- a/lib/tests.nix +++ b/lib/tests.nix @@ -38,7 +38,23 @@ let }; # list of devices generated inside qemu - devices = [ "/dev/vda" "/dev/vdb" "/dev/vdc" "/dev/vdd" "/dev/vde" "/dev/vdf" ]; + devices = [ + "/dev/vda" + "/dev/vdb" + "/dev/vdc" + "/dev/vdd" + "/dev/vde" + "/dev/vdf" + "/dev/vdg" + "/dev/vdh" + "/dev/vdi" + "/dev/vdj" + "/dev/vdk" + "/dev/vdl" + "/dev/vdm" + "/dev/vdn" + "/dev/vdo" + ]; # This is the test generator for a disko test makeDiskoTest = diff --git a/lib/types/zpool.nix b/lib/types/zpool.nix index 46bab779..c36acbfa 100644 --- a/lib/types/zpool.nix +++ b/lib/types/zpool.nix @@ -1,6 +1,6 @@ { config, options, lib, diskoLib, rootMountPoint, ... }: let - # TODO: Consider expanding to handle `disk` `file` and `draid` mode options. + # TODO: Consider expanding to handle `file` and `draid` mode options. modeOptions = [ "" "mirror" @@ -63,19 +63,79 @@ in https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#Virtual_Devices_(vdevs) for details. ''; - example = [{ - mode = "mirror"; - members = [ "x" "y" "/dev/sda1" ]; - }]; + example = [ + { + mode = "mirror"; + members = [ "x" "y" ]; + } + { + members = [ "z" ]; + } + ]; + }; + spare = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + A list of devices to use as hot spares. See + https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#Hot_Spares + for details. + ''; + example = [ "x" "y" ]; + }; + log = lib.mkOption { + type = lib.types.listOf vdev; + default = [ ]; + description = '' + A list of vdevs used for the zfs intent log (ZIL). See + https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#Intent_Log + for details. + ''; + example = [ + { + mode = "mirror"; + members = [ "x" "y" ]; + } + { + members = [ "z" ]; + } + ]; + }; + dedup = lib.mkOption { + type = lib.types.listOf vdev; + default = [ ]; + description = '' + A list of vdevs used for the deduplication table. See + https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#dedup + for details. + ''; + example = [ + { + mode = "mirror"; + members = [ "x" "y" ]; + } + { + members = [ "z" ]; + } + ]; }; special = lib.mkOption { - type = lib.types.nullOr vdev; - default = null; + type = lib.types.either (lib.types.listOf vdev) (lib.types.nullOr vdev); + default = [ ]; description = '' - A vdev definition for a special device. See + A list of vdevs used as special devices. See https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#special for details. ''; + example = [ + { + mode = "mirror"; + members = [ "x" "y" ]; + } + { + members = [ "z" ]; + } + ]; }; cache = lib.mkOption { type = lib.types.listOf lib.types.str; @@ -85,8 +145,8 @@ in https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#Cache_Devices for details. ''; + example = [ "x" "y" ]; }; - # TODO: Consider supporting log, spare, and dedup options. }; }); }; @@ -134,13 +194,16 @@ in inherit config options; default = let - formatOutput = mode: members: '' - entries+=("${mode}=${ + formatOutput = type: mode: members: '' + entries+=("${type} ${mode}=${ lib.concatMapStringsSep " " (d: if lib.strings.hasPrefix "/" d then d else "/dev/disk/by-partlabel/disk-${d}-zfs") members }") ''; - formatVdev = vdev: formatOutput vdev.mode vdev.members; + formatVdev = type: vdev: formatOutput type vdev.mode vdev.members; + formatVdevList = type: vdevs: lib.concatMapStrings + (formatVdev type) + (builtins.sort (a: _: a.mode == "") vdevs); hasTopology = !(builtins.isString config.mode); mode = if hasTopology then "prescribed" else config.mode; topology = lib.optionalAttrs hasTopology config.mode.topology; @@ -181,18 +244,31 @@ in else entries=() ${lib.optionalString (hasTopology && topology.vdev != null) - (lib.concatMapStrings formatVdev topology.vdev)} - ${lib.optionalString (hasTopology && topology.special != null) - (formatOutput "special ${topology.special.mode}" topology.special.members)} + (formatVdevList "" topology.vdev)} + ${lib.optionalString (hasTopology && topology.spare != []) + (formatOutput "spare" "" topology.spare)} + ${lib.optionalString (hasTopology && topology.log != []) + (formatVdevList "log" topology.log)} + ${lib.optionalString (hasTopology && topology.dedup != []) + (formatVdevList "dedup" topology.dedup)} + ${lib.optionalString (hasTopology && topology.special != null && topology.special != []) + (formatVdevList "special" (lib.lists.toList topology.special))} ${lib.optionalString (hasTopology && topology.cache != []) - (formatOutput "cache" topology.cache)} + (formatOutput "cache" "" topology.cache)} all_devices=() + last_type= for line in "''${entries[@]}"; do - # lineformat is mode=device1 device2 device3 - mode=''${line%%=*} - devs=''${line#*=} + # lineformat is type mode=device1 device2 device3 + mode="''${line%%=*}" + type="''${mode%% *}" + mode="''${mode#"$type "}" + devs="''${line#*=}" IFS=' ' read -r -a devices <<< "$devs" all_devices+=("''${devices[@]}") + if ! [ "$type" = "$last_type" ]; then + topology+=" $type" + last_type="$type" + fi topology+=" ''${mode} ''${devices[*]}" done # all_devices sorted should equal zfs_devices sorted diff --git a/tests/zfs-with-vdevs.nix b/tests/zfs-with-vdevs.nix index da301a01..704a1733 100644 --- a/tests/zfs-with-vdevs.nix +++ b/tests/zfs-with-vdevs.nix @@ -38,5 +38,40 @@ diskoLib.testLib.makeDiskoTest { assert_property("zroot/zfs_fs", "com.sun:auto-snapshot", "true") assert_property("zroot/zfs_fs", "compression", "zstd") machine.succeed("mountpoint /zfs_fs"); + + # Take the status output and flatten it so that each device is on a single line prefixed with with the group (either + # the pool name or a designation like log/cache/spare/dedup/special) and first portion of the vdev name (empty for a + # disk from a single vdev, mirror for devices in a mirror. This makes it easy to verify that the layout is as + # expected. + group = "" + vdev = "" + actual = [] + for line in machine.succeed("zpool status -P zroot").split("\n"): + first_word = line.strip().split(" ", 1)[0] + if line.startswith("\t ") and first_word.startswith("/"): + actual.append(f"{group}{vdev}{first_word}") + elif line.startswith("\t "): + vdev = f"{first_word.split('-', 1)[0]} " + elif line.startswith("\t"): + group = f"{first_word} " + vdev = "" + actual.sort() + expected=sorted([ + 'zroot /dev/disk/by-partlabel/disk-data3-zfs', + 'zroot mirror /dev/disk/by-partlabel/disk-data1-zfs', + 'zroot mirror /dev/disk/by-partlabel/disk-data2-zfs', + 'dedup /dev/disk/by-partlabel/disk-dedup3-zfs', + 'dedup mirror /dev/disk/by-partlabel/disk-dedup1-zfs', + 'dedup mirror /dev/disk/by-partlabel/disk-dedup2-zfs', + 'special /dev/disk/by-partlabel/disk-special3-zfs', + 'special mirror /dev/disk/by-partlabel/disk-special1-zfs', + 'special mirror /dev/disk/by-partlabel/disk-special2-zfs', + 'logs /dev/disk/by-partlabel/disk-log3-zfs', + 'logs mirror /dev/disk/by-partlabel/disk-log1-zfs', + 'logs mirror /dev/disk/by-partlabel/disk-log2-zfs', + 'cache /dev/disk/by-partlabel/disk-cache-zfs', + 'spares /dev/disk/by-partlabel/disk-spare-zfs', + ]) + assert actual == expected, f"Incorrect pool layout. Expected:\n\t{'\n\t'.join(expected)}\nActual:\n\t{'\n\t'.join(actual)}" ''; }