From 28fd9fe4741ee36d520fef1ae219ad80bb29ef14 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Wed, 14 Oct 2026 17:14:18 -0400 Subject: [PATCH] Fix the unattend that broke Engineer builds; gate unattend deploys on the linter a94c203 added a RunSynchronous Path of 417 chars to both std/eng answer files. Windows caps it at 259 and then rejects the WHOLE answer file, so an Engineer build stopped at 'Windows could not parse or process unattend answer file'. The live files were restored to their pre-a94c203 versions the same day. - The status steps now fit: Send-ImagingStage.ps1 -Persist copies itself out of C:\Deploy (no cmd copy chain), short PowerShell switches, default arguments left out. Longest Path is 168 chars; lint-unattend passes. NOT deployed: these files must go live only through deploy-unattend.sh. - scripts/deploy-unattend.sh: the only route for an answer file to a PXE server. Lints first and refuses on any error, backs up, swaps atomically, lints the live copy and restores the backup if that fails. - .githooks/pre-commit (git config core.hooksPath .githooks): refuses a commit whose staged unattend fails the linter. Verified by planting a 352-char Path (blocked) and against the exact a94c203 file (rejected). - Boot menu: also accept SecureBoot read as hex 01, and show the raw values in the warning so a wrong read can be diagnosed from the screen. --- .githooks/pre-commit | 20 +++++++ playbook/FlatUnattendW10-engineer.xml | 13 ++-- playbook/FlatUnattendW10.xml | 13 ++-- playbook/imaging-status/Send-ImagingStage.ps1 | 16 ++++- playbook/ipxe/GetPxeScript.aspx | 2 + scripts/deploy-unattend.sh | 60 +++++++++++++++++++ 6 files changed, 110 insertions(+), 14 deletions(-) create mode 100755 .githooks/pre-commit create mode 100755 scripts/deploy-unattend.sh diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..ed7bc32 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,20 @@ +#!/bin/bash +# Refuse a commit that stages an unattend answer file the linter rejects. +# A bad answer file stops every bay imaged with it at "Windows could not +# parse or process unattend answer file". Enable with: +# git config core.hooksPath .githooks +set -euo pipefail +ROOT="$(git rev-parse --show-toplevel)" +mapfile -t FILES < <(git diff --cached --name-only --diff-filter=ACM | grep -iE '(^|/)(Flat)?Unattend[^/]*\.xml$' || true) +[ ${#FILES[@]} -eq 0 ] && exit 0 +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT +STAGED=() +for f in "${FILES[@]}"; do + out="$TMP/$(echo "$f" | tr '/' '_')" + git show ":$f" > "$out" # lint what is staged, not the working copy + STAGED+=("$out") +done +if ! python3 "$ROOT/scripts/lint-unattend.py" --local "${STAGED[@]}"; then + echo "pre-commit: unattend lint failed for: ${FILES[*]}" >&2 + exit 1 +fi diff --git a/playbook/FlatUnattendW10-engineer.xml b/playbook/FlatUnattendW10-engineer.xml index 8641c72..ae26e91 100644 --- a/playbook/FlatUnattendW10-engineer.xml +++ b/playbook/FlatUnattendW10-engineer.xml @@ -61,11 +61,12 @@ cmd /c sc config wlansvc start= auto & net start wlansvc & exit /b 0 Start WLAN AutoConfig so the WiFi profile can be added - + 2 - cmd /c (if not exist C:\ProgramData\PxeStatus mkdir C:\ProgramData\PxeStatus) & copy /Y "C:\Deploy\Applications\extra\pxe-status\Send-ImagingStage.ps1" C:\ProgramData\PxeStatus\ & powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\PxeStatus\Send-ImagingStage.ps1" -PCType gea-engineer -StageIndex 2 -StageTotal 5 -Stage "Windows setup: installing apps" -Status in_progress & exit /b 0 + powershell.exe -NoP -EP Bypass -File C:\Deploy\Applications\extra\pxe-status\Send-ImagingStage.ps1 -Persist -PCType gea-engineer -StageIndex 2 -Stage "Windows setup: installing apps" PXE status: stage 2 of 5 @@ -310,7 +311,7 @@ 49 - powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\PxeStatus\Send-ImagingStage.ps1" -PCType gea-engineer -StageIndex 3 -StageTotal 5 -Stage "Apps installed - waiting for first logon" -Status in_progress + powershell.exe -NoP -EP Bypass -File C:\ProgramData\PxeStatus\Send-ImagingStage.ps1 -PCType gea-engineer -StageIndex 3 -Stage "Apps installed - waiting for first logon" PXE status: stage 3 of 5 @@ -337,7 +338,7 @@ - powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\PxeStatus\Send-ImagingStage.ps1" -PCType gea-engineer -StageIndex 4 -StageTotal 5 -Stage "First logon: installing engineering layer" -Status in_progress + powershell.exe -NoP -EP Bypass -File C:\ProgramData\PxeStatus\Send-ImagingStage.ps1 -PCType gea-engineer -StageIndex 4 -Stage "First logon: installing engineering layer" PXE status: stage 4 of 5 1 @@ -391,7 +392,7 @@ Engineering Layer: stage payload out of C:\Deploy then install - powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\PxeStatus\Send-ImagingStage.ps1" -PCType gea-engineer -StageIndex 5 -StageTotal 5 -Stage "Setup complete - enrollment continues on the PC" -Status succeeded + powershell.exe -NoP -EP Bypass -File C:\ProgramData\PxeStatus\Send-ImagingStage.ps1 -PCType gea-engineer -StageIndex 5 -Stage "Setup complete - enrollment continues on the PC" -Status succeeded PXE status: stage 5 of 5 12 diff --git a/playbook/FlatUnattendW10.xml b/playbook/FlatUnattendW10.xml index ab198f4..cf36f14 100644 --- a/playbook/FlatUnattendW10.xml +++ b/playbook/FlatUnattendW10.xml @@ -61,11 +61,12 @@ cmd /c sc config wlansvc start= auto & net start wlansvc & exit /b 0 Start WLAN AutoConfig so the WiFi profile can be added - + 2 - cmd /c (if not exist C:\ProgramData\PxeStatus mkdir C:\ProgramData\PxeStatus) & copy /Y "C:\Deploy\Applications\extra\pxe-status\Send-ImagingStage.ps1" C:\ProgramData\PxeStatus\ & powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\PxeStatus\Send-ImagingStage.ps1" -PCType gea-standard -StageIndex 2 -StageTotal 5 -Stage "Windows setup: installing apps" -Status in_progress & exit /b 0 + powershell.exe -NoP -EP Bypass -File C:\Deploy\Applications\extra\pxe-status\Send-ImagingStage.ps1 -Persist -PCType gea-standard -StageIndex 2 -Stage "Windows setup: installing apps" PXE status: stage 2 of 5 @@ -310,7 +311,7 @@ 49 - powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\PxeStatus\Send-ImagingStage.ps1" -PCType gea-standard -StageIndex 3 -StageTotal 5 -Stage "Apps installed - waiting for first logon" -Status in_progress + powershell.exe -NoP -EP Bypass -File C:\ProgramData\PxeStatus\Send-ImagingStage.ps1 -PCType gea-standard -StageIndex 3 -Stage "Apps installed - waiting for first logon" PXE status: stage 3 of 5 @@ -337,7 +338,7 @@ - powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\PxeStatus\Send-ImagingStage.ps1" -PCType gea-standard -StageIndex 4 -StageTotal 5 -Stage "First logon" -Status in_progress + powershell.exe -NoP -EP Bypass -File C:\ProgramData\PxeStatus\Send-ImagingStage.ps1 -PCType gea-standard -StageIndex 4 -Stage "First logon" PXE status: stage 4 of 5 1 @@ -380,7 +381,7 @@ 10 - powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\PxeStatus\Send-ImagingStage.ps1" -PCType gea-standard -StageIndex 5 -StageTotal 5 -Stage "Setup complete - enrollment continues on the PC" -Status succeeded + powershell.exe -NoP -EP Bypass -File C:\ProgramData\PxeStatus\Send-ImagingStage.ps1 -PCType gea-standard -StageIndex 5 -Stage "Setup complete - enrollment continues on the PC" -Status succeeded PXE status: stage 5 of 5 11 diff --git a/playbook/imaging-status/Send-ImagingStage.ps1 b/playbook/imaging-status/Send-ImagingStage.ps1 index ec65fdd..d5d135f 100644 --- a/playbook/imaging-status/Send-ImagingStage.ps1 +++ b/playbook/imaging-status/Send-ImagingStage.ps1 @@ -4,7 +4,9 @@ # own reporting in Shopfloor\lib\Send-PxeStatus.ps1). # # Called from the image's FlatUnattendW10.xml (specialize RunSynchronous and -# oobeSystem FirstLogonCommands). Deployed to +# oobeSystem FirstLogonCommands). Keep those command lines short: Windows +# rejects the whole unattend if a RunSynchronous Path exceeds 259 characters +# (check with scripts/lint-unattend.py --local). Deployed to # _shared\Applications\extra\pxe-status\ on the PXE server, which PESetup # copies to C:\Deploy\Applications\extra\pxe-status\. # @@ -20,7 +22,10 @@ param( [ValidateSet('in_progress', 'succeeded', 'failed')][string]$Status = 'in_progress', [string]$PxeServer = '172.16.9.1', [int]$Port = 9009, - [int]$TimeoutSec = 5 + [int]$TimeoutSec = 5, + # Copy this script to C:\ProgramData\PxeStatus first: the bulk-enrollment + # task deletes C:\Deploy before first logon, when the later steps run. + [switch]$Persist ) $logFile = 'C:\Logs\send-imaging-stage.log' @@ -31,6 +36,13 @@ function Log([string]$msg) { } catch { } } +if ($Persist) { + try { + New-Item -ItemType Directory -Path 'C:\ProgramData\PxeStatus' -Force | Out-Null + Copy-Item -LiteralPath $PSCommandPath -Destination 'C:\ProgramData\PxeStatus\Send-ImagingStage.ps1' -Force + } catch { Log "persist failed: $($_.Exception.Message)" } +} + try { $serial = $null try { $serial = (Get-CimInstance -ClassName Win32_BIOS -ErrorAction Stop).SerialNumber } catch { diff --git a/playbook/ipxe/GetPxeScript.aspx b/playbook/ipxe/GetPxeScript.aspx index 908a8ed..34f2299 100644 --- a/playbook/ipxe/GetPxeScript.aspx +++ b/playbook/ipxe/GetPxeScript.aspx @@ -21,8 +21,10 @@ choose --default winpe --timeout 30000 target && goto ${target} # iPXE 2.0 reads the firmware variable; older iPXE cannot (empty), so it # warns as before. Secure Boot confirmed on: no warning, no wait. iseq ${efi/SecureBoot:uint8} 1 && goto winpe_boot || +iseq ${efi/SecureBoot:hex} 01 && goto winpe_boot || echo echo Secure Boot is OFF on this PC (or this iPXE cannot tell). +echo (iPXE read SecureBoot as: [${efi/SecureBoot:hex}] [${efi/SecureBoot:uint8}]) echo Windows deployment requires Secure Boot to be ENABLED. echo If you disabled it for Clonezilla/Memtest, re-enable it now. echo diff --git a/scripts/deploy-unattend.sh b/scripts/deploy-unattend.sh new file mode 100755 index 0000000..add7b4c --- /dev/null +++ b/scripts/deploy-unattend.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# +# deploy-unattend.sh - the ONLY way to put an unattend answer file on a PXE +# server. A bad answer file does not fail at copy time: every bay imaged +# with it stops at "Windows could not parse or process unattend answer +# file" and has to be re-imaged. So this refuses to copy unless the +# linter passes, backs the live file up, swaps it in atomically, and lints +# the copy on the server afterwards. +# +# Usage: scripts/deploy-unattend.sh +# e.g. scripts/deploy-unattend.sh gea-engineer playbook/FlatUnattendW10-engineer.xml +# +# Env: PXE_SERVER (default 172.16.9.1), PXE_USER (default pxe); the +# remote password is read from PXE_PASS or prompted for. + +set -euo pipefail + +IMAGE="${1:?usage: $0 }" +SRC="${2:?usage: $0 }" +SERVER="${PXE_SERVER:-172.16.9.1}" +USER_="${PXE_USER:-pxe}" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)" +LINT="$REPO_ROOT/scripts/lint-unattend.py" +DEST="/srv/samba/winpeapps/$IMAGE/Deploy/FlatUnattendW10.xml" +STAMP="$(date +%Y%m%d-%H%M%S)" + +case "$IMAGE" in + *[!A-Za-z0-9._-]*|"") echo "bad image type: $IMAGE" >&2; exit 2 ;; +esac +[ -f "$SRC" ] || { echo "no such file: $SRC" >&2; exit 2; } + +echo "== 1/3 lint $SRC" +if ! python3 "$LINT" --local "$SRC"; then + echo "REFUSED: the linter failed; nothing was copied." >&2 + exit 1 +fi + +if [ -z "${PXE_PASS:-}" ]; then + read -rs -p "password for $USER_@$SERVER: " PXE_PASS; echo +fi +export SSHPASS="$PXE_PASS" +SSH=(sshpass -e ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 "$USER_@$SERVER") + +echo "== 2/3 back up and swap $DEST on $SERVER" +sshpass -e scp -q -o StrictHostKeyChecking=no "$SRC" "$USER_@$SERVER:/tmp/unattend-$STAMP.xml" +"${SSH[@]}" "set -e; P() { echo \"\$SSHPASS_REMOTE\" | sudo -S -p '' \"\$@\"; }; SSHPASS_REMOTE='$PXE_PASS' + [ -f '$DEST' ] || { echo 'no live file at $DEST' >&2; exit 1; } + P cp -a '$DEST' '$DEST.bak-$STAMP' + P install -m 644 -o root -g root /tmp/unattend-$STAMP.xml '$DEST.new' + P mv -f '$DEST.new' '$DEST' + rm -f /tmp/unattend-$STAMP.xml + echo \"backup: $DEST.bak-$STAMP\"; sha256sum '$DEST'" + +echo "== 3/3 lint the live copy" +if ! python3 "$LINT" --server "$SERVER" --image "$IMAGE"; then + echo "LIVE FILE FAILS LINT: restoring the backup." >&2 + "${SSH[@]}" "echo '$PXE_PASS' | sudo -S -p '' cp -a '$DEST.bak-$STAMP' '$DEST'" + exit 1 +fi +echo "Deployed $SRC to $IMAGE."