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 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-02-06 17:38:55 -05:00
parent 725c8f43de
commit 851225d062
8 changed files with 1622 additions and 1295 deletions

226
README.md Normal file
View File

@@ -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 |

162
SETUP.md
View File

@@ -7,21 +7,23 @@ Automated build process for deploying an Ubuntu-based PXE boot server that hosts
``` ```
Client PXE boot Client PXE boot
-> Broadcom signed iPXE (Secure Boot) -> Broadcom signed iPXE (Secure Boot)
-> wimboot (HTTP from Apache) -> iPXE boot menu (HTTP, port 4433)
-> WinPE (boot.wim) ├── Windows PE -> wimboot -> boot.wim -> startnet.cmd -> Samba -> Image deployment
-> startnet.cmd maps Samba shares ├── Clonezilla -> vmlinuz/initrd -> Disk cloning/imaging
-> GE Aerospace image deployment ├── Blancco -> vmlinuz/initrd -> NIST 800-88 drive erasure (auto-reports)
└── Memtest86+ -> Memory diagnostics
``` ```
### Services on the PXE Server ### Services on the PXE Server
| Service | Port | Purpose | | Service | Port | Purpose |
|----------|----------|--------------------------------------| |-------------|-----------|------------------------------------------|
| dnsmasq | 67/udp | DHCP (10.9.100.10100) | | dnsmasq | 67/udp | DHCP (10.9.100.10-100) |
| dnsmasq | 69/udp | TFTP (serves ipxe.efi) | | dnsmasq | 69/udp | TFTP (serves ipxe.efi) |
| Apache | 80/tcp | HTTP (wimboot, WinPE boot files) | | Apache | 80/tcp | HTTP (wimboot, WinPE boot files, proxy) |
| Apache | 4433/tcp | iPXE boot script (GetPxeScript.aspx) | | Apache | 4433/tcp | iPXE boot script (GetPxeScript.aspx) |
| Samba | 445/tcp | Deployment content shares | | Samba | 445/tcp | Deployment content + backup + reports |
| Flask Webapp| 9009/tcp | Web management interface |
## Prerequisites ## Prerequisites
@@ -31,7 +33,7 @@ Client PXE boot
### Software (on your workstation) ### Software (on your workstation)
- Ubuntu Server 24.04 ISO — https://ubuntu.com/download/server - 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 Aerospace Media Creator LITE (for caching WinPE images)
### GE Access Packages ### GE Access Packages
@@ -41,15 +43,30 @@ Client PXE boot
## Setup Process ## Setup Process
### Step 1: Download Offline Packages (one-time, requires internet + Docker) ### Step 1: Download Offline Packages (one-time, requires internet)
```bash ```bash
./download-packages.sh ./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 ```bash
# Basic — server only (import WinPE images later) # 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: This creates a bootable USB with:
- Partition 1: Ubuntu Server installer - **Partition 1:** Ubuntu Server installer
- Partition 2: CIDATA (autoinstall config, offline .debs, Ansible playbook, optional WinPE images) - **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 1. Insert the USB into the target machine
2. Press F12 (or vendor boot key) and select the USB 2. Press F12 (or vendor boot key) and select the USB
3. Ubuntu auto-installs — no interaction needed 3. Ubuntu auto-installs — no interaction needed
4. After reboot, the first-boot script installs all .deb packages and runs the Ansible playbook 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 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. 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 ```bash
sudo mkdir -p /mnt/usb2 sudo mkdir -p /mnt/usb2
sudo mount /dev/sdb2 /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 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 ## Verification
1. Connect a test workstation to the isolated switch 1. Connect a test workstation to the isolated switch
2. Set Network Boot (PXE) as first boot in BIOS/UEFI 2. Set Network Boot (PXE) as first boot in BIOS/UEFI
3. Boot — the client should pull an IP from 10.9.100.x 3. Boot — the client should pull an IP from 10.9.100.x
4. iPXE loads, fetches the boot script from port 4433 4. iPXE loads, fetches the boot script from port 4433
5. WinPE boots via wimboot + boot.wim over HTTP 5. Select an option from the boot menu:
6. WinPE maps Samba shares and begins image deployment - **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 ## Project Structure
``` ```
pxe-server/ pxe-server/
├── autoinstall/ ├── autoinstall/
│ ├── user-data # Cloud-init autoinstall (Ubuntu config, first-boot script) │ ├── user-data # Cloud-init autoinstall + first-boot script
│ └── meta-data # Cloud-init metadata (empty, required) │ └── meta-data # Cloud-init metadata (required, empty)
├── playbook/ ├── playbook/
│ ├── pxe_server_setup.yml # Ansible: dnsmasq, Apache, Samba, iPXE, firewall, netplan │ ├── pxe_server_setup.yml # Ansible: dnsmasq, Apache, Samba, iPXE, UFW, webapp
│ └── inventory.ini # Ansible inventory │ └── 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/ ├── unattend/
│ └── FlatUnattendW10.xml # Windows unattend.xml sample │ └── FlatUnattendW10.xml # Windows unattend.xml template
├── offline-packages/ # .deb files (gitignored, built by download-packages.sh) ├── boot-tools/ # Extracted boot files (gitignored, built by prepare-boot-tools.sh)
├── build-usb.sh # Builds the installer USB ├── blancco/ # Blancco Drive Eraser
├── download-packages.sh # Downloads offline .debs via Docker │ ├── clonezilla/ # Clonezilla Live
└── setup-guide-original.txt # Original manual setup doc (reference) │ └── 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 Types
| Image Type | Domain | Description | | Image Type | Domain | Description |
|---------------|-----------------|---------------------| |----------------------|-----------------|-------------------------|
| gea-standard | geaerospace.com | Standard desktop | | gea-standard | geaerospace.com | Standard desktop |
| gea-engineer | geaerospace.com | Engineering desktop | | gea-engineer | geaerospace.com | Engineering desktop |
| gea-shopfloor | geaerospace.com | Shop floor kiosk | | gea-shopfloor | geaerospace.com | Shop floor kiosk |
| ge-standard | ge.com | Standard desktop | | ge-standard | ge.com | Standard desktop |
| ge-engineer | ge.com | Engineering desktop | | ge-engineer | ge.com | Engineering desktop |
| ge-shopfloor | ge.com | Shop floor kiosk | | ge-shopfloor-lockdown| ge.com | Shop floor (locked) |
| ge-shopfloor-mce | ge.com | Shop floor (MCE) |
## Network Configuration ## Network Configuration
@@ -130,3 +199,14 @@ pxe-server/
- DHCP range: `10.9.100.10` - `10.9.100.100` - DHCP range: `10.9.100.10` - `10.9.100.100`
- Lease time: 12 hours - Lease time: 12 hours
- DNS: `8.8.8.8` (passed to clients, not used by server) - 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.

View File

