Files
pxe-server/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1
cproudlock 04db6d6605 Generic build for other GE sites: no WJ data, one shopfloor type, Windows USB builder
Everything West Jefferson specific moved out to a separate repo
(pxe-site-wj): NTLARS/UDC/heat-treat backups keyed by WJ machine numbers,
CMM and Wax/Trace bay lists, WJ OpenText profiles, site-config.json, the
WJ preinstall list, and the WJ shopfloor PC types (Collections,
NoCollections, Common, Keyence, CMM, Genspect, HeatTreat, Wax/Trace,
Display, PartMarker) with GE-Enforce, machine numbers and the desktop
layout. The part-marker bench tools moved to their own repo. Dead
scripts removed: build-proxmox-iso, deploy-bios, sync_hardware_models,
mirror-from-gold, pull-bios, startnet-template.cmd.

Shopfloor is one generic type, gea-shopfloor-standard, with today's
timing:
- PPKG at first logon (unattend Order 10, unchanged).
- Install-AppList.ps1 runs C:\PreInstall\preinstall.json during setup.
- A new SYSTEM task, GE Shopfloor Post-PPKG, runs postppkg.json once
  after the post-PPKG reboot. It also clears AutoLogonCount after the
  ShopFloor handover (the Users-level monitor can be denied that HKLM
  write) and removes the Intune Sync task when done.
- Both lists ship empty. A site adds entries and installers.
- WinPE choice 3 goes straight to the PPKG menu. Staging, fetch and heal
  carry only the generic payload. The webapp's shopfloor sub-menu editor
  is gone, and the imaging tiles name the new stages.
- The playbook deploys the empty lists only if missing. It now deploys
  the PowerShell 7 MSI the shopfloor unattend installs, which it never
  did before.

Blancco site settings move into the webapp (Updates > Blancco releases):
business location, licensing Wi-Fi SSID and password. They are rendered
into the served preferences.xml and applied to every staged release, and
are seeded from site-vars (site_name, blancco_wifi_ssid,
blancco_wifi_password) only while none exist. The repo XML carries no
site values; the plaintext Wi-Fi password is gone from it.

New site_name and site_timezone settings: report and evidence-pack site
name, server clock and webapp time zone. No more West Jefferson defaults
in webapp/config.py or the dashboard title. Sync-BPRT.ps1 gets the
server address filled in at download instead of WJ's GE address.

USB on Windows:
- scripts/Build-PxeUsb.ps1 runs the same checks as build-usb.sh. It
  writes one FAT32 CIDATA partition (ISO files plus patched grub.cfg plus
  payload) with diskpart and reads every file back by SHA-256.
- Tested with Windows PowerShell 5.1 in the win11 VM, and the layout
  installs and first-boots in the pxetest VM.
- scripts/stage-usb-share.sh stages the build folder on a share.
- site-content/ is optional for both builders.

KB docs/kb/How_To_Deploy_PXE_Server_New_Site.html: new-site deployment,
Windows only, with a site-vars.yml guide.

Checks: webapp tests pass (5 suites); the unattend lint and playbook
syntax checks pass; every .ps1 parses; there are no WJ hosts, IPs, names
or PC types in playbook/, webapp/, scripts/ or autoinstall/.
2026-10-16 20:37:22 -04:00

330 lines
16 KiB
PowerShell

