From f3a384fa1a6061e91282a7b9011c7eb4db58a72a Mon Sep 17 00:00:00 2001 From: cproudlock Date: Mon, 9 Feb 2026 20:01:19 -0500 Subject: [PATCH] Add Proxmox ISO builder, CSRF protection, boot-files integration - Add build-proxmox-iso.sh: remaster Ubuntu ISO with autoinstall config, offline packages, playbook, webapp, and boot files for zero-touch Proxmox VM deployment - Add boot-files/ directory for WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.) sourced from WestJeff playbook - Update build-usb.sh and test-vm.sh to bundle boot-files automatically - Add usb_root variable to playbook, fix all file copy paths to use it - Unify Apache VirtualHost config (merge default site + webapp proxy) - Add CSRF token protection to all webapp POST forms and API endpoints - Update README with Proxmox deployment instructions Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 + README.md | 45 +++- build-proxmox-iso.sh | 348 ++++++++++++++++++++++++++ build-usb.sh | 16 ++ playbook/pxe_server_setup.yml | 45 ++-- test-vm.sh | 25 +- webapp/app.py | 29 +++ webapp/static/app.js | 6 +- webapp/templates/backups.html | 2 + webapp/templates/base.html | 1 + webapp/templates/import.html | 1 + webapp/templates/reports.html | 1 + webapp/templates/startnet_editor.html | 1 + webapp/templates/unattend_editor.html | 1 + 14 files changed, 492 insertions(+), 32 deletions(-) create mode 100755 build-proxmox-iso.sh diff --git a/.gitignore b/.gitignore index 1c05a2c..4f5b5f5 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ offline-packages/ # Boot tool binaries (built by prepare-boot-tools.sh) boot-tools/ +# WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.) +boot-files/ + # Python wheels for offline install (built by download-packages.sh) pip-wheels/ diff --git a/README.md b/README.md index 57e0aee..1e66605 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ pxe-server/ ├── download-packages.sh # Downloads offline .debs + pip wheels ├── build-usb.sh # Builds the installer USB (2-partition) ├── prepare-boot-tools.sh # Extracts and patches boot tool files +├── build-proxmox-iso.sh # Builds self-contained Proxmox installer ISO ├── test-vm.sh # KVM test environment for validation ├── SETUP.md # Detailed setup guide └── setup-guide-original.txt # Original manual setup notes (reference) @@ -183,6 +184,41 @@ sudo ./test-vm.sh --destroy The test VM creates an isolated libvirt network (10.9.100.0/24) and runs the full autoinstall + Ansible provisioning. +## Proxmox Deployment + +A single ISO can be built for deploying the PXE server in a Proxmox VM: + +### Build the ISO + +```bash +# Prerequisites (on build workstation) +sudo apt install xorriso p7zip-full + +# Build the installer ISO +./build-proxmox-iso.sh /path/to/ubuntu-24.04-live-server-amd64.iso +``` + +This creates `pxe-server-proxmox.iso` containing the Ubuntu installer, autoinstall config, all offline packages, the Ansible playbook, webapp, and boot tools. + +### Deploy on Proxmox + +1. Upload `pxe-server-proxmox.iso` to Proxmox storage (Datacenter -> Storage -> ISO Images) +2. Create a new VM: + - **OS:** Linux 6.x kernel + - **BIOS:** OVMF (UEFI) or SeaBIOS + - **Memory:** 4096 MB + - **CPU:** 2+ cores + - **Disk:** 40+ GB (VirtIO SCSI) + - **Network:** Bridge connected to your isolated PXE network +3. Attach the ISO as CD-ROM and start the VM +4. Ubuntu auto-installs with zero interaction (~10-15 minutes) +5. After reboot, first-boot configures all PXE services automatically +6. Access the web interface at `http://10.9.100.1:9009` + +### Import WinPE Images + +After the server is running, import deployment images via the web interface at `http://10.9.100.1:9009/import` or by mounting a USB drive with WinPE content. + ## Samba Shares | Share | Path | Purpose | @@ -203,13 +239,10 @@ Blancco Drive Eraser is configured to automatically save XML erasure reports to Reports are viewable and downloadable from the web interface at `http://10.9.100.1:9009/reports`. -## Known Issues / TODO +## Notes -- **wimtools** must be downloaded with `download-packages.sh` before building USB (used for startnet.cmd editing) -- **Apache VirtualHost conflict**: Two VirtualHosts on port 80 (default site and pxe-webapp proxy) — should disable default or merge -- **WinPE boot files** (wimboot, BCD, boot.sdi, bootx64.efi, boot.stl, boot.wim) must be manually placed on USB or sourced from the legacy `WestJeff` playbook folder -- **CSRF protection** not yet implemented on webapp POST forms -- Test VM requires re-download of Ubuntu ISO if `--destroy` is run (fixed in latest test-vm.sh) +- Run `download-packages.sh` before building USB — it downloads all offline `.deb` packages including wimtools (needed for startnet.cmd editing) +- The webapp uses session-based CSRF tokens on all POST forms and API endpoints ## Commit History diff --git a/build-proxmox-iso.sh b/build-proxmox-iso.sh new file mode 100755 index 0000000..68e5164 --- /dev/null +++ b/build-proxmox-iso.sh @@ -0,0 +1,348 @@ +#!/bin/bash +# +# build-proxmox-iso.sh — Build a self-contained PXE server installer ISO for Proxmox +# +# Repackages the Ubuntu 24.04 Server ISO with: +# - Autoinstall configuration (zero-touch install) +# - All offline .deb packages and Python wheels +# - Ansible playbook, Flask webapp, and boot tools +# +# The resulting ISO can be uploaded to Proxmox, attached to a VM, and booted. +# Ubuntu auto-installs, then first-boot configures all PXE services automatically. +# +# Usage: +# ./build-proxmox-iso.sh /path/to/ubuntu-24.04-live-server-amd64.iso [output.iso] +# +# Prerequisites (on build workstation): +# sudo apt install xorriso p7zip-full +# +# Before building, run: +# ./download-packages.sh (downloads offline .debs + pip wheels) +# ./prepare-boot-tools.sh ... (extracts Clonezilla, Blancco, Memtest) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +AUTOINSTALL_DIR="$SCRIPT_DIR/autoinstall" +PLAYBOOK_DIR="$SCRIPT_DIR/playbook" +OFFLINE_PKG_DIR="$SCRIPT_DIR/offline-packages" +WEBAPP_DIR="$SCRIPT_DIR/webapp" +PIP_WHEELS_DIR="$SCRIPT_DIR/pip-wheels" +BOOT_TOOLS_DIR="$SCRIPT_DIR/boot-tools" + +# --- Validate arguments --- +if [ $# -lt 1 ]; then + echo "Usage: $0 /path/to/ubuntu-24.04-live-server-amd64.iso [output.iso]" + echo "" + echo " Creates a self-contained ISO for deploying the PXE server in Proxmox." + echo " The ISO auto-installs Ubuntu and configures all PXE services." + echo "" + echo "Prerequisites:" + echo " sudo apt install xorriso p7zip-full" + exit 1 +fi + +UBUNTU_ISO="$(realpath "$1")" +OUTPUT_ISO="${2:-$SCRIPT_DIR/pxe-server-proxmox.iso}" + +# --- Validate prerequisites --- +echo "============================================" +echo "PXE Server Proxmox ISO Builder" +echo "============================================" +echo "" + +MISSING_CMDS=() +for cmd in xorriso 7z; do + if ! command -v "$cmd" &>/dev/null; then + MISSING_CMDS+=("$cmd") + fi +done + +if [ ${#MISSING_CMDS[@]} -gt 0 ]; then + echo "ERROR: Missing required tools: ${MISSING_CMDS[*]}" + echo "Install with: sudo apt install xorriso p7zip-full" + exit 1 +fi + +if [ ! -f "$UBUNTU_ISO" ]; then + echo "ERROR: ISO not found at $UBUNTU_ISO" + exit 1 +fi + +# Quick sanity check: ensure it looks like an Ubuntu Server ISO +ISO_CONTENTS=$(7z l "$UBUNTU_ISO" 2>&1) || true +if ! echo "$ISO_CONTENTS" | grep -q "casper/vmlinuz"; then + echo "ERROR: Does not appear to be an Ubuntu Server ISO (missing casper/vmlinuz)" + exit 1 +fi + +if [ ! -f "$AUTOINSTALL_DIR/user-data" ]; then + echo "ERROR: user-data not found at $AUTOINSTALL_DIR/user-data" + exit 1 +fi + +if [ ! -f "$PLAYBOOK_DIR/pxe_server_setup.yml" ]; then + echo "ERROR: pxe_server_setup.yml not found at $PLAYBOOK_DIR/" + exit 1 +fi + +echo "Ubuntu ISO : $UBUNTU_ISO" +echo "Output ISO : $OUTPUT_ISO" +echo "Source Dir : $SCRIPT_DIR" +echo "" + +# --- Setup work directory with cleanup trap --- +WORK_DIR=$(mktemp -d) +cleanup() { rm -rf "$WORK_DIR"; } +trap cleanup EXIT + +EXTRACT_DIR="$WORK_DIR/iso" +BOOT_IMG_DIR="$WORK_DIR/BOOT" + +# --- Step 1: Extract Ubuntu ISO --- +echo "[1/6] Extracting Ubuntu ISO..." +7z x -o"$EXTRACT_DIR" "$UBUNTU_ISO" -y >/dev/null 2>&1 + +# 7z extracts [BOOT] directory containing EFI images needed for rebuild +# Move it out so it doesn't end up in the final ISO filesystem +if [ -d "$EXTRACT_DIR/[BOOT]" ]; then + mv "$EXTRACT_DIR/[BOOT]" "$BOOT_IMG_DIR" + echo " Extracted boot images for BIOS + UEFI" +else + echo "ERROR: [BOOT] directory not found in extracted ISO" + echo " The Ubuntu ISO may be corrupted or an unsupported version." + exit 1 +fi + +# Ensure files are writable (ISO extraction may set read-only) +chmod -R u+w "$EXTRACT_DIR" + +# --- Step 2: Generate autoinstall user-data --- +echo "[2/6] Generating autoinstall configuration..." +mkdir -p "$EXTRACT_DIR/server" +touch "$EXTRACT_DIR/server/meta-data" + +# Reuse the common sections (identity, network, storage, SSH) from existing user-data +# and replace late-commands with ISO-specific versions +sed '/^ late-commands:/,$d' "$AUTOINSTALL_DIR/user-data" > "$EXTRACT_DIR/server/user-data" + +# Append ISO-specific late-commands +cat >> "$EXTRACT_DIR/server/user-data" << 'LATE_COMMANDS' + late-commands: + # Copy project files from ISO (/cdrom/pxe-data/) to the installed system + - mkdir -p /target/opt/pxe-setup + - cp -r /cdrom/pxe-data/packages /target/opt/pxe-setup/ 2>/dev/null || true + - cp -r /cdrom/pxe-data/playbook /target/opt/pxe-setup/ 2>/dev/null || true + - cp -r /cdrom/pxe-data/webapp /target/opt/pxe-setup/ 2>/dev/null || true + - cp -r /cdrom/pxe-data/pip-wheels /target/opt/pxe-setup/ 2>/dev/null || true + - cp -r /cdrom/pxe-data/boot-tools /target/opt/pxe-setup/ 2>/dev/null || true + # Copy boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.) from pxe-data root + - sh -c 'for f in /cdrom/pxe-data/*; do [ -f "$f" ] && cp "$f" /target/opt/pxe-setup/; done' || true + + # Install deb packages in target chroot + - | + curtin in-target --target=/target -- bash -c ' + if compgen -G "/opt/pxe-setup/packages/*.deb" > /dev/null; then + dpkg -i /opt/pxe-setup/packages/*.deb 2>/dev/null || true + dpkg -i /opt/pxe-setup/packages/*.deb 2>/dev/null || true + if command -v nmcli >/dev/null; then + systemctl enable NetworkManager + fi + fi + ' + + # Create first-boot script (reads from local /opt/pxe-setup/) + - | + curtin in-target --target=/target -- bash -c ' + cat <<"EOF" > /opt/first-boot.sh + #!/bin/bash + SRC=/opt/pxe-setup + # Install all offline .deb packages + if compgen -G "$SRC/packages/*.deb" > /dev/null; then + dpkg -i $SRC/packages/*.deb 2>/dev/null || true + dpkg -i $SRC/packages/*.deb 2>/dev/null || true + fi + # Run the Ansible playbook (override USB paths to local source) + if [ -f $SRC/playbook/pxe_server_setup.yml ]; then + cd $SRC/playbook + ansible-playbook -i localhost, -c local pxe_server_setup.yml \ + -e usb_root=$SRC -e usb_mount=$SRC/playbook + fi + # Disable rc.local to prevent rerunning + sed -i "s|^/opt/first-boot.sh.*|# &|" /etc/rc.local + lvextend -r -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv || true + # Clean up large setup files to save disk space + rm -rf $SRC/packages $SRC/pip-wheels $SRC/boot-tools + rm -f $SRC/boot.wim $SRC/boot.sdi $SRC/bootx64.efi $SRC/wimboot $SRC/ipxe.efi $SRC/BCD $SRC/boot.stl + EOF + ' + - curtin in-target --target=/target -- chmod +x /opt/first-boot.sh + + # Create rc.local to run first-boot on next startup + - | + curtin in-target --target=/target -- bash -c ' + cat <<"EOF" > /etc/rc.local + #!/bin/bash + /opt/first-boot.sh > /var/log/first-boot.log 2>&1 & + exit 0 + EOF + ' + - curtin in-target --target=/target -- chmod +x /etc/rc.local + + user-data: + disable_root: false + + refresh-installer: + update: no +LATE_COMMANDS + +echo " Generated server/user-data and server/meta-data" + +# --- Step 3: Copy project files to pxe-data/ --- +echo "[3/6] Copying project files to ISO..." +PXE_DATA="$EXTRACT_DIR/pxe-data" +mkdir -p "$PXE_DATA" + +# Offline .deb packages +if [ -d "$OFFLINE_PKG_DIR" ]; then + mkdir -p "$PXE_DATA/packages" + DEB_COUNT=0 + for deb in "$OFFLINE_PKG_DIR"/*.deb; do + if [ -f "$deb" ]; then + cp "$deb" "$PXE_DATA/packages/" + DEB_COUNT=$((DEB_COUNT + 1)) + fi + done + echo " Copied $DEB_COUNT .deb packages" +else + echo " WARNING: No offline-packages/ directory. Run download-packages.sh first." +fi + +# Ansible playbook +mkdir -p "$PXE_DATA/playbook" +cp "$PLAYBOOK_DIR/"* "$PXE_DATA/playbook/" 2>/dev/null || true +echo " Copied playbook/" + +# Flask webapp +if [ -d "$WEBAPP_DIR" ]; then + mkdir -p "$PXE_DATA/webapp" + cp "$WEBAPP_DIR/app.py" "$WEBAPP_DIR/requirements.txt" "$PXE_DATA/webapp/" + cp -r "$WEBAPP_DIR/templates" "$WEBAPP_DIR/static" "$PXE_DATA/webapp/" + echo " Copied webapp/" +fi + +# Python wheels +if [ -d "$PIP_WHEELS_DIR" ]; then + cp -r "$PIP_WHEELS_DIR" "$PXE_DATA/pip-wheels" + echo " Copied pip-wheels/" +else + echo " WARNING: No pip-wheels/ found (run download-packages.sh first)" +fi + +# WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.) +BOOT_FILES_DIR="$SCRIPT_DIR/boot-files" +if [ -d "$BOOT_FILES_DIR" ]; then + BOOT_FILE_COUNT=0 + for bf in "$BOOT_FILES_DIR"/*; do + if [ -f "$bf" ]; then + cp "$bf" "$PXE_DATA/" + BOOT_FILE_COUNT=$((BOOT_FILE_COUNT + 1)) + fi + done + BOOT_FILES_SIZE=$(du -sh "$BOOT_FILES_DIR" | cut -f1) + echo " Copied $BOOT_FILE_COUNT boot files ($BOOT_FILES_SIZE) — wimboot, boot.wim, ipxe.efi, etc." +else + echo " WARNING: No boot-files/ found (copy WinPE boot files from Media Creator)" +fi + +# Boot tools (Clonezilla, Blancco, Memtest) +if [ -d "$BOOT_TOOLS_DIR" ]; then + cp -r "$BOOT_TOOLS_DIR" "$PXE_DATA/boot-tools" + TOOLS_SIZE=$(du -sh "$PXE_DATA/boot-tools" | cut -f1) + echo " Copied boot-tools/ ($TOOLS_SIZE)" +else + echo " No boot-tools/ found (run prepare-boot-tools.sh first)" +fi + +# --- Step 4: Modify GRUB for autoinstall --- +echo "[4/6] Configuring autoinstall boot..." +GRUB_CFG="$EXTRACT_DIR/boot/grub/grub.cfg" + +if [ ! -f "$GRUB_CFG" ]; then + echo "ERROR: boot/grub/grub.cfg not found in extracted ISO" + exit 1 +fi + +# Add autoinstall kernel parameter with nocloud datasource pointing to /cdrom/server/ +# The semicolon must be escaped as \; in GRUB (it's a command separator) +# Apply to both regular and HWE kernels +sed -i 's|/casper/vmlinuz\b|/casper/vmlinuz autoinstall ds=nocloud\\;s=/cdrom/server/|g' "$GRUB_CFG" +sed -i 's|/casper/hwe-vmlinuz\b|/casper/hwe-vmlinuz autoinstall ds=nocloud\\;s=/cdrom/server/|g' "$GRUB_CFG" + +# Reduce timeout for automatic boot (1 second instead of default 30) +sed -i 's/set timeout=.*/set timeout=1/' "$GRUB_CFG" + +echo " Modified GRUB: autoinstall enabled, timeout=1s" + +# --- Step 5: Rebuild ISO --- +echo "[5/6] Rebuilding ISO (this may take a few minutes)..." + +# Verify required boot images exist +EFI_IMG="$BOOT_IMG_DIR/2-Boot-NoEmul.img" +if [ ! -f "$EFI_IMG" ]; then + echo "ERROR: EFI boot image not found at $EFI_IMG" + exit 1 +fi + +if [ ! -f "$EXTRACT_DIR/boot/grub/i386-pc/eltorito.img" ]; then + echo "ERROR: BIOS boot image not found at boot/grub/i386-pc/eltorito.img" + exit 1 +fi + +xorriso -as mkisofs -r \ + -V 'PXE-SERVER' \ + -o "$OUTPUT_ISO" \ + --grub2-mbr --interval:local_fs:0s-15s:zero_mbrpt,zero_gpt:"$UBUNTU_ISO" \ + --protective-msdos-label \ + -partition_cyl_align off \ + -partition_offset 16 \ + --mbr-force-bootable \ + -append_partition 2 28732ac11ff8d211ba4b00a0c93ec93b "$EFI_IMG" \ + -appended_part_as_gpt \ + -iso_mbr_part_type a2a0d0ebe5b9334487c068b6b72699c7 \ + -c '/boot.catalog' \ + -b '/boot/grub/i386-pc/eltorito.img' \ + -no-emul-boot -boot-load-size 4 -boot-info-table --grub2-boot-info \ + -eltorito-alt-boot \ + -e '--interval:appended_partition_2:::' \ + -no-emul-boot \ + "$EXTRACT_DIR" + +# --- Step 6: Done --- +echo "[6/6] Cleaning up..." + +ISO_SIZE=$(du -sh "$OUTPUT_ISO" | cut -f1) + +echo "" +echo "============================================" +echo "Proxmox ISO build complete!" +echo "============================================" +echo "" +echo "Output: $OUTPUT_ISO ($ISO_SIZE)" +echo "" +echo "Proxmox deployment:" +echo " 1. Upload ISO to Proxmox storage (Datacenter -> Storage -> ISO Images)" +echo " 2. Create a new VM:" +echo " - BIOS: OVMF (UEFI) — or SeaBIOS (both work)" +echo " - Memory: 4096 MB" +echo " - CPU: 2+ cores" +echo " - Disk: 40+ GB (VirtIO SCSI)" +echo " - Network: Bridge connected to isolated PXE network" +echo " 3. Attach ISO as CD-ROM and start the VM" +echo " 4. Ubuntu auto-installs (~10-15 minutes, zero interaction)" +echo " 5. After reboot, first-boot configures all PXE services" +echo " 6. Access webapp at http://10.9.100.1:9009" +echo "" +echo "NOTE: The VM's network bridge must be connected to your isolated PXE" +echo " network. The server will use static IP 10.9.100.1/24." +echo "" diff --git a/build-usb.sh b/build-usb.sh index 1e9dd43..e3cfd2b 100755 --- a/build-usb.sh +++ b/build-usb.sh @@ -194,6 +194,22 @@ else echo " No pip-wheels/ found (run download-packages.sh first)" fi +# Copy WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.) +BOOT_FILES_DIR="$SCRIPT_DIR/boot-files" +if [ -d "$BOOT_FILES_DIR" ]; then + BOOT_FILE_COUNT=0 + for bf in "$BOOT_FILES_DIR"/*; do + if [ -f "$bf" ]; then + cp "$bf" "$MOUNT_POINT/" + BOOT_FILE_COUNT=$((BOOT_FILE_COUNT + 1)) + fi + done + BOOT_FILES_SIZE=$(du -sh "$BOOT_FILES_DIR" | cut -f1) + echo " Copied $BOOT_FILE_COUNT boot files ($BOOT_FILES_SIZE) — wimboot, boot.wim, ipxe.efi, etc." +else + echo " WARNING: No boot-files/ found (copy WinPE boot files from Media Creator)" +fi + # Copy boot tools (Clonezilla, Blancco, Memtest) if prepared BOOT_TOOLS_DIR="$SCRIPT_DIR/boot-tools" if [ -d "$BOOT_TOOLS_DIR" ]; then diff --git a/playbook/pxe_server_setup.yml b/playbook/pxe_server_setup.yml index 26937d9..0932bca 100644 --- a/playbook/pxe_server_setup.yml +++ b/playbook/pxe_server_setup.yml @@ -31,7 +31,8 @@ tftp_dir: "/srv/tftp" web_root: "/var/www/html" samba_share: "/srv/samba/winpeapps" - usb_mount: "/mnt/usb/playbook" # where your USB is mounted + usb_mount: "/mnt/usb/playbook" # playbook location on USB + usb_root: "/mnt/usb" # CIDATA partition root image_types: - gea-standard - gea-engineer @@ -274,7 +275,7 @@ - name: "Copy WinPE & boot files from USB (skipped if not present)" copy: - src: "{{ usb_mount }}/{{ item.src }}" + src: "{{ usb_root }}/{{ item.src }}" dest: "{{ web_root }}/win11/{{ item.dest }}" mode: '0644' loop: @@ -288,7 +289,7 @@ - name: "Copy iPXE binaries from USB (skipped if not present)" copy: - src: "{{ usb_mount }}/{{ item }}" + src: "{{ usb_root }}/{{ item }}" dest: "{{ tftp_dir }}/{{ item }}" mode: '0755' loop: @@ -297,8 +298,7 @@ - name: "Copy boot tool files from USB (Clonezilla, Blancco, Memtest)" shell: > - cp -r "{{ usb_mount }}/../boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null || - cp -r "{{ usb_mount }}/boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null || true + cp -r "{{ usb_root }}/boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null || true loop: - clonezilla - blancco @@ -306,12 +306,12 @@ - name: "Check for WinPE deployment content on USB" stat: - path: "{{ usb_mount }}/images" + path: "{{ usb_root }}/images" register: usb_images_dir - name: "Import WinPE deployment content from USB (if present)" shell: > - cp -rn "{{ usb_mount }}/images/{{ item }}/"* "{{ samba_share }}/{{ item }}/" 2>/dev/null || true + cp -rn "{{ usb_root }}/images/{{ item }}/"* "{{ samba_share }}/{{ item }}/" 2>/dev/null || true loop: "{{ image_types }}" when: usb_images_dir.stat.exists @@ -359,8 +359,7 @@ - name: "Copy webapp from USB" shell: > - cp -r "{{ usb_mount }}/../webapp/"* /opt/pxe-webapp/ 2>/dev/null || - cp -r "{{ usb_mount }}/webapp/"* /opt/pxe-webapp/ 2>/dev/null || true + cp -r "{{ usb_root }}/webapp/"* /opt/pxe-webapp/ 2>/dev/null || true args: creates: /opt/pxe-webapp/app.py @@ -370,7 +369,7 @@ shell: | # Find the pip-wheels directory on the CIDATA mount export WHEEL_DIR="" - for d in "{{ usb_mount }}/../pip-wheels" "{{ usb_mount }}/pip-wheels"; do + for d in "{{ usb_root }}/pip-wheels" "{{ usb_mount }}/pip-wheels"; do if [ -d "$d" ] && compgen -G "$d/*.whl" > /dev/null; then export WHEEL_DIR="$(cd "$d" && pwd)" break @@ -427,11 +426,17 @@ enabled: yes daemon_reload: yes - - name: "Configure Apache reverse proxy for webapp" + - name: "Configure unified Apache site (static files + webapp proxy)" copy: - dest: /etc/apache2/sites-available/pxe-webapp.conf + dest: /etc/apache2/sites-available/pxe-server.conf content: | + DocumentRoot {{ web_root }} + + Options Indexes FollowSymLinks + AllowOverride None + Require all granted + ProxyPreserveHost On ProxyPass /manage http://127.0.0.1:9009/ ProxyPassReverse /manage http://127.0.0.1:9009/ @@ -442,10 +447,20 @@ args: creates: /etc/apache2/mods-enabled/proxy.load - - name: "Enable webapp Apache site" - command: a2ensite pxe-webapp.conf + - name: "Disable default Apache site" + command: a2dissite 000-default.conf args: - creates: /etc/apache2/sites-enabled/pxe-webapp.conf + removes: /etc/apache2/sites-enabled/000-default.conf + + - name: "Enable unified PXE server site" + command: a2ensite pxe-server.conf + args: + creates: /etc/apache2/sites-enabled/pxe-server.conf + + - name: "Reload Apache after site changes" + systemd: + name: apache2 + state: reloaded - name: "Configure static IP for PXE interface" copy: diff --git a/test-vm.sh b/test-vm.sh index 6d96aea..183aa13 100755 --- a/test-vm.sh +++ b/test-vm.sh @@ -22,7 +22,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" VM_NAME="pxe-test" VM_DISK="/var/lib/libvirt/images/${VM_NAME}.qcow2" -CIDATA_ISO="/tmp/${VM_NAME}-cidata.iso" +CIDATA_ISO="${SCRIPT_DIR}/.${VM_NAME}-cidata.iso" VM_RAM=4096 VM_CPUS=2 VM_DISK_SIZE=40 # GB @@ -32,7 +32,7 @@ if [ "${1:-}" = "--destroy" ]; then echo "Destroying test environment..." virsh destroy "$VM_NAME" 2>/dev/null || true virsh undefine "$VM_NAME" 2>/dev/null || true - rm -f "$VM_DISK" + virsh vol-delete "${VM_NAME}.qcow2" --pool default 2>/dev/null || true rm -f "$CIDATA_ISO" rm -f "/tmp/${VM_NAME}-vmlinuz" "/tmp/${VM_NAME}-initrd" echo "Done." @@ -95,6 +95,14 @@ elif [ -d "$SCRIPT_DIR/offline-packages/pip-wheels" ]; then echo " Copied pip-wheels/ (from offline-packages/)" fi +# WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.) +if [ -d "$SCRIPT_DIR/boot-files" ]; then + for bf in "$SCRIPT_DIR/boot-files"/*; do + [ -f "$bf" ] && cp "$bf" "$CIDATA_DIR/" + done + echo " Copied boot-files/ (wimboot, boot.wim, ipxe.efi, etc.)" +fi + # Boot tools if [ -d "$SCRIPT_DIR/boot-tools" ]; then cp -r "$SCRIPT_DIR/boot-tools" "$CIDATA_DIR/boot-tools" @@ -110,23 +118,20 @@ rm -rf "$CIDATA_DIR" # --- Step 2: Create VM disk --- echo "" echo "[2/4] Creating VM disk (${VM_DISK_SIZE}GB)..." -if [ -f "$VM_DISK" ]; then +if virsh vol-info "$VM_NAME.qcow2" --pool default &>/dev/null; then echo " Disk already exists. Destroy first with: $0 --destroy" exit 1 fi -qemu-img create -f qcow2 "$VM_DISK" "${VM_DISK_SIZE}G" +virsh vol-create-as default "${VM_NAME}.qcow2" "${VM_DISK_SIZE}G" --format qcow2 # --- Step 3: Extract kernel/initrd from ISO --- echo "" echo "[3/4] Extracting kernel and initrd from ISO..." -ISO_MNT=$(mktemp -d) -mount -o loop,ro "$UBUNTU_ISO" "$ISO_MNT" KERNEL="/tmp/${VM_NAME}-vmlinuz" INITRD="/tmp/${VM_NAME}-initrd" -cp "$ISO_MNT/casper/vmlinuz" "$KERNEL" -cp "$ISO_MNT/casper/initrd" "$INITRD" -umount "$ISO_MNT" -rmdir "$ISO_MNT" +7z e -o/tmp -y "$UBUNTU_ISO" casper/vmlinuz casper/initrd 2>/dev/null +mv /tmp/vmlinuz "$KERNEL" +mv /tmp/initrd "$INITRD" echo " Extracted vmlinuz and initrd from casper/" # --- Step 4: Launch VM --- diff --git a/webapp/app.py b/webapp/app.py index b282f13..e50d235 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -3,6 +3,7 @@ import logging import os +import secrets import shutil import subprocess import tempfile @@ -11,12 +12,14 @@ from pathlib import Path from flask import ( Flask, + abort, flash, jsonify, redirect, render_template, request, send_file, + session, url_for, ) from lxml import etree @@ -71,6 +74,32 @@ FRIENDLY_NAMES = { "ge-shopfloor-mce": "GE Legacy Shop Floor MCE", } +# --------------------------------------------------------------------------- +# CSRF protection +# --------------------------------------------------------------------------- +def generate_csrf_token(): + """Return the CSRF token for the current session, creating one if needed.""" + if "_csrf_token" not in session: + session["_csrf_token"] = secrets.token_hex(32) + return session["_csrf_token"] + + +@app.context_processor +def inject_csrf_token(): + """Make csrf_token() available in all templates.""" + return {"csrf_token": generate_csrf_token} + + +@app.before_request +def validate_csrf(): + """Reject POST requests with a missing or invalid CSRF token.""" + if request.method != "POST": + return + token = request.form.get("_csrf_token") or request.headers.get("X-CSRF-Token") + if not token or token != generate_csrf_token(): + abort(403) + + NS = "urn:schemas-microsoft-com:unattend" WCM = "http://schemas.microsoft.com/WMIConfig/2002/State" NSMAP = {None: NS, "wcm": WCM} diff --git a/webapp/static/app.js b/webapp/static/app.js index 1c6b18b..115ab91 100644 --- a/webapp/static/app.js +++ b/webapp/static/app.js @@ -243,9 +243,13 @@ document.addEventListener('DOMContentLoaded', function () { saveRawBtn.disabled = true; saveRawBtn.innerHTML = ' Saving...'; + var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); fetch(url, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + }, body: JSON.stringify({ raw_xml: xmlContent }) }) .then(function (resp) { return resp.json(); }) diff --git a/webapp/templates/backups.html b/webapp/templates/backups.html index 2abef12..e887678 100644 --- a/webapp/templates/backups.html +++ b/webapp/templates/backups.html @@ -72,6 +72,7 @@