@@ -1,106 +1,111 @@
#cloud-config #cloud-config
autoinstall: autoinstall:
version: 1 version: 1
# Locale, keyboard, timezone # Locale, keyboard, timezone
locale: en_US.UTF-8 locale: en_US.UTF-8
keyboard: keyboard:
layout: us layout: us
variant: "" variant: ""
timezone: America/New_York timezone: America/New_York
# Network configuration # Network configuration
# Uses a broad match so any wired NIC gets the static PXE address. # Uses a broad match so any wired NIC gets the static PXE address.
# No WiFi needed — all packages are on the CIDATA partition. # No WiFi needed — all packages are on the CIDATA partition.
network: network:
version: 2 version: 2
ethernets: ethernets:
any-eth: any-eth:
match: match:
name: "en*" name: "en*"
addresses: addresses:
- 10.9.100.1/24 - 10.9.100.1/24
dhcp4: false dhcp4: false
dhcp6: false dhcp6: false
optional: true optional: true
# Storage configuration # Storage configuration
storage: storage:
layout: layout:
name: lvm name: lvm
match: match:
size: largest size: largest
swap: swap:
size: 0 size: 0
# User identity # User identity
identity: identity:
hostname: pxeserver hostname: pxeserver
username: pxe username: pxe
password: "$6$rounds=656000$TpsuBw0N85085mpx$KtKsCwFlowg4NY41gUqx5ljef8cJ8uPFfgg43MyCPWByfXkhM5XushcdtkNps6lKeQFQZtli/QU.s52AUc7XC." password: "$6$rounds=656000$TpsuBw0N85085mpx$KtKsCwFlowg4NY41gUqx5ljef8cJ8uPFfgg43MyCPWByfXkhM5XushcdtkNps6lKeQFQZtli/QU.s52AUc7XC."
# Installer-stage late commands # Enable SSH
late-commands: ssh:
# Install deb packages from CIDATA USB install-server: true
- | allow-pw: true
curtin in-target --target=/target -- bash -c '
mkdir -p /mnt/cidata # Installer-stage late commands
CIDATA_DEV=$(blkid -L CIDATA) late-commands:
if [ -n "$CIDATA_DEV" ]; then # Install deb packages from CIDATA USB
mount "$CIDATA_DEV" /mnt/cidata - |
if compgen -G "/mnt/cidata/packages/*.deb" > /dev/null; then curtin in-target --target=/target -- bash -c '
cp /mnt/cidata/packages/*.deb /tmp/ mkdir -p /mnt/cidata
dpkg -i /tmp/*.deb 2>/dev/null || true CIDATA_DEV=$(blkid -L CIDATA)
dpkg -i /tmp/*.deb 2>/dev/null || true if [ -n "$CIDATA_DEV" ]; then
if command -v nmcli >/dev/null; then mount "$CIDATA_DEV" /mnt/cidata
systemctl enable NetworkManager if compgen -G "/mnt/cidata/packages/*.deb" > /dev/null; then
fi cp /mnt/cidata/packages/*.deb /tmp/
fi dpkg -i /tmp/*.deb 2>/dev/null || true
umount /mnt/cidata dpkg -i /tmp/*.deb 2>/dev/null || true
fi if command -v nmcli >/dev/null; then
' systemctl enable NetworkManager
fi
# Create first-boot.sh fi
- | umount /mnt/cidata
curtin in-target --target=/target -- bash -c ' fi
cat <<"EOF" > /opt/first-boot.sh '
#!/bin/bash
CIDATA_DEV=$(blkid -L CIDATA) # Create first-boot.sh
if [ -n "$CIDATA_DEV" ]; then - |
mkdir -p /mnt/usb curtin in-target --target=/target -- bash -c '
mount "$CIDATA_DEV" /mnt/usb cat <<"EOF" > /opt/first-boot.sh
# Install all offline .deb packages (ansible, dnsmasq, apache2, samba, etc.) #!/bin/bash
if compgen -G "/mnt/usb/packages/*.deb" > /dev/null; then CIDATA_DEV=$(blkid -L CIDATA)
dpkg -i /mnt/usb/packages/*.deb 2>/dev/null || true if [ -n "$CIDATA_DEV" ]; then
dpkg -i /mnt/usb/packages/*.deb 2>/dev/null || true mkdir -p /mnt/usb
fi mount "$CIDATA_DEV" /mnt/usb
# Run the Ansible playbook # Install all offline .deb packages (ansible, dnsmasq, apache2, samba, etc.)
if [ -f /mnt/usb/playbook/pxe_server_setup.yml ]; then if compgen -G "/mnt/usb/packages/*.deb" > /dev/null; then
cd /mnt/usb/playbook dpkg -i /mnt/usb/packages/*.deb 2>/dev/null || true
ansible-playbook -i localhost, -c local pxe_server_setup.yml dpkg -i /mnt/usb/packages/*.deb 2>/dev/null || true
fi fi
umount /mnt/usb # Run the Ansible playbook
fi if [ -f /mnt/usb/playbook/pxe_server_setup.yml ]; then
# Disable rc.local to prevent rerunning cd /mnt/usb/playbook
sed -i "s|^/opt/first-boot.sh.*|# &|" /etc/rc.local ansible-playbook -i localhost, -c local pxe_server_setup.yml
lvextend -r -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv || true fi
EOF umount /mnt/usb
' fi
- curtin in-target --target=/target -- chmod +x /opt/first-boot.sh # Disable rc.local to prevent rerunning
sed -i "s|^/opt/first-boot.sh.*|# &|" /etc/rc.local
# Create rc.local without unintended indentation lvextend -r -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv || true
- | EOF
curtin in-target --target=/target -- bash -c ' '
cat <<"EOF" > /etc/rc.local - curtin in-target --target=/target -- chmod +x /opt/first-boot.sh
#!/bin/bash
/opt/first-boot.sh > /var/log/first-boot.log 2>&1 & # Create rc.local without unintended indentation
exit 0 - |
EOF curtin in-target --target=/target -- bash -c '
' cat <<"EOF" > /etc/rc.local
- curtin in-target --target=/target -- chmod +x /etc/rc.local #!/bin/bash
/opt/first-boot.sh > /var/log/first-boot.log 2>&1 &
user-data: exit 0
disable_root: false EOF
'
refresh-installer: - curtin in-target --target=/target -- chmod +x /etc/rc.local
update: yes
user-data:
disable_root: false
refresh-installer:
update: yes

View File

@@ -1,240 +1,240 @@
#!/bin/bash #!/bin/bash
# #
# build-usb.sh — Build a bootable PXE-server installer USB # build-usb.sh — Build a bootable PXE-server installer USB
# #
# Creates a two-partition USB: # Creates a two-partition USB:
# Partition 1: Ubuntu Server 24.04 installer (ISO contents) # Partition 1: Ubuntu Server 24.04 installer (ISO contents)
# Partition 2: CIDATA volume (autoinstall config, .debs, playbook) # Partition 2: CIDATA volume (autoinstall config, .debs, playbook)
# #
# The target machine boots from this USB, Ubuntu auto-installs with # The target machine boots from this USB, Ubuntu auto-installs with
# cloud-init (user-data/meta-data from CIDATA), installs offline .debs, # cloud-init (user-data/meta-data from CIDATA), installs offline .debs,
# and on first boot runs the Ansible playbook to configure PXE services. # and on first boot runs the Ansible playbook to configure PXE services.
# #
# Usage: # Usage:
# sudo ./build-usb.sh /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso # sudo ./build-usb.sh /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso
# #
# WARNING: This will ERASE the target USB device. # WARNING: This will ERASE the target USB device.
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
AUTOINSTALL_DIR="$SCRIPT_DIR/autoinstall" AUTOINSTALL_DIR="$SCRIPT_DIR/autoinstall"
PLAYBOOK_DIR="$SCRIPT_DIR/playbook" PLAYBOOK_DIR="$SCRIPT_DIR/playbook"
OFFLINE_PKG_DIR="$SCRIPT_DIR/offline-packages" OFFLINE_PKG_DIR="$SCRIPT_DIR/offline-packages"
# --- Validate arguments --- # --- Validate arguments ---
if [ $# -lt 2 ]; then if [ $# -lt 2 ]; then
echo "Usage: sudo $0 /dev/sdX /path/to/ubuntu-24.04.iso [/path/to/winpe-images]" echo "Usage: sudo $0 /dev/sdX /path/to/ubuntu-24.04.iso [/path/to/winpe-images]"
echo "" echo ""
echo " The optional third argument is the path to WinPE deployment content" 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 " (e.g., the mounted Media Creator LITE USB). If provided, the images"
echo " will be bundled onto the CIDATA partition for automatic import." echo " will be bundled onto the CIDATA partition for automatic import."
echo "" echo ""
echo "Available removable devices:" echo "Available removable devices:"
lsblk -d -o NAME,SIZE,TRAN,RM | grep -E '^\S+\s+\S+\s+(usb)\s+1' lsblk -d -o NAME,SIZE,TRAN,RM | grep -E '^\S+\s+\S+\s+(usb)\s+1'
exit 1 exit 1
fi fi
USB_DEV="$1" USB_DEV="$1"
ISO_PATH="$2" ISO_PATH="$2"
WINPE_SOURCE="${3:-}" WINPE_SOURCE="${3:-}"
# Safety checks # Safety checks
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
echo "ERROR: Must run as root (sudo)." echo "ERROR: Must run as root (sudo)."
exit 1 exit 1
fi fi
if [ ! -b "$USB_DEV" ]; then if [ ! -b "$USB_DEV" ]; then
echo "ERROR: $USB_DEV is not a block device." echo "ERROR: $USB_DEV is not a block device."
exit 1 exit 1
fi fi
if [ ! -f "$ISO_PATH" ]; then if [ ! -f "$ISO_PATH" ]; then
echo "ERROR: ISO not found at $ISO_PATH" echo "ERROR: ISO not found at $ISO_PATH"
exit 1 exit 1
fi fi
# Verify it's a removable device (safety against wiping system disks) # Verify it's a removable device (safety against wiping system disks)
REMOVABLE=$(lsblk -nd -o RM "$USB_DEV" 2>/dev/null || echo "0") REMOVABLE=$(lsblk -nd -o RM "$USB_DEV" 2>/dev/null || echo "0")
if [ "$REMOVABLE" != "1" ]; then if [ "$REMOVABLE" != "1" ]; then
echo "WARNING: $USB_DEV does not appear to be a removable device." 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 read -rp "Are you SURE you want to erase $USB_DEV? (type YES): " CONFIRM
if [ "$CONFIRM" != "YES" ]; then if [ "$CONFIRM" != "YES" ]; then
echo "Aborted." echo "Aborted."
exit 1 exit 1
fi fi
fi fi
# Verify required source files exist # Verify required source files exist
if [ ! -f "$AUTOINSTALL_DIR/user-data" ]; then if [ ! -f "$AUTOINSTALL_DIR/user-data" ]; then
echo "ERROR: user-data not found at $AUTOINSTALL_DIR/user-data" echo "ERROR: user-data not found at $AUTOINSTALL_DIR/user-data"
exit 1 exit 1
fi fi
if [ ! -f "$AUTOINSTALL_DIR/meta-data" ]; then if [ ! -f "$AUTOINSTALL_DIR/meta-data" ]; then
echo "ERROR: meta-data not found at $AUTOINSTALL_DIR/meta-data" echo "ERROR: meta-data not found at $AUTOINSTALL_DIR/meta-data"
exit 1 exit 1
fi fi
if [ ! -f "$PLAYBOOK_DIR/pxe_server_setup.yml" ]; then if [ ! -f "$PLAYBOOK_DIR/pxe_server_setup.yml" ]; then
echo "ERROR: pxe_server_setup.yml not found at $PLAYBOOK_DIR/" echo "ERROR: pxe_server_setup.yml not found at $PLAYBOOK_DIR/"
exit 1 exit 1
fi fi
echo "============================================" echo "============================================"
echo "PXE Server USB Builder" echo "PXE Server USB Builder"
echo "============================================" echo "============================================"
echo "USB Device : $USB_DEV" echo "USB Device : $USB_DEV"
echo "ISO : $ISO_PATH" echo "ISO : $ISO_PATH"
echo "Source Dir : $SCRIPT_DIR" echo "Source Dir : $SCRIPT_DIR"
echo "" echo ""
echo "This will ERASE all data on $USB_DEV." echo "This will ERASE all data on $USB_DEV."
read -rp "Continue? (y/N): " PROCEED read -rp "Continue? (y/N): " PROCEED
if [[ ! "$PROCEED" =~ ^[Yy]$ ]]; then if [[ ! "$PROCEED" =~ ^[Yy]$ ]]; then
echo "Aborted." echo "Aborted."
exit 1 exit 1
fi fi
# --- Unmount any existing partitions --- # --- Unmount any existing partitions ---
echo "" echo ""
echo "[1/6] Unmounting existing partitions on $USB_DEV..." echo "[1/6] Unmounting existing partitions on $USB_DEV..."
for part in "${USB_DEV}"*; do for part in "${USB_DEV}"*; do
umount "$part" 2>/dev/null || true umount "$part" 2>/dev/null || true
done done
# --- Write ISO to USB --- # --- Write ISO to USB ---
echo "[2/6] Writing Ubuntu ISO to $USB_DEV (this may take several minutes)..." 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 dd if="$ISO_PATH" of="$USB_DEV" bs=4M status=progress oflag=sync
sync sync
# --- Find the end of the ISO to create CIDATA partition --- # --- Find the end of the ISO to create CIDATA partition ---
echo "[3/6] Creating CIDATA partition after ISO data..." echo "[3/6] Creating CIDATA partition after ISO data..."
# Get ISO size in bytes and calculate the start sector for the new partition # Get ISO size in bytes and calculate the start sector for the new partition
ISO_SIZE=$(stat -c%s "$ISO_PATH") ISO_SIZE=$(stat -c%s "$ISO_PATH")
SECTOR_SIZE=512 SECTOR_SIZE=512
# Start the CIDATA partition 1MB after the ISO ends (alignment) # Start the CIDATA partition 1MB after the ISO ends (alignment)
START_SECTOR=$(( (ISO_SIZE / SECTOR_SIZE) + 2048 )) START_SECTOR=$(( (ISO_SIZE / SECTOR_SIZE) + 2048 ))
# Use sfdisk to append a new partition # Use sfdisk to append a new partition
echo " ISO size: $((ISO_SIZE / 1024 / 1024)) MB" echo " ISO size: $((ISO_SIZE / 1024 / 1024)) MB"
echo " CIDATA partition starts at sector $START_SECTOR" echo " CIDATA partition starts at sector $START_SECTOR"
# Add a new partition using sfdisk --append # Add a new partition using sfdisk --append
echo "${START_SECTOR},+,L" | sfdisk --append "$USB_DEV" --no-reread 2>/dev/null || true echo "${START_SECTOR},+,L" | sfdisk --append "$USB_DEV" --no-reread 2>/dev/null || true
partprobe "$USB_DEV" partprobe "$USB_DEV"
sleep 2 sleep 2
# Determine the new partition name (could be sdX3, sdX4, etc.) # Determine the new partition name (could be sdX3, sdX4, etc.)
CIDATA_PART="" CIDATA_PART=""
for part in "${USB_DEV}"[0-9]*; do for part in "${USB_DEV}"[0-9]*; do
# Find the partition that starts at or after our start sector # 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]*') 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 if [ -n "$PART_START" ] && [ "$PART_START" -ge "$START_SECTOR" ]; then
CIDATA_PART="$part" CIDATA_PART="$part"
break break
fi fi
done done
# Fallback: use the last partition # Fallback: use the last partition
if [ -z "$CIDATA_PART" ]; then if [ -z "$CIDATA_PART" ]; then
CIDATA_PART=$(lsblk -ln -o NAME "$USB_DEV" | tail -1) CIDATA_PART=$(lsblk -ln -o NAME "$USB_DEV" | tail -1)
CIDATA_PART="/dev/$CIDATA_PART" CIDATA_PART="/dev/$CIDATA_PART"
fi fi
echo " CIDATA partition: $CIDATA_PART" echo " CIDATA partition: $CIDATA_PART"
# --- Format CIDATA partition --- # --- Format CIDATA partition ---
echo "[4/6] Formatting $CIDATA_PART as FAT32 (label: CIDATA)..." echo "[4/6] Formatting $CIDATA_PART as FAT32 (label: CIDATA)..."
mkfs.vfat -F 32 -n CIDATA "$CIDATA_PART" mkfs.vfat -F 32 -n CIDATA "$CIDATA_PART"
# --- Mount and copy files --- # --- Mount and copy files ---
echo "[5/6] Copying autoinstall config, packages, and playbook to CIDATA..." echo "[5/6] Copying autoinstall config, packages, and playbook to CIDATA..."
MOUNT_POINT=$(mktemp -d) MOUNT_POINT=$(mktemp -d)
mount "$CIDATA_PART" "$MOUNT_POINT" mount "$CIDATA_PART" "$MOUNT_POINT"
# Copy cloud-init files # Copy cloud-init files
cp "$AUTOINSTALL_DIR/user-data" "$MOUNT_POINT/" cp "$AUTOINSTALL_DIR/user-data" "$MOUNT_POINT/"
cp "$AUTOINSTALL_DIR/meta-data" "$MOUNT_POINT/" cp "$AUTOINSTALL_DIR/meta-data" "$MOUNT_POINT/"
# Copy offline .deb packages into packages/ subdirectory # Copy offline .deb packages into packages/ subdirectory
mkdir -p "$MOUNT_POINT/packages" mkdir -p "$MOUNT_POINT/packages"
DEB_COUNT=0 DEB_COUNT=0
if [ -d "$OFFLINE_PKG_DIR" ]; then if [ -d "$OFFLINE_PKG_DIR" ]; then
for deb in "$OFFLINE_PKG_DIR"/*.deb; do for deb in "$OFFLINE_PKG_DIR"/*.deb; do
if [ -f "$deb" ]; then if [ -f "$deb" ]; then
cp "$deb" "$MOUNT_POINT/packages/" cp "$deb" "$MOUNT_POINT/packages/"
DEB_COUNT=$((DEB_COUNT + 1)) DEB_COUNT=$((DEB_COUNT + 1))
fi fi
done done
fi fi
echo " Copied $DEB_COUNT .deb packages to packages/" echo " Copied $DEB_COUNT .deb packages to packages/"
# Copy playbook directory # Copy playbook directory
cp -r "$PLAYBOOK_DIR" "$MOUNT_POINT/playbook" cp -r "$PLAYBOOK_DIR" "$MOUNT_POINT/playbook"
echo " Copied playbook/" echo " Copied playbook/"
# Copy webapp # Copy webapp
WEBAPP_DIR="$SCRIPT_DIR/webapp" WEBAPP_DIR="$SCRIPT_DIR/webapp"
if [ -d "$WEBAPP_DIR" ]; then if [ -d "$WEBAPP_DIR" ]; then
mkdir -p "$MOUNT_POINT/webapp" mkdir -p "$MOUNT_POINT/webapp"
cp -r "$WEBAPP_DIR/app.py" "$WEBAPP_DIR/requirements.txt" "$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/" cp -r "$WEBAPP_DIR/templates" "$WEBAPP_DIR/static" "$MOUNT_POINT/webapp/"
echo " Copied webapp/" echo " Copied webapp/"
fi fi
# Copy pip wheels for offline Flask install # Copy pip wheels for offline Flask install
PIP_WHEELS_DIR="$SCRIPT_DIR/pip-wheels" PIP_WHEELS_DIR="$SCRIPT_DIR/pip-wheels"
if [ -d "$PIP_WHEELS_DIR" ]; then if [ -d "$PIP_WHEELS_DIR" ]; then
cp -r "$PIP_WHEELS_DIR" "$MOUNT_POINT/pip-wheels" cp -r "$PIP_WHEELS_DIR" "$MOUNT_POINT/pip-wheels"
echo " Copied pip-wheels/" echo " Copied pip-wheels/"
else else
echo " No pip-wheels/ found (run download-packages.sh first)" echo " No pip-wheels/ found (run download-packages.sh first)"
fi fi
# Copy boot tools (Clonezilla, Blancco, Memtest) if prepared # Copy boot tools (Clonezilla, Blancco, Memtest) if prepared
BOOT_TOOLS_DIR="$SCRIPT_DIR/boot-tools" BOOT_TOOLS_DIR="$SCRIPT_DIR/boot-tools"
if [ -d "$BOOT_TOOLS_DIR" ]; then if [ -d "$BOOT_TOOLS_DIR" ]; then
cp -r "$BOOT_TOOLS_DIR" "$MOUNT_POINT/boot-tools" cp -r "$BOOT_TOOLS_DIR" "$MOUNT_POINT/boot-tools"
TOOLS_SIZE=$(du -sh "$MOUNT_POINT/boot-tools" | cut -f1) TOOLS_SIZE=$(du -sh "$MOUNT_POINT/boot-tools" | cut -f1)
echo " Copied boot-tools/ ($TOOLS_SIZE)" echo " Copied boot-tools/ ($TOOLS_SIZE)"
else else
echo " No boot-tools/ found (run prepare-boot-tools.sh first)" echo " No boot-tools/ found (run prepare-boot-tools.sh first)"
fi fi
# Optionally copy WinPE deployment images # Optionally copy WinPE deployment images
if [ -n "$WINPE_SOURCE" ] && [ -d "$WINPE_SOURCE" ]; then if [ -n "$WINPE_SOURCE" ] && [ -d "$WINPE_SOURCE" ]; then
echo " Copying WinPE deployment content from $WINPE_SOURCE..." echo " Copying WinPE deployment content from $WINPE_SOURCE..."
mkdir -p "$MOUNT_POINT/images" mkdir -p "$MOUNT_POINT/images"
cp -r "$WINPE_SOURCE"/* "$MOUNT_POINT/images/" 2>/dev/null || true cp -r "$WINPE_SOURCE"/* "$MOUNT_POINT/images/" 2>/dev/null || true
IMG_SIZE=$(du -sh "$MOUNT_POINT/images" | cut -f1) IMG_SIZE=$(du -sh "$MOUNT_POINT/images" | cut -f1)
echo " Copied WinPE images ($IMG_SIZE)" echo " Copied WinPE images ($IMG_SIZE)"
elif [ -n "$WINPE_SOURCE" ]; then elif [ -n "$WINPE_SOURCE" ]; then
echo " WARNING: WinPE source path not found: $WINPE_SOURCE (skipping)" echo " WARNING: WinPE source path not found: $WINPE_SOURCE (skipping)"
fi fi
# List what's on CIDATA # List what's on CIDATA
echo "" echo ""
echo " CIDATA contents:" echo " CIDATA contents:"
ls -lh "$MOUNT_POINT/" | sed 's/^/ /' ls -lh "$MOUNT_POINT/" | sed 's/^/ /'
# --- Cleanup --- # --- Cleanup ---
echo "" echo ""
echo "[6/6] Syncing and unmounting..." echo "[6/6] Syncing and unmounting..."
sync sync
umount "$MOUNT_POINT" umount "$MOUNT_POINT"
rmdir "$MOUNT_POINT" rmdir "$MOUNT_POINT"
echo "" echo ""
echo "============================================" echo "============================================"
echo "USB build complete!" echo "USB build complete!"
echo "============================================" echo "============================================"
echo "" echo ""
echo "Next steps:" echo "Next steps:"
echo " 1. Insert USB into target machine" echo " 1. Insert USB into target machine"
echo " 2. Boot from USB (F12 / boot menu)" echo " 2. Boot from USB (F12 / boot menu)"
echo " 3. Ubuntu will auto-install and configure the PXE server" echo " 3. Ubuntu will auto-install and configure the PXE server"
echo " 4. After reboot, move the NIC to the isolated PXE network" echo " 4. After reboot, move the NIC to the isolated PXE network"
echo "" echo ""

View File

@@ -1,94 +1,94 @@
#!/bin/bash #!/bin/bash
# #
# download-packages.sh — Download all .deb packages needed for offline PXE server setup # 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). # 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, # It downloads every .deb needed by the Ansible playbook into a local directory,
# which then gets bundled onto the installer USB. # which then gets bundled onto the installer USB.
# #
# Usage: # Usage:
# ./download-packages.sh [output_directory] # ./download-packages.sh [output_directory]
# #
# Default output: ./offline-packages/ # Default output: ./offline-packages/
set -euo pipefail set -euo pipefail
OUT_DIR="${1:-./offline-packages}" OUT_DIR="${1:-./offline-packages}"
mkdir -p "$OUT_DIR" mkdir -p "$OUT_DIR"
# Packages installed by the Ansible playbook (pxe_server_setup.yml) # Packages installed by the Ansible playbook (pxe_server_setup.yml)
PLAYBOOK_PACKAGES=( PLAYBOOK_PACKAGES=(
ansible ansible
dnsmasq dnsmasq
apache2 apache2
samba samba
unzip unzip
ufw ufw
cron cron
wimtools wimtools
python3-pip python3-pip
python3-venv python3-venv
p7zip-full p7zip-full
) )
# Packages installed during autoinstall late-commands (NetworkManager, WiFi, etc.) # 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. # These are already in your ubuntu_playbook/*.deb files, but we can refresh them here too.
AUTOINSTALL_PACKAGES=( AUTOINSTALL_PACKAGES=(
network-manager network-manager
wpasupplicant wpasupplicant
wireless-tools wireless-tools
linux-firmware linux-firmware
firmware-sof-signed firmware-sof-signed
) )
ALL_PACKAGES=("${PLAYBOOK_PACKAGES[@]}" "${AUTOINSTALL_PACKAGES[@]}") ALL_PACKAGES=("${PLAYBOOK_PACKAGES[@]}" "${AUTOINSTALL_PACKAGES[@]}")
echo "============================================" echo "============================================"
echo "Offline Package Downloader" echo "Offline Package Downloader"
echo "============================================" echo "============================================"
echo "Output directory: $OUT_DIR" echo "Output directory: $OUT_DIR"
echo "" echo ""
echo "Packages to resolve:" echo "Packages to resolve:"
printf ' - %s\n' "${ALL_PACKAGES[@]}" printf ' - %s\n' "${ALL_PACKAGES[@]}"
echo "" echo ""
# Update package cache # Update package cache
echo "[1/3] Updating package cache..." echo "[1/3] Updating package cache..."
sudo apt-get update -qq sudo apt-get update -qq
# Simulate install to find all dependencies # Simulate install to find all dependencies
echo "[2/3] Resolving dependencies..." echo "[2/3] Resolving dependencies..."
DEPS=$(apt-get install --simulate "${ALL_PACKAGES[@]}" 2>&1 \ DEPS=$(apt-get install --simulate "${ALL_PACKAGES[@]}" 2>&1 \
| grep "^Inst " \ | grep "^Inst " \
| awk '{print $2}' \ | awk '{print $2}' \
| sort -u) | sort -u)
DEP_COUNT=$(echo "$DEPS" | wc -l) DEP_COUNT=$(echo "$DEPS" | wc -l)
echo " Found $DEP_COUNT packages (including dependencies)" echo " Found $DEP_COUNT packages (including dependencies)"
# Download all packages # Download all packages
echo "[3/4] Downloading .deb packages to $OUT_DIR..." echo "[3/4] Downloading .deb packages to $OUT_DIR..."
cd "$OUT_DIR" cd "$OUT_DIR"
apt-get download $DEPS 2>&1 | tail -5 apt-get download $DEPS 2>&1 | tail -5
DEB_COUNT=$(ls -1 *.deb 2>/dev/null | wc -l) DEB_COUNT=$(ls -1 *.deb 2>/dev/null | wc -l)
TOTAL_SIZE=$(du -sh . | cut -f1) TOTAL_SIZE=$(du -sh . | cut -f1)
echo " $DEB_COUNT packages ($TOTAL_SIZE)" echo " $DEB_COUNT packages ($TOTAL_SIZE)"
# Download pip wheels for Flask webapp (offline install) # Download pip wheels for Flask webapp (offline install)
echo "[4/4] Downloading Python wheels for webapp..." echo "[4/4] Downloading Python wheels for webapp..."
PIP_DIR="$(dirname "$OUT_DIR")/pip-wheels" PIP_DIR="$(dirname "$OUT_DIR")/pip-wheels"
mkdir -p "$PIP_DIR" mkdir -p "$PIP_DIR"
pip3 download -d "$PIP_DIR" flask lxml 2>&1 | tail -5 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) 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 " $WHL_COUNT Python packages downloaded to pip-wheels/"
echo "" echo ""
echo "============================================" echo "============================================"
echo "Download complete!" echo "Download complete!"
echo "============================================" echo "============================================"
echo " .deb packages: $DEB_COUNT ($TOTAL_SIZE) in $OUT_DIR/" echo " .deb packages: $DEB_COUNT ($TOTAL_SIZE) in $OUT_DIR/"
echo " Python wheels: $WHL_COUNT in $PIP_DIR/" echo " Python wheels: $WHL_COUNT in $PIP_DIR/"
echo "" echo ""

View File

@@ -1,443 +1,445 @@
--- ---
- name: PXE Server Setup (Ubuntu with dnsmasq) - name: PXE Server Setup (Ubuntu with dnsmasq)
hosts: localhost hosts: localhost
connection: local connection: local
become: yes become: yes
gather_facts: yes gather_facts: yes
pre_tasks: pre_tasks:
- name: "Verify required packages are installed (pre-installed from offline .debs)" - name: "Verify required packages are installed (pre-installed from offline .debs)"
command: dpkg -s {{ item }} command: dpkg -s {{ item }}
loop: loop:
- dnsmasq - dnsmasq
- apache2 - apache2
- samba - samba
- unzip - unzip
- ufw - ufw
- cron - cron
- ansible - ansible
- wimtools - wimtools
register: pkg_check register: pkg_check
failed_when: false failed_when: false
changed_when: false changed_when: false
- name: "Warn about missing packages" - name: "Warn about missing packages"
debug: debug:
msg: "WARNING: {{ item.item }} is not installed! Install offline .debs first." msg: "WARNING: {{ item.item }} is not installed! Install offline .debs first."
loop: "{{ pkg_check.results }}" loop: "{{ pkg_check.results }}"
when: item.rc != 0 when: item.rc != 0
vars: vars:
tftp_dir: "/srv/tftp" tftp_dir: "/srv/tftp"
web_root: "/var/www/html" web_root: "/var/www/html"
samba_share: "/srv/samba/winpeapps" samba_share: "/srv/samba/winpeapps"
usb_mount: "/mnt/usb/playbook" # where your USB is mounted usb_mount: "/mnt/usb/playbook" # where your USB is mounted
image_types: image_types:
- gea-standard - gea-standard
- gea-engineer - gea-engineer
- gea-shopfloor - gea-shopfloor
- ge-standard - ge-standard
- ge-engineer - ge-engineer
- ge-shopfloor-lockdown - ge-shopfloor-lockdown
- ge-shopfloor-mce - ge-shopfloor-mce
deploy_subdirs: deploy_subdirs:
- Applications - Applications
- Control - Control
- "Operating Systems" - "Operating Systems"
- "Out-of-box Drivers" - "Out-of-box Drivers"
- Packages - Packages
- Tools - Tools
tasks: tasks:
- name: "Gather minimal network facts" - name: "Gather minimal network facts"
ansible.builtin.setup: ansible.builtin.setup:
filter: filter:
- ansible_interfaces - ansible_interfaces
- ansible_default_ipv4 - ansible_default_ipv4
- name: "Bring up all ethernet-like interfaces" - name: "Bring up all ethernet-like interfaces"
command: ip link set dev {{ item }} up command: ip link set dev {{ item }} up
loop: "{{ ansible_interfaces | select('match','^e(th|n)') | list }}" loop: "{{ ansible_interfaces | select('match','^e(th|n)') | list }}"
ignore_errors: yes ignore_errors: yes
- name: "Determine PXE interface" - name: "Determine PXE interface"
set_fact: set_fact:
pxe_iface: >- pxe_iface: >-
{{ (ansible_interfaces {{ (ansible_interfaces
| select('match','^e(th|n)') | select('match','^e(th|n)')
| reject('equalto','lo') | reject('equalto','lo')
| reject('equalto', ansible_default_ipv4.interface) | reject('equalto', ansible_default_ipv4.interface | default(''))
| list | list
) )
| first | first
| default(ansible_default_ipv4.interface) }} | 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: "Debug: final pxe_iface choice"
debug:
- name: "Configure dnsmasq for DHCP and TFTP" msg: "Using {{ pxe_iface }} for DHCP/TFTP"
copy:
dest: /etc/dnsmasq.conf - name: "Configure dnsmasq for DHCP and TFTP"
backup: yes copy:
content: | dest: /etc/dnsmasq.conf
port=0 backup: yes
interface={{ pxe_iface }} content: |
bind-interfaces port=0
dhcp-range=10.9.100.10,10.9.100.100,12h interface={{ pxe_iface }}
dhcp-option=3,10.9.100.1 bind-interfaces
dhcp-option=6,8.8.8.8 dhcp-range=10.9.100.10,10.9.100.100,12h
enable-tftp dhcp-option=3,10.9.100.1
tftp-root={{ tftp_dir }} dhcp-option=6,8.8.8.8
dhcp-boot=ipxe.efi enable-tftp
tftp-root={{ tftp_dir }}
- name: "Create TFTP directory" dhcp-boot=ipxe.efi
file:
path: "{{ tftp_dir }}" - name: "Create TFTP directory"
state: directory file:
mode: '0755' path: "{{ tftp_dir }}"
owner: nobody state: directory
group: nogroup mode: '0755'
owner: nobody
- name: "Create Win11 directory structure" group: nogroup
file:
path: "{{ web_root }}/win11/{{ item }}" - name: "Create Win11 directory structure"
state: directory file:
mode: '0755' path: "{{ web_root }}/win11/{{ item }}"
loop: state: directory
- "EFI/Boot" mode: '0755'
- "EFI/Microsoft/Boot" loop:
- "Boot" - "EFI/Boot"
- "sources" - "EFI/Microsoft/Boot"
- "Boot"
- name: "Create Altiris iPXE directory" - "sources"
file:
path: "{{ web_root }}/Altiris/iPXE" - name: "Create Altiris iPXE directory"
state: directory file:
mode: '0755' path: "{{ web_root }}/Altiris/iPXE"
state: directory
- name: "Create boot tool directories" mode: '0755'
file:
path: "{{ web_root }}/{{ item }}" - name: "Create boot tool directories"
state: directory file:
mode: '0755' path: "{{ web_root }}/{{ item }}"
loop: state: directory
- clonezilla mode: '0755'
- blancco loop:
- memtest - clonezilla
- blancco
- name: "Create GetPxeScript.aspx (iPXE boot menu)" - memtest
copy:
dest: "{{ web_root }}/Altiris/iPXE/GetPxeScript.aspx" - name: "Create GetPxeScript.aspx (iPXE boot menu)"
backup: yes copy:
content: | dest: "{{ web_root }}/Altiris/iPXE/GetPxeScript.aspx"
#!ipxe backup: yes
content: |
set server 10.9.100.1 #!ipxe
:menu set server 10.9.100.1
menu GE Aerospace PXE Boot Menu
item --gap -- ---- Windows Deployment ---- :menu
item winpe Windows PE (Image Deployment) menu GE Aerospace PXE Boot Menu
item --gap -- ---- Utilities ---- item --gap -- ---- Windows Deployment ----
item clonezilla Clonezilla Live (Disk Imaging) item winpe Windows PE (Image Deployment)
item blancco Blancco Drive Eraser item --gap -- ---- Utilities ----
item memtest Memtest86+ (Memory Diagnostics) item clonezilla Clonezilla Live (Disk Imaging)
item --gap -- ---- item blancco Blancco Drive Eraser
item reboot Reboot item memtest Memtest86+ (Memory Diagnostics)
item exit Exit to BIOS item --gap -- ----
choose --default winpe --timeout 30000 target && goto ${target} item reboot Reboot
item exit Exit to BIOS
:winpe choose --default winpe --timeout 30000 target && goto ${target}
kernel http://${server}/win11/wimboot gui
initrd http://${server}/win11/EFI/Microsoft/Boot/boot.stl EFI/Microsoft/Boot/Boot.stl :winpe
initrd http://${server}/win11/EFI/Microsoft/Boot/BCD EFI/Microsoft/Boot/BCD kernel http://${server}/win11/wimboot gui
initrd http://${server}/win11/EFI/Boot/bootx64.efi EFI/Boot/bootx64.efi initrd http://${server}/win11/EFI/Microsoft/Boot/boot.stl EFI/Microsoft/Boot/Boot.stl
initrd http://${server}/win11/Boot/boot.sdi Boot/boot.sdi initrd http://${server}/win11/EFI/Microsoft/Boot/BCD EFI/Microsoft/Boot/BCD
initrd http://${server}/win11/sources/boot.wim sources/boot.wim initrd http://${server}/win11/EFI/Boot/bootx64.efi EFI/Boot/bootx64.efi
boot initrd http://${server}/win11/Boot/boot.sdi Boot/boot.sdi
initrd http://${server}/win11/sources/boot.wim sources/boot.wim
:clonezilla boot
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 :clonezilla
initrd ${base}/initrd.img set base http://${server}/clonezilla
boot 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
:blancco boot
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 :blancco
initrd ${bbase}/intel-ucode.img ${bbase}/amd-ucode.img ${bbase}/config.img ${bbase}/initramfs-bde-linux.img set bbase http://${server}/blancco
boot 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
:memtest boot
kernel http://${server}/memtest/memtest.efi
boot :memtest
kernel http://${server}/memtest/memtest.efi
:reboot boot
reboot
:reboot
:exit reboot
exit
:exit
- name: "Ensure Apache listens on port 4433" exit
lineinfile:
path: /etc/apache2/ports.conf - name: "Ensure Apache listens on port 4433"
line: "Listen 4433" lineinfile:
backup: yes path: /etc/apache2/ports.conf
state: present line: "Listen 4433"
backup: yes
- name: "Create VirtualHost for Altiris iPXE on 4433" state: present
copy:
dest: /etc/apache2/sites-available/altiris-ipxe.conf - name: "Create VirtualHost for Altiris iPXE on 4433"
backup: yes copy:
content: | dest: /etc/apache2/sites-available/altiris-ipxe.conf
<VirtualHost *:4433> backup: yes
DocumentRoot {{ web_root }} content: |
<Directory "{{ web_root }}/Altiris/iPXE"> <VirtualHost *:4433>
Options Indexes FollowSymLinks DocumentRoot {{ web_root }}
AllowOverride None <Directory "{{ web_root }}/Altiris/iPXE">
Require all granted Options Indexes FollowSymLinks
AddType text/plain .aspx AllowOverride None
</Directory> Require all granted
</VirtualHost> AddType text/plain .aspx
</Directory>
- name: "Enable Altiris iPXE site" </VirtualHost>
command: a2ensite altiris-ipxe.conf
args: - name: "Enable Altiris iPXE site"
creates: /etc/apache2/sites-enabled/altiris-ipxe.conf command: a2ensite altiris-ipxe.conf
args:
- name: "Reload Apache to apply changes" creates: /etc/apache2/sites-enabled/altiris-ipxe.conf
systemd:
name: apache2 - name: "Reload Apache to apply changes"
state: reloaded systemd:
name: apache2
- name: "Create Samba share root" state: reloaded
file:
path: "{{ samba_share }}" - name: "Create Samba share root"
state: directory file:
mode: '0777' path: "{{ samba_share }}"
state: directory
- name: "Create Clonezilla backup share directory" mode: '0777'
file:
path: /srv/samba/clonezilla - name: "Create Clonezilla backup share directory"
state: directory file:
mode: '0777' path: /srv/samba/clonezilla
state: directory
- name: "Create Blancco reports share directory" mode: '0777'
file:
path: /srv/samba/blancco-reports - name: "Create Blancco reports share directory"
state: directory file:
mode: '0777' path: /srv/samba/blancco-reports
state: directory
- name: "Configure Samba shares" mode: '0777'
blockinfile:
path: /etc/samba/smb.conf - name: "Configure Samba shares"
backup: yes blockinfile:
block: | path: /etc/samba/smb.conf
[winpeapps] backup: yes
path = {{ samba_share }} block: |
browseable = yes [winpeapps]
read only = no path = {{ samba_share }}
guest ok = yes browseable = yes
read only = no
[clonezilla] guest ok = yes
path = /srv/samba/clonezilla
browseable = yes [clonezilla]
read only = no path = /srv/samba/clonezilla
guest ok = yes browseable = yes
comment = Clonezilla backup images read only = no
guest ok = yes
[blancco-reports] comment = Clonezilla backup images
path = /srv/samba/blancco-reports
browseable = yes [blancco-reports]
read only = no path = /srv/samba/blancco-reports
guest ok = yes browseable = yes
comment = Blancco Drive Eraser reports read only = no
guest ok = yes
- name: "Create image-type top-level directories" comment = Blancco Drive Eraser reports
file:
path: "{{ samba_share }}/{{ item }}" - name: "Create image-type top-level directories"
state: directory file:
mode: '0777' path: "{{ samba_share }}/{{ item }}"
loop: "{{ image_types }}" state: directory
mode: '0777'
- name: "Create Deploy subdirectories for each image type" loop: "{{ image_types }}"
file:
path: "{{ samba_share }}/{{ item.0 }}/Deploy/{{ item.1 }}" - name: "Create Deploy subdirectories for each image type"
state: directory file:
mode: '0777' path: "{{ samba_share }}/{{ item.0 }}/Deploy/{{ item.1 }}"
with_nested: state: directory
- "{{ image_types }}" mode: '0777'
- "{{ deploy_subdirs }}" with_nested:
- "{{ image_types }}"
- name: "Copy WinPE & boot files from USB" - "{{ deploy_subdirs }}"
copy:
src: "{{ usb_mount }}/{{ item.src }}" - name: "Copy WinPE & boot files from USB"
dest: "{{ web_root }}/win11/{{ item.dest }}" copy:
mode: '0644' src: "{{ usb_mount }}/{{ item.src }}"
loop: dest: "{{ web_root }}/win11/{{ item.dest }}"
- { src: "wimboot", dest: "wimboot" } mode: '0644'
- { src: "boot.stl", dest: "EFI/Microsoft/Boot/boot.stl" } loop:
- { src: "BCD", dest: "EFI/Microsoft/Boot/BCD" } - { src: "wimboot", dest: "wimboot" }
- { src: "bootx64.efi", dest: "EFI/Boot/bootx64.efi" } - { src: "boot.stl", dest: "EFI/Microsoft/Boot/boot.stl" }
- { src: "boot.sdi", dest: "Boot/boot.sdi" } - { src: "BCD", dest: "EFI/Microsoft/Boot/BCD" }
- { src: "boot.wim", dest: "sources/boot.wim" } - { src: "bootx64.efi", dest: "EFI/Boot/bootx64.efi" }
- { src: "boot.sdi", dest: "Boot/boot.sdi" }
- name: "Copy iPXE binaries from USB" - { src: "boot.wim", dest: "sources/boot.wim" }
copy:
src: "{{ usb_mount }}/{{ item }}" - name: "Copy iPXE binaries from USB"
dest: "{{ tftp_dir }}/{{ item }}" copy:
mode: '0755' src: "{{ usb_mount }}/{{ item }}"
loop: dest: "{{ tftp_dir }}/{{ item }}"
- ipxe.efi mode: '0755'
loop:
- name: "Copy boot tool files from USB (Clonezilla, Blancco, Memtest)" - ipxe.efi
shell: >
cp -r "{{ usb_mount }}/../boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null || - name: "Copy boot tool files from USB (Clonezilla, Blancco, Memtest)"
cp -r "{{ usb_mount }}/boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null || true shell: >
loop: cp -r "{{ usb_mount }}/../boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null ||
- clonezilla cp -r "{{ usb_mount }}/boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null || true
- blancco loop:
- memtest - clonezilla
- blancco
- name: "Check for WinPE deployment content on USB" - memtest
stat:
path: "{{ usb_mount }}/images" - name: "Check for WinPE deployment content on USB"
register: usb_images_dir stat:
path: "{{ usb_mount }}/images"
- name: "Import WinPE deployment content from USB (if present)" register: usb_images_dir
shell: >
cp -rn "{{ usb_mount }}/images/{{ item }}/"* "{{ samba_share }}/{{ item }}/" 2>/dev/null || true - name: "Import WinPE deployment content from USB (if present)"
loop: "{{ image_types }}" shell: >
when: usb_images_dir.stat.exists cp -rn "{{ usb_mount }}/images/{{ item }}/"* "{{ samba_share }}/{{ item }}/" 2>/dev/null || true
loop: "{{ image_types }}"
- name: "Restart and enable services" when: usb_images_dir.stat.exists
systemd:
name: "{{ item }}" - name: "Restart and enable services"
state: restarted systemd:
enabled: yes name: "{{ item }}"
loop: state: restarted
- dnsmasq enabled: yes
- apache2 loop:
- smbd - dnsmasq
- apache2
- name: "Allow necessary firewall ports (UFW)" - smbd
ufw:
rule: allow - name: "Allow necessary firewall ports (UFW)"
port: "{{ item }}" ufw:
proto: "{{ 'udp' if item in ['67','69'] else 'tcp' }}" rule: allow
loop: port: "{{ item }}"
- 67 proto: "{{ 'udp' if item in ['67','69'] else 'tcp' }}"
- 69 loop:
- 80 - 67
- 4433 - 69
- 445 - 80
- 9009 - 4433
- 445
- name: "Enable UFW firewall" - 9009
ufw:
state: enabled - name: "Enable UFW firewall"
policy: deny ufw:
state: enabled
- name: "Schedule dnsmasq restart 15s after reboot" policy: deny
cron:
name: "Restart dnsmasq after reboot" - name: "Schedule dnsmasq restart 15s after reboot"
user: root cron:
special_time: "reboot" name: "Restart dnsmasq after reboot"
job: "/bin/sleep 15 && /usr/bin/systemctl restart dnsmasq.service" user: root
special_time: "reboot"
# --- Web Management App (Flask) --- job: "/bin/sleep 15 && /usr/bin/systemctl restart dnsmasq.service"
- name: "Create webapp directory"
file: # --- Web Management App (Flask) ---
path: /opt/pxe-webapp - name: "Create webapp directory"
state: directory file:
mode: '0755' path: /opt/pxe-webapp
state: directory
- name: "Copy webapp from USB" mode: '0755'
shell: >
cp -r "{{ usb_mount }}/../webapp/"* /opt/pxe-webapp/ 2>/dev/null || - name: "Copy webapp from USB"
cp -r "{{ usb_mount }}/webapp/"* /opt/pxe-webapp/ 2>/dev/null || true shell: >
args: cp -r "{{ usb_mount }}/../webapp/"* /opt/pxe-webapp/ 2>/dev/null ||
creates: /opt/pxe-webapp/app.py cp -r "{{ usb_mount }}/webapp/"* /opt/pxe-webapp/ 2>/dev/null || true
args:
- name: "Create Python virtual environment for webapp" creates: /opt/pxe-webapp/app.py
command: python3 -m venv /opt/pxe-webapp/venv
args: - name: "Create Python virtual environment for webapp"
creates: /opt/pxe-webapp/venv/bin/python command: python3 -m venv /opt/pxe-webapp/venv
args:
- name: "Install webapp Python dependencies (offline wheels)" creates: /opt/pxe-webapp/venv/bin/python
shell: >
/opt/pxe-webapp/venv/bin/pip install --no-index - name: "Install webapp Python dependencies (offline wheels)"
--find-links="{{ usb_mount }}/../pip-wheels/" shell: >
--find-links="{{ usb_mount }}/pip-wheels/" /opt/pxe-webapp/venv/bin/pip install --no-index
-r /opt/pxe-webapp/requirements.txt 2>/dev/null || --find-links="{{ usb_mount }}/../pip-wheels/"
/opt/pxe-webapp/venv/bin/pip install -r /opt/pxe-webapp/requirements.txt --find-links="{{ usb_mount }}/pip-wheels/"
-r /opt/pxe-webapp/requirements.txt 2>/dev/null ||
- name: "Create systemd service for PXE webapp" /opt/pxe-webapp/venv/bin/pip install -r /opt/pxe-webapp/requirements.txt
copy:
dest: /etc/systemd/system/pxe-webapp.service - name: "Create systemd service for PXE webapp"
content: | copy:
[Unit] dest: /etc/systemd/system/pxe-webapp.service
Description=PXE Server Web Management content: |
After=network.target apache2.service [Unit]
Description=PXE Server Web Management
[Service] After=network.target apache2.service
Type=simple
User=root [Service]
WorkingDirectory=/opt/pxe-webapp Type=simple
Environment=SAMBA_SHARE={{ samba_share }} User=root
Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla WorkingDirectory=/opt/pxe-webapp
Environment=WEB_ROOT={{ web_root }} Environment=SAMBA_SHARE={{ samba_share }}
Environment=BLANCCO_REPORTS=/srv/samba/blancco-reports Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla
Environment=AUDIT_LOG=/var/log/pxe-webapp-audit.log Environment=WEB_ROOT={{ web_root }}
ExecStart=/opt/pxe-webapp/venv/bin/python app.py Environment=BLANCCO_REPORTS=/srv/samba/blancco-reports
Restart=always Environment=AUDIT_LOG=/var/log/pxe-webapp-audit.log
RestartSec=5 ExecStart=/opt/pxe-webapp/venv/bin/python app.py
Restart=always
[Install] RestartSec=5
WantedBy=multi-user.target
[Install]
- name: "Enable and start PXE webapp service" WantedBy=multi-user.target
systemd:
name: pxe-webapp - name: "Enable and start PXE webapp service"
state: started systemd:
enabled: yes name: pxe-webapp
daemon_reload: yes state: started
enabled: yes
- name: "Configure Apache reverse proxy for webapp" daemon_reload: yes
copy:
dest: /etc/apache2/sites-available/pxe-webapp.conf - name: "Configure Apache reverse proxy for webapp"
content: | copy:
<VirtualHost *:80> dest: /etc/apache2/sites-available/pxe-webapp.conf
ProxyPreserveHost On content: |
ProxyPass /manage http://127.0.0.1:9009/ <VirtualHost *:80>
ProxyPassReverse /manage http://127.0.0.1:9009/ ProxyPreserveHost On
</VirtualHost> ProxyPass /manage http://127.0.0.1:9009/
ProxyPassReverse /manage http://127.0.0.1:9009/
- name: "Enable Apache proxy modules" </VirtualHost>
command: a2enmod proxy proxy_http
args: - name: "Enable Apache proxy modules"
creates: /etc/apache2/mods-enabled/proxy.load command: a2enmod proxy proxy_http
args:
- name: "Enable webapp Apache site" creates: /etc/apache2/mods-enabled/proxy.load
command: a2ensite pxe-webapp.conf
args: - name: "Enable webapp Apache site"
creates: /etc/apache2/sites-enabled/pxe-webapp.conf command: a2ensite pxe-webapp.conf
args:
- name: "Configure static IP for PXE interface" creates: /etc/apache2/sites-enabled/pxe-webapp.conf
copy:
dest: /etc/netplan/50-cloud-init.yaml - name: "Configure static IP for PXE interface"
backup: yes copy:
content: | dest: /etc/netplan/50-cloud-init.yaml
network: backup: yes
version: 2 content: |
renderer: networkd network:
ethernets: version: 2
{{ pxe_iface }}: renderer: networkd
dhcp4: no ethernets:
addresses: [10.9.100.1/24] {{ pxe_iface }}:
notify: "Apply netplan" dhcp4: no
addresses: [10.9.100.1/24]
handlers: notify: "Apply netplan"
- name: "Apply netplan"
command: netplan apply handlers:
- name: "Apply netplan"
command: netplan apply

View File

@@ -1,197 +1,197 @@
#!/bin/bash #!/bin/bash
# #
# prepare-boot-tools.sh — Download/extract boot files for PXE boot tools # prepare-boot-tools.sh — Download/extract boot files for PXE boot tools
# #
# Downloads Clonezilla Live and Memtest86+ for PXE booting, # Downloads Clonezilla Live and Memtest86+ for PXE booting,
# and extracts Blancco Drive Eraser from its ISO. # and extracts Blancco Drive Eraser from its ISO.
# #
# Usage: # Usage:
# ./prepare-boot-tools.sh [/path/to/blancco.iso] # ./prepare-boot-tools.sh [/path/to/blancco.iso]
# #
# Output directories: # Output directories:
# boot-tools/clonezilla/ — vmlinuz, initrd.img, filesystem.squashfs # boot-tools/clonezilla/ — vmlinuz, initrd.img, filesystem.squashfs
# boot-tools/blancco/ — extracted boot files or ISO for memdisk # boot-tools/blancco/ — extracted boot files or ISO for memdisk
# boot-tools/memtest/ — memtest.efi # boot-tools/memtest/ — memtest.efi
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
OUT_DIR="$SCRIPT_DIR/boot-tools" OUT_DIR="$SCRIPT_DIR/boot-tools"
BLANCCO_ISO="${1:-}" BLANCCO_ISO="${1:-}"
# Auto-detect Blancco ISO in project directory # Auto-detect Blancco ISO in project directory
if [ -z "$BLANCCO_ISO" ]; then if [ -z "$BLANCCO_ISO" ]; then
BLANCCO_ISO=$(find "$SCRIPT_DIR" -maxdepth 1 -name '*DriveEraser*.iso' -o -name '*blancco*.iso' 2>/dev/null | head -1) BLANCCO_ISO=$(find "$SCRIPT_DIR" -maxdepth 1 -name '*DriveEraser*.iso' -o -name '*blancco*.iso' 2>/dev/null | head -1)
fi fi
mkdir -p "$OUT_DIR"/{clonezilla,blancco,memtest} mkdir -p "$OUT_DIR"/{clonezilla,blancco,memtest}
echo "============================================" echo "============================================"
echo "PXE Boot Tools Preparation" echo "PXE Boot Tools Preparation"
echo "============================================" echo "============================================"
# --- Clonezilla Live --- # --- Clonezilla Live ---
echo "" echo ""
echo "[1/3] Clonezilla Live" echo "[1/3] Clonezilla Live"
CLONEZILLA_VERSION="3.2.1-6" CLONEZILLA_VERSION="3.2.1-6"
CLONEZILLA_FILE="clonezilla-live-${CLONEZILLA_VERSION}-amd64.zip" CLONEZILLA_FILE="clonezilla-live-${CLONEZILLA_VERSION}-amd64.zip"
CLONEZILLA_URL="https://sourceforge.net/projects/clonezilla/files/clonezilla_live_stable/${CLONEZILLA_VERSION}/${CLONEZILLA_FILE}/download" 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 if [ -f "$OUT_DIR/clonezilla/vmlinuz" ] && [ -f "$OUT_DIR/clonezilla/filesystem.squashfs" ]; then
echo " Already prepared, skipping. Delete boot-tools/clonezilla/ to re-download." echo " Already prepared, skipping. Delete boot-tools/clonezilla/ to re-download."
else else
echo " Downloading Clonezilla Live ${CLONEZILLA_VERSION}..." echo " Downloading Clonezilla Live ${CLONEZILLA_VERSION}..."
TMPDIR=$(mktemp -d) TMPDIR=$(mktemp -d)
wget -q --show-progress -O "$TMPDIR/$CLONEZILLA_FILE" "$CLONEZILLA_URL" || { wget -q --show-progress -O "$TMPDIR/$CLONEZILLA_FILE" "$CLONEZILLA_URL" || {
echo " ERROR: Download failed. Trying alternative URL..." echo " ERROR: Download failed. Trying alternative URL..."
# Fallback: try OSDN mirror # Fallback: try OSDN mirror
wget -q --show-progress -O "$TMPDIR/$CLONEZILLA_FILE" \ wget -q --show-progress -O "$TMPDIR/$CLONEZILLA_FILE" \
"https://free.nchc.org.tw/clonezilla-live/stable/${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 " ERROR: Could not download Clonezilla. Download manually and place in boot-tools/clonezilla/"
echo " Need: vmlinuz, initrd.img, filesystem.squashfs from the live ZIP" echo " Need: vmlinuz, initrd.img, filesystem.squashfs from the live ZIP"
} }
} }
if [ -f "$TMPDIR/$CLONEZILLA_FILE" ]; then if [ -f "$TMPDIR/$CLONEZILLA_FILE" ]; then
echo " Extracting PXE boot files..." echo " Extracting PXE boot files..."
unzip -o -j "$TMPDIR/$CLONEZILLA_FILE" "live/vmlinuz" -d "$OUT_DIR/clonezilla/" 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/initrd.img" -d "$OUT_DIR/clonezilla/"
unzip -o -j "$TMPDIR/$CLONEZILLA_FILE" "live/filesystem.squashfs" -d "$OUT_DIR/clonezilla/" unzip -o -j "$TMPDIR/$CLONEZILLA_FILE" "live/filesystem.squashfs" -d "$OUT_DIR/clonezilla/"
rm -rf "$TMPDIR" rm -rf "$TMPDIR"
echo " Done." echo " Done."
fi fi
fi fi
ls -lh "$OUT_DIR/clonezilla/" 2>/dev/null | grep -E 'vmlinuz|initrd|squashfs' | sed 's/^/ /' ls -lh "$OUT_DIR/clonezilla/" 2>/dev/null | grep -E 'vmlinuz|initrd|squashfs' | sed 's/^/ /'
# --- Blancco Drive Eraser --- # --- Blancco Drive Eraser ---
echo "" echo ""
echo "[2/3] Blancco Drive Eraser" echo "[2/3] Blancco Drive Eraser"
if [ -n "$BLANCCO_ISO" ] && [ -f "$BLANCCO_ISO" ]; then if [ -n "$BLANCCO_ISO" ] && [ -f "$BLANCCO_ISO" ]; then
echo " Extracting from: $BLANCCO_ISO" echo " Extracting from: $BLANCCO_ISO"
echo " Using 7z to extract (no root required)..." echo " Using 7z to extract (no root required)..."
# Blancco is Arch Linux-based. We need: # Blancco is Arch Linux-based. We need:
# arch/boot/x86_64/vmlinuz-bde-linux # arch/boot/x86_64/vmlinuz-bde-linux
# arch/boot/x86_64/initramfs-bde-linux.img # arch/boot/x86_64/initramfs-bde-linux.img
# arch/boot/intel-ucode.img # arch/boot/intel-ucode.img
# arch/boot/amd-ucode.img # arch/boot/amd-ucode.img
# arch/boot/config.img # arch/boot/config.img
# arch/x86_64/airootfs.sfs # arch/x86_64/airootfs.sfs
TMPDIR=$(mktemp -d) TMPDIR=$(mktemp -d)
7z x -o"$TMPDIR" "$BLANCCO_ISO" \ 7z x -o"$TMPDIR" "$BLANCCO_ISO" \
"arch/boot/x86_64/vmlinuz-bde-linux" \ "arch/boot/x86_64/vmlinuz-bde-linux" \
"arch/boot/x86_64/initramfs-bde-linux.img" \ "arch/boot/x86_64/initramfs-bde-linux.img" \
"arch/boot/intel-ucode.img" \ "arch/boot/intel-ucode.img" \
"arch/boot/amd-ucode.img" \ "arch/boot/amd-ucode.img" \
"arch/boot/config.img" \ "arch/boot/config.img" \
"arch/x86_64/airootfs.sfs" \ "arch/x86_64/airootfs.sfs" \
-r 2>/dev/null || { -r 2>/dev/null || {
echo " 7z extraction failed. Install p7zip-full: apt install p7zip-full" echo " 7z extraction failed. Install p7zip-full: apt install p7zip-full"
} }
# Flatten into blancco/ directory for HTTP serving # Flatten into blancco/ directory for HTTP serving
if [ -f "$TMPDIR/arch/boot/x86_64/vmlinuz-bde-linux" ]; then 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/vmlinuz-bde-linux" "$OUT_DIR/blancco/"
cp "$TMPDIR/arch/boot/x86_64/initramfs-bde-linux.img" "$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/intel-ucode.img" "$OUT_DIR/blancco/"
cp "$TMPDIR/arch/boot/amd-ucode.img" "$OUT_DIR/blancco/" cp "$TMPDIR/arch/boot/amd-ucode.img" "$OUT_DIR/blancco/"
cp "$TMPDIR/arch/boot/config.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 # airootfs.sfs needs to be in arch/x86_64/ path relative to HTTP root
mkdir -p "$OUT_DIR/blancco/arch/x86_64" mkdir -p "$OUT_DIR/blancco/arch/x86_64"
cp "$TMPDIR/arch/x86_64/airootfs.sfs" "$OUT_DIR/blancco/arch/x86_64/" cp "$TMPDIR/arch/x86_64/airootfs.sfs" "$OUT_DIR/blancco/arch/x86_64/"
echo " Extracted Blancco boot files." echo " Extracted Blancco boot files."
# Patch config.img to auto-save reports to PXE server Samba share # Patch config.img to auto-save reports to PXE server Samba share
if [ -f "$OUT_DIR/blancco/config.img" ]; then if [ -f "$OUT_DIR/blancco/config.img" ]; then
echo " Patching config.img for network report storage..." echo " Patching config.img for network report storage..."
CFGTMP=$(mktemp -d) CFGTMP=$(mktemp -d)
cd "$CFGTMP" cd "$CFGTMP"
cpio -id < "$OUT_DIR/blancco/config.img" 2>/dev/null cpio -id < "$OUT_DIR/blancco/config.img" 2>/dev/null
if [ -f "$CFGTMP/preferences.xml" ]; then if [ -f "$CFGTMP/preferences.xml" ]; then
# Set network share to PXE server's blancco-reports Samba share # Set network share to PXE server's blancco-reports Samba share
sed -i 's|<hostname></hostname>|<hostname>10.9.100.1</hostname>|' "$CFGTMP/preferences.xml" sed -i 's|<hostname></hostname>|<hostname>10.9.100.1</hostname>|' "$CFGTMP/preferences.xml"
sed -i 's|<path></path>|<path>blancco-reports</path>|' "$CFGTMP/preferences.xml" sed -i 's|<path></path>|<path>blancco-reports</path>|' "$CFGTMP/preferences.xml"
# Enable auto-backup of reports to the network share # Enable auto-backup of reports to the network share
sed -i 's|<auto_backup>false</auto_backup>|<auto_backup>true</auto_backup>|' "$CFGTMP/preferences.xml" sed -i 's|<auto_backup>false</auto_backup>|<auto_backup>true</auto_backup>|' "$CFGTMP/preferences.xml"
# Repack config.img # Repack config.img
ls -1 | cpio -o -H newc > "$OUT_DIR/blancco/config.img" 2>/dev/null 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" echo " Reports will auto-save to \\\\10.9.100.1\\blancco-reports"
fi fi
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
rm -rf "$CFGTMP" rm -rf "$CFGTMP"
fi fi
else else
echo " Could not extract boot files from ISO." echo " Could not extract boot files from ISO."
fi fi
rm -rf "$TMPDIR" rm -rf "$TMPDIR"
else else
echo " No Blancco ISO found. Provide path as argument or place in project directory." echo " No Blancco ISO found. Provide path as argument or place in project directory."
echo " Usage: $0 /path/to/DriveEraser.iso" echo " Usage: $0 /path/to/DriveEraser.iso"
fi fi
ls -lh "$OUT_DIR/blancco/" 2>/dev/null | grep -v '^total' | sed 's/^/ /' ls -lh "$OUT_DIR/blancco/" 2>/dev/null | grep -v '^total' | sed 's/^/ /'
# --- Memtest86+ --- # --- Memtest86+ ---
echo "" echo ""
echo "[3/3] Memtest86+" echo "[3/3] Memtest86+"
MEMTEST_VERSION="7.20" MEMTEST_VERSION="7.20"
MEMTEST_URL="https://memtest.org/download/${MEMTEST_VERSION}/mt86plus_${MEMTEST_VERSION}.binaries.zip" MEMTEST_URL="https://memtest.org/download/${MEMTEST_VERSION}/mt86plus_${MEMTEST_VERSION}.binaries.zip"
if [ -f "$OUT_DIR/memtest/memtest.efi" ]; then if [ -f "$OUT_DIR/memtest/memtest.efi" ]; then
echo " Already prepared, skipping." echo " Already prepared, skipping."
else else
echo " Downloading Memtest86+ v${MEMTEST_VERSION}..." echo " Downloading Memtest86+ v${MEMTEST_VERSION}..."
TMPDIR=$(mktemp -d) TMPDIR=$(mktemp -d)
wget -q --show-progress -O "$TMPDIR/memtest.zip" "$MEMTEST_URL" || { wget -q --show-progress -O "$TMPDIR/memtest.zip" "$MEMTEST_URL" || {
echo " ERROR: Download failed. Download manually from https://memtest.org" echo " ERROR: Download failed. Download manually from https://memtest.org"
TMPDIR="" TMPDIR=""
} }
if [ -n "$TMPDIR" ] && [ -f "$TMPDIR/memtest.zip" ]; then if [ -n "$TMPDIR" ] && [ -f "$TMPDIR/memtest.zip" ]; then
echo " Extracting EFI binary..." 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" "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 -j "$TMPDIR/memtest.zip" "mt86plus_${MEMTEST_VERSION}.x64.efi" -d "$OUT_DIR/memtest/" 2>/dev/null || \
unzip -o "$TMPDIR/memtest.zip" -d "$TMPDIR/extract/" unzip -o "$TMPDIR/memtest.zip" -d "$TMPDIR/extract/"
# Find the EFI file regardless of exact name # Find the EFI file regardless of exact name
EFI_FILE=$(find "$TMPDIR" "$OUT_DIR/memtest" -name '*.efi' -name '*64*' 2>/dev/null | head -1) 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 if [ -n "$EFI_FILE" ] && [ ! -f "$OUT_DIR/memtest/memtest.efi" ]; then
cp "$EFI_FILE" "$OUT_DIR/memtest/memtest.efi" cp "$EFI_FILE" "$OUT_DIR/memtest/memtest.efi"
fi fi
rm -rf "$TMPDIR" rm -rf "$TMPDIR"
echo " Done." echo " Done."
fi fi
fi fi
ls -lh "$OUT_DIR/memtest/" 2>/dev/null | grep -v '^total' | sed 's/^/ /' ls -lh "$OUT_DIR/memtest/" 2>/dev/null | grep -v '^total' | sed 's/^/ /'
# --- Summary --- # --- Summary ---
echo "" echo ""
echo "============================================" echo "============================================"
echo "Boot tools prepared in: $OUT_DIR/" echo "Boot tools prepared in: $OUT_DIR/"
echo "============================================" echo "============================================"
echo "" echo ""
for tool in clonezilla blancco memtest; do for tool in clonezilla blancco memtest; do
COUNT=$(find "$OUT_DIR/$tool" -type f 2>/dev/null | wc -l) COUNT=$(find "$OUT_DIR/$tool" -type f 2>/dev/null | wc -l)
SIZE=$(du -sh "$OUT_DIR/$tool" 2>/dev/null | cut -f1) SIZE=$(du -sh "$OUT_DIR/$tool" 2>/dev/null | cut -f1)
printf " %-15s %s (%d files)\n" "$tool" "$SIZE" "$COUNT" printf " %-15s %s (%d files)\n" "$tool" "$SIZE" "$COUNT"
done done
echo "" echo ""
echo "These files need to be copied to the PXE server's web root:" echo "These files need to be copied to the PXE server's web root:"
echo " /var/www/html/clonezilla/" echo " /var/www/html/clonezilla/"
echo " /var/www/html/blancco/" echo " /var/www/html/blancco/"
echo " /var/www/html/memtest/" echo " /var/www/html/memtest/"
echo "" echo ""
echo "The build-usb.sh script will include them automatically," echo "The build-usb.sh script will include them automatically,"
echo "or copy them manually to the server." echo "or copy them manually to the server."
echo "" echo ""

View File

@@ -1,174 +1,188 @@
#!/bin/bash #!/bin/bash
# #
# test-vm.sh — Create a test VM to validate the PXE server setup # test-vm.sh — Create a test VM to validate the PXE server setup
# #
# This script: # This script:
# 1. Builds a CIDATA ISO with autoinstall config, packages, playbook, and webapp # 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) # 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 # 3. Launches an Ubuntu 24.04 Server VM that auto-installs and configures itself
# #
# Usage: # Usage:
# ./test-vm.sh /path/to/ubuntu-24.04-live-server-amd64.iso # ./test-vm.sh /path/to/ubuntu-24.04-live-server-amd64.iso
# #
# After install completes (~10-15 min), access the webapp at: # After install completes (~10-15 min), access the webapp at:
# http://10.9.100.1:9009 # http://10.9.100.1:9009
# #
# To watch progress: # To watch progress:
# virsh console pxe-test # virsh console pxe-test
# #
# To clean up: # To clean up:
# ./test-vm.sh --destroy # ./test-vm.sh --destroy
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VM_NAME="pxe-test" VM_NAME="pxe-test"
NET_NAME="pxe-test" NET_NAME="pxe-test"
VM_DISK="/var/lib/libvirt/images/${VM_NAME}.qcow2" VM_DISK="/var/lib/libvirt/images/${VM_NAME}.qcow2"
CIDATA_ISO="/tmp/${VM_NAME}-cidata.iso" CIDATA_ISO="/tmp/${VM_NAME}-cidata.iso"
VM_RAM=4096 VM_RAM=4096
VM_CPUS=2 VM_CPUS=2
VM_DISK_SIZE=40 # GB VM_DISK_SIZE=40 # GB
# --- Handle --destroy flag --- # --- Handle --destroy flag ---
if [ "${1:-}" = "--destroy" ]; then if [ "${1:-}" = "--destroy" ]; then
echo "Destroying test environment..." echo "Destroying test environment..."
virsh destroy "$VM_NAME" 2>/dev/null || true virsh destroy "$VM_NAME" 2>/dev/null || true
virsh undefine "$VM_NAME" --remove-all-storage 2>/dev/null || true virsh undefine "$VM_NAME" 2>/dev/null || true
virsh net-destroy "$NET_NAME" 2>/dev/null || true rm -f "$VM_DISK"
virsh net-undefine "$NET_NAME" 2>/dev/null || true virsh net-destroy "$NET_NAME" 2>/dev/null || true
rm -f "$CIDATA_ISO" virsh net-undefine "$NET_NAME" 2>/dev/null || true
echo "Done." rm -f "$CIDATA_ISO"
exit 0 echo "Done."
fi exit 0
fi
# --- Validate Ubuntu ISO argument ---
UBUNTU_ISO="${1:-}" # --- Validate Ubuntu ISO argument ---
if [ -z "$UBUNTU_ISO" ] || [ ! -f "$UBUNTU_ISO" ]; then UBUNTU_ISO="${1:-}"
echo "Usage: $0 /path/to/ubuntu-24.04-live-server-amd64.iso" if [ -z "$UBUNTU_ISO" ] || [ ! -f "$UBUNTU_ISO" ]; then
echo "" echo "Usage: $0 /path/to/ubuntu-24.04-live-server-amd64.iso"
echo "Download from: https://ubuntu.com/download/server" echo ""
echo "" echo "Download from: https://ubuntu.com/download/server"
echo "Other commands:" echo ""
echo " $0 --destroy Remove the test VM and network" echo "Other commands:"
exit 1 echo " $0 --destroy Remove the test VM and network"
fi exit 1
fi
echo "============================================"
echo "PXE Server Test VM Setup" echo "============================================"
echo "============================================" echo "PXE Server Test VM Setup"
echo "" echo "============================================"
echo ""
# --- Step 1: Build CIDATA ISO ---
echo "[1/4] Building CIDATA ISO..." # --- Step 1: Build CIDATA ISO ---
CIDATA_DIR=$(mktemp -d) echo "[1/4] Building CIDATA ISO..."
CIDATA_DIR=$(mktemp -d)
# Autoinstall config
cp "$SCRIPT_DIR/autoinstall/user-data" "$CIDATA_DIR/user-data" # Autoinstall config
touch "$CIDATA_DIR/meta-data" 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 # Offline .deb packages
mkdir -p "$CIDATA_DIR/packages" if [ -d "$SCRIPT_DIR/offline-packages" ]; then
cp "$SCRIPT_DIR/offline-packages/"*.deb "$CIDATA_DIR/packages/" 2>/dev/null || true mkdir -p "$CIDATA_DIR/packages"
echo " Copied $(ls -1 "$CIDATA_DIR/packages/"*.deb 2>/dev/null | wc -l) .deb packages" cp "$SCRIPT_DIR/offline-packages/"*.deb "$CIDATA_DIR/packages/" 2>/dev/null || true
else echo " Copied $(ls -1 "$CIDATA_DIR/packages/"*.deb 2>/dev/null | wc -l) .deb packages"
echo " WARNING: No offline-packages/ directory. Run download-packages.sh first." else
fi echo " WARNING: No offline-packages/ directory. Run download-packages.sh first."
fi
# Ansible playbook
mkdir -p "$CIDATA_DIR/playbook" # Ansible playbook
cp "$SCRIPT_DIR/playbook/"* "$CIDATA_DIR/playbook/" 2>/dev/null || true mkdir -p "$CIDATA_DIR/playbook"
echo " Copied playbook/" cp "$SCRIPT_DIR/playbook/"* "$CIDATA_DIR/playbook/" 2>/dev/null || true
echo " Copied playbook/"
# Webapp
if [ -d "$SCRIPT_DIR/webapp" ]; then # Webapp
mkdir -p "$CIDATA_DIR/webapp" if [ -d "$SCRIPT_DIR/webapp" ]; then
cp "$SCRIPT_DIR/webapp/app.py" "$SCRIPT_DIR/webapp/requirements.txt" "$CIDATA_DIR/webapp/" mkdir -p "$CIDATA_DIR/webapp"
cp -r "$SCRIPT_DIR/webapp/templates" "$SCRIPT_DIR/webapp/static" "$CIDATA_DIR/webapp/" cp "$SCRIPT_DIR/webapp/app.py" "$SCRIPT_DIR/webapp/requirements.txt" "$CIDATA_DIR/webapp/"
echo " Copied webapp/" cp -r "$SCRIPT_DIR/webapp/templates" "$SCRIPT_DIR/webapp/static" "$CIDATA_DIR/webapp/"
fi echo " Copied webapp/"
fi
# Pip wheels
if [ -d "$SCRIPT_DIR/pip-wheels" ]; then # Pip wheels
cp -r "$SCRIPT_DIR/pip-wheels" "$CIDATA_DIR/pip-wheels" if [ -d "$SCRIPT_DIR/pip-wheels" ]; then
echo " Copied pip-wheels/" cp -r "$SCRIPT_DIR/pip-wheels" "$CIDATA_DIR/pip-wheels"
fi echo " Copied pip-wheels/"
fi
# Boot tools
if [ -d "$SCRIPT_DIR/boot-tools" ]; then # Boot tools
cp -r "$SCRIPT_DIR/boot-tools" "$CIDATA_DIR/boot-tools" if [ -d "$SCRIPT_DIR/boot-tools" ]; then
echo " Copied boot-tools/" cp -r "$SCRIPT_DIR/boot-tools" "$CIDATA_DIR/boot-tools"
fi echo " Copied boot-tools/"
fi
# Generate the CIDATA ISO
genisoimage -output "$CIDATA_ISO" -volid CIDATA -joliet -rock "$CIDATA_DIR" 2>/dev/null # Generate the CIDATA ISO
CIDATA_SIZE=$(du -sh "$CIDATA_ISO" | cut -f1) genisoimage -output "$CIDATA_ISO" -volid CIDATA -joliet -rock "$CIDATA_DIR" 2>/dev/null
echo " CIDATA ISO: $CIDATA_ISO ($CIDATA_SIZE)" CIDATA_SIZE=$(du -sh "$CIDATA_ISO" | cut -f1)
rm -rf "$CIDATA_DIR" echo " CIDATA ISO: $CIDATA_ISO ($CIDATA_SIZE)"
rm -rf "$CIDATA_DIR"
# --- Step 2: Create isolated network ---
echo "" # --- Step 2: Create isolated network ---
echo "[2/4] Setting up isolated network ($NET_NAME)..." echo ""
echo "[2/4] Setting up isolated network ($NET_NAME)..."
# Check if network already exists
if virsh net-info "$NET_NAME" &>/dev/null; then # Check if network already exists
echo " Network $NET_NAME already exists, reusing." if virsh net-info "$NET_NAME" &>/dev/null; then
else echo " Network $NET_NAME already exists, reusing."
cat > /tmp/${NET_NAME}-net.xml <<NETEOF else
<network> cat > /tmp/${NET_NAME}-net.xml <<NETEOF
<name>${NET_NAME}</name> <network>
<bridge name="virbr-pxe" stp="on" delay="0"/> <name>${NET_NAME}</name>
<ip address="10.9.100.254" netmask="255.255.255.0"/> <bridge name="virbr-pxe" stp="on" delay="0"/>
</network> <ip address="10.9.100.254" netmask="255.255.255.0"/>
NETEOF </network>
virsh net-define /tmp/${NET_NAME}-net.xml NETEOF
virsh net-start "$NET_NAME" virsh net-define /tmp/${NET_NAME}-net.xml
rm -f /tmp/${NET_NAME}-net.xml virsh net-start "$NET_NAME"
echo " Created isolated network 10.9.100.0/24 (no DHCP, no NAT)" rm -f /tmp/${NET_NAME}-net.xml
fi echo " Created isolated network 10.9.100.0/24 (no DHCP, no NAT)"
fi
# --- Step 3: Create VM disk ---
echo "" # --- Step 3: Create VM disk ---
echo "[3/4] Creating VM disk (${VM_DISK_SIZE}GB)..." echo ""
if [ -f "$VM_DISK" ]; then echo "[3/4] Creating VM disk (${VM_DISK_SIZE}GB)..."
echo " Disk already exists. Destroy first with: $0 --destroy" if [ -f "$VM_DISK" ]; then
exit 1 echo " Disk already exists. Destroy first with: $0 --destroy"
fi exit 1
qemu-img create -f qcow2 "$VM_DISK" "${VM_DISK_SIZE}G" fi
qemu-img create -f qcow2 "$VM_DISK" "${VM_DISK_SIZE}G"
# --- Step 4: Launch VM ---
echo "" # --- Step 4: Extract kernel/initrd from ISO ---
echo "[4/4] Launching VM ($VM_NAME)..." echo ""
virt-install \ echo "[4/5] Extracting kernel and initrd from ISO..."
--name "$VM_NAME" \ ISO_MNT=$(mktemp -d)
--memory "$VM_RAM" \ mount -o loop,ro "$UBUNTU_ISO" "$ISO_MNT"
--vcpus "$VM_CPUS" \ KERNEL="/tmp/${VM_NAME}-vmlinuz"
--disk path="$VM_DISK",format=qcow2 \ INITRD="/tmp/${VM_NAME}-initrd"
--cdrom "$UBUNTU_ISO" \ cp "$ISO_MNT/casper/vmlinuz" "$KERNEL"
--disk path="$CIDATA_ISO",device=cdrom \ cp "$ISO_MNT/casper/initrd" "$INITRD"
--network network="$NET_NAME" \ umount "$ISO_MNT"
--os-variant ubuntu24.04 \ rmdir "$ISO_MNT"
--graphics none \ echo " Extracted vmlinuz and initrd from casper/"
--console pty,target_type=serial \
--extra-args "console=ttyS0,115200n8 autoinstall" \ # --- Step 5: Launch VM ---
--noautoconsole echo ""
echo "[5/5] Launching VM ($VM_NAME)..."
echo "" virt-install \
echo "============================================" --name "$VM_NAME" \
echo "VM launched! The autoinstall will take ~10-15 minutes." --memory "$VM_RAM" \
echo "============================================" --vcpus "$VM_CPUS" \
echo "" --disk path="$VM_DISK",format=qcow2 \
echo "Watch progress:" --disk path="$UBUNTU_ISO",device=cdrom,readonly=on \
echo " virsh console $VM_NAME" --disk path="$CIDATA_ISO",device=cdrom \
echo " (Press Ctrl+] to detach)" --network network="$NET_NAME" \
echo "" --os-variant ubuntu24.04 \
echo "After install + first boot:" --graphics none \
echo " SSH: ssh pxe@10.9.100.1" --console pty,target_type=serial \
echo " Webapp: http://10.9.100.1:9009" --install kernel="$KERNEL",initrd="$INITRD",kernel_args="console=ttyS0,115200n8 autoinstall" \
echo "" --noautoconsole
echo "Manage:"
echo " virsh start $VM_NAME" echo ""
echo " virsh shutdown $VM_NAME" echo "============================================"
echo " $0 --destroy (remove everything)" echo "VM launched! The autoinstall will take ~10-15 minutes."
echo "" 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 ""