Files
pxe-server/scripts/prepare-boot-tools.sh
cproudlock 678d10d5c2 blancco: upgrade to Drive Eraser 7.20.0
playbook/blancco-preferences.xml is now the vendor 7.20 preferences
(schema 4.20, "Non_GEAE_Wired" profile) with only our site overrides:
SMB report share on 172.16.9.1 with auto_backup, WiFi INTERNETACCESS for
cloud licensing (wired stays up for SMB), manual as the fallback process,
bootable report, screensaver 86400. workflow default=true is kept: the
cloud workflow erases unattended, as live 7.15 already did. No custom
fields, matching both the vendor image and live 7.15.

prepare-boot-tools.sh:
- 7.20 initramfs is an uncompressed early cpio (NIC firmware, itself
  .xz files) followed by the XZ archive. The old patch decompressed from
  byte 0, failed, warned and carried on, so PXE boots would drop network
  after copytoram. Walk the cpio to its trailer and patch the XZ part.
- Patch exactly one copytoram match or fail. The looser fallback pattern
  matches five hooks in 7.20.
- config.img now carries playbook/blancco-preferences.xml, so the BIOS
  (native kernel) and UEFI (HTTP) paths read the same settings. Refuses
  to build if the playbook preferences are for another Blancco version.
- The old global replace() also set the 802.1x, WiFi and repeater
  passwords to "blancco". Gone with the above.
- Keep preferences.xml size by refilling the NUL gap before the trailing
  filler comment (7.20), or trimming the comment (7.15).
- OUT_DIR can be overridden to stage a build outside boot-tools/.
2026-10-05 09:32:32 -04:00

326 lines
13 KiB
Bash
Executable File

