photoncloud-monorepo/chainfire/baremetal/pxe-server/setup.sh
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

498 lines
14 KiB
Bash
Executable file

#!/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 "$@"