# Run-ShopfloorSetup.ps1 - shopfloor setup (gea-shopfloor-standard).
#
# Called by the unattend FirstLogonCommands as SupportUser (admin), after
# Order 10 has already applied the PPKG (run-enrollment.ps1).
# Flow:
# 1. Site app list "during setup": C:\PreInstall\preinstall.json
# (Shopfloor\Install-AppList.ps1; empty in the generic build)
# 2. Copy desktop tools (sync_intune, Force-Lockdown, SetShopfloorAutoLogon)
# 3. Register the "Shopfloor Intune Sync" logon task (Monitor-IntuneProgress:
# waits for the PPKG/Intune lockdown, then hands the PC to ShopFloor)
# 4. Register "GE Shopfloor Post-PPKG" (SYSTEM, at startup): installs
# C:\PreInstall\postppkg.json once after the reboot (empty in the
# generic build) and clears AutoLogonCount after the ShopFloor handover
# 5. Network gate, run-enrollment (a no-op when Order 10 applied the PPKG),
# then Monitor-IntuneProgress -PostPpkg: settle and reboot
# --- Transcript logging ---
$logDir = 'C:\Logs\SFLD'
if (-not (Test-Path $logDir)) {
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
}
$transcriptPath = Join-Path $logDir 'shopfloor-setup.log'
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
Write-Host ""
Write-Host "================================================================"
Write-Host "=== Run-ShopfloorSetup.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ==="
Write-Host " Transcript: $transcriptPath"
Write-Host "================================================================"
Write-Host ""
# Imaging-progress reporter. Posts coarse stage updates to the PXE webapp
# at http://172.16.9.1:9009/imaging/status so the operator can watch
# progress in a browser. Best-effort: failures never block imaging.
$pxeStatusLib = Join-Path $PSScriptRoot 'shopfloor-setup\Shopfloor\lib\Send-PxeStatus.ps1'
if (Test-Path $pxeStatusLib) {
try { . $pxeStatusLib } catch { Write-Warning "Send-PxeStatus load failed: $_" }
}
function Report-Stage {
param([string]$Stage, [int]$Index, [int]$Total = 8, [string]$Status = 'in_progress', [string]$Error_ = '')
if (Get-Command Send-PxeStatus -ErrorAction SilentlyContinue) {
Send-PxeStatus -Stage $Stage -StageIndex $Index -StageTotal $Total -Status $Status -Error_ $Error_
}
}
Report-Stage -Stage 'Run-ShopfloorSetup: starting' -Index 2
# AutoLogonCount is NOT set here. Previously we bumped it to 99/4, but
# Windows decrements it per-logon and at 0 clears AutoAdminLogon -- which
# nukes the lockdown-configured ShopFloor autologon later in the chain.
# The unattend XML's <AutoLogon><LogonCount> handles SupportUser logons;
# the lockdown's Autologon.exe handles ShopFloor. We stay out of it.
# Cancel any pending reboot so it doesn't interrupt setup
cmd /c "shutdown /a 2>nul" *>$null
# Self-resume: register this script as a RunOnce so a vendor-installer-
# forced reboot mid-flight (FormTracePak Setup.exe, eDNC MSI, etc) auto-
# resumes the chain after the next SupportUser auto-login. RunOnce is
# single-shot - if we complete normally we remove this key at end of
# script. If we're killed mid-flight by a forced reboot, the key
# survives and fires after reboot.
#
# Idempotent design throughout this script: every step checks detection
# before installing, so a forced-reboot re-entry just skips the already-
# done work and continues from where it left off.
#
# Also top up AutoLogonCount so the SupportUser autologon budget
# (LogonCount=12 from unattend XML) survives extra unplanned reboots.
$selfResumeKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
$selfResumeName = 'ResumeRunShopfloorSetup'
$selfResumeCmd = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + $PSCommandPath + '"'
try {
Set-ItemProperty -Path $selfResumeKey -Name $selfResumeName -Value $selfResumeCmd -Type String -Force -ErrorAction Stop
Write-Host "Self-resume RunOnce registered: will re-fire $PSCommandPath if interrupted"
} catch {
Write-Warning "Failed to register self-resume RunOnce: $_"
}
try {
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name 'AutoLogonCount' -Value 10 -Type DWord -Force -ErrorAction Stop
Write-Host "AutoLogonCount topped up to 10 (vendor-forced reboot resilience)"
} catch {
Write-Warning "Failed to top up AutoLogonCount: $_"
}
# Wired NIC state handling moved to sync_intune (Monitor-IntuneProgress.ps1).
# Previously this script prompted the tech to unplug the PXE cable and
# then re-enabled wired adapters interactively - that blocked the whole
# imaging chain on human keypress. The new flow leaves the wired state
# exactly as Order 5 (migrate-to-wifi.ps1) left it:
# - Tower (no WiFi): wired stays enabled, Run-ShopfloorSetup runs on wired.
# - Laptop (WiFi): wired disabled, Run-ShopfloorSetup runs on WiFi.
# Sync_intune re-enables wired at the start of its monitor loop, by which
# time the tech has had ample wall-clock time to physically unplug PXE
# and re-cable into production without blocking the chain on a keypress.
$enrollDir = "C:\Enrollment"
$typeFile = Join-Path $enrollDir "pc-type.txt"
$setupDir = Join-Path $enrollDir "shopfloor-setup"
# NOTE: there is deliberately NO wait for an Entra join here - but NOT because
# shopfloor bays skip enrollment. They do enrol: the SFLD provisioning package
# joins Entra using the BPRT token it carries, and a human then assigns the
# device category in Intune.
#
# The wait is absent because the join cannot happen yet. At this point the bay is
# still on the isolated PXE LAN with no route to Entra - observed on 579C144
# 2026-08-06, holding 172.16.9.81 and 172.24.19.142, neither in the production
# ranges. sync_intune retries every 30 seconds until the tech re-cables to
# production, which is the right place to wait. An earlier version blocked here
# for 45 minutes and then warned about a failure that had not happened.
# "Entra ID Joined: false" in C:\Logs\BPRT\criticalChecks.json straight after
# imaging is therefore NORMAL, not a fault.
#
# CORRECTION (2026-08-06): this comment previously claimed shopfloor PCs are
# "vanilla by design" and that the orchestrator runs with -ManualFallback to skip
# BPRT injection and the package entirely. That was wrong and dangerous.
# -ManualFallback runs sysprep /oobe /reboot, so wiring it to an at-logon task
# syspreped finished machines seconds after autologon and destroyed the
# deployment chain. See docs/OWNERSHIP.md.
if (-not (Test-Path $typeFile)) {
# A missing pc-type.txt means one of two very different things, and the old
# blanket "skip + exit 0" hid the bad one for weeks: four Display bays sat
# at imaging stage 2 with a green exit code and nobody noticed.
# - no C:\Enrollment at all -> this machine was never staged by WinPE
# (pre-imaging, or the staging block never ran). That is a FAILURE on a
# machine that has clearly just been imaged, so say so loudly.
# - C:\Enrollment exists but no pc-type.txt -> staging ran and the write
# failed. Also a failure.
$stagingLog = Join-Path $enrollDir 'winpe-staging.log'
$detail = if (Test-Path $enrollDir) {
"C:\Enrollment exists but pc-type.txt is missing - WinPE staging ran but did not write it. Check $stagingLog."
} else {
"C:\Enrollment does not exist - WinPE staging never ran. The Windows volume was probably not found in startnet.cmd, so pc-type.txt, the enrollment package and shopfloor-setup were ALL skipped."
}
Write-Host ""
Write-Host "================================================================"
Write-Host " FAILED: no pc-type.txt at $typeFile"
Write-Host " $detail"
Write-Host " Shopfloor setup cannot run. This PC is imaged but NOT configured."
Write-Host "================================================================"
Write-Host ""
Report-Stage -Stage 'Run-ShopfloorSetup: FAILED - no pc-type.txt' -Index 2 -Status 'failed' -Error_ $detail
exit 1
}
$pcType = (Get-Content $typeFile -First 1).Trim()
if (-not $pcType) {
Write-Host "pc-type.txt is empty - skipping shopfloor setup."
exit 0
}
Write-Host "Shopfloor PC Type: $pcType"
# --- 1. Site app list, during setup ---
Report-Stage -Stage 'Run-ShopfloorSetup: installing the site app list' -Index 3
$appList = Join-Path $setupDir 'Shopfloor\Install-AppList.ps1'
if (Test-Path -LiteralPath $appList) {
cmd /c "shutdown /a 2>nul" *>$null
try {
& $appList -ListPath 'C:\PreInstall\preinstall.json' -Phase 'PreInstall'
} catch {
Write-Warning "Install-AppList failed: $_"
}
} else {
Write-Warning "Install-AppList.ps1 not found at $appList"
}
cmd /c "shutdown /a 2>nul" *>$null
Report-Stage -Stage 'Run-ShopfloorSetup: site app list done' -Index 4
Write-Host "Shopfloor setup complete for $pcType."
# --- 2. Copy utility scripts to SupportUser desktop ---
foreach ($tool in @('sync_intune.bat', 'Force-Lockdown.bat', 'SetShopfloorAutoLogon.bat')) {
$src = Join-Path $setupDir "Shopfloor\$tool"
if (Test-Path $src) {
Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$tool" -Force
Write-Host "$tool copied to desktop."
}
}
# --- 3. Register sync_intune as persistent @logon scheduled task ---
# Must be registered BEFORE enrollment because Install-ProvisioningPackage
# triggers an immediate reboot that kills run-enrollment.ps1. The task
# registration must survive the PPKG reboot, so we do it here while
# Run-ShopfloorSetup.ps1 is still running.
#
# The task fires at every logon until sync_intune detects completion and
# unregisters itself. It monitors Intune enrollment (Phase 1-5), NOT BPRT
# app installs -- BPRT finishes on its own in the background after the
# PPKG reboot, and is irrelevant to the Intune lifecycle.
$taskName = 'Shopfloor Intune Sync'
$monitorScript = Join-Path $setupDir 'Shopfloor\lib\Monitor-IntuneProgress.ps1'
if (Test-Path -LiteralPath $monitorScript) {
try {
$action = New-ScheduledTaskAction `
-Execute 'powershell.exe' `
-Argument "-NoProfile -NoExit -ExecutionPolicy Bypass -File `"$monitorScript`" -AsTask"
$trigger = New-ScheduledTaskTrigger -AtLogOn
$principal = New-ScheduledTaskPrincipal `
-GroupId 'S-1-5-32-545' `
-RunLevel Limited
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-ExecutionTimeLimit (New-TimeSpan -Hours 2)
Register-ScheduledTask `
-TaskName $taskName `
-Action $action `
-Trigger $trigger `
-Principal $principal `
-Settings $settings `
-Force `
-ErrorAction Stop | Out-Null
Write-Host "Registered '$taskName' logon task."
} catch {
Write-Warning "Failed to register sync task: $_"
}
} else {
Write-Warning "Monitor-IntuneProgress.ps1 not found at $monitorScript"
}
# --- 4. Post-PPKG app list + AutoLogonCount cleanup (SYSTEM task) ---
# Registered before the PPKG step: the PPKG reboot can kill this script.
$registerPost = Join-Path $setupDir 'Shopfloor\Register-PostPpkgTask.ps1'
if (Test-Path -LiteralPath $registerPost) {
try { & $registerPost } catch { Write-Warning "Post-PPKG task registration failed: $_" }
} else {
Write-Warning "Register-PostPpkgTask.ps1 not found at $registerPost"
}
# --- Run enrollment (PPKG install) ---
# Enrollment is the LAST thing we do. Install-ProvisioningPackage triggers
# an immediate reboot -- everything after this call is unlikely to execute.
# The sync_intune task is already registered above, so the PPKG reboot
# can kill us and the chain continues on the next boot.
# ---- Network-handoff gate (BEFORE PPKG) ----
# PXE imaging LAN has no DHCP gateway by design. Laptops with WiFi auto-
# connect to corp SSID and get a default route via WiFi - PPKG/AAD/Intune
# work fine. Towers without WiFi have ONLY the wired link to PXE LAN ->
# no default route -> AAD + Intune endpoints unreachable -> enrollment
# stalls -> re-image required (recurring failure mode observed
# 2026-05-05). Block run-enrollment until tech provides a usable internet
# path. Cheap to verify, prevents wasted imaging cycles.
$hasWifi = [bool](Get-NetAdapter -ErrorAction SilentlyContinue |
Where-Object { $_.PhysicalMediaType -eq 'Native 802.11' -or $_.MediaType -like '*802.11*' })
$hasDefaultRoute = [bool](Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue)
if (-not $hasWifi -and -not $hasDefaultRoute) {
Write-Host ""
Write-Host "================================================================" -ForegroundColor Red
Write-Host " STOP - NO USABLE INTERNET PATH" -ForegroundColor Red
Write-Host "================================================================" -ForegroundColor Red
Write-Host ""
Write-Host " This PC has no WiFi adapter and no default route." -ForegroundColor Yellow
Write-Host " Currently on the PXE imaging LAN, which has no gateway." -ForegroundColor Yellow
Write-Host " PPKG enrollment WILL fail because AAD + Intune endpoints" -ForegroundColor Yellow
Write-Host " are unreachable from this network." -ForegroundColor Yellow
Write-Host ""
Write-Host " FIX: Plug this PC into a corp wall jack now." -ForegroundColor Cyan
Write-Host ""
Write-Host " Verify with: ipconfig" -ForegroundColor Cyan
Write-Host " A non-blank Default Gateway must show on the wired NIC." -ForegroundColor Cyan
Write-Host ""
Write-Host " Press R to retry after moving the cable." -ForegroundColor Cyan
Write-Host " Press X to abort imaging (no enrollment runs)." -ForegroundColor Cyan
Write-Host ""
while ($true) {
try { $key = ([Console]::ReadKey($true).KeyChar.ToString()).ToUpper() }
catch { $key = (Read-Host 'Press R or X').Trim().ToUpper() }
if ($key -eq 'X') {
Write-Host "Aborted by tech. Imaging stopped before PPKG." -ForegroundColor Yellow
try { Stop-Transcript | Out-Null } catch {}
exit 1
}
if ($key -eq 'R') {
$hasDefaultRoute = [bool](Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue)
if ($hasDefaultRoute) {
Write-Host "Default route detected. Continuing to PPKG enrollment." -ForegroundColor Green
break
}
Write-Host "Still no default route. Verify cable + corp jack." -ForegroundColor Red
}
}
}
$enrollScript = Join-Path $enrollDir 'run-enrollment.ps1'
if (Test-Path -LiteralPath $enrollScript) {
Write-Host ""
Report-Stage -Stage 'Run-ShopfloorSetup: PPKG enrollment' -Index 5
Write-Host "=== Running enrollment (PPKG install) ==="
Write-Host "NOTE: PPKG schedules a near-immediate reboot. We will cancel"
Write-Host " it and hand off to Monitor-IntuneProgress -PostPpkg, which"
Write-Host " runs a 120s settle (giving MDM time to push baseline"
Write-Host " policy) and then performs a clean reboot."
try { Stop-Transcript | Out-Null } catch {}
& $enrollScript
Write-Host ""
Report-Stage -Stage 'Run-ShopfloorSetup: handoff to Monitor-IntuneProgress' -Index 6
Write-Host "=== Handing off to Monitor-IntuneProgress -PostPpkg ==="
cmd /c "shutdown /a 2>nul" | Out-Null
# Made it past all the reboot-prone vendor installers. Clear the
# self-resume RunOnce so a normal completion + reboot does not re-fire
# this script post-PPKG (PPKG install owns the reboot chain from here).
try { Remove-ItemProperty -Path $selfResumeKey -Name $selfResumeName -ErrorAction SilentlyContinue } catch {}
$monitor = Join-Path $setupDir 'Shopfloor\lib\Monitor-IntuneProgress.ps1'
if (Test-Path -LiteralPath $monitor) {
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $monitor -PostPpkg
} else {
Write-Warning "Monitor-IntuneProgress.ps1 not found at $monitor - falling back to plain reboot"
shutdown /r /t 10
}
} else {
Write-Host "run-enrollment.ps1 not found - skipping enrollment."
Write-Host ""
Write-Host "================================================================"
Write-Host "=== Run-ShopfloorSetup.ps1 complete $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ==="
Write-Host "================================================================"
try { Remove-ItemProperty -Path $selfResumeKey -Name $selfResumeName -ErrorAction SilentlyContinue } catch {}
try { Stop-Transcript | Out-Null } catch {}
Write-Host "Rebooting in 10 seconds..."
shutdown /r /t 10
}