From 74c29b6a13a9d01ebc87f37a4690de296ae51e1e Mon Sep 17 00:00:00 2001 From: Galin Stefanov Date: Tue, 7 Jan 2025 10:57:20 +0200 Subject: [PATCH] feat(modules/mcl-disko): Create ZFS configuration module --- modules/default.nix | 1 + modules/mcl-disko/default.nix | 196 ++++++++++++++++++++ modules/mcl-disko/primaryZfsPartition.nix | 95 ++++++++++ modules/mcl-disko/secondaryZfsPartition.nix | 22 +++ modules/mcl-disko/zpool.nix | 122 ++++++++++++ 5 files changed, 436 insertions(+) create mode 100644 modules/mcl-disko/default.nix create mode 100644 modules/mcl-disko/primaryZfsPartition.nix create mode 100644 modules/mcl-disko/secondaryZfsPartition.nix create mode 100644 modules/mcl-disko/zpool.nix diff --git a/modules/default.nix b/modules/default.nix index 19a71c0d..9cce5874 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -7,5 +7,6 @@ ./random-alerts ./host-info.nix ./secrets.nix + ./mcl-disko ]; } diff --git a/modules/mcl-disko/default.nix b/modules/mcl-disko/default.nix new file mode 100644 index 00000000..86cd0a09 --- /dev/null +++ b/modules/mcl-disko/default.nix @@ -0,0 +1,196 @@ +{ withSystem, inputs, ... }: +{ + flake.modules.nixos.mcl-disko = + { + pkgs, + config, + lib, + ... + }: + with lib; + let + cfg = config.mcl.disko; + in + { + imports = [ + inputs.disko.nixosModules.disko + ]; + options.mcl.disko = { + enable = mkEnableOption "Enable Module"; + + legacyBoot = mkOption { + type = types.bool; + default = false; + example = true; + description = "Declare if the configuration is for a Hetzner server or not"; + }; + + swapSize = mkOption { + type = types.str; + default = "32G"; + example = "32768M"; + description = "The size of the hard disk space used when RAM is full"; + }; + + espSize = mkOption { + type = types.str; + default = "4G"; + example = "4096M"; + description = "The size of the hard disk space used for the ESP filesystem"; + }; + + disks = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "/dev/disk/sda" + "/dev/disk/sdb" + "/dev/disk/sdc" + ]; + description = "The disk partitions to be used when ZFS is being created"; + }; + + disksNames = mkOption { + type = types.listOf types.str; + default = cfg.disks; + example = [ + "sda" + "sdb" + "sdc" + ]; + description = "The disk partitions names to be used when ZFS is being created"; + }; + + zpool = { + name = mkOption { + type = types.str; + default = "zfs_root"; + description = "The name of the ZFS Pool"; + }; + + mode = mkOption { + type = types.enum [ + "stripe" + "mirror" + "raidz1" + "raidz2" + "raidz3" + ]; + default = "stripe"; + description = "Set ZFS Pool redundancy - e.g. 'mirror', 'raidz1', etc."; + }; + + extraDatasets = mkOption { + type = types.attrsOf ( + types.submodule ( + { name, ... }: + { + options = { + mountpoint = mkOption { + type = types.nullOr types.str; + default = name; + example = "/var/lib"; + description = "The ZFS dataset mountpoint"; + }; + + type = mkOption { + type = types.enum [ + "zfs_fs" + "zfs_volume" + ]; + default = "zfs_fs"; + description = "Type of ZFS dataset"; + }; + + options = mkOption { + type = types.attrs; + default = { }; + example = { + refreservation = "50G"; + }; + description = ""; + }; + + snapshot = mkEnableOption "Whether to enable ZFS snapshots"; + + refreservation = mkOption { + type = types.nullOr types.str; + default = null; + description = "Size of reservations"; + }; + }; + } + ) + ); + default = { }; + example = { + "/opt".snapshot = false; + "/opt/downloads".snapshot = false; + "/opt/downloads/vm-images" = { + snapshot = false; + options = { + quota = "120G"; + }; + }; + }; + description = "Extra ZFS Pool datasets"; + }; + }; + }; + + config.disko = + let + makePrimaryZfsDisk = import ./primaryZfsPartition.nix; + makeSecondaryZfsDisk = import ./secondaryZfsPartition.nix; + + first = builtins.head cfg.disks; + rest = builtins.tail cfg.disks; + + firstDiskName = builtins.head cfg.disksNames; + restDisksNames = builtins.tail cfg.disksNames; + + secondaryDisks = builtins.listToAttrs ( + lib.zipListsWith (disk: diskName: { + name = diskName; + value = + if cfg.zpool.mode != "stripe" then + makePrimaryZfsDisk { + inherit disk lib; + isSecondary = true; + espSize = cfg.espSize; + swapSize = cfg.swapSize; + legacyBoot = cfg.legacyBoot; + poolName = cfg.zpool.name; + } + else + makeSecondaryZfsDisk { + poolName = cfg.zpool.name; + inherit disk lib; + }; + }) rest restDisksNames + ); + + in + lib.mkIf cfg.enable { + devices = { + disk = secondaryDisks // { + "${firstDiskName}" = makePrimaryZfsDisk { + inherit lib; + disk = first; + isSecondary = false; + espSize = cfg.espSize; + swapSize = cfg.swapSize; + legacyBoot = cfg.legacyBoot; + poolName = cfg.zpool.name; + }; + }; + zpool = import ./zpool.nix { + poolName = cfg.zpool.name; + poolMode = cfg.zpool.mode; + poolExtraDatasets = cfg.zpool.extraDatasets; + inherit lib; + }; + }; + }; + }; +} diff --git a/modules/mcl-disko/primaryZfsPartition.nix b/modules/mcl-disko/primaryZfsPartition.nix new file mode 100644 index 00000000..71b365fe --- /dev/null +++ b/modules/mcl-disko/primaryZfsPartition.nix @@ -0,0 +1,95 @@ +{ + lib, + disk, + isSecondary, + espSize, + swapSize, + legacyBoot, + poolName, +}: +{ + type = "disk"; + device = disk; + content = + { + type = if legacyBoot then "table" else "gpt"; + partitions = + if !legacyBoot then + { + "ESP" = { + device = "${disk}-part1"; + size = espSize; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = if isSecondary then null else "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + + "zfs" = { + device = "${disk}-part2"; + end = "-${swapSize}"; + type = "BF00"; + content = { + type = "zfs"; + pool = "${poolName}"; + }; + }; + + "swap" = { + device = "${disk}-part3"; + size = swapSize; + content = { + type = "swap"; + randomEncryption = true; + }; + }; + } + else + [ + { + name = "boot"; + start = "1MiB"; + end = "2MiB"; + part-type = "primary"; + flags = [ "bios_grub" ]; + } + { + name = "ESP"; + start = "2MiB"; + end = espSize; + bootable = true; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = if isSecondary then null else "/boot"; + }; + } + { + name = "zfs"; + start = espSize; + end = "-${swapSize}"; + part-type = "primary"; + content = { + type = "zfs"; + pool = "${poolName}"; + }; + } + { + name = "swap"; + start = "-${swapSize}"; + end = "100%"; + part-type = "primary"; + content = { + type = "swap"; + randomEncryption = true; + }; + } + ]; + } + // lib.optionalAttrs legacyBoot { + format = "gpt"; + }; +} diff --git a/modules/mcl-disko/secondaryZfsPartition.nix b/modules/mcl-disko/secondaryZfsPartition.nix new file mode 100644 index 00000000..ccec952e --- /dev/null +++ b/modules/mcl-disko/secondaryZfsPartition.nix @@ -0,0 +1,22 @@ +{ + lib, + poolName, + disk, +}: +{ + type = "disk"; + device = disk; + content = { + type = "gpt"; + partitions = { + zfs = { + device = "${disk}-part1"; + size = "100%"; + content = { + type = "zfs"; + pool = "${poolName}"; + }; + }; + }; + }; +} diff --git a/modules/mcl-disko/zpool.nix b/modules/mcl-disko/zpool.nix new file mode 100644 index 00000000..459ff1db --- /dev/null +++ b/modules/mcl-disko/zpool.nix @@ -0,0 +1,122 @@ +{ + lib, + poolName, + poolMode, + poolExtraDatasets, +}: +let + translateDataSetToDiskoConfig = + dataset@{ snapshot, refreservation, ... }: + lib.recursiveUpdate dataset { + type = "zfs_fs"; + options = { + "com.sun:auto-snapshot" = if dataset.snapshot then "on" else "off"; + canmount = "on"; + } // (if (refreservation != null) then { inherit refreservation; } else { }); + }; + + restructuredDatasets = builtins.mapAttrs ( + n: v: + (builtins.removeAttrs (translateDataSetToDiskoConfig poolExtraDatasets.${n}) [ + "refreservation" + "snapshot" + ]) + ) poolExtraDatasets; +in +{ + ${poolName} = { + type = "zpool"; + mode = if poolMode == "stripe" then "" else poolMode; + rootFsOptions = { + acltype = "posixacl"; + atime = "off"; + canmount = "off"; + checksum = "sha512"; + compression = "lz4"; + xattr = "sa"; + mountpoint = "none"; + "com.sun:auto-snapshot" = "false"; + }; + options = { + autotrim = "on"; + listsnapshots = "on"; + }; + + postCreateHook = "zfs snapshot ${poolName}@blank"; + + datasets = lib.recursiveUpdate { + root = { + mountpoint = "/"; + type = "zfs_fs"; + options = { + "com.sun:auto-snapshot" = "false"; + mountpoint = "legacy"; + }; + }; + + "root/nix" = { + mountpoint = "/nix"; + type = "zfs_fs"; + options = { + "com.sun:auto-snapshot" = "false"; + canmount = "on"; + mountpoint = "legacy"; + refreservation = "100GiB"; + }; + }; + + "root/var" = { + mountpoint = "/var"; + type = "zfs_fs"; + options = { + "com.sun:auto-snapshot" = "true"; + canmount = "on"; + mountpoint = "legacy"; + }; + }; + + "root/var/lib" = { + mountpoint = "/var/lib"; + type = "zfs_fs"; + options = { + "com.sun:auto-snapshot" = "true"; + canmount = "on"; + mountpoint = "legacy"; + }; + }; + + "root/home" = { + mountpoint = "/home"; + type = "zfs_fs"; + options = { + "com.sun:auto-snapshot" = "true"; + canmount = "on"; + mountpoint = "legacy"; + refreservation = "200GiB"; + }; + }; + + "root/var/lib/docker" = { + mountpoint = "/var/lib/docker"; + type = "zfs_fs"; + options = { + "com.sun:auto-snapshot" = "false"; + canmount = "on"; + mountpoint = "legacy"; + refreservation = "100GiB"; + }; + }; + + "root/var/lib/containers" = { + mountpoint = "/var/lib/containers"; + type = "zfs_fs"; + options = { + "com.sun:auto-snapshot" = "false"; + canmount = "on"; + mountpoint = "legacy"; + refreservation = "100GiB"; + }; + }; + } restructuredDatasets; + }; +}