diff --git a/playbook/preinstall/preinstall.json b/playbook/preinstall/preinstall.json new file mode 100644 index 0000000..7b894c3 --- /dev/null +++ b/playbook/preinstall/preinstall.json @@ -0,0 +1,72 @@ +{ + "Version": "1.0", + "Site": "West Jefferson", + "Applications": [ + { + "Name": "Oracle Client 10.2.0.3", + "Installer": "Oracle 10.2.0.3.msi", + "Type": "MSI", + "InstallArgs": "/qn /norestart", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\ORACLE\\KEY_OraClientInfra10_2_0", + "DetectionName": "ORACLE_HOME_NAME", + "DetectionValue": "OraClientInfra10_2_0", + "PCTypes": ["*"] + }, + { + "Name": "VC++ Redistributable 2010 x86", + "Installer": "vcredist2010_x86.exe", + "Type": "EXE", + "InstallArgs": "/quiet /norestart", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{F0C3E5D1-1ADE-321E-8167-68EF0DE699A5}", + "PCTypes": ["*"] + }, + { + "Name": "VC++ Redistributable 2012 x86", + "Installer": "vcredist2012_x86.exe", + "Type": "EXE", + "InstallArgs": "/quiet /norestart", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BD95A8CD-1D9F-35AD-981A-3E7925026EBB}", + "PCTypes": ["*"] + }, + { + "Name": "VC++ Redistributable 2013 x86", + "Installer": "vcredist2013_x86.exe", + "Type": "EXE", + "InstallArgs": "/quiet /norestart", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{13A4EE12-23EA-3371-91EE-EFB36DDFFF3E}", + "PCTypes": ["*"] + }, + { + "Name": "VC++ Redistributable 2015-2022 x86", + "Installer": "vcredist2015_2017_2019_2022_x86.exe", + "Type": "EXE", + "InstallArgs": "/install /quiet /norestart", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{922480B5-CAEB-4B1B-AAA4-9716EFDCE26B}", + "PCTypes": ["*"] + }, + { + "Name": "UDC", + "Installer": "UDC_Setup.exe", + "Type": "EXE", + "InstallArgs": "\"West Jefferson\" 9999", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\UDC", + "PCTypes": ["Standard"] + }, + { + "_comment": "VC++ 2008 — installed via the extracted vc_red.msi (NOT the bootstrapper) because vcredist2008_x86.exe ignores /norestart (per Aaron Stebner's Microsoft docs). REBOOT=ReallySuppress is a Windows Installer property that hard-blocks reboots even when files are in use; msiexec may return 3010 but won't actually reboot. vc_red.cab must live in the same directory as vc_red.msi or msiexec can't find the data files.", + "Name": "VC++ Redistributable 2008 x86", + "Installer": "vc_red.msi", + "Type": "MSI", + "InstallArgs": "/qn /norestart REBOOT=ReallySuppress", + "DetectionMethod": "Registry", + "DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{9BE518E6-ECC6-35A9-88E4-87755C07200F}", + "PCTypes": ["*"] + } + ] +} diff --git a/playbook/pxe_server_setup.yml b/playbook/pxe_server_setup.yml index 846624a..17dd309 100644 --- a/playbook/pxe_server_setup.yml +++ b/playbook/pxe_server_setup.yml @@ -313,6 +313,22 @@ directory_mode: '0755' ignore_errors: yes + - name: "Create preinstall bundle directory on enrollment share" + file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - /srv/samba/enrollment/preinstall + - /srv/samba/enrollment/preinstall/installers + + - name: "Deploy preinstall.json (installer binaries staged separately)" + copy: + src: "{{ usb_mount }}/preinstall/preinstall.json" + dest: /srv/samba/enrollment/preinstall/preinstall.json + mode: '0644' + ignore_errors: yes + - name: "Create BIOS update directory on enrollment share" file: path: /srv/samba/enrollment/BIOS diff --git a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 index 1123c1e..f490d39 100644 --- a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 +++ b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 @@ -1,6 +1,12 @@ # Run-ShopfloorSetup.ps1 — Dispatcher for shopfloor PC type setup # Runs Shopfloor baseline scripts first, then type-specific scripts on top. +# Bump AutoLogonCount HIGH at the start so reboots during setup (e.g. VC++ 2008 +# triggering an immediate ExitWindowsEx) don't exhaust autologin attempts before +# the dispatcher can complete. The end-of-script reset puts it back to 2 once +# everything succeeds. +reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 99 /f | Out-Null + # Cancel any pending reboot so it doesn't interrupt setup shutdown -a 2>$null @@ -77,6 +83,17 @@ if (Test-Path $syncScript) { Write-Host "sync_intune.bat copied to desktop." } +# Standard PCs get the UDC/eDNC machine number helper +if ($pcType -eq "Standard") { + foreach ($helper in @("Set-MachineNumber.bat", "Set-MachineNumber.ps1")) { + $src = Join-Path $setupDir "Standard\$helper" + if (Test-Path $src) { + Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$helper" -Force + Write-Host "$helper copied to SupportUser desktop." + } + } +} + # Set auto-logon to expire after 2 more logins reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 2 /f | Out-Null Write-Host "Auto-logon set to 2 remaining logins." diff --git a/playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 b/playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 new file mode 100644 index 0000000..c0dd51e --- /dev/null +++ b/playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 @@ -0,0 +1,284 @@ +# 00-PreInstall-MachineApps.ps1 — Install pre-staged apps from C:\PreInstall (Shopfloor baseline) +# +# Reads C:\PreInstall\preinstall.json (staged by startnet.cmd during WinPE phase) and +# installs each app whose PCTypes filter matches the current PC type. Detection runs first +# so already-installed apps are skipped. Numbered 00- so it runs before all other Shopfloor +# baseline scripts — apps need to exist before later scripts (e.g. 01-eDNC.ps1) reference +# them or rely on their state. +# +# Failure mode: log + continue. Intune DSC (Simple-Install.ps1) is the safety net for any +# install that fails here. +# +# Logs to C:\Logs\PreInstall\install.log (wiped each run). + +$preInstallDir = "C:\PreInstall" +$jsonPath = Join-Path $preInstallDir "preinstall.json" +$installerDir = Join-Path $preInstallDir "installers" +$logDir = "C:\Logs\PreInstall" +$logFile = Join-Path $logDir "install.log" + +# --- Setup logging --- +# IMPORTANT: do NOT wipe the log on each run. The runner can get killed by an +# installer-triggered reboot mid-execution. On the next autologon, the dispatcher +# re-runs us — if we wipe the log here, we destroy the forensic record from the +# previous (interrupted) run. Instead, append a session header so runs are visible +# but history is preserved. +if (-not (Test-Path $logDir)) { + New-Item -Path $logDir -ItemType Directory -Force | Out-Null +} + +function Write-PreInstallLog { + param( + [Parameter(Mandatory=$true, Position=0)] + [string]$Message, + [ValidateSet('INFO','WARN','ERROR')] + [string]$Level = 'INFO' + ) + $stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $line = "[$stamp] [$Level] $Message" + Write-Host $line + + # Synchronous write-through so each line hits disk immediately, even if a system + # reboot kills the process right after this call returns. Bypasses the OS write + # cache via FileOptions.WriteThrough. + try { + $fs = New-Object System.IO.FileStream( + $logFile, + [System.IO.FileMode]::Append, + [System.IO.FileAccess]::Write, + [System.IO.FileShare]::Read, + 4096, + [System.IO.FileOptions]::WriteThrough + ) + $bytes = [System.Text.Encoding]::UTF8.GetBytes($line + "`r`n") + $fs.Write($bytes, 0, $bytes.Length) + $fs.Flush() + $fs.Dispose() + } + catch { + # Last-resort fallback if the FileStream open fails for any reason + Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue + } +} + +# Session header so multiple runs are visually distinguishable in the log +Write-PreInstallLog "================================================================" +Write-PreInstallLog "=== Runner session start (PID $PID) ===" +Write-PreInstallLog "================================================================" + +# --- Bail early if no preinstall bundle staged --- +if (-not (Test-Path $jsonPath)) { + Write-PreInstallLog "No preinstall.json at $jsonPath - skipping (no bundle staged for this PC)." + exit 0 +} + +# --- Read PCTYPE from C:\Enrollment\pc-type.txt (set by startnet.cmd) --- +$pcTypeFile = "C:\Enrollment\pc-type.txt" +if (-not (Test-Path $pcTypeFile)) { + Write-PreInstallLog "No pc-type.txt at $pcTypeFile - skipping" "WARN" + exit 0 +} +$pcType = (Get-Content $pcTypeFile -First 1).Trim() +if (-not $pcType) { + Write-PreInstallLog "pc-type.txt is empty - skipping" "WARN" + exit 0 +} +Write-PreInstallLog "PC type: $pcType" + +# --- Parse JSON --- +try { + $config = Get-Content $jsonPath -Raw | ConvertFrom-Json +} catch { + Write-PreInstallLog "Failed to parse $jsonPath : $_" "ERROR" + exit 0 +} + +if (-not $config.Applications) { + Write-PreInstallLog "No Applications in preinstall.json" + exit 0 +} + +Write-PreInstallLog "Staged installer dir: $installerDir" +Write-PreInstallLog "Found $($config.Applications.Count) app entries in preinstall.json" + +# --- Detection helper (mirrors Simple-Install.ps1's Test-ApplicationInstalled) --- +function Test-AppInstalled { + param($App) + + if (-not $App.DetectionMethod) { + return $false + } + + try { + switch ($App.DetectionMethod) { + "Registry" { + if (-not (Test-Path $App.DetectionPath)) { + return $false + } + if ($App.DetectionName) { + $value = Get-ItemProperty -Path $App.DetectionPath -Name $App.DetectionName -ErrorAction SilentlyContinue + if (-not $value) { + return $false + } + if ($App.DetectionValue) { + return ($value.$($App.DetectionName) -eq $App.DetectionValue) + } + return $true + } + return $true + } + "File" { + return Test-Path $App.DetectionPath + } + default { + Write-PreInstallLog " Unknown detection method: $($App.DetectionMethod)" "WARN" + return $false + } + } + } catch { + Write-PreInstallLog " Detection check threw: $_" "WARN" + return $false + } +} + +# --- Iterate apps --- +$installed = 0 +$skipped = 0 +$failed = 0 + +foreach ($app in $config.Applications) { + # Cancel any reboot a previous installer scheduled (some respect /norestart, some + # don't — VC++ 2008's bootstrapper sometimes triggers an immediate Windows reboot + # despite the flag). Doing this BEFORE each install protects the rest of the loop. + & shutdown.exe /a 2>$null + + Write-PreInstallLog "==> $($app.Name)" + + # Filter by PCTypes + $allowedTypes = @($app.PCTypes) + $matchesType = ($allowedTypes -contains "*") -or ($allowedTypes -contains $pcType) + if (-not $matchesType) { + Write-PreInstallLog " PCTypes filter excludes '$pcType' (allowed: $($allowedTypes -join ', ')) - skipping" + $skipped++ + continue + } + + # Detection check + if (Test-AppInstalled -App $app) { + Write-PreInstallLog " Already installed - skipping" + $skipped++ + continue + } + + # Locate installer file + $installerPath = Join-Path $installerDir $app.Installer + if (-not (Test-Path $installerPath)) { + Write-PreInstallLog " Installer file not found: $installerPath" "ERROR" + $failed++ + continue + } + + Write-PreInstallLog " Installing from $installerPath" + if ($app.InstallArgs) { + Write-PreInstallLog " InstallArgs: $($app.InstallArgs)" + } + + try { + # Start without -Wait so we can poll. UDC_Setup.exe in particular hangs forever + # after install (it spawns UDC.exe and hides its main window without exiting), + # so we can't rely on the process exiting on its own. + if ($app.Type -eq "MSI") { + $msiArgs = "/i `"$installerPath`"" + if ($app.InstallArgs) { + $msiArgs += " " + $app.InstallArgs + } + $proc = Start-Process -FilePath "msiexec.exe" -ArgumentList $msiArgs -PassThru -NoNewWindow + } + elseif ($app.Type -eq "EXE") { + if ($app.InstallArgs) { + $proc = Start-Process -FilePath $installerPath -ArgumentList $app.InstallArgs -PassThru -NoNewWindow + } else { + $proc = Start-Process -FilePath $installerPath -PassThru -NoNewWindow + } + } + else { + Write-PreInstallLog " Unsupported Type: $($app.Type) - skipping" "ERROR" + $failed++ + continue + } + + # Poll for completion: process exit OR detection success (whichever happens first) + $timeoutSec = 600 # 10 min hard cap per app + $pollInterval = 5 + $elapsed = 0 + $killedAfterDetect = $false + + while ($elapsed -lt $timeoutSec) { + if ($proc.HasExited) { break } + + # If detection passes mid-install, the installer already did its job — + # we can kill any zombie process (like UDC_Setup.exe waiting on its hidden + # WPF window) and move on. + if (Test-AppInstalled -App $app) { + Write-PreInstallLog " Detection passed at $elapsed s - killing installer to advance" + try { $proc.Kill(); $proc.WaitForExit(5000) | Out-Null } catch { } + + # UDC's installer auto-launches UDC.exe in silent mode. Kill that too + # so it can't write the placeholder MachineNumber to udc_settings.json. + if ($app.Name -eq "UDC") { + Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object { + try { $_.Kill(); $_.WaitForExit(2000) | Out-Null } catch { } + } + } + + $killedAfterDetect = $true + break + } + + Start-Sleep -Seconds $pollInterval + $elapsed += $pollInterval + } + + if (-not $proc.HasExited -and -not $killedAfterDetect) { + Write-PreInstallLog " TIMEOUT after $timeoutSec seconds - killing installer" "ERROR" + try { $proc.Kill() } catch { } + if ($app.Name -eq "UDC") { + Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object { + try { $_.Kill() } catch { } + } + } + $failed++ + continue + } + + if ($killedAfterDetect) { + Write-PreInstallLog " SUCCESS (verified via detection during install)" + $installed++ + } + elseif ($proc.ExitCode -eq 0 -or $proc.ExitCode -eq 3010) { + Write-PreInstallLog " Exit code $($proc.ExitCode) after $elapsed s - SUCCESS" + if ($proc.ExitCode -eq 3010) { + Write-PreInstallLog " (Reboot required for $($app.Name))" + } + $installed++ + } + else { + Write-PreInstallLog " Exit code $($proc.ExitCode) - FAILED" "ERROR" + $failed++ + } + } catch { + Write-PreInstallLog " Install threw: $_" "ERROR" + $failed++ + } +} + +Write-PreInstallLog "============================================" +Write-PreInstallLog "PreInstall complete: $installed installed, $skipped skipped, $failed failed" +Write-PreInstallLog "============================================" + +# Final reboot cancel — if the last installer in the loop scheduled one, the +# dispatcher's later `shutdown /a` won't fire until the next baseline script starts. +# Cancel here so control returns cleanly to Run-ShopfloorSetup.ps1. +& shutdown.exe /a 2>$null + +exit 0 diff --git a/playbook/shopfloor-setup/Standard/Set-MachineNumber.bat b/playbook/shopfloor-setup/Standard/Set-MachineNumber.bat new file mode 100644 index 0000000..842ce01 --- /dev/null +++ b/playbook/shopfloor-setup/Standard/Set-MachineNumber.bat @@ -0,0 +1,6 @@ +@echo off +REM Set-MachineNumber.bat — Wrapper for Set-MachineNumber.ps1 +REM Runs the PowerShell helper with bypass execution policy so a double-click +REM from the desktop just works. + +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Set-MachineNumber.ps1" diff --git a/playbook/shopfloor-setup/Standard/Set-MachineNumber.ps1 b/playbook/shopfloor-setup/Standard/Set-MachineNumber.ps1 new file mode 100644 index 0000000..d9d0cdb --- /dev/null +++ b/playbook/shopfloor-setup/Standard/Set-MachineNumber.ps1 @@ -0,0 +1,141 @@ +# Set-MachineNumber.ps1 — Update UDC + eDNC machine number on a Standard shopfloor PC +# +# Purpose: +# Both UDC and eDNC use the same per-machine identifier ("Workstation Number" / +# "Machine Number"). On Standard PCs imaged via PXE preinstall, both are installed +# with a placeholder. When the PC is brought to its physical machine and assigned +# a real number, this helper updates both apps in one step. +# +# Persistence locations updated: +# 1. UDC: C:\ProgramData\UDC\udc_settings.json (GeneralSettings.MachineNumber) +# 2. eDNC: HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General\MachineNo +# +# After updating, kills any running UDC.exe and relaunches it with the new args +# so the in-memory state matches the persisted value. +# +# Run as SupportUser (admin). Requires write access to ProgramData and HKLM. + +Add-Type -AssemblyName Microsoft.VisualBasic +Add-Type -AssemblyName System.Windows.Forms + +$udcSettingsPath = "C:\ProgramData\UDC\udc_settings.json" +$udcExePath = "C:\Program Files\UDC\UDC.exe" +$ednRegPath = "HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General" +$site = "West Jefferson" + +# --- Read current values for display --- +$currentUdc = $null +$currentEdnc = $null + +if (Test-Path $udcSettingsPath) { + try { + $json = Get-Content $udcSettingsPath -Raw | ConvertFrom-Json + $currentUdc = $json.GeneralSettings.MachineNumber + } catch { + $currentUdc = "(unreadable: $_)" + } +} + +if (Test-Path $ednRegPath) { + try { + $currentEdnc = (Get-ItemProperty -Path $ednRegPath -Name MachineNo -ErrorAction Stop).MachineNo + } catch { + $currentEdnc = $null + } +} + +# --- Show prompt with current state --- +$promptLines = @() +$promptLines += "Current UDC machine number: $(if ($currentUdc) { $currentUdc } else { '(not set)' })" +$promptLines += "Current eDNC machine number: $(if ($currentEdnc) { $currentEdnc } else { '(not set)' })" +$promptLines += "" +$promptLines += "Enter the new Machine Number for this PC:" +$prompt = $promptLines -join "`n" + +$new = [Microsoft.VisualBasic.Interaction]::InputBox($prompt, "Set Machine Number", "") + +if ([string]::IsNullOrWhiteSpace($new)) { + Write-Host "Cancelled." + exit 0 +} + +$new = $new.Trim() + +# --- Validate: digits only (loosen if you need alphanumerics) --- +if ($new -notmatch '^\d+$') { + [System.Windows.Forms.MessageBox]::Show( + "Machine number must be digits only.`n`nYou entered: '$new'", + "Invalid Machine Number", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Error + ) | Out-Null + exit 1 +} + +$results = @() + +# --- 1. Stop UDC.exe before editing its JSON (avoid stale shutdown write) --- +$udcProcs = Get-Process UDC -ErrorAction SilentlyContinue +if ($udcProcs) { + Write-Host "Stopping UDC.exe ($($udcProcs.Count) instance(s))..." + $udcProcs | ForEach-Object { + try { + $_.Kill() + $_.WaitForExit(5000) | Out-Null + } catch { + Write-Warning "Failed to stop UDC.exe (PID $($_.Id)): $_" + } + } + Start-Sleep -Seconds 1 +} + +# --- 2. Update UDC settings JSON --- +if (Test-Path $udcSettingsPath) { + try { + $json = Get-Content $udcSettingsPath -Raw | ConvertFrom-Json + $json.GeneralSettings.MachineNumber = $new + $json | ConvertTo-Json -Depth 99 | Set-Content -Path $udcSettingsPath -Encoding UTF8 + Write-Host "UDC: $currentUdc -> $new" + $results += "UDC updated to $new" + } catch { + Write-Warning "Failed to update UDC settings: $_" + $results += "UDC FAILED: $_" + } +} else { + Write-Warning "UDC settings file not found at $udcSettingsPath. Run UDC.exe at least once to initialize." + $results += "UDC: settings file missing (run UDC.exe once first)" +} + +# --- 3. Update eDNC MachineNo registry value --- +if (Test-Path $ednRegPath) { + try { + Set-ItemProperty -Path $ednRegPath -Name MachineNo -Value $new -Type String -Force + Write-Host "eDNC: $currentEdnc -> $new" + $results += "eDNC updated to $new" + } catch { + Write-Warning "Failed to update eDNC MachineNo: $_" + $results += "eDNC FAILED: $_" + } +} else { + Write-Warning "eDNC registry key not found at $ednRegPath. Is eDNC installed?" + $results += "eDNC: registry key missing (eDNC not installed?)" +} + +# --- 4. Relaunch UDC.exe with new args (if installed) --- +if (Test-Path $udcExePath) { + try { + Start-Process -FilePath $udcExePath -ArgumentList @("-site", "`"$site`"", "-machine", $new) + Write-Host "UDC.exe relaunched." + } catch { + Write-Warning "Failed to relaunch UDC.exe: $_" + } +} + +# --- 5. Show summary --- +$summary = ($results -join "`n") + "`n`nTo apply eDNC changes, restart any running DncMain.exe." +[System.Windows.Forms.MessageBox]::Show( + $summary, + "Set Machine Number — Done", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Information +) | Out-Null diff --git a/playbook/startnet.cmd b/playbook/startnet.cmd index 3439fc2..6a32409 100644 --- a/playbook/startnet.cmd +++ b/playbook/startnet.cmd @@ -259,6 +259,21 @@ if exist "Y:\shopfloor-setup\%PCTYPE%" ( ) else ( echo WARNING: No setup files found for PC type %PCTYPE%. ) + +REM --- Stage preinstall bundle (apps installed locally to save Azure bandwidth) --- +if exist "Y:\preinstall\preinstall.json" ( + mkdir W:\PreInstall 2>NUL + mkdir W:\PreInstall\installers 2>NUL + copy /Y "Y:\preinstall\preinstall.json" "W:\PreInstall\preinstall.json" + if exist "Y:\preinstall\installers" ( + xcopy /E /Y /I "Y:\preinstall\installers" "W:\PreInstall\installers\" + echo Staged preinstall bundle to W:\PreInstall. + ) else ( + echo WARNING: Y:\preinstall\installers not found - preinstall.json staged without installers. + ) +) else ( + echo No preinstall bundle on PXE server - skipping. +) :pctype_done :cleanup_enroll diff --git a/playbook/sync-preinstall.sh b/playbook/sync-preinstall.sh new file mode 100755 index 0000000..880a050 --- /dev/null +++ b/playbook/sync-preinstall.sh @@ -0,0 +1,151 @@ +#!/bin/bash +# sync-preinstall.sh — Push preinstall.json + installer binaries to the live PXE server. +# +# Run this on the workstation (not on the PXE server) any time: +# - You update preinstall.json in playbook/preinstall/ +# - You update installer binaries in /home/camp/pxe-images/main/ +# +# The PXE server's /srv/samba/enrollment/preinstall/ tree is updated in place. The +# next imaged PC picks up the new files via startnet.cmd's xcopy during WinPE phase. +# +# Usage: +# ./playbook/sync-preinstall.sh +# +# Requires: sshpass (apt install sshpass), scp, ssh + +set -euo pipefail + +# --- Config --- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +PXE_HOST="${PXE_HOST:-10.9.100.1}" +PXE_USER="${PXE_USER:-pxe}" +PXE_PASS="${PXE_PASS:-pxe}" + +PREINSTALL_JSON="$PROJECT_ROOT/playbook/preinstall/preinstall.json" +PXE_IMAGES_DIR="${PXE_IMAGES_DIR:-/home/camp/pxe-images/main}" + +REMOTE_DIR="/srv/samba/enrollment/preinstall" +REMOTE_INSTALLERS="$REMOTE_DIR/installers" +REMOTE_TEMP="/tmp/preinstall-stage" + +# Files to push (source paths under PXE_IMAGES_DIR). +# vc_red.msi + vc_red.cab are extracted from vcredist2008_x86.exe and used directly +# (instead of the bootstrapper) because the 2008 bootstrapper ignores /norestart and +# triggers an immediate reboot. The MSI honors REBOOT=ReallySuppress, the bootstrapper +# does not. The .cab MUST be named "vc_red.cab" exactly because that name is hardcoded +# in the MSI's Media table. +INSTALLERS=( + "dependencies/vc_red.msi" + "dependencies/vc_red.cab" + "dependencies/vcredist2010_x86.exe" + "dependencies/vcredist2012_x86.exe" + "dependencies/vcredist2013_x86.exe" + "dependencies/vcredist2015_2017_2019_2022_x86.exe" + "machineapps/UDC_Setup.exe" + "globalassets/oracleclient/Oracle 10.2.0.3.msi" +) + +# --- Helpers --- +ssh_run() { + sshpass -p "$PXE_PASS" ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR "$PXE_USER@$PXE_HOST" "$@" +} + +scp_to() { + # Remote path is double-escaped: outer ssh layer + inner shell layer. + # Wrap in single-quotes inside the destination so spaces in filenames survive. + sshpass -p "$PXE_PASS" scp -o StrictHostKeyChecking=no -o LogLevel=ERROR "$1" "$PXE_USER@$PXE_HOST:'$2'" +} + +# --- Validate sources --- +echo "Validating source files..." + +if [ ! -f "$PREINSTALL_JSON" ]; then + echo "ERROR: preinstall.json not found at $PREINSTALL_JSON" >&2 + exit 1 +fi + +missing=0 +for rel in "${INSTALLERS[@]}"; do + src="$PXE_IMAGES_DIR/$rel" + if [ ! -f "$src" ]; then + echo " MISSING: $src" >&2 + missing=$((missing + 1)) + else + printf " OK %10d %s\n" "$(stat -c %s "$src")" "$rel" + fi +done + +if [ "$missing" -gt 0 ]; then + echo "ERROR: $missing installer file(s) missing in $PXE_IMAGES_DIR" >&2 + exit 1 +fi + +# --- Verify PXE server reachable --- +echo "Pinging PXE server $PXE_HOST..." +if ! ping -c 1 -W 2 "$PXE_HOST" >/dev/null 2>&1; then + echo "ERROR: PXE server $PXE_HOST not reachable" >&2 + exit 1 +fi + +# --- Stage to /tmp on PXE, then sudo install --- +echo "Staging files to $PXE_HOST:$REMOTE_TEMP..." +ssh_run "mkdir -p $REMOTE_TEMP && rm -f $REMOTE_TEMP/*" + +# preinstall.json +echo " -> preinstall.json" +scp_to "$PREINSTALL_JSON" "$REMOTE_TEMP/preinstall.json" + +# installers (preserve filenames including spaces) +for rel in "${INSTALLERS[@]}"; do + src="$PXE_IMAGES_DIR/$rel" + base="$(basename "$rel")" + echo " -> $base" + scp_to "$src" "$REMOTE_TEMP/$base" +done + +# --- Build remote install script (runs under sudo on PXE) --- +LOCAL_TEMP_SCRIPT="$(mktemp /tmp/sync-preinstall-remote.XXXXXX.sh)" +trap 'rm -f "$LOCAL_TEMP_SCRIPT"' EXIT + +cat > "$LOCAL_TEMP_SCRIPT" < /srv/samba/enrollment/preinstall/preinstall.json +cp "$REMOTE_TEMP/preinstall.json" "$REMOTE_DIR/preinstall.json" +chmod 0644 "$REMOTE_DIR/preinstall.json" +chown root:root "$REMOTE_DIR/preinstall.json" + +# All other files -> installers/ +shopt -s dotglob nullglob +for f in "$REMOTE_TEMP"/*; do + base="\$(basename "\$f")" + if [ "\$base" != "preinstall.json" ] && [ "\$base" != "_install.sh" ]; then + cp "\$f" "$REMOTE_INSTALLERS/\$base" + chmod 0644 "$REMOTE_INSTALLERS/\$base" + chown root:root "$REMOTE_INSTALLERS/\$base" + fi +done + +rm -rf "$REMOTE_TEMP" + +echo +echo "Final state of $REMOTE_DIR:" +ls -la "$REMOTE_DIR" +echo +echo "Final state of $REMOTE_INSTALLERS:" +ls -la "$REMOTE_INSTALLERS" +REMOTE_SCRIPT + +# Stage the install script alongside the data files +scp_to "$LOCAL_TEMP_SCRIPT" "$REMOTE_TEMP/_install.sh" + +# Execute remotely with sudo +echo "Installing files to $REMOTE_DIR (sudo)..." +ssh_run "echo '$PXE_PASS' | sudo -S bash $REMOTE_TEMP/_install.sh" + +echo +echo "Done. Preinstall bundle synced to $PXE_HOST:$REMOTE_DIR" diff --git a/startnet-template.cmd b/startnet-template.cmd index 0116090..6a32409 100644 --- a/startnet-template.cmd +++ b/startnet-template.cmd @@ -86,7 +86,7 @@ echo 2. Wax and Trace echo 3. Keyence echo 4. Genspect echo 5. Display -echo 6. Shopfloor (General) +echo 6. Standard echo. set PCTYPE= set /p pctype_choice=Enter your choice (1-6): @@ -95,7 +95,7 @@ if "%pctype_choice%"=="2" set PCTYPE=WaxAndTrace if "%pctype_choice%"=="3" set PCTYPE=Keyence if "%pctype_choice%"=="4" set PCTYPE=Genspect if "%pctype_choice%"=="5" set PCTYPE=Display -if "%pctype_choice%"=="6" set PCTYPE=Shopfloor +if "%pctype_choice%"=="6" set PCTYPE=Standard if "%PCTYPE%"=="" goto pctype_menu REM --- Display sub-type selection --- @@ -245,13 +245,13 @@ if not "%DISPLAYTYPE%"=="" echo %DISPLAYTYPE%> W:\Enrollment\display-type.txt copy /Y "Y:\shopfloor-setup\Run-ShopfloorSetup.ps1" "W:\Enrollment\Run-ShopfloorSetup.ps1" REM --- Always copy Shopfloor baseline scripts --- mkdir W:\Enrollment\shopfloor-setup 2>NUL +copy /Y "Y:\shopfloor-setup\backup_lockdown.bat" "W:\Enrollment\shopfloor-setup\backup_lockdown.bat" if exist "Y:\shopfloor-setup\Shopfloor" ( mkdir W:\Enrollment\shopfloor-setup\Shopfloor 2>NUL xcopy /E /Y /I "Y:\shopfloor-setup\Shopfloor" "W:\Enrollment\shopfloor-setup\Shopfloor\" echo Copied Shopfloor baseline setup files. ) REM --- Copy type-specific scripts on top of baseline --- -if "%PCTYPE%"=="Shopfloor" goto pctype_done if exist "Y:\shopfloor-setup\%PCTYPE%" ( mkdir "W:\Enrollment\shopfloor-setup\%PCTYPE%" 2>NUL xcopy /E /Y /I "Y:\shopfloor-setup\%PCTYPE%" "W:\Enrollment\shopfloor-setup\%PCTYPE%\" @@ -259,6 +259,21 @@ if exist "Y:\shopfloor-setup\%PCTYPE%" ( ) else ( echo WARNING: No setup files found for PC type %PCTYPE%. ) + +REM --- Stage preinstall bundle (apps installed locally to save Azure bandwidth) --- +if exist "Y:\preinstall\preinstall.json" ( + mkdir W:\PreInstall 2>NUL + mkdir W:\PreInstall\installers 2>NUL + copy /Y "Y:\preinstall\preinstall.json" "W:\PreInstall\preinstall.json" + if exist "Y:\preinstall\installers" ( + xcopy /E /Y /I "Y:\preinstall\installers" "W:\PreInstall\installers\" + echo Staged preinstall bundle to W:\PreInstall. + ) else ( + echo WARNING: Y:\preinstall\installers not found - preinstall.json staged without installers. + ) +) else ( + echo No preinstall bundle on PXE server - skipping. +) :pctype_done :cleanup_enroll