diff options
Diffstat (limited to 'types.nix')
-rw-r--r-- | types.nix | 1414 |
1 files changed, 1414 insertions, 0 deletions
diff --git a/types.nix b/types.nix new file mode 100644 index 0000000..c0d18d0 --- /dev/null +++ b/types.nix @@ -0,0 +1,1414 @@ +{ lib }: +with 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: if x ? type then typeAttr.${x.type}.check x else throw "No type option set in:\n${generators.toPretty {} x}"; + merge = loc: defs: + foldl' (res: def: typeAttr.${def.value.type}.merge loc [def]) {} defs; + nestedTypes = typeAttr; + }; + + # option for valid contents of partitions (basically like devices, but without tables) + partitionType = mkOption { + type = types.nullOr (diskoLib.subType { inherit btrfs filesystem zfs mdraid luks lvm_pv swap; }); + default = null; + }; + + # option for valid contents of devices + deviceType = mkOption { + type = types.nullOr (diskoLib.subType { inherit table btrfs filesystem zfs mdraid luks lvm_pv swap; }); + 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 = { bla = 234; }; } ] + => { x = { y = 1; z = 3; bla = 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: + if match "/dev/[vs]d.+" dev != null then + dev + toString index # /dev/{s,v}da style + else if match "/dev/disk/.+" dev != null then + "${dev}-part${toString index}" # /dev/disk/by-id/xxx style + else if match "/dev/(nvme|md/|mmcblk).+" dev != null then + "${dev}p${toString index}" # /dev/nvme0n1p1 style + else + abort "${dev} seems not to be a supported disk format"; + + /* A nix option type representing a json datastructure, vendored from nixpkgs to avoid dependency on pkgs */ + jsonType = let + valueType = types.nullOr (types.oneOf [ + types.bool + types.int + types.float + types.str + types.path + (types.attrsOf valueType) + (types.listOf valueType) + ]) // { + description = "JSON value"; + }; + in valueType; + + /* Given a attrset of deviceDependencies and a devices attrset + returns a sorted list by deviceDependencies. aborts if a loop is found + + sortDevicesByDependencies :: AttrSet -> AttrSet -> [ [ str str ] ] + */ + sortDevicesByDependencies = deviceDependencies: devices: + let + dependsOn = a: b: + elem a (attrByPath b [] deviceDependencies); + 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; + + /* Takes a disko device specification, returns an attrset with metadata + + meta :: types.devices -> AttrSet + */ + meta = devices: diskoLib.deepMergeMap (dev: dev._meta) (flatten (map attrValues (attrValues devices))); + + /* Takes a disko device specification and returns a string which formats the disks + + create :: types.devices -> str + */ + create = devices: let + sortedDeviceList = diskoLib.sortDevicesByDependencies ((diskoLib.meta devices).deviceDependencies or {}) devices; + in '' + set -efux + ${concatStrings (map (dev: attrByPath (dev ++ [ "_create" ]) "" devices) sortedDeviceList)} + ''; + /* Takes a disko device specification and returns a string which mounts the disks + + mount :: types.devices -> str + */ + mount = devices: let + fsMounts = diskoLib.deepMergeMap (dev: dev._mount.fs or {}) (flatten (map attrValues (attrValues devices))); + sortedDeviceList = diskoLib.sortDevicesByDependencies ((diskoLib.meta devices).deviceDependencies or {}) devices; + in '' + set -efux + # first create the necessary devices + ${concatStrings (map (dev: attrByPath (dev ++ [ "_mount" "dev" ]) "" devices) 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)} + ''; + + /* takes a disko device specification and returns a string which unmounts, destroys all disks and then runs create and mount + + zapCreateMount :: types.devices -> str + */ + zapCreateMount = devices: '' + set -efux + umount -Rv /mnt || : + + for dev in ${toString (lib.catAttrs "device" (lib.attrValues devices.disk))}; do + ${./disk-deactivate}/disk-deactivate "$dev" | bash -x + done + + echo 'creating partitions...' + ${diskoLib.create devices} + echo 'mounting partitions...' + ${diskoLib.mount devices} + ''; + /* Takes a disko device specification and returns a nixos configuration + + config :: types.devices -> nixosConfig + */ + config = devices: flatten (map (dev: dev._config) (flatten (map attrValues (attrValues devices)))); + /* Takes a disko device specification and returns a function to get the needed packages to format/mount the disks + + packages :: types.devices -> pkgs -> [ derivation ] + */ + packages = devices: pkgs: unique (flatten (map (dev: dev._pkgs pkgs) (flatten (map attrValues (attrValues devices))))); + }; + + 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, nodevs, and lvm vgs. + */ + devices = types.submodule { + options = { + 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 = {}; + }; + nodev = mkOption { + type = types.attrsOf nodev; + default = {}; + }; + }; + }; + + nodev = types.submodule ({ config, ... }: { + options = { + type = mkOption { + type = types.enum [ "nodev" ]; + default = "nodev"; + internal = true; + }; + fsType = mkOption { + type = types.str; + }; + device = mkOption { + type = types.str; + default = "none"; + }; + mountpoint = mkOption { + type = optionTypes.absolute-pathname; + default = config._module.args.name; + }; + mountOptions = mkOption { + type = types.listOf types.str; + default = [ "defaults" ]; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = diskoLib.jsonType; + default = { + }; + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.str; + default = ""; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = diskoLib.jsonType; + default = { + fs.${config.mountpoint} = '' + if ! findmnt ${config.fsType} "/mnt${config.mountpoint}" > /dev/null 2>&1; then + mount -t ${config.fsType} ${config.device} "/mnt${config.mountpoint}" \ + ${concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \ + -o X-mount.mkdir + fi + ''; + }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = [{ + fileSystems.${config.mountpoint} = { + device = config.device; + fsType = config.fsType; + options = config.mountOptions; + }; + }]; + }; + _pkgs = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.listOf types.package); + default = pkgs: []; + }; + }; + }); + + btrfs = types.submodule ({ config, ... }: { + options = { + type = mkOption { + type = types.enum [ "btrfs" ]; + internal = true; + }; + extraArgs = mkOption { + type = types.str; + default = ""; + }; + mountOptions = mkOption { + type = types.listOf types.str; + default = [ "defaults" ]; + }; + subvolumes = mkOption { + type = types.attrsOf btrfs_subvol; + default = {}; + }; + mountpoint = mkOption { + type = types.nullOr optionTypes.absolute-pathname; + default = null; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo diskoLib.jsonType; + default = dev: + diskoLib.deepMergeMap (subvol: subvol._meta dev) (attrValues config.subvolumes); + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = dev: '' + mkfs.btrfs ${dev} ${config.extraArgs} + ${concatMapStrings (subvol: subvol._create dev) (attrValues config.subvolumes)} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo diskoLib.jsonType; + default = dev: + let + subvolMounts = diskoLib.deepMergeMap (subvol: subvol._mount dev config.mountpoint) (attrValues config.subvolumes); + in { + fs = subvolMounts.fs // optionalAttrs (!isNull config.mountpoint) { + ${config.mountpoint} = '' + if ! findmnt ${dev} "/mnt${config.mountpoint}" > /dev/null 2>&1; then + mount ${dev} "/mnt${config.mountpoint}" \ + ${concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \ + -o X-mount.mkdir + fi + ''; + }; + }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: [ + (map (subvol: subvol._config dev config.mountpoint) (attrValues config.subvolumes)) + (optional (!isNull config.mountpoint) { + fileSystems.${config.mountpoint} = { + device = dev; + fsType = "btrfs"; + options = config.mountOptions; + }; + }) + ]; + }; + _pkgs = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.listOf types.package); + default = pkgs: + [ pkgs.btrfs-progs ] ++ flatten (map (subvolume: subvolume._pkgs pkgs) (attrValues config.subvolumes)); + }; + }; + }); + + btrfs_subvol = types.submodule ({ config, ... }: { + options = { + name = mkOption { + type = types.str; + default = config._module.args.name; + }; + type = mkOption { + type = types.enum [ "btrfs_subvol" ]; + default = "btrfs_subvol"; + internal = true; + }; + extraArgs = mkOption { + type = types.str; + default = ""; + }; + mountOptions = mkOption { + type = types.listOf types.str; + default = [ "defaults" ]; + }; + mountpoint = mkOption { + type = types.nullOr optionTypes.absolute-pathname; + default = null; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo diskoLib.jsonType; + default = dev: { + }; + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = dev: '' + MNTPOINT=$(mktemp -d) + ( + mount ${dev} "$MNTPOINT" -o subvol=/ + trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT + btrfs subvolume create "$MNTPOINT"/${config.name} ${config.extraArgs} + ) + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.functionTo diskoLib.jsonType); + default = dev: parent: let + mountpoint = if (!isNull config.mountpoint) then config.mountpoint + else if (isNull parent) then config.name + else null; + in optionalAttrs (!isNull mountpoint) { + fs.${mountpoint} = '' + if ! findmnt ${dev} "/mnt${mountpoint}" > /dev/null 2>&1; then + mount ${dev} "/mnt${mountpoint}" \ + ${concatMapStringsSep " " (opt: "-o ${opt}") (config.mountOptions ++ [ "subvol=${config.name}" ])} \ + -o X-mount.mkdir + fi + ''; + }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: parent: let + mountpoint = if (!isNull config.mountpoint) then config.mountpoint + else if (isNull parent) then config.name + else null; + in optional (!isNull mountpoint) { + fileSystems.${mountpoint} = { + device = dev; + fsType = "btrfs"; + options = config.mountOptions ++ [ "subvol=${config.name}" ]; + }; + }; + }; + _pkgs = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.listOf types.package); + default = pkgs: [ pkgs.coreutils ]; + }; + }; + }); + + 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 = [ "defaults" ]; + }; + mountpoint = mkOption { + type = optionTypes.absolute-pathname; + }; + format = mkOption { + type = types.str; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo diskoLib.jsonType; + 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 diskoLib.jsonType; + default = dev: { + fs.${config.mountpoint} = '' + if ! findmnt ${dev} "/mnt${config.mountpoint}" > /dev/null 2>&1; then + mount ${dev} "/mnt${config.mountpoint}" \ + ${concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \ + -o X-mount.mkdir + fi + ''; + }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: [{ + fileSystems.${config.mountpoint} = { + device = dev; + fsType = config.format; + options = config.mountOptions; + }; + }]; + }; + _pkgs = mkOption { + internal = true; + readOnly = true; + # type = types.functionTo (types.listOf types.package); + default = pkgs: + [ pkgs.util-linux ] ++ ( + # TODO add many more + if (config.format == "xfs") then [ pkgs.xfsprogs ] + else if (config.format == "btrfs") then [ pkgs.btrfs-progs ] + else if (config.format == "vfat") then [ pkgs.dosfstools ] + else if (config.format == "ext2") then [ pkgs.e2fsprogs ] + else if (config.format == "ext3") then [ pkgs.e2fsprogs ] + else if (config.format == "ext4") then [ pkgs.e2fsprogs ] + else [] + ); + }; + }; + }); + + 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 diskoLib.jsonType; + 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 diskoLib.jsonType; + 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: + map (partition: partition._config dev) config.partitions; + }; + _pkgs = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.listOf types.package); + default = pkgs: + [ pkgs.parted pkgs.systemdMinimal ] ++ flatten (map (partition: partition._pkgs pkgs) 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; + }; + 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 diskoLib.jsonType; + 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} + # ensure further operations can detect new partitions + udevadm trigger --subsystem-match=block; udevadm settle + ${optionalString (!isNull config.content) (config.content._create (diskoLib.deviceNumbering dev config.index))} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo diskoLib.jsonType; + default = dev: + optionalAttrs (!isNull config.content) (config.content._mount (diskoLib.deviceNumbering dev config.index)); + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: + optional (!isNull config.content) (config.content._config (diskoLib.deviceNumbering dev config.index)); + }; + _pkgs = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.listOf types.package); + default = pkgs: optionals (!isNull config.content) (config.content._pkgs pkgs); + }; + }; + }); + + swap = types.submodule ({ config, ... }: { + options = { + type = mkOption { + type = types.enum [ "swap" ]; + internal = true; + }; + randomEncryption = mkOption { + type = types.bool; + default = false; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = types.functionTo diskoLib.jsonType; + default = dev: { + }; + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = dev: '' + mkswap ${dev} + ''; + }; + _mount = mkOption { + internal = true; + readOnly = true; + type = types.functionTo diskoLib.jsonType; + default = dev: { + fs.${dev} = '' + if ! swapon --show | grep -q '^${dev} '; then + swapon ${dev} + fi + ''; + }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: [{ + swapDevices = [{ + device = dev; + randomEncryption = config.randomEncryption; + }]; + }]; + }; + _pkgs = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.listOf types.package); + default = pkgs: [ pkgs.gnugrep pkgs.util-linux ]; + }; + }; + }); + + 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 diskoLib.jsonType; + default = dev: { + deviceDependencies.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 diskoLib.jsonType; + default = dev: + {}; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: []; + }; + _pkgs = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.listOf types.package); + default = pkgs: [ pkgs.lvm2 ]; + }; + }; + }); + + 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 = diskoLib.jsonType; + 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 = diskoLib.jsonType; + 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 = + map (lv: lv._config config.name) (attrValues config.lvs); + }; + _pkgs = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.listOf types.package); + default = pkgs: flatten (map (lv: lv._pkgs pkgs) (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 diskoLib.jsonType; + default = dev: + optionalAttrs (!isNull config.content) (config.content._meta dev); + }; + _create = mkOption { + internal = true; + readOnly = true; + type = types.functionTo types.str; + default = vg: '' + lvcreate \ + --yes \ + ${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 diskoLib.jsonType; + default = vg: + optionalAttrs (!isNull config.content) (config.content._mount "/dev/${vg}/${config.name}"); + }; + _config = mkOption { + internal = true; + readOnly = true; + default = vg: + [ + (optional (!isNull config.content) (config.content._config "/dev/${vg}/${config.name}")) + (optional (!isNull config.lvm_type) { + boot.initrd.kernelModules = [ "dm-${config.lvm_type}" ]; + }) + ]; + }; + _pkgs = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.listOf types.package); + default = pkgs: lib.optionals (!isNull config.content) (config.content._pkgs pkgs); + }; + }; + }); + + 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 diskoLib.jsonType; + default = dev: { + deviceDependencies.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 diskoLib.jsonType; + default = dev: + {}; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = dev: []; + }; + _pkgs = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.listOf types.package); + default = pkgs: [ pkgs.zfs ]; + }; + }; + }); + + zpool = types.submodule ({ config, ... }: { + options = { + name = mkOption { + type = types.str; + default = config._module.args.name; + }; + type = mkOption { + type = types.enum [ "zpool" ]; + default = "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 = [ "defaults" ]; + }; + datasets = mkOption { + type = types.attrsOf zfs_dataset; + }; + _meta = mkOption { + internal = true; + readOnly = true; + type = diskoLib.jsonType; + 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 = diskoLib.jsonType; + 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"} \ + ${concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \ + -o X-mount.mkdir \ + -t zfs + fi + ''; + }; + }; + }; + _config = mkOption { + internal = true; + readOnly = true; + default = [ + (map (dataset: dataset._config config.name) (attrValues config.datasets)) + (optional (!isNull config.mountpoint) { + fileSystems.${config.mountpoint} = { + device = config.name; + fsType = "zfs"; + options = config.mountOptions ++ lib.optional ((config.options.mountpoint or "") != "legacy") "zfsutil"; + }; + }) + ]; + }; + _pkgs = mkOption { + internal = true; + readOnly = true; + type = types.functionTo (types.listOf types.package); + default = pkgs: [ pkgs.util-linux ] ++ flatten (map (dataset: dataset._pkgs pkgs) (attrValues config.datasets)); + }; + }; + }); + + 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"; + internal = true; + }; + zfs_type = mkOption { + type = types.enum [ "filesystem" "volume" ]; + }; + options = mkOption { + type = types.attrsOf types.str; + default = {}; + }; + mountOptions = mkOption { + type = types.listOf types.str; + default = [ "defaults" ]; + }; + + # filesystem options + mountpoint = |