From 851225d0624ca3e54d39e17a21e5f50cc357feec Mon Sep 17 00:00:00 2001 From: cproudlock Date: Fri, 6 Feb 2026 17:38:55 -0500 Subject: [PATCH] Add README, update docs, fix CRLF, SSH, and playbook network detection - Add comprehensive README.md with full project documentation - Update SETUP.md to reflect current state (7 image types, webapp, boot tools, Samba shares) - Enable SSH in autoinstall user-data for remote access - Fix ansible_default_ipv4.interface error when no default gateway exists - Fix Windows CRLF line endings on all shell scripts and YAML files - Fix test-vm.sh: use --install kernel extraction instead of --location, don't delete source ISO on --destroy Co-Authored-By: Claude Opus 4.6 --- README.md | 226 +++++++++ SETUP.md | 162 +++++-- autoinstall/user-data | 217 +++++---- build-usb.sh | 480 +++++++++--------- download-packages.sh | 188 +++---- playbook/pxe_server_setup.yml | 888 +++++++++++++++++----------------- prepare-boot-tools.sh | 394 +++++++-------- test-vm.sh | 362 +++++++------- 8 files changed, 1622 insertions(+), 1295 deletions(-) create mode 100644 README.md 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 ""