Files
pxe-server/playbook/shopfloor-setup/run-enrollment.ps1
cproudlock 8c21282024 Verify staging in WinPE, harvest imaging logs, make enrollment run once
Three changes aimed at the same failure mode: a bay that images green and is
silently unusable, diagnosed only by walking over and copying files off by hand.

VERIFY STAGING (startnet, at :pctype_done)
Checks pc-type.txt, Run-ShopfloorSetup.ps1, shopfloor-setup/common and
shopfloor-setup/<PCTYPE> exist on the applied volume before anything depends on
them, retries the small trees once, and prints a loud banner if the retry does
not fix it. The enrollment package is checked but never blindly re-pulled - it is
8 GB, so a miss is reported instead.
Done here because a tech is still at the machine: a short copy found in WinPE
costs 30 seconds, the same copy found at first logon costs 20 minutes, and found
never costs a rebuild. Four Display bays sat green at stage 2 for weeks.

HARVEST LOGS (startnet, after PESetup exits)
Collects X:\*.log, the generated X:\Unattend.xml, PESetup's own log from the
target's Panther directory and winpe-staging.log into
\<server>\enrollment\imaging-logs\<serial>\, plus a build-context.txt naming
PCTYPE, PPKG, machine number and media. All of it was being discarded at reboot.
Runs after PESetup exits so the logs are final, which means re-mapping Y: since
cleanup already dropped it. Best-effort throughout - a bay must never fail to
reboot because a log copy failed.

W: WAIT CAP 20 -> 45 MINUTES
270 polls instead of 120. Fine on NVMe either way, but a WIM apply can exceed 20
minutes on slow media, and the failure mode is the os_not_found banner plus
nothing staged. The loop still exits the moment the SYSTEM hive appears.

RUN-ENROLLMENT RUNS ONCE
Marker at C:\Enrollment\.ppkg-applied, written on exit 0 and also on 0x800700B7
ERROR_ALREADY_EXISTS. Observed running twice on 579C144; the second pass
re-applied a pending rename over the package's own and otherwise did nothing.

Verified: startnet parens balance, every goto resolves, 899 CRLF lines with no
bare LF; run-enrollment parses clean under the PowerShell parser. Deployed -
boot.wim md5 159c2a4d, live run-enrollment dce9d50a.
2026-08-06 14:18:25 -04:00

193 lines
9.4 KiB
PowerShell
Executable File

