Files

218 lines
8.8 KiB
Nix

# 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";
# Interface par laquelle CE serveur a lui-même accès à Internet — c'est
# elle qui sert de sortie NAT pour le segment PXE (utile seulement en
# standaloneDhcp = true, où ce serveur est aussi la passerelle).
internetInterface = "wlp3s0";
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";
# i18n.defaultLocale ne règle pas le clavier de la console — option
# séparée, absente jusqu'ici pour ce host (vérifié : wiki.nixos.org/wiki/Keyboard_Layout_Customization).
console.keyMap = "fr";
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
# port = 0 désactive le DNS de dnsmasq : sans ça, les PC installés
# via DHCP de ce serveur n'ont aucun serveur DNS fonctionnel et ne
# peuvent résoudre ni GitHub ni Gitea pendant l'install. Pareil
# pour la passerelle : sans "router" explicite, les PC installés
# n'ont aucune route vers Internet (voir networking.nat plus bas).
"dhcp-option" = [
"option:dns-server,1.1.1.1,9.9.9.9"
"option:router,${serverIp}"
];
}
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
];
# Se lance aussi une fois à chaque démarrage du serveur (pas seulement
# via le timer quotidien) : après un reboot, l'image publiée doit être
# dispo tout de suite, sans attendre la prochaine échéance du timer.
wantedBy = [ "multi-user.target" ];
# Sans ça, le service pourrait se lancer 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
}
];
# Route + NAT le segment PXE vers Internet via internetInterface. Sans
# ça, les PC installés reçoivent bien une IP/passerelle/DNS mais aucun
# paquet ne sort réellement du segment 192.168.1.0/24 — uniquement
# nécessaire en standaloneDhcp = true (ce serveur EST la passerelle).
boot.kernel.sysctl."net.ipv4.ip_forward" = lib.mkIf standaloneDhcp 1;
networking.nat = lib.mkIf standaloneDhcp {
enable = true;
internalInterfaces = [ interface ];
externalInterface = internetInterface;
};
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";
}