diff --git a/README.md b/README.md
new file mode 100644
index 0000000..57e0aee
--- /dev/null
+++ b/README.md
@@ -0,0 +1,226 @@
+# GE Aerospace PXE Boot Server
+
+Automated, air-gapped PXE boot server for deploying GE Aerospace Windows images. Built on Ubuntu 24.04 Server with zero-touch provisioning via autoinstall and Ansible.
+
+## Overview
+
+This project provides a complete, repeatable build process for a PXE boot server that serves Windows PE images to client machines on an isolated network. Everything runs offline after initial setup — no internet required on the target server.
+
+### Boot Chain
+
+```
+Client PXE boot (UEFI Secure Boot)
+ -> iPXE (TFTP, Broadcom-signed for Secure Boot)
+ -> iPXE boot menu (HTTP, port 4433)
+ -> User selects boot option:
+ ├── Windows PE -> wimboot -> boot.wim -> startnet.cmd -> Samba share -> Image deployment
+ ├── Clonezilla -> vmlinuz/initrd -> Disk cloning/imaging
+ ├── Blancco -> vmlinuz/initrd -> NIST 800-88 drive erasure
+ └── Memtest86+ -> Memory diagnostics
+```
+
+### Services
+
+| Service | Port | Purpose |
+|-------------|-----------|------------------------------------------|
+| dnsmasq | 67/udp | DHCP (10.9.100.10-100, 12h lease) |
+| dnsmasq | 69/udp | TFTP (serves ipxe.efi) |
+| Apache | 80/tcp | HTTP (wimboot, WinPE boot files, proxy) |
+| Apache | 4433/tcp | iPXE boot script (GetPxeScript.aspx) |
+| Samba | 445/tcp | Deployment content + Clonezilla + Blancco|
+| Flask Webapp| 9009/tcp | Web management interface |
+
+### Network
+
+- **PXE server IP:** `10.9.100.1/24`
+- **DHCP range:** `10.9.100.10` - `10.9.100.100`
+- **Firewall:** UFW deny-by-default, only service ports open
+
+## Quick Start
+
+### Prerequisites
+
+**On your workstation (internet-connected):**
+- Ubuntu 24.04 (or Linux Mint / similar) for downloading packages
+- Ubuntu Server 24.04 ISO
+- GE Aerospace Media Creator LITE (for WinPE images)
+- USB drive >= 8 GB (32+ GB if bundling WinPE images)
+
+**GE Access Packages (MyAccess portal):**
+- EPM Rufus Exception Request
+- EPM DT Functions
+- DLP - Encrypted Removable (USB) Long Term Access
+
+### Step 1: Download Offline Packages
+
+```bash
+./download-packages.sh
+```
+
+Downloads all .deb packages and Python wheels for offline installation (~140 MB of debs, ~20 MB of wheels).
+
+### Step 2: Prepare Boot Tools (optional)
+
+```bash
+./prepare-boot-tools.sh /path/to/blancco.iso /path/to/clonezilla.zip /path/to/memtest.bin
+```
+
+Extracts and configures boot tool files (Blancco, Clonezilla, Memtest86+). Automatically patches Blancco's config.img to auto-save erasure reports to the PXE server's Samba share.
+
+### Step 3: Build the USB
+
+```bash
+sudo ./build-usb.sh /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso
+```
+
+Creates a bootable USB with two partitions:
+- **Partition 1:** Ubuntu Server installer
+- **Partition 2:** CIDATA (autoinstall config, offline .debs, pip wheels, Ansible playbook, webapp, boot tools)
+
+### Step 4: Install on Target Server
+
+1. Insert USB into the target machine
+2. Press F12 and boot from USB
+3. Ubuntu auto-installs with no interaction
+4. After reboot, the first-boot script:
+ - Installs all offline .deb packages
+ - Runs the Ansible playbook (configures dnsmasq, Apache, Samba, UFW, webapp)
+ - Configures static IP `10.9.100.1/24`
+5. Move the server's wired NIC to the isolated PXE switch
+
+### Step 5: Access the Web Interface
+
+Open `http://10.9.100.1:9009` from any machine on the isolated network.
+
+## Web Management Interface
+
+The Flask webapp (port 9009) provides a browser-based management UI:
+
+- **Dashboard** — Service status overview, disk usage, connected DHCP clients
+- **Image Import** — Import WinPE deployment images from USB drives
+- **Unattend Editor** — Edit Windows unattend.xml files per image type (XML syntax highlighting)
+- **startnet.cmd Editor** — Modify the startnet.cmd inside boot.wim without Windows (uses wimtools)
+- **Clonezilla Backups** — Upload, download, and manage disk backup images
+- **Blancco Reports** — View, download, and manage drive erasure reports (auto-collected via Samba)
+- **Audit Log** — Activity history for all write operations (imports, edits, deletes)
+
+### Image Types Supported
+
+| Image Type | Domain | Description |
+|----------------------|-----------------|-------------------------|
+| gea-standard | geaerospace.com | Standard desktop |
+| gea-engineer | geaerospace.com | Engineering desktop |
+| gea-shopfloor | geaerospace.com | Shop floor kiosk |
+| ge-standard | ge.com | Standard desktop |
+| ge-engineer | ge.com | Engineering desktop |
+| ge-shopfloor-lockdown| ge.com | Shop floor (locked) |
+| ge-shopfloor-mce | ge.com | Shop floor (MCE) |
+
+## Project Structure
+
+```
+pxe-server/
+├── autoinstall/
+│ ├── user-data # Cloud-init autoinstall config + first-boot script
+│ └── meta-data # Cloud-init metadata (required, empty)
+├── playbook/
+│ ├── pxe_server_setup.yml # Ansible playbook: all server configuration
+│ └── inventory.ini # Ansible inventory
+├── webapp/
+│ ├── app.py # Flask application (~900 lines)
+│ ├── requirements.txt # Python deps (flask, lxml)
+│ ├── static/
+│ │ ├── ge-aerospace-logo.svg # GE Aerospace branding
+│ │ ├── favicon.ico # Browser favicon
+│ │ ├── app.js # Frontend JavaScript
+│ │ ├── bootstrap.min.css # Bootstrap 5 (bundled offline)
+│ │ ├── bootstrap.bundle.min.js
+│ │ ├── bootstrap-icons.min.css
+│ │ └── fonts/ # Icon fonts (woff/woff2)
+│ └── templates/
+│ ├── base.html # Layout with GE branding and sidebar nav
+│ ├── dashboard.html # Service status and overview
+│ ├── import.html # USB image import wizard
+│ ├── unattend_editor.html # XML editor for unattend files
+│ ├── startnet_editor.html # startnet.cmd WIM editor
+│ ├── backups.html # Clonezilla backup management
+│ ├── reports.html # Blancco erasure reports
+│ └── audit.html # Activity audit log
+├── unattend/
+│ └── FlatUnattendW10.xml # Windows unattend.xml template
+├── boot-tools/ # Extracted boot tool files (gitignored)
+│ ├── blancco/ # Blancco Drive Eraser (Arch Linux-based)
+│ ├── clonezilla/ # Clonezilla Live
+│ └── memtest/ # Memtest86+
+├── offline-packages/ # .deb files (gitignored, built by download-packages.sh)
+├── pip-wheels/ # Python wheels (gitignored, built by download-packages.sh)
+├── 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
+├── test-vm.sh # KVM test environment for validation
+├── SETUP.md # Detailed setup guide
+└── setup-guide-original.txt # Original manual setup notes (reference)
+```
+
+## Testing with KVM
+
+A test VM script is included for validating the full provisioning pipeline without dedicated hardware:
+
+```bash
+# Download Ubuntu Server ISO
+wget -O ~/Downloads/ubuntu-24.04.3-live-server-amd64.iso \
+ https://releases.ubuntu.com/noble/ubuntu-24.04.3-live-server-amd64.iso
+
+# Launch test VM (requires libvirt/KVM)
+sudo ./test-vm.sh ~/Downloads/ubuntu-24.04.3-live-server-amd64.iso
+
+# Watch install progress
+sudo virsh console pxe-test
+
+# Clean up when done
+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.
+
+## Samba Shares
+
+| Share | Path | Purpose |
+|-----------------|---------------------------|--------------------------------|
+| winpeapps | /srv/samba/winpeapps | WinPE deployment images |
+| clonezilla | /srv/samba/clonezilla | Clonezilla disk backup images |
+| blancco-reports | /srv/samba/blancco-reports| Blancco erasure reports (auto) |
+
+All shares use guest access (no authentication) for ease of use on the isolated network.
+
+## Blancco Drive Erasure
+
+Blancco Drive Eraser is configured to automatically save XML erasure reports to the PXE server's Samba share (`blancco-reports`). The `prepare-boot-tools.sh` script patches Blancco's `config.img` to set:
+- Network share hostname: `10.9.100.1`
+- Share path: `blancco-reports`
+- Auto-backup: enabled
+- Erasure standard: NIST 800-88 Purge
+
+Reports are viewable and downloadable from the web interface at `http://10.9.100.1:9009/reports`.
+
+## Known Issues / TODO
+
+- **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)
+
+## Commit History
+
+| Commit | Description |
+|---------|--------------------------------------------------------------------|
+| 5791bd1 | Initial project setup: automated PXE server provisioning |
+| cee4ecd | Add web management UI, offline packages, WinPE consolidation |
+| f614596 | Fix unattend.xml path to match actual image structure |
+| e7313c2 | Add multi-boot PXE menu, Clonezilla backups, GE Aerospace branding|
+| 89b5834 | Add wimtools and startnet.cmd editor for boot.wim modification |
+| 05dbb7e | Add Blancco erasure reports Samba share and webapp viewer |
+| ef75839 | Auto-patch Blancco config.img for network report storage |
+| 92c9b0f | Fix review findings: offline assets, security, audit logging |
+| 725c8f4 | Change webapp to port 9009, add test VM script |
diff --git a/SETUP.md b/SETUP.md
index 0e2c69b..2bdf53b 100644
--- a/SETUP.md
+++ b/SETUP.md
@@ -7,21 +7,23 @@ Automated build process for deploying an Ubuntu-based PXE boot server that hosts
```
Client PXE boot
-> Broadcom signed iPXE (Secure Boot)
- -> wimboot (HTTP from Apache)
- -> WinPE (boot.wim)
- -> startnet.cmd maps Samba shares
- -> GE Aerospace image deployment
+ -> iPXE boot menu (HTTP, port 4433)
+ ├── Windows PE -> wimboot -> boot.wim -> startnet.cmd -> Samba -> Image deployment
+ ├── Clonezilla -> vmlinuz/initrd -> Disk cloning/imaging
+ ├── Blancco -> vmlinuz/initrd -> NIST 800-88 drive erasure (auto-reports)
+ └── Memtest86+ -> Memory diagnostics
```
### Services on the PXE Server
-| Service | Port | Purpose |
-|----------|----------|--------------------------------------|
-| dnsmasq | 67/udp | DHCP (10.9.100.10–100) |
-| dnsmasq | 69/udp | TFTP (serves ipxe.efi) |
-| Apache | 80/tcp | HTTP (wimboot, WinPE boot files) |
-| Apache | 4433/tcp | iPXE boot script (GetPxeScript.aspx) |
-| Samba | 445/tcp | Deployment content shares |
+| Service | Port | Purpose |
+|-------------|-----------|------------------------------------------|
+| dnsmasq | 67/udp | DHCP (10.9.100.10-100) |
+| dnsmasq | 69/udp | TFTP (serves ipxe.efi) |
+| Apache | 80/tcp | HTTP (wimboot, WinPE boot files, proxy) |
+| Apache | 4433/tcp | iPXE boot script (GetPxeScript.aspx) |
+| Samba | 445/tcp | Deployment content + backup + reports |
+| Flask Webapp| 9009/tcp | Web management interface |
## Prerequisites
@@ -31,7 +33,7 @@ Client PXE boot
### Software (on your workstation)
- Ubuntu Server 24.04 ISO — https://ubuntu.com/download/server
-- Docker (for downloading offline packages)
+- Linux workstation (Ubuntu/Mint) for running download and build scripts
- GE Aerospace Media Creator LITE (for caching WinPE images)
### GE Access Packages
@@ -41,15 +43,30 @@ Client PXE boot
## Setup Process
-### Step 1: Download Offline Packages (one-time, requires internet + Docker)
+### Step 1: Download Offline Packages (one-time, requires internet)
```bash
./download-packages.sh
```
-This runs an Ubuntu 24.04 Docker container to download all .deb packages (ansible, dnsmasq, apache2, samba, etc.) into `offline-packages/`. ~102 MB total.
+Downloads all .deb packages (ansible, dnsmasq, apache2, samba, wimtools, etc.) into `offline-packages/` and Python wheels (flask, lxml) into `pip-wheels/`. Approximately 252 packages (~140 MB) + 8 Python wheels.
-### Step 2: Build the USB
+**Packages included:**
+- **Server:** dnsmasq, apache2, samba, ufw, cron
+- **Automation:** ansible
+- **Tools:** wimtools, unzip, p7zip-full
+- **Python:** python3-pip, python3-venv
+- **Network:** network-manager, wpasupplicant, wireless-tools, linux-firmware
+
+### Step 2: Prepare Boot Tools (optional)
+
+```bash
+./prepare-boot-tools.sh /path/to/blancco.iso /path/to/clonezilla.zip /path/to/memtest.bin
+```
+
+Extracts boot files for Blancco, Clonezilla, and Memtest86+ into the `boot-tools/` directory. Automatically patches Blancco's `config.img` to auto-save erasure reports to the PXE server's Samba share.
+
+### Step 3: Build the USB
```bash
# Basic — server only (import WinPE images later)
@@ -60,69 +77,121 @@ sudo ./build-usb.sh /dev/sdX /path/to/ubuntu-24.04.iso /path/to/winpe-images
```
This creates a bootable USB with:
-- Partition 1: Ubuntu Server installer
-- Partition 2: CIDATA (autoinstall config, offline .debs, Ansible playbook, optional WinPE images)
+- **Partition 1:** Ubuntu Server installer
+- **Partition 2:** CIDATA (autoinstall config, offline .debs, pip wheels, Ansible playbook, webapp, boot tools)
-### Step 3: Boot the Target Machine
+### Step 4: Boot the Target Machine
1. Insert the USB into the target machine
2. Press F12 (or vendor boot key) and select the USB
3. Ubuntu auto-installs — no interaction needed
4. After reboot, the first-boot script installs all .deb packages and runs the Ansible playbook
5. PXE services (dnsmasq, Apache, Samba) are configured automatically
+6. Flask webapp starts on port 9009
-### Step 4: Connect to Isolated Network
+### Step 5: Connect to Isolated Network
Move the server's wired NIC to the isolated switch for PXE clients.
-### Step 5: Import WinPE Content (if not bundled in Step 2)
+### Step 6: Import WinPE Content (if not bundled in Step 3)
-Insert the Media Creator LITE USB and copy content to the Samba share:
+**Option A:** Use the web interface at `http://10.9.100.1:9009` to import from USB.
+**Option B:** Manual copy:
```bash
sudo mkdir -p /mnt/usb2
sudo mount /dev/sdb2 /mnt/usb2
-sudo cp -r /mnt/usb2/. /srv/samba/winpeapps/standard
+sudo cp -r /mnt/usb2/. /srv/samba/winpeapps/gea-standard
sudo umount /mnt/usb2
```
+## Web Management Interface
+
+Access at `http://10.9.100.1:9009` from any machine on the isolated network.
+
+| Page | URL Path | Purpose |
+|-------------------|-------------|-----------------------------------------------|
+| Dashboard | / | Service status, disk usage, DHCP clients |
+| Image Import | /import | Import WinPE images from USB drives |
+| Unattend Editor | /unattend | Edit Windows unattend.xml per image type |
+| startnet.cmd | /startnet | Edit startnet.cmd inside boot.wim (wimtools) |
+| Clonezilla Backups| /backups | Upload/download/manage disk backup images |
+| Blancco Reports | /reports | View/download drive erasure reports |
+| Audit Log | /audit | Activity history for all write operations |
+
## Verification
1. Connect a test workstation to the isolated switch
2. Set Network Boot (PXE) as first boot in BIOS/UEFI
3. Boot — the client should pull an IP from 10.9.100.x
4. iPXE loads, fetches the boot script from port 4433
-5. WinPE boots via wimboot + boot.wim over HTTP
-6. WinPE maps Samba shares and begins image deployment
+5. Select an option from the boot menu:
+ - **Windows PE**: Boots via wimboot + boot.wim, maps Samba shares, begins deployment
+ - **Clonezilla**: Boots Clonezilla Live for disk imaging
+ - **Blancco**: Boots Drive Eraser, auto-saves reports to server
+ - **Memtest86+**: Runs memory diagnostics
+
+## Testing with KVM
+
+```bash
+# Download Ubuntu ISO
+wget -O ~/Downloads/ubuntu-24.04.3-live-server-amd64.iso \
+ https://releases.ubuntu.com/noble/ubuntu-24.04.3-live-server-amd64.iso
+
+# Launch test VM
+sudo ./test-vm.sh ~/Downloads/ubuntu-24.04.3-live-server-amd64.iso
+
+# Watch progress (Ctrl+] to detach)
+sudo virsh console pxe-test
+
+# After install: ssh pxe@10.9.100.1 / http://10.9.100.1:9009
+
+# Clean up
+sudo ./test-vm.sh --destroy
+```
## Project Structure
```
pxe-server/
├── autoinstall/
-│ ├── user-data # Cloud-init autoinstall (Ubuntu config, first-boot script)
-│ └── meta-data # Cloud-init metadata (empty, required)
+│ ├── user-data # Cloud-init autoinstall + first-boot script
+│ └── meta-data # Cloud-init metadata (required, empty)
├── playbook/
-│ ├── pxe_server_setup.yml # Ansible: dnsmasq, Apache, Samba, iPXE, firewall, netplan
-│ └── inventory.ini # Ansible inventory
+│ ├── pxe_server_setup.yml # Ansible: dnsmasq, Apache, Samba, iPXE, UFW, webapp
+│ └── inventory.ini # Ansible inventory
+├── webapp/
+│ ├── app.py # Flask application
+│ ├── requirements.txt # Python deps (flask, lxml)
+│ ├── static/ # CSS, JS, fonts, logo (all bundled offline)
+│ └── templates/ # Jinja2 HTML templates
├── unattend/
-│ └── FlatUnattendW10.xml # Windows unattend.xml sample
-├── offline-packages/ # .deb files (gitignored, built by download-packages.sh)
-├── build-usb.sh # Builds the installer USB
-├── download-packages.sh # Downloads offline .debs via Docker
-└── setup-guide-original.txt # Original manual setup doc (reference)
+│ └── FlatUnattendW10.xml # Windows unattend.xml template
+├── boot-tools/ # Extracted boot files (gitignored, built by prepare-boot-tools.sh)
+│ ├── blancco/ # Blancco Drive Eraser
+│ ├── clonezilla/ # Clonezilla Live
+│ └── memtest/ # Memtest86+
+├── offline-packages/ # .deb files (gitignored, built by download-packages.sh)
+├── pip-wheels/ # Python wheels (gitignored, built by download-packages.sh)
+├── download-packages.sh # Downloads all offline packages
+├── build-usb.sh # Builds the 2-partition installer USB
+├── prepare-boot-tools.sh # Extracts/patches boot tools from ISOs
+├── test-vm.sh # KVM test environment
+├── README.md # Project overview
+└── setup-guide-original.txt # Original manual setup notes (reference)
```
## Image Types
-| Image Type | Domain | Description |
-|---------------|-----------------|---------------------|
-| gea-standard | geaerospace.com | Standard desktop |
-| gea-engineer | geaerospace.com | Engineering desktop |
-| gea-shopfloor | geaerospace.com | Shop floor kiosk |
-| ge-standard | ge.com | Standard desktop |
-| ge-engineer | ge.com | Engineering desktop |
-| ge-shopfloor | ge.com | Shop floor kiosk |
+| Image Type | Domain | Description |
+|----------------------|-----------------|-------------------------|
+| gea-standard | geaerospace.com | Standard desktop |
+| gea-engineer | geaerospace.com | Engineering desktop |
+| gea-shopfloor | geaerospace.com | Shop floor kiosk |
+| ge-standard | ge.com | Standard desktop |
+| ge-engineer | ge.com | Engineering desktop |
+| ge-shopfloor-lockdown| ge.com | Shop floor (locked) |
+| ge-shopfloor-mce | ge.com | Shop floor (MCE) |
## Network Configuration
@@ -130,3 +199,14 @@ pxe-server/
- DHCP range: `10.9.100.10` - `10.9.100.100`
- Lease time: 12 hours
- DNS: `8.8.8.8` (passed to clients, not used by server)
+- Firewall: UFW deny-by-default, allow 67/udp 69/udp 80/tcp 445/tcp 4433/tcp 9009/tcp
+
+## Samba Shares
+
+| Share | Path | Purpose |
+|-----------------|----------------------------|-------------------------------|
+| winpeapps | /srv/samba/winpeapps | WinPE deployment images |
+| clonezilla | /srv/samba/clonezilla | Clonezilla disk backup images |
+| blancco-reports | /srv/samba/blancco-reports | Blancco erasure reports (auto)|
+
+All shares use guest access for the isolated network.
diff --git a/autoinstall/user-data b/autoinstall/user-data
index 5a01193..caf6e7c 100644
--- a/autoinstall/user-data
+++ b/autoinstall/user-data
@@ -1,106 +1,111 @@
-#cloud-config
-autoinstall:
- version: 1
-
- # Locale, keyboard, timezone
- locale: en_US.UTF-8
- keyboard:
- layout: us
- variant: ""
- timezone: America/New_York
-
- # Network configuration
- # Uses a broad match so any wired NIC gets the static PXE address.
- # No WiFi needed — all packages are on the CIDATA partition.
- network:
- version: 2
- ethernets:
- any-eth:
- match:
- name: "en*"
- addresses:
- - 10.9.100.1/24
- dhcp4: false
- dhcp6: false
- optional: true
-
- # Storage configuration
- storage:
- layout:
- name: lvm
- match:
- size: largest
- swap:
- size: 0
-
- # User identity
- identity:
- hostname: pxeserver
- username: pxe
- password: "$6$rounds=656000$TpsuBw0N85085mpx$KtKsCwFlowg4NY41gUqx5ljef8cJ8uPFfgg43MyCPWByfXkhM5XushcdtkNps6lKeQFQZtli/QU.s52AUc7XC."
-
- # Installer-stage late commands
- late-commands:
- # Install deb packages from CIDATA USB
- - |
- curtin in-target --target=/target -- bash -c '
- mkdir -p /mnt/cidata
- CIDATA_DEV=$(blkid -L CIDATA)
- if [ -n "$CIDATA_DEV" ]; then
- mount "$CIDATA_DEV" /mnt/cidata
- if compgen -G "/mnt/cidata/packages/*.deb" > /dev/null; then
- cp /mnt/cidata/packages/*.deb /tmp/
- dpkg -i /tmp/*.deb 2>/dev/null || true
- dpkg -i /tmp/*.deb 2>/dev/null || true
- if command -v nmcli >/dev/null; then
- systemctl enable NetworkManager
- fi
- fi
- umount /mnt/cidata
- fi
- '
-
- # Create first-boot.sh
- - |
- curtin in-target --target=/target -- bash -c '
- cat <<"EOF" > /opt/first-boot.sh
- #!/bin/bash
- CIDATA_DEV=$(blkid -L CIDATA)
- if [ -n "$CIDATA_DEV" ]; then
- mkdir -p /mnt/usb
- mount "$CIDATA_DEV" /mnt/usb
- # Install all offline .deb packages (ansible, dnsmasq, apache2, samba, etc.)
- if compgen -G "/mnt/usb/packages/*.deb" > /dev/null; then
- dpkg -i /mnt/usb/packages/*.deb 2>/dev/null || true
- dpkg -i /mnt/usb/packages/*.deb 2>/dev/null || true
- fi
- # Run the Ansible playbook
- if [ -f /mnt/usb/playbook/pxe_server_setup.yml ]; then
- cd /mnt/usb/playbook
- ansible-playbook -i localhost, -c local pxe_server_setup.yml
- fi
- umount /mnt/usb
- 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
- EOF
- '
- - curtin in-target --target=/target -- chmod +x /opt/first-boot.sh
-
- # Create rc.local without unintended indentation
- - |
- 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: yes
+#cloud-config
+autoinstall:
+ version: 1
+
+ # Locale, keyboard, timezone
+ locale: en_US.UTF-8
+ keyboard:
+ layout: us
+ variant: ""
+ timezone: America/New_York
+
+ # Network configuration
+ # Uses a broad match so any wired NIC gets the static PXE address.
+ # No WiFi needed — all packages are on the CIDATA partition.
+ network:
+ version: 2
+ ethernets:
+ any-eth:
+ match:
+ name: "en*"
+ addresses:
+ - 10.9.100.1/24
+ dhcp4: false
+ dhcp6: false
+ optional: true
+
+ # Storage configuration
+ storage:
+ layout:
+ name: lvm
+ match:
+ size: largest
+ swap:
+ size: 0
+
+ # User identity
+ identity:
+ hostname: pxeserver
+ username: pxe
+ password: "$6$rounds=656000$TpsuBw0N85085mpx$KtKsCwFlowg4NY41gUqx5ljef8cJ8uPFfgg43MyCPWByfXkhM5XushcdtkNps6lKeQFQZtli/QU.s52AUc7XC."
+
+ # Enable SSH
+ ssh:
+ install-server: true
+ allow-pw: true
+
+ # Installer-stage late commands
+ late-commands:
+ # Install deb packages from CIDATA USB
+ - |
+ curtin in-target --target=/target -- bash -c '
+ mkdir -p /mnt/cidata
+ CIDATA_DEV=$(blkid -L CIDATA)
+ if [ -n "$CIDATA_DEV" ]; then
+ mount "$CIDATA_DEV" /mnt/cidata
+ if compgen -G "/mnt/cidata/packages/*.deb" > /dev/null; then
+ cp /mnt/cidata/packages/*.deb /tmp/
+ dpkg -i /tmp/*.deb 2>/dev/null || true
+ dpkg -i /tmp/*.deb 2>/dev/null || true
+ if command -v nmcli >/dev/null; then
+ systemctl enable NetworkManager
+ fi
+ fi
+ umount /mnt/cidata
+ fi
+ '
+
+ # Create first-boot.sh
+ - |
+ curtin in-target --target=/target -- bash -c '
+ cat <<"EOF" > /opt/first-boot.sh
+ #!/bin/bash
+ CIDATA_DEV=$(blkid -L CIDATA)
+ if [ -n "$CIDATA_DEV" ]; then
+ mkdir -p /mnt/usb
+ mount "$CIDATA_DEV" /mnt/usb
+ # Install all offline .deb packages (ansible, dnsmasq, apache2, samba, etc.)
+ if compgen -G "/mnt/usb/packages/*.deb" > /dev/null; then
+ dpkg -i /mnt/usb/packages/*.deb 2>/dev/null || true
+ dpkg -i /mnt/usb/packages/*.deb 2>/dev/null || true
+ fi
+ # Run the Ansible playbook
+ if [ -f /mnt/usb/playbook/pxe_server_setup.yml ]; then
+ cd /mnt/usb/playbook
+ ansible-playbook -i localhost, -c local pxe_server_setup.yml
+ fi
+ umount /mnt/usb
+ 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
+ EOF
+ '
+ - curtin in-target --target=/target -- chmod +x /opt/first-boot.sh
+
+ # Create rc.local without unintended indentation
+ - |
+ 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: yes
diff --git a/build-usb.sh b/build-usb.sh
index 2cba1c3..1e9dd43 100755
--- a/build-usb.sh
+++ b/build-usb.sh
@@ -1,240 +1,240 @@
-#!/bin/bash
-#
-# build-usb.sh — Build a bootable PXE-server installer USB
-#
-# Creates a two-partition USB:
-# Partition 1: Ubuntu Server 24.04 installer (ISO contents)
-# Partition 2: CIDATA volume (autoinstall config, .debs, playbook)
-#
-# The target machine boots from this USB, Ubuntu auto-installs with
-# cloud-init (user-data/meta-data from CIDATA), installs offline .debs,
-# and on first boot runs the Ansible playbook to configure PXE services.
-#
-# Usage:
-# sudo ./build-usb.sh /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso
-#
-# WARNING: This will ERASE the target USB device.
-
-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"
-
-# --- Validate arguments ---
-if [ $# -lt 2 ]; then
- echo "Usage: sudo $0 /dev/sdX /path/to/ubuntu-24.04.iso [/path/to/winpe-images]"
- echo ""
- echo " The optional third argument is the path to WinPE deployment content"
- echo " (e.g., the mounted Media Creator LITE USB). If provided, the images"
- echo " will be bundled onto the CIDATA partition for automatic import."
- echo ""
- echo "Available removable devices:"
- lsblk -d -o NAME,SIZE,TRAN,RM | grep -E '^\S+\s+\S+\s+(usb)\s+1'
- exit 1
-fi
-
-USB_DEV="$1"
-ISO_PATH="$2"
-WINPE_SOURCE="${3:-}"
-
-# Safety checks
-if [ "$(id -u)" -ne 0 ]; then
- echo "ERROR: Must run as root (sudo)."
- exit 1
-fi
-
-if [ ! -b "$USB_DEV" ]; then
- echo "ERROR: $USB_DEV is not a block device."
- exit 1
-fi
-
-if [ ! -f "$ISO_PATH" ]; then
- echo "ERROR: ISO not found at $ISO_PATH"
- exit 1
-fi
-
-# Verify it's a removable device (safety against wiping system disks)
-REMOVABLE=$(lsblk -nd -o RM "$USB_DEV" 2>/dev/null || echo "0")
-if [ "$REMOVABLE" != "1" ]; then
- echo "WARNING: $USB_DEV does not appear to be a removable device."
- read -rp "Are you SURE you want to erase $USB_DEV? (type YES): " CONFIRM
- if [ "$CONFIRM" != "YES" ]; then
- echo "Aborted."
- exit 1
- fi
-fi
-
-# Verify required source files exist
-if [ ! -f "$AUTOINSTALL_DIR/user-data" ]; then
- echo "ERROR: user-data not found at $AUTOINSTALL_DIR/user-data"
- exit 1
-fi
-
-if [ ! -f "$AUTOINSTALL_DIR/meta-data" ]; then
- echo "ERROR: meta-data not found at $AUTOINSTALL_DIR/meta-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 "============================================"
-echo "PXE Server USB Builder"
-echo "============================================"
-echo "USB Device : $USB_DEV"
-echo "ISO : $ISO_PATH"
-echo "Source Dir : $SCRIPT_DIR"
-echo ""
-echo "This will ERASE all data on $USB_DEV."
-read -rp "Continue? (y/N): " PROCEED
-if [[ ! "$PROCEED" =~ ^[Yy]$ ]]; then
- echo "Aborted."
- exit 1
-fi
-
-# --- Unmount any existing partitions ---
-echo ""
-echo "[1/6] Unmounting existing partitions on $USB_DEV..."
-for part in "${USB_DEV}"*; do
- umount "$part" 2>/dev/null || true
-done
-
-# --- Write ISO to USB ---
-echo "[2/6] Writing Ubuntu ISO to $USB_DEV (this may take several minutes)..."
-dd if="$ISO_PATH" of="$USB_DEV" bs=4M status=progress oflag=sync
-sync
-
-# --- Find the end of the ISO to create CIDATA partition ---
-echo "[3/6] Creating CIDATA partition after ISO data..."
-
-# Get ISO size in bytes and calculate the start sector for the new partition
-ISO_SIZE=$(stat -c%s "$ISO_PATH")
-SECTOR_SIZE=512
-# Start the CIDATA partition 1MB after the ISO ends (alignment)
-START_SECTOR=$(( (ISO_SIZE / SECTOR_SIZE) + 2048 ))
-
-# Use sfdisk to append a new partition
-echo " ISO size: $((ISO_SIZE / 1024 / 1024)) MB"
-echo " CIDATA partition starts at sector $START_SECTOR"
-
-# Add a new partition using sfdisk --append
-echo "${START_SECTOR},+,L" | sfdisk --append "$USB_DEV" --no-reread 2>/dev/null || true
-partprobe "$USB_DEV"
-sleep 2
-
-# Determine the new partition name (could be sdX3, sdX4, etc.)
-CIDATA_PART=""
-for part in "${USB_DEV}"[0-9]*; do
- # Find the partition that starts at or after our start sector
- PART_START=$(sfdisk -d "$USB_DEV" 2>/dev/null | grep "$part" | grep -o 'start=[[:space:]]*[0-9]*' | grep -o '[0-9]*')
- if [ -n "$PART_START" ] && [ "$PART_START" -ge "$START_SECTOR" ]; then
- CIDATA_PART="$part"
- break
- fi
-done
-
-# Fallback: use the last partition
-if [ -z "$CIDATA_PART" ]; then
- CIDATA_PART=$(lsblk -ln -o NAME "$USB_DEV" | tail -1)
- CIDATA_PART="/dev/$CIDATA_PART"
-fi
-
-echo " CIDATA partition: $CIDATA_PART"
-
-# --- Format CIDATA partition ---
-echo "[4/6] Formatting $CIDATA_PART as FAT32 (label: CIDATA)..."
-mkfs.vfat -F 32 -n CIDATA "$CIDATA_PART"
-
-# --- Mount and copy files ---
-echo "[5/6] Copying autoinstall config, packages, and playbook to CIDATA..."
-MOUNT_POINT=$(mktemp -d)
-mount "$CIDATA_PART" "$MOUNT_POINT"
-
-# Copy cloud-init files
-cp "$AUTOINSTALL_DIR/user-data" "$MOUNT_POINT/"
-cp "$AUTOINSTALL_DIR/meta-data" "$MOUNT_POINT/"
-
-# Copy offline .deb packages into packages/ subdirectory
-mkdir -p "$MOUNT_POINT/packages"
-DEB_COUNT=0
-
-if [ -d "$OFFLINE_PKG_DIR" ]; then
- for deb in "$OFFLINE_PKG_DIR"/*.deb; do
- if [ -f "$deb" ]; then
- cp "$deb" "$MOUNT_POINT/packages/"
- DEB_COUNT=$((DEB_COUNT + 1))
- fi
- done
-fi
-echo " Copied $DEB_COUNT .deb packages to packages/"
-
-# Copy playbook directory
-cp -r "$PLAYBOOK_DIR" "$MOUNT_POINT/playbook"
-echo " Copied playbook/"
-
-# Copy webapp
-WEBAPP_DIR="$SCRIPT_DIR/webapp"
-if [ -d "$WEBAPP_DIR" ]; then
- mkdir -p "$MOUNT_POINT/webapp"
- cp -r "$WEBAPP_DIR/app.py" "$WEBAPP_DIR/requirements.txt" "$MOUNT_POINT/webapp/"
- cp -r "$WEBAPP_DIR/templates" "$WEBAPP_DIR/static" "$MOUNT_POINT/webapp/"
- echo " Copied webapp/"
-fi
-
-# Copy pip wheels for offline Flask install
-PIP_WHEELS_DIR="$SCRIPT_DIR/pip-wheels"
-if [ -d "$PIP_WHEELS_DIR" ]; then
- cp -r "$PIP_WHEELS_DIR" "$MOUNT_POINT/pip-wheels"
- echo " Copied pip-wheels/"
-else
- echo " No pip-wheels/ found (run download-packages.sh first)"
-fi
-
-# Copy boot tools (Clonezilla, Blancco, Memtest) if prepared
-BOOT_TOOLS_DIR="$SCRIPT_DIR/boot-tools"
-if [ -d "$BOOT_TOOLS_DIR" ]; then
- cp -r "$BOOT_TOOLS_DIR" "$MOUNT_POINT/boot-tools"
- TOOLS_SIZE=$(du -sh "$MOUNT_POINT/boot-tools" | cut -f1)
- echo " Copied boot-tools/ ($TOOLS_SIZE)"
-else
- echo " No boot-tools/ found (run prepare-boot-tools.sh first)"
-fi
-
-# Optionally copy WinPE deployment images
-if [ -n "$WINPE_SOURCE" ] && [ -d "$WINPE_SOURCE" ]; then
- echo " Copying WinPE deployment content from $WINPE_SOURCE..."
- mkdir -p "$MOUNT_POINT/images"
- cp -r "$WINPE_SOURCE"/* "$MOUNT_POINT/images/" 2>/dev/null || true
- IMG_SIZE=$(du -sh "$MOUNT_POINT/images" | cut -f1)
- echo " Copied WinPE images ($IMG_SIZE)"
-elif [ -n "$WINPE_SOURCE" ]; then
- echo " WARNING: WinPE source path not found: $WINPE_SOURCE (skipping)"
-fi
-
-# List what's on CIDATA
-echo ""
-echo " CIDATA contents:"
-ls -lh "$MOUNT_POINT/" | sed 's/^/ /'
-
-# --- Cleanup ---
-echo ""
-echo "[6/6] Syncing and unmounting..."
-sync
-umount "$MOUNT_POINT"
-rmdir "$MOUNT_POINT"
-
-echo ""
-echo "============================================"
-echo "USB build complete!"
-echo "============================================"
-echo ""
-echo "Next steps:"
-echo " 1. Insert USB into target machine"
-echo " 2. Boot from USB (F12 / boot menu)"
-echo " 3. Ubuntu will auto-install and configure the PXE server"
-echo " 4. After reboot, move the NIC to the isolated PXE network"
-echo ""
+#!/bin/bash
+#
+# build-usb.sh — Build a bootable PXE-server installer USB
+#
+# Creates a two-partition USB:
+# Partition 1: Ubuntu Server 24.04 installer (ISO contents)
+# Partition 2: CIDATA volume (autoinstall config, .debs, playbook)
+#
+# The target machine boots from this USB, Ubuntu auto-installs with
+# cloud-init (user-data/meta-data from CIDATA), installs offline .debs,
+# and on first boot runs the Ansible playbook to configure PXE services.
+#
+# Usage:
+# sudo ./build-usb.sh /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso
+#
+# WARNING: This will ERASE the target USB device.
+
+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"
+
+# --- Validate arguments ---
+if [ $# -lt 2 ]; then
+ echo "Usage: sudo $0 /dev/sdX /path/to/ubuntu-24.04.iso [/path/to/winpe-images]"
+ echo ""
+ echo " The optional third argument is the path to WinPE deployment content"
+ echo " (e.g., the mounted Media Creator LITE USB). If provided, the images"
+ echo " will be bundled onto the CIDATA partition for automatic import."
+ echo ""
+ echo "Available removable devices:"
+ lsblk -d -o NAME,SIZE,TRAN,RM | grep -E '^\S+\s+\S+\s+(usb)\s+1'
+ exit 1
+fi
+
+USB_DEV="$1"
+ISO_PATH="$2"
+WINPE_SOURCE="${3:-}"
+
+# Safety checks
+if [ "$(id -u)" -ne 0 ]; then
+ echo "ERROR: Must run as root (sudo)."
+ exit 1
+fi
+
+if [ ! -b "$USB_DEV" ]; then
+ echo "ERROR: $USB_DEV is not a block device."
+ exit 1
+fi
+
+if [ ! -f "$ISO_PATH" ]; then
+ echo "ERROR: ISO not found at $ISO_PATH"
+ exit 1
+fi
+
+# Verify it's a removable device (safety against wiping system disks)
+REMOVABLE=$(lsblk -nd -o RM "$USB_DEV" 2>/dev/null || echo "0")
+if [ "$REMOVABLE" != "1" ]; then
+ echo "WARNING: $USB_DEV does not appear to be a removable device."
+ read -rp "Are you SURE you want to erase $USB_DEV? (type YES): " CONFIRM
+ if [ "$CONFIRM" != "YES" ]; then
+ echo "Aborted."
+ exit 1
+ fi
+fi
+
+# Verify required source files exist
+if [ ! -f "$AUTOINSTALL_DIR/user-data" ]; then
+ echo "ERROR: user-data not found at $AUTOINSTALL_DIR/user-data"
+ exit 1
+fi
+
+if [ ! -f "$AUTOINSTALL_DIR/meta-data" ]; then
+ echo "ERROR: meta-data not found at $AUTOINSTALL_DIR/meta-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 "============================================"
+echo "PXE Server USB Builder"
+echo "============================================"
+echo "USB Device : $USB_DEV"
+echo "ISO : $ISO_PATH"
+echo "Source Dir : $SCRIPT_DIR"
+echo ""
+echo "This will ERASE all data on $USB_DEV."
+read -rp "Continue? (y/N): " PROCEED
+if [[ ! "$PROCEED" =~ ^[Yy]$ ]]; then
+ echo "Aborted."
+ exit 1
+fi
+
+# --- Unmount any existing partitions ---
+echo ""
+echo "[1/6] Unmounting existing partitions on $USB_DEV..."
+for part in "${USB_DEV}"*; do
+ umount "$part" 2>/dev/null || true
+done
+
+# --- Write ISO to USB ---
+echo "[2/6] Writing Ubuntu ISO to $USB_DEV (this may take several minutes)..."
+dd if="$ISO_PATH" of="$USB_DEV" bs=4M status=progress oflag=sync
+sync
+
+# --- Find the end of the ISO to create CIDATA partition ---
+echo "[3/6] Creating CIDATA partition after ISO data..."
+
+# Get ISO size in bytes and calculate the start sector for the new partition
+ISO_SIZE=$(stat -c%s "$ISO_PATH")
+SECTOR_SIZE=512
+# Start the CIDATA partition 1MB after the ISO ends (alignment)
+START_SECTOR=$(( (ISO_SIZE / SECTOR_SIZE) + 2048 ))
+
+# Use sfdisk to append a new partition
+echo " ISO size: $((ISO_SIZE / 1024 / 1024)) MB"
+echo " CIDATA partition starts at sector $START_SECTOR"
+
+# Add a new partition using sfdisk --append
+echo "${START_SECTOR},+,L" | sfdisk --append "$USB_DEV" --no-reread 2>/dev/null || true
+partprobe "$USB_DEV"
+sleep 2
+
+# Determine the new partition name (could be sdX3, sdX4, etc.)
+CIDATA_PART=""
+for part in "${USB_DEV}"[0-9]*; do
+ # Find the partition that starts at or after our start sector
+ PART_START=$(sfdisk -d "$USB_DEV" 2>/dev/null | grep "$part" | grep -o 'start=[[:space:]]*[0-9]*' | grep -o '[0-9]*')
+ if [ -n "$PART_START" ] && [ "$PART_START" -ge "$START_SECTOR" ]; then
+ CIDATA_PART="$part"
+ break
+ fi
+done
+
+# Fallback: use the last partition
+if [ -z "$CIDATA_PART" ]; then
+ CIDATA_PART=$(lsblk -ln -o NAME "$USB_DEV" | tail -1)
+ CIDATA_PART="/dev/$CIDATA_PART"
+fi
+
+echo " CIDATA partition: $CIDATA_PART"
+
+# --- Format CIDATA partition ---
+echo "[4/6] Formatting $CIDATA_PART as FAT32 (label: CIDATA)..."
+mkfs.vfat -F 32 -n CIDATA "$CIDATA_PART"
+
+# --- Mount and copy files ---
+echo "[5/6] Copying autoinstall config, packages, and playbook to CIDATA..."
+MOUNT_POINT=$(mktemp -d)
+mount "$CIDATA_PART" "$MOUNT_POINT"
+
+# Copy cloud-init files
+cp "$AUTOINSTALL_DIR/user-data" "$MOUNT_POINT/"
+cp "$AUTOINSTALL_DIR/meta-data" "$MOUNT_POINT/"
+
+# Copy offline .deb packages into packages/ subdirectory
+mkdir -p "$MOUNT_POINT/packages"
+DEB_COUNT=0
+
+if [ -d "$OFFLINE_PKG_DIR" ]; then
+ for deb in "$OFFLINE_PKG_DIR"/*.deb; do
+ if [ -f "$deb" ]; then
+ cp "$deb" "$MOUNT_POINT/packages/"
+ DEB_COUNT=$((DEB_COUNT + 1))
+ fi
+ done
+fi
+echo " Copied $DEB_COUNT .deb packages to packages/"
+
+# Copy playbook directory
+cp -r "$PLAYBOOK_DIR" "$MOUNT_POINT/playbook"
+echo " Copied playbook/"
+
+# Copy webapp
+WEBAPP_DIR="$SCRIPT_DIR/webapp"
+if [ -d "$WEBAPP_DIR" ]; then
+ mkdir -p "$MOUNT_POINT/webapp"
+ cp -r "$WEBAPP_DIR/app.py" "$WEBAPP_DIR/requirements.txt" "$MOUNT_POINT/webapp/"
+ cp -r "$WEBAPP_DIR/templates" "$WEBAPP_DIR/static" "$MOUNT_POINT/webapp/"
+ echo " Copied webapp/"
+fi
+
+# Copy pip wheels for offline Flask install
+PIP_WHEELS_DIR="$SCRIPT_DIR/pip-wheels"
+if [ -d "$PIP_WHEELS_DIR" ]; then
+ cp -r "$PIP_WHEELS_DIR" "$MOUNT_POINT/pip-wheels"
+ echo " Copied pip-wheels/"
+else
+ echo " No pip-wheels/ found (run download-packages.sh first)"
+fi
+
+# Copy boot tools (Clonezilla, Blancco, Memtest) if prepared
+BOOT_TOOLS_DIR="$SCRIPT_DIR/boot-tools"
+if [ -d "$BOOT_TOOLS_DIR" ]; then
+ cp -r "$BOOT_TOOLS_DIR" "$MOUNT_POINT/boot-tools"
+ TOOLS_SIZE=$(du -sh "$MOUNT_POINT/boot-tools" | cut -f1)
+ echo " Copied boot-tools/ ($TOOLS_SIZE)"
+else
+ echo " No boot-tools/ found (run prepare-boot-tools.sh first)"
+fi
+
+# Optionally copy WinPE deployment images
+if [ -n "$WINPE_SOURCE" ] && [ -d "$WINPE_SOURCE" ]; then
+ echo " Copying WinPE deployment content from $WINPE_SOURCE..."
+ mkdir -p "$MOUNT_POINT/images"
+ cp -r "$WINPE_SOURCE"/* "$MOUNT_POINT/images/" 2>/dev/null || true
+ IMG_SIZE=$(du -sh "$MOUNT_POINT/images" | cut -f1)
+ echo " Copied WinPE images ($IMG_SIZE)"
+elif [ -n "$WINPE_SOURCE" ]; then
+ echo " WARNING: WinPE source path not found: $WINPE_SOURCE (skipping)"
+fi
+
+# List what's on CIDATA
+echo ""
+echo " CIDATA contents:"
+ls -lh "$MOUNT_POINT/" | sed 's/^/ /'
+
+# --- Cleanup ---
+echo ""
+echo "[6/6] Syncing and unmounting..."
+sync
+umount "$MOUNT_POINT"
+rmdir "$MOUNT_POINT"
+
+echo ""
+echo "============================================"
+echo "USB build complete!"
+echo "============================================"
+echo ""
+echo "Next steps:"
+echo " 1. Insert USB into target machine"
+echo " 2. Boot from USB (F12 / boot menu)"
+echo " 3. Ubuntu will auto-install and configure the PXE server"
+echo " 4. After reboot, move the NIC to the isolated PXE network"
+echo ""
diff --git a/download-packages.sh b/download-packages.sh
index 92e1337..edf9dbf 100755
--- a/download-packages.sh
+++ b/download-packages.sh
@@ -1,94 +1,94 @@
-#!/bin/bash
-#
-# download-packages.sh — Download all .deb packages needed for offline PXE server setup
-#
-# Run this on a machine with internet access running Ubuntu 24.04 (Noble).
-# It downloads every .deb needed by the Ansible playbook into a local directory,
-# which then gets bundled onto the installer USB.
-#
-# Usage:
-# ./download-packages.sh [output_directory]
-#
-# Default output: ./offline-packages/
-
-set -euo pipefail
-
-OUT_DIR="${1:-./offline-packages}"
-mkdir -p "$OUT_DIR"
-
-# Packages installed by the Ansible playbook (pxe_server_setup.yml)
-PLAYBOOK_PACKAGES=(
- ansible
- dnsmasq
- apache2
- samba
- unzip
- ufw
- cron
- wimtools
- python3-pip
- python3-venv
- p7zip-full
-)
-
-# Packages installed during autoinstall late-commands (NetworkManager, WiFi, etc.)
-# These are already in your ubuntu_playbook/*.deb files, but we can refresh them here too.
-AUTOINSTALL_PACKAGES=(
- network-manager
- wpasupplicant
- wireless-tools
- linux-firmware
- firmware-sof-signed
-)
-
-ALL_PACKAGES=("${PLAYBOOK_PACKAGES[@]}" "${AUTOINSTALL_PACKAGES[@]}")
-
-echo "============================================"
-echo "Offline Package Downloader"
-echo "============================================"
-echo "Output directory: $OUT_DIR"
-echo ""
-echo "Packages to resolve:"
-printf ' - %s\n' "${ALL_PACKAGES[@]}"
-echo ""
-
-# Update package cache
-echo "[1/3] Updating package cache..."
-sudo apt-get update -qq
-
-# Simulate install to find all dependencies
-echo "[2/3] Resolving dependencies..."
-DEPS=$(apt-get install --simulate "${ALL_PACKAGES[@]}" 2>&1 \
- | grep "^Inst " \
- | awk '{print $2}' \
- | sort -u)
-
-DEP_COUNT=$(echo "$DEPS" | wc -l)
-echo " Found $DEP_COUNT packages (including dependencies)"
-
-# Download all packages
-echo "[3/4] Downloading .deb packages to $OUT_DIR..."
-cd "$OUT_DIR"
-apt-get download $DEPS 2>&1 | tail -5
-
-DEB_COUNT=$(ls -1 *.deb 2>/dev/null | wc -l)
-TOTAL_SIZE=$(du -sh . | cut -f1)
-
-echo " $DEB_COUNT packages ($TOTAL_SIZE)"
-
-# Download pip wheels for Flask webapp (offline install)
-echo "[4/4] Downloading Python wheels for webapp..."
-PIP_DIR="$(dirname "$OUT_DIR")/pip-wheels"
-mkdir -p "$PIP_DIR"
-pip3 download -d "$PIP_DIR" flask lxml 2>&1 | tail -5
-
-WHL_COUNT=$(ls -1 "$PIP_DIR"/*.whl "$PIP_DIR"/*.tar.gz 2>/dev/null | wc -l)
-echo " $WHL_COUNT Python packages downloaded to pip-wheels/"
-
-echo ""
-echo "============================================"
-echo "Download complete!"
-echo "============================================"
-echo " .deb packages: $DEB_COUNT ($TOTAL_SIZE) in $OUT_DIR/"
-echo " Python wheels: $WHL_COUNT in $PIP_DIR/"
-echo ""
+#!/bin/bash
+#
+# download-packages.sh — Download all .deb packages needed for offline PXE server setup
+#
+# Run this on a machine with internet access running Ubuntu 24.04 (Noble).
+# It downloads every .deb needed by the Ansible playbook into a local directory,
+# which then gets bundled onto the installer USB.
+#
+# Usage:
+# ./download-packages.sh [output_directory]
+#
+# Default output: ./offline-packages/
+
+set -euo pipefail
+
+OUT_DIR="${1:-./offline-packages}"
+mkdir -p "$OUT_DIR"
+
+# Packages installed by the Ansible playbook (pxe_server_setup.yml)
+PLAYBOOK_PACKAGES=(
+ ansible
+ dnsmasq
+ apache2
+ samba
+ unzip
+ ufw
+ cron
+ wimtools
+ python3-pip
+ python3-venv
+ p7zip-full
+)
+
+# Packages installed during autoinstall late-commands (NetworkManager, WiFi, etc.)
+# These are already in your ubuntu_playbook/*.deb files, but we can refresh them here too.
+AUTOINSTALL_PACKAGES=(
+ network-manager
+ wpasupplicant
+ wireless-tools
+ linux-firmware
+ firmware-sof-signed
+)
+
+ALL_PACKAGES=("${PLAYBOOK_PACKAGES[@]}" "${AUTOINSTALL_PACKAGES[@]}")
+
+echo "============================================"
+echo "Offline Package Downloader"
+echo "============================================"
+echo "Output directory: $OUT_DIR"
+echo ""
+echo "Packages to resolve:"
+printf ' - %s\n' "${ALL_PACKAGES[@]}"
+echo ""
+
+# Update package cache
+echo "[1/3] Updating package cache..."
+sudo apt-get update -qq
+
+# Simulate install to find all dependencies
+echo "[2/3] Resolving dependencies..."
+DEPS=$(apt-get install --simulate "${ALL_PACKAGES[@]}" 2>&1 \
+ | grep "^Inst " \
+ | awk '{print $2}' \
+ | sort -u)
+
+DEP_COUNT=$(echo "$DEPS" | wc -l)
+echo " Found $DEP_COUNT packages (including dependencies)"
+
+# Download all packages
+echo "[3/4] Downloading .deb packages to $OUT_DIR..."
+cd "$OUT_DIR"
+apt-get download $DEPS 2>&1 | tail -5
+
+DEB_COUNT=$(ls -1 *.deb 2>/dev/null | wc -l)
+TOTAL_SIZE=$(du -sh . | cut -f1)
+
+echo " $DEB_COUNT packages ($TOTAL_SIZE)"
+
+# Download pip wheels for Flask webapp (offline install)
+echo "[4/4] Downloading Python wheels for webapp..."
+PIP_DIR="$(dirname "$OUT_DIR")/pip-wheels"
+mkdir -p "$PIP_DIR"
+pip3 download -d "$PIP_DIR" flask lxml 2>&1 | tail -5
+
+WHL_COUNT=$(ls -1 "$PIP_DIR"/*.whl "$PIP_DIR"/*.tar.gz 2>/dev/null | wc -l)
+echo " $WHL_COUNT Python packages downloaded to pip-wheels/"
+
+echo ""
+echo "============================================"
+echo "Download complete!"
+echo "============================================"
+echo " .deb packages: $DEB_COUNT ($TOTAL_SIZE) in $OUT_DIR/"
+echo " Python wheels: $WHL_COUNT in $PIP_DIR/"
+echo ""
diff --git a/playbook/pxe_server_setup.yml b/playbook/pxe_server_setup.yml
index daa3ca2..2221fc5 100644
--- a/playbook/pxe_server_setup.yml
+++ b/playbook/pxe_server_setup.yml
@@ -1,443 +1,445 @@
----
-- name: PXE Server Setup (Ubuntu with dnsmasq)
- hosts: localhost
- connection: local
- become: yes
- gather_facts: yes
-
- pre_tasks:
- - name: "Verify required packages are installed (pre-installed from offline .debs)"
- command: dpkg -s {{ item }}
- loop:
- - dnsmasq
- - apache2
- - samba
- - unzip
- - ufw
- - cron
- - ansible
- - wimtools
- register: pkg_check
- failed_when: false
- changed_when: false
-
- - name: "Warn about missing packages"
- debug:
- msg: "WARNING: {{ item.item }} is not installed! Install offline .debs first."
- loop: "{{ pkg_check.results }}"
- when: item.rc != 0
-
- vars:
- tftp_dir: "/srv/tftp"
- web_root: "/var/www/html"
- samba_share: "/srv/samba/winpeapps"
- usb_mount: "/mnt/usb/playbook" # where your USB is mounted
- image_types:
- - gea-standard
- - gea-engineer
- - gea-shopfloor
- - ge-standard
- - ge-engineer
- - ge-shopfloor-lockdown
- - ge-shopfloor-mce
- deploy_subdirs:
- - Applications
- - Control
- - "Operating Systems"
- - "Out-of-box Drivers"
- - Packages
- - Tools
-
- tasks:
- - name: "Gather minimal network facts"
- ansible.builtin.setup:
- filter:
- - ansible_interfaces
- - ansible_default_ipv4
-
- - name: "Bring up all ethernet-like interfaces"
- command: ip link set dev {{ item }} up
- loop: "{{ ansible_interfaces | select('match','^e(th|n)') | list }}"
- ignore_errors: yes
-
- - name: "Determine PXE interface"
- set_fact:
- pxe_iface: >-
- {{ (ansible_interfaces
- | select('match','^e(th|n)')
- | reject('equalto','lo')
- | reject('equalto', ansible_default_ipv4.interface)
- | list
- )
- | first
- | default(ansible_default_ipv4.interface) }}
-
- - name: "Debug: final pxe_iface choice"
- debug:
- msg: "Using {{ pxe_iface }} for DHCP/TFTP"
-
- - name: "Configure dnsmasq for DHCP and TFTP"
- copy:
- dest: /etc/dnsmasq.conf
- backup: yes
- content: |
- port=0
- interface={{ pxe_iface }}
- bind-interfaces
- dhcp-range=10.9.100.10,10.9.100.100,12h
- dhcp-option=3,10.9.100.1
- dhcp-option=6,8.8.8.8
- enable-tftp
- tftp-root={{ tftp_dir }}
- dhcp-boot=ipxe.efi
-
- - name: "Create TFTP directory"
- file:
- path: "{{ tftp_dir }}"
- state: directory
- mode: '0755'
- owner: nobody
- group: nogroup
-
- - name: "Create Win11 directory structure"
- file:
- path: "{{ web_root }}/win11/{{ item }}"
- state: directory
- mode: '0755'
- loop:
- - "EFI/Boot"
- - "EFI/Microsoft/Boot"
- - "Boot"
- - "sources"
-
- - name: "Create Altiris iPXE directory"
- file:
- path: "{{ web_root }}/Altiris/iPXE"
- state: directory
- mode: '0755'
-
- - name: "Create boot tool directories"
- file:
- path: "{{ web_root }}/{{ item }}"
- state: directory
- mode: '0755'
- loop:
- - clonezilla
- - blancco
- - memtest
-
- - name: "Create GetPxeScript.aspx (iPXE boot menu)"
- copy:
- dest: "{{ web_root }}/Altiris/iPXE/GetPxeScript.aspx"
- backup: yes
- content: |
- #!ipxe
-
- set server 10.9.100.1
-
- :menu
- menu GE Aerospace PXE Boot Menu
- item --gap -- ---- Windows Deployment ----
- item winpe Windows PE (Image Deployment)
- item --gap -- ---- Utilities ----
- item clonezilla Clonezilla Live (Disk Imaging)
- item blancco Blancco Drive Eraser
- item memtest Memtest86+ (Memory Diagnostics)
- item --gap -- ----
- item reboot Reboot
- item exit Exit to BIOS
- choose --default winpe --timeout 30000 target && goto ${target}
-
- :winpe
- kernel http://${server}/win11/wimboot gui
- initrd http://${server}/win11/EFI/Microsoft/Boot/boot.stl EFI/Microsoft/Boot/Boot.stl
- initrd http://${server}/win11/EFI/Microsoft/Boot/BCD EFI/Microsoft/Boot/BCD
- initrd http://${server}/win11/EFI/Boot/bootx64.efi EFI/Boot/bootx64.efi
- initrd http://${server}/win11/Boot/boot.sdi Boot/boot.sdi
- initrd http://${server}/win11/sources/boot.wim sources/boot.wim
- boot
-
- :clonezilla
- set base http://${server}/clonezilla
- kernel ${base}/vmlinuz boot=live username=user union=overlay config components noswap edd=on nomodeset nodmraid locales= keyboard-layouts= ocs_live_run="ocs-live-general" ocs_live_extra_param="" ocs_live_batch=no net.ifnames=0 nosplash noprompt fetch=${base}/filesystem.squashfs
- initrd ${base}/initrd.img
- boot
-
- :blancco
- set bbase http://${server}/blancco
- kernel ${bbase}/vmlinuz-bde-linux archisobasedir=arch archiso_http_srv=http://${server}/blancco/ copytoram=y cow_spacesize=50% memtest=00 vmalloc=400M ip=dhcp quiet nomodeset libata.allow_tpm=1
- initrd ${bbase}/intel-ucode.img ${bbase}/amd-ucode.img ${bbase}/config.img ${bbase}/initramfs-bde-linux.img
- boot
-
- :memtest
- kernel http://${server}/memtest/memtest.efi
- boot
-
- :reboot
- reboot
-
- :exit
- exit
-
- - name: "Ensure Apache listens on port 4433"
- lineinfile:
- path: /etc/apache2/ports.conf
- line: "Listen 4433"
- backup: yes
- state: present
-
- - name: "Create VirtualHost for Altiris iPXE on 4433"
- copy:
- dest: /etc/apache2/sites-available/altiris-ipxe.conf
- backup: yes
- content: |
-
- DocumentRoot {{ web_root }}
-
- Options Indexes FollowSymLinks
- AllowOverride None
- Require all granted
- AddType text/plain .aspx
-
-
-
- - name: "Enable Altiris iPXE site"
- command: a2ensite altiris-ipxe.conf
- args:
- creates: /etc/apache2/sites-enabled/altiris-ipxe.conf
-
- - name: "Reload Apache to apply changes"
- systemd:
- name: apache2
- state: reloaded
-
- - name: "Create Samba share root"
- file:
- path: "{{ samba_share }}"
- state: directory
- mode: '0777'
-
- - name: "Create Clonezilla backup share directory"
- file:
- path: /srv/samba/clonezilla
- state: directory
- mode: '0777'
-
- - name: "Create Blancco reports share directory"
- file:
- path: /srv/samba/blancco-reports
- state: directory
- mode: '0777'
-
- - name: "Configure Samba shares"
- blockinfile:
- path: /etc/samba/smb.conf
- backup: yes
- block: |
- [winpeapps]
- path = {{ samba_share }}
- browseable = yes
- read only = no
- guest ok = yes
-
- [clonezilla]
- path = /srv/samba/clonezilla
- browseable = yes
- read only = no
- guest ok = yes
- comment = Clonezilla backup images
-
- [blancco-reports]
- path = /srv/samba/blancco-reports
- browseable = yes
- read only = no
- guest ok = yes
- comment = Blancco Drive Eraser reports
-
- - name: "Create image-type top-level directories"
- file:
- path: "{{ samba_share }}/{{ item }}"
- state: directory
- mode: '0777'
- loop: "{{ image_types }}"
-
- - name: "Create Deploy subdirectories for each image type"
- file:
- path: "{{ samba_share }}/{{ item.0 }}/Deploy/{{ item.1 }}"
- state: directory
- mode: '0777'
- with_nested:
- - "{{ image_types }}"
- - "{{ deploy_subdirs }}"
-
- - name: "Copy WinPE & boot files from USB"
- copy:
- src: "{{ usb_mount }}/{{ item.src }}"
- dest: "{{ web_root }}/win11/{{ item.dest }}"
- mode: '0644'
- loop:
- - { src: "wimboot", dest: "wimboot" }
- - { src: "boot.stl", dest: "EFI/Microsoft/Boot/boot.stl" }
- - { src: "BCD", dest: "EFI/Microsoft/Boot/BCD" }
- - { src: "bootx64.efi", dest: "EFI/Boot/bootx64.efi" }
- - { src: "boot.sdi", dest: "Boot/boot.sdi" }
- - { src: "boot.wim", dest: "sources/boot.wim" }
-
- - name: "Copy iPXE binaries from USB"
- copy:
- src: "{{ usb_mount }}/{{ item }}"
- dest: "{{ tftp_dir }}/{{ item }}"
- mode: '0755'
- loop:
- - ipxe.efi
-
- - 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
- loop:
- - clonezilla
- - blancco
- - memtest
-
- - name: "Check for WinPE deployment content on USB"
- stat:
- path: "{{ usb_mount }}/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
- loop: "{{ image_types }}"
- when: usb_images_dir.stat.exists
-
- - name: "Restart and enable services"
- systemd:
- name: "{{ item }}"
- state: restarted
- enabled: yes
- loop:
- - dnsmasq
- - apache2
- - smbd
-
- - name: "Allow necessary firewall ports (UFW)"
- ufw:
- rule: allow
- port: "{{ item }}"
- proto: "{{ 'udp' if item in ['67','69'] else 'tcp' }}"
- loop:
- - 67
- - 69
- - 80
- - 4433
- - 445
- - 9009
-
- - name: "Enable UFW firewall"
- ufw:
- state: enabled
- policy: deny
-
- - name: "Schedule dnsmasq restart 15s after reboot"
- cron:
- name: "Restart dnsmasq after reboot"
- user: root
- special_time: "reboot"
- job: "/bin/sleep 15 && /usr/bin/systemctl restart dnsmasq.service"
-
- # --- Web Management App (Flask) ---
- - name: "Create webapp directory"
- file:
- path: /opt/pxe-webapp
- state: directory
- mode: '0755'
-
- - 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
- args:
- creates: /opt/pxe-webapp/app.py
-
- - name: "Create Python virtual environment for webapp"
- command: python3 -m venv /opt/pxe-webapp/venv
- args:
- creates: /opt/pxe-webapp/venv/bin/python
-
- - name: "Install webapp Python dependencies (offline wheels)"
- shell: >
- /opt/pxe-webapp/venv/bin/pip install --no-index
- --find-links="{{ usb_mount }}/../pip-wheels/"
- --find-links="{{ usb_mount }}/pip-wheels/"
- -r /opt/pxe-webapp/requirements.txt 2>/dev/null ||
- /opt/pxe-webapp/venv/bin/pip install -r /opt/pxe-webapp/requirements.txt
-
- - name: "Create systemd service for PXE webapp"
- copy:
- dest: /etc/systemd/system/pxe-webapp.service
- content: |
- [Unit]
- Description=PXE Server Web Management
- After=network.target apache2.service
-
- [Service]
- Type=simple
- User=root
- WorkingDirectory=/opt/pxe-webapp
- Environment=SAMBA_SHARE={{ samba_share }}
- Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla
- Environment=WEB_ROOT={{ web_root }}
- Environment=BLANCCO_REPORTS=/srv/samba/blancco-reports
- Environment=AUDIT_LOG=/var/log/pxe-webapp-audit.log
- ExecStart=/opt/pxe-webapp/venv/bin/python app.py
- Restart=always
- RestartSec=5
-
- [Install]
- WantedBy=multi-user.target
-
- - name: "Enable and start PXE webapp service"
- systemd:
- name: pxe-webapp
- state: started
- enabled: yes
- daemon_reload: yes
-
- - name: "Configure Apache reverse proxy for webapp"
- copy:
- dest: /etc/apache2/sites-available/pxe-webapp.conf
- content: |
-
- ProxyPreserveHost On
- ProxyPass /manage http://127.0.0.1:9009/
- ProxyPassReverse /manage http://127.0.0.1:9009/
-
-
- - name: "Enable Apache proxy modules"
- command: a2enmod proxy proxy_http
- args:
- creates: /etc/apache2/mods-enabled/proxy.load
-
- - name: "Enable webapp Apache site"
- command: a2ensite pxe-webapp.conf
- args:
- creates: /etc/apache2/sites-enabled/pxe-webapp.conf
-
- - name: "Configure static IP for PXE interface"
- copy:
- dest: /etc/netplan/50-cloud-init.yaml
- backup: yes
- content: |
- network:
- version: 2
- renderer: networkd
- ethernets:
- {{ pxe_iface }}:
- dhcp4: no
- addresses: [10.9.100.1/24]
- notify: "Apply netplan"
-
- handlers:
- - name: "Apply netplan"
- command: netplan apply
+---
+- name: PXE Server Setup (Ubuntu with dnsmasq)
+ hosts: localhost
+ connection: local
+ become: yes
+ gather_facts: yes
+
+ pre_tasks:
+ - name: "Verify required packages are installed (pre-installed from offline .debs)"
+ command: dpkg -s {{ item }}
+ loop:
+ - dnsmasq
+ - apache2
+ - samba
+ - unzip
+ - ufw
+ - cron
+ - ansible
+ - wimtools
+ register: pkg_check
+ failed_when: false
+ changed_when: false
+
+ - name: "Warn about missing packages"
+ debug:
+ msg: "WARNING: {{ item.item }} is not installed! Install offline .debs first."
+ loop: "{{ pkg_check.results }}"
+ when: item.rc != 0
+
+ vars:
+ tftp_dir: "/srv/tftp"
+ web_root: "/var/www/html"
+ samba_share: "/srv/samba/winpeapps"
+ usb_mount: "/mnt/usb/playbook" # where your USB is mounted
+ image_types:
+ - gea-standard
+ - gea-engineer
+ - gea-shopfloor
+ - ge-standard
+ - ge-engineer
+ - ge-shopfloor-lockdown
+ - ge-shopfloor-mce
+ deploy_subdirs:
+ - Applications
+ - Control
+ - "Operating Systems"
+ - "Out-of-box Drivers"
+ - Packages
+ - Tools
+
+ tasks:
+ - name: "Gather minimal network facts"
+ ansible.builtin.setup:
+ filter:
+ - ansible_interfaces
+ - ansible_default_ipv4
+
+ - name: "Bring up all ethernet-like interfaces"
+ command: ip link set dev {{ item }} up
+ loop: "{{ ansible_interfaces | select('match','^e(th|n)') | list }}"
+ ignore_errors: yes
+
+ - name: "Determine PXE interface"
+ set_fact:
+ pxe_iface: >-
+ {{ (ansible_interfaces
+ | select('match','^e(th|n)')
+ | reject('equalto','lo')
+ | reject('equalto', ansible_default_ipv4.interface | default(''))
+ | list
+ )
+ | first
+ | default(ansible_default_ipv4.interface | default(
+ ansible_interfaces | select('match','^e(th|n)') | first | default('eth0')
+ )) }}
+
+ - name: "Debug: final pxe_iface choice"
+ debug:
+ msg: "Using {{ pxe_iface }} for DHCP/TFTP"
+
+ - name: "Configure dnsmasq for DHCP and TFTP"
+ copy:
+ dest: /etc/dnsmasq.conf
+ backup: yes
+ content: |
+ port=0
+ interface={{ pxe_iface }}
+ bind-interfaces
+ dhcp-range=10.9.100.10,10.9.100.100,12h
+ dhcp-option=3,10.9.100.1
+ dhcp-option=6,8.8.8.8
+ enable-tftp
+ tftp-root={{ tftp_dir }}
+ dhcp-boot=ipxe.efi
+
+ - name: "Create TFTP directory"
+ file:
+ path: "{{ tftp_dir }}"
+ state: directory
+ mode: '0755'
+ owner: nobody
+ group: nogroup
+
+ - name: "Create Win11 directory structure"
+ file:
+ path: "{{ web_root }}/win11/{{ item }}"
+ state: directory
+ mode: '0755'
+ loop:
+ - "EFI/Boot"
+ - "EFI/Microsoft/Boot"
+ - "Boot"
+ - "sources"
+
+ - name: "Create Altiris iPXE directory"
+ file:
+ path: "{{ web_root }}/Altiris/iPXE"
+ state: directory
+ mode: '0755'
+
+ - name: "Create boot tool directories"
+ file:
+ path: "{{ web_root }}/{{ item }}"
+ state: directory
+ mode: '0755'
+ loop:
+ - clonezilla
+ - blancco
+ - memtest
+
+ - name: "Create GetPxeScript.aspx (iPXE boot menu)"
+ copy:
+ dest: "{{ web_root }}/Altiris/iPXE/GetPxeScript.aspx"
+ backup: yes
+ content: |
+ #!ipxe
+
+ set server 10.9.100.1
+
+ :menu
+ menu GE Aerospace PXE Boot Menu
+ item --gap -- ---- Windows Deployment ----
+ item winpe Windows PE (Image Deployment)
+ item --gap -- ---- Utilities ----
+ item clonezilla Clonezilla Live (Disk Imaging)
+ item blancco Blancco Drive Eraser
+ item memtest Memtest86+ (Memory Diagnostics)
+ item --gap -- ----
+ item reboot Reboot
+ item exit Exit to BIOS
+ choose --default winpe --timeout 30000 target && goto ${target}
+
+ :winpe
+ kernel http://${server}/win11/wimboot gui
+ initrd http://${server}/win11/EFI/Microsoft/Boot/boot.stl EFI/Microsoft/Boot/Boot.stl
+ initrd http://${server}/win11/EFI/Microsoft/Boot/BCD EFI/Microsoft/Boot/BCD
+ initrd http://${server}/win11/EFI/Boot/bootx64.efi EFI/Boot/bootx64.efi
+ initrd http://${server}/win11/Boot/boot.sdi Boot/boot.sdi
+ initrd http://${server}/win11/sources/boot.wim sources/boot.wim
+ boot
+
+ :clonezilla
+ set base http://${server}/clonezilla
+ kernel ${base}/vmlinuz boot=live username=user union=overlay config components noswap edd=on nomodeset nodmraid locales= keyboard-layouts= ocs_live_run="ocs-live-general" ocs_live_extra_param="" ocs_live_batch=no net.ifnames=0 nosplash noprompt fetch=${base}/filesystem.squashfs
+ initrd ${base}/initrd.img
+ boot
+
+ :blancco
+ set bbase http://${server}/blancco
+ kernel ${bbase}/vmlinuz-bde-linux archisobasedir=arch archiso_http_srv=http://${server}/blancco/ copytoram=y cow_spacesize=50% memtest=00 vmalloc=400M ip=dhcp quiet nomodeset libata.allow_tpm=1
+ initrd ${bbase}/intel-ucode.img ${bbase}/amd-ucode.img ${bbase}/config.img ${bbase}/initramfs-bde-linux.img
+ boot
+
+ :memtest
+ kernel http://${server}/memtest/memtest.efi
+ boot
+
+ :reboot
+ reboot
+
+ :exit
+ exit
+
+ - name: "Ensure Apache listens on port 4433"
+ lineinfile:
+ path: /etc/apache2/ports.conf
+ line: "Listen 4433"
+ backup: yes
+ state: present
+
+ - name: "Create VirtualHost for Altiris iPXE on 4433"
+ copy:
+ dest: /etc/apache2/sites-available/altiris-ipxe.conf
+ backup: yes
+ content: |
+
+ DocumentRoot {{ web_root }}
+
+ Options Indexes FollowSymLinks
+ AllowOverride None
+ Require all granted
+ AddType text/plain .aspx
+
+
+
+ - name: "Enable Altiris iPXE site"
+ command: a2ensite altiris-ipxe.conf
+ args:
+ creates: /etc/apache2/sites-enabled/altiris-ipxe.conf
+
+ - name: "Reload Apache to apply changes"
+ systemd:
+ name: apache2
+ state: reloaded
+
+ - name: "Create Samba share root"
+ file:
+ path: "{{ samba_share }}"
+ state: directory
+ mode: '0777'
+
+ - name: "Create Clonezilla backup share directory"
+ file:
+ path: /srv/samba/clonezilla
+ state: directory
+ mode: '0777'
+
+ - name: "Create Blancco reports share directory"
+ file:
+ path: /srv/samba/blancco-reports
+ state: directory
+ mode: '0777'
+
+ - name: "Configure Samba shares"
+ blockinfile:
+ path: /etc/samba/smb.conf
+ backup: yes
+ block: |
+ [winpeapps]
+ path = {{ samba_share }}
+ browseable = yes
+ read only = no
+ guest ok = yes
+
+ [clonezilla]
+ path = /srv/samba/clonezilla
+ browseable = yes
+ read only = no
+ guest ok = yes
+ comment = Clonezilla backup images
+
+ [blancco-reports]
+ path = /srv/samba/blancco-reports
+ browseable = yes
+ read only = no
+ guest ok = yes
+ comment = Blancco Drive Eraser reports
+
+ - name: "Create image-type top-level directories"
+ file:
+ path: "{{ samba_share }}/{{ item }}"
+ state: directory
+ mode: '0777'
+ loop: "{{ image_types }}"
+
+ - name: "Create Deploy subdirectories for each image type"
+ file:
+ path: "{{ samba_share }}/{{ item.0 }}/Deploy/{{ item.1 }}"
+ state: directory
+ mode: '0777'
+ with_nested:
+ - "{{ image_types }}"
+ - "{{ deploy_subdirs }}"
+
+ - name: "Copy WinPE & boot files from USB"
+ copy:
+ src: "{{ usb_mount }}/{{ item.src }}"
+ dest: "{{ web_root }}/win11/{{ item.dest }}"
+ mode: '0644'
+ loop:
+ - { src: "wimboot", dest: "wimboot" }
+ - { src: "boot.stl", dest: "EFI/Microsoft/Boot/boot.stl" }
+ - { src: "BCD", dest: "EFI/Microsoft/Boot/BCD" }
+ - { src: "bootx64.efi", dest: "EFI/Boot/bootx64.efi" }
+ - { src: "boot.sdi", dest: "Boot/boot.sdi" }
+ - { src: "boot.wim", dest: "sources/boot.wim" }
+
+ - name: "Copy iPXE binaries from USB"
+ copy:
+ src: "{{ usb_mount }}/{{ item }}"
+ dest: "{{ tftp_dir }}/{{ item }}"
+ mode: '0755'
+ loop:
+ - ipxe.efi
+
+ - 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
+ loop:
+ - clonezilla
+ - blancco
+ - memtest
+
+ - name: "Check for WinPE deployment content on USB"
+ stat:
+ path: "{{ usb_mount }}/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
+ loop: "{{ image_types }}"
+ when: usb_images_dir.stat.exists
+
+ - name: "Restart and enable services"
+ systemd:
+ name: "{{ item }}"
+ state: restarted
+ enabled: yes
+ loop:
+ - dnsmasq
+ - apache2
+ - smbd
+
+ - name: "Allow necessary firewall ports (UFW)"
+ ufw:
+ rule: allow
+ port: "{{ item }}"
+ proto: "{{ 'udp' if item in ['67','69'] else 'tcp' }}"
+ loop:
+ - 67
+ - 69
+ - 80
+ - 4433
+ - 445
+ - 9009
+
+ - name: "Enable UFW firewall"
+ ufw:
+ state: enabled
+ policy: deny
+
+ - name: "Schedule dnsmasq restart 15s after reboot"
+ cron:
+ name: "Restart dnsmasq after reboot"
+ user: root
+ special_time: "reboot"
+ job: "/bin/sleep 15 && /usr/bin/systemctl restart dnsmasq.service"
+
+ # --- Web Management App (Flask) ---
+ - name: "Create webapp directory"
+ file:
+ path: /opt/pxe-webapp
+ state: directory
+ mode: '0755'
+
+ - 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
+ args:
+ creates: /opt/pxe-webapp/app.py
+
+ - name: "Create Python virtual environment for webapp"
+ command: python3 -m venv /opt/pxe-webapp/venv
+ args:
+ creates: /opt/pxe-webapp/venv/bin/python
+
+ - name: "Install webapp Python dependencies (offline wheels)"
+ shell: >
+ /opt/pxe-webapp/venv/bin/pip install --no-index
+ --find-links="{{ usb_mount }}/../pip-wheels/"
+ --find-links="{{ usb_mount }}/pip-wheels/"
+ -r /opt/pxe-webapp/requirements.txt 2>/dev/null ||
+ /opt/pxe-webapp/venv/bin/pip install -r /opt/pxe-webapp/requirements.txt
+
+ - name: "Create systemd service for PXE webapp"
+ copy:
+ dest: /etc/systemd/system/pxe-webapp.service
+ content: |
+ [Unit]
+ Description=PXE Server Web Management
+ After=network.target apache2.service
+
+ [Service]
+ Type=simple
+ User=root
+ WorkingDirectory=/opt/pxe-webapp
+ Environment=SAMBA_SHARE={{ samba_share }}
+ Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla
+ Environment=WEB_ROOT={{ web_root }}
+ Environment=BLANCCO_REPORTS=/srv/samba/blancco-reports
+ Environment=AUDIT_LOG=/var/log/pxe-webapp-audit.log
+ ExecStart=/opt/pxe-webapp/venv/bin/python app.py
+ Restart=always
+ RestartSec=5
+
+ [Install]
+ WantedBy=multi-user.target
+
+ - name: "Enable and start PXE webapp service"
+ systemd:
+ name: pxe-webapp
+ state: started
+ enabled: yes
+ daemon_reload: yes
+
+ - name: "Configure Apache reverse proxy for webapp"
+ copy:
+ dest: /etc/apache2/sites-available/pxe-webapp.conf
+ content: |
+
+ ProxyPreserveHost On
+ ProxyPass /manage http://127.0.0.1:9009/
+ ProxyPassReverse /manage http://127.0.0.1:9009/
+
+
+ - name: "Enable Apache proxy modules"
+ command: a2enmod proxy proxy_http
+ args:
+ creates: /etc/apache2/mods-enabled/proxy.load
+
+ - name: "Enable webapp Apache site"
+ command: a2ensite pxe-webapp.conf
+ args:
+ creates: /etc/apache2/sites-enabled/pxe-webapp.conf
+
+ - name: "Configure static IP for PXE interface"
+ copy:
+ dest: /etc/netplan/50-cloud-init.yaml
+ backup: yes
+ content: |
+ network:
+ version: 2
+ renderer: networkd
+ ethernets:
+ {{ pxe_iface }}:
+ dhcp4: no
+ addresses: [10.9.100.1/24]
+ notify: "Apply netplan"
+
+ handlers:
+ - name: "Apply netplan"
+ command: netplan apply
diff --git a/prepare-boot-tools.sh b/prepare-boot-tools.sh
index f4ce00c..d1b3cba 100755
--- a/prepare-boot-tools.sh
+++ b/prepare-boot-tools.sh
@@ -1,197 +1,197 @@
-#!/bin/bash
-#
-# prepare-boot-tools.sh — Download/extract boot files for PXE boot tools
-#
-# Downloads Clonezilla Live and Memtest86+ for PXE booting,
-# and extracts Blancco Drive Eraser from its ISO.
-#
-# Usage:
-# ./prepare-boot-tools.sh [/path/to/blancco.iso]
-#
-# Output directories:
-# boot-tools/clonezilla/ — vmlinuz, initrd.img, filesystem.squashfs
-# boot-tools/blancco/ — extracted boot files or ISO for memdisk
-# boot-tools/memtest/ — memtest.efi
-
-set -euo pipefail
-
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-OUT_DIR="$SCRIPT_DIR/boot-tools"
-BLANCCO_ISO="${1:-}"
-
-# Auto-detect Blancco ISO in project directory
-if [ -z "$BLANCCO_ISO" ]; then
- BLANCCO_ISO=$(find "$SCRIPT_DIR" -maxdepth 1 -name '*DriveEraser*.iso' -o -name '*blancco*.iso' 2>/dev/null | head -1)
-fi
-
-mkdir -p "$OUT_DIR"/{clonezilla,blancco,memtest}
-
-echo "============================================"
-echo "PXE Boot Tools Preparation"
-echo "============================================"
-
-# --- Clonezilla Live ---
-echo ""
-echo "[1/3] Clonezilla Live"
-
-CLONEZILLA_VERSION="3.2.1-6"
-CLONEZILLA_FILE="clonezilla-live-${CLONEZILLA_VERSION}-amd64.zip"
-CLONEZILLA_URL="https://sourceforge.net/projects/clonezilla/files/clonezilla_live_stable/${CLONEZILLA_VERSION}/${CLONEZILLA_FILE}/download"
-
-if [ -f "$OUT_DIR/clonezilla/vmlinuz" ] && [ -f "$OUT_DIR/clonezilla/filesystem.squashfs" ]; then
- echo " Already prepared, skipping. Delete boot-tools/clonezilla/ to re-download."
-else
- echo " Downloading Clonezilla Live ${CLONEZILLA_VERSION}..."
- TMPDIR=$(mktemp -d)
-
- wget -q --show-progress -O "$TMPDIR/$CLONEZILLA_FILE" "$CLONEZILLA_URL" || {
- echo " ERROR: Download failed. Trying alternative URL..."
- # Fallback: try OSDN mirror
- wget -q --show-progress -O "$TMPDIR/$CLONEZILLA_FILE" \
- "https://free.nchc.org.tw/clonezilla-live/stable/${CLONEZILLA_FILE}" || {
- echo " ERROR: Could not download Clonezilla. Download manually and place in boot-tools/clonezilla/"
- echo " Need: vmlinuz, initrd.img, filesystem.squashfs from the live ZIP"
- }
- }
-
- if [ -f "$TMPDIR/$CLONEZILLA_FILE" ]; then
- echo " Extracting PXE boot files..."
- unzip -o -j "$TMPDIR/$CLONEZILLA_FILE" "live/vmlinuz" -d "$OUT_DIR/clonezilla/"
- unzip -o -j "$TMPDIR/$CLONEZILLA_FILE" "live/initrd.img" -d "$OUT_DIR/clonezilla/"
- unzip -o -j "$TMPDIR/$CLONEZILLA_FILE" "live/filesystem.squashfs" -d "$OUT_DIR/clonezilla/"
- rm -rf "$TMPDIR"
- echo " Done."
- fi
-fi
-
-ls -lh "$OUT_DIR/clonezilla/" 2>/dev/null | grep -E 'vmlinuz|initrd|squashfs' | sed 's/^/ /'
-
-# --- Blancco Drive Eraser ---
-echo ""
-echo "[2/3] Blancco Drive Eraser"
-
-if [ -n "$BLANCCO_ISO" ] && [ -f "$BLANCCO_ISO" ]; then
- echo " Extracting from: $BLANCCO_ISO"
- echo " Using 7z to extract (no root required)..."
-
- # Blancco is Arch Linux-based. We need:
- # arch/boot/x86_64/vmlinuz-bde-linux
- # arch/boot/x86_64/initramfs-bde-linux.img
- # arch/boot/intel-ucode.img
- # arch/boot/amd-ucode.img
- # arch/boot/config.img
- # arch/x86_64/airootfs.sfs
- TMPDIR=$(mktemp -d)
- 7z x -o"$TMPDIR" "$BLANCCO_ISO" \
- "arch/boot/x86_64/vmlinuz-bde-linux" \
- "arch/boot/x86_64/initramfs-bde-linux.img" \
- "arch/boot/intel-ucode.img" \
- "arch/boot/amd-ucode.img" \
- "arch/boot/config.img" \
- "arch/x86_64/airootfs.sfs" \
- -r 2>/dev/null || {
- echo " 7z extraction failed. Install p7zip-full: apt install p7zip-full"
- }
-
- # Flatten into blancco/ directory for HTTP serving
- if [ -f "$TMPDIR/arch/boot/x86_64/vmlinuz-bde-linux" ]; then
- cp "$TMPDIR/arch/boot/x86_64/vmlinuz-bde-linux" "$OUT_DIR/blancco/"
- cp "$TMPDIR/arch/boot/x86_64/initramfs-bde-linux.img" "$OUT_DIR/blancco/"
- cp "$TMPDIR/arch/boot/intel-ucode.img" "$OUT_DIR/blancco/"
- cp "$TMPDIR/arch/boot/amd-ucode.img" "$OUT_DIR/blancco/"
- cp "$TMPDIR/arch/boot/config.img" "$OUT_DIR/blancco/"
- # airootfs.sfs needs to be in arch/x86_64/ path relative to HTTP root
- mkdir -p "$OUT_DIR/blancco/arch/x86_64"
- cp "$TMPDIR/arch/x86_64/airootfs.sfs" "$OUT_DIR/blancco/arch/x86_64/"
- echo " Extracted Blancco boot files."
-
- # Patch config.img to auto-save reports to PXE server Samba share
- if [ -f "$OUT_DIR/blancco/config.img" ]; then
- echo " Patching config.img for network report storage..."
- CFGTMP=$(mktemp -d)
- cd "$CFGTMP"
- cpio -id < "$OUT_DIR/blancco/config.img" 2>/dev/null
-
- if [ -f "$CFGTMP/preferences.xml" ]; then
- # Set network share to PXE server's blancco-reports Samba share
- sed -i 's||10.9.100.1|' "$CFGTMP/preferences.xml"
- sed -i 's||blancco-reports|' "$CFGTMP/preferences.xml"
- # Enable auto-backup of reports to the network share
- sed -i 's|false|true|' "$CFGTMP/preferences.xml"
-
- # Repack config.img
- ls -1 | cpio -o -H newc > "$OUT_DIR/blancco/config.img" 2>/dev/null
- echo " Reports will auto-save to \\\\10.9.100.1\\blancco-reports"
- fi
- cd "$SCRIPT_DIR"
- rm -rf "$CFGTMP"
- fi
- else
- echo " Could not extract boot files from ISO."
- fi
- rm -rf "$TMPDIR"
-else
- echo " No Blancco ISO found. Provide path as argument or place in project directory."
- echo " Usage: $0 /path/to/DriveEraser.iso"
-fi
-
-ls -lh "$OUT_DIR/blancco/" 2>/dev/null | grep -v '^total' | sed 's/^/ /'
-
-# --- Memtest86+ ---
-echo ""
-echo "[3/3] Memtest86+"
-
-MEMTEST_VERSION="7.20"
-MEMTEST_URL="https://memtest.org/download/${MEMTEST_VERSION}/mt86plus_${MEMTEST_VERSION}.binaries.zip"
-
-if [ -f "$OUT_DIR/memtest/memtest.efi" ]; then
- echo " Already prepared, skipping."
-else
- echo " Downloading Memtest86+ v${MEMTEST_VERSION}..."
- TMPDIR=$(mktemp -d)
-
- wget -q --show-progress -O "$TMPDIR/memtest.zip" "$MEMTEST_URL" || {
- echo " ERROR: Download failed. Download manually from https://memtest.org"
- TMPDIR=""
- }
-
- if [ -n "$TMPDIR" ] && [ -f "$TMPDIR/memtest.zip" ]; then
- echo " Extracting EFI binary..."
- unzip -o -j "$TMPDIR/memtest.zip" "memtest64.efi" -d "$OUT_DIR/memtest/" 2>/dev/null || \
- unzip -o -j "$TMPDIR/memtest.zip" "mt86plus_${MEMTEST_VERSION}.x64.efi" -d "$OUT_DIR/memtest/" 2>/dev/null || \
- unzip -o "$TMPDIR/memtest.zip" -d "$TMPDIR/extract/"
-
- # Find the EFI file regardless of exact name
- EFI_FILE=$(find "$TMPDIR" "$OUT_DIR/memtest" -name '*.efi' -name '*64*' 2>/dev/null | head -1)
- if [ -n "$EFI_FILE" ] && [ ! -f "$OUT_DIR/memtest/memtest.efi" ]; then
- cp "$EFI_FILE" "$OUT_DIR/memtest/memtest.efi"
- fi
- rm -rf "$TMPDIR"
- echo " Done."
- fi
-fi
-
-ls -lh "$OUT_DIR/memtest/" 2>/dev/null | grep -v '^total' | sed 's/^/ /'
-
-# --- Summary ---
-echo ""
-echo "============================================"
-echo "Boot tools prepared in: $OUT_DIR/"
-echo "============================================"
-echo ""
-
-for tool in clonezilla blancco memtest; do
- COUNT=$(find "$OUT_DIR/$tool" -type f 2>/dev/null | wc -l)
- SIZE=$(du -sh "$OUT_DIR/$tool" 2>/dev/null | cut -f1)
- printf " %-15s %s (%d files)\n" "$tool" "$SIZE" "$COUNT"
-done
-
-echo ""
-echo "These files need to be copied to the PXE server's web root:"
-echo " /var/www/html/clonezilla/"
-echo " /var/www/html/blancco/"
-echo " /var/www/html/memtest/"
-echo ""
-echo "The build-usb.sh script will include them automatically,"
-echo "or copy them manually to the server."
-echo ""
+#!/bin/bash
+#
+# prepare-boot-tools.sh — Download/extract boot files for PXE boot tools
+#
+# Downloads Clonezilla Live and Memtest86+ for PXE booting,
+# and extracts Blancco Drive Eraser from its ISO.
+#
+# Usage:
+# ./prepare-boot-tools.sh [/path/to/blancco.iso]
+#
+# Output directories:
+# boot-tools/clonezilla/ — vmlinuz, initrd.img, filesystem.squashfs
+# boot-tools/blancco/ — extracted boot files or ISO for memdisk
+# boot-tools/memtest/ — memtest.efi
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+OUT_DIR="$SCRIPT_DIR/boot-tools"
+BLANCCO_ISO="${1:-}"
+
+# Auto-detect Blancco ISO in project directory
+if [ -z "$BLANCCO_ISO" ]; then
+ BLANCCO_ISO=$(find "$SCRIPT_DIR" -maxdepth 1 -name '*DriveEraser*.iso' -o -name '*blancco*.iso' 2>/dev/null | head -1)
+fi
+
+mkdir -p "$OUT_DIR"/{clonezilla,blancco,memtest}
+
+echo "============================================"
+echo "PXE Boot Tools Preparation"
+echo "============================================"
+
+# --- Clonezilla Live ---
+echo ""
+echo "[1/3] Clonezilla Live"
+
+CLONEZILLA_VERSION="3.2.1-6"
+CLONEZILLA_FILE="clonezilla-live-${CLONEZILLA_VERSION}-amd64.zip"
+CLONEZILLA_URL="https://sourceforge.net/projects/clonezilla/files/clonezilla_live_stable/${CLONEZILLA_VERSION}/${CLONEZILLA_FILE}/download"
+
+if [ -f "$OUT_DIR/clonezilla/vmlinuz" ] && [ -f "$OUT_DIR/clonezilla/filesystem.squashfs" ]; then
+ echo " Already prepared, skipping. Delete boot-tools/clonezilla/ to re-download."
+else
+ echo " Downloading Clonezilla Live ${CLONEZILLA_VERSION}..."
+ TMPDIR=$(mktemp -d)
+
+ wget -q --show-progress -O "$TMPDIR/$CLONEZILLA_FILE" "$CLONEZILLA_URL" || {
+ echo " ERROR: Download failed. Trying alternative URL..."
+ # Fallback: try OSDN mirror
+ wget -q --show-progress -O "$TMPDIR/$CLONEZILLA_FILE" \
+ "https://free.nchc.org.tw/clonezilla-live/stable/${CLONEZILLA_FILE}" || {
+ echo " ERROR: Could not download Clonezilla. Download manually and place in boot-tools/clonezilla/"
+ echo " Need: vmlinuz, initrd.img, filesystem.squashfs from the live ZIP"
+ }
+ }
+
+ if [ -f "$TMPDIR/$CLONEZILLA_FILE" ]; then
+ echo " Extracting PXE boot files..."
+ unzip -o -j "$TMPDIR/$CLONEZILLA_FILE" "live/vmlinuz" -d "$OUT_DIR/clonezilla/"
+ unzip -o -j "$TMPDIR/$CLONEZILLA_FILE" "live/initrd.img" -d "$OUT_DIR/clonezilla/"
+ unzip -o -j "$TMPDIR/$CLONEZILLA_FILE" "live/filesystem.squashfs" -d "$OUT_DIR/clonezilla/"
+ rm -rf "$TMPDIR"
+ echo " Done."
+ fi
+fi
+
+ls -lh "$OUT_DIR/clonezilla/" 2>/dev/null | grep -E 'vmlinuz|initrd|squashfs' | sed 's/^/ /'
+
+# --- Blancco Drive Eraser ---
+echo ""
+echo "[2/3] Blancco Drive Eraser"
+
+if [ -n "$BLANCCO_ISO" ] && [ -f "$BLANCCO_ISO" ]; then
+ echo " Extracting from: $BLANCCO_ISO"
+ echo " Using 7z to extract (no root required)..."
+
+ # Blancco is Arch Linux-based. We need:
+ # arch/boot/x86_64/vmlinuz-bde-linux
+ # arch/boot/x86_64/initramfs-bde-linux.img
+ # arch/boot/intel-ucode.img
+ # arch/boot/amd-ucode.img
+ # arch/boot/config.img
+ # arch/x86_64/airootfs.sfs
+ TMPDIR=$(mktemp -d)
+ 7z x -o"$TMPDIR" "$BLANCCO_ISO" \
+ "arch/boot/x86_64/vmlinuz-bde-linux" \
+ "arch/boot/x86_64/initramfs-bde-linux.img" \
+ "arch/boot/intel-ucode.img" \
+ "arch/boot/amd-ucode.img" \
+ "arch/boot/config.img" \
+ "arch/x86_64/airootfs.sfs" \
+ -r 2>/dev/null || {
+ echo " 7z extraction failed. Install p7zip-full: apt install p7zip-full"
+ }
+
+ # Flatten into blancco/ directory for HTTP serving
+ if [ -f "$TMPDIR/arch/boot/x86_64/vmlinuz-bde-linux" ]; then
+ cp "$TMPDIR/arch/boot/x86_64/vmlinuz-bde-linux" "$OUT_DIR/blancco/"
+ cp "$TMPDIR/arch/boot/x86_64/initramfs-bde-linux.img" "$OUT_DIR/blancco/"
+ cp "$TMPDIR/arch/boot/intel-ucode.img" "$OUT_DIR/blancco/"
+ cp "$TMPDIR/arch/boot/amd-ucode.img" "$OUT_DIR/blancco/"
+ cp "$TMPDIR/arch/boot/config.img" "$OUT_DIR/blancco/"
+ # airootfs.sfs needs to be in arch/x86_64/ path relative to HTTP root
+ mkdir -p "$OUT_DIR/blancco/arch/x86_64"
+ cp "$TMPDIR/arch/x86_64/airootfs.sfs" "$OUT_DIR/blancco/arch/x86_64/"
+ echo " Extracted Blancco boot files."
+
+ # Patch config.img to auto-save reports to PXE server Samba share
+ if [ -f "$OUT_DIR/blancco/config.img" ]; then
+ echo " Patching config.img for network report storage..."
+ CFGTMP=$(mktemp -d)
+ cd "$CFGTMP"
+ cpio -id < "$OUT_DIR/blancco/config.img" 2>/dev/null
+
+ if [ -f "$CFGTMP/preferences.xml" ]; then
+ # Set network share to PXE server's blancco-reports Samba share
+ sed -i 's||10.9.100.1|' "$CFGTMP/preferences.xml"
+ sed -i 's||blancco-reports|' "$CFGTMP/preferences.xml"
+ # Enable auto-backup of reports to the network share
+ sed -i 's|false|true|' "$CFGTMP/preferences.xml"
+
+ # Repack config.img
+ ls -1 | cpio -o -H newc > "$OUT_DIR/blancco/config.img" 2>/dev/null
+ echo " Reports will auto-save to \\\\10.9.100.1\\blancco-reports"
+ fi
+ cd "$SCRIPT_DIR"
+ rm -rf "$CFGTMP"
+ fi
+ else
+ echo " Could not extract boot files from ISO."
+ fi
+ rm -rf "$TMPDIR"
+else
+ echo " No Blancco ISO found. Provide path as argument or place in project directory."
+ echo " Usage: $0 /path/to/DriveEraser.iso"
+fi
+
+ls -lh "$OUT_DIR/blancco/" 2>/dev/null | grep -v '^total' | sed 's/^/ /'
+
+# --- Memtest86+ ---
+echo ""
+echo "[3/3] Memtest86+"
+
+MEMTEST_VERSION="7.20"
+MEMTEST_URL="https://memtest.org/download/${MEMTEST_VERSION}/mt86plus_${MEMTEST_VERSION}.binaries.zip"
+
+if [ -f "$OUT_DIR/memtest/memtest.efi" ]; then
+ echo " Already prepared, skipping."
+else
+ echo " Downloading Memtest86+ v${MEMTEST_VERSION}..."
+ TMPDIR=$(mktemp -d)
+
+ wget -q --show-progress -O "$TMPDIR/memtest.zip" "$MEMTEST_URL" || {
+ echo " ERROR: Download failed. Download manually from https://memtest.org"
+ TMPDIR=""
+ }
+
+ if [ -n "$TMPDIR" ] && [ -f "$TMPDIR/memtest.zip" ]; then
+ echo " Extracting EFI binary..."
+ unzip -o -j "$TMPDIR/memtest.zip" "memtest64.efi" -d "$OUT_DIR/memtest/" 2>/dev/null || \
+ unzip -o -j "$TMPDIR/memtest.zip" "mt86plus_${MEMTEST_VERSION}.x64.efi" -d "$OUT_DIR/memtest/" 2>/dev/null || \
+ unzip -o "$TMPDIR/memtest.zip" -d "$TMPDIR/extract/"
+
+ # Find the EFI file regardless of exact name
+ EFI_FILE=$(find "$TMPDIR" "$OUT_DIR/memtest" -name '*.efi' -name '*64*' 2>/dev/null | head -1)
+ if [ -n "$EFI_FILE" ] && [ ! -f "$OUT_DIR/memtest/memtest.efi" ]; then
+ cp "$EFI_FILE" "$OUT_DIR/memtest/memtest.efi"
+ fi
+ rm -rf "$TMPDIR"
+ echo " Done."
+ fi
+fi
+
+ls -lh "$OUT_DIR/memtest/" 2>/dev/null | grep -v '^total' | sed 's/^/ /'
+
+# --- Summary ---
+echo ""
+echo "============================================"
+echo "Boot tools prepared in: $OUT_DIR/"
+echo "============================================"
+echo ""
+
+for tool in clonezilla blancco memtest; do
+ COUNT=$(find "$OUT_DIR/$tool" -type f 2>/dev/null | wc -l)
+ SIZE=$(du -sh "$OUT_DIR/$tool" 2>/dev/null | cut -f1)
+ printf " %-15s %s (%d files)\n" "$tool" "$SIZE" "$COUNT"
+done
+
+echo ""
+echo "These files need to be copied to the PXE server's web root:"
+echo " /var/www/html/clonezilla/"
+echo " /var/www/html/blancco/"
+echo " /var/www/html/memtest/"
+echo ""
+echo "The build-usb.sh script will include them automatically,"
+echo "or copy them manually to the server."
+echo ""
diff --git a/test-vm.sh b/test-vm.sh
index 81a2e6e..e52e996 100755
--- a/test-vm.sh
+++ b/test-vm.sh
@@ -1,174 +1,188 @@
-#!/bin/bash
-#
-# test-vm.sh — Create a test VM to validate the PXE server setup
-#
-# This script:
-# 1. Builds a CIDATA ISO with autoinstall config, packages, playbook, and webapp
-# 2. Creates an isolated libvirt network (pxe-test, 10.9.100.0/24)
-# 3. Launches an Ubuntu 24.04 Server VM that auto-installs and configures itself
-#
-# Usage:
-# ./test-vm.sh /path/to/ubuntu-24.04-live-server-amd64.iso
-#
-# After install completes (~10-15 min), access the webapp at:
-# http://10.9.100.1:9009
-#
-# To watch progress:
-# virsh console pxe-test
-#
-# To clean up:
-# ./test-vm.sh --destroy
-
-set -euo pipefail
-
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-VM_NAME="pxe-test"
-NET_NAME="pxe-test"
-VM_DISK="/var/lib/libvirt/images/${VM_NAME}.qcow2"
-CIDATA_ISO="/tmp/${VM_NAME}-cidata.iso"
-VM_RAM=4096
-VM_CPUS=2
-VM_DISK_SIZE=40 # GB
-
-# --- Handle --destroy flag ---
-if [ "${1:-}" = "--destroy" ]; then
- echo "Destroying test environment..."
- virsh destroy "$VM_NAME" 2>/dev/null || true
- virsh undefine "$VM_NAME" --remove-all-storage 2>/dev/null || true
- virsh net-destroy "$NET_NAME" 2>/dev/null || true
- virsh net-undefine "$NET_NAME" 2>/dev/null || true
- rm -f "$CIDATA_ISO"
- echo "Done."
- exit 0
-fi
-
-# --- Validate Ubuntu ISO argument ---
-UBUNTU_ISO="${1:-}"
-if [ -z "$UBUNTU_ISO" ] || [ ! -f "$UBUNTU_ISO" ]; then
- echo "Usage: $0 /path/to/ubuntu-24.04-live-server-amd64.iso"
- echo ""
- echo "Download from: https://ubuntu.com/download/server"
- echo ""
- echo "Other commands:"
- echo " $0 --destroy Remove the test VM and network"
- exit 1
-fi
-
-echo "============================================"
-echo "PXE Server Test VM Setup"
-echo "============================================"
-echo ""
-
-# --- Step 1: Build CIDATA ISO ---
-echo "[1/4] Building CIDATA ISO..."
-CIDATA_DIR=$(mktemp -d)
-
-# Autoinstall config
-cp "$SCRIPT_DIR/autoinstall/user-data" "$CIDATA_DIR/user-data"
-touch "$CIDATA_DIR/meta-data"
-
-# Offline .deb packages
-if [ -d "$SCRIPT_DIR/offline-packages" ]; then
- mkdir -p "$CIDATA_DIR/packages"
- cp "$SCRIPT_DIR/offline-packages/"*.deb "$CIDATA_DIR/packages/" 2>/dev/null || true
- echo " Copied $(ls -1 "$CIDATA_DIR/packages/"*.deb 2>/dev/null | wc -l) .deb packages"
-else
- echo " WARNING: No offline-packages/ directory. Run download-packages.sh first."
-fi
-
-# Ansible playbook
-mkdir -p "$CIDATA_DIR/playbook"
-cp "$SCRIPT_DIR/playbook/"* "$CIDATA_DIR/playbook/" 2>/dev/null || true
-echo " Copied playbook/"
-
-# Webapp
-if [ -d "$SCRIPT_DIR/webapp" ]; then
- mkdir -p "$CIDATA_DIR/webapp"
- cp "$SCRIPT_DIR/webapp/app.py" "$SCRIPT_DIR/webapp/requirements.txt" "$CIDATA_DIR/webapp/"
- cp -r "$SCRIPT_DIR/webapp/templates" "$SCRIPT_DIR/webapp/static" "$CIDATA_DIR/webapp/"
- echo " Copied webapp/"
-fi
-
-# Pip wheels
-if [ -d "$SCRIPT_DIR/pip-wheels" ]; then
- cp -r "$SCRIPT_DIR/pip-wheels" "$CIDATA_DIR/pip-wheels"
- echo " Copied pip-wheels/"
-fi
-
-# Boot tools
-if [ -d "$SCRIPT_DIR/boot-tools" ]; then
- cp -r "$SCRIPT_DIR/boot-tools" "$CIDATA_DIR/boot-tools"
- echo " Copied boot-tools/"
-fi
-
-# Generate the CIDATA ISO
-genisoimage -output "$CIDATA_ISO" -volid CIDATA -joliet -rock "$CIDATA_DIR" 2>/dev/null
-CIDATA_SIZE=$(du -sh "$CIDATA_ISO" | cut -f1)
-echo " CIDATA ISO: $CIDATA_ISO ($CIDATA_SIZE)"
-rm -rf "$CIDATA_DIR"
-
-# --- Step 2: Create isolated network ---
-echo ""
-echo "[2/4] Setting up isolated network ($NET_NAME)..."
-
-# Check if network already exists
-if virsh net-info "$NET_NAME" &>/dev/null; then
- echo " Network $NET_NAME already exists, reusing."
-else
- cat > /tmp/${NET_NAME}-net.xml <
- ${NET_NAME}
-
-
-
-NETEOF
- virsh net-define /tmp/${NET_NAME}-net.xml
- virsh net-start "$NET_NAME"
- rm -f /tmp/${NET_NAME}-net.xml
- echo " Created isolated network 10.9.100.0/24 (no DHCP, no NAT)"
-fi
-
-# --- Step 3: Create VM disk ---
-echo ""
-echo "[3/4] Creating VM disk (${VM_DISK_SIZE}GB)..."
-if [ -f "$VM_DISK" ]; then
- echo " Disk already exists. Destroy first with: $0 --destroy"
- exit 1
-fi
-qemu-img create -f qcow2 "$VM_DISK" "${VM_DISK_SIZE}G"
-
-# --- Step 4: Launch VM ---
-echo ""
-echo "[4/4] Launching VM ($VM_NAME)..."
-virt-install \
- --name "$VM_NAME" \
- --memory "$VM_RAM" \
- --vcpus "$VM_CPUS" \
- --disk path="$VM_DISK",format=qcow2 \
- --cdrom "$UBUNTU_ISO" \
- --disk path="$CIDATA_ISO",device=cdrom \
- --network network="$NET_NAME" \
- --os-variant ubuntu24.04 \
- --graphics none \
- --console pty,target_type=serial \
- --extra-args "console=ttyS0,115200n8 autoinstall" \
- --noautoconsole
-
-echo ""
-echo "============================================"
-echo "VM launched! The autoinstall will take ~10-15 minutes."
-echo "============================================"
-echo ""
-echo "Watch progress:"
-echo " virsh console $VM_NAME"
-echo " (Press Ctrl+] to detach)"
-echo ""
-echo "After install + first boot:"
-echo " SSH: ssh pxe@10.9.100.1"
-echo " Webapp: http://10.9.100.1:9009"
-echo ""
-echo "Manage:"
-echo " virsh start $VM_NAME"
-echo " virsh shutdown $VM_NAME"
-echo " $0 --destroy (remove everything)"
-echo ""
+#!/bin/bash
+#
+# test-vm.sh — Create a test VM to validate the PXE server setup
+#
+# This script:
+# 1. Builds a CIDATA ISO with autoinstall config, packages, playbook, and webapp
+# 2. Creates an isolated libvirt network (pxe-test, 10.9.100.0/24)
+# 3. Launches an Ubuntu 24.04 Server VM that auto-installs and configures itself
+#
+# Usage:
+# ./test-vm.sh /path/to/ubuntu-24.04-live-server-amd64.iso
+#
+# After install completes (~10-15 min), access the webapp at:
+# http://10.9.100.1:9009
+#
+# To watch progress:
+# virsh console pxe-test
+#
+# To clean up:
+# ./test-vm.sh --destroy
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+VM_NAME="pxe-test"
+NET_NAME="pxe-test"
+VM_DISK="/var/lib/libvirt/images/${VM_NAME}.qcow2"
+CIDATA_ISO="/tmp/${VM_NAME}-cidata.iso"
+VM_RAM=4096
+VM_CPUS=2
+VM_DISK_SIZE=40 # GB
+
+# --- Handle --destroy flag ---
+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 net-destroy "$NET_NAME" 2>/dev/null || true
+ virsh net-undefine "$NET_NAME" 2>/dev/null || true
+ rm -f "$CIDATA_ISO"
+ echo "Done."
+ exit 0
+fi
+
+# --- Validate Ubuntu ISO argument ---
+UBUNTU_ISO="${1:-}"
+if [ -z "$UBUNTU_ISO" ] || [ ! -f "$UBUNTU_ISO" ]; then
+ echo "Usage: $0 /path/to/ubuntu-24.04-live-server-amd64.iso"
+ echo ""
+ echo "Download from: https://ubuntu.com/download/server"
+ echo ""
+ echo "Other commands:"
+ echo " $0 --destroy Remove the test VM and network"
+ exit 1
+fi
+
+echo "============================================"
+echo "PXE Server Test VM Setup"
+echo "============================================"
+echo ""
+
+# --- Step 1: Build CIDATA ISO ---
+echo "[1/4] Building CIDATA ISO..."
+CIDATA_DIR=$(mktemp -d)
+
+# Autoinstall config
+cp "$SCRIPT_DIR/autoinstall/user-data" "$CIDATA_DIR/user-data"
+touch "$CIDATA_DIR/meta-data"
+
+# Offline .deb packages
+if [ -d "$SCRIPT_DIR/offline-packages" ]; then
+ mkdir -p "$CIDATA_DIR/packages"
+ cp "$SCRIPT_DIR/offline-packages/"*.deb "$CIDATA_DIR/packages/" 2>/dev/null || true
+ echo " Copied $(ls -1 "$CIDATA_DIR/packages/"*.deb 2>/dev/null | wc -l) .deb packages"
+else
+ echo " WARNING: No offline-packages/ directory. Run download-packages.sh first."
+fi
+
+# Ansible playbook
+mkdir -p "$CIDATA_DIR/playbook"
+cp "$SCRIPT_DIR/playbook/"* "$CIDATA_DIR/playbook/" 2>/dev/null || true
+echo " Copied playbook/"
+
+# Webapp
+if [ -d "$SCRIPT_DIR/webapp" ]; then
+ mkdir -p "$CIDATA_DIR/webapp"
+ cp "$SCRIPT_DIR/webapp/app.py" "$SCRIPT_DIR/webapp/requirements.txt" "$CIDATA_DIR/webapp/"
+ cp -r "$SCRIPT_DIR/webapp/templates" "$SCRIPT_DIR/webapp/static" "$CIDATA_DIR/webapp/"
+ echo " Copied webapp/"
+fi
+
+# Pip wheels
+if [ -d "$SCRIPT_DIR/pip-wheels" ]; then
+ cp -r "$SCRIPT_DIR/pip-wheels" "$CIDATA_DIR/pip-wheels"
+ echo " Copied pip-wheels/"
+fi
+
+# Boot tools
+if [ -d "$SCRIPT_DIR/boot-tools" ]; then
+ cp -r "$SCRIPT_DIR/boot-tools" "$CIDATA_DIR/boot-tools"
+ echo " Copied boot-tools/"
+fi
+
+# Generate the CIDATA ISO
+genisoimage -output "$CIDATA_ISO" -volid CIDATA -joliet -rock "$CIDATA_DIR" 2>/dev/null
+CIDATA_SIZE=$(du -sh "$CIDATA_ISO" | cut -f1)
+echo " CIDATA ISO: $CIDATA_ISO ($CIDATA_SIZE)"
+rm -rf "$CIDATA_DIR"
+
+# --- Step 2: Create isolated network ---
+echo ""
+echo "[2/4] Setting up isolated network ($NET_NAME)..."
+
+# Check if network already exists
+if virsh net-info "$NET_NAME" &>/dev/null; then
+ echo " Network $NET_NAME already exists, reusing."
+else
+ cat > /tmp/${NET_NAME}-net.xml <
+ ${NET_NAME}
+
+
+
+NETEOF
+ virsh net-define /tmp/${NET_NAME}-net.xml
+ virsh net-start "$NET_NAME"
+ rm -f /tmp/${NET_NAME}-net.xml
+ echo " Created isolated network 10.9.100.0/24 (no DHCP, no NAT)"
+fi
+
+# --- Step 3: Create VM disk ---
+echo ""
+echo "[3/4] Creating VM disk (${VM_DISK_SIZE}GB)..."
+if [ -f "$VM_DISK" ]; then
+ echo " Disk already exists. Destroy first with: $0 --destroy"
+ exit 1
+fi
+qemu-img create -f qcow2 "$VM_DISK" "${VM_DISK_SIZE}G"
+
+# --- Step 4: Extract kernel/initrd from ISO ---
+echo ""
+echo "[4/5] 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"
+echo " Extracted vmlinuz and initrd from casper/"
+
+# --- Step 5: Launch VM ---
+echo ""
+echo "[5/5] Launching VM ($VM_NAME)..."
+virt-install \
+ --name "$VM_NAME" \
+ --memory "$VM_RAM" \
+ --vcpus "$VM_CPUS" \
+ --disk path="$VM_DISK",format=qcow2 \
+ --disk path="$UBUNTU_ISO",device=cdrom,readonly=on \
+ --disk path="$CIDATA_ISO",device=cdrom \
+ --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" \
+ --noautoconsole
+
+echo ""
+echo "============================================"
+echo "VM launched! The autoinstall will take ~10-15 minutes."
+echo "============================================"
+echo ""
+echo "Watch progress:"
+echo " virsh console $VM_NAME"
+echo " (Press Ctrl+] to detach)"
+echo ""
+echo "After install + first boot:"
+echo " SSH: ssh pxe@10.9.100.1"
+echo " Webapp: http://10.9.100.1:9009"
+echo ""
+echo "Manage:"
+echo " virsh start $VM_NAME"
+echo " virsh shutdown $VM_NAME"
+echo " $0 --destroy (remove everything)"
+echo ""