From dcb0c42857a2ada8754d4514d4b8c4c1d67ab3f5 Mon Sep 17 00:00:00 2001 From: lassulus Date: Mon, 29 Aug 2022 11:45:19 +0200 Subject: [PATCH] reimplement disko using the nixos type system This should make the code cleaner, more robust and errors should be clearer. we also changed the configuration format a bit. --- default.nix | 328 +---------- example/btrfs-subvolumes.nix | 43 +- example/complex.nix | 194 +++++++ example/luks-lvm.nix | 132 ++--- example/lvm-raid.nix | 114 ++-- example/mdadm.nix | 72 ++- example/zfs-over-legacy.nix | 63 +- example/zfs.nix | 53 +- tests/complex.nix | 20 + tests/lib.nix | 2 +- types.nix | 1058 ++++++++++++++++++++++++++++++++++ 11 files changed, 1545 insertions(+), 534 deletions(-) create mode 100644 example/complex.nix create mode 100644 tests/complex.nix create mode 100644 types.nix diff --git a/default.nix b/default.nix index 51b70f5..9febd80 100644 --- a/default.nix +++ b/default.nix @@ -1,312 +1,22 @@ -{ lib ? import }: -with lib; -with builtins; - +{ lib ? import +, pkgs ? import {} +}: let - - helper.find-device = device: - let - environment = helper.device-id device; - in - # DEVICE points already to /dev/disk, so we don't handle it via /dev/disk/by-path - if hasPrefix "/dev/disk" device then - "${environment}='${device}'" - else '' - ${environment}=$(for x in $(find /dev/disk/{by-path,by-id}/); do - dev=$x - if [ "$(readlink -f $x)" = "$(readlink -f '${device}')" ]; then - target=$dev - break - fi - done - if test -z ''${target+x}; then - echo 'unable to find path of disk: ${device}, bailing out' >&2 - exit 1 - else - echo $target - fi) - ''; - - helper.device-id = device: "DEVICE${builtins.substring 0 5 (builtins.hashString "sha1" device)}"; - - config-f = q: x: config.${x.type} q x; - - config.filesystem = q: x: { - fileSystems.${x.mountpoint} = { - device = q.device; - fsType = x.format; - ${if x ? options then "options" else null} = x.options; - }; - }; - - config.zfs_filesystem = q: x: { - fileSystems.${x.mountpoint} = { - device = q.device; - fsType = "zfs"; - }; - }; - - config.devices = q: x: - foldl' recursiveUpdate { } (mapAttrsToList (name: config-f { device = "/dev/${name}"; }) x.content); - - config.luks = q: x: { - boot.initrd.luks.devices.${x.name}.device = q.device; - } // config-f { device = "/dev/mapper/${x.name}"; } x.content; - - config.lvm_lv = q: x: - config-f { device = "/dev/${q.vgname}/${q.name}"; } x.content; - - config.lvm_vg = q: x: - foldl' recursiveUpdate { } (mapAttrsToList (name: config-f { inherit name; vgname = x.name; }) x.lvs); - - config.noop = q: x: { }; - - config.partition = q: x: - config-f { device = q.device + toString q.index; } x.content; - - config.table = q: x: - foldl' recursiveUpdate { } (imap (index: config-f (q // { inherit index; })) x.partitions); - - - create-f = q: x: create.${x.type} q x; - - create.btrfs = q: x: '' - mkfs.btrfs ${q.device} - ${lib.optionalString (!isNull x.subvolumes or null) '' - MNTPOINT=$(mktemp -d) - ( - mount ${q.device} "$MNTPOINT" - trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT - ${concatMapStringsSep "\n" (subvolume: "btrfs subvolume create \"$MNTPOINT\"/${subvolume}") x.subvolumes} - ) - ''} - ''; - - create.filesystem = q: x: '' - mkfs.${x.format} \ - ${lib.optionalString (!isNull x.extraArgs or null) x.extraArgs} \ - ${q.device} - ''; - - create.devices = q: x: - let - raid-devices = lib.filterAttrs (_: dev: dev.type == "mdadm" || dev.type == "zpool" || dev.type == "lvm_vg") x.content; - other-devices = lib.filterAttrs (_: dev: dev.type != "mdadm" && dev.type != "zpool" && dev.type != "lvm_vg") x.content; - in - '' - ${concatStrings (mapAttrsToList (name: create-f { device = "/dev/${name}"; }) other-devices)} - ${concatStrings (mapAttrsToList (name: create-f { device = "/dev/${name}"; name = name; }) raid-devices)} - ''; - - create.mdraid = q: x: '' - RAIDDEVICES_N_${x.name}=$((''${RAIDDEVICES_N_${x.name}:-0}+1)) - RAIDDEVICES_${x.name}="''${RAIDDEVICES_${x.name}:-}${q.device} " - ''; - - create.mdadm = q: x: '' - echo 'y' | mdadm --create /dev/md/${q.name} --level=${toString x.level or 1} --raid-devices=''${RAIDDEVICES_N_${q.name}} ''${RAIDDEVICES_${q.name}} - udevadm trigger --subsystem-match=block; udevadm settle - ${create-f { device = "/dev/md/${q.name}"; } x.content} - ''; - - create.luks = q: x: '' - cryptsetup -q luksFormat ${q.device} ${if builtins.hasAttr "keyfile" x then x.keyfile else ""} ${toString (x.extraArgs or [])} - cryptsetup luksOpen ${q.device} ${x.name} ${if builtins.hasAttr "keyfile" x then "--key-file " + x.keyfile else ""} - ${create-f { device = "/dev/mapper/${x.name}"; } x.content} - ''; - - create.lvm_pv = q: x: '' - pvcreate ${q.device} - LVMDEVICES_${x.vg}="''${LVMDEVICES_${x.vg}:-}${q.device} " - ''; - - create.lvm_lv = q: x: '' - lvcreate \ - ${if hasInfix "%" x.size then "-l" else "-L"} ${x.size} \ - -n ${q.name} \ - ${lib.optionalString (!isNull x.lvm_type or null) "--type=${x.lvm_type}"} \ - ${lib.optionalString (!isNull x.extraArgs or null) x.extraArgs} \ - ${q.vgname} - ${create-f { device = "/dev/${q.vgname}/${q.name}"; } x.content} - ''; - - create.lvm_vg = q: x: '' - vgcreate ${q.name} $LVMDEVICES_${q.name} - ${concatStrings (mapAttrsToList (name: create-f { inherit name; vgname = q.name; }) x.lvs)} - ''; - - create.noop = q: x: ""; - - create.partition = q: x: - let - env = helper.device-id q.device; - in - '' - parted -s "''${${env}}" mkpart ${x.part-type} ${x.fs-type or ""} ${x.start} ${x.end} - # ensure /dev/disk/by-path/..-partN exists before continuing - udevadm trigger --subsystem-match=block; udevadm settle - ${optionalString (x.bootable or false) '' - parted -s "''${${env}}" set ${toString q.index} boot on - ''} - ${concatMapStringsSep "" (flag: '' - parted -s "''${${env}}" set ${toString q.index} ${flag} on - '') (x.flags or [])} - ${create-f { device = "\"\${${env}}\"-part" + toString q.index; } x.content} - ''; - - create.table = q: x: '' - ${helper.find-device q.device} - parted -s "''${${helper.device-id q.device}}" mklabel ${x.format} - ${concatStrings (imap (index: create-f (q // { inherit index; })) x.partitions)} - ''; - - create.zfs = q: x: '' - ZFSDEVICES_${x.pool}="''${ZFSDEVICES_${x.pool}:-}${q.device} " - ''; - - create.zfs_filesystem = q: x: '' - zfs create ${q.pool}/${x.name} \ - ${lib.optionalString (isAttrs x.options or null) (concatStringsSep " " (mapAttrsToList (n: v: "-o ${n}=${v}") x.options))} - ''; - - create.zfs_volume = q: x: '' - zfs create ${q.pool}/${x.name} \ - -V ${x.size} \ - ${lib.optionalString (isAttrs x.options or null) (concatStringsSep " " (mapAttrsToList (n: v: "-o ${n}=${v}") x.options))} - udevadm trigger --subsystem-match=block; udevadm settle - ${create-f { device = "/dev/zvol/${q.pool}/${x.name}"; } x.content} - ''; - - create.zpool = q: x: '' - zpool create ${q.name} \ - ${lib.optionalString (!isNull (x.mode or null) && x.mode != "stripe") x.mode} \ - ${lib.optionalString (isAttrs x.options or null) (concatStringsSep " " (mapAttrsToList (n: v: "-o ${n}=${v}") x.options))} \ - ${lib.optionalString (isAttrs x.rootFsOptions or null) (concatStringsSep " " (mapAttrsToList (n: v: "-O ${n}=${v}") x.rootFsOptions))} \ - ''${ZFSDEVICES_${q.name}} - ${concatMapStrings (create-f (q // { pool = q.name; })) x.datasets} - ''; - - - mount-f = q: x: mount.${x.type} q x; - - mount.filesystem = q: x: { - fs.${x.mountpoint} = '' - if ! findmnt ${q.device} "/mnt${x.mountpoint}" > /dev/null 2>&1; then - mount ${q.device} "/mnt${x.mountpoint}" \ - -o X-mount.mkdir \ - ${lib.optionalString (isList x.mountOptions or null) (concatStringsSep " " x.mountOptions)} - fi - ''; - }; - - mount.zfs_filesystem = q: x: - optionalAttrs ((x.options.mountpoint or "") != "none") - (mount.filesystem (q // { device = q.dataset; }) (x // { mountOptions = [ - (lib.optionalString ((x.options.mountpoint or "") != "legacy") "-o zfsutil") - "-t zfs" - ]; })); - - mount.btrfs = mount.filesystem; - - mount.devices = q: x: - let - z = foldl' recursiveUpdate { } (mapAttrsToList (name: mount-f { device = "/dev/${name}"; inherit name; }) x.content); - # attrValues returns values sorted by name. This is important, because it - # ensures that "/" is processed before "/foo" etc. - in - '' - ${optionalString (hasAttr "table" z) (concatStringsSep "\n" (attrValues z.table))} - ${optionalString (hasAttr "luks" z) (concatStringsSep "\n" (attrValues z.luks))} - ${optionalString (hasAttr "lvm" z) (concatStringsSep "\n" (attrValues z.lvm))} - ${optionalString (hasAttr "zpool" z) (concatStringsSep "\n" (attrValues z.zpool))} - ${optionalString (hasAttr "zfs" z) (concatStringsSep "\n" (attrValues z.zfs))} - ${optionalString (hasAttr "fs" z) (concatStringsSep "\n" (attrValues z.fs))} - ''; - - mount.luks = q: x: ( - recursiveUpdate - (mount-f { device = "/dev/mapper/${x.name}"; } x.content) - { - luks.${q.device} = '' - cryptsetup status ${x.name} >/dev/null 2>/dev/null || cryptsetup luksOpen ${q.device} ${x.name} ${if builtins.hasAttr "keyfile" x then "--key-file " + x.keyfile else ""} - ''; - } - ); - - mount.lvm_lv = q: x: - mount-f { device = "/dev/${q.vgname}/${q.name}"; } x.content; - - mount.lvm_vg = q: x: ( - recursiveUpdate - (foldl' recursiveUpdate { } (mapAttrsToList (name: mount-f { inherit name; vgname = q.name; }) x.lvs)) - { - lvm.${q.device} = '' - vgchange -a y - ''; - } - ); - - mount.lvm_pv = mount.noop; - - mount.noop = q: x: { }; - - mount.mdadm = q: x: - mount-f { device = "/dev/md/${q.name}"; } x.content; - mount.mdraid = mount.noop; - - mount.partition = q: x: - mount-f { device = "\"\${${q.device}}\"-part" + toString q.index; } x.content; - - mount.table = q: x: ( - recursiveUpdate - (foldl' recursiveUpdate { } (imap (index: mount-f (q // { inherit index; device = helper.device-id q.device; })) x.partitions)) - { table.${q.device} = helper.find-device q.device; } - ); - - mount.zfs = mount.noop; - - mount.zpool = q: x: - let - datasets = [{ - inherit (q) name; - type = "zfs_filesystem"; - dataset = q.name; - mountpoint = x.mountpoint or "/${q.name}"; - options = q.rootFsOptions or { }; - }] ++ x.datasets; - in - recursiveUpdate - (foldl' recursiveUpdate { } - ( - (map - (x: mount-f - ({ - dataset = x.dataset or "${q.name}/${x.name}"; - mountpoint = x.mountpoint or "/${q.name}/${x.name}"; - } // q) - x) - datasets) - ) - ) - { - zpool.${q.device} = '' - zpool list '${q.name}' >/dev/null 2>/dev/null || zpool import '${q.name}' - ''; + types = import ./types.nix { inherit pkgs; }; + eval = cfg: lib.evalModules { + modules = lib.singleton { + # _file = toString input; + imports = lib.singleton { topLevel.devices = cfg; }; + options = { + topLevel = lib.mkOption { + type = types.topLevel; + }; }; - - mount.zfs_volume = q: x: - mount-f { device = "/dev/zvol/${q.dataset}"; } x.content; - -in -{ - config = config-f { }; - create = cfg: '' - set -efux - ${create-f {} cfg} - ''; - mount = cfg: '' - set -efux - ${mount-f {} cfg} - ''; - + }; + }; +in { + types = types; + create = cfg: (eval cfg).config.topLevel.create; + mount = cfg: (eval cfg).config.topLevel.mount; + config = cfg: (eval cfg).config.topLevel.config; } diff --git a/example/btrfs-subvolumes.nix b/example/btrfs-subvolumes.nix index 0124c86..83ea71e 100644 --- a/example/btrfs-subvolumes.nix +++ b/example/btrfs-subvolumes.nix @@ -1,25 +1,28 @@ { - type = "devices"; - content = { + disk = { vdb = { - type = "table"; - format = "gpt"; - partitions = [ - { - type = "partition"; - part-type = "primary"; - start = "0%"; - end = "100%"; - content = { - type = "btrfs"; - mountpoint = "/"; - subvolumes = [ - "/home" - "/test" - ]; - }; - } - ]; + type = "disk"; + device = "/dev/vdb"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + name = "root"; + type = "partition"; + start = "0%"; + end = "100%"; + content = { + type = "btrfs"; + mountpoint = "/"; + subvolumes = [ + "/home" + "/test" + ]; + }; + } + ]; + }; }; }; } diff --git a/example/complex.nix b/example/complex.nix new file mode 100644 index 0000000..078f9c5 --- /dev/null +++ b/example/complex.nix @@ -0,0 +1,194 @@ +{ + disk = { + disk1 = { + type = "disk"; + device = "/dev/vdb"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + type = "partition"; + start = "0"; + end = "1M"; + name = "grub"; + flags = ["bios_grub"]; + } + { + type = "partition"; + start = "1M"; + end = "100%"; + name = "luks"; + bootable = true; + content = { + type = "luks"; + name = "crypted1"; + keyFile = "/tmp/secret.key"; + extraArgs = [ + "--hash sha512" + "--iter-time 5000" + ]; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + } + ]; + }; + }; + disk2 = { + type = "disk"; + device = "/dev/vdc"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + type = "partition"; + start = "0"; + end = "1M"; + name = "grub"; + flags = ["bios_grub"]; + } + { + type = "partition"; + start = "1M"; + end = "100%"; + name = "luks"; + bootable = true; + content = { + type = "luks"; + name = "crypted2"; + keyFile = "/tmp/secret.key"; + extraArgs = [ + "--hash sha512" + "--iter-time 5000" + ]; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + } + ]; + }; + }; + }; + mdadm = { + raid1 = { + type = "mdadm"; + level = 1; + content = { + type = "table"; + format = "msdos"; + partitions = [ + { + type = "partition"; + name = "xfs"; + start = "1MiB"; + end = "100%"; + content = { + type = "filesystem"; + format = "xfs"; + mountpoint = "/xfs_mdadm_lvm"; + }; + } + ]; + }; + }; + }; + lvm_vg = { + pool = { + type = "lvm_vg"; + lvs = { + root = { + type = "lvm_lv"; + size = "10M"; + lvm_type = "mirror"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/ext4_on_lvm"; + options = [ + "defaults" + ]; + }; + }; + raid1 = { + type = "lvm_lv"; + size = "30M"; + lvm_type = "raid0"; + content = { + type = "mdraid"; + name = "raid1"; + }; + }; + raid2 = { + type = "lvm_lv"; + size = "30M"; + lvm_type = "raid0"; + content = { + type = "mdraid"; + name = "raid1"; + }; + }; + zfs1 = { + type = "lvm_lv"; + size = "128M"; + lvm_type = "raid0"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + zfs2 = { + type = "lvm_lv"; + size = "128M"; + lvm_type = "raid0"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + zpool = { + zroot = { + type = "zpool"; + mode = "mirror"; + rootFsOptions = { + compression = "lz4"; + "com.sun:auto-snapshot" = "false"; + }; + mountpoint = "/"; + + datasets = { + zfs_fs = { + zfs_type = "filesystem"; + mountpoint = "/zfs_fs"; + options."com.sun:auto-snapshot" = "true"; + }; + zfs_unmounted_fs = { + zfs_type = "filesystem"; + options.mountpoint = "none"; + }; + zfs_legacy_fs = { + zfs_type = "filesystem"; + options.mountpoint = "legacy"; + mountpoint = "/zfs_legacy_fs"; + }; + zfs_testvolume = { + zfs_type = "volume"; + size = "10M"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/ext4onzfs"; + }; + }; + }; + }; + }; +} diff --git a/example/luks-lvm.nix b/example/luks-lvm.nix index 22c029e..17e18a2 100644 --- a/example/luks-lvm.nix +++ b/example/luks-lvm.nix @@ -1,81 +1,81 @@ { - type = "devices"; - content = { + disk = { vdb = { - type = "table"; - format = "gpt"; - partitions = [ - { - type = "partition"; - part-type = "ESP"; - start = "1MiB"; - end = "100MiB"; - fs-type = "FAT32"; - bootable = true; - content = { - type = "filesystem"; - format = "vfat"; - mountpoint = "/boot"; - options = [ - "defaults" - ]; - }; - } - { - type = "partition"; - part-type = "primary"; - start = "100MiB"; - end = "100%"; - content = { - type = "luks"; - algo = "aes-xts..."; - name = "crypted"; - keyfile = "/tmp/secret.key"; - extraArgs = [ - "--hash sha512" - "--iter-time 5000" - ]; - content = { - type = "lvm_pv"; - vg = "pool"; - }; - }; - } - ]; - }; - pool = { - type = "lvm_vg"; - lvs = { - root = { - type = "lvm_lv"; - size = "100M"; - mountpoint = "/"; + type = "disk"; + device = "/dev/vdb"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + type = "partition"; + name = "ESP"; + # fs-type = "FAT32"; + start = "1MiB"; + end = "100MiB"; + bootable = true; content = { type = "filesystem"; - format = "ext4"; - mountpoint = "/"; + format = "vfat"; + mountpoint = "/boot"; options = [ "defaults" ]; }; - }; - home = { - type = "lvm_lv"; - size = "10M"; + } + { + type = "partition"; + name = "luks"; + start = "100MiB"; + end = "100%"; content = { - type = "filesystem"; - format = "ext4"; - mountpoint = "/home"; - }; - }; - raw = { - type = "lvm_lv"; - size = "10M"; - content = { - type = "noop"; + type = "luks"; + name = "crypted"; + keyFile = "/tmp/secret.key"; + extraArgs = [ + "--hash sha512" + "--iter-time 5000" + ]; + content = { + type = "lvm_pv"; + vg = "pool"; + }; }; + } + ]; + }; + }; + }; + lvm_vg = { + pool = { + type = "lvm_vg"; + lvs = { + root = { + type = "lvm_lv"; + size = "100M"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + options = [ + "defaults" + ]; }; }; + home = { + type = "lvm_lv"; + size = "10M"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/home"; + }; + }; + raw = { + type = "lvm_lv"; + size = "10M"; + }; + }; }; }; } diff --git a/example/lvm-raid.nix b/example/lvm-raid.nix index 48930ec..3c5ee69 100644 --- a/example/lvm-raid.nix +++ b/example/lvm-raid.nix @@ -1,66 +1,74 @@ { - type = "devices"; - content = { + disk = { vdb = { - type = "table"; - format = "gpt"; - partitions = [ - { - type = "partition"; - part-type = "primary"; - start = "0%"; - end = "100%"; - content = { - type = "lvm_pv"; - vg = "pool"; - }; - } - ]; + type = "disk"; + device = "/dev/vdb"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + type = "partition"; + name = "primary"; + start = "0%"; + end = "100%"; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + } + ]; + }; }; vdc = { - type = "table"; - format = "gpt"; - partitions = [ - { - type = "partition"; - part-type = "primary"; - start = "0%"; - end = "100%"; - content = { - type = "lvm_pv"; - vg = "pool"; - }; - } - ]; + type = "disk"; + device = "/dev/vdc"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + type = "partition"; + name = "primary"; + start = "0%"; + end = "100%"; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + } + ]; + }; }; + }; + lvm_vg = { pool = { type = "lvm_vg"; - lvs = { - root = { - type = "lvm_lv"; - size = "100M"; + lvs = { + root = { + type = "lvm_lv"; + size = "100M"; + lvm_type = "mirror"; + content = { + type = "filesystem"; + format = "ext4"; mountpoint = "/"; - lvm_type = "mirror"; - content = { - type = "filesystem"; - format = "ext4"; - mountpoint = "/"; - options = [ - "defaults" - ]; - }; - }; - home = { - type = "lvm_lv"; - size = "10M"; - lvm_type = "raid0"; - content = { - type = "filesystem"; - format = "ext4"; - mountpoint = "/home"; - }; + options = [ + "defaults" + ]; }; }; + home = { + type = "lvm_lv"; + size = "10M"; + lvm_type = "raid0"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/home"; + }; + }; + }; }; }; } diff --git a/example/mdadm.nix b/example/mdadm.nix index cb0ad91..094499d 100644 --- a/example/mdadm.nix +++ b/example/mdadm.nix @@ -1,39 +1,47 @@ -# usage: nix-instantiate --eval --json --strict example/config.nix | jq . { - type = "devices"; - content = { + disk = { vdb = { - type = "table"; - format = "gpt"; - partitions = [ - { - type = "partition"; - part-type = "primary"; - start = "1MiB"; - end = "100%"; - content = { - type = "mdraid"; - name = "raid1"; - }; - } - ]; + type = "disk"; + device = "/dev/vdb"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + type = "partition"; + name = "mdadm"; + start = "1MiB"; + end = "100%"; + content = { + type = "mdraid"; + name = "raid1"; + }; + } + ]; + }; }; vdc = { - type = "table"; - format = "gpt"; - partitions = [ - { - type = "partition"; - part-type = "primary"; - start = "1MiB"; - end = "100%"; - content = { - type = "mdraid"; - name = "raid1"; - }; - } - ]; + type = "disk"; + device = "/dev/vdc"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + type = "partition"; + name = "mdadm"; + start = "1MiB"; + end = "100%"; + content = { + type = "mdraid"; + name = "raid1"; + }; + } + ]; + }; }; + }; + mdadm = { raid1 = { type = "mdadm"; level = 1; @@ -43,7 +51,7 @@ partitions = [ { type = "partition"; - part-type = "primary"; + name = "primary"; start = "1MiB"; end = "100%"; content = { diff --git a/example/zfs-over-legacy.nix b/example/zfs-over-legacy.nix index 8f5a8bc..81a5975 100644 --- a/example/zfs-over-legacy.nix +++ b/example/zfs-over-legacy.nix @@ -1,41 +1,46 @@ { - type = "devices"; - content = { + disk = { vdb = { - type = "table"; - format = "gpt"; - partitions = [ - { - type = "partition"; - # leave space for the grub aka BIOS boot - start = "0%"; - end = "100%"; - part-type = "primary"; - bootable = true; - content = { - type = "filesystem"; - format = "ext4"; - mountpoint = "/"; - }; - } - ]; + type = "disk"; + device = "/dev/vdb"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + type = "partition"; + start = "0%"; + end = "100%"; + name = "primary"; + bootable = true; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + } + ]; + }; }; vdc = { - type = "zfs"; - pool = "zroot"; + type = "disk"; + device = "/dev/vdc"; + content = { + type = "zfs"; + pool = "zroot"; + }; }; + }; + zpool = { zroot = { type = "zpool"; - mountpoint = "/"; - - datasets = [ - { - type = "zfs_filesystem"; - name = "zfs_fs"; + datasets = { + zfs_fs = { + zfs_type = "filesystem"; mountpoint = "/zfs_fs"; options."com.sun:auto-snapshot" = "true"; - } - ]; + }; + }; }; }; } diff --git a/example/zfs.nix b/example/zfs.nix index 60e4700..16da367 100644 --- a/example/zfs.nix +++ b/example/zfs.nix @@ -1,14 +1,23 @@ { - type = "devices"; - content = { + disk = { vdb = { - type = "zfs"; - pool = "zroot"; + type = "disk"; + device = "/dev/vdb"; + content = { + type = "zfs"; + pool = "zroot"; + }; }; vdc = { - type = "zfs"; - pool = "zroot"; + type = "disk"; + device = "/dev/vdc"; + content = { + type = "zfs"; + pool = "zroot"; + }; }; + }; + zpool = { zroot = { type = "zpool"; mode = "mirror"; @@ -18,35 +27,31 @@ }; mountpoint = "/"; - datasets = [ - { - type = "zfs_filesystem"; - name = "zfs_fs"; + datasets = { + zfs_fs = { + zfs_type = "filesystem"; mountpoint = "/zfs_fs"; options."com.sun:auto-snapshot" = "true"; - } - { - type = "zfs_filesystem"; - name = "zfs_unmounted_fs"; + }; + zfs_unmounted_fs = { + zfs_type = "filesystem"; options.mountpoint = "none"; - } - { - type = "zfs_filesystem"; - name = "zfs_legacy_fs"; + }; + zfs_legacy_fs = { + zfs_type = "filesystem"; options.mountpoint = "legacy"; mountpoint = "/zfs_legacy_fs"; - } - { - type = "zfs_volume"; - name = "zfs_testvolume"; + }; + zfs_testvolume = { + zfs_type = "volume"; size = "10M"; content = { type = "filesystem"; format = "ext4"; mountpoint = "/ext4onzfs"; }; - } - ]; + }; + }; }; }; } diff --git a/tests/complex.nix b/tests/complex.nix new file mode 100644 index 0000000..3fa861e --- /dev/null +++ b/tests/complex.nix @@ -0,0 +1,20 @@ +{ pkgs ? (import { }) +, makeDiskoTest ? (pkgs.callPackage ./lib.nix { }).makeDiskoTest +}: +makeDiskoTest { + disko-config = import ../example/complex.nix; + extraTestScript = '' + machine.succeed("test -b /dev/zroot/zfs_testvolume"); + machine.succeed("test -b /dev/md/raid1p1"); + + + machine.succeed("mountpoint /mnt"); + machine.succeed("mountpoint /mnt/zfs_fs"); + machine.succeed("mountpoint /mnt/zfs_legacy_fs"); + machine.succeed("mountpoint /mnt/ext4onzfs"); + machine.succeed("mountpoint /mnt/ext4_on_lvm"); + ''; + extraConfig = { + boot.kernelModules = [ "dm-raid" "dm-mirror" ]; + }; +} diff --git a/tests/lib.nix b/tests/lib.nix index 2ec42e6..0bcba2b 100644 --- a/tests/lib.nix +++ b/tests/lib.nix @@ -17,7 +17,7 @@ }; tsp-create = pkgs.writeScript "create" ((pkgs.callPackage ../. { }).create disko-config); tsp-mount = pkgs.writeScript "mount" ((pkgs.callPackage ../. { }).mount disko-config); - num-disks = builtins.length (builtins.filter (x: builtins.match "vd." x == [ ]) (lib.attrNames disko-config.content)); + num-disks = builtins.length (lib.attrNames disko-config.disk); in makeTest' { name = "disko"; diff --git a/types.nix b/types.nix new file mode 100644 index 0000000..e7dcf60 --- /dev/null +++ b/types.nix @@ -0,0 +1,1058 @@ +{ pkgs ? import {} }: +with pkgs.lib; +with builtins; + +rec { + + diskoLib = { + # like types.oneOf but instead of a list takes an attrset + # uses the field "type" to find the correct type in the attrset + subType = typeAttr: mkOptionType rec { + name = "subType"; + description = "one of ${attrNames typeAttr}"; + check = x: typeAttr.${x.type}.check x; + merge = loc: defs: + foldl' (res: def: typeAttr.${def.value.type}.merge loc [def]) {} defs; + nestedTypes = typeAttr; + }; + + # option for valid contents of partitons (basically like devices, but without tables) + partitionType = mkOption { + type = types.nullOr (diskoLib.subType { inherit btrfs filesystem zfs mdraid luks lvm_pv; }); + default = null; + }; + + # option for valid contents of devices + deviceType = mkOption { + type = types.nullOr (diskoLib.subType { inherit table btrfs filesystem zfs mdraid luks lvm_pv; }); + default = null; + }; + + /* deepMergeMap takes a function and a list of attrsets and deep merges them + + deepMergeMap :: -> (AttrSet -> AttrSet ) -> [ AttrSet ] -> Attrset + + Example: + deepMergeMap (x: x.t = "test") [ { x = { y = 1; z = 3; }; } { x = { 123 = 234; }; } ] + => { x = { y = 1; z = 3; 123 = 234; t = "test"; }; } + */ + deepMergeMap = f: listOfAttrs: + foldr (attr: acc: (recursiveUpdate acc (f attr))) {} listOfAttrs; + + /* get a device and an index to get the matching device name + + deviceNumbering :: str -> int -> str + + Example: + deviceNumbering "/dev/sda" 3 + => "/dev/sda3" + + deviceNumbering "/dev/disk/by-id/xxx" 2 + => "/dev/disk/by-id/xxx-part2" + */ + deviceNumbering = dev: index: + let + schemas = { + dev__da = dev + toString index; # /dev/{s,v}da style + dev_disk = "${dev}-part${toString index}"; # /dev/disk/by-id/xxx style + dev_nvme = "${dev}n1p${toString index}"; # /dev/nvme0n1p1 style + dev_md = "${dev}p${toString index}"; # /dev/nvme0n1p1 style + }; + detectSchema = + if match "/dev/[vs]d.*" dev != null then "dev__da" else + if match "/dev/disk/.*" dev != null then "dev_disk" else + if match "/dev/nvme.*" dev != null then "dev_nvme" else + if match "/dev/md/.*" dev != null then "dev_md" else + abort "${dev} seems not to be a supported disk format"; + in schemas.${detectSchema}; + + /* Given a attrset of dependencies and a devices attrset + returns a sorted list by dependencies. aborts if a loop is found + + sortDevicesByDependencies :: AttrSet -> AttrSet -> [ [ str str ] ] + */ + sortDevicesByDependencies = dependencies: devices: + let + dependsOn = a: b: + elem a (attrByPath b [] dependencies); + maybeSortedDevices = toposort dependsOn (diskoLib.deviceList devices); + in + if (hasAttr "cycle" maybeSortedDevices) then + abort "detected a cycle in your disk setup: ${maybeSortedDevices.cycle}" + else + maybeSortedDevices.result; + + /* Takes a devices attrSet and returns it as a list + + deviceList :: AttrSet -> [ [ str str ] ] + + Example: + deviceList { zfs.pool1 = {}; zfs.pool2 = {}; mdadm.raid1 = {}; } + => [ [ "zfs" "pool1" ] [ "zfs" "pool2" ] [ "mdadm" "raid1" ] ] + */ + deviceList = devices: + concatLists (mapAttrsToList (n: v: (map (x: [ n x ]) (attrNames v))) devices); + + /* Takes either a string or null and returns the string or an empty string + + maybeStr :: Either (str null) -> str + + Example: + maybeStr null + => "" + maybeSTr "hello world" + => "hello world" + */ + maybeStr = x: optionalString (!isNull x) x; + }; + + optionTypes = rec { + # POSIX.1‐2017, 3.281 Portable Filename + filename = mkOptionType { + name = "POSIX portable filename"; + check = x: isString x && builtins.match "[0-9A-Za-z._][0-9A-Za-z._-]*" x != null; + merge = mergeOneOption; + }; + + # POSIX.1‐2017, 3.2 Absolute Pathname + absolute-pathname = mkOptionType { + name = "POSIX absolute pathname"; + check = x: isString x && substring 0 1 x == "/" && pathname.check x; + merge = mergeOneOption; + }; + + # POSIX.1-2017, 3.271 Pathname + pathname = mkOptionType { + name = "POSIX pathname"; + check = x: + let + # The filter is used to normalize paths, i.e. to remove duplicated and + # trailing slashes. It also removes leading slashes, thus we have to + # check for "/" explicitly below. + xs = filter (s: stringLength s > 0) (splitString "/" x); + in + isString x && (x == "/" || (length xs > 0 && all filename.check xs)); + merge = mergeOneOption; + }; + }; + + /* topLevel type of the disko config, takes attrsets of disks mdadms zpools and lvm vgs. + exports create, mount, meta and config + */ + topLevel = types.submodule ({ config, ... }: { + options = { + devices = { + disk = mkOption { + type = types.attrsOf disk; + default = {}; + }; + mdadm = mkOption { + type = types.attrsOf mdadm; + default = {}; + }; + zpool = mkOption { + type = types.attrsOf zpool; + default = {}; + }; + lvm_vg = mkOption { + type = types.attrsOf lvm_vg; + default = {}; + }; + }; + meta = mkOption { + readOnly = true; + default = diskoLib.deepMergeMap (dev: dev._meta) (flatten (map attrValues [ + config.devices.disk + config.devices.lvm_vg + config.devices.mdadm + config.devices.zpool + ])) // { + sortedDeviceList = diskoLib.sortDevicesByDependencies config.meta.dependencies config.devices; + }; + }; + create = mkOption { + readOnly = true; + type = types.str; + default = '' + set -efux + ${concatStrings (map (dev: attrByPath (dev ++ [ "_create" ]) "" config.devices) config.meta.sortedDeviceList)} + ''; + }; + mount = mkOption { + readOnly = true; + type = types.str; + default = let + fsMounts = diskoLib.deepMergeMap (dev: dev._mount.fs or {}) (flatten (map attrValues [ + config.devices.disk + config.devices.lvm_vg + config.devices.mdadm + config.devices.zpool + ])); + in '' + set -efux + # first create the neccessary devices + ${concatStrings (map (dev: attrByPath (dev ++ [ "_mount" "dev" ]) "" config.devices) config.meta.sortedDeviceList)} + + # and then mount the filesystems in alphabetical order + # attrValues returns values sorted by name. This is important, because it + # ensures that "/" is processed before "/foo" etc. + ${concatStrings (attrValues fsMounts)} + ''; + }; + config = mkOption { + readOnly = true; + default = diskoLib.deepMergeMap (dev: dev._config) (flatten (map attrValues [ + config.devices.disk + config.devices.lvm_vg + config.devices.mdadm + config.devices.zpool + ])); + }; + }; + }); + + btrfs = types.submodule ({ config, ... }: { + options = { + type = mkOption { + type = types.enum [ "btrfs" ]; + internal = true; + }; + mountOptions = mkOption { + type = types.listOf types.str; + default = []; + }; + subvolumes = mkOption { + type = types.listOf optionTypes.pathname; + default = []; + }; + mountpoint = mkOption { + type = optionTypes.absolute-pathname; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: { + }; + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = dev: '' + mkfs.btrfs ${dev} + ${optionalString (!isNull config.subvolumes or null) '' + MNTPOINT=$(mktemp -d) + ( + mount ${dev} "$MNTPOINT" + trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT + ${concatMapStringsSep "\n" (subvolume: "btrfs subvolume create \"$MNTPOINT\"/${subvolume}") config.subvolumes} + ) + ''} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: { + fs.${config.mountpoint} = '' + if ! findmnt ${dev} "/mnt${config.mountpoint}" > /dev/null 2>&1; then + mount ${dev} "/mnt${config.mountpoint}" \ + ${concatStringsSep " " config.mountOptions} \ + -o X-mount.mkdir + fi + ''; + }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: { + fileSystems.${config.mountpoint} = { + device = dev; + fsType = "btrfs"; + }; + }; + }; + }; + }); + + filesystem = types.submodule ({ config, ... }: { + options = { + type = mkOption { + type = types.enum [ "filesystem" ]; + internal = true; + }; + extraArgs = mkOption { + type = types.str; + default = ""; + }; + mountOptions = mkOption { + type = types.listOf types.str; + default = []; + }; + options = mkOption { + type = types.listOf types.str; + default = []; + }; + mountpoint = mkOption { + type = optionTypes.absolute-pathname; + }; + format = mkOption { + type = types.str; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: { + }; + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = dev: '' + mkfs.${config.format} \ + ${config.extraArgs} \ + ${dev} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: { + fs.${config.mountpoint} = '' + if ! findmnt ${dev} "/mnt${config.mountpoint}" > /dev/null 2>&1; then + mount ${dev} "/mnt${config.mountpoint}" \ + ${toString config.mountOptions} \ + -o X-mount.mkdir + fi + ''; + }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: { + fileSystems.${config.mountpoint} = { + device = dev; + fsType = config.format; + }; + }; + }; + }; + }); + + table = types.submodule ({ config, ... }: { + options = { + type = mkOption { + type = types.enum [ "table" ]; + internal = true; + }; + format = mkOption { + type = types.enum [ "gpt" "msdos" ]; + default = "gpt"; + }; + partitions = mkOption { + type = types.listOf partition; + default = []; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: + diskoLib.deepMergeMap (partition: partition._meta dev) config.partitions; + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = dev: '' + parted -s ${dev} mklabel ${config.format} + ${concatMapStrings (partition: partition._create dev config.format) config.partitions} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: + let + partMounts = diskoLib.deepMergeMap (partition: partition._mount dev) config.partitions; + in { + dev = '' + ${concatStrings (map (x: x.dev or "") (attrValues partMounts))} + ''; + fs = partMounts.fs or {}; + }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: + diskoLib.deepMergeMap (partition: partition._config dev) config.partitions; + }; + }; + }); + + partition = types.submodule ({ config, ... }: { + options = { + type = mkOption { + type = types.enum [ "partition" ]; + internal = true; + }; + part-type = mkOption { + type = types.enum [ "primary" "logical" "extended" ]; + default = "primary"; + }; + fs-type = mkOption { + type = types.nullOr (types.enum [ "btrfs" "ext2" "ext3" "ext4" "fat16" "fat32" "hfs" "hfs+" "linux-swap" "ntfs" "reiserfs" "udf" "xfs" ]); + default = null; + }; + name = mkOption { + type = types.nullOr types.str; + default = null; + }; + start = mkOption { + type = types.str; + default = "0%"; + }; + end = mkOption { + type = types.str; + default = "100%"; + }; + index = mkOption { + type = types.int; + # TODO find a better way to get the index + default = toInt (head (match ".*entry ([[:digit:]]+)]" config._module.args.name)); + }; + flags = mkOption { + type = types.listOf types.str; + default = []; + }; + bootable = mkOption { + type = types.bool; + default = false; + }; + content = diskoLib.partitionType; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: + optionalAttrs (!isNull config.content) (config.content._meta dev); + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.functionTo types.str); + default = dev: type: '' + ${optionalString (type == "gpt") '' + parted -s ${dev} mkpart ${config.name} ${diskoLib.maybeStr config.fs-type} ${config.start} ${config.end} + ''} + ${optionalString (type == "msdos") '' + parted -s ${dev} mkpart ${config.part-type} ${diskoLib.maybeStr config.fs-type} ${diskoLib.maybeStr config.fs-type} ${config.start} ${config.end} + ''} + # ensure /dev/disk/by-path/..-partN exists before continuing + udevadm trigger --subsystem-match=block; udevadm settle + ${optionalString (config.bootable) '' + parted -s ${dev} set ${toString config.index} boot on + ''} + ${concatMapStringsSep "" (flag: '' + parted -s ${dev} set ${toString config.index} ${flag} on + '') config.flags} + ${optionalString (!isNull config.content) (config.content._create (diskoLib.deviceNumbering dev config.index))} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: + optionalAttrs (!isNull config.content) (config.content._mount (diskoLib.deviceNumbering dev config.index)); + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: + optionalAttrs (!isNull config.content) (config.content._config (diskoLib.deviceNumbering dev config.index)); + }; + }; + }); + + lvm_pv = types.submodule ({ config, ... }: { + options = { + type = mkOption { + type = types.enum [ "lvm_pv" ]; + internal = true; + }; + vg = mkOption { + type = types.str; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: { + dependencies.lvm_vg.${config.vg} = [ dev ]; + }; + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = dev: '' + pvcreate ${dev} + LVMDEVICES_${config.vg}="''${LVMDEVICES_${config.vg}:-}${dev} " + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: + {}; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: {}; + }; + }; + }); + + lvm_vg = types.submodule ({ config, ... }: { + options = { + name = mkOption { + type = types.str; + default = config._module.args.name; + }; + type = mkOption { + type = types.enum [ "lvm_vg" ]; + internal = true; + }; + lvs = mkOption { + type = types.attrsOf lvm_lv; + default = {}; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = (pkgs.formats.json {}).type; + default = + diskoLib.deepMergeMap (lv: lv._meta [ "lvm_vg" config.name ]) (attrValues config.lvs); + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.str; + default = '' + vgcreate ${config.name} $LVMDEVICES_${config.name} + ${concatMapStrings (lv: lv._create config.name) (attrValues config.lvs)} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = (pkgs.formats.json {}).type; + default = let + lvMounts = diskoLib.deepMergeMap (lv: lv._mount config.name) (attrValues config.lvs); + in { + dev = '' + vgchange -a y + ${concatStrings (map (x: x.dev or "") (attrValues lvMounts))} + ''; + fs = lvMounts.fs; + }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = + diskoLib.deepMergeMap (lv: lv._config config.name) (attrValues config.lvs); + }; + }; + }); + + lvm_lv = types.submodule ({ config, ... }: { + options = { + name = mkOption { + type = types.str; + default = config._module.args.name; + }; + type = mkOption { + type = types.enum [ "lvm_lv" ]; + default = "lvm_lv"; + internal = true; + }; + size = mkOption { + type = types.str; # TODO lvm size type + }; + lvm_type = mkOption { + type = types.nullOr (types.enum [ "mirror" "raid0" "raid1" ]); # TODO add all types + default = null; # maybe there is always a default type? + }; + extraArgs = mkOption { + type = types.str; + default = ""; + }; + content = diskoLib.partitionType; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: + optionalAttrs (!isNull config.content) (config.content._meta dev); + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = vg: '' + lvcreate \ + ${if hasInfix "%" config.size then "-l" else "-L"} ${config.size} \ + -n ${config.name} \ + ${optionalString (!isNull config.lvm_type) "--type=${config.lvm_type}"} \ + ${config.extraArgs} \ + ${vg} + ${optionalString (!isNull config.content) (config.content._create "/dev/${vg}/${config.name}")} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = vg: + optionalAttrs (!isNull config.content) (config.content._mount "/dev/${vg}/${config.name}"); + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: + optionalAttrs (!isNull config.content) (config.content._config dev); + }; + }; + }); + + zfs = types.submodule ({ config, ... }: { + options = { + type = mkOption { + type = types.enum [ "zfs" ]; + internal = true; + }; + pool = mkOption { + type = types.str; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: { + dependencies.zpool.${config.pool} = [ dev ]; + }; + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = dev: '' + ZFSDEVICES_${config.pool}="''${ZFSDEVICES_${config.pool}:-}${dev} " + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: + {}; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: {}; + }; + }; + }); + + zpool = types.submodule ({ config, ... }: { + options = { + name = mkOption { + type = types.str; + default = config._module.args.name; + }; + type = mkOption { + type = types.enum [ "zpool" ]; + internal = true; + }; + mode = mkOption { + type = types.str; # TODO zfs modes + default = ""; + }; + options = mkOption { + type = types.attrsOf types.str; + default = {}; + }; + rootFsOptions = mkOption { + type = types.attrsOf types.str; + default = {}; + }; + mountpoint = mkOption { + type = types.nullOr optionTypes.absolute-pathname; + default = null; + }; + mountOptions = mkOption { + type = types.listOf types.str; + default = []; + }; + datasets = mkOption { + type = types.attrsOf zfs_dataset; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = (pkgs.formats.json {}).type; + default = + diskoLib.deepMergeMap (dataset: dataset._meta [ "zpool" config.name ]) (attrValues config.datasets); + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.str; + default = '' + zpool create ${config.name} \ + ${config.mode} \ + ${concatStringsSep " " (mapAttrsToList (n: v: "-o ${n}=${v}") config.options)} \ + ${concatStringsSep " " (mapAttrsToList (n: v: "-O ${n}=${v}") config.rootFsOptions)} \ + ''${ZFSDEVICES_${config.name}} + ${concatMapStrings (dataset: dataset._create config.name) (attrValues config.datasets)} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = (pkgs.formats.json {}).type; + default = let + datasetMounts = diskoLib.deepMergeMap (dataset: dataset._mount config.name) (attrValues config.datasets); + in { + dev = '' + zpool list '${config.name}' >/dev/null 2>/dev/null || zpool import '${config.name}' + ${concatStrings (map (x: x.dev or "") (attrValues datasetMounts))} + ''; + fs = datasetMounts.fs // optionalAttrs (!isNull config.mountpoint) { + ${config.mountpoint} = '' + if ! findmnt ${config.name} "/mnt${config.mountpoint}" > /dev/null 2>&1; then + mount ${config.name} "/mnt${config.mountpoint}" \ + ${optionalString ((config.options.mountpoint or "") != "legacy") "-o zfsutil"} \ + ${toString config.mountOptions} \ + -o X-mount.mkdir \ + -t zfs + fi + ''; + }; + }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = + recursiveUpdate + (diskoLib.deepMergeMap (dataset: dataset._config config.name) (attrValues config.datasets)) + (optionalAttrs (!isNull config.mountpoint) { + fileSystems.${config.mountpoint} = { + device = config.name; + fsType = [ "zfs" ]; + }; + }); + }; + }; + }); + + zfs_dataset = types.submodule ({ config, ... }: { + options = { + name = mkOption { + type = types.str; + default = config._module.args.name; + }; + type = mkOption { + type = types.enum [ "zfs_dataset" ]; + default = "zfs_dataset"; + }; + zfs_type = mkOption { + type = types.enum [ "filesystem" "volume" ]; + }; + options = mkOption { + type = types.attrsOf types.str; + default = {}; + }; + mountOptions = mkOption { + type = types.listOf types.str; + default = []; + }; + + # filesystem options + mountpoint = mkOption { + type = types.nullOr optionTypes.absolute-pathname; + default = null; + }; + + # volume options + size = mkOption { + type = types.nullOr types.str; # TODO size + default = null; + }; + + content = diskoLib.partitionType; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: + optionalAttrs (!isNull config.content) (config.content._meta dev); + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = zpool: '' + zfs create ${zpool}/${config.name} \ + ${concatStringsSep " " (mapAttrsToList (n: v: "-o ${n}=${v}") config.options)} \ + ${optionalString (config.zfs_type == "volume") "-V ${config.size}"} + ${optionalString (config.zfs_type == "volume") '' + udevadm trigger --subsystem-match=block; udevadm settle + ${optionalString (!isNull config.content) (config.content._create "/dev/zvol/${zpool}/${config.name}")} + ''} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = zpool: + optionalAttrs (config.zfs_type == "volume" && !isNull config.content) (config.content._mount "/dev/zvol/${zpool}/${config.name}") // + optionalAttrs (config.zfs_type == "filesystem" && config.options.mountpoint or "" != "none") { fs.${config.mountpoint} = '' + if ! findmnt ${zpool}/${config.name} "/mnt${config.mountpoint}" > /dev/null 2>&1; then + mount ${zpool}/${config.name} "/mnt${config.mountpoint}" \ + -o X-mount.mkdir \ + ${toString config.mountOptions} \ + ${optionalString ((config.options.mountpoint or "") != "legacy") "-o zfsutil"} \ + -t zfs + fi + ''; }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = zpool: + optionalAttrs (config.zfs_type == "volume" && !isNull config.content) (config.content._config "/dev/zvol/${zpool}/${config.name}") // + optionalAttrs (config.zfs_type == "filesystem" && config.options.mountpoint or "" != "none") { + fileSystems.${config.mountpoint} = { + device = "${zpool}/${config.name}"; + fsType = [ "zfs" ]; + }; + }; + }; + }; + }); + + mdadm = types.submodule ({ config, ... }: { + options = { + name = mkOption { + type = types.str; + default = config._module.args.name; + }; + type = mkOption { + type = types.enum [ "mdadm" ]; + default = "mdadm"; + }; + level = mkOption { + type = types.int; + default = 1; + }; + content = diskoLib.deviceType; + _meta = mkOption { + internal = true; + readOnly = true; + type = (pkgs.formats.json {}).type; + default = + optionalAttrs (!isNull config.content) (config.content._meta [ "mdadm" config.name ]); + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.str; + default = '' + echo 'y' | mdadm --create /dev/md/${config.name} \ + --level=${toString config.level} \ + --raid-devices=''${RAIDDEVICES_N_${config.name}} \ + ''${RAIDDEVICES_${config.name}} + udevadm trigger --subsystem-match=block; udevadm settle + ${optionalString (!isNull config.content) (config.content._create "/dev/md/${config.name}")} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = (pkgs.formats.json {}).type; + default = + optionalAttrs (!isNull config.content) (config.content._mount "/dev/md/${config.name}"); + # TODO we probably need to assemble the mdadm somehow + }; + _config = mkOption { + internal = true; + readOnly = true; + default = + optionalAttrs (!isNull config.content) (config.content._config "/dev/md/${config.name}"); + }; + }; + }); + + mdraid = types.submodule ({ config, ... }: { + options = { + type = mkOption { + type = types.enum [ "mdraid" ]; + }; + + name = mkOption { + type = types.str; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: { + dependencies.mdadm.${config.name} = [ dev ]; + }; + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = dev: '' + RAIDDEVICES_N_${config.name}=$((''${RAIDDEVICES_N_${config.name}:-0}+1)) + RAIDDEVICES_${config.name}="''${RAIDDEVICES_${config.name}:-}${dev} " + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: + {}; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: {}; + }; + }; + }); + + luks = types.submodule ({ config, ... }: { + options = { + type = mkOption { + type = types.enum [ "luks" ]; + }; + name = mkOption { + type = types.str; + }; + keyFile = mkOption { + type = types.nullOr optionTypes.absolute-pathname; + default = null; + }; + extraArgs = mkOption { + type = types.listOf types.str; + default = []; + }; + content = diskoLib.deviceType; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: + optionalAttrs (!isNull config.content) (config.content._meta dev); + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = dev: '' + cryptsetup -q luksFormat ${dev} ${diskoLib.maybeStr config.keyFile} ${toString config.extraArgs} + cryptsetup luksOpen ${dev} ${config.name} ${optionalString (!isNull config.keyFile) "--key-file ${config.keyFile}"} + ${optionalString (!isNull config.content) (config.content._create "/dev/mapper/${config.name}")} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (pkgs.formats.json {}).type; + default = dev: + let + contentMount = config.content._mount "/dev/mapper/${config.name}"; + in + { + dev = '' + cryptsetup status ${config.name} >/dev/null 2>/dev/null || + cryptsetup luksOpen ${dev} ${config.name} ${optionalString (!isNull config.keyFile) "--key-file ${config.keyFile}"} + ${optionalString (!isNull config.content) contentMount.dev or ""} + ''; + fs = optionalAttrs (!isNull config.content) contentMount.fs or {}; + }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: + recursiveUpdate { + # TODO do we need this always in initrd and only there? + boot.initrd.luks.devices.${config.name}.device = dev; + } (optionalAttrs (!isNull config.content) (config.content._config "/dev/mapper/${config.name}")); + }; + }; + }); + + disk = types.submodule ({ config, ... }: { + options = { + name = mkOption { + type = types.str; + default = config._module.args.name; + }; + type = mkOption { + type = types.enum [ "disk" ]; + }; + device = mkOption { + type = optionTypes.absolute-pathname; # TODO check if subpath of /dev ? + }; + content = diskoLib.deviceType; + _meta = mkOption { + internal = true; + readOnly = true; + type = (pkgs.formats.json {}).type; + default = + optionalAttrs (!isNull config.content) (config.content._meta [ "disk" config.device ]); + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.str; + default = config.content._create config.device; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = (pkgs.formats.json {}).type; + default = + optionalAttrs (!isNull config.content) (config.content._mount config.device); + }; + _config = mkOption { + internal = true; + readOnly = true; + default = + optionalAttrs (!isNull config.content) (config.content._config config.device); + }; + }; + }); +}