Archived
showmount and the NFSv3 mount protocol need mountd reachable after querying portmapper on 111; the server was only opening TCP 111 and 2049, causing clients (e.g. Proxmox GUI NFS storage scan) to time out connecting to mountd on 20048. Also adds UDP for all three ports — portmapper, nfsd, and mountd all use both protocols. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
97 lines
4.0 KiB
Nix
97 lines
4.0 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.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}(rw,sync,no_subtree_check,no_root_squash)
|
|
${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
|
${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
|
${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
|
${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
|
${vars.storageRoot}/${vars.nfsShares.proxmoxIsos.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
|
${vars.storageRoot}/${vars.nfsShares.proxmoxPxeImages.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
|
'';
|
|
};
|
|
|
|
# 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 ];
|
|
}
|