#!/usr/bin/env bash ############################################################################### # PXE Boot Server Setup Script # # This script prepares a PXE boot server for Centra Cloud bare-metal # provisioning. It performs the following tasks: # # 1. Creates directory structure for boot assets # 2. Downloads iPXE bootloaders (or provides build instructions) # 3. Copies configuration files to appropriate locations # 4. Validates configuration files # 5. Tests DHCP/TFTP/HTTP services # # Usage: # sudo ./setup.sh [options] # # Options: # --install Install and configure services # --download Download iPXE bootloaders # --build-ipxe Build iPXE from source (recommended for production) # --validate Validate configuration files # --test Test services (DHCP, TFTP, HTTP) # --help Show this help message # # Example: # sudo ./setup.sh --install --download --validate ############################################################################### set -euo pipefail # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BOOT_ASSETS_DIR="/var/lib/pxe-boot" IPXE_DIR="${BOOT_ASSETS_DIR}/ipxe" NIXOS_DIR="${BOOT_ASSETS_DIR}/nixos" TFTP_DIR="/var/lib/tftpboot" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Logging functions log_info() { echo -e "${BLUE}[INFO]${NC} $*" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $*" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $*" } log_error() { echo -e "${RED}[ERROR]${NC} $*" } # Check if running as root check_root() { if [[ $EUID -ne 0 ]]; then log_error "This script must be run as root (use sudo)" exit 1 fi } # Display help show_help() { cat << EOF PXE Boot Server Setup Script Usage: sudo $0 [options] Options: --install Install and configure services --download Download iPXE bootloaders from boot.ipxe.org --build-ipxe Build iPXE from source (recommended for production) --validate Validate configuration files --test Test services (DHCP, TFTP, HTTP) --clean Clean up boot assets directory --help Show this help message Examples: # Full setup with pre-built bootloaders sudo $0 --install --download --validate # Build iPXE from source (more secure, customizable) sudo $0 --install --build-ipxe --validate # Validate configuration only sudo $0 --validate # Test services sudo $0 --test For more information, see README.md EOF } # Create directory structure create_directories() { log_info "Creating directory structure..." mkdir -p "${IPXE_DIR}" mkdir -p "${NIXOS_DIR}" mkdir -p "${TFTP_DIR}" mkdir -p /var/log/dhcpd mkdir -p /var/log/nginx # Set permissions chown -R nginx:nginx "${BOOT_ASSETS_DIR}" 2>/dev/null || \ log_warning "nginx user not found, skipping chown (install nginx first)" chmod -R 755 "${BOOT_ASSETS_DIR}" log_success "Directory structure created at ${BOOT_ASSETS_DIR}" } # Download iPXE bootloaders download_ipxe() { log_info "Downloading iPXE bootloaders from boot.ipxe.org..." # URLs for iPXE bootloaders IPXE_BASE_URL="https://boot.ipxe.org" # Download BIOS bootloader (undionly.kpxe) if [[ ! -f "${IPXE_DIR}/undionly.kpxe" ]]; then log_info "Downloading undionly.kpxe (BIOS bootloader)..." curl -L -o "${IPXE_DIR}/undionly.kpxe" "${IPXE_BASE_URL}/undionly.kpxe" || { log_error "Failed to download undionly.kpxe" return 1 } log_success "Downloaded undionly.kpxe ($(du -h "${IPXE_DIR}/undionly.kpxe" | cut -f1))" else log_info "undionly.kpxe already exists, skipping download" fi # Download UEFI bootloader (ipxe.efi) if [[ ! -f "${IPXE_DIR}/ipxe.efi" ]]; then log_info "Downloading ipxe.efi (UEFI x86-64 bootloader)..." curl -L -o "${IPXE_DIR}/ipxe.efi" "${IPXE_BASE_URL}/ipxe.efi" || { log_error "Failed to download ipxe.efi" return 1 } log_success "Downloaded ipxe.efi ($(du -h "${IPXE_DIR}/ipxe.efi" | cut -f1))" else log_info "ipxe.efi already exists, skipping download" fi # Download UEFI 32-bit bootloader (optional, rare) if [[ ! -f "${IPXE_DIR}/ipxe-i386.efi" ]]; then log_info "Downloading ipxe-i386.efi (UEFI x86 32-bit bootloader)..." curl -L -o "${IPXE_DIR}/ipxe-i386.efi" "${IPXE_BASE_URL}/ipxe-i386.efi" || { log_warning "Failed to download ipxe-i386.efi (this is optional)" } if [[ -f "${IPXE_DIR}/ipxe-i386.efi" ]]; then log_success "Downloaded ipxe-i386.efi ($(du -h "${IPXE_DIR}/ipxe-i386.efi" | cut -f1))" fi else log_info "ipxe-i386.efi already exists, skipping download" fi # Set permissions chmod 644 "${IPXE_DIR}"/*.{kpxe,efi} 2>/dev/null || true log_success "iPXE bootloaders downloaded successfully" } # Build iPXE from source build_ipxe() { log_info "Building iPXE from source..." # Check for required tools if ! command -v git &> /dev/null; then log_error "git is required to build iPXE" return 1 fi if ! command -v make &> /dev/null; then log_error "make is required to build iPXE" return 1 fi # Create temporary build directory BUILD_DIR=$(mktemp -d) log_info "Build directory: ${BUILD_DIR}" # Clone iPXE repository log_info "Cloning iPXE repository..." git clone https://github.com/ipxe/ipxe.git "${BUILD_DIR}/ipxe" || { log_error "Failed to clone iPXE repository" return 1 } cd "${BUILD_DIR}/ipxe/src" # Build BIOS bootloader log_info "Building undionly.kpxe (BIOS bootloader)..." make bin/undionly.kpxe || { log_error "Failed to build undionly.kpxe" return 1 } cp bin/undionly.kpxe "${IPXE_DIR}/undionly.kpxe" log_success "Built undionly.kpxe ($(du -h "${IPXE_DIR}/undionly.kpxe" | cut -f1))" # Build UEFI bootloader log_info "Building ipxe.efi (UEFI x86-64 bootloader)..." make bin-x86_64-efi/ipxe.efi || { log_error "Failed to build ipxe.efi" return 1 } cp bin-x86_64-efi/ipxe.efi "${IPXE_DIR}/ipxe.efi" log_success "Built ipxe.efi ($(du -h "${IPXE_DIR}/ipxe.efi" | cut -f1))" # Clean up cd / rm -rf "${BUILD_DIR}" # Set permissions chmod 644 "${IPXE_DIR}"/*.{kpxe,efi} 2>/dev/null || true log_success "iPXE bootloaders built successfully" } # Install boot scripts install_boot_scripts() { log_info "Installing boot scripts..." # Copy boot.ipxe if [[ -f "${SCRIPT_DIR}/ipxe/boot.ipxe" ]]; then cp "${SCRIPT_DIR}/ipxe/boot.ipxe" "${IPXE_DIR}/boot.ipxe" chmod 644 "${IPXE_DIR}/boot.ipxe" log_success "Installed boot.ipxe" else log_warning "boot.ipxe not found in ${SCRIPT_DIR}/ipxe/" fi # Copy MAC mappings documentation if [[ -f "${SCRIPT_DIR}/ipxe/mac-mappings.txt" ]]; then cp "${SCRIPT_DIR}/ipxe/mac-mappings.txt" "${IPXE_DIR}/mac-mappings.txt" chmod 644 "${IPXE_DIR}/mac-mappings.txt" log_success "Installed mac-mappings.txt" fi } # Create symlinks for TFTP create_tftp_symlinks() { log_info "Creating TFTP symlinks..." # Symlink bootloaders to TFTP directory for file in undionly.kpxe ipxe.efi ipxe-i386.efi; do if [[ -f "${IPXE_DIR}/${file}" ]]; then ln -sf "${IPXE_DIR}/${file}" "${TFTP_DIR}/${file}" log_success "Symlinked ${file} to TFTP directory" fi done } # Validate configuration files validate_configs() { log_info "Validating configuration files..." local errors=0 # Check DHCP configuration if [[ -f "${SCRIPT_DIR}/dhcp/dhcpd.conf" ]]; then log_info "Checking DHCP configuration..." if command -v dhcpd &> /dev/null; then dhcpd -t -cf "${SCRIPT_DIR}/dhcp/dhcpd.conf" &> /dev/null && \ log_success "DHCP configuration is valid" || { log_error "DHCP configuration is invalid" dhcpd -t -cf "${SCRIPT_DIR}/dhcp/dhcpd.conf" ((errors++)) } else log_warning "dhcpd not installed, skipping DHCP validation" fi else log_error "dhcpd.conf not found" ((errors++)) fi # Check Nginx configuration if [[ -f "${SCRIPT_DIR}/http/nginx.conf" ]]; then log_info "Checking Nginx configuration..." if command -v nginx &> /dev/null; then nginx -t -c "${SCRIPT_DIR}/http/nginx.conf" &> /dev/null && \ log_success "Nginx configuration is valid" || { log_error "Nginx configuration is invalid" nginx -t -c "${SCRIPT_DIR}/http/nginx.conf" ((errors++)) } else log_warning "nginx not installed, skipping Nginx validation" fi else log_error "nginx.conf not found" ((errors++)) fi # Check iPXE boot script if [[ -f "${SCRIPT_DIR}/ipxe/boot.ipxe" ]]; then log_info "Checking iPXE boot script..." # Basic syntax check (iPXE doesn't have a validation tool) if grep -q "#!ipxe" "${SCRIPT_DIR}/ipxe/boot.ipxe"; then log_success "iPXE boot script appears valid" else log_error "iPXE boot script is missing #!ipxe shebang" ((errors++)) fi else log_error "boot.ipxe not found" ((errors++)) fi # Check for required bootloaders log_info "Checking for iPXE bootloaders..." for file in undionly.kpxe ipxe.efi; do if [[ -f "${IPXE_DIR}/${file}" ]]; then log_success "Found ${file} ($(du -h "${IPXE_DIR}/${file}" | cut -f1))" else log_warning "${file} not found (run --download or --build-ipxe)" fi done if [[ $errors -eq 0 ]]; then log_success "All configuration files are valid" return 0 else log_error "Found $errors configuration error(s)" return 1 fi } # Test services test_services() { log_info "Testing PXE boot services..." local errors=0 # Test TFTP server log_info "Testing TFTP server..." if systemctl is-active --quiet atftpd 2>/dev/null; then log_success "TFTP server (atftpd) is running" # Try to fetch a file via TFTP if command -v tftp &> /dev/null; then timeout 5 tftp localhost -c get undionly.kpxe /tmp/test-undionly.kpxe &> /dev/null && { log_success "TFTP fetch test successful" rm -f /tmp/test-undionly.kpxe } || { log_warning "TFTP fetch test failed (this may be normal if files aren't ready)" } fi else log_error "TFTP server is not running" ((errors++)) fi # Test HTTP server log_info "Testing HTTP server..." if systemctl is-active --quiet nginx 2>/dev/null; then log_success "HTTP server (nginx) is running" # Try to fetch health endpoint if command -v curl &> /dev/null; then curl -f -s http://localhost/health &> /dev/null && { log_success "HTTP health check successful" } || { log_warning "HTTP health check failed" ((errors++)) } fi else log_error "HTTP server is not running" ((errors++)) fi # Test DHCP server log_info "Testing DHCP server..." if systemctl is-active --quiet dhcpd4 2>/dev/null || \ systemctl is-active --quiet isc-dhcp-server 2>/dev/null; then log_success "DHCP server is running" else log_error "DHCP server is not running" ((errors++)) fi # Network connectivity test log_info "Checking network interfaces..." ip addr show | grep -q "inet " && { log_success "Network interfaces are up" } || { log_error "No network interfaces with IP addresses found" ((errors++)) } if [[ $errors -eq 0 ]]; then log_success "All service tests passed" return 0 else log_error "Found $errors service error(s)" return 1 fi } # Clean up boot assets clean_assets() { log_warning "Cleaning up boot assets directory..." read -p "This will delete ${BOOT_ASSETS_DIR}. Continue? (y/N) " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then rm -rf "${BOOT_ASSETS_DIR}" rm -rf "${TFTP_DIR}" log_success "Boot assets cleaned up" else log_info "Cleanup cancelled" fi } # Full installation full_install() { log_info "Starting full PXE server installation..." create_directories install_boot_scripts create_tftp_symlinks log_success "Installation complete!" log_info "" log_info "Next steps:" log_info " 1. Download or build iPXE bootloaders:" log_info " sudo $0 --download" log_info " OR" log_info " sudo $0 --build-ipxe" log_info "" log_info " 2. Configure your network settings in:" log_info " ${SCRIPT_DIR}/dhcp/dhcpd.conf" log_info " ${SCRIPT_DIR}/nixos-module.nix" log_info "" log_info " 3. Deploy NixOS configuration or manually start services" log_info "" log_info " 4. Add NixOS boot images to ${NIXOS_DIR}/" log_info " (This will be done by T032.S3 - Image Builder)" } # Main script main() { if [[ $# -eq 0 ]]; then show_help exit 0 fi check_root while [[ $# -gt 0 ]]; do case $1 in --install) full_install shift ;; --download) download_ipxe shift ;; --build-ipxe) build_ipxe shift ;; --validate) validate_configs shift ;; --test) test_services shift ;; --clean) clean_assets shift ;; --help) show_help exit 0 ;; *) log_error "Unknown option: $1" show_help exit 1 ;; esac done } main "$@"