{ lib, pkgs, config, inputs, ... }: let # there currently is a bug with disko that causes long disk names to be generated improperly this hash function should alleviate it when used for disk names instead of what we are defaulting to # max gpt length is 36 and disk adds formats it like disk-xxxx-zfs which means we need to be 9 characters under that hashDisk = drive: (builtins.substring 0 27 (builtins.hashString "sha256" drive)); vdevs = builtins.map ( disks: builtins.map (disk: lib.attrsets.nameValuePair (hashDisk disk) disk) disks ) config.host.storage.pool.vdevs; cache = builtins.map ( disk: lib.attrsets.nameValuePair (hashDisk disk) disk ) config.host.storage.pool.cache; datasets = config.host.storage.pool.datasets // config.host.storage.pool.extraDatasets; in { options.host.storage = { enable = lib.mkEnableOption "are we going create zfs disks with disko on this device"; encryption = lib.mkEnableOption "is the vdev going to be encrypted"; notifications = { enable = lib.mkEnableOption "are notifications enabled"; host = lib.mkOption { type = lib.types.str; description = "what is the host that we are going to send the email to"; }; port = lib.mkOption { type = lib.types.port; description = "what port is the host using to receive mail on"; }; to = lib.mkOption { type = lib.types.str; description = "what account is the email going to be sent to"; }; user = lib.mkOption { type = lib.types.str; description = "what user is the email going to be set from"; }; tokenFile = lib.mkOption { type = lib.types.str; description = "file containing the password to be used by msmtp for notifications"; }; }; pool = { mode = lib.mkOption { type = lib.types.str; default = "raidz2"; description = "what level of redundancy should this pool have"; }; # list of drives in pool that will have a boot partition put onto them bootDrives = lib.mkOption { type = lib.types.listOf lib.types.str; description = "list of disks that are going to have a boot partition installed on them"; default = lib.lists.flatten config.host.storage.pool.vdevs; }; # shorthand for vdevs if you only have 1 vdev drives = lib.mkOption { type = lib.types.listOf lib.types.str; description = "list of drives that are going to be in the vdev"; default = []; }; # list of all drives in each vdev vdevs = lib.mkOption { type = lib.types.listOf (lib.types.listOf lib.types.str); description = "list of disks that are going to be in"; default = [config.host.storage.pool.drives]; }; # list of cache drives for pool cache = lib.mkOption { type = lib.types.listOf lib.types.str; description = "list of drives that are going to be used as cache"; default = []; }; # Default datasets that are needed to make a functioning system datasets = lib.mkOption { type = lib.types.attrsOf (inputs.disko.lib.subType { types = {inherit (inputs.disko.lib.types) zfs_fs zfs_volume;}; }); default = { "local" = { type = "zfs_fs"; options.canmount = "off"; }; # nix directory needs to be available pre persist and doesn't need to be snapshotted or backed up "local/system/nix" = { type = "zfs_fs"; mountpoint = "/nix"; options = { atime = "off"; relatime = "off"; canmount = "on"; }; }; # dataset for root that gets rolled back on every boot "local/system/root" = { type = "zfs_fs"; mountpoint = "/"; options = { canmount = "on"; }; postCreateHook = '' zfs snapshot rpool/local/system/root@blank ''; }; }; }; extraDatasets = lib.mkOption { type = lib.types.attrsOf (inputs.disko.lib.subType { types = {inherit (inputs.disko.lib.types) zfs_fs zfs_volume;}; }); description = "List of datasets to define"; default = {}; }; }; }; config = lib.mkIf config.host.storage.enable { programs.msmtp = lib.mkIf config.host.storage.notifications.enable { enable = true; setSendmail = true; defaults = { aliases = "/etc/aliases"; port = config.host.storage.notifications.port; tls_trust_file = "/etc/ssl/certs/ca-certificates.crt"; tls = "on"; auth = "login"; tls_starttls = "off"; }; accounts = { zfs_notifications = { host = config.host.storage.notifications.host; passwordeval = "cat ${config.host.storage.notifications.tokenFile}"; user = config.host.storage.notifications.user; from = config.host.storage.notifications.user; }; }; }; services.zfs = { autoScrub.enable = true; autoSnapshot.enable = true; zed = lib.mkIf config.host.storage.notifications.enable { # this option is broken we are just going to disable it enableMail = false; settings = { ZED_DEBUG_LOG = "/tmp/zed.debug.log"; ZED_EMAIL_ADDR = [config.host.storage.notifications.to]; ZED_EMAIL_PROG = "${pkgs.msmtp}/bin/msmtp"; ZED_EMAIL_OPTS = "@ADDRESS@"; ZED_NOTIFY_INTERVAL_SECS = 3600; ZED_NOTIFY_VERBOSE = true; ZED_USE_ENCLOSURE_LEDS = true; ZED_SCRUB_AFTER_RESILVER = true; }; }; }; disko.devices = { disk = ( builtins.listToAttrs ( builtins.map (drive: lib.attrsets.nameValuePair (drive.name) { type = "disk"; device = "/dev/disk/by-id/${drive.value}"; content = { type = "gpt"; partitions = { ESP = lib.mkIf (builtins.elem drive.value config.host.storage.pool.bootDrives) { # The 2GB here for the boot partition might be a bit overkill we probably only need like 1/4th of that but storage is cheap size = "2G"; type = "EF00"; content = { type = "filesystem"; format = "vfat"; mountpoint = "/boot"; mountOptions = ["umask=0077"]; }; }; zfs = { size = "100%"; content = { type = "zfs"; pool = "rpool"; }; }; }; }; }) ( (lib.lists.flatten vdevs) ++ cache ) ) ); zpool = { rpool = { type = "zpool"; mode = { topology = { type = "topology"; vdev = ( builtins.map (disks: { mode = config.host.storage.pool.mode; members = builtins.map (disk: disk.name) disks; }) vdevs ); cache = builtins.map (disk: disk.name) cache; }; }; options = { ashift = "12"; autotrim = "on"; }; rootFsOptions = { canmount = "off"; mountpoint = "none"; xattr = "sa"; acltype = "posixacl"; relatime = "on"; compression = "lz4"; "com.sun:auto-snapshot" = "false"; } // ( lib.attrsets.optionalAttrs config.host.storage.encryption { encryption = "on"; keyformat = "hex"; keylocation = "prompt"; } ); datasets = lib.mkMerge [ ( lib.attrsets.mapAttrs (name: value: { type = value.type; options = value.options; mountpoint = value.mountpoint; postCreateHook = value.postCreateHook; }) datasets ) ]; }; }; }; }; }