# run-enrollment.ps1
# Installs GCCH enrollment provisioning package. That's it.
#
# Install-ProvisioningPackage triggers an immediate reboot -- nothing after
# that call executes. The sync_intune task and all other post-enrollment
# setup are registered by Run-ShopfloorSetup.ps1 BEFORE calling this script.
$ErrorActionPreference = 'Continue'
$logFile = "C:\Logs\enrollment.log"
New-Item -ItemType Directory -Path "C:\Logs" -Force -ErrorAction SilentlyContinue | Out-Null
function Log {
param([string]$Message)
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "$ts $Message"
Write-Host $line
Add-Content -Path $logFile -Value $line
}
Log "=== GE Aerospace GCCH Enrollment ==="
# --- Run once ------------------------------------------------------------
# This script has been observed running twice on one build (579C144,
# 2026-08-06: 13:31:55 and 13:36:45). The second provtool call returned
# 0x800700B7 ERROR_ALREADY_EXISTS and achieved nothing except re-applying a
# pending computer rename over the one the package had just set. The rename is
# gone now, but a second full pass is still pointless work on an 8 GB package.
$appliedMarker = 'C:\Enrollment\.ppkg-applied'
if (Test-Path $appliedMarker) {
Log "Provisioning package already applied on $(Get-Content $appliedMarker -First 1) - skipping."
return
}
# --- Find the .ppkg ---
$ppkgFile = Get-ChildItem "C:\Enrollment\*.ppkg" -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $ppkgFile) {
Log "No .ppkg found in C:\Enrollment\ - skipping enrollment."
return
}
Log "Package: $($ppkgFile.Name)"
# --- Computer name: the PPKG owns it, do NOT rename here ---
# The package declares <DNSComputerName>F%SERIAL%</DNSComputerName>, so a bay is
# meant to come up as F<serial>. This script used to run
# Rename-Computer -NewName "E$serial"
# which is a second pending rename racing the package's. Observed on 579C144
# 2026-08-06:
#
# 13:31:55 run-enrollment Rename-Computer -> E579C144 (pending)
# 13:32:17 ppkg Reboot/DeviceName -> F%SERIAL% (pending)
# "Reboot successfully scheduled. Device name set successfully."
# 13:36:45 run-enrollment runs AGAIN -> E579C144 (overwrites F)
# ~13:37 reboot, E579C144 wins
#
# Last writer wins at reboot, so the hardcoded E prefix silently beat the
# package's F. Nothing in this repo ever produced an F name; the convention only
# works if we leave naming to the package.
# --- Install provisioning package ---
# IMPORTANT: The PPKG must be installed BEFORE OOBEComplete is set. Bulk
# enrollment PPKGs are designed to run during OOBE; on Windows 11 22H2+ they
# can hang indefinitely if OOBE is already marked complete.
#
# We invoke provtool.exe directly instead of Install-ProvisioningPackage.
# The PowerShell cmdlet enforces a hardcoded 180-second timeout on the
# underlying provtool call, which a 7-8 GB GCCH PPKG often exceeds on
# slower disks. When the cmdlet times out it throws, and the Add-
# ProvisioningPackage fallback has been observed to invoke provtool with
# an empty packagePathsToAdd (session registered but never started),
# leaving the PC un-enrolled. provtool.exe directly has no caller-side
# timeout; Start-Process -Wait waits on the actual child process.
#
# The PPKG triggers an IMMEDIATE reboot once fully applied. Nothing below
# that point executes on the current boot. BPRT app installs (Chrome,
# Office, Tanium, etc.) happen on the next boot. The sync_intune
# scheduled task (registered by Run-ShopfloorSetup.ps1 before calling us)
# fires at the next logon to monitor Intune enrollment.
$ppkgLogDir = "C:\Logs\PPKG"
New-Item -ItemType Directory -Path $ppkgLogDir -Force -ErrorAction SilentlyContinue | Out-Null
$provtool = Join-Path $env:SystemRoot 'System32\provtool.exe'
# Arg order matches what the Install-ProvisioningPackage cmdlet invokes
# internally (observed in ProvEventLog.txt): positional path, then /quiet,
# then /source. No /log: or /ppkg: prefix - those are not valid provtool
# flags and caused 0x80004005 E_FAIL in the first test.
#
# /source PSCmdlet matches what Install-ProvisioningPackage invokes
# internally and is the correct post-OOBE context. BPRT was tried first
# and verified to stop after the first RestartRequired command (DotNet35):
# only 3 of 21 commands ran (PPKG Version Check, Lock Screen, DotNet35),
# leaving Office/Chrome/Tanium/Activate-Windows etc never executed
# because BPRT expects the OOBE runtime to own the reboot-and-resume
# loop, and there is no OOBE runtime here. PSCmdlet registers a
# RunOnce-style resume handler so the remaining commands continue after
# the reboot Run-ShopfloorSetup issues. Timeout concerns that previously
# motivated BPRT don't apply here because we invoke provtool.exe
# directly, not via the 180s-capped Install-ProvisioningPackage cmdlet.
$provArgs = @("`"$($ppkgFile.FullName)`"", "/quiet", "/source", "PSCmdlet")
# Enable the Provisioning-Diagnostics-Provider Admin channel so events
# from the BPRT run land somewhere we can export afterward. This is
# idempotent - running each time is safe.
wevtutil.exe set-log 'Microsoft-Windows-Provisioning-Diagnostics-Provider/Admin' /enabled:true 2>$null | Out-Null
Log "Installing provisioning package via provtool.exe (no PowerShell timeout)..."
Log "Command: $provtool $($provArgs -join ' ')"
Log "PPKG diagnostic logs -> $ppkgLogDir (provtool writes them automatically)"
try {
$p = Start-Process -FilePath $provtool -ArgumentList $provArgs -Wait -PassThru -NoNewWindow -ErrorAction Stop
Log "provtool.exe exit code: $($p.ExitCode)"
if ($p.ExitCode -eq 0) {
Set-Content -Path $appliedMarker -Value (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') -ErrorAction SilentlyContinue
Log "Wrote applied-marker $appliedMarker"
} else {
$hex = '0x{0:X8}' -f $p.ExitCode
Log "WARNING: provtool.exe returned non-zero exit code ($hex). Check $ppkgLogDir for diagnostic bundle."
# 0x800700B7 ERROR_ALREADY_EXISTS means the package is already applied.
# That is a success for our purposes, and it is what a second run of
# this script produced on 579C144 (2026-08-06) - so mark it applied and
# let the next run skip instead of repeating the work.
if ($p.ExitCode -eq -2147024713) {
Set-Content -Path $appliedMarker -Value (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') -ErrorAction SilentlyContinue
Log "Package was already applied (ERROR_ALREADY_EXISTS) - marker written."
}
}
} catch {
Log "ERROR: Failed to launch provtool.exe: $_"
}
# --- Harvest Windows' own provisioning diagnostics into $ppkgLogDir ---
# provtool.exe /quiet does not drop a zip bundle on success, so the real
# detail lives under C:\ProgramData\Microsoft\Provisioning\, in the
# registry under HKLM\Software\Microsoft\Provisioning\Sessions\*, and in
# the Provisioning-Diagnostics-Provider event log. Copy/export all three
# into our log dir so they ride back with the shopfloor logs bundle.
Log "Harvesting Windows provisioning diagnostics to $ppkgLogDir..."
try {
$provData = 'C:\ProgramData\Microsoft\Provisioning'
if (Test-Path $provData) {
Copy-Item -Path (Join-Path $provData '*') -Destination $ppkgLogDir `
-Recurse -Force -ErrorAction SilentlyContinue
Log " copied $provData -> $ppkgLogDir"
} else {
Log " $provData not present (provtool may not have touched it)"
}
} catch {
Log " WARN: ProgramData copy threw: $_"
}
try {
$sessions = Get-ChildItem 'HKLM:\Software\Microsoft\Provisioning\Sessions' -ErrorAction SilentlyContinue
if ($sessions) {
$snap = $sessions | ForEach-Object {
$props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
if ($props) {
[pscustomobject]@{
Session = $_.PSChildName
BeginTime = $props.BeginTime
LastRunTime = $props.LastRunTime
RebootCount = $props.RebootCount
State = $props.State
StateValue = $props.StateValue
}
}
}
$snap | ConvertTo-Json -Depth 3 |
Out-File -FilePath (Join-Path $ppkgLogDir 'provisioning-sessions.json') -Encoding UTF8
Log " wrote provisioning-sessions.json ($($snap.Count) session(s))"
foreach ($s in $snap) {
Log " session $($s.Session): State=$($s.State) RebootCount=$($s.RebootCount)"
}
} else {
Log " no sessions under HKLM:\Software\Microsoft\Provisioning\Sessions"
}
} catch {
Log " WARN: session snapshot threw: $_"
}
try {
$evtx = Join-Path $ppkgLogDir 'Provisioning-Diagnostics-Admin.evtx'
if (Test-Path $evtx) { Remove-Item $evtx -Force -ErrorAction SilentlyContinue }
$null = & wevtutil.exe epl 'Microsoft-Windows-Provisioning-Diagnostics-Provider/Admin' $evtx 2>&1
if (Test-Path $evtx) { Log " exported $evtx" }
} catch {
Log " WARN: wevtutil export threw: $_"
}
# --- Set OOBE complete (only reached if PPKG didn't trigger immediate reboot) ---
Log "Setting OOBE as complete..."
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\OOBE" /v OOBEComplete /t REG_DWORD /d 1 /f | Out-Null
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\OOBE" /v SetupDisplayedEula /t REG_DWORD /d 1 /f | Out-Null
# If we get here, the PPKG didn't reboot immediately. Unlikely but handle it.
Log "PPKG did not trigger immediate reboot. Returning to caller."