diff --git a/default.nix b/default.nix index 3265f02..6476340 100644 --- a/default.nix +++ b/default.nix @@ -2,7 +2,7 @@ , rootMountPoint ? "/mnt" }: let - types = import ./types.nix { inherit lib rootMountPoint; }; + types = import ./types { inherit lib rootMountPoint; }; eval = cfg: lib.evalModules { modules = lib.singleton { # _file = toString input; diff --git a/doc.nix b/doc.nix index be556ab..2c427ec 100644 --- a/doc.nix +++ b/doc.nix @@ -1,7 +1,7 @@ { lib, nixosOptionsDoc, runCommand, fetchurl, pandoc }: let - types = import ./types.nix { + types = import ./types { inherit lib; rootMountPoint = "/mnt"; }; diff --git a/module.nix b/module.nix index 66a828e..49afeb5 100644 --- a/module.nix +++ b/module.nix @@ -1,6 +1,6 @@ { config, lib, pkgs, ... }: let - types = import ./types.nix { + types = import ./types { inherit lib; rootMountPoint = config.disko.rootMountPoint; }; diff --git a/types/btrfs.nix b/types/btrfs.nix new file mode 100644 index 0000000..a91f822 --- /dev/null +++ b/types/btrfs.nix @@ -0,0 +1,86 @@ +({ config, options, diskoLib, lib, subTypes, optionTypes, rootMountPoint, ... }: +{ + options = { + type = lib.mkOption { + type = lib.types.enum [ "btrfs" ]; + internal = true; + description = "Type"; + }; + extraArgs = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Arguments to pass to BTRFS"; + }; + mountOptions = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "defaults" ]; + description = "A list of options to pass to mount."; + }; + subvolumes = lib.mkOption { + type = lib.types.attrsOf subTypes.btrfs_subvol; + default = { }; + description = "Subvolumes to define for BTRFS."; + }; + mountpoint = lib.mkOption { + type = lib.types.nullOr optionTypes.absolute-pathname; + default = null; + description = "A path to mount the BTRFS filesystem to."; + }; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = dev: + diskoLib.deepMergeMap (subvol: subvol._meta dev) (lib.attrValues config.subvolumes); + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = { dev }: '' + mkfs.btrfs ${dev} ${config.extraArgs} + ${lib.concatMapStrings (subvol: subvol._create { inherit dev; }) (lib.attrValues config.subvolumes)} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = { dev }: + let + subvolMounts = diskoLib.deepMergeMap (subvol: subvol._mount { inherit dev; parent = config.mountpoint; }) (lib.attrValues config.subvolumes); + in + { + fs = subvolMounts.fs // lib.optionalAttrs (!isNull config.mountpoint) { + ${config.mountpoint} = '' + if ! findmnt ${dev} "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then + mount ${dev} "${rootMountPoint}${config.mountpoint}" \ + ${lib.concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \ + -o X-mount.mkdir + fi + ''; + }; + }; + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = dev: [ + (map (subvol: subvol._config dev config.mountpoint) (lib.attrValues config.subvolumes)) + (lib.optional (!isNull config.mountpoint) { + fileSystems.${config.mountpoint} = { + device = dev; + fsType = "btrfs"; + options = config.mountOptions; + }; + }) + ]; + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: + [ pkgs.btrfs-progs ] ++ lib.flatten (map (subvolume: subvolume._pkgs pkgs) (lib.attrValues config.subvolumes)); + description = "Packages"; + }; + }; +}) diff --git a/types/btrfs_subvol.nix b/types/btrfs_subvol.nix new file mode 100644 index 0000000..c3223d1 --- /dev/null +++ b/types/btrfs_subvol.nix @@ -0,0 +1,94 @@ +{ config, options, diskoLib, lib, optionTypes, rootMountPoint, ... }: +{ + options = { + name = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + description = "Name of the BTRFS subvolume."; + }; + type = lib.mkOption { + type = lib.types.enum [ "btrfs_subvol" ]; + default = "btrfs_subvol"; + internal = true; + description = "Type"; + }; + extraArgs = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Extra arguments to pass"; + }; + mountOptions = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "defaults" ]; + description = "Options to pass to mount"; + }; + mountpoint = lib.mkOption { + type = lib.types.nullOr optionTypes.absolute-pathname; + default = null; + description = "Location to mount the subvolume to."; + }; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = dev: { }; + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + 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 = diskoLib.mkMountOption { + inherit config options; + default = { dev, parent }: + let + mountpoint = + if (!isNull config.mountpoint) then config.mountpoint + else if (isNull parent) then config.name + else null; + in + lib.optionalAttrs (!isNull mountpoint) { + fs.${mountpoint} = '' + if ! findmnt ${dev} "${rootMountPoint}${mountpoint}" > /dev/null 2>&1; then + mount ${dev} "${rootMountPoint}${mountpoint}" \ + ${lib.concatMapStringsSep " " (opt: "-o ${opt}") (config.mountOptions ++ [ "subvol=${config.name}" ])} \ + -o X-mount.mkdir + fi + ''; + }; + }; + _config = lib.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 + lib.optional (!isNull mountpoint) { + fileSystems.${mountpoint} = { + device = dev; + fsType = "btrfs"; + options = config.mountOptions ++ [ "subvol=${config.name}" ]; + }; + }; + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: [ pkgs.coreutils ]; + description = "Packages"; + }; + }; +} diff --git a/types/default.nix b/types/default.nix new file mode 100644 index 0000000..96c7dfa --- /dev/null +++ b/types/default.nix @@ -0,0 +1,346 @@ +{ lib, rootMountPoint }: +with lib; +with builtins; + +rec { + + diskoLib = { + # like lib.types.oneOf but instead of a list takes an attrset + # uses the field "type" to find the correct type in the attrset + subType = typeAttr: lib.mkOptionType rec { + name = "subType"; + description = "one of ${concatStringsSep "," (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 = lib.mkOption { + type = lib.types.nullOr (diskoLib.subType { inherit (subTypes) btrfs filesystem zfs mdraid luks lvm_pv swap; }); + default = null; + description = "The type of partition"; + }; + + # option for valid contents of devices + deviceType = lib.mkOption { + type = lib.types.nullOr (diskoLib.subType { inherit (subTypes) table btrfs filesystem zfs mdraid luks lvm_pv swap; }); + default = null; + description = "The type of device"; + }; + + /* 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 = lib.types.nullOr + (lib.types.oneOf [ + lib.types.bool + lib.types.int + lib.types.float + lib.types.str + lib.types.path + (lib.types.attrsOf valueType) + (lib.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 Submodules config and options argument and returns a serializable + subset of config variables as a shell script snippet. + */ + defineHookVariables = { config, options }: + let + sanitizeName = lib.replaceStrings [ "-" ] [ "_" ]; + isAttrsOfSubmodule = o: o.type.name == "attrsOf" && o.type.nestedTypes.elemType.name == "submodule"; + isSerializable = n: o: !( + lib.hasPrefix "_" n + || lib.hasSuffix "Hook" n + || isAttrsOfSubmodule o + # TODO don't hardcode diskoLib.subType options. + || n == "content" || n == "partitions" + ); + in + lib.toShellVars + (lib.mapAttrs' + (n: o: lib.nameValuePair (sanitizeName n) o.value) + (lib.filterAttrs isSerializable options)); + + mkHook = description: lib.mkOption { + inherit description; + type = lib.types.str; + default = ""; + }; + + mkSubType = module: lib.types.submodule ([ + module + + { + options = { + preCreateHook = diskoLib.mkHook "shell commands to run before create"; + postCreateHook = diskoLib.mkHook "shell commands to run after create"; + preMountHook = diskoLib.mkHook "shell commands to run before mount"; + postMountHook = diskoLib.mkHook "shell commands to run after mount"; + }; + config._module.args = { + inherit diskoLib optionTypes subTypes rootMountPoint; + }; + } + ]); + + mkCreateOption = { config, options, default }@attrs: + lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo lib.types.str; + default = args: + let + name = "format"; + test = lib.optionalString (config ? name) "${config.${name}}"; + in + '' + ( # ${config.type} ${concatMapStringsSep " " (n: toString (config.${n} or "")) ["name" "device" "format" "mountpoint"]} + ${diskoLib.defineHookVariables { inherit config options; }} + ${config.preCreateHook} + ${attrs.default args} + ${config.postCreateHook} + ) + ''; + description = "Creation script"; + }; + + mkMountOption = { config, options, default }@attrs: + lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = args: attrs.default args; + description = "Mount script"; + }; + + + /* Takes a disko device specification, returns an attrset with metadata + + meta :: lib.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 :: lib.types.devices -> str + */ + create = devices: + let + sortedDeviceList = diskoLib.sortDevicesByDependencies ((diskoLib.meta devices).deviceDependencies or { }) devices; + in + '' + set -efux + + disko_devices_dir=$(mktemp -d) + trap 'rm -rf "$disko_devices_dir"' EXIT + mkdir -p "$disko_devices_dir" + + ${concatMapStrings (dev: (attrByPath (dev ++ [ "_create" ]) ({}: {}) devices) {}) sortedDeviceList} + ''; + /* Takes a disko device specification and returns a string which mounts the disks + + mount :: lib.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 + ${concatMapStrings (dev: ((attrByPath (dev ++ [ "_mount" ]) {} devices) {}).dev or "") sortedDeviceList} + + # and then mount the filesystems in alphabetical order + ${concatStrings (attrValues fsMounts)} + ''; + + /* takes a disko device specification and returns a string which unmounts, destroys all disks and then runs create and mount + + zapCreateMount :: lib.types.devices -> str + */ + zapCreateMount = devices: '' + set -efux + umount -Rv "${rootMountPoint}" || : + + 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 :: lib.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 :: lib.types.devices -> pkgs -> [ derivation ] + */ + packages = devices: pkgs: unique (flatten (map (dev: dev._pkgs pkgs) (flatten (map attrValues (attrValues devices))))); + }; + + optionTypes = rec { + filename = lib.mkOptionType { + name = "filename"; + check = isString; + merge = mergeOneOption; + description = "A filename"; + }; + + absolute-pathname = lib.mkOptionType { + name = "absolute pathname"; + check = x: isString x && substring 0 1 x == "/" && pathname.check x; + merge = mergeOneOption; + description = "An absolute path"; + }; + + pathname = lib.mkOptionType { + name = "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; + description = "A path name"; + }; + }; + + /* topLevel type of the disko config, takes attrsets of disks, mdadms, zpools, nodevs, and lvm vgs. + */ + devices = lib.types.submodule { + options = { + disk = lib.mkOption { + type = lib.types.attrsOf subTypes.disk; + default = { }; + description = "Block device"; + }; + mdadm = lib.mkOption { + type = lib.types.attrsOf subTypes.mdadm; + default = { }; + description = "mdadm device"; + }; + zpool = lib.mkOption { + type = lib.types.attrsOf subTypes.zpool; + default = { }; + description = "ZFS pool device"; + }; + lvm_vg = lib.mkOption { + type = lib.types.attrsOf subTypes.lvm_vg; + default = { }; + description = "LVM VG device"; + }; + nodev = lib.mkOption { + type = lib.types.attrsOf subTypes.nodev; + default = { }; + description = "A non-block device"; + }; + }; + }; + + subTypes = lib.mapAttrs (_: module: diskoLib.mkSubType module) { + nodev = ./nodev.nix; + btrfs = ./btrfs.nix; + btrfs_subvol = ./btrfs_subvol.nix; + filesystem = ./filesystem.nix; + table = ./table.nix; + partition = ./partition.nix; + swap = ./swap.nix; + lvm_pv = ./lvm_pv.nix; + lvm_vg = ./lvm_vg.nix; + lvm_lv = ./lvm_lv.nix; + zfs = ./zfs.nix; + zpool = ./zpool.nix; + zfs_dataset = ./zfs_dataset.nix; + mdadm = ./mdadm.nix; + mdraid = ./mdraid.nix; + luks = ./luks.nix; + disk = ./disk.nix; + }; +} diff --git a/types/disk.nix b/types/disk.nix new file mode 100644 index 0000000..ab59de9 --- /dev/null +++ b/types/disk.nix @@ -0,0 +1,52 @@ +{ config, options, lib, diskoLib, optionTypes, ... }: +{ + options = { + name = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + description = "Device name"; + }; + type = lib.mkOption { + type = lib.types.enum [ "disk" ]; + default = "disk"; + internal = true; + description = "Type"; + }; + device = lib.mkOption { + type = optionTypes.absolute-pathname; # TODO check if subpath of /dev ? - No! eg: /.swapfile + description = "Device path"; + }; + content = diskoLib.deviceType; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = diskoLib.jsonType; + default = + lib.optionalAttrs (!isNull config.content) (config.content._meta [ "disk" config.device ]); + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = {}: config.content._create { dev = config.device; }; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = {}: + lib.optionalAttrs (!isNull config.content) (config.content._mount { dev = config.device; }); + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = + lib.optional (!isNull config.content) (config.content._config config.device); + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: [ pkgs.jq ] ++ lib.optionals (!isNull config.content) (config.content._pkgs pkgs); + description = "Packages"; + }; + }; +} diff --git a/types/filesystem.nix b/types/filesystem.nix new file mode 100644 index 0000000..c9a3319 --- /dev/null +++ b/types/filesystem.nix @@ -0,0 +1,86 @@ +{ config, options, lib, diskoLib, optionTypes, rootMountPoint, ... }: +{ + options = { + type = lib.mkOption { + type = lib.types.enum [ "filesystem" ]; + internal = true; + description = "Type"; + }; + extraArgs = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Arguments to pass"; + }; + mountOptions = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "defaults" ]; + description = "Options to pass to mount"; + }; + mountpoint = lib.mkOption { + type = optionTypes.absolute-pathname; + description = "Path to mount the filesystem to"; + }; + format = lib.mkOption { + type = lib.types.str; + description = "Format of the filesystem"; + }; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = dev: { }; + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = { dev }: '' + mkfs.${config.format} \ + ${config.extraArgs} \ + ${dev} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = { dev }: { + fs.${config.mountpoint} = '' + if ! findmnt ${dev} "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then + mount ${dev} "${rootMountPoint}${config.mountpoint}" \ + -t "${config.format}" \ + ${lib.concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \ + -o X-mount.mkdir + fi + ''; + }; + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = dev: [{ + fileSystems.${config.mountpoint} = { + device = dev; + fsType = config.format; + options = config.mountOptions; + }; + }]; + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + # type = lib.types.functionTo (lib.types.listOf lib.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 if (config.format == "bcachefs") then [ pkgs.bcachefs-tools ] + else [ ] + ); + description = "Packages"; + }; + }; +} diff --git a/types/luks.nix b/types/luks.nix new file mode 100644 index 0000000..07d685f --- /dev/null +++ b/types/luks.nix @@ -0,0 +1,73 @@ +{ config, options, lib, diskoLib, optionTypes, ... }: +{ + options = { + type = lib.mkOption { + type = lib.types.enum [ "luks" ]; + internal = true; + description = "Type"; + }; + name = lib.mkOption { + type = lib.types.str; + description = "Name of the LUKS"; + }; + keyFile = lib.mkOption { + type = lib.types.nullOr optionTypes.absolute-pathname; + default = null; + description = "Path to the key for encryption"; + }; + extraArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Extra arguments"; + }; + content = diskoLib.deviceType; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = dev: + lib.optionalAttrs (!isNull config.content) (config.content._meta dev); + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = { dev }: '' + cryptsetup -q luksFormat ${dev} ${diskoLib.maybeStr config.keyFile} ${toString config.extraArgs} + cryptsetup luksOpen ${dev} ${config.name} ${lib.optionalString (!isNull config.keyFile) "--key-file ${config.keyFile}"} + ${lib.optionalString (!isNull config.content) (config.content._create {dev = "/dev/mapper/${config.name}";})} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = { dev }: + let + contentMount = config.content._mount { dev = "/dev/mapper/${config.name}"; }; + in + { + dev = '' + cryptsetup status ${config.name} >/dev/null 2>/dev/null || + cryptsetup luksOpen ${dev} ${config.name} ${lib.optionalString (!isNull config.keyFile) "--key-file ${config.keyFile}"} + ${lib.optionalString (!isNull config.content) contentMount.dev or ""} + ''; + fs = lib.optionalAttrs (!isNull config.content) contentMount.fs or { }; + }; + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = dev: + [ + # TODO do we need this always in initrd and only there? + { boot.initrd.luks.devices.${config.name}.device = dev; } + ] ++ (lib.optional (!isNull config.content) (config.content._config "/dev/mapper/${config.name}")); + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: [ pkgs.cryptsetup ] ++ (lib.optionals (!isNull config.content) (config.content._pkgs pkgs)); + description = "Packages"; + }; + }; +} diff --git a/types/lvm_lv.nix b/types/lvm_lv.nix new file mode 100644 index 0000000..6b1328b --- /dev/null +++ b/types/lvm_lv.nix @@ -0,0 +1,76 @@ +{ config, options, lib, diskoLib, ... }: +{ + options = { + name = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + description = "Name of the logical volume"; + }; + type = lib.mkOption { + type = lib.types.enum [ "lvm_lv" ]; + default = "lvm_lv"; + internal = true; + description = "Type"; + }; + size = lib.mkOption { + type = lib.types.str; # TODO lvm size type + description = "Size of the logical volume"; + }; + lvm_type = lib.mkOption { + type = lib.types.nullOr (lib.types.enum [ "mirror" "raid0" "raid1" ]); # TODO add all lib.types + default = null; # maybe there is always a default type? + description = "LVM type"; + }; + extraArgs = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Extra arguments"; + }; + content = diskoLib.partitionType; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = dev: + lib.optionalAttrs (!isNull config.content) (config.content._meta dev); + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = { vg }: '' + lvcreate \ + --yes \ + ${if lib.hasInfix "%" config.size then "-l" else "-L"} ${config.size} \ + -n ${config.name} \ + ${lib.optionalString (!isNull config.lvm_type) "--type=${config.lvm_type}"} \ + ${config.extraArgs} \ + ${vg} + ${lib.optionalString (!isNull config.content) (config.content._create {dev = "/dev/${vg}/${config.name}";})} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = { vg }: + lib.optionalAttrs (!isNull config.content) (config.content._mount { dev = "/dev/${vg}/${config.name}"; }); + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = vg: + [ + (lib.optional (!isNull config.content) (config.content._config "/dev/${vg}/${config.name}")) + (lib.optional (!isNull config.lvm_type) { + boot.initrd.kernelModules = [ "dm-${config.lvm_type}" ]; + }) + ]; + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: lib.optionals (!isNull config.content) (config.content._pkgs pkgs); + description = "Packages"; + }; + }; +} diff --git a/types/lvm_pv.nix b/types/lvm_pv.nix new file mode 100644 index 0000000..03a95c7 --- /dev/null +++ b/types/lvm_pv.nix @@ -0,0 +1,48 @@ +{ config, options, lib, diskoLib, ... }: +{ + options = { + type = lib.mkOption { + type = lib.types.enum [ "lvm_pv" ]; + internal = true; + description = "Type"; + }; + vg = lib.mkOption { + type = lib.types.str; + description = "Volume group"; + }; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = dev: { + deviceDependencies.lvm_vg.${config.vg} = [ dev ]; + }; + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = { dev }: '' + pvcreate ${dev} + echo "${dev}" >> $disko_devices_dir/lvm_${config.vg} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = { dev }: + { }; + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = dev: [ ]; + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: [ pkgs.lvm2 ]; + description = "Packages"; + }; + }; +} diff --git a/types/lvm_vg.nix b/types/lvm_vg.nix new file mode 100644 index 0000000..786fbd7 --- /dev/null +++ b/types/lvm_vg.nix @@ -0,0 +1,63 @@ +{ config, options, lib, diskoLib, subTypes, ... }: +{ + options = { + name = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + description = "Name of the volume gorup"; + }; + type = lib.mkOption { + type = lib.types.enum [ "lvm_vg" ]; + internal = true; + description = "Type"; + }; + lvs = lib.mkOption { + type = lib.types.attrsOf subTypes.lvm_lv; + default = { }; + description = "LVS for the volume group"; + }; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = diskoLib.jsonType; + default = + diskoLib.deepMergeMap (lv: lv._meta [ "lvm_vg" config.name ]) (lib.attrValues config.lvs); + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = {}: '' + vgcreate ${config.name} $(tr '\n' ' ' < $disko_devices_dir/lvm_${config.name}) + ${lib.concatMapStrings (lv: lv._create {vg = config.name; }) (lib.attrValues config.lvs)} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = {}: + let + lvMounts = diskoLib.deepMergeMap (lv: lv._mount { vg = config.name; }) (lib.attrValues config.lvs); + in + { + dev = '' + vgchange -a y + ${lib.concatMapStrings (x: x.dev or "") (lib.attrValues lvMounts)} + ''; + fs = lvMounts.fs; + }; + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = + map (lv: lv._config config.name) (lib.attrValues config.lvs); + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: lib.flatten (map (lv: lv._pkgs pkgs) (lib.attrValues config.lvs)); + description = "Packages"; + }; + }; +} diff --git a/types/mdadm.nix b/types/mdadm.nix new file mode 100644 index 0000000..d2e88d0 --- /dev/null +++ b/types/mdadm.nix @@ -0,0 +1,69 @@ +{ config, options, lib, diskoLib, ... }: +{ + options = { + name = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + description = "Name"; + }; + type = lib.mkOption { + type = lib.types.enum [ "mdadm" ]; + default = "mdadm"; + internal = true; + description = "Type"; + }; + level = lib.mkOption { + type = lib.types.int; + default = 1; + description = "mdadm level"; + }; + metadata = lib.mkOption { + type = lib.types.enum [ "1" "1.0" "1.1" "1.2" "default" "ddf" "imsm" ]; + default = "default"; + description = "Metadata"; + }; + content = diskoLib.deviceType; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = diskoLib.jsonType; + default = + lib.optionalAttrs (!isNull config.content) (config.content._meta [ "mdadm" config.name ]); + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = {}: '' + echo 'y' | mdadm --create /dev/md/${config.name} \ + --level=${toString config.level} \ + --raid-devices=$(wc -l $disko_devices_dir/raid_${config.name} | cut -f 1 -d " ") \ + --metadata=${config.metadata} \ + --force \ + --homehost=any \ + $(tr '\n' ' ' < $disko_devices_dir/raid_${config.name}) + udevadm trigger --subsystem-match=block; udevadm settle + ${lib.optionalString (!isNull config.content) (config.content._create {dev = "/dev/md/${config.name}";})} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = {}: + lib.optionalAttrs (!isNull config.content) (config.content._mount { dev = "/dev/md/${config.name}"; }); + # TODO we probably need to assemble the mdadm somehow + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = + lib.optional (!isNull config.content) (config.content._config "/dev/md/${config.name}"); + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: (lib.optionals (!isNull config.content) (config.content._pkgs pkgs)); + description = "Packages"; + }; + }; +} diff --git a/types/mdraid.nix b/types/mdraid.nix new file mode 100644 index 0000000..6eb24bc --- /dev/null +++ b/types/mdraid.nix @@ -0,0 +1,48 @@ +{ config, options, lib, diskoLib, ... }: +{ + options = { + type = lib.mkOption { + type = lib.types.enum [ "mdraid" ]; + internal = true; + description = "Type"; + }; + + name = lib.mkOption { + type = lib.types.str; + description = "Name"; + }; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = dev: { + deviceDependencies.mdadm.${config.name} = [ dev ]; + }; + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = { dev }: '' + echo "${dev}" >> $disko_devices_dir/raid_${config.name} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = { dev }: + { }; + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = dev: [ ]; + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: [ pkgs.mdadm ]; + description = "Packages"; + }; + }; +} diff --git a/types/nodev.nix b/types/nodev.nix new file mode 100644 index 0000000..4d4a11e --- /dev/null +++ b/types/nodev.nix @@ -0,0 +1,72 @@ +{ lib, config, options, diskoLib, optionTypes, rootMountPoint, ... }: +{ + options = { + type = lib.mkOption { + type = lib.types.enum [ "nodev" ]; + default = "nodev"; + internal = true; + description = "Device type"; + }; + fsType = lib.mkOption { + type = lib.types.str; + description = "File system type"; + }; + device = lib.mkOption { + type = lib.types.str; + default = "none"; + description = "Device to use"; + }; + mountpoint = lib.mkOption { + type = optionTypes.absolute-pathname; + default = config._module.args.name; + description = "Location to mount the file system at"; + }; + mountOptions = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "defaults" ]; + description = "Options to pass to mount"; + }; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = diskoLib.jsonType; + default = { }; + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = {}: ""; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = {}: { + fs.${config.mountpoint} = '' + if ! findmnt ${config.fsType} "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then + mount -t ${config.fsType} ${config.device} "${rootMountPoint}${config.mountpoint}" \ + ${lib.concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \ + -o X-mount.mkdir + fi + ''; + }; + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = [{ + fileSystems.${config.mountpoint} = { + device = config.device; + fsType = config.fsType; + options = config.mountOptions; + }; + }]; + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: [ ]; + description = "Packages"; + }; + }; +} diff --git a/types/partition.nix b/types/partition.nix new file mode 100644 index 0000000..7ca0379 --- /dev/null +++ b/types/partition.nix @@ -0,0 +1,100 @@ +{ config, options, lib, diskoLib, ... }: +{ + options = { + type = lib.mkOption { + type = lib.types.enum [ "partition" ]; + internal = true; + description = "Type"; + }; + part-type = lib.mkOption { + type = lib.types.enum [ "primary" "logical" "extended" ]; + default = "primary"; + description = "Partition type"; + }; + fs-type = lib.mkOption { + type = lib.types.nullOr (lib.types.enum [ "btrfs" "ext2" "ext3" "ext4" "fat16" "fat32" "hfs" "hfs+" "linux-swap" "ntfs" "reiserfs" "udf" "xfs" ]); + default = null; + description = "Filesystem type to use"; + }; + name = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "Name of the partition"; + }; + start = lib.mkOption { + type = lib.types.str; + default = "0%"; + description = "Start of the partition"; + }; + end = lib.mkOption { + type = lib.types.str; + default = "100%"; + description = "End of the partition"; + }; + index = lib.mkOption { + type = lib.types.int; + # TODO find a better way to get the index + default = lib.toInt (lib.head (builtins.match ".*entry ([[:digit:]]+)]" config._module.args.name)); + description = "Index of the partition"; + }; + flags = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Partition flags"; + }; + bootable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to make the partition bootable"; + }; + content = diskoLib.partitionType; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = dev: + lib.optionalAttrs (!isNull config.content) (config.content._meta dev); + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = { dev, type }: '' + ${lib.optionalString (type == "gpt") '' + parted -s ${dev} -- mkpart ${config.name} ${diskoLib.maybeStr config.fs-type} ${config.start} ${config.end} + ''} + ${lib.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 + ${lib.optionalString (config.bootable) '' + parted -s ${dev} -- set ${toString config.index} boot on + ''} + ${lib.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 + ${lib.optionalString (!isNull config.content) (config.content._create {dev = (diskoLib.deviceNumbering dev config.index);})} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = { dev }: + lib.optionalAttrs (!isNull config.content) (config.content._mount { dev = (diskoLib.deviceNumbering dev config.index); }); + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = dev: + lib.optional (!isNull config.content) (config.content._config (diskoLib.deviceNumbering dev config.index)); + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: lib.optionals (!isNull config.content) (config.content._pkgs pkgs); + description = "Packages"; + }; + }; +} diff --git a/types/swap.nix b/types/swap.nix new file mode 100644 index 0000000..60a7aff --- /dev/null +++ b/types/swap.nix @@ -0,0 +1,56 @@ +{ diskoLib, config, options, lib, ... }: +{ + options = { + type = lib.mkOption { + type = lib.types.enum [ "swap" ]; + internal = true; + description = "Type"; + }; + randomEncryption = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to randomly encrypt the swap"; + }; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = dev: { }; + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = { dev }: '' + mkswap ${dev} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = { dev }: { + fs.${dev} = '' + if ! swapon --show | grep -q '^${dev} '; then + swapon ${dev} + fi + ''; + }; + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = dev: [{ + swapDevices = [{ + device = dev; + randomEncryption = config.randomEncryption; + }]; + }]; + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: [ pkgs.gnugrep pkgs.util-linux ]; + description = "Packages"; + }; + }; +} diff --git a/types/table.nix b/types/table.nix new file mode 100644 index 0000000..fee81a5 --- /dev/null +++ b/types/table.nix @@ -0,0 +1,63 @@ +{ config, options, lib, diskoLib, subTypes, ... }: +{ + options = { + type = lib.mkOption { + type = lib.types.enum [ "table" ]; + internal = true; + description = "Partition table"; + }; + format = lib.mkOption { + type = lib.types.enum [ "gpt" "msdos" ]; + default = "gpt"; + description = "The kind of partition table"; + }; + partitions = lib.mkOption { + type = lib.types.listOf subTypes.partition; + default = [ ]; + description = "List of partitions to add to the partition table"; + }; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = dev: + diskoLib.deepMergeMap (partition: partition._meta dev) config.partitions; + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = { dev }: '' + parted -s ${dev} -- mklabel ${config.format} + ${lib.concatMapStrings (partition: partition._create {inherit dev; type = config.format;} ) config.partitions} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = { dev }: + let + partMounts = diskoLib.deepMergeMap (partition: partition._mount { inherit dev; }) config.partitions; + in + { + dev = '' + ${lib.concatMapStrings (x: x.dev or "") (lib.attrValues partMounts)} + ''; + fs = partMounts.fs or { }; + }; + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = dev: + map (partition: partition._config dev) config.partitions; + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: + [ pkgs.parted pkgs.systemdMinimal ] ++ lib.flatten (map (partition: partition._pkgs pkgs) config.partitions); + description = "Packages"; + }; + }; +} diff --git a/types/zfs.nix b/types/zfs.nix new file mode 100644 index 0000000..b30ad44 --- /dev/null +++ b/types/zfs.nix @@ -0,0 +1,47 @@ +{ config, options, lib, diskoLib, ... }: +{ + options = { + type = lib.mkOption { + type = lib.types.enum [ "zfs" ]; + internal = true; + description = "Type"; + }; + pool = lib.mkOption { + type = lib.types.str; + description = "Name of the ZFS pool"; + }; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = dev: { + deviceDependencies.zpool.${config.pool} = [ dev ]; + }; + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = { dev }: '' + echo "${dev}" >> $disko_devices_dir/zfs_${config.pool} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = { dev }: + { }; + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = dev: [ ]; + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: [ pkgs.zfs ]; + description = "Packages"; + }; + }; +} diff --git a/types/zfs_dataset.nix b/types/zfs_dataset.nix new file mode 100644 index 0000000..704e378 --- /dev/null +++ b/types/zfs_dataset.nix @@ -0,0 +1,103 @@ +{ config, options, lib, diskoLib, optionTypes, rootMountPoint, ... }: +{ + options = { + name = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + description = "Name of the dataset"; + }; + type = lib.mkOption { + type = lib.types.enum [ "zfs_dataset" ]; + default = "zfs_dataset"; + internal = true; + description = "Type"; + }; + zfs_type = lib.mkOption { + type = lib.types.enum [ "filesystem" "volume" ]; + description = "The type of the dataset"; + }; + options = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Options to set for the dataset"; + }; + mountOptions = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "defaults" ]; + description = "Mount options"; + }; + + # filesystem options + mountpoint = lib.mkOption { + type = lib.types.nullOr optionTypes.absolute-pathname; + default = null; + description = "Path to mount the dataset to"; + }; + + # volume options + size = lib.mkOption { + type = lib.types.nullOr lib.types.str; # TODO size + default = null; + description = "Size of the dataset"; + }; + + content = diskoLib.partitionType; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo diskoLib.jsonType; + default = dev: + lib.optionalAttrs (!isNull config.content) (config.content._meta dev); + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = { zpool }: '' + zfs create ${zpool}/${config.name} \ + ${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "-o ${n}=${v}") config.options)} \ + ${lib.optionalString (config.zfs_type == "volume") "-V ${config.size}"} + ${lib.optionalString (config.zfs_type == "volume") '' + udevadm trigger --subsystem-match=block; udevadm settle + ${lib.optionalString (!isNull config.content) (config.content._create {dev = "/dev/zvol/${zpool}/${config.name}";})} + ''} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = { zpool }: + lib.optionalAttrs (config.zfs_type == "volume" && !isNull config.content) (config.content._mount { dev = "/dev/zvol/${zpool}/${config.name}"; }) // + lib.optionalAttrs (config.zfs_type == "filesystem" && config.options.mountpoint or "" != "none") { + fs.${config.mountpoint} = '' + if ! findmnt ${zpool}/${config.name} "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then + mount ${zpool}/${config.name} "${rootMountPoint}${config.mountpoint}" \ + -o X-mount.mkdir \ + ${lib.concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \ + ${lib.optionalString ((config.options.mountpoint or "") != "legacy") "-o zfsutil"} \ + -t zfs + fi + ''; + }; + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = zpool: + (lib.optional (config.zfs_type == "volume" && !isNull config.content) (config.content._config "/dev/zvol/${zpool}/${config.name}")) ++ + (lib.optional (config.zfs_type == "filesystem" && config.options.mountpoint or "" != "none") { + fileSystems.${config.mountpoint} = { + device = "${zpool}/${config.name}"; + fsType = "zfs"; + options = config.mountOptions ++ lib.optional ((config.options.mountpoint or "") != "legacy") "zfsutil"; + }; + }); + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: [ pkgs.util-linux ] ++ lib.optionals (!isNull config.content) (config.content._pkgs pkgs); + description = "Packages"; + }; + }; +} diff --git a/types/zpool.nix b/types/zpool.nix new file mode 100644 index 0000000..18f21cf --- /dev/null +++ b/types/zpool.nix @@ -0,0 +1,110 @@ +{ config, options, lib, diskoLib, optionTypes, subTypes, rootMountPoint, ... }: +{ + options = { + name = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + description = "Name of the ZFS pool"; + }; + type = lib.mkOption { + type = lib.types.enum [ "zpool" ]; + default = "zpool"; + internal = true; + description = "Type"; + }; + mode = lib.mkOption { + type = lib.types.str; # TODO zfs modes + default = ""; + description = "Mode of the ZFS pool"; + }; + options = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Options for the ZFS pool"; + }; + rootFsOptions = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Options for the root filesystem"; + }; + mountpoint = lib.mkOption { + type = lib.types.nullOr optionTypes.absolute-pathname; + default = null; + description = "The mountpoint of the pool"; + }; + mountOptions = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "defaults" ]; + description = "Options to pass to mount"; + }; + datasets = lib.mkOption { + type = lib.types.attrsOf subTypes.zfs_dataset; + description = "List of datasets to define"; + }; + _meta = lib.mkOption { + internal = true; + readOnly = true; + type = diskoLib.jsonType; + default = + diskoLib.deepMergeMap (dataset: dataset._meta [ "zpool" config.name ]) (lib.attrValues config.datasets); + description = "Metadata"; + }; + _create = diskoLib.mkCreateOption { + inherit config options; + default = {}: '' + zpool create ${config.name} \ + ${config.mode} \ + ${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "-o ${n}=${v}") config.options)} \ + ${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "-O ${n}=${v}") config.rootFsOptions)} \ + $(tr '\n' ' ' < $disko_devices_dir/zfs_${config.name}) + ${lib.concatMapStrings (dataset: dataset._create {zpool = config.name;}) (lib.attrValues config.datasets)} + ''; + }; + _mount = diskoLib.mkMountOption { + inherit config options; + default = {}: + let + datasetMounts = diskoLib.deepMergeMap (dataset: dataset._mount { zpool = config.name; }) (lib.attrValues config.datasets); + in + { + dev = '' + zpool list '${config.name}' >/dev/null 2>/dev/null || zpool import '${config.name}' + ${lib.concatMapStrings (x: x.dev or "") (lib.attrValues datasetMounts)} + ''; + fs = datasetMounts.fs // lib.optionalAttrs (!isNull config.mountpoint) { + ${config.mountpoint} = '' + if ! findmnt ${config.name} "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then + mount ${config.name} "${rootMountPoint}${config.mountpoint}" \ + ${lib.optionalString ((config.options.mountpoint or "") != "legacy") "-o zfsutil"} \ + ${lib.concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \ + -o X-mount.mkdir \ + -t zfs + fi + ''; + }; + }; + }; + _config = lib.mkOption { + internal = true; + readOnly = true; + default = [ + (map (dataset: dataset._config config.name) (lib.attrValues config.datasets)) + (lib.optional (!isNull config.mountpoint) { + fileSystems.${config.mountpoint} = { + device = config.name; + fsType = "zfs"; + options = config.mountOptions ++ lib.optional ((config.options.mountpoint or "") != "legacy") "zfsutil"; + }; + }) + ]; + description = "NixOS configuration"; + }; + _pkgs = lib.mkOption { + internal = true; + readOnly = true; + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = pkgs: [ pkgs.util-linux ] ++ lib.flatten (map (dataset: dataset._pkgs pkgs) (lib.attrValues config.datasets)); + description = "Packages"; + }; + }; +}