diff --git a/playbook/FlatUnattendW10-shopfloor.xml b/playbook/FlatUnattendW10-shopfloor.xml index 52011fa..a810fc5 100644 --- a/playbook/FlatUnattendW10-shopfloor.xml +++ b/playbook/FlatUnattendW10-shopfloor.xml @@ -156,26 +156,31 @@ 4 + powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Fetch-StagingPayload.ps1" + Fetch bulk staging (shopfloor-setup tree + preinstall bundle) from the PXE share on a fresh mount, BEFORE the production-network switch takes the bay off the imaging LAN. Detailed log at C:\Logs\Fetch\. + + + 5 powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\wait-for-internet.ps1" Prompt to connect production network then wait for TCP 443 connectivity - 5 + 6 powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\migrate-to-wifi.ps1" Migrate from wired to WiFi if WiFi adapter present, else stay on wired - 6 + 7 msiexec.exe /i "C:\PreInstall\installers\powershell7\PowerShell-7.5.4-win-x64.msi" /qn /norestart ADD_PATH=1 USE_MU=0 ENABLE_MU=0 DISABLE_TELEMETRY=1 Install PowerShell 7 BEFORE PPKG so Intune SetupCredentials Win32App finds pwsh.exe (race fix) - 7 + 8 powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1" Run GCCH Enrollment - 8 + 9 powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1" Run shopfloor PC type setup diff --git a/playbook/shopfloor-setup/Fetch-StagingPayload.ps1 b/playbook/shopfloor-setup/Fetch-StagingPayload.ps1 new file mode 100644 index 0000000..8340a96 --- /dev/null +++ b/playbook/shopfloor-setup/Fetch-StagingPayload.ps1 @@ -0,0 +1,172 @@ +# Fetch-StagingPayload.ps1 - post-boot bulk staging fetch (first-logon). +# +# WHY THIS EXISTS +# WinPE used to stage the whole shopfloor-setup tree + preinstall bundle to +# the target disk DURING the WinPE phase. But WinPE maps the enrollment share +# (Y:) early, then idles for many minutes while the full Windows image applies. +# Samba's `deadtime` drops idle sessions, so by the time WinPE reached the +# copies the Y: mount was dead and most copies failed (symptom: a bay with +# only site-config.json staged, then nothing). Doing the bulk copy here - at +# first logon, in full Windows, on a FRESH share mount with no prior idle - +# sidesteps that entirely. +# +# WHEN IT RUNS +# The unattend FirstLogonCommands runs this BEFORE the PowerShell 7 MSI install +# (which needs C:\PreInstall\installers\powershell7\) and before +# Run-ShopfloorSetup.ps1 (which needs C:\Enrollment\shopfloor-setup\). So this +# must populate both trees before those steps fire. +# +# WHAT IT FETCHES (generic bulk - Phase 1) +# \\\enrollment\shopfloor-setup\Run-ShopfloorSetup.ps1 -> C:\Enrollment\ +# \\\enrollment\shopfloor-setup\{backup_lockdown.bat,Shopfloor,common, +# _ntlars-backups,gea-shopfloor-} -> C:\Enrollment\shopfloor-setup\ +# \\\enrollment\pre-install\{preinstall.json,installers,udc-backups} +# -> C:\PreInstall\ +# (Heavy per-type payloads - CMM/Keyence/WaxTrace - are still staged in WinPE +# for now; Phase 2 moves those here too.) +# +# LOGGING +# Verbose transcript + a per-item table to C:\Logs\Fetch\. Every robocopy logs +# its exit code, file/dir counts, byte total, and elapsed time, so a failed +# fetch is fully diagnosable (unlike the old opaque WinPE staging). +# +# Always exits 0 - a fetch failure must not abort the FirstLogonCommands chain; +# the log carries the truth and Run-ShopfloorSetup surfaces missing pieces. + +$ErrorActionPreference = 'Continue' + +# --- Logging setup --- +$logDir = 'C:\Logs\Fetch' +if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } +$stamp = Get-Date -Format 'yyyyMMdd_HHmmss' +$logFile = Join-Path $logDir "fetch-staging-$stamp.log" +try { Start-Transcript -Path $logFile -Append -Force | Out-Null } catch {} + +function Log { + param([string]$Message, [string]$Level = 'INFO') + $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + Write-Host "[$ts] [$Level] $Message" +} + +Log "================================================================" +Log "=== Fetch-StagingPayload start (PID $PID) ===" +Log "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" +Log "Host: $env:COMPUTERNAME" +Log "================================================================" + +# --- Resolve the share source + creds (written by startnet to fetch-source.txt; +# falls back to the historical defaults if absent) --- +$shareUnc = '\\172.16.9.1\enrollment' +$shareUser = 'pxe-upload' +$sharePass = 'pxe' +$srcFile = 'C:\Enrollment\fetch-source.txt' +if (Test-Path -LiteralPath $srcFile) { + # Format: line1=UNC, line2=user, line3=pass + $lines = @(Get-Content -LiteralPath $srcFile -ErrorAction SilentlyContinue) + if ($lines.Count -ge 1 -and $lines[0].Trim()) { $shareUnc = $lines[0].Trim() } + if ($lines.Count -ge 2 -and $lines[1].Trim()) { $shareUser = $lines[1].Trim() } + if ($lines.Count -ge 3 -and $lines[2].Trim()) { $sharePass = $lines[2].Trim() } + Log "fetch-source.txt: UNC=$shareUnc user=$shareUser" +} else { + Log "fetch-source.txt absent - using defaults: UNC=$shareUnc user=$shareUser" +} + +# --- pc-type (drives which gea-shopfloor- dir to fetch) --- +$pcType = '' +if (Test-Path -LiteralPath 'C:\Enrollment\pc-type.txt') { + $pcType = (Get-Content -LiteralPath 'C:\Enrollment\pc-type.txt' -First 1 -EA 0).Trim() +} +Log "PC type: $(if ($pcType) { $pcType } else { '(none)' })" + +# --- Status push (best-effort) --- +$pxeStatusLib = 'C:\Enrollment\shopfloor-setup\Shopfloor\lib\Send-PxeStatus.ps1' +# (lib not fetched yet on first run; ignore if absent) +if (Test-Path $pxeStatusLib) { try { . $pxeStatusLib; Send-PxeStatus -Stage 'Fetch-StagingPayload: starting' -StageIndex 1 -StageTotal 8 } catch {} } + +# --- Mount the share fresh (use Z:; retry to ride out a brief blip) --- +$drive = 'Z:' +function Mount-Share { + & net use $drive /delete /y 2>$null | Out-Null + $r = & net use $drive $shareUnc /user:$shareUser $sharePass /persistent:no 2>&1 + return ($LASTEXITCODE -eq 0) +} +$mounted = $false +for ($attempt = 1; $attempt -le 5; $attempt++) { + Log "Mounting $shareUnc as $drive (attempt $attempt/5)..." + if (Mount-Share) { $mounted = $true; Log "Mounted OK"; break } + Log "Mount failed (exit $LASTEXITCODE) - waiting 10s" 'WARN' + Start-Sleep -Seconds 10 +} +if (-not $mounted) { + Log "Could not mount $shareUnc after 5 attempts - ABORTING fetch. Bay will be under-provisioned; re-run this script once the share is reachable." 'ERROR' + try { Stop-Transcript | Out-Null } catch {} + exit 0 +} + +# --- Fetch helper: robocopy one item, log exit + counts + timing --- +$results = @() +function Fetch-Item { + param( + [string]$Label, + [string]$SrcDir, # under $drive + [string]$DstDir, + [string[]]$Files, # named files for a flat copy; empty = whole-dir /E + [switch]$Recurse # /E whole directory + ) + $src = Join-Path $drive $SrcDir + if (-not (Test-Path -LiteralPath $src)) { + Log "[SKIP] $Label - source not on share: $src" 'WARN' + $script:results += [pscustomobject]@{ Item=$Label; Exit='n/a'; Result='SOURCE-MISSING' } + return + } + if (-not (Test-Path -LiteralPath $DstDir)) { New-Item -ItemType Directory -Path $DstDir -Force | Out-Null } + $args = @($src, $DstDir) + if ($Recurse) { $args += '/E' } else { $args += $Files } + $args += @('/R:2','/W:3','/NFL','/NDL','/NP') + $sw = [System.Diagnostics.Stopwatch]::StartNew() + Log "[COPY] $Label : robocopy $src -> $DstDir $(if ($Recurse){'/E'}else{$Files -join ','})" + $out = & robocopy @args 2>&1 + $rc = $LASTEXITCODE + $sw.Stop() + # robocopy 0-7 = success, 8+ = failure + $ok = ($rc -lt 8) + # pull the summary counts robocopy prints + $summary = ($out | Select-String -Pattern 'Files :|Dirs :|Bytes :' ) -join ' | ' + Log "[$(if($ok){'OK'}else{'FAIL'})] $Label exit=$rc time=$([math]::Round($sw.Elapsed.TotalSeconds,1))s $summary" + $script:results += [pscustomobject]@{ Item=$Label; Exit=$rc; Result=$(if($ok){'OK'}else{'FAIL'}) } +} + +# --- Generic bulk fetch --- +$ENR = 'C:\Enrollment' +$SFD = 'C:\Enrollment\shopfloor-setup' +$PIN = 'C:\PreInstall' + +Fetch-Item -Label 'Run-ShopfloorSetup.ps1' -SrcDir 'shopfloor-setup' -DstDir $ENR -Files @('Run-ShopfloorSetup.ps1') +Fetch-Item -Label 'backup_lockdown.bat' -SrcDir 'shopfloor-setup' -DstDir $SFD -Files @('backup_lockdown.bat') +Fetch-Item -Label 'Shopfloor baseline' -SrcDir 'shopfloor-setup\Shopfloor' -DstDir (Join-Path $SFD 'Shopfloor') -Recurse +Fetch-Item -Label 'common' -SrcDir 'shopfloor-setup\common' -DstDir (Join-Path $SFD 'common') -Recurse +Fetch-Item -Label '_ntlars-backups' -SrcDir 'shopfloor-setup\_ntlars-backups' -DstDir (Join-Path $SFD '_ntlars-backups') -Recurse +if ($pcType) { + Fetch-Item -Label "type:$pcType" -SrcDir "shopfloor-setup\$pcType" -DstDir (Join-Path $SFD $pcType) -Recurse +} +# preinstall bundle +Fetch-Item -Label 'preinstall.json' -SrcDir 'pre-install' -DstDir $PIN -Files @('preinstall.json') +Fetch-Item -Label 'preinstall installers' -SrcDir 'pre-install\installers' -DstDir (Join-Path $PIN 'installers') -Recurse +Fetch-Item -Label 'udc-backups' -SrcDir 'pre-install\udc-backups' -DstDir (Join-Path $PIN 'udc-backups') -Recurse + +# --- Unmount --- +& net use $drive /delete /y 2>$null | Out-Null + +# --- Summary table --- +Log "================================================================" +Log "FETCH SUMMARY:" +foreach ($r in $results) { Log (" {0,-28} exit={1,-4} {2}" -f $r.Item, $r.Exit, $r.Result) } +$failed = @($results | Where-Object { $_.Result -eq 'FAIL' }) +if ($failed.Count -gt 0) { + Log "$($failed.Count) item(s) FAILED: $(( $failed | ForEach-Object { $_.Item }) -join ', ')" 'ERROR' +} else { + Log "All fetched items OK." 'INFO' +} +Log "=== Fetch-StagingPayload complete ===" +try { Stop-Transcript | Out-Null } catch {} +exit 0 diff --git a/playbook/startnet.cmd b/playbook/startnet.cmd index d98e051..3390d81 100644 --- a/playbook/startnet.cmd +++ b/playbook/startnet.cmd @@ -441,6 +441,17 @@ set /p CMMDODA= W:\Enrollment\pc-subtype.txt :cmm_id_done robocopy "Y:\shopfloor-setup" "W:\Enrollment" "Run-ShopfloorSetup.ps1" /R:1 /W:1 /NFL /NDL /NJH /NJS +REM Post-boot bulk re-fetch. WinPE staging below is best-effort (it can fail if +REM the Y: mount went idle-dead during the WIM apply); Fetch-StagingPayload.ps1 +REM re-pulls the shopfloor-setup tree + preinstall bundle at first logon on a +REM FRESH mount. The unattend FirstLogonCommands runs it before the PowerShell 7 +REM MSI + Run-ShopfloorSetup. Stage the script + its source coords here. +robocopy "Y:\shopfloor-setup" "W:\Enrollment" "Fetch-StagingPayload.ps1" /R:1 /W:1 /NFL /NDL /NJH /NJS +> W:\Enrollment\fetch-source.txt ( +echo \\172.16.9.1\enrollment +echo pxe-upload +echo pxe +) REM --- Always copy Shopfloor baseline scripts --- mkdir W:\Enrollment\shopfloor-setup 2>NUL robocopy "Y:\shopfloor-setup" "W:\Enrollment\shopfloor-setup" "backup_lockdown.bat" /R:1 /W:1 /NFL /NDL /NJH /NJS