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:
226
README.md
Normal file
226
README.md
Normal 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
162
SETUP.md
@@ -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.10–100) |
|
| 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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
480
build-usb.sh
480
build-usb.sh
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
362
test-vm.sh
362
test-vm.sh
@@ -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 ""
|
||||||
|
|||||||
Reference in New Issue
Block a user