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 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,6 +25,9 @@ offline-packages/
|
|||||||
# Boot tool binaries (built by prepare-boot-tools.sh)
|
# Boot tool binaries (built by prepare-boot-tools.sh)
|
||||||
boot-tools/
|
boot-tools/
|
||||||
|
|
||||||
|
# WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.)
|
||||||
|
boot-files/
|
||||||
|
|
||||||
# Python wheels for offline install (built by download-packages.sh)
|
# Python wheels for offline install (built by download-packages.sh)
|
||||||
pip-wheels/
|
pip-wheels/
|
||||||
|
|
||||||
|
|||||||
45
README.md
45
README.md
@@ -157,6 +157,7 @@ pxe-server/
|
|||||||
├── download-packages.sh # Downloads offline .debs + pip wheels
|
├── download-packages.sh # Downloads offline .debs + pip wheels
|
||||||
├── build-usb.sh # Builds the installer USB (2-partition)
|
├── build-usb.sh # Builds the installer USB (2-partition)
|
||||||
├── prepare-boot-tools.sh # Extracts and patches boot tool files
|
├── 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
|
├── test-vm.sh # KVM test environment for validation
|
||||||
├── SETUP.md # Detailed setup guide
|
├── SETUP.md # Detailed setup guide
|
||||||
└── setup-guide-original.txt # Original manual setup notes (reference)
|
└── 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.
|
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
|
## Samba Shares
|
||||||
|
|
||||||
| Share | Path | Purpose |
|
| 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`.
|
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)
|
- Run `download-packages.sh` before building USB — it downloads all offline `.deb` packages including wimtools (needed for startnet.cmd editing)
|
||||||
- **Apache VirtualHost conflict**: Two VirtualHosts on port 80 (default site and pxe-webapp proxy) — should disable default or merge
|
- The webapp uses session-based CSRF tokens on all POST forms and API endpoints
|
||||||
- **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)
|
|
||||||
|
|
||||||
## Commit History
|
## Commit History
|
||||||
|
|
||||||
|
|||||||
348
build-proxmox-iso.sh
Executable file
348
build-proxmox-iso.sh
Executable file
@@ -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 ""
|
||||||
16
build-usb.sh
16
build-usb.sh
@@ -194,6 +194,22 @@ else
|
|||||||
echo " No pip-wheels/ found (run download-packages.sh first)"
|
echo " No pip-wheels/ found (run download-packages.sh first)"
|
||||||
fi
|
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
|
# Copy boot tools (Clonezilla, Blancco, Memtest) if prepared
|
||||||
BOOT_TOOLS_DIR="$SCRIPT_DIR/boot-tools"
|
BOOT_TOOLS_DIR="$SCRIPT_DIR/boot-tools"
|
||||||
if [ -d "$BOOT_TOOLS_DIR" ]; then
|
if [ -d "$BOOT_TOOLS_DIR" ]; then
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
tftp_dir: "/srv/tftp"
|
tftp_dir: "/srv/tftp"
|
||||||
web_root: "/var/www/html"
|
web_root: "/var/www/html"
|
||||||
samba_share: "/srv/samba/winpeapps"
|
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:
|
image_types:
|
||||||
- gea-standard
|
- gea-standard
|
||||||
- gea-engineer
|
- gea-engineer
|
||||||
@@ -274,7 +275,7 @@
|
|||||||
|
|
||||||
- name: "Copy WinPE & boot files from USB (skipped if not present)"
|
- name: "Copy WinPE & boot files from USB (skipped if not present)"
|
||||||
copy:
|
copy:
|
||||||
src: "{{ usb_mount }}/{{ item.src }}"
|
src: "{{ usb_root }}/{{ item.src }}"
|
||||||
dest: "{{ web_root }}/win11/{{ item.dest }}"
|
dest: "{{ web_root }}/win11/{{ item.dest }}"
|
||||||
mode: '0644'
|
mode: '0644'
|
||||||
loop:
|
loop:
|
||||||
@@ -288,7 +289,7 @@
|
|||||||
|
|
||||||
- name: "Copy iPXE binaries from USB (skipped if not present)"
|
- name: "Copy iPXE binaries from USB (skipped if not present)"
|
||||||
copy:
|
copy:
|
||||||
src: "{{ usb_mount }}/{{ item }}"
|
src: "{{ usb_root }}/{{ item }}"
|
||||||
dest: "{{ tftp_dir }}/{{ item }}"
|
dest: "{{ tftp_dir }}/{{ item }}"
|
||||||
mode: '0755'
|
mode: '0755'
|
||||||
loop:
|
loop:
|
||||||
@@ -297,8 +298,7 @@
|
|||||||
|
|
||||||
- name: "Copy boot tool files from USB (Clonezilla, Blancco, Memtest)"
|
- name: "Copy boot tool files from USB (Clonezilla, Blancco, Memtest)"
|
||||||
shell: >
|
shell: >
|
||||||
cp -r "{{ usb_mount }}/../boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null ||
|
cp -r "{{ usb_root }}/boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null || true
|
||||||
cp -r "{{ usb_mount }}/boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null || true
|
|
||||||
loop:
|
loop:
|
||||||
- clonezilla
|
- clonezilla
|
||||||
- blancco
|
- blancco
|
||||||
@@ -306,12 +306,12 @@
|
|||||||
|
|
||||||
- name: "Check for WinPE deployment content on USB"
|
- name: "Check for WinPE deployment content on USB"
|
||||||
stat:
|
stat:
|
||||||
path: "{{ usb_mount }}/images"
|
path: "{{ usb_root }}/images"
|
||||||
register: usb_images_dir
|
register: usb_images_dir
|
||||||
|
|
||||||
- name: "Import WinPE deployment content from USB (if present)"
|
- name: "Import WinPE deployment content from USB (if present)"
|
||||||
shell: >
|
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 }}"
|
loop: "{{ image_types }}"
|
||||||
when: usb_images_dir.stat.exists
|
when: usb_images_dir.stat.exists
|
||||||
|
|
||||||
@@ -359,8 +359,7 @@
|
|||||||
|
|
||||||
- name: "Copy webapp from USB"
|
- name: "Copy webapp from USB"
|
||||||
shell: >
|
shell: >
|
||||||
cp -r "{{ usb_mount }}/../webapp/"* /opt/pxe-webapp/ 2>/dev/null ||
|
cp -r "{{ usb_root }}/webapp/"* /opt/pxe-webapp/ 2>/dev/null || true
|
||||||
cp -r "{{ usb_mount }}/webapp/"* /opt/pxe-webapp/ 2>/dev/null || true
|
|
||||||
args:
|
args:
|
||||||
creates: /opt/pxe-webapp/app.py
|
creates: /opt/pxe-webapp/app.py
|
||||||
|
|
||||||
@@ -370,7 +369,7 @@
|
|||||||
shell: |
|
shell: |
|
||||||
# Find the pip-wheels directory on the CIDATA mount
|
# Find the pip-wheels directory on the CIDATA mount
|
||||||
export WHEEL_DIR=""
|
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
|
if [ -d "$d" ] && compgen -G "$d/*.whl" > /dev/null; then
|
||||||
export WHEEL_DIR="$(cd "$d" && pwd)"
|
export WHEEL_DIR="$(cd "$d" && pwd)"
|
||||||
break
|
break
|
||||||
@@ -427,11 +426,17 @@
|
|||||||
enabled: yes
|
enabled: yes
|
||||||
daemon_reload: yes
|
daemon_reload: yes
|
||||||
|
|
||||||
- name: "Configure Apache reverse proxy for webapp"
|
- name: "Configure unified Apache site (static files + webapp proxy)"
|
||||||
copy:
|
copy:
|
||||||
dest: /etc/apache2/sites-available/pxe-webapp.conf
|
dest: /etc/apache2/sites-available/pxe-server.conf
|
||||||
content: |
|
content: |
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
|
DocumentRoot {{ web_root }}
|
||||||
|
<Directory "{{ web_root }}">
|
||||||
|
Options Indexes FollowSymLinks
|
||||||
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
ProxyPreserveHost On
|
ProxyPreserveHost On
|
||||||
ProxyPass /manage http://127.0.0.1:9009/
|
ProxyPass /manage http://127.0.0.1:9009/
|
||||||
ProxyPassReverse /manage http://127.0.0.1:9009/
|
ProxyPassReverse /manage http://127.0.0.1:9009/
|
||||||
@@ -442,10 +447,20 @@
|
|||||||
args:
|
args:
|
||||||
creates: /etc/apache2/mods-enabled/proxy.load
|
creates: /etc/apache2/mods-enabled/proxy.load
|
||||||
|
|
||||||
- name: "Enable webapp Apache site"
|
- name: "Disable default Apache site"
|
||||||
command: a2ensite pxe-webapp.conf
|
command: a2dissite 000-default.conf
|
||||||
args:
|
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"
|
- name: "Configure static IP for PXE interface"
|
||||||
copy:
|
copy:
|
||||||
|
|||||||
25
test-vm.sh
25
test-vm.sh
@@ -22,7 +22,7 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
VM_NAME="pxe-test"
|
VM_NAME="pxe-test"
|
||||||
VM_DISK="/var/lib/libvirt/images/${VM_NAME}.qcow2"
|
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_RAM=4096
|
||||||
VM_CPUS=2
|
VM_CPUS=2
|
||||||
VM_DISK_SIZE=40 # GB
|
VM_DISK_SIZE=40 # GB
|
||||||
@@ -32,7 +32,7 @@ if [ "${1:-}" = "--destroy" ]; then
|
|||||||
echo "Destroying test environment..."
|
echo "Destroying test environment..."
|
||||||
virsh destroy "$VM_NAME" 2>/dev/null || true
|
virsh destroy "$VM_NAME" 2>/dev/null || true
|
||||||
virsh undefine "$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 "$CIDATA_ISO"
|
||||||
rm -f "/tmp/${VM_NAME}-vmlinuz" "/tmp/${VM_NAME}-initrd"
|
rm -f "/tmp/${VM_NAME}-vmlinuz" "/tmp/${VM_NAME}-initrd"
|
||||||
echo "Done."
|
echo "Done."
|
||||||
@@ -95,6 +95,14 @@ elif [ -d "$SCRIPT_DIR/offline-packages/pip-wheels" ]; then
|
|||||||
echo " Copied pip-wheels/ (from offline-packages/)"
|
echo " Copied pip-wheels/ (from offline-packages/)"
|
||||||
fi
|
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
|
# Boot tools
|
||||||
if [ -d "$SCRIPT_DIR/boot-tools" ]; then
|
if [ -d "$SCRIPT_DIR/boot-tools" ]; then
|
||||||
cp -r "$SCRIPT_DIR/boot-tools" "$CIDATA_DIR/boot-tools"
|
cp -r "$SCRIPT_DIR/boot-tools" "$CIDATA_DIR/boot-tools"
|
||||||
@@ -110,23 +118,20 @@ rm -rf "$CIDATA_DIR"
|
|||||||
# --- Step 2: Create VM disk ---
|
# --- Step 2: Create VM disk ---
|
||||||
echo ""
|
echo ""
|
||||||
echo "[2/4] Creating VM disk (${VM_DISK_SIZE}GB)..."
|
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"
|
echo " Disk already exists. Destroy first with: $0 --destroy"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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 ---
|
# --- Step 3: Extract kernel/initrd from ISO ---
|
||||||
echo ""
|
echo ""
|
||||||
echo "[3/4] Extracting kernel and initrd from ISO..."
|
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"
|
KERNEL="/tmp/${VM_NAME}-vmlinuz"
|
||||||
INITRD="/tmp/${VM_NAME}-initrd"
|
INITRD="/tmp/${VM_NAME}-initrd"
|
||||||
cp "$ISO_MNT/casper/vmlinuz" "$KERNEL"
|
7z e -o/tmp -y "$UBUNTU_ISO" casper/vmlinuz casper/initrd 2>/dev/null
|
||||||
cp "$ISO_MNT/casper/initrd" "$INITRD"
|
mv /tmp/vmlinuz "$KERNEL"
|
||||||
umount "$ISO_MNT"
|
mv /tmp/initrd "$INITRD"
|
||||||
rmdir "$ISO_MNT"
|
|
||||||
echo " Extracted vmlinuz and initrd from casper/"
|
echo " Extracted vmlinuz and initrd from casper/"
|
||||||
|
|
||||||
# --- Step 4: Launch VM ---
|
# --- Step 4: Launch VM ---
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import secrets
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -11,12 +12,14 @@ from pathlib import Path
|
|||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask,
|
Flask,
|
||||||
|
abort,
|
||||||
flash,
|
flash,
|
||||||
jsonify,
|
jsonify,
|
||||||
redirect,
|
redirect,
|
||||||
render_template,
|
render_template,
|
||||||
request,
|
request,
|
||||||
send_file,
|
send_file,
|
||||||
|
session,
|
||||||
url_for,
|
url_for,
|
||||||
)
|
)
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@@ -71,6 +74,32 @@ FRIENDLY_NAMES = {
|
|||||||
"ge-shopfloor-mce": "GE Legacy Shop Floor MCE",
|
"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"
|
NS = "urn:schemas-microsoft-com:unattend"
|
||||||
WCM = "http://schemas.microsoft.com/WMIConfig/2002/State"
|
WCM = "http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||||
NSMAP = {None: NS, "wcm": WCM}
|
NSMAP = {None: NS, "wcm": WCM}
|
||||||
|
|||||||
@@ -243,9 +243,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
saveRawBtn.disabled = true;
|
saveRawBtn.disabled = true;
|
||||||
saveRawBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Saving...';
|
saveRawBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Saving...';
|
||||||
|
|
||||||
|
var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrfToken
|
||||||
|
},
|
||||||
body: JSON.stringify({ raw_xml: xmlContent })
|
body: JSON.stringify({ raw_xml: xmlContent })
|
||||||
})
|
})
|
||||||
.then(function (resp) { return resp.json(); })
|
.then(function (resp) { return resp.json(); })
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<form action="{{ url_for('clonezilla_upload') }}" method="post" enctype="multipart/form-data">
|
<form action="{{ url_for('clonezilla_upload') }}" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title"><i class="bi bi-upload me-2"></i>Upload Backup</h5>
|
<h5 class="modal-title"><i class="bi bi-upload me-2"></i>Upload Backup</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
@@ -100,6 +101,7 @@
|
|||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<form id="deleteForm" method="post">
|
<form id="deleteForm" method="post">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
|
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
<title>{% block title %}PXE Server Manager{% endblock %}</title>
|
<title>{% block title %}PXE Server Manager{% endblock %}</title>
|
||||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
|
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
|
||||||
<link href="{{ url_for('static', filename='bootstrap.min.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='bootstrap.min.css') }}" rel="stylesheet">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if usb_mounts %}
|
{% if usb_mounts %}
|
||||||
<form method="POST" id="importForm">
|
<form method="POST" id="importForm">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="source" class="form-label fw-semibold">Source (USB Mount Point)</label>
|
<label for="source" class="form-label fw-semibold">Source (USB Mount Point)</label>
|
||||||
<select class="form-select" name="source" id="source" required>
|
<select class="form-select" name="source" id="source" required>
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<form id="deleteForm" method="post">
|
<form id="deleteForm" method="post">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
|
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<form action="{{ url_for('startnet_save') }}" method="post" id="startnetForm">
|
<form action="{{ url_for('startnet_save') }}" method="post" id="startnetForm">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<textarea name="content" class="form-control cmd-editor" id="cmdEditor"
|
<textarea name="content" class="form-control cmd-editor" id="cmdEditor"
|
||||||
spellcheck="false">{{ content }}</textarea>
|
spellcheck="false">{{ content }}</textarea>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<form method="POST" id="unattendForm">
|
<form method="POST" id="unattendForm">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
|
||||||
<!-- ==================== FORM VIEW ==================== -->
|
<!-- ==================== FORM VIEW ==================== -->
|
||||||
|
|||||||
Reference in New Issue
Block a user