diff --git a/modules/nixos-modules/storage/impermanence.nix b/modules/nixos-modules/storage/impermanence.nix index 470ce48..6619bc5 100644 --- a/modules/nixos-modules/storage/impermanence.nix +++ b/modules/nixos-modules/storage/impermanence.nix @@ -70,6 +70,7 @@ in { lib.mapAttrs (datasetName: dataset: { enable = true; hideMounts = true; + persistentStoragePath = "/${datasetName}"; directories = lib.mapAttrsToList (path: dirConfig: { directory = path; user = dirConfig.owner.name; @@ -78,9 +79,11 @@ in { }) (lib.filterAttrs (_: dirConfig: dirConfig.enable) dataset.directories); files = lib.mapAttrsToList (path: fileConfig: { file = path; - user = fileConfig.owner.name; - group = fileConfig.group.name; - mode = permissionsToMode fileConfig; + parentDirectory = { + user = fileConfig.owner.name; + group = fileConfig.group.name; + mode = permissionsToMode fileConfig; + }; }) (lib.filterAttrs (_: fileConfig: fileConfig.enable) dataset.files); }) config.storage.impermanence.datasets; diff --git a/modules/nixos-modules/storage/storage.nix b/modules/nixos-modules/storage/storage.nix index 06e29f1..b6428f6 100644 --- a/modules/nixos-modules/storage/storage.nix +++ b/modules/nixos-modules/storage/storage.nix @@ -32,13 +32,6 @@ autoSnapshot = false; }; }; - "persist/system/root" = { - type = "zfs_fs"; - mount = { - enable = true; - mountPoint = "/"; - }; - }; }; } (lib.mkIf (!config.storage.impermanence.enable) { @@ -46,6 +39,10 @@ storage.zfs.datasets = { "persist/system/root" = { type = "zfs_fs"; + mount = { + enable = false; + mountPoint = "/"; + }; snapshot = { autoSnapshot = true; }; @@ -55,6 +52,10 @@ (lib.mkIf config.storage.impermanence.enable { storage.impermanence.datasets = { "persist/system/root" = { + mount = { + enable = false; + mountPoint = "/"; + }; directories = { "/var/lib/nixos".enable = true; "/var/lib/systemd/coredump".enable = true; diff --git a/modules/nixos-modules/storage/submodules/dataset.nix b/modules/nixos-modules/storage/submodules/dataset.nix index a3102fc..3de7719 100644 --- a/modules/nixos-modules/storage/submodules/dataset.nix +++ b/modules/nixos-modules/storage/submodules/dataset.nix @@ -44,12 +44,12 @@ mount = { enable = lib.mkOption { - type = lib.types.nullOr (lib.types.either lib.types.bool (lib.types.enum ["on" "off" "noauto"])); - default = null; + type = lib.types.either lib.types.bool (lib.types.enum ["on" "off" "noauto"]); + default = true; + description = "Whether and how the dataset should be mounted"; }; mountPoint = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; + type = lib.types.str; description = "Controls the mount point used for this file system"; }; }; @@ -57,18 +57,15 @@ encryption = { enable = lib.mkEnableOption "should encryption be enabled"; type = lib.mkOption { - type = lib.types.nullOr (lib.types.enum ["aes-128-ccm" "aes-192-ccm" "aes-256-ccm" "aes-128-gcm" "aes-192-gcm" "aes-256-gcm"]); - default = null; + type = lib.types.enum ["aes-128-ccm" "aes-192-ccm" "aes-256-ccm" "aes-128-gcm" "aes-192-gcm" "aes-256-gcm"]; description = "What encryption type to use"; }; keyformat = lib.mkOption { - type = lib.types.nullOr (lib.types.enum ["raw" "hex" "passphrase"]); - default = null; + type = lib.types.enum ["raw" "hex" "passphrase"]; description = "Format of the encryption key"; }; keylocation = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; + type = lib.types.str; description = "Location of the encryption key"; }; }; @@ -77,14 +74,11 @@ # This option should set this option flag # "com.sun:auto-snapshot" = "false"; autoSnapshot = lib.mkOption { - type = lib.types.nullOr lib.types.bool; - default = null; + type = lib.types.bool; + default = false; description = "Enable automatic snapshots for this dataset"; }; - # TODO: this is what blank snapshot should set - # postCreateHook = '' - # zfs snapshot rpool/local/system/root@blank - # ''; + # Creates a blank snapshot in the post create hook for rollback purposes blankSnapshot = lib.mkEnableOption "Should a blank snapshot be auto created in the post create hook"; }; diff --git a/modules/nixos-modules/storage/submodules/impermanenceDataset.nix b/modules/nixos-modules/storage/submodules/impermanenceDataset.nix index 5f47c18..7154e90 100644 --- a/modules/nixos-modules/storage/submodules/impermanenceDataset.nix +++ b/modules/nixos-modules/storage/submodules/impermanenceDataset.nix @@ -49,6 +49,7 @@ in { config = { mount = { mountPoint = lib.mkDefault "/${name}"; + enable = lib.mkDefault true; }; }; } diff --git a/modules/nixos-modules/storage/zfs.nix b/modules/nixos-modules/storage/zfs.nix index 65ddbd0..451e226 100644 --- a/modules/nixos-modules/storage/zfs.nix +++ b/modules/nixos-modules/storage/zfs.nix @@ -5,6 +5,98 @@ args @ { ... }: let datasetSubmodule = (import ./submodules/dataset.nix) args; + + # Hash function for disk names (max 27 chars to fit GPT limitations) + hashDisk = drive: (builtins.substring 0 27 (builtins.hashString "sha256" drive)); + + # Helper to flatten vdevs into list of devices with names + allVdevDevices = lib.lists.flatten (builtins.map ( + vdev: + builtins.map ( + device: + lib.attrsets.nameValuePair (hashDisk device.device) device + ) + vdev + ) + config.storage.zfs.pool.vdevs); + + # Cache devices with names + allCacheDevices = builtins.map ( + device: + lib.attrsets.nameValuePair (hashDisk device.device) device + ) (config.storage.zfs.pool.cache); + + # All devices (vdevs + cache) + allDevices = allVdevDevices ++ allCacheDevices; + + # Boot devices - filter devices that have boot = true + bootDevices = builtins.filter (device: device.value.boot) allDevices; + + # Helper function to convert dataset options to ZFS properties + datasetToZfsOptions = dataset: let + baseOptions = + (lib.attrsets.optionalAttrs (dataset.acltype != null) {acltype = dataset.acltype;}) + // (lib.attrsets.optionalAttrs (dataset.relatime != null) {relatime = dataset.relatime;}) + // (lib.attrsets.optionalAttrs (dataset.atime != null) {atime = dataset.atime;}) + // (lib.attrsets.optionalAttrs (dataset.xattr != null) {xattr = dataset.xattr;}) + // (lib.attrsets.optionalAttrs (dataset.compression != null) {compression = dataset.compression;}) + // (lib.attrsets.optionalAttrs (dataset.sync != null) {sync = dataset.sync;}) + // (lib.attrsets.optionalAttrs (dataset.recordSize != null) {recordSize = dataset.recordSize;}); + + encryptionOptions = lib.attrsets.optionalAttrs (dataset.encryption.enable) ( + (lib.attrsets.optionalAttrs (dataset.encryption ? type) {encryption = dataset.encryption.type;}) + // (lib.attrsets.optionalAttrs (dataset.encryption ? keyformat) {keyformat = dataset.encryption.keyformat;}) + // (lib.attrsets.optionalAttrs (dataset.encryption ? keylocation) {keylocation = dataset.encryption.keylocation;}) + ); + + mountOptions = lib.attrsets.optionalAttrs (dataset ? mount && dataset.mount ? enable) ( + if builtins.isBool dataset.mount.enable + then { + canmount = + if dataset.mount.enable + then "on" + else "off"; + } + else {canmount = dataset.mount.enable;} + ); + + snapshotOptions = lib.attrsets.optionalAttrs (dataset ? snapshot && dataset.snapshot ? autoSnapshot) { + "com.sun:auto-snapshot" = + if dataset.snapshot.autoSnapshot + then "true" + else "false"; + }; + in + baseOptions // encryptionOptions // mountOptions // snapshotOptions; + + # Helper to generate post create hooks + generatePostCreateHook = name: dataset: + dataset.postCreateHook + + (lib.optionalString dataset.snapshot.blankSnapshot '' + zfs snapshot rpool/${name}@blank + ''); + + # Convert datasets to disko format + convertedDatasets = builtins.listToAttrs ( + (lib.attrsets.mapAttrsToList ( + name: dataset: + lib.attrsets.nameValuePair name { + type = dataset.type; + options = datasetToZfsOptions dataset; + mountpoint = dataset.mount.mountPoint or null; + postCreateHook = generatePostCreateHook name dataset; + } + ) + config.storage.zfs.datasets) + ++ (lib.optional (config.storage.zfs.rootDataset != null) ( + lib.attrsets.nameValuePair "" { + type = config.storage.zfs.rootDataset.type; + options = datasetToZfsOptions config.storage.zfs.rootDataset; + mountpoint = config.storage.zfs.rootDataset.mount.mountPoint or null; + postCreateHook = generatePostCreateHook "" config.storage.zfs.rootDataset; + } + )) + ); in { options.storage = { zfs = { @@ -39,12 +131,14 @@ in { lib.types.coercedTo lib.types.str (device: { device = device; boot = false; - }) { - device = lib.mkOption { - type = lib.types.str; + }) (lib.types.submodule { + options = { + device = lib.mkOption { + type = lib.types.str; + }; + boot = lib.mkEnableOption "should this device be a boot device"; }; - boot = lib.mkEnableOption "should this device be a boot device"; - }; + }); in { encryption = { enable = lib.mkEnableOption "Should encryption be enabled on this pool."; @@ -75,15 +169,15 @@ in { description = "List of vdevs, where each vdev is a list of devices"; }; cache = lib.mkOption { - type = lib.types.attrsOf deviceType; + type = lib.types.listOf deviceType; default = {}; }; }; rootDataset = lib.mkOption { - type = lib.types.submodule datasetSubmodule; + type = lib.types.nullOr (lib.types.submodule datasetSubmodule); description = "Root ZFS dataset to create"; - default = {}; + default = null; }; datasets = lib.mkOption { @@ -96,15 +190,109 @@ in { config = lib.mkIf config.storage.zfs.enable (lib.mkMerge [ { - services.zfs = { - autoScrub.enable = true; - autoSnapshot.enable = true; - }; + # Assertion that we have at least one boot device + assertions = [ + { + assertion = (builtins.length bootDevices) > 0; + message = "ZFS configuration requires at least one boot device. Set boot = true for at least one device in your vdevs or cache."; + } + ]; - # TODO: configure disko - # TODO: assertion that we have a boot device - # TODO: check that disks on system match configuration and warn user if they don't - # TODO: check that datasets on system match configuration and warn user if they don't + # # Warning about disk/dataset mismatches - these would be runtime checks + # warnings = let + # configuredDisks = builtins.map (device: device.device) (builtins.map (dev: dev.value) allDevices); + # diskWarnings = + # lib.optional (config.storage.zfs.enable) + # "ZFS: Please ensure the following disks are available on your system: ${builtins.concatStringsSep ", " configuredDisks}"; + + # configuredDatasets = builtins.attrNames config.storage.zfs.datasets; + # datasetWarnings = + # lib.optional (config.storage.zfs.enable && (builtins.length configuredDatasets) > 0) + # "ZFS: Configured datasets: ${builtins.concatStringsSep ", " configuredDatasets}. Ensure these match your intended ZFS layout."; + # in + # diskWarnings ++ datasetWarnings; + + # services.zfs = { + # autoScrub.enable = true; + # autoSnapshot.enable = true; + # }; + + # # Configure disko for ZFS setup + disko.devices = { + disk = builtins.listToAttrs ( + builtins.map ( + drive: + lib.attrsets.nameValuePair (drive.name) { + type = "disk"; + device = "/dev/disk/by-id/${drive.value.device}"; + content = { + type = "gpt"; + partitions = { + ESP = lib.mkIf drive.value.boot { + size = config.storage.zfs.pool.bootPartitionSize; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = ["umask=0077"]; + }; + }; + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "rpool"; + }; + }; + }; + }; + } + ) + allDevices + ); + + zpool = { + rpool = { + type = "zpool"; + mode = { + topology = { + type = "topology"; + vdev = + builtins.map (vdev: { + mode = config.storage.zfs.pool.mode; + members = builtins.map (device: hashDisk device.device) vdev; + }) + config.storage.zfs.pool.vdevs; + cache = builtins.map (device: hashDisk device.device) (builtins.attrValues config.storage.zfs.pool.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.storage.zfs.pool.encryption.enable { + encryption = "on"; + keyformat = config.storage.zfs.pool.encryption.keyformat; + keylocation = config.storage.zfs.pool.encryption.keylocation; + }); + + datasets = convertedDatasets; + }; + }; + }; } (lib.mkIf config.storage.zfs.notifications.enable { programs.msmtp = {