Autoinstall user-data now disables cloud-init in multiple stages (late-commands + runcmd + systemd masks) to prevent post-install hangs. Also disables networkd-wait-online for air-gapped networks. build-usb.sh switched from in-place ISO patching to full extract and rebuild with xorriso mkisofs for reliable UEFI boot. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
307 lines
9.8 KiB
Bash
Executable File
307 lines
9.8 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# build-usb.sh — Build a bootable PXE-server installer USB
|
|
#
|
|
# Creates a two-partition USB:
|
|
# Partition 1: Ubuntu Server 24.04 installer (ISO contents)
|
|
# Partition 2: CIDATA volume (autoinstall config, .debs, playbook)
|
|
#
|
|
# The target machine boots from this USB, Ubuntu auto-installs with
|
|
# cloud-init (user-data/meta-data from CIDATA), installs offline .debs,
|
|
# and on first boot runs the Ansible playbook to configure PXE services.
|
|
#
|
|
# Usage:
|
|
# sudo ./build-usb.sh /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso
|
|
#
|
|
# WARNING: This will ERASE the target USB device.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
AUTOINSTALL_DIR="$SCRIPT_DIR/autoinstall"
|
|
PLAYBOOK_DIR="$SCRIPT_DIR/playbook"
|
|
OFFLINE_PKG_DIR="$SCRIPT_DIR/offline-packages"
|
|
|
|
# --- Validate arguments ---
|
|
if [ $# -lt 2 ]; then
|
|
echo "Usage: sudo $0 /dev/sdX /path/to/ubuntu-24.04.iso [/path/to/winpe-images]"
|
|
echo ""
|
|
echo " The optional third argument is the path to WinPE deployment content"
|
|
echo " (e.g., the mounted Media Creator LITE USB). If provided, the images"
|
|
echo " will be bundled onto the CIDATA partition for automatic import."
|
|
echo ""
|
|
echo "Available removable devices:"
|
|
lsblk -d -o NAME,SIZE,TRAN,RM | grep -E '^\S+\s+\S+\s+(usb)\s+1'
|
|
exit 1
|
|
fi
|
|
|
|
USB_DEV="$1"
|
|
ISO_PATH="$2"
|
|
WINPE_SOURCE="${3:-}"
|
|
|
|
# Safety checks
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
echo "ERROR: Must run as root (sudo)."
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -b "$USB_DEV" ]; then
|
|
echo "ERROR: $USB_DEV is not a block device."
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -f "$ISO_PATH" ]; then
|
|
echo "ERROR: ISO not found at $ISO_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify it's a removable device (safety against wiping system disks)
|
|
REMOVABLE=$(lsblk -nd -o RM "$USB_DEV" 2>/dev/null || echo "0")
|
|
if [ "$REMOVABLE" != "1" ]; then
|
|
echo "WARNING: $USB_DEV does not appear to be a removable device."
|
|
read -rp "Are you SURE you want to erase $USB_DEV? (type YES): " CONFIRM
|
|
if [ "$CONFIRM" != "YES" ]; then
|
|
echo "Aborted."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Verify required source files exist
|
|
if [ ! -f "$AUTOINSTALL_DIR/user-data" ]; then
|
|
echo "ERROR: user-data not found at $AUTOINSTALL_DIR/user-data"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -f "$AUTOINSTALL_DIR/meta-data" ]; then
|
|
echo "ERROR: meta-data not found at $AUTOINSTALL_DIR/meta-data"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -f "$PLAYBOOK_DIR/pxe_server_setup.yml" ]; then
|
|
echo "ERROR: pxe_server_setup.yml not found at $PLAYBOOK_DIR/"
|
|
exit 1
|
|
fi
|
|
|
|
echo "============================================"
|
|
echo "PXE Server USB Builder"
|
|
echo "============================================"
|
|
echo "USB Device : $USB_DEV"
|
|
echo "ISO : $ISO_PATH"
|
|
echo "Source Dir : $SCRIPT_DIR"
|
|
echo ""
|
|
echo "This will ERASE all data on $USB_DEV."
|
|
read -rp "Continue? (y/N): " PROCEED
|
|
if [[ ! "$PROCEED" =~ ^[Yy]$ ]]; then
|
|
echo "Aborted."
|
|
exit 1
|
|
fi
|
|
|
|
# --- Unmount any existing partitions ---
|
|
echo ""
|
|
echo "[1/6] Unmounting existing partitions on $USB_DEV..."
|
|
for part in "${USB_DEV}"*; do
|
|
umount "$part" 2>/dev/null || true
|
|
done
|
|
|
|
# --- Rebuild ISO with 'autoinstall' kernel parameter ---
|
|
echo "[2/6] Rebuilding ISO with autoinstall kernel parameter..."
|
|
|
|
ISO_WORK=$(mktemp -d)
|
|
7z -y x "$ISO_PATH" -o"$ISO_WORK/iso" >/dev/null 2>&1
|
|
mv "$ISO_WORK/iso/[BOOT]" "$ISO_WORK/BOOT"
|
|
chmod -R u+w "$ISO_WORK/iso"
|
|
|
|
# Patch grub.cfg: add 'autoinstall' to kernel cmdline, reduce timeout
|
|
sed -i 's|linux\t/casper/vmlinuz ---|linux\t/casper/vmlinuz autoinstall ---|' "$ISO_WORK/iso/boot/grub/grub.cfg"
|
|
sed -i 's/^set timeout=30/set timeout=5/' "$ISO_WORK/iso/boot/grub/grub.cfg"
|
|
|
|
PATCHED_ISO="$ISO_WORK/patched.iso"
|
|
cd "$ISO_WORK/iso"
|
|
xorriso -as mkisofs -r \
|
|
-V 'Ubuntu-Server 24.04.3 LTS amd64' \
|
|
-o "$PATCHED_ISO" \
|
|
--grub2-mbr ../BOOT/1-Boot-NoEmul.img \
|
|
--protective-msdos-label \
|
|
-partition_cyl_align off \
|
|
-partition_offset 16 \
|
|
--mbr-force-bootable \
|
|
-append_partition 2 28732ac11ff8d211ba4b00a0c93ec93b ../BOOT/2-Boot-NoEmul.img \
|
|
-appended_part_as_gpt \
|
|
-iso_mbr_part_type a2a0d0ebe5b9334487c068b6b72699c7 \
|
|
-c '/boot.catalog' \
|
|
-b '/boot/grub/i386-pc/eltorito.img' \
|
|
-no-emul-boot \
|
|
-boot-load-size 4 \
|
|
-boot-info-table \
|
|
--grub2-boot-info \
|
|
-eltorito-alt-boot \
|
|
-e '--interval:appended_partition_2:::' \
|
|
-no-emul-boot \
|
|
-boot-load-size 10160 \
|
|
. 2>/dev/null
|
|
cd "$SCRIPT_DIR"
|
|
echo " ISO rebuilt with 'autoinstall' kernel param and 5s GRUB timeout"
|
|
|
|
echo " Writing patched ISO to $USB_DEV..."
|
|
dd if="$PATCHED_ISO" of="$USB_DEV" bs=4M status=progress oflag=sync
|
|
sync
|
|
ISO_SIZE=$(stat -c%s "$PATCHED_ISO")
|
|
rm -rf "$ISO_WORK"
|
|
|
|
# --- Find the end of the ISO to create CIDATA partition ---
|
|
echo "[3/6] Creating CIDATA partition after ISO data..."
|
|
|
|
# Get ISO size in bytes and calculate the start sector for the new partition
|
|
SECTOR_SIZE=512
|
|
# Start the CIDATA partition 1MB after the ISO ends (alignment)
|
|
START_SECTOR=$(( (ISO_SIZE / SECTOR_SIZE) + 2048 ))
|
|
|
|
# Use sfdisk to append a new partition
|
|
echo " ISO size: $((ISO_SIZE / 1024 / 1024)) MB"
|
|
echo " CIDATA partition starts at sector $START_SECTOR"
|
|
|
|
# Add a new partition using sfdisk --append
|
|
echo "${START_SECTOR},+,L" | sfdisk --append "$USB_DEV" --no-reread 2>/dev/null || true
|
|
partprobe "$USB_DEV"
|
|
sleep 2
|
|
|
|
# Determine the new partition name (could be sdX3, sdX4, etc.)
|
|
CIDATA_PART=""
|
|
for part in "${USB_DEV}"[0-9]*; do
|
|
# Find the partition that starts at or after our start sector
|
|
PART_START=$(sfdisk -d "$USB_DEV" 2>/dev/null | grep "$part" | grep -o 'start=[[:space:]]*[0-9]*' | grep -o '[0-9]*')
|
|
if [ -n "$PART_START" ] && [ "$PART_START" -ge "$START_SECTOR" ]; then
|
|
CIDATA_PART="$part"
|
|
break
|
|
fi
|
|
done
|
|
|
|
# Fallback: use the last partition
|
|
if [ -z "$CIDATA_PART" ]; then
|
|
CIDATA_PART=$(lsblk -ln -o NAME "$USB_DEV" | tail -1)
|
|
CIDATA_PART="/dev/$CIDATA_PART"
|
|
fi
|
|
|
|
echo " CIDATA partition: $CIDATA_PART"
|
|
|
|
# --- Format CIDATA partition ---
|
|
echo "[4/6] Formatting $CIDATA_PART as FAT32 (label: CIDATA)..."
|
|
mkfs.vfat -F 32 -n CIDATA "$CIDATA_PART"
|
|
|
|
# --- Mount and copy files ---
|
|
echo "[5/6] Copying autoinstall config, packages, and playbook to CIDATA..."
|
|
MOUNT_POINT=$(mktemp -d)
|
|
mount "$CIDATA_PART" "$MOUNT_POINT"
|
|
|
|
# Copy cloud-init files
|
|
cp "$AUTOINSTALL_DIR/user-data" "$MOUNT_POINT/"
|
|
cp "$AUTOINSTALL_DIR/meta-data" "$MOUNT_POINT/"
|
|
|
|
# Copy offline .deb packages into packages/ subdirectory
|
|
mkdir -p "$MOUNT_POINT/packages"
|
|
DEB_COUNT=0
|
|
|
|
if [ -d "$OFFLINE_PKG_DIR" ]; then
|
|
for deb in "$OFFLINE_PKG_DIR"/*.deb; do
|
|
if [ -f "$deb" ]; then
|
|
cp "$deb" "$MOUNT_POINT/packages/"
|
|
DEB_COUNT=$((DEB_COUNT + 1))
|
|
fi
|
|
done
|
|
fi
|
|
echo " Copied $DEB_COUNT .deb packages to packages/"
|
|
|
|
# Copy playbook directory
|
|
cp -r "$PLAYBOOK_DIR" "$MOUNT_POINT/playbook"
|
|
echo " Copied playbook/"
|
|
|
|
# Copy webapp
|
|
WEBAPP_DIR="$SCRIPT_DIR/webapp"
|
|
if [ -d "$WEBAPP_DIR" ]; then
|
|
mkdir -p "$MOUNT_POINT/webapp"
|
|
cp -r "$WEBAPP_DIR/app.py" "$WEBAPP_DIR/requirements.txt" "$MOUNT_POINT/webapp/"
|
|
cp -r "$WEBAPP_DIR/templates" "$WEBAPP_DIR/static" "$MOUNT_POINT/webapp/"
|
|
echo " Copied webapp/"
|
|
fi
|
|
|
|
# Copy pip wheels for offline Flask install
|
|
PIP_WHEELS_DIR="$SCRIPT_DIR/pip-wheels"
|
|
if [ ! -d "$PIP_WHEELS_DIR" ]; then
|
|
echo " pip-wheels/ not found — downloading now..."
|
|
mkdir -p "$PIP_WHEELS_DIR"
|
|
if pip3 download -d "$PIP_WHEELS_DIR" flask lxml 2>/dev/null; then
|
|
echo " Downloaded pip wheels successfully."
|
|
else
|
|
echo " WARNING: Failed to download pip wheels (no internet?)"
|
|
echo " The PXE server will need internet to install Flask later,"
|
|
echo " or manually copy wheels to pip-wheels/ and rebuild."
|
|
rmdir "$PIP_WHEELS_DIR" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
if [ -d "$PIP_WHEELS_DIR" ]; then
|
|
cp -r "$PIP_WHEELS_DIR" "$MOUNT_POINT/pip-wheels"
|
|
WHEEL_COUNT=$(find "$PIP_WHEELS_DIR" -name '*.whl' | wc -l)
|
|
echo " Copied pip-wheels/ ($WHEEL_COUNT wheels)"
|
|
fi
|
|
|
|
# Copy WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.)
|
|
BOOT_FILES_DIR="$SCRIPT_DIR/boot-files"
|
|
if [ -d "$BOOT_FILES_DIR" ]; then
|
|
BOOT_FILE_COUNT=0
|
|
for bf in "$BOOT_FILES_DIR"/*; do
|
|
if [ -f "$bf" ]; then
|
|
cp "$bf" "$MOUNT_POINT/"
|
|
BOOT_FILE_COUNT=$((BOOT_FILE_COUNT + 1))
|
|
fi
|
|
done
|
|
BOOT_FILES_SIZE=$(du -sh "$BOOT_FILES_DIR" | cut -f1)
|
|
echo " Copied $BOOT_FILE_COUNT boot files ($BOOT_FILES_SIZE) — wimboot, boot.wim, ipxe.efi, etc."
|
|
else
|
|
echo " WARNING: No boot-files/ found (copy WinPE boot files from Media Creator)"
|
|
fi
|
|
|
|
# Copy boot tools (Clonezilla, Blancco, Memtest) if prepared
|
|
BOOT_TOOLS_DIR="$SCRIPT_DIR/boot-tools"
|
|
if [ -d "$BOOT_TOOLS_DIR" ]; then
|
|
cp -r "$BOOT_TOOLS_DIR" "$MOUNT_POINT/boot-tools"
|
|
TOOLS_SIZE=$(du -sh "$MOUNT_POINT/boot-tools" | cut -f1)
|
|
echo " Copied boot-tools/ ($TOOLS_SIZE)"
|
|
else
|
|
echo " No boot-tools/ found (run prepare-boot-tools.sh first)"
|
|
fi
|
|
|
|
# Optionally copy WinPE deployment images
|
|
if [ -n "$WINPE_SOURCE" ] && [ -d "$WINPE_SOURCE" ]; then
|
|
echo " Copying WinPE deployment content from $WINPE_SOURCE..."
|
|
mkdir -p "$MOUNT_POINT/images"
|
|
cp -r "$WINPE_SOURCE"/* "$MOUNT_POINT/images/" 2>/dev/null || true
|
|
IMG_SIZE=$(du -sh "$MOUNT_POINT/images" | cut -f1)
|
|
echo " Copied WinPE images ($IMG_SIZE)"
|
|
elif [ -n "$WINPE_SOURCE" ]; then
|
|
echo " WARNING: WinPE source path not found: $WINPE_SOURCE (skipping)"
|
|
fi
|
|
|
|
# List what's on CIDATA
|
|
echo ""
|
|
echo " CIDATA contents:"
|
|
ls -lh "$MOUNT_POINT/" | sed 's/^/ /'
|
|
|
|
# --- Cleanup ---
|
|
echo ""
|
|
echo "[6/6] Syncing and unmounting..."
|
|
sync
|
|
umount "$MOUNT_POINT"
|
|
rmdir "$MOUNT_POINT"
|
|
|
|
echo ""
|
|
echo "============================================"
|
|
echo "USB build complete!"
|
|
echo "============================================"
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " 1. Insert USB into target machine"
|
|
echo " 2. Boot from USB (F12 / boot menu)"
|
|
echo " 3. Ubuntu will auto-install and configure the PXE server"
|
|
echo " 4. After reboot, move the NIC to the isolated PXE network"
|
|
echo ""
|