#!/bin/sh # Blancco PXE Loader - init script for custom initramfs # Boot chain: iPXE -> GRUB EFI -> Ubuntu kernel + this initramfs -> switch_root to Blancco # # Blancco's own kernel freezes / lacks NIC drivers for some Dell Precision # hardware during PXE boot. Workaround: boot Ubuntu kernel (which has a wider # NIC driver set), download Blancco rootfs (squashfs), overlay-mount, and # switch_root into Blancco's userspace. # # Quiet by default: one line per step. Add blancco.debug=1 to the kernel # cmdline for the full trace (set -x, modprobe -v, ip link/addr). Failures # always print their diagnostics and drop to sh so the operator can # investigate without re-imaging. exec >/dev/console 2>&1 export PATH=/bin:/sbin mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs devtmpfs /dev 2>/dev/null || mount -t tmpfs tmpfs /dev DEBUG="" grep -q 'blancco.debug=1' /proc/cmdline && DEBUG=1 # dbg CMD...: run only in debug mode dbg() { [ -n "$DEBUG" ] && "$@"; return 0; } if [ -n "$DEBUG" ]; then set -x else printf '\033c' # clear the kernel messages off the screen dmesg -n 1 2>/dev/null || true fi echo "" echo " Blancco Drive Eraser - loading from $( [ -n "$DEBUG" ] && echo "PXE (debug)" || echo "PXE")" echo "" mkdir -p /tmp /run /run/lower /run/upper /run/work /run/newroot KVER=$(uname -r) dbg echo "Running kernel: $KVER" dbg ls /lib/modules/ echo " [1/5] Network drivers" # Throw the full common-NIC driver list at the wall. modprobe resolves the # deps from /lib/modules/$KVER/modules.dep (built by prepare-boot-tools). # Anything missing is silently ignored; whatever matches PCI IDs will bind. # USB host controllers first: xhci_pci is a module in the Ubuntu kernel # (CONFIG_USB_XHCI_PCI=m) and there is no udev here, so without this no # USB 3 / USB-C port works and USB-C to RJ45 adapters never appear. for drv in \ xhci_pci xhci_pci_renesas \ mii libphy ptp \ e1000 e1000e igb igc ixgbe ixgbevf i40e ice iavf \ tg3 bnx2 bnx2x bnxt_en b44 \ r8169 r8152 atlantic \ vmxnet3 virtio_net virtio_pci \ pcnet32 8139too 8139cp \ sfc sfc_ef100 mlx4_en mlx5_core \ alx atl1c atl1e atl2 \ via_rhine via_velocity forcedeth \ pegasus dm9601 asix ax88179_178a cdc_ether cdc_ncm rndis_host; do if [ -n "$DEBUG" ]; then modprobe -v "$drv" 2>/dev/null && echo " OK $drv" || true else modprobe "$drv" >/dev/null 2>&1 || true fi done sleep 3 echo " [2/5] Waiting for a network link" dbg ls /sys/class/net/ dbg ip link dbg echo " Waiting up to 60s for non-lo interface..." IFACE="" for i in $(seq 1 60); do for n in /sys/class/net/*; do name="${n##*/}" [ "$name" = "lo" ] && continue [ -d "$n" ] && IFACE="$name" && break 2 done sleep 1 echo -n "." done echo "" if [ -z "$IFACE" ]; then echo "ERROR: No network interface after 60s" echo "=== dmesg tail ==="; dmesg | tail -40 echo "=== PCI devices (sysfs) ==="; ls /sys/bus/pci/devices/ 2>/dev/null echo "=== loaded modules ==="; cat /proc/modules echo "Dropping to shell - type 'exit' to reboot." exec /bin/sh fi # The first interface to appear is not necessarily the cabled one: a # second onboard port, a dock or a USB NIC can come first, and a static # IP on a dead port leaves wget at "Connecting" forever. Bring every # wired interface up and use the first with carrier. for n in /sys/class/net/*; do name="${n##*/}" case "$name" in lo|wl*) continue ;; esac ip link set "$name" up 2>/dev/null || ifconfig "$name" up 2>/dev/null || true done dbg echo " Waiting up to 30s for a cabled interface..." CARRIER="" for i in $(seq 1 30); do for n in /sys/class/net/*; do name="${n##*/}" case "$name" in lo|wl*) continue ;; esac # USB NICs enumerate seconds after onboard ports: bring up late ones too [ "$(cat "$n/operstate" 2>/dev/null)" = "down" ] && { ip link set "$name" up 2>/dev/null || ifconfig "$name" up 2>/dev/null || true; } [ "$(cat "$n/carrier" 2>/dev/null)" = "1" ] && CARRIER="$name" && break 2 done sleep 1 echo -n "." done echo "" if [ -z "$CARRIER" ]; then echo "ERROR: no wired interface has link. Interfaces seen:" for n in /sys/class/net/*; do drv=$(readlink "$n/device/driver" 2>/dev/null) echo " ${n##*/} carrier=$(cat "$n/carrier" 2>/dev/null) driver=${drv##*/}" done echo "=== kernel messages (NIC / firmware / link) ===" dmesg | grep -i -E 'eth|enp|enx|r8152|r8169|e1000e|igc|cdc|firmware|link' | tail -20 echo "Check the cable, then type 'exit' to reboot." exec /bin/sh fi IFACE="$CARRIER" echo " using $IFACE" ip link set "$IFACE" up || ifconfig "$IFACE" up sleep 2 SERVER=172.16.9.1 # Where the release files (rootfs, preferences, licence container, # config.img) come from. Default /blancco, the active release. The webapp's # test menu entry adds blancco.base=/blancco/staged/ to boot a # staged release before it is activated. The kmod tarball, progress # reporter and debug key are not part of a release and stay in /blancco. BASE=/blancco for arg in $(cat /proc/cmdline); do case "$arg" in blancco.base=/blancco/*) BASE="${arg#blancco.base=}" ;; esac done case "$BASE" in *..*|*[!A-Za-z0-9._/-]*) BASE=/blancco ;; esac BASE="${BASE%/}" [ "$BASE" = /blancco ] || echo " release under test: $BASE" # Coarse progress for the webapp's /imaging dashboard (same endpoint the # WinPE imaging clients use). Stage 1 also resets a finished tile for this # serial. Best effort: a webapp outage must never stop a wipe. post_status() { # sed, not tr: the initrd's busybox has no tr link serial=$(sed 's/["\\]//g' /sys/class/dmi/id/product_serial 2>/dev/null) [ -n "$serial" ] || return 0 wget -q -T 5 -O /dev/null --header 'Content-Type: application/json' \ --post-data "{\"serial\":\"$serial\",\"pctype\":\"blancco\",\"source\":\"blancco\",\"current_stage\":\"$2\",\"stage_index\":$1,\"stage_total\":7,\"status\":\"in_progress\",\"ip\":\"172.16.9.250\"}" \ "http://$SERVER:9009/imaging/status" 2>/dev/null || true } ifconfig "$IFACE" 172.16.9.250 netmask 255.255.255.0 up sleep 1 dbg echo " IP: 172.16.9.250 SERVER: $SERVER" dbg ip addr echo " [3/5] Downloading Blancco (~850 MB)" post_status 1 "Downloading Blancco" wget -T 30 -O /tmp/airootfs.sfs http://$SERVER$BASE/arch/x86_64/airootfs.sfs 2>&1 [ -s /tmp/airootfs.sfs ] || { echo "ERROR: download failed"; exec /bin/sh; } echo " [4/5] Preparing system" modprobe overlay 2>/dev/null || insmod /lib/modules/$KVER/kernel/fs/overlayfs/overlay.ko 2>/dev/null modprobe squashfs 2>/dev/null || insmod /lib/modules/$KVER/kernel/fs/squashfs/squashfs.ko 2>/dev/null modprobe loop 2>/dev/null losetup /dev/loop0 /tmp/airootfs.sfs mount -t squashfs -o ro /dev/loop0 /run/lower mount -t tmpfs -o size=50% tmpfs /run/upper mkdir -p /run/upper/upper /run/upper/work mount -t overlay overlay -o lowerdir=/run/lower,upperdir=/run/upper/upper,workdir=/run/upper/work /run/newroot echo " [5/5] Loading drivers and settings" # Modules (+ iwlwifi firmware) for the running kernel. kmod-$KVER.tar.gz # lets several boot kernels share one web root; kmod.tar.gz is the # original single-kernel name, kept as the fallback. wget -q -T 30 -O /tmp/kmod.tar.gz "http://$SERVER/blancco/kmod-$KVER.tar.gz" 2>/dev/null || wget -q -T 30 -O /tmp/kmod.tar.gz http://$SERVER/blancco/kmod.tar.gz 2>&1 [ -s /tmp/kmod.tar.gz ] && (cd /run/newroot && gunzip -c /tmp/kmod.tar.gz | tar xf - && rm -f /tmp/kmod.tar.gz) mkdir -p /run/newroot/albus # preferences.xml REQUIRED. Without valid file, Blancco airootfs's # /opt/scripts/validate_preferences.sh silently restores # /albus/preferences.save (factory defaults, empty network_share) so # erasure reports never reach SMB. Fail loud here instead. PREFS=/run/newroot/albus/preferences.xml if ! wget -q -T 30 -O "$PREFS" "http://$SERVER$BASE/preferences.xml"; then echo "ERROR: preferences.xml download failed from http://$SERVER$BASE/preferences.xml" echo "Reports would fall back to factory defaults (no SMB target)." echo "Dropping to shell - check Apache + network." exec /bin/sh fi # Busybox initramfs has no xmllint. Grep for required SMB markers instead; # if either is missing, the prefs file in web_root is stale or corrupted. if [ ! -s "$PREFS" ] || ! grep -q "$SERVER" "$PREFS" \ || ! grep -q 'blancco-reports' "$PREFS"; then echo "ERROR: preferences.xml missing required network_share entries." echo "Expected $SERVER + blancco-reports." echo "=== first 60 lines of $PREFS ===" head -60 "$PREFS" exec /bin/sh fi # Clobber preferences.save too. validate_preferences.sh in airootfs falls # back to preferences.save on any future xmllint failure; if that ever # fires, we want the fallback to still have the right SMB target. cp -f "$PREFS" /run/newroot/albus/preferences.save if ! wget -q -T 30 -O /run/newroot/albus/config.xml "http://$SERVER$BASE/config-clean.xml"; then echo "ERROR: config.xml (license container) download failed." exec /bin/sh fi # On a native boot config.img is an initrd, its files land in the # initramfs root and archiso keeps them in /run/initramfs, where Blancco's # /opt/scripts/inc/* pick up report.key (report signing), certificate.ca, # hybrid.ids and flexible_container. This path never had them, so # 1_reportKey wrote an empty /albus/report.key. Recreate the layout on a # tmpfs at the new root's /run; systemd leaves an existing /run mounted. mount -t tmpfs -o mode=0755,nosuid,nodev tmpfs /run/newroot/run mkdir -p /run/newroot/run/initramfs if wget -q -T 30 -O /tmp/config.img "http://$SERVER$BASE/config.img" && (cd /run/newroot/run/initramfs && cpio -id < /tmp/config.img 2>/dev/null) && [ -s /run/newroot/run/initramfs/report.key ]; then dbg ls -la /run/newroot/run/initramfs else echo "ERROR: config.img (report key, certificate) download or unpack failed." exec /bin/sh fi rm -f /tmp/config.img mkdir -p /run/newroot/etc/X11/xorg.conf.d # Don't pin a single Xorg driver - hardware varies per site (Intel iGPU, # NVIDIA GK208, AMD, etc). Let Xorg auto-pick. With nomodeset + KMS # blacklist on kernel cmdline, Xorg falls back to fbdev (uses # kernel-provided framebuffer from vga=normal) which works on most boxes. # Previous "modesetting" pin needed KMS we disabled; "vesa" pin also # didn't drive NVIDIA cards. Removing the pin entirely. rm -f /run/newroot/etc/X11/xorg.conf.d/20-failsafeDriver.conf 2>/dev/null || true # Hard-mask sleep/suspend/hibernate targets in the airootfs overlay. systemd # kernel cmdline systemd.mask= only blocks systemd-side activation; userspace # can still write /sys/power/state directly. /dev/null symlinks under # /etc/systemd/system/ block ALL systemd-mediated paths AND stop logind from # advertising sleep capability to userland. Combined with the logind drop-in # below, /sys/power/state writes from non-root userland (Albus) also fail. mkdir -p /run/newroot/etc/systemd/system for tgt in sleep.target suspend.target hibernate.target hybrid-sleep.target suspend-then-hibernate.target; do ln -sf /dev/null "/run/newroot/etc/systemd/system/$tgt" done mkdir -p /run/newroot/etc/systemd/logind.conf.d cat > /run/newroot/etc/systemd/logind.conf.d/no-suspend.conf << XEOF [Login] IdleAction=ignore HandleSuspendKey=ignore HandleHibernateKey=ignore HandleLidSwitch=ignore HandleLidSwitchDocked=ignore HandlePowerKey=ignore XEOF # Restart after an erasure stalled on "A stop job is running for # /mnt/share": the network goes down before the SMB report share is # unmounted, and the CIFS umount waits out the 90s timeout. A mount unit's # timeout is TimeoutSec= (default: the START timeout), so # DefaultTimeoutStopSec alone does not reach it. Lazy + forced umount # detaches the dead share at once; reports are written long before this. mkdir -p /run/newroot/etc/systemd/system.conf.d /run/newroot/etc/systemd/system/mnt-share.mount.d cat > /run/newroot/etc/systemd/system.conf.d/10-fast-stop.conf << XEOF [Manager] DefaultTimeoutStopSec=10s XEOF cat > /run/newroot/etc/systemd/system/mnt-share.mount.d/10-fast-umount.conf << XEOF [Mount] TimeoutSec=10s LazyUnmount=yes ForceUnmount=yes XEOF # Disable Xorg screensaver + DPMS so display stays on during long erasures. # Numbered 00-* to run before vendor xinitrc.d hooks. mkdir -p /run/newroot/etc/X11/xinit/xinitrc.d cat > /run/newroot/etc/X11/xinit/xinitrc.d/00-no-screen-blank.sh << 'XEOF' #!/bin/sh xset s off -dpms 2>/dev/null || true xset s noblank 2>/dev/null || true setterm -blank 0 -powerdown 0 2>/dev/null || true XEOF chmod +x /run/newroot/etc/X11/xinit/xinitrc.d/00-no-screen-blank.sh mkdir -p /run/newroot/proc /run/newroot/sys /run/newroot/dev /run/newroot/run /run/newroot/tmp # Auto-save reports to the SMB share. process=auto never writes the share # (Albus forces auto_backup off outside workflow/manual) but does keep each # report in /reports; a small service copies them across. mkdir -p /run/newroot/usr/local/sbin /run/newroot/etc/systemd/system/multi-user.target.wants cat > /run/newroot/usr/local/sbin/blancco-report-sync << 'XEOF' #!/bin/bash # Copy Blancco reports to the SMB report share. # # In process=auto Albus saves each report to /reports/.xml # (and .pdf when pdf+xml is selected) and uploads it to the cloud, but never # writes the share: auto_backup is forced off outside workflow/manual mode. # Albus has the share mounted at /mnt/share already, so copy each finished # file there, named the way a manual Save names it (UTC # YYYYMMDD_HHMMSS_report., the time taken from the XML so the PDF of # the same report gets the same name). # Usage: blancco-report-sync [--once] DONE=/var/lib/blancco-report-sync mkdir -p "$DONE" while :; do for f in /reports/*.xml /reports/*.pdf; do [ -s "$f" ] || continue base=$(basename "$f") [ -e "$DONE/$base" ] && continue # Leave files Albus may still be writing. [ $(( $(date +%s) - $(stat -c %Y "$f") )) -ge 5 ] || continue mountpoint -q /mnt/share || continue id=${base%.*} ext=${base##*.} ref="/reports/$id.xml" [ -f "$ref" ] || ref="$f" name="$(date -u -r "$ref" +%Y%m%d_%H%M%S)_report.$ext" if cp "$f" "/mnt/share/$name.part" && mv -f "/mnt/share/$name.part" "/mnt/share/$name"; then touch "$DONE/$base" echo "copied $base to $name" serial=$(tr -d '"\\' 2>/dev/null < /sys/class/dmi/id/product_serial) [ -n "$serial" ] && curl -s -m 5 -o /dev/null -H 'Content-Type: application/json' \ -d "{\"serial\":\"$serial\",\"log_lines\":[\"Report copied to share: $name\"]}" \ http://172.16.9.1:9009/imaging/status || true else rm -f "/mnt/share/$name.part" echo "copy of $base failed, will retry" fi done [ "$1" = "--once" ] && exit 0 sleep 10 done XEOF chmod 755 /run/newroot/usr/local/sbin/blancco-report-sync cat > /run/newroot/etc/systemd/system/blancco-report-sync.service << 'XEOF' [Unit] Description=Copy Blancco reports to the SMB report share [Service] ExecStart=/usr/local/sbin/blancco-report-sync Restart=always RestartSec=10 [Install] WantedBy=multi-user.target XEOF ln -sf /etc/systemd/system/blancco-report-sync.service \ /run/newroot/etc/systemd/system/multi-user.target.wants/blancco-report-sync.service # Live erase progress for the /imaging dashboard: follows the AlbusSeven # journal and posts stages and drive percentages. Fetched at boot so the # server copy (playbook/blancco-progress.py) is the only one. if wget -q -T 10 -O /run/newroot/usr/local/sbin/blancco-progress "http://$SERVER/blancco/blancco-progress.py"; then chmod 755 /run/newroot/usr/local/sbin/blancco-progress cat > /run/newroot/etc/systemd/system/blancco-progress.service << 'XEOF' [Unit] Description=Report Blancco erase progress to the PXE webapp [Service] ExecStart=/usr/bin/python3 /usr/local/sbin/blancco-progress Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target XEOF ln -sf /etc/systemd/system/blancco-progress.service \ /run/newroot/etc/systemd/system/multi-user.target.wants/blancco-progress.service else echo " (no blancco-progress.py on server: no live progress on the dashboard)" fi # Site timezone for Blancco's own clock (GUI, logs). Report timestamps keep # an explicit UTC offset; report.utc_offset in the preferences stays 0 # because it is a fixed offset and cannot follow daylight saving. if [ -e /run/newroot/usr/share/zoneinfo/America/New_York ]; then ln -sf /usr/share/zoneinfo/America/New_York /run/newroot/etc/localtime fi # Debug boots only: Blancco's own debug mode (/etc/debug.environment makes # isDebugMode true, so 2_sshd starts sshd) plus an SSH key for root. # Root is locked and sshd is key-only, so without the key there is no way # in. The key is served from the PXE server and never baked in here. if [ -n "$DEBUG" ]; then touch /run/newroot/etc/debug.environment mkdir -p /run/newroot/root/.ssh if wget -q -T 10 -O /run/newroot/root/.ssh/authorized_keys "http://$SERVER/blancco/debug_authorized_keys"; then chmod 700 /run/newroot/root/.ssh chmod 600 /run/newroot/root/.ssh/authorized_keys echo " debug: sshd on, root key installed" else echo " debug: no debug_authorized_keys on server, SSH stays closed" fi fi mount --move /proc /run/newroot/proc mount --move /sys /run/newroot/sys mount --move /dev /run/newroot/dev # Hard-block kernel suspend/hibernate path. Bind-mount /dev/null over the # sysfs power-control files so any userland write (Albus auto-suspend, # pm-utils, etc) becomes a no-op. This is the LAST line of defense - kernel # cmdline systemd.mask, /dev/null symlinks for sleep targets, and logind # drop-ins were all bypassed by Albus writing /sys/power/state directly. # Bind-mount works at the VFS layer below sysfs, so even kernel-side mount # remounts wouldn't undo it. mount --bind /dev/null /run/newroot/sys/power/state 2>/dev/null || true mount --bind /dev/null /run/newroot/sys/power/disk 2>/dev/null || true mount --bind /dev/null /run/newroot/sys/power/mem_sleep 2>/dev/null || true mount --bind /dev/null /run/newroot/sys/power/autosleep 2>/dev/null || true echo " Starting Blancco..." post_status 2 "Starting Blancco" exec switch_root /run/newroot /sbin/init