From f7c32aff12e2c55ad8700a5d8be4dcb10e5c9d7d Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Mon, 27 Jul 2026 03:23:14 +1000 Subject: [PATCH 1/3] feat(pxe-boot): add Debian bookworm minimal netboot menu entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `fetch-debian-netboot.service` oneshot that downloads the Debian bookworm netboot kernel and initrd from deb.debian.org on first boot, stages them under /srv/pxe/http/debian/, and serves them via a generated debian.ipxe chain script. The service is idempotent — it skips the download if both files are already present. Also merges the previously split systemd.tmpfiles.rules and systemd.services blocks into a single systemd = { ... } attrset to satisfy statix's repeated-keys lint. Co-Authored-By: Claude Sonnet 4.6 --- docs/pxe-boot.md | 29 ++++++++ modules/build-types/pxe-boot.nix | 113 +++++++++++++++++++++++-------- 2 files changed, 115 insertions(+), 27 deletions(-) diff --git a/docs/pxe-boot.md b/docs/pxe-boot.md index 5d1fe18..26a6712 100644 --- a/docs/pxe-boot.md +++ b/docs/pxe-boot.md @@ -15,6 +15,7 @@ rescue/inspection use. - TFTP root for first-stage bootloaders: `/srv/pxe/tftp` - iPXE entry script: `/srv/pxe/http/boot.ipxe` - Generated iPXE menu: `/srv/pxe/http/menu.ipxe` +- Debian Minimal iPXE script: `/srv/pxe/http/debian.ipxe` - SystemRescue iPXE script: `/srv/pxe/http/systemrescue.ipxe` - TFTP fallback script: `/srv/pxe/tftp/autoexec.ipxe` - Boot binaries copied from the Nix `ipxe` package: @@ -31,6 +32,7 @@ The host creates these directories with systemd tmpfiles: /srv/pxe/http/images /srv/pxe/http/auto-installer /srv/pxe/http/nixos-minimal +/srv/pxe/http/debian /srv/pxe/http/systemrescue /srv/pxe/http/ubuntu /srv/pxe/http/rescue @@ -54,6 +56,7 @@ The generated menu currently exposes entries for: - NixOS Auto-Installer - NixOS Minimal +- Debian Minimal - SystemRescue environment - iPXE shell - Reboot @@ -84,6 +87,29 @@ directory name (`auto-installer` / `nixos-minimal`), so each one's generated system name (`nixos-system--*`) is self-describing rather than the nixpkgs default of `nixos-system-nixos-*` for both. +The Debian Minimal entry chains `http:///debian.ipxe`, which loads +the Debian bookworm netboot kernel and initrd from `/srv/pxe/http/debian/`. The +`fetch-debian-netboot.service` oneshot downloads these files from +`deb.debian.org` on first boot (idempotent — skips if files are already +present): + +```text +/srv/pxe/http/debian/linux (Debian bookworm netboot kernel) +/srv/pxe/http/debian/initrd.gz (Debian bookworm netboot initrd) +``` + +The service requires outbound internet access on the pxe-boot host. To +re-download (e.g. after a Debian point release), delete the files and restart +the service: + +```bash +rm /srv/pxe/http/debian/linux /srv/pxe/http/debian/initrd.gz +systemctl restart fetch-debian-netboot.service +``` + +To update to a different Debian release, change `debianRelease` in +`modules/build-types/pxe-boot.nix` and redeploy. + The SystemRescue entry expects the source ISO at: ```text @@ -113,6 +139,9 @@ After deployment by an operator, basic service checks are: ```bash curl http://pxe-boot/boot.ipxe curl http://pxe-boot/menu.ipxe +curl http://pxe-boot/debian.ipxe +curl -I http://pxe-boot/debian/linux +curl -I http://pxe-boot/debian/initrd.gz curl http://pxe-boot/systemrescue.ipxe curl -I http://pxe-boot/systemrescue/sysresccd/boot/x86_64/vmlinuz curl -I http://pxe-boot/systemrescue/sysresccd/boot/x86_64/sysresccd.img diff --git a/modules/build-types/pxe-boot.nix b/modules/build-types/pxe-boot.nix index 4dcd88b..7e74f06 100644 --- a/modules/build-types/pxe-boot.nix +++ b/modules/build-types/pxe-boot.nix @@ -21,6 +21,39 @@ let chain ${pxeBaseUrl}/boot.ipxe ''; + debianRelease = "bookworm"; + debianMirror = "https://deb.debian.org/debian"; + debianNetbootBase = "${debianMirror}/dists/${debianRelease}/main/installer-amd64/current/images/netboot/debian-installer/amd64"; + + debianIpxe = pkgs.writeText "debian.ipxe" '' + #!ipxe + + set base ${pxeBaseUrl} + + kernel ''${base}/debian/linux + initrd ''${base}/debian/initrd.gz + boot + ''; + + fetchDebianNetboot = pkgs.writeShellScript "fetch-debian-netboot" '' + set -eu + + dir="${httpRoot}/debian" + mirror="${debianNetbootBase}" + + if [ -f "$dir/linux" ] && [ -f "$dir/initrd.gz" ]; then + echo "Debian ${debianRelease} netboot files already present; skipping download." + exit 0 + fi + + echo "Downloading Debian ${debianRelease} netboot kernel and initrd from $mirror ..." + ${pkgs.curl}/bin/curl -fsSL -o "$dir/linux.tmp" "$mirror/linux" + ${pkgs.curl}/bin/curl -fsSL -o "$dir/initrd.gz.tmp" "$mirror/initrd.gz" + mv "$dir/linux.tmp" "$dir/linux" + mv "$dir/initrd.gz.tmp" "$dir/initrd.gz" + echo "Debian ${debianRelease} netboot files staged." + ''; + systemRescueIpxe = pkgs.writeText "systemrescue.ipxe" '' #!ipxe @@ -70,6 +103,7 @@ let menu PXE Boot Menu item auto-installer NixOS Auto-Installer item nixos-minimal NixOS Minimal + item debian Debian Minimal item rescue Rescue Environment item shell iPXE Shell item reboot Reboot @@ -82,6 +116,9 @@ let :nixos-minimal chain ''${base}/nixos-minimal/netboot.ipxe + :debian + chain ''${base}/debian.ipxe + :rescue chain ''${base}/systemrescue.ipxe @@ -129,34 +166,56 @@ in openssh.settings.PermitRootLogin = "yes"; }; - systemd.tmpfiles.rules = [ - "d ${pxeRoot} 0755 root root -" - "d ${httpRoot} 0755 root root -" - "d ${httpRoot}/images 0755 root root -" - "d ${httpRoot}/auto-installer 0755 root root -" - "d ${httpRoot}/nixos-minimal 0755 root root -" - "d ${httpRoot}/systemrescue 0755 root root -" - "d ${httpRoot}/ubuntu 0755 root root -" - "d ${httpRoot}/rescue 0755 root root -" - "d ${tftpRoot} 0755 root root -" - "C+ ${httpRoot}/boot.ipxe 0644 root root - ${bootIpxe}" - "C+ ${httpRoot}/menu.ipxe 0644 root root - ${menuIpxe}" - "C+ ${httpRoot}/systemrescue.ipxe 0644 root root - ${systemRescueIpxe}" - "C+ ${tftpRoot}/autoexec.ipxe 0644 root root - ${autoexecIpxe}" - "C+ ${tftpRoot}/ipxe.efi 0644 root root - ${pkgs.ipxe}/ipxe.efi" - "C+ ${tftpRoot}/undionly.kpxe 0644 root root - ${pkgs.ipxe}/undionly.kpxe" - ]; - - systemd.services.stage-systemrescue = { - description = "Stage SystemRescue ISO contents for HTTP PXE boot"; - after = [ - "local-fs.target" - "systemd-tmpfiles-setup.service" + systemd = { + tmpfiles.rules = [ + "d ${pxeRoot} 0755 root root -" + "d ${httpRoot} 0755 root root -" + "d ${httpRoot}/images 0755 root root -" + "d ${httpRoot}/auto-installer 0755 root root -" + "d ${httpRoot}/nixos-minimal 0755 root root -" + "d ${httpRoot}/systemrescue 0755 root root -" + "d ${httpRoot}/debian 0755 root root -" + "d ${httpRoot}/ubuntu 0755 root root -" + "d ${httpRoot}/rescue 0755 root root -" + "d ${tftpRoot} 0755 root root -" + "C+ ${httpRoot}/boot.ipxe 0644 root root - ${bootIpxe}" + "C+ ${httpRoot}/menu.ipxe 0644 root root - ${menuIpxe}" + "C+ ${httpRoot}/debian.ipxe 0644 root root - ${debianIpxe}" + "C+ ${httpRoot}/systemrescue.ipxe 0644 root root - ${systemRescueIpxe}" + "C+ ${tftpRoot}/autoexec.ipxe 0644 root root - ${autoexecIpxe}" + "C+ ${tftpRoot}/ipxe.efi 0644 root root - ${pkgs.ipxe}/ipxe.efi" + "C+ ${tftpRoot}/undionly.kpxe 0644 root root - ${pkgs.ipxe}/undionly.kpxe" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "oneshot"; - ExecStart = stageSystemRescue; + + services = { + fetch-debian-netboot = { + description = "Download Debian ${debianRelease} netboot kernel and initrd for HTTP PXE boot"; + after = [ + "local-fs.target" + "systemd-tmpfiles-setup.service" + "network-online.target" + ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = fetchDebianNetboot; + RemainAfterExit = true; + }; + }; + + stage-systemrescue = { + description = "Stage SystemRescue ISO contents for HTTP PXE boot"; + after = [ + "local-fs.target" + "systemd-tmpfiles-setup.service" + ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = stageSystemRescue; + }; + }; }; }; From 321048626e38d75c89ab7543fa70be4128fd046d Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Mon, 27 Jul 2026 03:54:37 +1000 Subject: [PATCH 2/3] feat(pxe-boot): mount pxe-images NFS share and symlink to /srv/pxe/http/images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds modules/pxe-boot/mount-pxe-images.nix, which mounts server.sweet.home:/tank/proxmox/pxe-images at /mnt/pxe-images via NFSv4.2 (x-systemd.automount on Proxmox VMs, nofail on LXC containers — same pattern as docker/mount-data.nix). The pxe-boot build-type now imports this module and replaces the previous local /srv/pxe/http/images directory rule with an L+ symlink pointing to /mnt/pxe-images, so large images (ISOs, disk images) live on the NFS share rather than the host's own root disk. Co-Authored-By: Claude Sonnet 4.6 --- docs/pxe-boot.md | 15 +++++++++++---- modules/build-types/pxe-boot.nix | 3 ++- modules/pxe-boot/mount-pxe-images.nix | 24 ++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 modules/pxe-boot/mount-pxe-images.nix diff --git a/docs/pxe-boot.md b/docs/pxe-boot.md index 26a6712..5b8cc5f 100644 --- a/docs/pxe-boot.md +++ b/docs/pxe-boot.md @@ -29,7 +29,7 @@ The host creates these directories with systemd tmpfiles: ```text /srv/pxe /srv/pxe/http -/srv/pxe/http/images +/srv/pxe/http/images -> /mnt/pxe-images (symlink to NFS share) /srv/pxe/http/auto-installer /srv/pxe/http/nixos-minimal /srv/pxe/http/debian @@ -39,9 +39,13 @@ The host creates these directories with systemd tmpfiles: /srv/pxe/tftp ``` -Mount shared image storage under `/srv/pxe/http`, preferably -`/srv/pxe/http/images` unless a menu entry expects files in a specific -directory such as `/srv/pxe/http/auto-installer`. +`/srv/pxe/http/images` is a symlink to `/mnt/pxe-images`, which is an NFSv4.2 +mount of `server.sweet.home:/tank/proxmox/pxe-images` +(`modules/pxe-boot/mount-pxe-images.nix`). Place large images there (ISOs, +disk images) rather than on the pxe-boot host's own root disk. For an LXC +pxe-boot container the mount uses `nofail` (eager, non-blocking on server +unavailability); for a Proxmox VM it uses `x-systemd.automount` (lazy, +triggered on first access). The HTTP iPXE chain is: @@ -116,6 +120,9 @@ The SystemRescue entry expects the source ISO at: /srv/pxe/http/images/systemrescue.iso ``` +Since `/srv/pxe/http/images` is the NFS-backed symlink, place the ISO on the +NFS share at `server.sweet.home:/tank/proxmox/pxe-images/systemrescue.iso`. + The `stage-systemrescue.service` oneshot extracts that ISO into: ```text diff --git a/modules/build-types/pxe-boot.nix b/modules/build-types/pxe-boot.nix index 7e74f06..8179adb 100644 --- a/modules/build-types/pxe-boot.nix +++ b/modules/build-types/pxe-boot.nix @@ -132,6 +132,7 @@ in { imports = [ ../pxe-boot/stage-installer-artifacts.nix + ../pxe-boot/mount-pxe-images.nix ]; environment.systemPackages = with pkgs; [ @@ -170,7 +171,7 @@ in tmpfiles.rules = [ "d ${pxeRoot} 0755 root root -" "d ${httpRoot} 0755 root root -" - "d ${httpRoot}/images 0755 root root -" + "L+ ${httpRoot}/images - - - - ${vars.nfsShares.proxmoxPxeImages.mountpoint}" "d ${httpRoot}/auto-installer 0755 root root -" "d ${httpRoot}/nixos-minimal 0755 root root -" "d ${httpRoot}/systemrescue 0755 root root -" diff --git a/modules/pxe-boot/mount-pxe-images.nix b/modules/pxe-boot/mount-pxe-images.nix new file mode 100644 index 0000000..853ab08 --- /dev/null +++ b/modules/pxe-boot/mount-pxe-images.nix @@ -0,0 +1,24 @@ +{ config, vars, ... }: + +let + # Use the same FQDN approach as docker/mount-data.nix — a bare hostname is + # unreliable: systemd-resolved only tries LLMNR for single-label names, and + # a global search domain causes it to skip the interface-scoped LAN DNS. + nfsServer = "${vars.nfsServerHost}.${vars.homeDomain}"; + + # x-systemd.automount is unsupported inside LXC containers (systemd logs + # "Starting of .automount unsupported" and never mounts). Use nofail + # there so a boot with the NFS server unreachable doesn't hang instead. + automountOpts = if config.boot.isContainer then [ "nofail" ] else [ "x-systemd.automount" ]; +in +{ + fileSystems.${vars.nfsShares.proxmoxPxeImages.mountpoint} = { + device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.proxmoxPxeImages.subpath}"; + fsType = "nfs"; + options = [ + "nfsvers=4.2" + "_netdev" + "noatime" + ] ++ automountOpts; + }; +} From 2e9d3da301e23505e06745c36c8a575c094448f6 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Mon, 27 Jul 2026 03:56:39 +1000 Subject: [PATCH 3/3] fix(server): open mountd port 20048 so NFS clients can scan and mount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- modules/build-types/server.nix | 6 +++++- variables.nix | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/modules/build-types/server.nix b/modules/build-types/server.nix index c55291a..882a5b4 100644 --- a/modules/build-types/server.nix +++ b/modules/build-types/server.nix @@ -88,5 +88,9 @@ in ''; }; - networking.firewall.allowedTCPPorts = [ vars.ports.nfsRpcbind vars.ports.nfsd ]; + # 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 ]; } diff --git a/variables.nix b/variables.nix index f6a34b5..9f27d49 100644 --- a/variables.nix +++ b/variables.nix @@ -137,10 +137,14 @@ # (modules/build-types/pxe-boot.nix). pxeBootTftp = 69; - # `server`'s NFS exports need both the portmapper (rpcbind) and the - # NFS data port itself opened (modules/build-types/server.nix). + # `server`'s NFS exports: portmapper (rpcbind), NFS data, and the + # mountd RPC service (used by showmount/NFSv3 mount protocol). + # Mountd listens on a fixed port so the firewall can whitelist it + # explicitly rather than opening all of rpcbind's dynamic range. + # All three need both TCP and UDP (modules/build-types/server.nix). nfsRpcbind = 111; nfsd = 2049; + nfsMountd = 20048; # Opened on the docker host's firewall for the Traefik-fronted # container stack (docker-compose config lives in the separate