# # Capture-SiteContent.ps1 - Pull the site content from a running PXE server # into the pxe-usb build folder, on a Windows PC. # # The Windows twin of scripts/capture-site-content.sh. A new PXE server # needs content that neither the repo nor the MCL image import brings; it # exists only on a working server. This pulls, into the build folder: # site-content\shared\Applications\extra\ Office, Adobe, OpenText, Java, # fonts, Wi-Fi profile, Zscaler (every unattend installs from it; # pxe-status\ is left out, the playbook deploys it) # site-content\images\\Applications\ each image's own files and # folders there: GE CA certificates, the Engineering Layer payload # (without Install-EngineeringLayer.ps1, which the repo carries) # site-content\links.txt the links images keep there # site-content\SHA256SUMS checked by the playbook # bios-staging\ *.exe and models.txt from _shared\BIOS # boot-files\boot.wim # boot-tools\blancco\ the Blancco files already in that folder # # How: ONE ssh session (one password prompt) runs a small script on the # server that collects those files, hashes them there (SHA-256) and streams # them back as a tar archive. Here the archive is unpacked into a scratch # folder and every file is checked against the server's hashes, and against # what FAT32 can hold (under 4 GiB, no two names that differ only in case). # Only when all of that passes does the new content replace the old. The # server is only read. # # Needs: Windows 10/11 (ssh.exe and tar.exe are built in), SSH access to the # server as pxe (from the GE network use the server's GE address). # # Usage: double-click Capture-SiteContent.cmd in the build folder (asks for # the server address), or: # .\scripts\Capture-SiteContent.ps1 -Server 10.x.x.x # # Exit codes: 0 captured and verified, 1 failed (the old content is kept). [CmdletBinding()] param( [string]$Server = '', [string]$User = 'pxe', [string]$PayloadRoot = '', [switch]$Interactive ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2 $RepoRoot = Split-Path -Parent $PSScriptRoot if (-not $PayloadRoot) { $PayloadRoot = $RepoRoot } $PayloadRoot = (Resolve-Path -LiteralPath $PayloadRoot).Path $FatMax = [int64]4GB - 1 function Say([string]$m) { Write-Host $m } function Done([int]$code) { if ($Interactive) { Write-Host ''; Read-Host 'Press Enter to close' | Out-Null } exit $code } function Fail([string]$m) { Write-Host "ERROR: $m" -ForegroundColor Red; Done 1 } trap { Write-Host "ERROR: $_" -ForegroundColor Red; Done 1 } foreach ($exe in 'ssh.exe', 'tar.exe') { if (-not (Get-Command $exe -ErrorAction SilentlyContinue)) { Fail "$exe not found (Windows 10 1803 or later has both)" } } if (-not $Server) { if (-not $Interactive) { Fail 'give -Server (the PXE server address; from the GE network its GE address)' } Say 'Address of the running PXE server to copy from.' Say 'From the GE network use its GE address; on the imaging switch 172.16.9.1.' $Server = (Read-Host 'Server').Trim() if (-not $Server) { Fail 'no server given' } } if ($Server -notmatch '^[A-Za-z0-9.\-:]+$') { Fail "not a host name or address: $Server" } # The Blancco files to refresh: whatever is already in boot-tools\blancco # (the server also holds files its playbook builds; those are not pulled). $blanccoDir = Join-Path $PayloadRoot 'boot-tools\blancco' $blancco = @() if (Test-Path -LiteralPath $blanccoDir) { $blancco = @(Get-ChildItem -LiteralPath $blanccoDir -Force | ForEach-Object { $_.Name } | Where-Object { $_ -match '^[A-Za-z0-9._+-]+$' }) } # --- the script that runs on the server (bash, stdout = the tar stream) ----- $remote = @' set -euo pipefail SHARE=${CAPTURE_SHARE:-/srv/samba/winpeapps} WWW=${CAPTURE_WWW:-/var/www/html} ST=$(mktemp -d /tmp/pxe-capture.XXXXXX) trap 'rm -rf "$ST"' EXIT SC=$ST/site-content mkdir -p "$SC/shared/Applications" "$SC/images" "$ST/bios-staging" "$ST/boot-files" "$ST/boot-tools/blancco" [ -d "$SHARE/_shared/Applications/extra" ] && ln -s "$SHARE/_shared/Applications/extra" "$SC/shared/Applications/extra" printf '# imagename in Deploy/Applicationslink target under winpeapps/\n' >"$SC/links.txt" for apps in "$SHARE"/*/Deploy/Applications; do [ -d "$apps" ] || continue img=${apps#"$SHARE"/}; img=${img%%/*} case "$img" in _*) continue ;; esac for e in "$apps"/* "$apps"/.[!.]*; do [ -e "$e" ] || [ -L "$e" ] || continue n=${e##*/} if [ -L "$e" ]; then t=$(readlink "$e") case "$t" in "$SHARE"/_shared/*) printf '%s\t%s\t%s\n' "$img" "$n" "${t#"$SHARE"/}" >>"$SC/links.txt" ;; *) echo "WARNING: $img/Deploy/Applications/$n links outside _shared ($t); not captured" >&2 ;; esac continue fi # a real BPRT folder is an old import's copy of the vendor folder [ "${n,,}" = bprt ] && continue mkdir -p "$SC/images/$img/Applications" ln -s "$e" "$SC/images/$img/Applications/$n" done done for f in "$SHARE"/_shared/BIOS/*.exe "$SHARE"/_shared/BIOS/models.txt; do [ -f "$f" ] && ln -s "$f" "$ST/bios-staging/${f##*/}" done [ -f "$WWW/win11/sources/boot.wim" ] && ln -s "$WWW/win11/sources/boot.wim" "$ST/boot-files/boot.wim" for n in "$@"; do [ -e "$WWW/blancco/$n" ] && ln -s "$WWW/blancco/$n" "$ST/boot-tools/blancco/$n" done EXCL=(--exclude=./site-content/shared/Applications/extra/pxe-status --exclude=Install-EngineeringLayer.ps1) echo "pxe-capture: hashing on the server (a few minutes for about 12 GB)..." >&2 ( cd "$ST" && find -L . -type f ! -path './site-content/shared/Applications/extra/pxe-status/*' \ ! -name Install-EngineeringLayer.ps1 ! -name capture-SHA256SUMS -print0 | sort -z | xargs -0 -r sha256sum ) >"$ST/capture-SHA256SUMS.tmp" mv "$ST/capture-SHA256SUMS.tmp" "$ST/capture-SHA256SUMS" echo "pxe-capture: $(wc -l <"$ST/capture-SHA256SUMS") files; sending..." >&2 tar -chf - -C "$ST" "${EXCL[@]}" . '@ $scratch = Join-Path $PayloadRoot '.capture-new' $old = Join-Path $PayloadRoot '.capture-old' foreach ($d in $scratch, $old) { if (Test-Path -LiteralPath $d) { Remove-Item -LiteralPath $d -Recurse -Force } } New-Item -ItemType Directory -Path $scratch | Out-Null $remoteFile = Join-Path ([IO.Path]::GetTempPath()) ('pxe-capture-' + [guid]::NewGuid().ToString('N') + '.sh') [IO.File]::WriteAllText($remoteFile, ($remote -replace "`r", ''), (New-Object System.Text.UTF8Encoding($false))) Say '============================================' Say 'PXE site content capture (Windows)' Say '============================================' Say "Server : $User@$Server" Say "Folder : $PayloadRoot" Say "Blancco : $($blancco.Count) known files" Say '' Say "[1/3] Copying from the server (enter the $User password when asked)..." # cmd's pipe passes bytes unchanged; a PowerShell pipe would re-encode the tar. $names = ($blancco | ForEach-Object { "'" + $_ + "'" }) -join ' ' $cmdline = "ssh.exe -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 $User@$Server `"bash -s -- $names`" < `"$remoteFile`" | tar.exe -xf - -C `"$scratch`"" & cmd.exe /c $cmdline $tarCode = $LASTEXITCODE Remove-Item -LiteralPath $remoteFile -Force -ErrorAction SilentlyContinue $sumFile = Join-Path $scratch 'capture-SHA256SUMS' if (-not (Test-Path -LiteralPath $sumFile)) { Remove-Item -LiteralPath $scratch -Recurse -Force -ErrorAction SilentlyContinue Fail "nothing usable arrived (ssh/tar exit $tarCode): wrong address or password, or the server is not reachable. Nothing was changed." } Say '' Say '[2/3] Checking every file against the server''s SHA-256...' $lines = @([IO.File]::ReadAllLines($sumFile) | Where-Object { $_ }) $want = @{} $lower = @{} $errors = New-Object System.Collections.Generic.List[string] foreach ($l in $lines) { if ($l -notmatch '^([0-9a-f]{64}) \./(.+)$') { $errors.Add("bad line in the server's list: $l"); continue } $rel = $Matches[2] $key = $rel.ToLowerInvariant() if ($lower.ContainsKey($key)) { $errors.Add("names differ only in case (one would overwrite the other): $rel / $($lower[$key])") } $lower[$key] = $rel $want[$rel] = $Matches[1] } $i = 0 foreach ($rel in $want.Keys) { $i++ if ($i % 25 -eq 0) { Write-Progress -Activity 'Verifying' -Status $rel -PercentComplete ([int](100 * $i / $want.Count)) } $p = Join-Path $scratch ($rel -replace '/', '\') if (-not (Test-Path -LiteralPath $p -PathType Leaf)) { $errors.Add("missing: $rel"); continue } $f = Get-Item -LiteralPath $p if ($f.Length -gt $FatMax) { $errors.Add("4 GiB or more (FAT32 cannot hold it): $rel") } if ((Get-FileHash -LiteralPath $p -Algorithm SHA256).Hash.ToLowerInvariant() -ne $want[$rel]) { $errors.Add("differs from the server: $rel") } } Write-Progress -Activity 'Verifying' -Completed if ($errors.Count -gt 0) { Say "$($errors.Count) problem(s); nothing was changed:" $errors | Select-Object -First 20 | ForEach-Object { Say " $_" } Remove-Item -LiteralPath $scratch -Recurse -Force -ErrorAction SilentlyContinue Fail 'capture not usable (long paths? see https://learn.microsoft.com/windows/win32/fileio/maximum-file-path-limitation)' } Say " $($want.Count) files match" # site-content\SHA256SUMS in the format the playbook checks (sha256sum -c, # paths relative to site-content), from the server's own hashes. $sc = New-Object System.Collections.Generic.List[string] foreach ($l in $lines) { if ($l -match '^([0-9a-f]{64}) \./site-content/(.+)$') { $sc.Add("$($Matches[1]) ./$($Matches[2])") } } [IO.File]::WriteAllText((Join-Path $scratch 'site-content\SHA256SUMS'), (($sc -join "`n") + "`n"), (New-Object System.Text.UTF8Encoding($false))) Say '' Say '[3/3] Replacing the old content...' New-Item -ItemType Directory -Path $old | Out-Null function Swap([string]$rel) { $new = Join-Path $scratch $rel if (-not (Test-Path -LiteralPath $new)) { return } $cur = Join-Path $PayloadRoot $rel if (Test-Path -LiteralPath $cur) { $bak = Join-Path $old $rel New-Item -ItemType Directory -Path (Split-Path -Parent $bak) -Force | Out-Null Move-Item -LiteralPath $cur -Destination $bak } New-Item -ItemType Directory -Path (Split-Path -Parent $cur) -Force | Out-Null Move-Item -LiteralPath $new -Destination $cur Say " $rel" } Swap 'site-content' Swap 'bios-staging' Swap 'boot-files\boot.wim' foreach ($n in $blancco) { Swap (Join-Path 'boot-tools\blancco' $n) } Remove-Item -LiteralPath $old -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $scratch -Recurse -Force -ErrorAction SilentlyContinue $size = (Get-ChildItem -LiteralPath (Join-Path $PayloadRoot 'site-content') -Recurse -File | Measure-Object Length -Sum).Sum Say '' Say ('Done: site-content {0:N1} GB, {1} files. Build the USB next (Build-PxeUsb.cmd).' -f ($size / 1GB), $sc.Count) Done 0