storage-refactor #9

Open
jan-leila wants to merge 40 commits from storage-refactor into main
5 changed files with 229 additions and 42 deletions
Showing only changes of commit 3ca0e9bf0a - Show all commits

View file

@ -70,6 +70,7 @@ in {
lib.mapAttrs (datasetName: dataset: { lib.mapAttrs (datasetName: dataset: {
enable = true; enable = true;
hideMounts = true; hideMounts = true;
persistentStoragePath = "/${datasetName}";
directories = lib.mapAttrsToList (path: dirConfig: { directories = lib.mapAttrsToList (path: dirConfig: {
directory = path; directory = path;
user = dirConfig.owner.name; user = dirConfig.owner.name;
@ -78,9 +79,11 @@ in {
}) (lib.filterAttrs (_: dirConfig: dirConfig.enable) dataset.directories); }) (lib.filterAttrs (_: dirConfig: dirConfig.enable) dataset.directories);
files = lib.mapAttrsToList (path: fileConfig: { files = lib.mapAttrsToList (path: fileConfig: {
file = path; file = path;
parentDirectory = {
user = fileConfig.owner.name; user = fileConfig.owner.name;
group = fileConfig.group.name; group = fileConfig.group.name;
mode = permissionsToMode fileConfig; mode = permissionsToMode fileConfig;
};
}) (lib.filterAttrs (_: fileConfig: fileConfig.enable) dataset.files); }) (lib.filterAttrs (_: fileConfig: fileConfig.enable) dataset.files);
}) })
config.storage.impermanence.datasets; config.storage.impermanence.datasets;

View file

@ -32,13 +32,6 @@
autoSnapshot = false; autoSnapshot = false;
}; };
}; };
"persist/system/root" = {
type = "zfs_fs";
mount = {
enable = true;
mountPoint = "/";
};
};
}; };
} }
(lib.mkIf (!config.storage.impermanence.enable) { (lib.mkIf (!config.storage.impermanence.enable) {
@ -46,6 +39,10 @@
storage.zfs.datasets = { storage.zfs.datasets = {
"persist/system/root" = { "persist/system/root" = {
type = "zfs_fs"; type = "zfs_fs";
mount = {
enable = false;
mountPoint = "/";
};
snapshot = { snapshot = {
autoSnapshot = true; autoSnapshot = true;
}; };
@ -55,6 +52,10 @@
(lib.mkIf config.storage.impermanence.enable { (lib.mkIf config.storage.impermanence.enable {
storage.impermanence.datasets = { storage.impermanence.datasets = {
"persist/system/root" = { "persist/system/root" = {
mount = {
enable = false;
mountPoint = "/";
};
directories = { directories = {
"/var/lib/nixos".enable = true; "/var/lib/nixos".enable = true;
"/var/lib/systemd/coredump".enable = true; "/var/lib/systemd/coredump".enable = true;

View file

@ -44,12 +44,12 @@
mount = { mount = {
enable = lib.mkOption { enable = lib.mkOption {
type = lib.types.nullOr (lib.types.either lib.types.bool (lib.types.enum ["on" "off" "noauto"])); type = lib.types.either lib.types.bool (lib.types.enum ["on" "off" "noauto"]);
default = null; default = true;
description = "Whether and how the dataset should be mounted";
}; };
mountPoint = lib.mkOption { mountPoint = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.str;
default = null;
description = "Controls the mount point used for this file system"; description = "Controls the mount point used for this file system";
}; };
}; };
@ -57,18 +57,15 @@
encryption = { encryption = {
enable = lib.mkEnableOption "should encryption be enabled"; enable = lib.mkEnableOption "should encryption be enabled";
type = lib.mkOption { 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"]); type = lib.types.enum ["aes-128-ccm" "aes-192-ccm" "aes-256-ccm" "aes-128-gcm" "aes-192-gcm" "aes-256-gcm"];
default = null;
description = "What encryption type to use"; description = "What encryption type to use";
}; };
keyformat = lib.mkOption { keyformat = lib.mkOption {
type = lib.types.nullOr (lib.types.enum ["raw" "hex" "passphrase"]); type = lib.types.enum ["raw" "hex" "passphrase"];
default = null;
description = "Format of the encryption key"; description = "Format of the encryption key";
}; };
keylocation = lib.mkOption { keylocation = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.str;
default = null;
description = "Location of the encryption key"; description = "Location of the encryption key";
}; };
}; };
@ -77,14 +74,11 @@
# This option should set this option flag # This option should set this option flag
# "com.sun:auto-snapshot" = "false"; # "com.sun:auto-snapshot" = "false";
autoSnapshot = lib.mkOption { autoSnapshot = lib.mkOption {
type = lib.types.nullOr lib.types.bool; type = lib.types.bool;
default = null; default = false;
description = "Enable automatic snapshots for this dataset"; description = "Enable automatic snapshots for this dataset";
}; };
# TODO: this is what blank snapshot should set # Creates a blank snapshot in the post create hook for rollback purposes
# postCreateHook = ''
# zfs snapshot rpool/local/system/root@blank
# '';
blankSnapshot = lib.mkEnableOption "Should a blank snapshot be auto created in the post create hook"; blankSnapshot = lib.mkEnableOption "Should a blank snapshot be auto created in the post create hook";
}; };

View file

@ -49,6 +49,7 @@ in {
config = { config = {
mount = { mount = {
mountPoint = lib.mkDefault "/${name}"; mountPoint = lib.mkDefault "/${name}";
enable = lib.mkDefault true;
}; };
}; };
} }

View file

@ -5,6 +5,98 @@ args @ {
... ...
}: let }: let
datasetSubmodule = (import ./submodules/dataset.nix) args; 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 { in {
options.storage = { options.storage = {
zfs = { zfs = {
@ -39,12 +131,14 @@ in {
lib.types.coercedTo lib.types.str (device: { lib.types.coercedTo lib.types.str (device: {
device = device; device = device;
boot = false; boot = false;
}) { }) (lib.types.submodule {
options = {
device = lib.mkOption { device = lib.mkOption {
type = lib.types.str; 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 { in {
encryption = { encryption = {
enable = lib.mkEnableOption "Should encryption be enabled on this pool."; 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"; description = "List of vdevs, where each vdev is a list of devices";
}; };
cache = lib.mkOption { cache = lib.mkOption {
type = lib.types.attrsOf deviceType; type = lib.types.listOf deviceType;
default = {}; default = {};
}; };
}; };
rootDataset = lib.mkOption { rootDataset = lib.mkOption {
type = lib.types.submodule datasetSubmodule; type = lib.types.nullOr (lib.types.submodule datasetSubmodule);
description = "Root ZFS dataset to create"; description = "Root ZFS dataset to create";
default = {}; default = null;
}; };
datasets = lib.mkOption { datasets = lib.mkOption {
@ -96,15 +190,109 @@ in {
config = lib.mkIf config.storage.zfs.enable (lib.mkMerge [ config = lib.mkIf config.storage.zfs.enable (lib.mkMerge [
{ {
services.zfs = { # Assertion that we have at least one boot device
autoScrub.enable = true; assertions = [
autoSnapshot.enable = true; {
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.";
}
];
# # 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);
};
}; };
# TODO: configure disko options = {
# TODO: assertion that we have a boot device ashift = "12";
# TODO: check that disks on system match configuration and warn user if they don't autotrim = "on";
# TODO: check that datasets on system match configuration and warn user if they don't };
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 { (lib.mkIf config.storage.zfs.notifications.enable {
programs.msmtp = { programs.msmtp = {