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/.
330 lines
16 KiB
PowerShell
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
|
|
}
|