{ vars, lib, pkgs, ... }: let poolName = lib.removePrefix "/" vars.storageRoot; # For each NFS share subpath, generate every ancestor path so ZFS datasets # are created parent-first. e.g. "docker/config" → ["docker" "docker/config"] ancestors = path: let parts = lib.splitString "/" path; in lib.imap1 (i: _: lib.concatStringsSep "/" (lib.take i parts)) parts; poolDatasets = lib.unique ( lib.concatMap (share: ancestors share.subpath) (lib.filter builtins.isAttrs (lib.attrValues vars.nfsShares)) ); in { imports = [ ../beszel/enable-agent.nix ../services/zfs/enable-service.nix ]; boot.zfs.extraPools = [ poolName ]; # On a fresh image deploy the data disk (scsi1) starts blank — no pool # exists yet, so zfs-import-tank.service would spin for 60 s and fail. # This service runs first: if the pool is already present it exits instantly; # otherwise it creates it (with all required datasets) so the standard # import service finds it ready on the very first boot. systemd.services."zfs-init-${poolName}" = { description = "Initialize '${poolName}' ZFS pool on first boot if not present"; wantedBy = [ "zfs-import-${poolName}.service" ]; before = [ "zfs-import-${poolName}.service" ]; after = [ "systemd-udev-settle.service" ]; unitConfig.DefaultDependencies = false; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; path = [ pkgs.zfs_unstable ]; script = '' # Already imported — nothing to do. if zpool list "${poolName}" >/dev/null 2>&1; then exit 0 fi # Pool exists on a device but not yet imported — let the standard # zfs-import-${poolName}.service handle it normally. if zpool import -d /dev/disk/by-id -N "${poolName}" 2>/dev/null; then exit 0 fi # No pool found at all. Create it on the Proxmox data disk (scsi1), # which appears as /dev/disk/by-id/scsi-*drive-scsi1 inside the VM. DATA_DISK="" for candidate in /dev/disk/by-id/scsi-*drive-scsi1; do [[ "$candidate" == *-part* ]] && continue [ -b "$candidate" ] && DATA_DISK="$candidate" && break done if [ -z "$DATA_DISK" ]; then echo "zfs-init-${poolName}: no data disk found (expected /dev/disk/by-id/scsi-*drive-scsi1)" >&2 exit 1 fi echo "zfs-init-${poolName}: creating pool on $DATA_DISK" zpool create -f "${poolName}" "$DATA_DISK" ${lib.concatMapStrings (ds: '' zfs create "${poolName}/${ds}" '') poolDatasets} ''; }; systemd.services.nfs-server = { after = [ "zfs-mount.service" ]; requires = [ "zfs-mount.service" ]; }; services.nfs.server = { enable = true; exports = '' ${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath} ${vars.lanCidr}${vars.nfsShares.options} ${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath} ${vars.lanCidr}${vars.nfsShares.options} ${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath} ${vars.lanCidr}${vars.nfsShares.options} ${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath} ${vars.lanCidr}${vars.nfsShares.options} ${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath} ${vars.lanCidr}${vars.nfsShares.options} ${vars.storageRoot}/${vars.nfsShares.proxmoxIsos.subpath} ${vars.lanCidr}${vars.nfsShares.options} ${vars.storageRoot}/${vars.nfsShares.proxmoxLxcImages.subpath} ${vars.lanCidr}${vars.nfsShares.options} ${vars.storageRoot}/${vars.nfsShares.pxebootImages.subpath} ${vars.lanCidr}${vars.nfsShares.options} ''; }; # mountd (20048) is needed for showmount/NFSv3 mount protocol — without it # clients can reach portmapper (111) and get the mountd port back, then # time out trying to connect to it. All three ports need TCP and UDP. networking.firewall.allowedTCPPorts = [ vars.ports.nfsRpcbind vars.ports.nfsd vars.ports.nfsMountd ]; networking.firewall.allowedUDPPorts = [ vars.ports.nfsRpcbind vars.ports.nfsd vars.ports.nfsMountd ]; }