- Shopfloor PC type menu (CMM, WaxAndTrace, Keyence, Genspect, Display, Standard) - Baseline scripts: OpenText CSF, Start Menu shortcuts, network/WinRM, power/display - Standard type: eDNC + MarkZebra with 64-bit path mirroring - CMM type: Hexagon CLM Tools, PC-DMIS 2016/2019 R2 - Display sub-type: Lobby vs Dashboard - Webapp: enrollment management, image config editor, UI refresh - Upload-Image.ps1: robocopy MCL cache to PXE server - Download-Drivers.ps1: Dell driver download pipeline - Slim Blancco GRUB EFI (10MB -> 660KB) for old hardware compat - Shopfloor display imaging guide docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
380 lines
15 KiB
PowerShell
380 lines
15 KiB
PowerShell
#
|
|
# Upload-Image.ps1 — Copy MCL cached image to the PXE server
|
|
#
|
|
# Reads user_selections.json to upload only the selected OS, matching
|
|
# packages, and config files. Drivers are EXCLUDED by default.
|
|
#
|
|
# Usage:
|
|
# .\Upload-Image.ps1 (selected OS + packages, no drivers)
|
|
# .\Upload-Image.ps1 -IncludeDrivers (also upload selected hardware drivers)
|
|
# .\Upload-Image.ps1 -CachePath "D:\MCL\Cache" (custom cache location)
|
|
# .\Upload-Image.ps1 -Server 10.9.100.1 (custom server IP)
|
|
#
|
|
# After upload, use the PXE webapp (http://10.9.100.1:9009) to import
|
|
# the uploaded content into the desired image type.
|
|
#
|
|
|
|
param(
|
|
[string]$CachePath = "C:\ProgramData\GEAerospace\MediaCreator\Cache",
|
|
[string]$Server = "10.9.100.1",
|
|
[string]$User = "pxe-upload",
|
|
[string]$Pass = "pxe",
|
|
[switch]$IncludeDrivers
|
|
)
|
|
|
|
$Share = "\\$Server\image-upload"
|
|
|
|
function Format-Size {
|
|
param([long]$Bytes)
|
|
if ($Bytes -ge 1GB) { return "{0:N1} GB" -f ($Bytes / 1GB) }
|
|
if ($Bytes -ge 1MB) { return "{0:N1} MB" -f ($Bytes / 1MB) }
|
|
return "{0:N0} KB" -f ($Bytes / 1KB)
|
|
}
|
|
|
|
function Resolve-DestDir {
|
|
param([string]$Dir)
|
|
return ($Dir -replace '^\*destinationdir\*\\?', '')
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host " PXE Server Image Uploader" -ForegroundColor Cyan
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
|
|
# --- Validate source paths ---
|
|
$DeployPath = Join-Path $CachePath "Deploy"
|
|
$ToolsPath = Join-Path (Split-Path $CachePath -Parent) "Tools"
|
|
if (-not (Test-Path $ToolsPath -PathType Container)) {
|
|
$ToolsPath = "C:\ProgramData\GEAerospace\MediaCreator\Tools"
|
|
}
|
|
$SourcesZip = Join-Path $CachePath "Boot\Sources.zip"
|
|
|
|
if (-not (Test-Path $DeployPath -PathType Container)) {
|
|
Write-Host "ERROR: Deploy directory not found at $DeployPath" -ForegroundColor Red
|
|
Write-Host " .\Upload-Image.ps1 -CachePath ""D:\Path\To\Cache""" -ForegroundColor Yellow
|
|
exit 1
|
|
}
|
|
|
|
# --- Parse user_selections.json ---
|
|
$SelectionsFile = Join-Path $ToolsPath "user_selections.json"
|
|
if (-not (Test-Path $SelectionsFile)) {
|
|
Write-Host "ERROR: user_selections.json not found at $SelectionsFile" -ForegroundColor Red
|
|
Write-Host " Run Media Creator Lite first to create a configuration." -ForegroundColor Yellow
|
|
exit 1
|
|
}
|
|
|
|
$selections = (Get-Content $SelectionsFile -Raw | ConvertFrom-Json)[0]
|
|
$selectedOsId = $selections.OperatingSystemSelection
|
|
$selectedModelIds = @($selections.HardwareModelSelection | ForEach-Object { $_.Id } | Select-Object -Unique)
|
|
|
|
# --- Parse control JSONs ---
|
|
$ControlPath = Join-Path $DeployPath "Control"
|
|
|
|
$osJsonFile = Join-Path $ControlPath "OperatingSystem.json"
|
|
$driverJsonFile = Join-Path $ControlPath "HardwareDriver.json"
|
|
$pkgJsonFile = Join-Path $ControlPath "packages.json"
|
|
|
|
if (-not (Test-Path $osJsonFile)) {
|
|
Write-Host "ERROR: OperatingSystem.json not found in $ControlPath" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
$osJson = Get-Content $osJsonFile -Raw | ConvertFrom-Json
|
|
$driverJson = if (Test-Path $driverJsonFile) { Get-Content $driverJsonFile -Raw | ConvertFrom-Json } else { @() }
|
|
$pkgJson = if (Test-Path $pkgJsonFile) { Get-Content $pkgJsonFile -Raw | ConvertFrom-Json } else { @() }
|
|
|
|
# --- Resolve selections to paths ---
|
|
|
|
# OS: match OperatingSystemSelection ID to OperatingSystem.json entries
|
|
$matchedOs = @($osJson | Where-Object { $_.operatingSystemVersion.id -eq [int]$selectedOsId })
|
|
$osDirs = @()
|
|
$osTotalSize = [long]0
|
|
foreach ($os in $matchedOs) {
|
|
$rel = Resolve-DestDir $os.operatingSystemVersion.wim.DestinationDir
|
|
$osDirs += $rel
|
|
$osTotalSize += $os.operatingSystemVersion.wim.size
|
|
}
|
|
|
|
# Packages: enabled + matching OS ID
|
|
$matchedPkgs = @($pkgJson | Where-Object { $_.aOsIds -contains $selectedOsId -and $_.enabled -eq 1 })
|
|
$pkgTotalSize = [long]0
|
|
foreach ($pkg in $matchedPkgs) { $pkgTotalSize += $pkg.size }
|
|
|
|
# Drivers: match selected model IDs (family) + OS ID, deduplicate by path
|
|
$allMatchingDrivers = @($driverJson | Where-Object {
|
|
$selectedModelIds -contains $_.family -and $_.aOsIds -contains $selectedOsId
|
|
})
|
|
$allDriverDirSet = [ordered]@{}
|
|
foreach ($drv in $allMatchingDrivers) {
|
|
$rel = Resolve-DestDir $drv.DestinationDir
|
|
if (-not $allDriverDirSet.Contains($rel)) { $allDriverDirSet[$rel] = $drv.size }
|
|
}
|
|
$allDriverCount = $allDriverDirSet.Count
|
|
$allDriverTotalSize = [long]0
|
|
$allDriverDirSet.Values | ForEach-Object { $allDriverTotalSize += $_ }
|
|
|
|
$driverDirs = @()
|
|
$driverTotalSize = [long]0
|
|
if ($IncludeDrivers) {
|
|
$driverDirs = @($allDriverDirSet.Keys)
|
|
$driverTotalSize = $allDriverTotalSize
|
|
}
|
|
|
|
# --- Display upload plan ---
|
|
Write-Host " Cache: $CachePath"
|
|
Write-Host " Server: $Server"
|
|
Write-Host ""
|
|
Write-Host " Upload Plan (from user_selections.json):" -ForegroundColor Cyan
|
|
Write-Host " ------------------------------------------"
|
|
|
|
if ($matchedOs.Count -gt 0) {
|
|
$osName = $matchedOs[0].operatingSystemVersion.marketingName
|
|
Write-Host " OS: $osName ($(Format-Size $osTotalSize))" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " OS: No match for selection ID $selectedOsId" -ForegroundColor Red
|
|
}
|
|
|
|
Write-Host " Packages: $($matchedPkgs.Count) update(s) ($(Format-Size $pkgTotalSize))" -ForegroundColor Green
|
|
|
|
if ($IncludeDrivers) {
|
|
Write-Host " Drivers: $($driverDirs.Count) model(s) ($(Format-Size $driverTotalSize))" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " Drivers: SKIPPED -- $allDriverCount available, use -IncludeDrivers" -ForegroundColor Yellow
|
|
}
|
|
|
|
Write-Host " Control: Always included" -ForegroundColor Gray
|
|
Write-Host " Tools: $(if (Test-Path $ToolsPath) { 'Yes' } else { 'Not found' })" -ForegroundColor $(if (Test-Path $ToolsPath) { "Gray" } else { "Yellow" })
|
|
Write-Host " Sources: $(if (Test-Path $SourcesZip) { 'Yes (from Boot\Sources.zip)' } else { 'Not found' })" -ForegroundColor $(if (Test-Path $SourcesZip) { "Gray" } else { "Yellow" })
|
|
Write-Host ""
|
|
|
|
# --- Connect to SMB share ---
|
|
Write-Host "Connecting to $Share ..." -ForegroundColor Gray
|
|
net use $Share /delete 2>$null | Out-Null
|
|
|
|
$netResult = net use $Share /user:$User $Pass 2>&1
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Host "ERROR: Could not connect to $Share" -ForegroundColor Red
|
|
Write-Host $netResult -ForegroundColor Red
|
|
Write-Host ""
|
|
Write-Host "Make sure:" -ForegroundColor Yellow
|
|
Write-Host " - The PXE server is running at $Server" -ForegroundColor Yellow
|
|
Write-Host " - This PC is on the 10.9.100.x network" -ForegroundColor Yellow
|
|
Write-Host " - Samba is running on the PXE server" -ForegroundColor Yellow
|
|
exit 1
|
|
}
|
|
Write-Host "Connected." -ForegroundColor Green
|
|
Write-Host ""
|
|
|
|
$failed = $false
|
|
$stepNum = 0
|
|
$totalSteps = 1 # Deploy base always
|
|
if ($matchedOs.Count -gt 0) { $totalSteps++ }
|
|
if ($matchedPkgs.Count -gt 0) { $totalSteps++ }
|
|
if ($IncludeDrivers -and $driverDirs.Count -gt 0) { $totalSteps++ }
|
|
if (Test-Path $ToolsPath -PathType Container) { $totalSteps++ }
|
|
if (Test-Path $SourcesZip) { $totalSteps++ }
|
|
|
|
# --- Step: Deploy base (Control, Applications, config -- skip big dirs) ---
|
|
$stepNum++
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host "[$stepNum/$totalSteps] Copying Deploy\ base (Control, Applications, config) ..." -ForegroundColor Cyan
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
|
|
robocopy $DeployPath "$Share\Deploy" /E /XD "Operating Systems" "Out-of-box Drivers" "Packages" /R:3 /W:5 /NP /ETA
|
|
if ($LASTEXITCODE -ge 8) {
|
|
Write-Host "ERROR: Deploy base copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
|
$failed = $true
|
|
}
|
|
|
|
# --- Step: Operating System ---
|
|
if ($matchedOs.Count -gt 0) {
|
|
$stepNum++
|
|
Write-Host ""
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host "[$stepNum/$totalSteps] Copying Operating System ..." -ForegroundColor Cyan
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
|
|
foreach ($osDir in $osDirs) {
|
|
$src = Join-Path $CachePath $osDir
|
|
$dst = Join-Path $Share $osDir
|
|
if (Test-Path $src -PathType Container) {
|
|
Write-Host " $osDir" -ForegroundColor Gray
|
|
robocopy $src $dst /E /R:3 /W:5 /NP /ETA
|
|
if ($LASTEXITCODE -ge 8) {
|
|
Write-Host "ERROR: OS copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
|
$failed = $true
|
|
}
|
|
} else {
|
|
Write-Host " SKIPPED (not cached): $osDir" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- Step: Packages ---
|
|
if ($matchedPkgs.Count -gt 0) {
|
|
$stepNum++
|
|
Write-Host ""
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host "[$stepNum/$totalSteps] Copying Packages ..." -ForegroundColor Cyan
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
|
|
# Group packages by destination directory for efficient robocopy
|
|
$pkgGroups = [ordered]@{}
|
|
foreach ($pkg in $matchedPkgs) {
|
|
$rel = Resolve-DestDir $pkg.destinationDir
|
|
if (-not $pkgGroups.Contains($rel)) { $pkgGroups[$rel] = @() }
|
|
$pkgGroups[$rel] += $pkg.fileName
|
|
}
|
|
|
|
foreach ($dir in $pkgGroups.Keys) {
|
|
$src = Join-Path $CachePath $dir
|
|
$dst = Join-Path $Share $dir
|
|
$files = $pkgGroups[$dir]
|
|
if (Test-Path $src -PathType Container) {
|
|
foreach ($f in $files) { Write-Host " $f" -ForegroundColor Gray }
|
|
$robocopyArgs = @($src, $dst) + $files + @("/R:3", "/W:5", "/NP", "/ETA")
|
|
& robocopy @robocopyArgs
|
|
if ($LASTEXITCODE -ge 8) {
|
|
Write-Host "ERROR: Package copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
|
$failed = $true
|
|
}
|
|
} else {
|
|
Write-Host " SKIPPED (not cached): $dir" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- Step: Drivers (only with -IncludeDrivers) ---
|
|
if ($IncludeDrivers -and $driverDirs.Count -gt 0) {
|
|
$stepNum++
|
|
Write-Host ""
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host "[$stepNum/$totalSteps] Copying Drivers ($($driverDirs.Count) model(s)) ..." -ForegroundColor Cyan
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
|
|
$drvCopied = 0
|
|
foreach ($drvDir in $driverDirs) {
|
|
$drvCopied++
|
|
$src = Join-Path $CachePath $drvDir
|
|
$dst = Join-Path $Share $drvDir
|
|
if (Test-Path $src -PathType Container) {
|
|
Write-Host " [$drvCopied/$($driverDirs.Count)] $drvDir" -ForegroundColor Gray
|
|
robocopy $src $dst /E /R:3 /W:5 /NP /ETA
|
|
if ($LASTEXITCODE -ge 8) {
|
|
Write-Host "ERROR: Driver copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
|
$failed = $true
|
|
}
|
|
} else {
|
|
Write-Host " SKIPPED (not cached): $drvDir" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- Step: Tools ---
|
|
if (Test-Path $ToolsPath -PathType Container) {
|
|
$stepNum++
|
|
Write-Host ""
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host "[$stepNum/$totalSteps] Copying Tools\ ..." -ForegroundColor Cyan
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
|
|
robocopy $ToolsPath "$Share\Tools" /E /R:3 /W:5 /NP /ETA
|
|
if ($LASTEXITCODE -ge 8) {
|
|
Write-Host "ERROR: Tools copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
|
$failed = $true
|
|
}
|
|
}
|
|
|
|
# --- Step: Sources ---
|
|
$TempSources = $null
|
|
if (Test-Path $SourcesZip) {
|
|
$stepNum++
|
|
Write-Host ""
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host "[$stepNum/$totalSteps] Extracting and copying Sources\ ..." -ForegroundColor Cyan
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
|
|
$TempExtract = Join-Path $env:TEMP "SourcesExtract"
|
|
Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue
|
|
Expand-Archive $SourcesZip -DestinationPath $TempExtract -Force
|
|
|
|
# Handle nested Sources folder (zip may contain Sources/ at root)
|
|
$TempSources = $TempExtract
|
|
if ((Test-Path (Join-Path $TempExtract "Sources")) -and -not (Test-Path (Join-Path $TempExtract "Diskpart"))) {
|
|
$TempSources = Join-Path $TempExtract "Sources"
|
|
}
|
|
|
|
robocopy $TempSources "$Share\Sources" /E /R:3 /W:5 /NP /ETA
|
|
if ($LASTEXITCODE -ge 8) {
|
|
Write-Host "ERROR: Sources copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
|
$failed = $true
|
|
}
|
|
|
|
Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
# --- Verify small files (SMB write-cache workaround) ---
|
|
Write-Host ""
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host "Verifying small files ..." -ForegroundColor Cyan
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
|
|
$fixCount = 0
|
|
$verifyPairs = @(
|
|
@{ Local = (Join-Path $DeployPath "Control"); Remote = "$Share\Deploy\Control" }
|
|
)
|
|
if (Test-Path $ToolsPath -PathType Container) {
|
|
$verifyPairs += @{ Local = $ToolsPath; Remote = "$Share\Tools" }
|
|
}
|
|
|
|
foreach ($pair in $verifyPairs) {
|
|
$localDir = $pair.Local
|
|
$remoteDir = $pair.Remote
|
|
if (-not (Test-Path $localDir -PathType Container)) { continue }
|
|
|
|
Get-ChildItem -Path $localDir -Recurse -File -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Length -gt 0 -and $_.Length -lt 1MB } |
|
|
ForEach-Object {
|
|
$rel = $_.FullName.Substring($localDir.Length)
|
|
$dstFile = Join-Path $remoteDir $rel
|
|
if (Test-Path $dstFile) {
|
|
$dstSize = (Get-Item $dstFile).Length
|
|
if ($dstSize -ne $_.Length) {
|
|
Write-Host " Fixing: $rel ($dstSize -> $($_.Length) bytes)" -ForegroundColor Yellow
|
|
$bytes = [System.IO.File]::ReadAllBytes($_.FullName)
|
|
[System.IO.File]::WriteAllBytes($dstFile, $bytes)
|
|
$fixCount++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($fixCount -eq 0) {
|
|
Write-Host " All files verified OK." -ForegroundColor Green
|
|
} else {
|
|
Write-Host " Fixed $fixCount file(s)." -ForegroundColor Yellow
|
|
}
|
|
|
|
# --- Disconnect ---
|
|
net use $Share /delete 2>$null | Out-Null
|
|
|
|
# --- Summary ---
|
|
Write-Host ""
|
|
if ($failed) {
|
|
Write-Host "========================================" -ForegroundColor Red
|
|
Write-Host " Upload completed with errors." -ForegroundColor Red
|
|
Write-Host "========================================" -ForegroundColor Red
|
|
} else {
|
|
Write-Host "========================================" -ForegroundColor Green
|
|
Write-Host " Upload complete!" -ForegroundColor Green
|
|
Write-Host "========================================" -ForegroundColor Green
|
|
}
|
|
Write-Host ""
|
|
Write-Host "Next steps:" -ForegroundColor Cyan
|
|
Write-Host " 1. Open the PXE webapp: http://$Server`:9009" -ForegroundColor White
|
|
Write-Host " 2. Go to Image Import" -ForegroundColor White
|
|
Write-Host " 3. Select source 'image-upload' and target image type" -ForegroundColor White
|
|
Write-Host " 4. Click Import" -ForegroundColor White
|
|
Write-Host ""
|