Archived
Check NixOS configurations / eval-hosts (pull_request) Failing after 9m43s
nfs-utils changed ConditionPathExists from !/etc/krb5.keytab to /etc/krb5.keytab, so on IPA-joined hosts the service now starts instead of skipping. Exports use standard auth (no sec=krb5) so the nfs/ Kerberos principal is never provisioned and the service fails. enable = false masks the unit so nfs-server's Wants= can't pull it in. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
122 lines
5.1 KiB
Nix
122 lines
5.1 KiB
Nix
{ 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))
|
|
);
|
|
|
|
# Generates /etc/exports lines for all nfsShares data entries (every
|
|
# attrset value — excludes the bare `options` string). Both server and
|
|
# ha-server export the same share set from different storage roots, so
|
|
# this helper is the single source of truth for the export line format.
|
|
mkNfsExports = storageRoot:
|
|
lib.concatMapStrings
|
|
(share: " ${storageRoot}/${share.subpath} ${vars.lanCidr}${vars.nfsShares.options}\n")
|
|
(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
|
|
|
|
# Locate the data disk first — used for both the fallback import
|
|
# attempt and, only if the disk is genuinely blank, pool creation.
|
|
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
|
|
|
|
# Try importing via the by-id symlink directory first (normal path),
|
|
# then fall back to scanning the disk directly. The two-step exists
|
|
# because of a udev race: systemd-udev-settle.service can clear before
|
|
# /dev/disk/by-id/ entries are fully populated, causing the first
|
|
# import to fail even when the pool is intact on the disk.
|
|
if zpool import -d /dev/disk/by-id -N "${poolName}" 2>/dev/null; then
|
|
exit 0
|
|
fi
|
|
if zpool import -d "$DATA_DISK" -N "${poolName}" 2>/dev/null; then
|
|
exit 0
|
|
fi
|
|
|
|
# Both import attempts failed. Before creating a new pool, verify the
|
|
# disk is genuinely blank — if ZFS label metadata is present the import
|
|
# failed for some other reason and we must not clobber existing data.
|
|
if zdb -l "$DATA_DISK" 2>/dev/null | grep -q "name: '${poolName}'"; then
|
|
echo "zfs-init-${poolName}: $DATA_DISK has ZFS pool '${poolName}' metadata but import failed — refusing to overwrite existing data. Run 'zpool import -d $DATA_DISK ${poolName}' manually to investigate." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Disk is genuinely blank: create the pool. -f is intentionally
|
|
# omitted so that if we somehow reach this point with an existing pool
|
|
# on the disk, zpool refuses rather than silently destroying data.
|
|
echo "zfs-init-${poolName}: creating pool on $DATA_DISK"
|
|
zpool create "${poolName}" "$DATA_DISK"
|
|
${lib.concatMapStrings (ds: ''
|
|
zfs create "${poolName}/${ds}"
|
|
'') poolDatasets}
|
|
'';
|
|
};
|
|
|
|
systemd.services.nfs-server = {
|
|
after = [ "zfs-mount.service" ];
|
|
requires = [ "zfs-mount.service" ];
|
|
};
|
|
|
|
# rpc-svcgssd handles Kerberos/GSS-API for NFS. Not needed: exports use
|
|
# standard auth, not sec=krb5. On IPA-joined hosts the keytab exists (host/
|
|
# principal only) but has no nfs/ principal, causing spurious failure.
|
|
# Mask it so nfs-server's Wants= can't pull it in.
|
|
systemd.services.rpc-svcgssd.enable = false;
|
|
|
|
services.nfs.server = {
|
|
enable = true;
|
|
exports = mkNfsExports vars.storageRoot;
|
|
};
|
|
|
|
# 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 ];
|
|
}
|