photoncloud-monorepo/chainfire/baremetal/pxe-server/nixos-module.nix
centra 5c6eb04a46 T036: Add VM cluster deployment configs for nixos-anywhere
- netboot-base.nix with SSH key auth
- Launch scripts for node01/02/03
- Node configuration.nix and disko.nix
- Nix modules for first-boot automation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-11 09:59:19 +09:00

456 lines
12 KiB
Nix

# NixOS Module for PXE Boot Server
#
# This module provides a complete PXE boot infrastructure for bare-metal
# provisioning of Centra Cloud nodes.
#
# Services provided:
# - DHCP server (ISC DHCP)
# - TFTP server (for iPXE bootloaders)
# - HTTP server (nginx, for iPXE scripts and NixOS images)
#
# Usage:
# 1. Import this module in your NixOS configuration
# 2. Enable and configure the PXE server
# 3. Deploy to your provisioning server
#
# Example:
# imports = [ ./baremetal/pxe-server/nixos-module.nix ];
#
# services.centra-pxe-server = {
# enable = true;
# interface = "eth0";
# serverAddress = "10.0.100.10";
# subnet = "10.0.100.0/24";
# dhcpRange = {
# start = "10.0.100.100";
# end = "10.0.100.200";
# };
# };
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.centra-pxe-server;
# DHCP configuration file
dhcpdConf = pkgs.writeText "dhcpd.conf" ''
# ISC DHCP Server Configuration for PXE Boot
# Auto-generated by NixOS module
option space pxelinux;
option architecture-type code 93 = unsigned integer 16;
default-lease-time ${toString cfg.dhcp.defaultLeaseTime};
max-lease-time ${toString cfg.dhcp.maxLeaseTime};
authoritative;
log-facility local7;
subnet ${cfg.dhcp.subnet} netmask ${cfg.dhcp.netmask} {
range ${cfg.dhcp.range.start} ${cfg.dhcp.range.end};
option routers ${cfg.dhcp.router};
option subnet-mask ${cfg.dhcp.netmask};
option broadcast-address ${cfg.dhcp.broadcast};
option domain-name-servers ${concatStringsSep ", " cfg.dhcp.nameservers};
option domain-name "${cfg.dhcp.domainName}";
next-server ${cfg.serverAddress};
if exists user-class and option user-class = "iPXE" {
filename "http://${cfg.serverAddress}/boot/ipxe/boot.ipxe";
} elsif option architecture-type = 00:00 {
filename "undionly.kpxe";
} elsif option architecture-type = 00:06 {
filename "ipxe-i386.efi";
} elsif option architecture-type = 00:07 {
filename "ipxe.efi";
} elsif option architecture-type = 00:09 {
filename "ipxe.efi";
} else {
filename "undionly.kpxe";
}
}
${cfg.dhcp.extraConfig}
'';
# iPXE boot script
bootIpxeScript = pkgs.writeText "boot.ipxe" ''
#!ipxe
set boot-server ${cfg.serverAddress}
set boot-url http://''${boot-server}/boot
set nixos-url ''${boot-url}/nixos
set provisioning-server http://''${boot-server}
echo Network Configuration:
echo IP Address: ''${ip}
echo MAC Address: ''${mac}
echo
isset ''${profile} || set profile unknown
${concatStringsSep "\n" (mapAttrsToList (mac: node:
"iseq ''${mac} ${mac} && set profile ${node.profile} && set hostname ${node.hostname} && goto boot ||"
) cfg.nodes)}
goto menu
:menu
clear menu
menu Centra Cloud - Bare-Metal Provisioning
item --gap -- ------------------------- Boot Profiles -------------------------
item control-plane 1. Control Plane Node (All Services)
item worker 2. Worker Node (Compute Services)
item all-in-one 3. All-in-One Node (Testing/Homelab)
item --gap -- ------------------------- Advanced Options -------------------------
item shell iPXE Shell (for debugging)
item reboot Reboot System
item exit Exit to BIOS
choose --timeout 30000 --default control-plane selected || goto cancel
goto ''${selected}
:cancel
echo Boot cancelled, rebooting in 5 seconds...
sleep 5
reboot
:control-plane
set profile control-plane
echo Booting: Control Plane Node
goto boot
:worker
set profile worker
echo Booting: Worker Node
goto boot
:all-in-one
set profile all-in-one
echo Booting: All-in-One Node
goto boot
:boot
isset ''${hostname} || set hostname centra-node-''${mac:hexhyp}
echo Profile: ''${profile}
echo Hostname: ''${hostname}
echo MAC Address: ''${mac}
set kernel-params initrd=initrd ip=dhcp
set kernel-params ''${kernel-params} centra.profile=''${profile}
set kernel-params ''${kernel-params} centra.hostname=''${hostname}
set kernel-params ''${kernel-params} centra.mac=''${mac}
set kernel-params ''${kernel-params} centra.provisioning-server=''${provisioning-server}
set kernel-params ''${kernel-params} console=tty0 console=ttyS0,115200n8
kernel ''${nixos-url}/bzImage ''${kernel-params} || goto failed
initrd ''${nixos-url}/initrd || goto failed
boot || goto failed
:failed
echo Boot Failed!
echo Failed to load kernel or initrd from ''${nixos-url}
goto shell
:shell
echo Entering iPXE shell...
shell
:reboot
reboot
:exit
exit
'';
# Nginx configuration
nginxConf = pkgs.writeText "nginx.conf" ''
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include ${pkgs.nginx}/conf/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
types {
application/octet-stream kpxe;
application/octet-stream efi;
text/plain ipxe;
}
server {
listen ${toString cfg.http.port};
server_name _;
root ${cfg.bootAssetsPath};
location /boot/ {
alias ${cfg.bootAssetsPath}/;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
}
location ~ \.ipxe$ {
add_header Cache-Control "no-store, no-cache, must-revalidate";
expires -1;
}
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
location = / {
return 200 "Centra Cloud PXE Boot Server\n";
add_header Content-Type text/plain;
}
}
}
'';
in {
options.services.centra-pxe-server = {
enable = mkEnableOption "Centra Cloud PXE Boot Server";
interface = mkOption {
type = types.str;
default = "eth0";
description = "Network interface to listen on for DHCP requests";
};
serverAddress = mkOption {
type = types.str;
example = "10.0.100.10";
description = "IP address of the PXE boot server";
};
bootAssetsPath = mkOption {
type = types.path;
default = "/var/lib/pxe-boot";
description = "Path to boot assets directory";
};
dhcp = {
subnet = mkOption {
type = types.str;
example = "10.0.100.0";
description = "Network subnet for DHCP";
};
netmask = mkOption {
type = types.str;
default = "255.255.255.0";
description = "Network netmask";
};
broadcast = mkOption {
type = types.str;
example = "10.0.100.255";
description = "Broadcast address";
};
range = {
start = mkOption {
type = types.str;
example = "10.0.100.100";
description = "Start of DHCP range";
};
end = mkOption {
type = types.str;
example = "10.0.100.200";
description = "End of DHCP range";
};
};
router = mkOption {
type = types.str;
example = "10.0.100.1";
description = "Default gateway";
};
nameservers = mkOption {
type = types.listOf types.str;
default = [ "8.8.8.8" "8.8.4.4" ];
description = "DNS nameservers";
};
domainName = mkOption {
type = types.str;
default = "centra.local";
description = "Domain name";
};
defaultLeaseTime = mkOption {
type = types.int;
default = 600;
description = "Default DHCP lease time in seconds";
};
maxLeaseTime = mkOption {
type = types.int;
default = 7200;
description = "Maximum DHCP lease time in seconds";
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = "Additional DHCP configuration";
};
};
http = {
port = mkOption {
type = types.int;
default = 80;
description = "HTTP server port";
};
};
tftp = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable TFTP server for bootloader files";
};
};
nodes = mkOption {
type = types.attrsOf (types.submodule {
options = {
profile = mkOption {
type = types.enum [ "control-plane" "worker" "all-in-one" ];
description = "Node profile";
};
hostname = mkOption {
type = types.str;
description = "Node hostname";
};
ipAddress = mkOption {
type = types.str;
description = "Fixed IP address (optional)";
default = "";
};
};
});
default = {};
example = literalExpression ''
{
"52:54:00:12:34:56" = {
profile = "control-plane";
hostname = "control-plane-01";
ipAddress = "10.0.100.50";
};
}
'';
description = "MAC address to node configuration mapping";
};
};
config = mkIf cfg.enable {
# DHCP Server
services.dhcpd4 = {
enable = true;
interfaces = [ cfg.interface ];
configFile = dhcpdConf;
};
# TFTP Server
services.atftpd = mkIf cfg.tftp.enable {
enable = true;
root = "${cfg.bootAssetsPath}/ipxe";
};
# HTTP Server (Nginx)
services.nginx = {
enable = true;
package = pkgs.nginx;
appendHttpConfig = ''
server {
listen ${toString cfg.http.port};
server_name _;
root ${cfg.bootAssetsPath};
location /boot/ {
alias ${cfg.bootAssetsPath}/;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
}
location ~ \.ipxe$ {
add_header Cache-Control "no-store, no-cache, must-revalidate";
expires -1;
}
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
}
'';
};
# Firewall Rules
networking.firewall = {
allowedUDPPorts = [
67 # DHCP server
68 # DHCP client
69 # TFTP
];
allowedTCPPorts = [
cfg.http.port # HTTP
];
};
# Create boot assets directory structure
systemd.tmpfiles.rules = [
"d ${cfg.bootAssetsPath} 0755 nginx nginx -"
"d ${cfg.bootAssetsPath}/ipxe 0755 nginx nginx -"
"d ${cfg.bootAssetsPath}/nixos 0755 nginx nginx -"
"L+ ${cfg.bootAssetsPath}/ipxe/boot.ipxe - - - - ${bootIpxeScript}"
];
# Systemd service dependencies
systemd.services.dhcpd4.after = [ "network-online.target" ];
systemd.services.dhcpd4.wants = [ "network-online.target" ];
systemd.services.atftpd.after = [ "network-online.target" ];
systemd.services.atftpd.wants = [ "network-online.target" ];
# Environment packages for management
environment.systemPackages = with pkgs; [
dhcp
tftp-hpa
curl
wget
ipxe
];
};
}