# Serveur PXE déclaratif : DHCP-proxy (ou DHCP complet)+TFTP (dnsmasq) # pour le premier étage iPXE, HTTP (Caddy) pour kernel/initrd/script iPXE # (TFTP seul est trop lent pour de gros fichiers). Ce host n'a besoin # d'aucune donnée privée : il ne sert que l'image d'installation publique # nixos_fleet ; le repo privé nixos_users n'est fetché qu'au moment de # l'install, directement par la machine en cours d'installation. { config, lib, pkgs, ... }: let # true = ce serveur devient l'UNIQUE serveur DHCP du réseau cible. # false = proxyDHCP uniquement : coexiste avec le routeur/DHCP existant, # répond seulement aux requêtes de boot PXE. C'est le mode # recommandé si les 100 PC restent sur un réseau qui a déjà un # DHCP (le cas le plus probable). standaloneDhcp = true; # À adapter à l'interface réseau réelle du serveur, et à son IP fixe # (le script iPXE doit pointer vers une adresse stable, pas un nom # d'interface). interface = "enp0s31f6"; serverIp = "192.168.1.10"; fleetRepo = "git+https://gitea.miniclic.net/Damien/Nixos_Fleet.git"; webroot = "/var/www/pxe"; tftpRoot = "/var/lib/tftpboot"; in { imports = [ # Chemin absolu, HORS de ce repo (emplacement par défaut de # `nixos-generate-config`) : le repo n'a donc rien à committer pour # que le build fonctionne, il reste purement "pull". Un flake ne # voit que les fichiers qu'il suit lui-même par git ; un chemin # absolu en dehors du repo échappe à cette règle, d'où le --impure # au build (nécessaire pour lire un chemin hors du flake). /etc/nixos/hardware-configuration.nix ]; i18n.defaultLocale = "fr_FR.UTF-8"; boot.loader.systemd-boot.enable = true; boot.loader.efi.canTouchEfiVariables = true; # Si le serveur est en BIOS legacy (pas UEFI), remplacer les deux lignes # au-dessus par boot.loader.grub.device = "/dev/sdX"; networking.hostName = "pxe-server"; networking.firewall.allowedUDPPorts = [ 67 69 4011 ]; networking.firewall.allowedTCPPorts = [ 80 ]; services.dnsmasq = { enable = true; # Par défaut, NixOS fait pointer /etc/resolv.conf sur 127.0.0.1 pour # utiliser dnsmasq comme résolveur DNS local. Comme port = 0 désactive # justement la partie DNS de dnsmasq (on ne veut que DHCP/TFTP/PXE), # ça cassait toute résolution DNS du système (dont git pull). On garde # le résolveur DNS normal (DHCP/NetworkManager). resolveLocalQueries = false; settings = { bind-dynamic = true; # ne pas exiger que l'interface existe au démarrage port = 0; # pas de résolution DNS, uniquement DHCP/TFTP/PXE inherit interface; "enable-tftp" = true; "tftp-root" = tftpRoot; # Détection d'architecture + évite la boucle de chainload iPXE : # une machine qui a déjà démarré en iPXE s'annonce via l'option 175, # on lui sert alors le script HTTP plutôt que de rechainer le même # binaire à l'infini. "dhcp-match" = [ "set:ipxe,175" "set:efi-x86_64,option:client-arch,7" ]; "dhcp-boot" = [ "tag:ipxe,http://${serverIp}/netboot.ipxe" "tag:!ipxe,tag:efi-x86_64,ipxe.efi" "tag:!ipxe,tag:!efi-x86_64,undionly.kpxe" ]; } // ( if standaloneDhcp then { "dhcp-range" = [ "192.168.1.50,192.168.1.150,12h" ]; } # à adapter au réseau cible else { "dhcp-range" = [ "192.168.1.0,proxy" ]; } # à adapter au sous-réseau existant ); }; # `:80` explicite (pas de nom de domaine) : Caddy ne tente donc aucune # demande de certificat ACME, il sert juste du HTTP nu en local — exactement # ce qu'il faut pour du netboot sur un LAN. services.caddy = { enable = true; virtualHosts.":80" = { extraConfig = '' root * ${webroot} file_server ''; }; }; # Attend que le réseau soit vraiment prêt avant de démarrer (en plus de # bind-dynamic ci-dessus, qui gère le cas où l'interface apparaît encore # plus tard, ex. carte USB). systemd.services.dnsmasq = { after = [ "network-online.target" ]; wants = [ "network-online.target" ]; }; systemd.tmpfiles.rules = [ "d ${tftpRoot} 0755 dnsmasq dnsmasq -" "d ${webroot} 0755 caddy caddy -" ]; # Republie l'image d'installation netboot depuis le repo public. À # relancer après chaque évolution notable de installer/ dans # nixos_fleet (systemctl start pxe-publish), en plus du rafraîchissement # quotidien automatique ci-dessous. systemd.services.pxe-publish = { description = "Publie l'image d'installation netboot Nixos_Fleet pour le PXE"; # git est nécessaire : nix doit fetcher l'URL git+https:// du flake. path = [ config.nix.package pkgs.git ]; # Sans ça, le timer quotidien pourrait se déclencher avant que le # réseau soit prêt après un reboot, et échouer sur le fetch git. after = [ "network-online.target" ]; wants = [ "network-online.target" ]; script = '' set -euo pipefail kernel=$(nix build --no-link --no-write-lock-file --refresh --print-out-paths "${fleetRepo}#nixosConfigurations.installer-netboot.config.system.build.kernel") initrd=$(nix build --no-link --no-write-lock-file --refresh --print-out-paths "${fleetRepo}#nixosConfigurations.installer-netboot.config.system.build.netbootRamdisk") ipxeScript=$(nix build --no-link --no-write-lock-file --refresh --print-out-paths "${fleetRepo}#nixosConfigurations.installer-netboot.config.system.build.netbootIpxeScript") install -m 0644 "$kernel/bzImage" "${webroot}/bzImage" install -m 0644 "$initrd/initrd" "${webroot}/initrd" install -m 0644 "$ipxeScript/netboot.ipxe" "${webroot}/netboot.ipxe" # Binaires iPXE premier étage (BIOS + UEFI). Noms de sortie du # paquet pkgs.ipxe à vérifier sur la version réelle avant le pilote. install -m 0644 "${pkgs.ipxe}/undionly.kpxe" "${tftpRoot}/undionly.kpxe" || true install -m 0644 "${pkgs.ipxe}/ipxe.efi" "${tftpRoot}/ipxe.efi" || true ''; serviceConfig.Type = "oneshot"; }; systemd.timers.pxe-publish = { description = "Rafraîchit quotidiennement l'image PXE (converge avec nixos_fleet)"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = "daily"; Persistent = true; }; }; nix.settings.experimental-features = [ "nix-command" "flakes" ]; networking.networkmanager.enable = lib.mkDefault true; # Cette interface est le segment PXE dédié (standaloneDhcp = true) : elle # a besoin d'une IP fixe = serverIp, pas d'un bail DHCP. On l'exclut de # NetworkManager pour éviter tout conflit avec l'assignation statique # ci-dessous. networking.networkmanager.unmanaged = [ "interface-name:${interface}" ]; networking.interfaces.${interface}.ipv4.addresses = [ { address = serverIp; prefixLength = 24; # à adapter si le sous-réseau n'est pas un /24 } ]; services.openssh.enable = true; # git installé pour de bon (pull/push manuels sans réouvrir un # nix-shell -p git à chaque fois). environment.systemPackages = [ pkgs.git ]; system.stateVersion = "26.05"; }