#!/usr/bin/env bash # ============================================================================ # install-pxe-server.sh # Automated PXE boot server setup — installs dnsmasq, TFTP, and nginx, # configures PXE boot menus, generates Kickstart and Preseed templates # # Author: Phil Connor # Contact: contact@mylinux.work # License: MIT # Version: 1.0.0 # ============================================================================ set -uo pipefail # ============================================================================ # Defaults # ============================================================================ INTERFACE="" DHCP_RANGE="10.0.0.100,10.0.0.200" TFTP_ROOT="/srv/tftp" HTTP_ROOT="/var/www/pxe" DISTROS="rocky9" SERVER_IP="" OS_FAMILY="" OS_ID="" OS_VERSION="" # ============================================================================ # Colour output # ============================================================================ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' log_info() { echo -e "${CYAN}[INFO]${NC} $*"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } log_error() { echo -e "${RED}[ERROR]${NC} $*"; } log_success() { echo -e "${GREEN}[OK]${NC} $*"; } # ============================================================================ # Usage # ============================================================================ usage() { cat < Network interface for DHCP/TFTP binding (default: auto-detected from default route) --dhcp-range DHCP range as start,end (default: 10.0.0.100,10.0.0.200) --tftp-root TFTP root directory (default: /srv/tftp) --http-root HTTP document root for install media (default: /var/www/pxe) --distros Comma-separated list of distros to configure (default: rocky9) --help Print this help and exit Supported distros: rocky9, rocky8, rhel9, rhel8, alma9, ubuntu2404, ubuntu2204, debian12, debian11 Examples: sudo $(basename "$0") sudo $(basename "$0") --interface eth0 --dhcp-range 10.0.0.100,10.0.0.200 sudo $(basename "$0") --distros rocky9,ubuntu2404,debian12 sudo $(basename "$0") --interface ens192 --tftp-root /data/tftp --distros rocky9 EOF exit 0 } # ============================================================================ # Parse arguments # ============================================================================ parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --interface) INTERFACE="$2" shift 2 ;; --dhcp-range) DHCP_RANGE="$2" shift 2 ;; --tftp-root) TFTP_ROOT="$2" shift 2 ;; --http-root) HTTP_ROOT="$2" shift 2 ;; --distros) DISTROS="$2" shift 2 ;; --help) usage ;; *) log_error "Unknown option: $1" usage ;; esac done } # ============================================================================ # Detect OS # ============================================================================ detect_os() { log_info "Detecting operating system..." if [[ ! -f /etc/os-release ]]; then log_error "Cannot detect OS — /etc/os-release not found" exit 1 fi # shellcheck disable=SC1091 source /etc/os-release OS_ID="${ID}" OS_VERSION="${VERSION_ID}" case "${OS_ID}" in debian|ubuntu) OS_FAMILY="debian" ;; rocky|rhel|almalinux|centos) OS_FAMILY="rhel" ;; *) log_error "Unsupported OS: ${OS_ID} ${OS_VERSION}" log_error "Supported: Debian 11+, Ubuntu 22.04+, RHEL/Rocky/Alma 8+" exit 1 ;; esac log_success "Detected ${OS_ID} ${OS_VERSION} (${OS_FAMILY} family)" } # ============================================================================ # Detect network interface # ============================================================================ detect_interface() { if [[ -n "${INTERFACE}" ]]; then if ! ip link show "${INTERFACE}" &>/dev/null; then log_error "Interface ${INTERFACE} does not exist" echo "Available interfaces:" ip -o link show | awk -F': ' '{print " " $2}' exit 1 fi log_info "Using specified interface: ${INTERFACE}" else log_info "Auto-detecting network interface..." INTERFACE=$(ip route show default 2>/dev/null | awk '{print $5; exit}') if [[ -z "${INTERFACE}" ]]; then log_error "Cannot detect default network interface" log_error "Specify one with --interface" exit 1 fi log_success "Detected interface: ${INTERFACE}" fi SERVER_IP=$(ip -4 addr show "${INTERFACE}" | awk '/inet / {split($2,a,"/"); print a[1]; exit}') if [[ -z "${SERVER_IP}" ]]; then log_error "Cannot determine IP address for interface ${INTERFACE}" exit 1 fi log_success "Server IP: ${SERVER_IP}" } # ============================================================================ # Validate distro list # ============================================================================ validate_distros() { local valid_distros="rocky9 rocky8 rhel9 rhel8 alma9 ubuntu2404 ubuntu2204 debian12 debian11" IFS=',' read -ra DISTRO_LIST <<< "${DISTROS}" for distro in "${DISTRO_LIST[@]}"; do local found=false for valid in ${valid_distros}; do if [[ "${distro}" == "${valid}" ]]; then found=true break fi done if [[ "${found}" == "false" ]]; then log_error "Invalid distro: ${distro}" log_error "Valid options: ${valid_distros}" exit 1 fi done log_success "Distros to configure: ${DISTROS}" } # ============================================================================ # Install packages # ============================================================================ install_packages() { log_info "Installing packages..." if [[ "${OS_FAMILY}" == "debian" ]]; then export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq \ dnsmasq \ tftpd-hpa \ syslinux-common \ pxelinux \ nginx \ wget \ curl \ >/dev/null 2>&1 if [[ $? -ne 0 ]]; then log_error "Package installation failed" exit 1 fi elif [[ "${OS_FAMILY}" == "rhel" ]]; then dnf install -y -q \ dnsmasq \ tftp-server \ syslinux \ nginx \ wget \ curl \ >/dev/null 2>&1 if [[ $? -ne 0 ]]; then log_error "Package installation failed" exit 1 fi fi log_success "Packages installed" } # ============================================================================ # Configure dnsmasq # ============================================================================ configure_dnsmasq() { log_info "Configuring dnsmasq..." local dhcp_start dhcp_end dhcp_start=$(echo "${DHCP_RANGE}" | cut -d',' -f1) dhcp_end=$(echo "${DHCP_RANGE}" | cut -d',' -f2) # Disable default dnsmasq DNS to avoid conflicts if [[ -f /etc/dnsmasq.conf ]]; then cp /etc/dnsmasq.conf /etc/dnsmasq.conf.bak fi mkdir -p /etc/dnsmasq.d cat > /etc/dnsmasq.d/pxe.conf <> /etc/dnsmasq.conf fi fi log_success "dnsmasq configured at /etc/dnsmasq.d/pxe.conf" } # ============================================================================ # Setup TFTP directory # ============================================================================ setup_tftp() { log_info "Setting up TFTP directory..." mkdir -p "${TFTP_ROOT}/pxelinux.cfg" mkdir -p "${TFTP_ROOT}/grub" # Copy syslinux/pxelinux files if [[ "${OS_FAMILY}" == "debian" ]]; then local pxe_src="/usr/lib/PXELINUX" local sys_src="/usr/lib/syslinux/modules/bios" if [[ -f "${pxe_src}/pxelinux.0" ]]; then cp "${pxe_src}/pxelinux.0" "${TFTP_ROOT}/" else log_warn "pxelinux.0 not found at ${pxe_src}/pxelinux.0" fi for file in ldlinux.c32 menu.c32 libmenu.c32 libutil.c32; do if [[ -f "${sys_src}/${file}" ]]; then cp "${sys_src}/${file}" "${TFTP_ROOT}/" else log_warn "${file} not found at ${sys_src}/${file}" fi done elif [[ "${OS_FAMILY}" == "rhel" ]]; then local sys_src="/usr/share/syslinux" for file in pxelinux.0 ldlinux.c32 menu.c32 libmenu.c32 libutil.c32; do if [[ -f "${sys_src}/${file}" ]]; then cp "${sys_src}/${file}" "${TFTP_ROOT}/" else log_warn "${file} not found at ${sys_src}/${file}" fi done fi # Create distro directories in TFTP root IFS=',' read -ra DISTRO_LIST <<< "${DISTROS}" for distro in "${DISTRO_LIST[@]}"; do mkdir -p "${TFTP_ROOT}/${distro}" done # Set permissions chmod -R 755 "${TFTP_ROOT}" if [[ "${OS_FAMILY}" == "debian" ]]; then chown -R tftp:tftp "${TFTP_ROOT}" # Configure tftpd-hpa cat > /etc/default/tftpd-hpa < "${menu_file}" <<'MENU_HEADER' DEFAULT menu.c32 PROMPT 0 TIMEOUT 300 ONTIMEOUT local MENU TITLE ===== PXE Boot Server ===== MENU COLOR border 30;44 #40ffffff #a0000000 std MENU COLOR title 1;36;44 #9033cccc #a0000000 std MENU COLOR sel 7;37;40 #e0ffffff #20ffffff all MENU COLOR unsel 37;44 #50ffffff #a0000000 std MENU COLOR help 37;40 #c0ffffff #a0000000 std MENU COLOR timeout_msg 37;40 #80ffffff #00000000 std MENU COLOR timeout 1;37;40 #c0ffffff #00000000 std LABEL local MENU LABEL Boot from local disk MENU DEFAULT LOCALBOOT 0 MENU_HEADER IFS=',' read -ra DISTRO_LIST <<< "${DISTROS}" for distro in "${DISTRO_LIST[@]}"; do case "${distro}" in rocky9) cat >> "${menu_file}" <> "${menu_file}" <> "${menu_file}" <> "${menu_file}" <> "${menu_file}" <> "${menu_file}" <> "${menu_file}" <> "${menu_file}" <> "${menu_file}" < "${ks_file}" <> /root/ks-post.log %end EOF log_success "Kickstart template created: ${ks_file}" } # ============================================================================ # Generate Preseed template # ============================================================================ generate_preseed() { local distro="$1" local preseed_dir="${HTTP_ROOT}/preseed" local preseed_file="${preseed_dir}/preseed-${distro}.cfg" mkdir -p "${preseed_dir}" log_info "Generating Preseed template: ${preseed_file}" local mirror_host="deb.debian.org" local mirror_dir="/debian" if [[ "${distro}" == ubuntu* ]]; then mirror_host="archive.ubuntu.com" mirror_dir="/ubuntu" fi cat > "${preseed_file}" <> /target/root/preseed-post.log ### Reboot after install d-i finish-install/reboot_in_progress note d-i debian-installer/exit/poweroff boolean false EOF log_success "Preseed template created: ${preseed_file}" } # ============================================================================ # Configure nginx # ============================================================================ configure_nginx() { log_info "Configuring nginx..." mkdir -p "${HTTP_ROOT}"/{ks,preseed} # Create distro directories in HTTP root IFS=',' read -ra DISTRO_LIST <<< "${DISTROS}" for distro in "${DISTRO_LIST[@]}"; do mkdir -p "${HTTP_ROOT}/${distro}" done # Remove default site if present rm -f /etc/nginx/sites-enabled/default 2>/dev/null rm -f /etc/nginx/conf.d/default.conf 2>/dev/null cat > /etc/nginx/conf.d/pxe.conf </dev/null; then log_success "nginx configured at /etc/nginx/conf.d/pxe.conf" else log_error "nginx configuration test failed" nginx -t exit 1 fi } # ============================================================================ # Configure firewall # ============================================================================ configure_firewall() { log_info "Configuring firewall..." if command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld; then log_info "Detected firewalld" firewall-cmd --permanent --add-port=67/udp # DHCP firewall-cmd --permanent --add-port=68/udp # DHCP client firewall-cmd --permanent --add-port=69/udp # TFTP firewall-cmd --permanent --add-port=80/tcp # HTTP firewall-cmd --permanent --add-port=4011/udp # ProxyDHCP firewall-cmd --reload log_success "Firewall ports opened (firewalld)" elif command -v ufw &>/dev/null && ufw status | grep -q "Status: active"; then log_info "Detected ufw" ufw allow 67/udp comment "DHCP server" ufw allow 68/udp comment "DHCP client" ufw allow 69/udp comment "TFTP" ufw allow 80/tcp comment "HTTP" ufw allow 4011/udp comment "ProxyDHCP" log_success "Firewall ports opened (ufw)" else log_warn "No active firewall detected — skipping firewall configuration" log_warn "Manually open ports: 67/udp, 68/udp, 69/udp, 80/tcp, 4011/udp" fi } # ============================================================================ # Enable services # ============================================================================ enable_services() { log_info "Enabling and starting services..." # dnsmasq systemctl enable dnsmasq systemctl restart dnsmasq if systemctl is-active --quiet dnsmasq; then log_success "dnsmasq is running" else log_error "dnsmasq failed to start" journalctl -u dnsmasq --no-pager -n 10 fi # TFTP if [[ "${OS_FAMILY}" == "debian" ]]; then systemctl enable tftpd-hpa systemctl restart tftpd-hpa if systemctl is-active --quiet tftpd-hpa; then log_success "tftpd-hpa is running" else log_error "tftpd-hpa failed to start" journalctl -u tftpd-hpa --no-pager -n 10 fi elif [[ "${OS_FAMILY}" == "rhel" ]]; then systemctl enable tftp.socket systemctl restart tftp.socket if systemctl is-active --quiet tftp.socket; then log_success "tftp.socket is running" else log_error "tftp.socket failed to start" journalctl -u tftp.socket --no-pager -n 10 fi fi # nginx systemctl enable nginx systemctl restart nginx if systemctl is-active --quiet nginx; then log_success "nginx is running" else log_error "nginx failed to start" journalctl -u nginx --no-pager -n 10 fi } # ============================================================================ # Print summary # ============================================================================ print_summary() { local dhcp_start dhcp_end dhcp_start=$(echo "${DHCP_RANGE}" | cut -d',' -f1) dhcp_end=$(echo "${DHCP_RANGE}" | cut -d',' -f2) echo "" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ PXE Boot Server — Setup Complete ║" echo "╠══════════════════════════════════════════════════════════════╣" echo "║ ║" printf "║ Server IP: %-41s║\n" "${SERVER_IP}" printf "║ Interface: %-41s║\n" "${INTERFACE}" printf "║ DHCP Range: %-41s║\n" "${dhcp_start} — ${dhcp_end}" printf "║ TFTP Root: %-41s║\n" "${TFTP_ROOT}" printf "║ HTTP Root: %-41s║\n" "${HTTP_ROOT}" printf "║ Distros: %-41s║\n" "${DISTROS}" echo "║ ║" echo "╠══════════════════════════════════════════════════════════════╣" echo "║ Configuration Files ║" echo "╠══════════════════════════════════════════════════════════════╣" printf "║ dnsmasq: %-41s║\n" "/etc/dnsmasq.d/pxe.conf" printf "║ PXE menu: %-41s║\n" "${TFTP_ROOT}/pxelinux.cfg/default" printf "║ nginx: %-41s║\n" "/etc/nginx/conf.d/pxe.conf" echo "║ ║" IFS=',' read -ra DISTRO_LIST <<< "${DISTROS}" for distro in "${DISTRO_LIST[@]}"; do case "${distro}" in rocky*|rhel*|alma*) printf "║ Kickstart: %-41s║\n" "http://${SERVER_IP}/ks/ks-${distro}.cfg" ;; ubuntu*|debian*) printf "║ Preseed: %-41s║\n" "http://${SERVER_IP}/preseed/preseed-${distro}.cfg" ;; esac done echo "║ ║" echo "╠══════════════════════════════════════════════════════════════╣" echo "║ Next Steps ║" echo "╠══════════════════════════════════════════════════════════════╣" echo "║ ║" echo "║ 1. Mount or extract distro ISOs into the HTTP root: ║" for distro in "${DISTRO_LIST[@]}"; do printf "║ mount -o loop,ro %-33s║\n" "${HTTP_ROOT}/${distro}/" done echo "║ ║" echo "║ 2. Copy kernel + initrd to TFTP root: ║" for distro in "${DISTRO_LIST[@]}"; do printf "║ %s -> %-43s║\n" "vmlinuz/initrd" "${TFTP_ROOT}/${distro}/" done echo "║ ║" echo "║ 3. Edit Kickstart/Preseed templates: ║" echo "║ - Set root/user password hashes ║" echo "║ - Adjust partitioning layout ║" echo "║ - Add site-specific packages ║" echo "║ ║" echo "║ 4. PXE boot a target machine and select a distro ║" echo "║ ║" echo "╚══════════════════════════════════════════════════════════════╝" echo "" } # ============================================================================ # Main # ============================================================================ main() { echo "" echo "================================================" echo " PXE Boot Server — Automated Setup" echo " Version 1.0.0" echo "================================================" echo "" # Check root if [[ "${EUID}" -ne 0 ]]; then log_error "This script must be run as root" exit 1 fi parse_args "$@" detect_os detect_interface validate_distros install_packages configure_dnsmasq setup_tftp create_pxe_menu # Generate answer file templates based on selected distros IFS=',' read -ra DISTRO_LIST <<< "${DISTROS}" for distro in "${DISTRO_LIST[@]}"; do case "${distro}" in rocky*|rhel*|alma*) generate_kickstart "${distro}" ;; ubuntu*|debian*) generate_preseed "${distro}" ;; esac done configure_nginx configure_firewall enable_services print_summary log_success "PXE boot server setup complete" } main "$@"