diff --git a/playbook/shopfloor-setup/Standard/Backup-UDCData.bat b/playbook/shopfloor-setup/Standard/Backup-UDCData.bat new file mode 100644 index 0000000..673f461 --- /dev/null +++ b/playbook/shopfloor-setup/Standard/Backup-UDCData.bat @@ -0,0 +1,47 @@ +@echo off +REM Backup-UDCData.bat - Tech-runnable wrapper for Backup-UDCData.ps1. +REM +REM Self-elevates via UAC so the script can read C:\ProgramData\UDC\* and +REM write to the SFLD share with the right cached creds. Forwards any +REM extra args verbatim (e.g. -KeepPriorBackup, -MachineNumber 7605). +REM +REM Usage on the OLD PC, before retirement: +REM 1. Double-click Backup-UDCData.bat (or right-click -> Run as admin) +REM 2. Approve the UAC prompt +REM 3. Watch the elevated PS window for the success summary +REM 4. Confirm \\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\backup\udc\\ +REM now contains CurrentData.json + ArchiveData\ + backup.manifest.json + +REM --- Self-elevate --------------------------------------------------- +net session >nul 2>&1 +if %errorLevel% neq 0 ( + echo Requesting admin rights... + powershell -Command "Start-Process '%~f0' -Verb RunAs -ArgumentList '%*'" + exit /b +) + +set "SCRIPT=%~dp0Backup-UDCData.ps1" + +if not exist "%SCRIPT%" ( + echo ERROR: %SCRIPT% not found. + echo This .bat must be in the same folder as Backup-UDCData.ps1. + pause + exit /b 1 +) + +echo ============================================================ +echo UDC Data Backup +echo ============================================================ +echo Script: %SCRIPT% +echo. + +PowerShell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT%" %* +set RC=%errorLevel% + +echo. +echo ============================================================ +echo Backup script exit code: %RC% +echo ============================================================ +echo. +pause +exit /b %RC% diff --git a/playbook/shopfloor-setup/Standard/Backup-UDCData.ps1 b/playbook/shopfloor-setup/Standard/Backup-UDCData.ps1 new file mode 100644 index 0000000..bec50cb --- /dev/null +++ b/playbook/shopfloor-setup/Standard/Backup-UDCData.ps1 @@ -0,0 +1,212 @@ +# Backup-UDCData.ps1 - Capture UDC's CurrentData.json + ArchiveData/ to the +# SFLD share, keyed by the PC's current machine number. Runs LOCALLY on the +# old PC before retirement; the new PC restores during its first +# placeholder-to-real machine-number assignment via Update-MachineNumber.ps1. +# +# DESIGN: backup lives at \\ root. Presence of CurrentData.json +# at that root means "available to restore". Restore consumes by moving content +# into \migrated\\, leaving the root empty so it can't +# be replayed. +# +# USAGE: +# - Direct: powershell -NoProfile -ExecutionPolicy Bypass -File Backup-UDCData.ps1 +# - Wrapper: Backup-UDCData.bat (handles the bypass + elevation prompts) +# - Remote (WinRM): ../powershell/remote-execution/Backup-UDCData-Remote.ps1 + +[CmdletBinding()] +param( + # Override auto-detection for unusual cases. Defaults to whatever UDC + # currently has in C:\ProgramData\UDC\udc_settings.json. + [string]$MachineNumber, + + # Override the share path. Defaults to the canonical SFLD location. + [string]$BackupShareRoot = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\backup\udc', + + # Local UDC data root. Default matches the vendor install path. + [string]$UdcDataDir = 'C:\ProgramData\UDC', + + # Cred for the SFLD share. If omitted, the current session's identity + # is used (works for SYSTEM-context runs and for local interactive + # admin sessions where the user has cached SFLD creds via SFLD-Reg). + [System.Management.Automation.PSCredential]$Credential, + + # If the destination already has a backup for this machine, append a + # timestamped suffix instead of overwriting. Default: overwrite (latest + # backup wins; the prior one only matters if the new PC didn't restore + # before this re-backup). + [switch]$KeepPriorBackup +) + +$ErrorActionPreference = 'Stop' +$logDir = 'C:\Logs\UDC' +if (-not (Test-Path $logDir)) { + try { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } catch { $logDir = $env:TEMP } +} +$logFile = Join-Path $logDir ("Backup-UDCData_$(Get-Date -Format 'yyyyMMdd-HHmmss').log") +try { Start-Transcript -Path $logFile -Append -Force | Out-Null } catch {} + +function Log { + param([string]$Msg, [string]$Level = 'INFO') + $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + Write-Host "[$ts][$Level] $Msg" +} + +Log "===============================================" +Log "Backup-UDCData starting" +Log "Hostname: $env:COMPUTERNAME" +Log "User: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" +Log "UDC data dir: $UdcDataDir" +Log "Backup share: $BackupShareRoot" +Log "Log file: $logFile" +Log "===============================================" + +# --- Resolve machine number --- +if (-not $MachineNumber) { + $settings = Join-Path $UdcDataDir 'udc_settings.json' + if (-not (Test-Path $settings)) { + Log "udc_settings.json not found at $settings - cannot auto-detect machine number" 'ERROR' + Log "Re-run with -MachineNumber to override" + try { Stop-Transcript | Out-Null } catch {} + exit 2 + } + try { + $json = Get-Content $settings -Raw | ConvertFrom-Json + $MachineNumber = $json.GeneralSettings.MachineNumber + } catch { + Log "Failed to parse udc_settings.json: $_" 'ERROR' + try { Stop-Transcript | Out-Null } catch {} + exit 3 + } +} +if (-not $MachineNumber -or $MachineNumber -eq '9999' -or $MachineNumber -notmatch '^\d+$') { + Log "Invalid or placeholder machine number: '$MachineNumber'. Aborting." 'ERROR' + Log "(Backups for placeholder 9999 would collide across PCs and aren't useful.)" + try { Stop-Transcript | Out-Null } catch {} + exit 4 +} +Log "Resolved machine number: $MachineNumber" + +# --- Verify source files exist --- +$srcCurrent = Join-Path $UdcDataDir 'CurrentData.json' +$srcArchive = Join-Path $UdcDataDir 'ArchiveData' + +$haveCurrent = Test-Path -LiteralPath $srcCurrent +$haveArchive = Test-Path -LiteralPath $srcArchive + +if (-not $haveCurrent -and -not $haveArchive) { + Log "Neither CurrentData.json nor ArchiveData/ exists under $UdcDataDir. Nothing to back up." 'WARN' + try { Stop-Transcript | Out-Null } catch {} + exit 0 +} +Log "CurrentData.json present: $haveCurrent" +Log "ArchiveData/ present: $haveArchive" + +# --- Mount share if credentials supplied (otherwise rely on ambient auth) --- +$psDrive = $null +$dest = Join-Path $BackupShareRoot $MachineNumber +if ($Credential) { + try { + $psDrive = New-PSDrive -Name UDCBKP -PSProvider FileSystem -Root $BackupShareRoot -Credential $Credential -ErrorAction Stop + Log "Share mounted as UDCBKP: with explicit credentials" + $dest = "UDCBKP:\$MachineNumber" + } catch { + Log "Failed to mount $BackupShareRoot with supplied credentials: $_" 'ERROR' + try { Stop-Transcript | Out-Null } catch {} + exit 5 + } +} + +# --- Create destination dir --- +try { + if (-not (Test-Path -LiteralPath $dest)) { + New-Item -ItemType Directory -Path $dest -Force | Out-Null + Log "Created destination: $dest" + } +} catch { + Log "Failed to create destination $dest : $_" 'ERROR' + if ($psDrive) { Remove-PSDrive -Name UDCBKP -Force } + try { Stop-Transcript | Out-Null } catch {} + exit 6 +} + +# --- Optional: rotate prior backup at this bay if -KeepPriorBackup --- +if ($KeepPriorBackup) { + $existingMarker = Join-Path $dest 'CurrentData.json' + if (Test-Path -LiteralPath $existingMarker) { + $rotateName = Join-Path $dest ('superseded-' + (Get-Date -Format 'yyyyMMdd-HHmmss')) + New-Item -ItemType Directory -Path $rotateName -Force | Out-Null + Get-ChildItem $dest -Force | Where-Object { $_.Name -ne 'migrated' -and $_.Name -ne (Split-Path $rotateName -Leaf) } | ForEach-Object { + Move-Item -LiteralPath $_.FullName -Destination $rotateName -Force + } + Log "Prior backup rotated into $rotateName (KeepPriorBackup=true)" + } +} + +# --- Copy CurrentData.json --- +$copiedCurrent = $false +$currentBytes = 0 +if ($haveCurrent) { + try { + Copy-Item -LiteralPath $srcCurrent -Destination (Join-Path $dest 'CurrentData.json') -Force -ErrorAction Stop + $currentBytes = (Get-Item -LiteralPath $srcCurrent).Length + $copiedCurrent = $true + Log "Copied CurrentData.json ($currentBytes bytes)" + } catch { + Log "Failed to copy CurrentData.json: $_" 'ERROR' + } +} + +# --- Copy ArchiveData/ recursively --- +$copiedArchive = $false +$archiveBytes = 0 +$archiveFiles = 0 +if ($haveArchive) { + try { + $destArchive = Join-Path $dest 'ArchiveData' + if (Test-Path -LiteralPath $destArchive) { + Remove-Item -LiteralPath $destArchive -Recurse -Force -ErrorAction Stop + } + Copy-Item -LiteralPath $srcArchive -Destination $destArchive -Recurse -Force -ErrorAction Stop + $copiedArchive = $true + $archiveItems = Get-ChildItem -LiteralPath $destArchive -Recurse -File -ErrorAction SilentlyContinue + $archiveBytes = ($archiveItems | Measure-Object Length -Sum).Sum + $archiveFiles = $archiveItems.Count + Log "Copied ArchiveData/ ($archiveFiles files, $archiveBytes bytes)" + } catch { + Log "Failed to copy ArchiveData/: $_" 'ERROR' + } +} + +# --- Drop a small backup.manifest.json next to the data for forensics --- +try { + $manifest = [ordered]@{ + BackedUpAt = (Get-Date -Format 'o') + SourceHostname = $env:COMPUTERNAME + SourceUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + MachineNumber = $MachineNumber + CurrentDataPresent = $copiedCurrent + CurrentDataBytes = $currentBytes + ArchiveDataPresent = $copiedArchive + ArchiveDataFiles = $archiveFiles + ArchiveDataBytes = $archiveBytes + } + $manifest | ConvertTo-Json | Set-Content -Path (Join-Path $dest 'backup.manifest.json') -Encoding UTF8 + Log "Wrote backup.manifest.json" +} catch { + Log "Manifest write failed (non-fatal): $_" 'WARN' +} + +# --- Tear down mount --- +if ($psDrive) { + try { Remove-PSDrive -Name UDCBKP -Force } catch {} +} + +Log "===============================================" +Log "Backup complete:" +Log " Bay: $MachineNumber" +Log " Destination: $BackupShareRoot\$MachineNumber\" +Log " CurrentData.json: $(if ($copiedCurrent) {'OK ('+$currentBytes+' bytes)'} else {'MISSING'})" +Log " ArchiveData/: $(if ($copiedArchive) {'OK ('+$archiveFiles+' files, '+$archiveBytes+' bytes)'} else {'MISSING'})" +Log "===============================================" +try { Stop-Transcript | Out-Null } catch {} +exit 0 diff --git a/playbook/shopfloor-setup/Standard/Restore-UDCData.ps1 b/playbook/shopfloor-setup/Standard/Restore-UDCData.ps1 new file mode 100644 index 0000000..4ca908b --- /dev/null +++ b/playbook/shopfloor-setup/Standard/Restore-UDCData.ps1 @@ -0,0 +1,191 @@ +# Restore-UDCData.ps1 - Idempotent UDC data restore for the manifest engine. +# +# Triggered by the GE Shopfloor Enforce scheduled task (runs as SYSTEM, every +# user logon + every 5 min). Standard-machine manifest entry uses +# DetectionMethod=Always so this fires every cycle; the script self-decides +# whether there's actually any work to do. +# +# CONTRACT: +# - 99% of cycles: no backup waiting -> exit 0 in ~1 second, no side effects +# - 1 cycle (the one after Backup-UDCData lands a backup for this PC's bay): +# stop UDC, copy CurrentData.json + ArchiveData/ to C:\ProgramData\UDC, +# move consumed backup to \migrated\\, write +# restore.manifest.json, restart UDC. After this, root is empty so the +# check returns "no backup waiting" again on subsequent cycles. +# +# DESIGNED FOR THE SWAP WORKFLOW: +# New PC gets pre-imaged with real machine number + locked down, sits in +# storage. Days/weeks later, tech runs Backup-UDCData on old PC -> backup +# lands on share. Tech swaps PCs. New PC powers on at the bay -> ShopFloor +# autologon -> manifest engine fires this script -> backup detected -> +# restored -> UDC opens with prior history intact. +# +# Replaces the placeholder->real trigger in Update-MachineNumber.ps1 for the +# pre-imaged-then-swapped case (where the trigger fired at imaging time, before +# the backup existed). Update-MachineNumber.ps1's branch still handles the +# secondary case (tech used 9999 placeholder + sets number at bay) - both +# triggers safely no-op if the other already consumed the backup. + +[CmdletBinding()] +param( + [string]$BackupShareRoot = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\backup\udc', + [string]$UdcDataDir = 'C:\ProgramData\UDC', + [string]$UdcExePath = 'C:\Program Files\UDC\UDC.exe', + [string]$UdcSettingsPath = 'C:\ProgramData\UDC\udc_settings.json', + [string]$Site = 'West Jefferson' +) + +$ErrorActionPreference = 'Stop' +$logDir = 'C:\Logs\UDC' +if (-not (Test-Path $logDir)) { + try { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } catch { $logDir = $env:TEMP } +} +$logFile = Join-Path $logDir 'Restore-UDCData.log' + +function Log { + param([string]$Msg, [string]$Level = 'INFO') + $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + $line = "[$ts][$Level] $Msg" + Add-Content -LiteralPath $logFile -Value $line + Write-Host $line +} + +# --- Resolve local machine number --- +if (-not (Test-Path -LiteralPath $UdcSettingsPath)) { + # No UDC installed yet. Manifest engine will catch up later if/when it lands. + exit 0 +} +try { + $json = Get-Content -LiteralPath $UdcSettingsPath -Raw | ConvertFrom-Json + $mn = $json.GeneralSettings.MachineNumber +} catch { + Log "Failed to parse $UdcSettingsPath : $_" 'ERROR' + exit 0 +} +if (-not $mn -or $mn -eq '9999' -or $mn -notmatch '^\d+$') { + # Placeholder or invalid - the placeholder->real trigger in + # Update-MachineNumber.ps1 will catch it when the tech sets a real number. + exit 0 +} + +# --- Probe for a waiting backup --- +$bayDir = Join-Path $BackupShareRoot $mn +$srcCur = Join-Path $bayDir 'CurrentData.json' +$srcArc = Join-Path $bayDir 'ArchiveData' + +if (-not (Test-Path -LiteralPath $srcCur)) { + # Most-common path: no backup waiting. Exit silently to keep enforce-cycle + # logs clean. (Manifest engine still records that the entry ran.) + exit 0 +} + +# We have a backup. From here on, log everything. +Log "===============================================" +Log "UDC data backup detected at $bayDir - restoring." +Log "Hostname: $env:COMPUTERNAME" +Log "Machine number: $mn" + +# --- Stop UDC.exe so CurrentData.json isn't locked --- +Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object { + try { + $_.Kill() + $_.WaitForExit(5000) | Out-Null + Log "Stopped existing UDC.exe (PID $($_.Id))" + } catch { + Log "Could not stop UDC.exe (PID $($_.Id)): $_" 'WARN' + } +} +Start-Sleep -Milliseconds 500 + +# --- Ensure local UDC data dir exists --- +if (-not (Test-Path -LiteralPath $UdcDataDir)) { + New-Item -ItemType Directory -Path $UdcDataDir -Force | Out-Null +} +$localCur = Join-Path $UdcDataDir 'CurrentData.json' +$localArc = Join-Path $UdcDataDir 'ArchiveData' + +# --- Copy CurrentData.json --- +$copiedCur = $false +try { + Copy-Item -LiteralPath $srcCur -Destination $localCur -Force -ErrorAction Stop + $copiedCur = $true + Log "Copied CurrentData.json ($((Get-Item $localCur).Length) bytes)" +} catch { + Log "Copy CurrentData.json failed: $_" 'ERROR' +} + +# --- Copy ArchiveData/ --- +$copiedArc = $false +$arcFiles = 0 +$arcBytes = 0 +if (Test-Path -LiteralPath $srcArc) { + try { + if (Test-Path -LiteralPath $localArc) { + Remove-Item -LiteralPath $localArc -Recurse -Force -ErrorAction SilentlyContinue + } + Copy-Item -LiteralPath $srcArc -Destination $localArc -Recurse -Force -ErrorAction Stop + $arcItems = Get-ChildItem -LiteralPath $localArc -Recurse -File -ErrorAction SilentlyContinue + $arcFiles = $arcItems.Count + $arcBytes = ($arcItems | Measure-Object Length -Sum).Sum + $copiedArc = $true + Log "Copied ArchiveData/ ($arcFiles files, $arcBytes bytes)" + } catch { + Log "Copy ArchiveData/ failed: $_" 'ERROR' + } +} + +# --- One-shot consumption: only move backup -> migrated/ if BOTH copies succeeded --- +# (If one failed, leave the live backup in place so the next cycle can retry.) +$consumeOk = ($copiedCur -and ($copiedArc -or -not (Test-Path -LiteralPath $srcArc))) +if ($consumeOk) { + try { + $stamp = Get-Date -Format 'yyyy-MM-ddTHH-mm-ssZ' + $migDir = Join-Path $bayDir 'migrated' + $migStamp = Join-Path $migDir $stamp + if (-not (Test-Path -LiteralPath $migDir)) { New-Item -ItemType Directory -Path $migDir -Force | Out-Null } + if (-not (Test-Path -LiteralPath $migStamp)) { New-Item -ItemType Directory -Path $migStamp -Force | Out-Null } + + Move-Item -LiteralPath $srcCur -Destination (Join-Path $migStamp 'CurrentData.json') -Force -ErrorAction Stop + if (Test-Path -LiteralPath $srcArc) { + Move-Item -LiteralPath $srcArc -Destination (Join-Path $migStamp 'ArchiveData') -Force -ErrorAction Stop + } + $bakManifest = Join-Path $bayDir 'backup.manifest.json' + if (Test-Path -LiteralPath $bakManifest) { + Move-Item -LiteralPath $bakManifest -Destination (Join-Path $migStamp 'backup.manifest.json') -Force -ErrorAction SilentlyContinue + } + + $restoreManifest = [ordered]@{ + RestoredAt = (Get-Date -Format 'o') + DestinationHostname = $env:COMPUTERNAME + DestinationUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + MachineNumber = $mn + CurrentDataBytes = (Get-Item $localCur).Length + ArchiveDataFiles = $arcFiles + ArchiveDataBytes = $arcBytes + RestoredVia = 'Restore-UDCData.ps1 (manifest engine, on logon)' + } + $restoreManifest | ConvertTo-Json | Set-Content -Path (Join-Path $migStamp 'restore.manifest.json') -Encoding UTF8 + + Log "Backup consumed -> migrated\$stamp\" + } catch { + Log "Move-to-migrated failed (data IS restored locally, but live backup remains - next cycle will retry consumption): $_" 'ERROR' + } +} else { + Log "Restore incomplete - leaving live backup at $bayDir for retry next cycle." 'WARN' +} + +# --- Relaunch UDC with the current machine number args so it picks up the +# restored CurrentData.json. UDC's vendor autostart in HKLM\Run will also +# fire on the next user logon, so this is belt-and-suspenders for the +# same-session case (e.g. tech is at the keyboard during the restore). --- +if ((Test-Path -LiteralPath $UdcExePath) -and $copiedCur) { + try { + Start-Process -FilePath $UdcExePath -ArgumentList @("`"$Site`"", "-$mn") + Log "Relaunched UDC.exe with `"$Site`" -$mn" + } catch { + Log "UDC relaunch failed: $_" 'WARN' + } +} + +Log "===============================================" +exit 0