From ade2f3b5fff471aa368e7f970f8b93f855fdf694 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Tue, 14 Apr 2026 12:57:28 -0400 Subject: [PATCH] Fix USB install reliability: bash, LV resize, deps, idempotency - autoinstall/user-data: move lvextend/growpart/pvresize BEFORE playbook so 130GB of drivers+PPKGs fits during first-boot copy. Use tr -d "[:space:]" to avoid breaking outer bash -c single-quote wrap. - playbook: add executable: /bin/bash to Dell driver deploy (process substitution) and Blancco initramfs builder (brace expansion). - playbook: make "Ensure Samba user for Blancco reports" idempotent via pdbedit check so re-runs don't abort the play. - download-packages.sh: also download dist-upgrade package set. Explicit --simulate misses transitive version bumps (e.g. gnupg 17.4 needs matching gpgv 17.4) causing offline dpkg "dependency problems" when ISO baseline is older than noble-updates. --- autoinstall/user-data | 11 ++++- download-packages.sh | 75 +++++++++++++++++++++++++------ playbook/pxe_server_setup.yml | 85 ++++++++++++++++++++++++++--------- 3 files changed, 136 insertions(+), 35 deletions(-) diff --git a/autoinstall/user-data b/autoinstall/user-data index ebf1dd2..f8ad1ac 100644 --- a/autoinstall/user-data +++ b/autoinstall/user-data @@ -72,6 +72,16 @@ autoinstall: curtin in-target --target=/target -- bash -c ' cat <<"EOF" > /opt/first-boot.sh #!/bin/bash + # Expand root LV to full disk BEFORE playbook (playbook copies ~130GB of drivers+PPKGs) + ROOT_DEV=$(findmnt -n -o SOURCE /) + ROOT_DISK=$(lsblk -n -o PKNAME "$(readlink -f "$ROOT_DEV")" | tail -1) + PV_PART=$(pvs --noheadings -o pv_name 2>/dev/null | tr -d "[:space:]" | head -1) + if [ -n "$ROOT_DISK" ] && [ -n "$PV_PART" ]; then + PART_NUM=$(echo "$PV_PART" | grep -o "[0-9]*$") + growpart "/dev/${ROOT_DISK}" "${PART_NUM}" 2>&1 || true + pvresize "$PV_PART" 2>&1 || true + fi + lvextend -r -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv 2>&1 || true CIDATA_DEV=$(blkid -L CIDATA) if [ -n "$CIDATA_DEV" ]; then mkdir -p /mnt/usb @@ -88,7 +98,6 @@ autoinstall: umount /mnt/usb fi sed -i "s|^/opt/first-boot.sh.*|# &|" /etc/rc.local - lvextend -r -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv || true EOF chmod +x /opt/first-boot.sh ' diff --git a/download-packages.sh b/download-packages.sh index d698934..507c7d5 100755 --- a/download-packages.sh +++ b/download-packages.sh @@ -1,10 +1,10 @@ #!/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). -# It downloads every .deb needed by the Ansible playbook into a local directory, -# which then gets bundled onto the installer USB. +# The PXE server installs Ubuntu 24.04 (Noble), so all packages MUST come from the +# 24.04 archive. If this script is run on a non-24.04 host (e.g. Zorin 17 / 22.04), +# it auto-spawns an Ubuntu 24.04 docker container to do the download. # # Usage: # ./download-packages.sh [output_directory] @@ -14,6 +14,40 @@ set -euo pipefail OUT_DIR="${1:-./offline-packages}" +OUT_DIR_ABS="$(cd "$(dirname "$OUT_DIR")" 2>/dev/null && pwd)/$(basename "$OUT_DIR")" + +# Detect host Ubuntu codename. Run inside the container if not Noble (24.04). +HOST_CODENAME="$(. /etc/os-release && echo "${UBUNTU_CODENAME:-${VERSION_CODENAME:-}}")" + +if [ "${IN_DOCKER:-}" != "1" ] && [ "$HOST_CODENAME" != "noble" ]; then + echo "Host is '$HOST_CODENAME', not 'noble' (Ubuntu 24.04)." + echo "Re-running inside ubuntu:24.04 docker container..." + echo "" + + if ! command -v docker >/dev/null; then + echo "ERROR: docker not installed. Install docker or run on a real Ubuntu 24.04 host." + exit 1 + fi + + SCRIPT_PATH="$(readlink -f "$0")" + REPO_DIR="$(dirname "$SCRIPT_PATH")" + mkdir -p "$OUT_DIR_ABS" + + docker run --rm -i \ + -v "$REPO_DIR:/repo" \ + -v "$OUT_DIR_ABS:/out" \ + -e IN_DOCKER=1 \ + -w /repo \ + ubuntu:24.04 \ + bash -c "apt-get update -qq && apt-get install -y --no-install-recommends sudo python3-pip python3-setuptools python3-wheel ca-certificates >/dev/null && /repo/download-packages.sh /out" + + echo "" + echo "============================================" + echo "Container build complete. Files in: $OUT_DIR_ABS" + echo "============================================" + exit 0 +fi + mkdir -p "$OUT_DIR" # Packages installed by the Ansible playbook (pxe_server_setup.yml) @@ -30,10 +64,12 @@ PLAYBOOK_PACKAGES=( grub-efi-amd64-bin grub-common conntrack + busybox-static + zstd + cpio ) # 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. AUTOINSTALL_PACKAGES=( network-manager wpasupplicant @@ -45,7 +81,7 @@ AUTOINSTALL_PACKAGES=( ALL_PACKAGES=("${PLAYBOOK_PACKAGES[@]}" "${AUTOINSTALL_PACKAGES[@]}") echo "============================================" -echo "Offline Package Downloader" +echo "Offline Package Downloader (Ubuntu 24.04 noble)" echo "============================================" echo "Output directory: $OUT_DIR" echo "" @@ -54,18 +90,29 @@ printf ' - %s\n' "${ALL_PACKAGES[@]}" echo "" # Update package cache -echo "[1/3] Updating package cache..." +echo "[1/4] Updating package cache..." sudo apt-get update -qq # Simulate install to find all dependencies -echo "[2/3] Resolving dependencies..." -DEPS=$(apt-get install --simulate "${ALL_PACKAGES[@]}" 2>&1 \ +echo "[2/4] Resolving dependencies..." +EXPLICIT_DEPS=$(apt-get install --simulate "${ALL_PACKAGES[@]}" 2>&1 \ | grep "^Inst " \ - | awk '{print $2}' \ - | sort -u) + | awk '{print $2}') + +# ALSO pull every package that would upgrade in a dist-upgrade. This is +# critical: the Ubuntu ISO ships a point-in-time baseline, but our explicit +# packages (from noble-updates) may depend on *newer* versions of ISO-baseline +# packages (e.g. gnupg 17.4 needs matching gpgv 17.4). Without this, offline +# install fails with dpkg "dependency problems" because transitive version +# bumps aren't captured by --simulate on the explicit list. +UPGRADE_DEPS=$(apt-get dist-upgrade --simulate 2>&1 \ + | grep "^Inst " \ + | awk '{print $2}') + +DEPS=$(printf '%s\n%s\n' "$EXPLICIT_DEPS" "$UPGRADE_DEPS" | sort -u | grep -v '^$') DEP_COUNT=$(echo "$DEPS" | wc -l) -echo " Found $DEP_COUNT packages (including dependencies)" +echo " Found $DEP_COUNT packages (explicit + baseline upgrades)" # Download all packages echo "[3/4] Downloading .deb packages to $OUT_DIR..." @@ -79,7 +126,9 @@ echo " $DEB_COUNT packages ($TOTAL_SIZE)" # Download pip wheels for Flask webapp (offline install) echo "[4/4] Downloading Python wheels for webapp..." -PIP_DIR="$(dirname "$OUT_DIR")/pip-wheels" +# Place pip-wheels next to the script (or /repo when in docker), not next to OUT_DIR +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PIP_DIR="$SCRIPT_DIR/pip-wheels" mkdir -p "$PIP_DIR" pip3 download -d "$PIP_DIR" flask lxml 2>&1 | tail -5 diff --git a/playbook/pxe_server_setup.yml b/playbook/pxe_server_setup.yml index 84b3e1b..df46e1e 100644 --- a/playbook/pxe_server_setup.yml +++ b/playbook/pxe_server_setup.yml @@ -307,9 +307,19 @@ mode: '0777' - name: "Deploy PPKG enrollment packages to enrollment share" - shell: cp -f {{ usb_mount }}/enrollment/*.ppkg /srv/samba/enrollment/ 2>/dev/null || true - args: - warn: false + shell: | + set +e + # Copy any whole PPKGs (small enough to fit on FAT32) + cp -f {{ usb_root }}/enrollment/*.ppkg /srv/samba/enrollment/ 2>/dev/null + # Reassemble any split files (foo.ppkg.part.00, .01, ... -> foo.ppkg) + for first in {{ usb_root }}/enrollment/*.part.00; do + [ -e "$first" ] || continue + base="${first%.part.00}" + name="$(basename "$base")" + echo "Reassembling $name from chunks..." + cat "${base}.part."* > "/srv/samba/enrollment/$name" + done + ls -lh /srv/samba/enrollment/*.ppkg 2>/dev/null ignore_errors: yes - name: "Deploy run-enrollment.ps1 to enrollment share" @@ -320,16 +330,29 @@ ignore_errors: yes - name: "Deploy Dell driver packs to shared Out-of-box Drivers" - shell: > - if [ -d "{{ usb_mount }}/drivers" ]; then - mkdir -p "/srv/samba/winpeapps/_shared/Out-of-box Drivers/Dell_11" - cp -r {{ usb_mount }}/drivers/* "/srv/samba/winpeapps/_shared/Out-of-box Drivers/Dell_11/" - echo "Deployed Dell drivers from USB" - else - echo "No drivers/ on USB - skipping" - fi args: - warn: false + executable: /bin/bash + shell: | + set +e + SRC="{{ usb_root }}/drivers" + DEST="/srv/samba/winpeapps/_shared/Out-of-box Drivers/Dell_11" + if [ ! -d "$SRC" ]; then + echo "No drivers/ on USB - skipping" + exit 0 + fi + mkdir -p "$DEST" + # Copy everything except split chunks + rsync -a --exclude='*.part.*' "$SRC/" "$DEST/" + # Reassemble any split driver files + while IFS= read -r first; do + base="${first%.part.00}" + rel="${base#$SRC/}" + out="$DEST/$rel" + mkdir -p "$(dirname "$out")" + echo "Reassembling $rel from chunks..." + cat "${base}.part."* > "$out" + done < <(find "$SRC" -name '*.part.00') + echo "Deployed Dell drivers from USB" ignore_errors: yes - name: "Deploy shopfloor setup scripts to enrollment share" @@ -372,6 +395,17 @@ - models.txt ignore_errors: yes + - name: "Deploy BIOS update binaries from USB" + shell: > + if [ -d "{{ usb_root }}/bios" ]; then + cp -f {{ usb_root }}/bios/*.exe /srv/samba/enrollment/BIOS/ 2>/dev/null || true + count=$(find /srv/samba/enrollment/BIOS -name '*.exe' | wc -l) + echo "Deployed $count BIOS binaries" + else + echo "No bios/ on USB - skipping" + fi + ignore_errors: yes + - name: "Create image upload staging directory" file: path: /home/pxe/image-upload @@ -579,13 +613,24 @@ # Boot Ubuntu kernel, download Blancco rootfs, overlay mount, switch_root. - name: "Build Blancco PXE initramfs" + args: + executable: /bin/bash + creates: "{{ web_root }}/blancco/kexec-initrd.img" shell: | set -e WORK=$(mktemp -d) mkdir -p "$WORK"/{bin,lib/modules,lib64,sbin,usr/share/udhcpc} - # Busybox (static) - cp /bin/busybox "$WORK/bin/" 2>/dev/null || apt-get install -y busybox-static >/dev/null && cp /bin/busybox "$WORK/bin/" + # Busybox (static) - bundled on USB at playbook/busybox-static + if [ -f /bin/busybox ]; then + cp /bin/busybox "$WORK/bin/" + elif [ -f "{{ usb_root }}/playbook/busybox-static" ]; then + cp "{{ usb_root }}/playbook/busybox-static" "$WORK/bin/busybox" + chmod +x "$WORK/bin/busybox" + else + echo "ERROR: No busybox available (not at /bin/busybox or on USB)" + exit 1 + fi for cmd in sh awk cat chmod echo grep gunzip ifconfig ip ln losetup ls mkdir mknod mount reboot route sed sleep switch_root tar udhcpc umount wget cpio; do ln -sf busybox "$WORK/bin/$cmd" done @@ -614,8 +659,7 @@ find . | cpio -o -H newc 2>/dev/null | gzip > "{{ web_root }}/blancco/kexec-initrd.img" rm -rf "$WORK" echo "Built kexec-initrd.img: $(stat -c %s '{{ web_root }}/blancco/kexec-initrd.img') bytes" - args: - creates: "{{ web_root }}/blancco/kexec-initrd.img" + ignore_errors: yes - name: "Copy Ubuntu kernel for Blancco PXE boot" copy: @@ -648,12 +692,11 @@ args: creates: "{{ web_root }}/blancco/config-clean.xml" - - name: "Create Samba user for Blancco reports" + - name: "Ensure Samba user for Blancco reports exists (idempotent)" shell: | - id blancco 2>/dev/null || useradd -r -s /usr/sbin/nologin blancco - echo -e "blancco\nblancco" | smbpasswd -a -s blancco 2>/dev/null - args: - creates: /etc/samba/smbpasswd + id blancco >/dev/null 2>&1 || useradd -r -s /usr/sbin/nologin blancco + pdbedit -L 2>/dev/null | grep -q '^blancco:' || (echo -e "blancco\nblancco" | smbpasswd -a -s blancco) + changed_when: false - name: "Check for WinPE deployment content on USB" stat: