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.
This commit is contained in:
cproudlock
2026-04-14 12:57:28 -04:00
parent 855af7312b
commit ade2f3b5ff
3 changed files with 136 additions and 35 deletions

View File

@@ -72,6 +72,16 @@ autoinstall:
curtin in-target --target=/target -- bash -c ' curtin in-target --target=/target -- bash -c '
cat <<"EOF" > /opt/first-boot.sh cat <<"EOF" > /opt/first-boot.sh
#!/bin/bash #!/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) CIDATA_DEV=$(blkid -L CIDATA)
if [ -n "$CIDATA_DEV" ]; then if [ -n "$CIDATA_DEV" ]; then
mkdir -p /mnt/usb mkdir -p /mnt/usb
@@ -88,7 +98,6 @@ autoinstall:
umount /mnt/usb umount /mnt/usb
fi fi
sed -i "s|^/opt/first-boot.sh.*|# &|" /etc/rc.local sed -i "s|^/opt/first-boot.sh.*|# &|" /etc/rc.local
lvextend -r -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv || true
EOF EOF
chmod +x /opt/first-boot.sh chmod +x /opt/first-boot.sh
' '

View File

@@ -1,10 +1,10 @@
#!/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). # The PXE server installs Ubuntu 24.04 (Noble), so all packages MUST come from the
# It downloads every .deb needed by the Ansible playbook into a local directory, # 24.04 archive. If this script is run on a non-24.04 host (e.g. Zorin 17 / 22.04),
# which then gets bundled onto the installer USB. # it auto-spawns an Ubuntu 24.04 docker container to do the download.
# #
# Usage: # Usage:
# ./download-packages.sh [output_directory] # ./download-packages.sh [output_directory]
@@ -14,6 +14,40 @@
set -euo pipefail set -euo pipefail
OUT_DIR="${1:-./offline-packages}" 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" mkdir -p "$OUT_DIR"
# Packages installed by the Ansible playbook (pxe_server_setup.yml) # Packages installed by the Ansible playbook (pxe_server_setup.yml)
@@ -30,10 +64,12 @@ PLAYBOOK_PACKAGES=(
grub-efi-amd64-bin grub-efi-amd64-bin
grub-common grub-common
conntrack conntrack
busybox-static
zstd
cpio
) )
# 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.
AUTOINSTALL_PACKAGES=( AUTOINSTALL_PACKAGES=(
network-manager network-manager
wpasupplicant wpasupplicant
@@ -45,7 +81,7 @@ AUTOINSTALL_PACKAGES=(
ALL_PACKAGES=("${PLAYBOOK_PACKAGES[@]}" "${AUTOINSTALL_PACKAGES[@]}") ALL_PACKAGES=("${PLAYBOOK_PACKAGES[@]}" "${AUTOINSTALL_PACKAGES[@]}")
echo "============================================" echo "============================================"
echo "Offline Package Downloader" echo "Offline Package Downloader (Ubuntu 24.04 noble)"
echo "============================================" echo "============================================"
echo "Output directory: $OUT_DIR" echo "Output directory: $OUT_DIR"
echo "" echo ""
@@ -54,18 +90,29 @@ printf ' - %s\n' "${ALL_PACKAGES[@]}"
echo "" echo ""
# Update package cache # Update package cache
echo "[1/3] Updating package cache..." echo "[1/4] 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/4] Resolving dependencies..."
DEPS=$(apt-get install --simulate "${ALL_PACKAGES[@]}" 2>&1 \ EXPLICIT_DEPS=$(apt-get install --simulate "${ALL_PACKAGES[@]}" 2>&1 \
| grep "^Inst " \ | grep "^Inst " \
| awk '{print $2}' \ | awk '{print $2}')
| sort -u)
# 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) DEP_COUNT=$(echo "$DEPS" | wc -l)
echo " Found $DEP_COUNT packages (including dependencies)" echo " Found $DEP_COUNT packages (explicit + baseline upgrades)"
# Download all packages # Download all packages
echo "[3/4] Downloading .deb packages to $OUT_DIR..." 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) # 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" # 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" 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

View File

@@ -307,9 +307,19 @@
mode: '0777' mode: '0777'
- name: "Deploy PPKG enrollment packages to enrollment share" - name: "Deploy PPKG enrollment packages to enrollment share"
shell: cp -f {{ usb_mount }}/enrollment/*.ppkg /srv/samba/enrollment/ 2>/dev/null || true shell: |
args: set +e
warn: false # 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 ignore_errors: yes
- name: "Deploy run-enrollment.ps1 to enrollment share" - name: "Deploy run-enrollment.ps1 to enrollment share"
@@ -320,16 +330,29 @@
ignore_errors: yes ignore_errors: yes
- name: "Deploy Dell driver packs to shared Out-of-box Drivers" - 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: 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 ignore_errors: yes
- name: "Deploy shopfloor setup scripts to enrollment share" - name: "Deploy shopfloor setup scripts to enrollment share"
@@ -372,6 +395,17 @@
- models.txt - models.txt
ignore_errors: yes 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" - name: "Create image upload staging directory"
file: file:
path: /home/pxe/image-upload path: /home/pxe/image-upload
@@ -579,13 +613,24 @@
# Boot Ubuntu kernel, download Blancco rootfs, overlay mount, switch_root. # Boot Ubuntu kernel, download Blancco rootfs, overlay mount, switch_root.
- name: "Build Blancco PXE initramfs" - name: "Build Blancco PXE initramfs"
args:
executable: /bin/bash
creates: "{{ web_root }}/blancco/kexec-initrd.img"
shell: | shell: |
set -e set -e
WORK=$(mktemp -d) WORK=$(mktemp -d)
mkdir -p "$WORK"/{bin,lib/modules,lib64,sbin,usr/share/udhcpc} mkdir -p "$WORK"/{bin,lib/modules,lib64,sbin,usr/share/udhcpc}
# Busybox (static) # Busybox (static) - bundled on USB at playbook/busybox-static
cp /bin/busybox "$WORK/bin/" 2>/dev/null || apt-get install -y busybox-static >/dev/null && cp /bin/busybox "$WORK/bin/" 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 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" ln -sf busybox "$WORK/bin/$cmd"
done done
@@ -614,8 +659,7 @@
find . | cpio -o -H newc 2>/dev/null | gzip > "{{ web_root }}/blancco/kexec-initrd.img" find . | cpio -o -H newc 2>/dev/null | gzip > "{{ web_root }}/blancco/kexec-initrd.img"
rm -rf "$WORK" rm -rf "$WORK"
echo "Built kexec-initrd.img: $(stat -c %s '{{ web_root }}/blancco/kexec-initrd.img') bytes" echo "Built kexec-initrd.img: $(stat -c %s '{{ web_root }}/blancco/kexec-initrd.img') bytes"
args: ignore_errors: yes
creates: "{{ web_root }}/blancco/kexec-initrd.img"
- name: "Copy Ubuntu kernel for Blancco PXE boot" - name: "Copy Ubuntu kernel for Blancco PXE boot"
copy: copy:
@@ -648,12 +692,11 @@
args: args:
creates: "{{ web_root }}/blancco/config-clean.xml" creates: "{{ web_root }}/blancco/config-clean.xml"
- name: "Create Samba user for Blancco reports" - name: "Ensure Samba user for Blancco reports exists (idempotent)"
shell: | shell: |
id blancco 2>/dev/null || useradd -r -s /usr/sbin/nologin blancco id blancco >/dev/null 2>&1 || useradd -r -s /usr/sbin/nologin blancco
echo -e "blancco\nblancco" | smbpasswd -a -s blancco 2>/dev/null pdbedit -L 2>/dev/null | grep -q '^blancco:' || (echo -e "blancco\nblancco" | smbpasswd -a -s blancco)
args: changed_when: false
creates: /etc/samba/smbpasswd
- name: "Check for WinPE deployment content on USB" - name: "Check for WinPE deployment content on USB"
stat: stat: