From 7486b9ed66efe6469db875bacee2d0e513721792 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Tue, 10 Feb 2026 17:45:10 -0500 Subject: [PATCH] Apache reverse proxy for webapp, UI improvements - Move Flask to localhost:9010, Apache serves port 9009 with static file handling and reverse proxy to fix intermittent asset loading on remote clients - Add "PXE Manager" branding beneath logo in sidebar - Increase code editor size (startnet.cmd and unattend XML) to 70vh - Add test-lab.sh for full lab VM testing Co-Authored-By: Claude Opus 4.6 --- playbook/pxe_server_setup.yml | 17 +- test-lab.sh | 332 ++++++++++++++++++++++++++ webapp/app.py | 2 +- webapp/templates/base.html | 3 +- webapp/templates/startnet_editor.html | 4 +- webapp/templates/unattend_editor.html | 3 +- 6 files changed, 355 insertions(+), 6 deletions(-) create mode 100755 test-lab.sh diff --git a/playbook/pxe_server_setup.yml b/playbook/pxe_server_setup.yml index 0932bca..8cee93c 100644 --- a/playbook/pxe_server_setup.yml +++ b/playbook/pxe_server_setup.yml @@ -331,6 +331,7 @@ port: "{{ item }}" proto: "{{ 'udp' if item in ['67','69'] else 'tcp' }}" loop: + - "22" - "67" - "69" - "80" @@ -430,6 +431,7 @@ copy: dest: /etc/apache2/sites-available/pxe-server.conf content: | + Listen 9009 DocumentRoot {{ web_root }} @@ -438,8 +440,19 @@ Require all granted ProxyPreserveHost On - ProxyPass /manage http://127.0.0.1:9009/ - ProxyPassReverse /manage http://127.0.0.1:9009/ + ProxyPass /manage http://127.0.0.1:9010/ + ProxyPassReverse /manage http://127.0.0.1:9010/ + + + Alias /static /opt/pxe-webapp/static + + Require all granted + Options -Indexes + + ProxyPreserveHost On + ProxyPass /static ! + ProxyPass / http://127.0.0.1:9010/ + ProxyPassReverse / http://127.0.0.1:9010/ - name: "Enable Apache proxy modules" diff --git a/test-lab.sh b/test-lab.sh new file mode 100755 index 0000000..637febf --- /dev/null +++ b/test-lab.sh @@ -0,0 +1,332 @@ +#!/bin/bash +# +# test-lab.sh — Full PXE lab: server + client VMs on an isolated network +# +# Creates an isolated libvirt network, boots the PXE server from the +# Proxmox installer ISO, then launches a UEFI PXE client to test the +# full boot chain (DHCP -> TFTP -> iPXE -> boot menu). +# +# Usage: +# ./test-lab.sh /path/to/ubuntu-24.04.iso # Launch server +# ./test-lab.sh --client # Launch PXE client +# ./test-lab.sh --status # Check if server is ready +# ./test-lab.sh --destroy # Remove everything +# +# Workflow: +# 1. Run the script with the Ubuntu ISO — server VM starts installing +# 2. Wait ~15 minutes (monitor with: sudo virsh console pxe-lab-server) +# 3. Run --status to check if PXE services are up +# 4. Run --client to launch a PXE client VM +# 5. Open virt-viewer to watch the client PXE boot: +# virt-viewer pxe-lab-client + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +NET_NAME="pxe-lab" +SERVER_NAME="pxe-lab-server" +CLIENT_NAME="pxe-lab-client" +SERVER_DISK="/var/lib/libvirt/images/${SERVER_NAME}.qcow2" +PROXMOX_ISO="$SCRIPT_DIR/pxe-server-proxmox.iso" +VM_RAM=4096 +VM_CPUS=2 +VM_DISK_SIZE=40 # GB + +# --- Helper: check if we can run virsh --- +check_virsh() { + if ! virsh net-list &>/dev/null; then + echo "ERROR: Cannot connect to libvirt. Are you in the 'libvirt' group?" + echo " sudo usermod -aG libvirt $USER && newgrp libvirt" + exit 1 + fi +} + +# --- Helper: ensure network exists --- +ensure_network() { + if ! virsh net-info "$NET_NAME" &>/dev/null; then + echo "Creating isolated network ($NET_NAME)..." + NET_XML=$(mktemp) + cat > "$NET_XML" << EOF + + $NET_NAME + + +EOF + virsh net-define "$NET_XML" >/dev/null + rm "$NET_XML" + fi + if ! virsh net-info "$NET_NAME" 2>/dev/null | grep -q "Active:.*yes"; then + virsh net-start "$NET_NAME" >/dev/null + fi +} + +# ===================================================================== +# --destroy: Remove everything +# ===================================================================== +if [ "${1:-}" = "--destroy" ]; then + check_virsh + echo "Destroying PXE lab environment..." + virsh destroy "$CLIENT_NAME" 2>/dev/null || true + virsh undefine "$CLIENT_NAME" --nvram 2>/dev/null || true + virsh vol-delete "${CLIENT_NAME}.qcow2" --pool default 2>/dev/null || true + virsh destroy "$SERVER_NAME" 2>/dev/null || true + virsh undefine "$SERVER_NAME" 2>/dev/null || true + virsh vol-delete "${SERVER_NAME}.qcow2" --pool default 2>/dev/null || true + rm -f "/tmp/${SERVER_NAME}-vmlinuz" "/tmp/${SERVER_NAME}-initrd" + virsh net-destroy "$NET_NAME" 2>/dev/null || true + virsh net-undefine "$NET_NAME" 2>/dev/null || true + echo "Done." + exit 0 +fi + +# ===================================================================== +# --status: Check if PXE server is ready +# ===================================================================== +if [ "${1:-}" = "--status" ]; then + check_virsh + echo "PXE Lab Status" + echo "============================================" + + # Network + if virsh net-info "$NET_NAME" &>/dev/null; then + echo " Network ($NET_NAME): $(virsh net-info "$NET_NAME" 2>/dev/null | grep Active | awk '{print $2}')" + else + echo " Network ($NET_NAME): not defined" + fi + + # Server VM + if virsh dominfo "$SERVER_NAME" &>/dev/null; then + STATE=$(virsh domstate "$SERVER_NAME" 2>/dev/null) + echo " Server ($SERVER_NAME): $STATE" + else + echo " Server ($SERVER_NAME): not defined" + fi + + # Client VM + if virsh dominfo "$CLIENT_NAME" &>/dev/null; then + STATE=$(virsh domstate "$CLIENT_NAME" 2>/dev/null) + echo " Client ($CLIENT_NAME): $STATE" + else + echo " Client ($CLIENT_NAME): not defined" + fi + + # Try to check PXE services on the server + # Add a temporary IP to the bridge so we can reach the server + echo "" + echo "Checking PXE server services..." + BRIDGE_HAS_IP=false + ADDED_IP=false + if ip addr show virbr-pxe 2>/dev/null | grep -q "10.9.100.254"; then + BRIDGE_HAS_IP=true + else + # Need sudo for IP manipulation — try it, skip if unavailable + if sudo -n ip addr add 10.9.100.254/24 dev virbr-pxe 2>/dev/null; then + BRIDGE_HAS_IP=true + ADDED_IP=true + fi + fi + + if [ "$BRIDGE_HAS_IP" = true ]; then + # Check each service with a short timeout + for check in \ + "DHCP/TFTP (dnsmasq):10.9.100.1:69:udp" \ + "HTTP (Apache):10.9.100.1:80:tcp" \ + "iPXE boot script:10.9.100.1:4433:tcp" \ + "Samba:10.9.100.1:445:tcp" \ + "Webapp:10.9.100.1:9009:tcp"; do + LABEL="${check%%:*}" + REST="${check#*:}" + HOST="${REST%%:*}" + REST="${REST#*:}" + PORT="${REST%%:*}" + PROTO="${REST#*:}" + + if [ "$PROTO" = "tcp" ]; then + if timeout 2 bash -c "echo >/dev/tcp/$HOST/$PORT" 2>/dev/null; then + echo " [UP] $LABEL (port $PORT)" + else + echo " [DOWN] $LABEL (port $PORT)" + fi + else + # UDP — just check if host is reachable + if ping -c1 -W1 "$HOST" &>/dev/null; then + echo " [PING] $LABEL (host reachable)" + else + echo " [DOWN] $LABEL (host unreachable)" + fi + fi + done + + # Clean up the temporary IP (only if we added it) + if [ "$ADDED_IP" = true ]; then + sudo -n ip addr del 10.9.100.254/24 dev virbr-pxe 2>/dev/null || true + fi + else + echo " (Cannot reach server — bridge not available)" + echo " Use 'sudo virsh console $SERVER_NAME' to check manually" + fi + + echo "" + echo "Commands:" + echo " sudo virsh console $SERVER_NAME # Server serial console" + echo " virt-viewer $CLIENT_NAME # Client VNC display" + exit 0 +fi + +# ===================================================================== +# --client: Launch PXE client VM +# ===================================================================== +if [ "${1:-}" = "--client" ]; then + check_virsh + ensure_network + + # Check if server VM exists and is running + if ! virsh domstate "$SERVER_NAME" 2>/dev/null | grep -q "running"; then + echo "WARNING: Server VM ($SERVER_NAME) is not running." + echo " The PXE client needs the server for DHCP and boot files." + read -rp " Continue anyway? (y/N): " PROCEED + if [[ ! "$PROCEED" =~ ^[Yy]$ ]]; then + exit 1 + fi + fi + + # Remove existing client if present + if virsh dominfo "$CLIENT_NAME" &>/dev/null; then + echo "Removing existing client VM..." + virsh destroy "$CLIENT_NAME" 2>/dev/null || true + virsh undefine "$CLIENT_NAME" --nvram 2>/dev/null || true + virsh vol-delete "${CLIENT_NAME}.qcow2" --pool default 2>/dev/null || true + fi + + echo "Launching PXE client ($CLIENT_NAME)..." + echo " UEFI PXE boot on network: $NET_NAME" + echo "" + + virsh vol-create-as default "${CLIENT_NAME}.qcow2" "${VM_DISK_SIZE}G" --format qcow2 >/dev/null + + virt-install \ + --name "$CLIENT_NAME" \ + --memory "$VM_RAM" \ + --vcpus "$VM_CPUS" \ + --disk "vol=default/${CLIENT_NAME}.qcow2" \ + --network network="$NET_NAME",model=virtio \ + --os-variant ubuntu24.04 \ + --boot uefi,network \ + --graphics vnc,listen=0.0.0.0 \ + --noautoconsole + + # Get VNC port + VNC_PORT=$(virsh vncdisplay "$CLIENT_NAME" 2>/dev/null | sed 's/://' || echo "?") + VNC_PORT=$((5900 + VNC_PORT)) + + echo "" + echo "============================================" + echo "PXE client launched!" + echo "============================================" + echo "" + echo "Watch the PXE boot:" + echo " virt-viewer $CLIENT_NAME" + echo " (or VNC to localhost:$VNC_PORT)" + echo "" + echo "Expected boot sequence:" + echo " 1. UEFI firmware -> PXE boot" + echo " 2. DHCP from server (10.9.100.x)" + echo " 3. TFTP download ipxe.efi" + echo " 4. iPXE loads boot menu from port 4433" + echo " 5. GE Aerospace PXE Boot Menu appears" + echo "" + echo "Manage:" + echo " sudo virsh reboot $CLIENT_NAME # Retry PXE boot" + echo " sudo virsh destroy $CLIENT_NAME # Stop client" + echo " $0 --destroy # Remove everything" + exit 0 +fi + +# ===================================================================== +# Default: Launch PXE server VM +# ===================================================================== +check_virsh + +UBUNTU_ISO="${1:-}" +if [ -z "$UBUNTU_ISO" ] || [ ! -f "$UBUNTU_ISO" ]; then + echo "Usage: sudo $0 /path/to/ubuntu-24.04-live-server-amd64.iso" + echo "" + echo "Commands:" + echo " $0 /path/to/ubuntu.iso Launch PXE server VM" + echo " $0 --client Launch PXE client VM" + echo " $0 --status Check server readiness" + echo " $0 --destroy Remove everything" + exit 1 +fi + +# Check if Proxmox ISO exists, build if not +if [ ! -f "$PROXMOX_ISO" ]; then + echo "Proxmox ISO not found. Building it first..." + echo "" + "$SCRIPT_DIR/build-proxmox-iso.sh" "$UBUNTU_ISO" + echo "" +fi + +# Check server doesn't already exist +if virsh dominfo "$SERVER_NAME" &>/dev/null; then + echo "ERROR: Server VM already exists. Destroy first with: sudo $0 --destroy" + exit 1 +fi + +echo "============================================" +echo "PXE Lab Environment Setup" +echo "============================================" +echo "" + +# --- Step 1: Create isolated network --- +echo "[1/3] Setting up isolated network ($NET_NAME)..." +ensure_network +echo " Bridge: virbr-pxe (isolated, no host DHCP)" + +# --- Step 2: Extract kernel/initrd for direct boot --- +echo "[2/3] Extracting kernel and initrd from ISO..." +KERNEL="/tmp/${SERVER_NAME}-vmlinuz" +INITRD="/tmp/${SERVER_NAME}-initrd" +7z e -o/tmp -y "$PROXMOX_ISO" casper/vmlinuz casper/initrd >/dev/null 2>&1 +mv /tmp/vmlinuz "$KERNEL" +mv /tmp/initrd "$INITRD" +echo " Extracted vmlinuz and initrd" + +# --- Step 3: Launch server VM --- +echo "[3/3] Launching PXE server ($SERVER_NAME)..." + +virsh vol-create-as default "${SERVER_NAME}.qcow2" "${VM_DISK_SIZE}G" --format qcow2 >/dev/null + +virt-install \ + --name "$SERVER_NAME" \ + --memory "$VM_RAM" \ + --vcpus "$VM_CPUS" \ + --disk "vol=default/${SERVER_NAME}.qcow2" \ + --disk path="$PROXMOX_ISO",device=cdrom,readonly=on \ + --network network="$NET_NAME" \ + --os-variant ubuntu24.04 \ + --graphics none \ + --console pty,target_type=serial \ + --install kernel="$KERNEL",initrd="$INITRD",kernel_args="console=ttyS0,115200n8 autoinstall ds=nocloud\;s=/cdrom/server/" \ + --noautoconsole + +echo "" +echo "============================================" +echo "PXE server VM launched!" +echo "============================================" +echo "" +echo "The autoinstall + first-boot will take ~15 minutes." +echo "" +echo "Step 1 — Monitor the server install:" +echo " virsh console $SERVER_NAME" +echo " (Ctrl+] to detach)" +echo "" +echo "Step 2 — Check when services are ready:" +echo " $0 --status" +echo "" +echo "Step 3 — Launch a PXE client to test booting:" +echo " $0 --client" +echo "" +echo "Cleanup:" +echo " $0 --destroy" +echo "" diff --git a/webapp/app.py b/webapp/app.py index e50d235..8745707 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -958,4 +958,4 @@ def inject_globals(): # --------------------------------------------------------------------------- if __name__ == "__main__": - app.run(host="0.0.0.0", port=9009, debug=False) + app.run(host="127.0.0.1", port=9010, debug=False, threaded=True) diff --git a/webapp/templates/base.html b/webapp/templates/base.html index 58d9b42..cf90eb2 100644 --- a/webapp/templates/base.html +++ b/webapp/templates/base.html @@ -55,8 +55,9 @@ color: #fff; border-bottom: 1px solid rgba(255,255,255,0.08); display: flex; + flex-direction: column; align-items: center; - gap: 0.5rem; + gap: 0.3rem; } .sidebar .brand .bi { font-size: 1.3rem; diff --git a/webapp/templates/startnet_editor.html b/webapp/templates/startnet_editor.html index 1f2a139..a90f10f 100644 --- a/webapp/templates/startnet_editor.html +++ b/webapp/templates/startnet_editor.html @@ -6,7 +6,9 @@ .cmd-editor { font-family: 'Consolas', 'Courier New', monospace; font-size: 0.9rem; - min-height: 400px; + min-height: 600px; + height: 70vh; + resize: vertical; background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #333; diff --git a/webapp/templates/unattend_editor.html b/webapp/templates/unattend_editor.html index 2dfd2c1..72c5638 100644 --- a/webapp/templates/unattend_editor.html +++ b/webapp/templates/unattend_editor.html @@ -16,7 +16,8 @@ .raw-xml-editor { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.85rem; - min-height: 500px; + min-height: 600px; + height: 70vh; tab-size: 2; white-space: pre; resize: vertical;