#!/bin/bash
#
# prepare-boot-tools.sh — Download/extract boot files for PXE boot tools
#
# Downloads Clonezilla Live and Memtest86+ for PXE booting,
# and extracts Blancco Drive Eraser from its ISO.
#
# Usage:
# ./prepare-boot-tools.sh [/path/to/blancco.iso]
#
# Output directories:
# boot-tools/clonezilla/ — vmlinuz, initrd.img, filesystem.squashfs
# boot-tools/blancco/ — extracted boot files or ISO for memdisk
# boot-tools/memtest/ — memtest.efi
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
OUT_DIR="${OUT_DIR:-$REPO_ROOT/boot-tools}"
BLANCCO_ISO="${1:-}"
# Auto-detect Blancco ISO in project directory
if [ -z "$BLANCCO_ISO" ]; then
BLANCCO_ISO=$(find "$REPO_ROOT" -maxdepth 1 -name '*DriveEraser*.iso' -o -name '*blancco*.iso' 2>/dev/null | head -1)
fi
mkdir -p "$OUT_DIR"/{clonezilla,blancco,memtest}
echo "============================================"
echo "PXE Boot Tools Preparation"
echo "============================================"
# --- Clonezilla Live ---
echo ""
echo "[1/3] Clonezilla Live"
CLONEZILLA_VERSION="3.2.1-6"
CLONEZILLA_FILE="clonezilla-live-${CLONEZILLA_VERSION}-amd64.zip"
CLONEZILLA_URL="https://sourceforge.net/projects/clonezilla/files/clonezilla_live_stable/${CLONEZILLA_VERSION}/${CLONEZILLA_FILE}/download"
if [ -f "$OUT_DIR/clonezilla/vmlinuz" ] && [ -f "$OUT_DIR/clonezilla/filesystem.squashfs" ]; then
echo " Already prepared, skipping. Delete boot-tools/clonezilla/ to re-download."
else
echo " Downloading Clonezilla Live ${CLONEZILLA_VERSION}..."
TMPDIR=$(mktemp -d)
wget -q --show-progress -O "$TMPDIR/$CLONEZILLA_FILE" "$CLONEZILLA_URL" || {
echo " ERROR: Download failed. Trying alternative URL..."
# Fallback: try OSDN mirror
wget -q --show-progress -O "$TMPDIR/$CLONEZILLA_FILE" \
"https://free.nchc.org.tw/clonezilla-live/stable/${CLONEZILLA_FILE}" || {
echo " ERROR: Could not download Clonezilla. Download manually and place in boot-tools/clonezilla/"
echo " Need: vmlinuz, initrd.img, filesystem.squashfs from the live ZIP"
}
}
if [ -f "$TMPDIR/$CLONEZILLA_FILE" ]; then
echo " Extracting PXE boot files..."
unzip -o -j "$TMPDIR/$CLONEZILLA_FILE" "live/vmlinuz" -d "$OUT_DIR/clonezilla/"
unzip -o -j "$TMPDIR/$CLONEZILLA_FILE" "live/initrd.img" -d "$OUT_DIR/clonezilla/"
unzip -o -j "$TMPDIR/$CLONEZILLA_FILE" "live/filesystem.squashfs" -d "$OUT_DIR/clonezilla/"
rm -rf "$TMPDIR"
echo " Done."
fi
fi
ls -lh "$OUT_DIR/clonezilla/" 2>/dev/null | grep -E 'vmlinuz|initrd|squashfs' | sed 's/^/ /'
# --- Blancco Drive Eraser ---
echo ""
echo "[2/3] Blancco Drive Eraser"
if [ -n "$BLANCCO_ISO" ] && [ -f "$BLANCCO_ISO" ]; then
echo " Extracting from: $BLANCCO_ISO"
echo " Using 7z to extract (no root required)..."
# Blancco is Arch Linux-based. We need:
# arch/boot/x86_64/vmlinuz-bde-linux
# arch/boot/x86_64/initramfs-bde-linux.img
# arch/boot/intel-ucode.img
# arch/boot/amd-ucode.img
# arch/boot/config.img
# arch/x86_64/airootfs.sfs
TMPDIR=$(mktemp -d)
7z x -o"$TMPDIR" "$BLANCCO_ISO" \
"arch/boot/x86_64/vmlinuz-bde-linux" \
"arch/boot/x86_64/initramfs-bde-linux.img" \
"arch/boot/intel-ucode.img" \
"arch/boot/amd-ucode.img" \
"arch/boot/config.img" \
"arch/x86_64/airootfs.sfs" \
-r 2>/dev/null || {
echo " 7z extraction failed. Install p7zip-full: apt install p7zip-full"
}
# Flatten into blancco/ directory for HTTP serving
if [ -f "$TMPDIR/arch/boot/x86_64/vmlinuz-bde-linux" ]; then
cp "$TMPDIR/arch/boot/x86_64/vmlinuz-bde-linux" "$OUT_DIR/blancco/"
cp "$TMPDIR/arch/boot/x86_64/initramfs-bde-linux.img" "$OUT_DIR/blancco/"
cp "$TMPDIR/arch/boot/intel-ucode.img" "$OUT_DIR/blancco/"
cp "$TMPDIR/arch/boot/amd-ucode.img" "$OUT_DIR/blancco/"
cp "$TMPDIR/arch/boot/config.img" "$OUT_DIR/blancco/"
# airootfs.sfs needs to be in arch/x86_64/ path relative to HTTP root
mkdir -p "$OUT_DIR/blancco/arch/x86_64"
cp "$TMPDIR/arch/x86_64/airootfs.sfs" "$OUT_DIR/blancco/arch/x86_64/"
echo " Extracted Blancco boot files."
# --- Patch config.img (size-preserving) ---
# config.img is a CPIO archive. Its preferences.xml is what the native
# kernel (BIOS) boot path reads; the UEFI path gets the same file over
# HTTP. Both now carry playbook/blancco-preferences.xml, so the two
# paths cannot drift. The vendor file is laid out as
# <xml> [NUL padding] <!-- filler comment --> and its size is kept:
# 7.20 pads to 32768 with ~15KB of NULs; 7.15 had none, so growth
# there comes out of the filler comment.
if [ -f "$OUT_DIR/blancco/config.img" ]; then
echo " Writing playbook preferences into config.img..."
CFGTMP=$(mktemp -d)
cd "$CFGTMP"
cpio -id < "$OUT_DIR/blancco/config.img" 2>/dev/null
if [ -f "$CFGTMP/preferences.xml" ]; then
PREFS_SRC="$REPO_ROOT/playbook/blancco-preferences.xml" python3 << 'PYEOF'
import os, re, sys
with open("preferences.xml", "rb") as f:
data = f.read()
with open(os.environ["PREFS_SRC"], "rb") as f:
ours = f.read().strip() + b"\n"
orig_size = len(data)
def version(x):
m = re.search(rb"<version>\s*<generation>(\d+)</generation>\s*"
rb"<major>(\d+)</major>", x)
return m.groups() if m else None
# Our file must be a merge onto THIS release's preferences, or settings the
# new schema added or renamed are silently lost.
if version(ours) != version(data):
sys.exit(f" ERROR: playbook preferences are for {version(ours)}, ISO is "
f"{version(data)}. Merge onto the new vendor preferences first.")
for need in (b"<hostname>172.16.9.1</hostname>", b"<path>blancco-reports</path>",
b"<auto_backup>true</auto_backup>"):
if need not in ours:
sys.exit(f" ERROR: playbook preferences missing {need.decode()}")
# Split off the trailing filler comment and any NUL padding before it.
end = data.find(b"</root>")
cstart = data.rfind(b"<!--")
if end < 0 or cstart < end:
sys.exit(" ERROR: vendor preferences.xml layout not recognised")
gap, tail = data[end + 7:cstart], data[cstart:]
xml = ours
room = orig_size - len(xml) - len(tail)
if room >= 0:
# Refill the gap with what it held: NULs if it was padding, else newlines.
gap = (b"\x00" if b"\x00" in gap else b"\n") * room
else:
# No room left: take it out of the filler comment body (keep "<!-- " + " -->").
cut = -room
if len(tail) - cut < len(b"<!-- -->\n"):
sys.exit(f" ERROR: preferences need {cut} more bytes than the "
"padding and filler comment can give")
tail = tail[:5] + tail[5 + cut:]
gap = b""
out = xml + gap + tail
if len(out) != orig_size:
sys.exit(f" ERROR: preferences.xml size changed {orig_size} -> {len(out)}")
with open("preferences.xml", "wb") as f:
f.write(out)
print(f" preferences.xml: {len(xml)} bytes of XML, {orig_size} total (preserved)")
PYEOF
# Repack CPIO padded to a 512-byte boundary
ls -1 "$CFGTMP" | (cd "$CFGTMP" && cpio -o -H newc 2>/dev/null) | \
dd bs=512 conv=sync 2>/dev/null > "$OUT_DIR/blancco/config.img"
echo " Reports: SMB blancco@172.16.9.1/blancco-reports (from playbook preferences)"
fi
cd "$REPO_ROOT"
rm -rf "$CFGTMP"
fi
# --- Patch initramfs to keep network interfaces up after copytoram ---
# Blancco uses copytoram=y which triggers archiso_pxe_common latehook to
# flush all network interfaces. Binary-patch the check from "y" to "N" so
# the condition never matches. IMPORTANT: full extract/repack BREAKS booting.
echo " Patching initramfs to preserve network after copytoram..."
INITRAMFS="$OUT_DIR/blancco/initramfs-bde-linux.img" python3 << 'PYEOF'
import lzma, os, sys
initramfs = os.environ["INITRAMFS"]
with open(initramfs, "rb") as f:
data = f.read()
# 7.20 prepends an uncompressed early cpio (NIC firmware) before the XZ
# archive; 7.15 was XZ from byte 0. Patch only the XZ part, keep the prefix.
# Walk the cpio headers to its trailer: the firmware files are themselves
# .xz, so searching for the XZ magic would land inside the prefix.
off = 0
while data.startswith(b"070701", off):
namesize = int(data[off + 94:off + 102], 16)
filesize = int(data[off + 54:off + 62], 16)
name = data[off + 110:off + 110 + namesize - 1]
off = (((off + 110 + namesize + 3) & ~3) + filesize + 3) & ~3
if name == b"TRAILER!!!":
while off < len(data) and data[off] == 0:
off += 1
if not data.startswith(b"\xfd7zXZ\x00", off):
sys.exit(f" ERROR: initramfs layout not recognised (no XZ archive at {off})")
prefix, compressed = data[:off], data[off:]
raw = lzma.decompress(compressed)
# Binary patch: change the copytoram check from "y" to "N" in
# archiso_pxe_common's run_latehook. Exact match only: the looser
# '"${copytoram}" = "y"' appears in several hooks and patching the first
# one found is not safe.
old = b'"y" ]; then\n for curif in /sys/class/net'
new = b'"N" ]; then\n for curif in /sys/class/net'
count = raw.count(old)
if count != 1:
sys.exit(f" ERROR: copytoram network-flush pattern found {count} times, "
"expected 1. PXE boot would lose network after copytoram.")
raw = raw.replace(old, new, 1)
# Recompress with same XZ settings as archiso
recompressed = lzma.compress(raw, format=lzma.FORMAT_XZ,
check=lzma.CHECK_CRC32, preset=6)
with open(initramfs, "wb") as f:
f.write(prefix + recompressed)
print(f" initramfs patched: copytoram network flush disabled "
f"(early cpio {len(prefix)} bytes kept)")
PYEOF
# --- Build GRUB EFI binary for Blancco chainload ---
# Broadcom iPXE can't pass initrd to Linux kernels in UEFI mode.
# Solution: iPXE chains to grubx64.efi, which loads kernel+initrd via TFTP.
GRUB_CFG="$REPO_ROOT/boot-tools/blancco/grub-blancco.cfg"
if [ -f "$GRUB_CFG" ]; then
if command -v grub-mkstandalone &>/dev/null; then
echo " Building grubx64.efi (GRUB chainload for Blancco)..."
grub-mkstandalone \
--format=x86_64-efi \
--output="$OUT_DIR/blancco/grubx64.efi" \
--modules="linux normal echo net efinet http tftp chain sleep all_video efi_gop" \
"boot/grub/grub.cfg=$GRUB_CFG" 2>/dev/null
echo " Built grubx64.efi ($(du -h "$OUT_DIR/blancco/grubx64.efi" | cut -f1))"
else
echo " WARNING: grub-mkstandalone not found. Install grub-efi-amd64-bin:"
echo " sudo apt install grub-efi-amd64-bin grub-common"
echo " Then re-run this script to build grubx64.efi"
fi
else
echo " WARNING: grub-blancco.cfg not found at $GRUB_CFG"
fi
else
echo " Could not extract boot files from ISO."
fi
rm -rf "$TMPDIR"
else
echo " No Blancco ISO found. Provide path as argument or place in project directory."
echo " Usage: $0 /path/to/DriveEraser.iso"
fi
ls -lh "$OUT_DIR/blancco/" 2>/dev/null | grep -v '^total' | sed 's/^/ /'
# --- Memtest86+ ---
echo ""
echo "[3/3] Memtest86+"
MEMTEST_VERSION="7.20"
MEMTEST_URL="https://memtest.org/download/${MEMTEST_VERSION}/mt86plus_${MEMTEST_VERSION}.binaries.zip"
if [ -f "$OUT_DIR/memtest/memtest.efi" ]; then
echo " Already prepared, skipping."
else
echo " Downloading Memtest86+ v${MEMTEST_VERSION}..."
TMPDIR=$(mktemp -d)
wget -q --show-progress -O "$TMPDIR/memtest.zip" "$MEMTEST_URL" || {
echo " ERROR: Download failed. Download manually from https://memtest.org"
TMPDIR=""
}
if [ -n "$TMPDIR" ] && [ -f "$TMPDIR/memtest.zip" ]; then
echo " Extracting EFI binary..."
unzip -o -j "$TMPDIR/memtest.zip" "memtest64.efi" -d "$OUT_DIR/memtest/" 2>/dev/null || \
unzip -o -j "$TMPDIR/memtest.zip" "mt86plus_${MEMTEST_VERSION}.x64.efi" -d "$OUT_DIR/memtest/" 2>/dev/null || \
unzip -o "$TMPDIR/memtest.zip" -d "$TMPDIR/extract/"
# Find the EFI file regardless of exact name
EFI_FILE=$(find "$TMPDIR" "$OUT_DIR/memtest" -name '*.efi' -name '*64*' 2>/dev/null | head -1)
if [ -n "$EFI_FILE" ] && [ ! -f "$OUT_DIR/memtest/memtest.efi" ]; then
cp "$EFI_FILE" "$OUT_DIR/memtest/memtest.efi"
fi
rm -rf "$TMPDIR"
echo " Done."
fi
fi
ls -lh "$OUT_DIR/memtest/" 2>/dev/null | grep -v '^total' | sed 's/^/ /'
# --- Summary ---
echo ""
echo "============================================"
echo "Boot tools prepared in: $OUT_DIR/"
echo "============================================"
echo ""
for tool in clonezilla blancco memtest; do
COUNT=$(find "$OUT_DIR/$tool" -type f 2>/dev/null | wc -l)
SIZE=$(du -sh "$OUT_DIR/$tool" 2>/dev/null | cut -f1)
printf " %-15s %s (%d files)\n" "$tool" "$SIZE" "$COUNT"
done
echo ""
echo "These files need to be copied to the PXE server's web root:"
echo " /var/www/html/clonezilla/"
echo " /var/www/html/blancco/"
echo " /var/www/html/memtest/"
echo ""
echo "The build-usb.sh script will include them automatically,"
echo "or copy them manually to the server."
echo ""