Imaging: defer bulk staging to first-logon Fetch (fresh mount) - Phase 1
WinPE maps Y: early then idles for minutes during the WIM apply; samba deadtime drops the idle session, so the WinPE staging copies failed (bay left with only site-config.json). Add Fetch-StagingPayload.ps1, run from the unattend FirstLogonCommands at first logon on a FRESH share mount (full Windows, no prior idle), to pull the shopfloor-setup tree + preinstall bundle. Detailed per-item log (exit code, counts, timing, mount retries) at C:\Logs\Fetch\ - the old WinPE staging was opaque. - Fetch runs as Order 4, BEFORE wait-for-internet.ps1 (Order 5) which switches the bay to the production network and off the imaging LAN. So Fetch still reaches \172.16.9.1\enrollment. - WinPE bulk staging kept as best-effort fail-fast fallback (Phase 1); the post-boot Fetch is now the authoritative path. Remove the WinPE bulk once validated. Heavy per-type payloads (CMM/Keyence/WaxTrace) stay in WinPE for now - Phase 2. - startnet stages Fetch-StagingPayload.ps1 + writes fetch-source.txt (UNC/user/pass) for the post-boot mount. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -156,26 +156,31 @@
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>4</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Fetch-StagingPayload.ps1"</CommandLine>
|
||||
<Description>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\.</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>5</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\wait-for-internet.ps1"</CommandLine>
|
||||
<Description>Prompt to connect production network then wait for TCP 443 connectivity</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>5</Order>
|
||||
<Order>6</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\migrate-to-wifi.ps1"</CommandLine>
|
||||
<Description>Migrate from wired to WiFi if WiFi adapter present, else stay on wired</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>6</Order>
|
||||
<Order>7</Order>
|
||||
<CommandLine>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</CommandLine>
|
||||
<Description>Install PowerShell 7 BEFORE PPKG so Intune SetupCredentials Win32App finds pwsh.exe (race fix)</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>7</Order>
|
||||
<Order>8</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1"</CommandLine>
|
||||
<Description>Run GCCH Enrollment</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>8</Order>
|
||||
<Order>9</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1"</CommandLine>
|
||||
<Description>Run shopfloor PC type setup</Description>
|
||||
</SynchronousCommand>
|
||||
|
||||
172
playbook/shopfloor-setup/Fetch-StagingPayload.ps1
Normal file
172
playbook/shopfloor-setup/Fetch-StagingPayload.ps1
Normal file
@@ -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)
|
||||
# \\<server>\enrollment\shopfloor-setup\Run-ShopfloorSetup.ps1 -> C:\Enrollment\
|
||||
# \\<server>\enrollment\shopfloor-setup\{backup_lockdown.bat,Shopfloor,common,
|
||||
# _ntlars-backups,gea-shopfloor-<pctype>} -> C:\Enrollment\shopfloor-setup\
|
||||
# \\<server>\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-<type> 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
|
||||
@@ -441,6 +441,17 @@ set /p CMMDODA=<W:\Enrollment\cmm\doda.txt
|
||||
if /i "%CMMDODA%"=="yes" echo doda> 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
|
||||
|
||||
Reference in New Issue
Block a user