summaryrefslogtreecommitdiffstats
path: root/types.nix
diff options
context:
space:
mode:
authorlassulus <lassulus@lassul.us>2022-08-29 11:45:19 +0200
committerlassulus <lassulus@lassul.us>2022-09-02 11:14:58 +0200
commitdcb0c42857a2ada8754d4514d4b8c4c1d67ab3f5 (patch)
treed4c0f34db5bb060d9133fc03f57e68e46fdb86ff /types.nix
parentadf901d58155ca268d15351fff164d3ef38a0890 (diff)
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.
Diffstat (limited to 'types.nix')
-rw-r--r--types.nix1058
1 files changed, 1058 insertions, 0 deletions
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 <nixpkgs> {} }:
+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);
+ };
+ };
+ });
+}