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/.
229 lines
11 KiB
PowerShell
229 lines
11 KiB
PowerShell
#
|
|
# 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\<image>\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 '# image<TAB>name in Deploy/Applications<TAB>link 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
|