Files
pxe-server/playbook/shopfloor-setup/common/scripts/Backup-NtlarsSettings.ps1
cproudlock 54cbe6b5d6 Bring the share's common scripts under version control
Sixteen files that run on every shopfloor PC existed only on the SFLD share.
The cost showed up while debugging the NTLARS backup: the script that posts to
ShopDB could not be read, reviewed or diffed, so its behaviour was inferred
from log output for most of a day. It turned out to hold a silent fallback that
had been governing the whole fleet for months.

Imported as-is from tsgwp00525-v2, no edits:

  lib/ShopdbBackupClient.psm1        the shared backup client
  scripts/Backup-NtlarsSettings.ps1  converted to use it
  scripts/Set-ShopdbCollectorKey.ps1 collector credential delivery
  scripts/Test-RegExport.ps1         exercises the .reg codec with mocks
  scripts/Set-EventSaver*.ps1        kiosk power / screensaver / disable
  scripts/Setup-OpenText.*           OpenText install + toolbar
  scripts/Migrate-PCType.ps1, Select-KioskType.ps1, Set-FmsHostsEntry.ps1,
  scripts/ensure-vnc-firewall.ps1, Install-AcroReader.cmd, Install-Oracle11r2.cmd

lib/Install-FromManifest.ps1 is also updated from the share, which was 37 lines
AHEAD of this repo and purely additive: the Add-EnforceResult reporting added
during the kiosk API cutover, done live and never committed back. Nothing was
removed.

Checked for embedded secrets before committing; there are none.
Set-ShopdbCollectorKey deliberately reads its token from a sibling file on the
share rather than holding it, so the script is safe to track.

The share remains what actually runs. This makes it reviewable, and makes the
next drift visible as a diff rather than a surprise.
2026-08-11 12:36:38 -04:00

204 lines
8.9 KiB
PowerShell

# Backup-NtlarsSettings.ps1
#
# Captures this PC's LIVE NTLARS/DNC registry settings and posts them to ShopDB,
# which files them against the MACHINE (not this PC) and keeps a revision
# history. A tech can then re-download the .reg from the machine's page instead
# of hunting for a per-machine file on the share.
#
# Runs from the SFLD share every GE-Enforce cycle as a Type=PS1 manifest entry
# with DetectionMethod=Always. Updating this file on the share changes fleet
# behaviour on the next cycle - there is no local copy to heal.
#
# WHY WOW6432Node IS EXPLICIT:
# NTLARS is a 32-bit app, so its settings physically live under
# HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC. GE-Enforce runs this
# script in 64-bit PowerShell, where HKLM:\SOFTWARE\GE Aircraft Engines\DNC
# is a DIFFERENT (usually absent) key. Reading the unredirected path would
# find nothing and back up an empty config - silently, every cycle. The path
# below is therefore spelled out and never abbreviated.
#
# Note the asymmetry with what NTLARS itself writes: its Save... button
# exports WITHOUT the WOW6432Node segment. ShopDB accepts either dialect and
# stores a dialect-neutral projection, so this script does not need to care.
#
# THROTTLE, LOGGING, CREDENTIALS:
# All handled by common\lib\ShopdbBackupClient.psm1, which every backup kind
# shares. GE-Enforce fires this every cycle, so the module holds the marker
# file, exits early until backups_intervalhours has elapsed, and logs a
# no-change outcome ONCE rather than every five minutes. The interval comes
# from ShopDB, so cadence is changed centrally rather than by editing this
# file on the share.
#
# Always exits 0 so the GE-Enforce "last run result" stays clean. Failures are
# logged, never thrown.
param(
[string]$BaseUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb',
[int]$TimeoutSec = 30,
# Force a post regardless of the throttle. For a tech capturing a
# known-good config on demand.
[switch]$Force
)
$ErrorActionPreference = 'Continue'
# The only path this script still owns. Log, marker, state, collector key,
# enrollment and base URL all moved to ShopdbBackupClient, which names its files
# per KIND so two backup kinds on one bay cannot collide.
$DNCKEY = 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC'
function Convert-RegTypeName {
param($Kind)
switch ("$Kind") {
'String' { 'REG_SZ' }
'ExpandString' { 'REG_EXPAND_SZ' }
'DWord' { 'REG_DWORD' }
'QWord' { 'REG_QWORD' }
'MultiString' { 'REG_MULTI_SZ' }
'Binary' { 'REG_BINARY' }
default { 'REG_SZ' }
}
}
function Get-DncKeys {
<#
The DNC key and every subkey, root first. Split out from
Export-DncToReg so the formatting logic can be exercised against mock
keys on a machine with no registry (see Test-RegExport.ps1).
#>
$keys = @(Get-Item -Path $DNCKEY -EA Stop)
$keys += @(Get-ChildItem -Path $DNCKEY -Recurse -EA SilentlyContinue)
return $keys
}
function Export-DncToReg {
<#
Emits .reg text in the WOW6432Node dialect for the supplied keys.
Built by hand rather than shelling out to `reg export` because reg.exe
writes UTF-16 to a temp file we would then have to read back, and
because this keeps the value types explicit instead of reparsing them.
Takes the key list as a parameter so it can be tested with mocks; the
escaping and dword formatting here are the part that would corrupt a
backup silently and only surface at restore time.
#>
param([Parameter(Mandatory)]$Keys)
$lines = @('Windows Registry Editor Version 5.00', '')
$lines += "; NTLARS DNC Registry Backup"
$lines += "; Computer: $env:COMPUTERNAME"
$lines += "; Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
$lines += ''
foreach ($key in $Keys) {
# PSPath -> the literal HKEY_LOCAL_MACHINE\... form a .reg file needs.
$path = $key.Name -replace '^HKEY_LOCAL_MACHINE', 'HKEY_LOCAL_MACHINE'
$lines += "[$path]"
foreach ($name in $key.GetValueNames()) {
$kind = Convert-RegTypeName $key.GetValueKind($name)
$data = $key.GetValue($name)
$lhs = if ($name -eq '') { '@' } else {
# .NET replacement strings do NOT process backslash escapes, so
# the replacement is the literal output: '\\' emits two
# backslashes, which is what .reg escaping wants. Writing
# '\\\\' here emits FOUR and silently corrupts every path-valued
# setting - verified on Windows before this was fixed.
'"{0}"' -f ($name -replace '\\', '\\' -replace '"', '\"')
}
switch ($kind) {
'REG_DWORD' {
$lines += ('{0}=dword:{1:x8}' -f $lhs, [uint32]$data)
}
'REG_BINARY' {
$hex = ($data | ForEach-Object { '{0:x2}' -f $_ }) -join ','
$lines += ('{0}=hex:{1}' -f $lhs, $hex)
}
'REG_QWORD' {
$bytes = [BitConverter]::GetBytes([uint64]$data)
$hex = ($bytes | ForEach-Object { '{0:x2}' -f $_ }) -join ','
$lines += ('{0}=hex(b):{1}' -f $lhs, $hex)
}
'REG_MULTI_SZ' {
$joined = (($data -join "`0") + "`0`0")
$bytes = [Text.Encoding]::Unicode.GetBytes($joined)
$hex = ($bytes | ForEach-Object { '{0:x2}' -f $_ }) -join ','
$lines += ('{0}=hex(7):{1}' -f $lhs, $hex)
}
'REG_EXPAND_SZ' {
$bytes = [Text.Encoding]::Unicode.GetBytes(("$data" + "`0"))
$hex = ($bytes | ForEach-Object { '{0:x2}' -f $_ }) -join ','
$lines += ('{0}=hex(2):{1}' -f $lhs, $hex)
}
default {
$escaped = ("$data" -replace '\\', '\\' -replace '"', '\"')
$lines += ('{0}="{1}"' -f $lhs, $escaped)
}
}
}
$lines += ''
}
return ($lines -join "`r`n") + "`r`n"
}
# =============================================================================
# Main
#
# Guarded so the file can be DOT-SOURCED to get the functions without running a
# backup. Test-RegExport.ps1 relies on this to exercise Export-DncToReg against
# mock keys on a machine with no registry.
# =============================================================================
if ($MyInvocation.InvocationName -eq '.') { return }
# The plumbing - key lookup, base URL, interval, throttle, logging, the POST and
# its response handling - lives in ShopdbBackupClient. It used to live here, and
# being the only implementation meant every defect in it was invisible: the
# interval read fell back to a hardcoded 24h for months because nobody had a
# second copy to compare against, and the log wrote three lines every five
# minutes whatever happened. Seven other Backup-*.ps1 scripts on this share
# capture device configs and post none of them; they can now do so without
# inheriting a copy of all that.
#
# What stays here is the part that is actually about NTLARS: where its registry
# lives, and how to turn it into .reg text.
Import-Module (Join-Path $PSScriptRoot '..\lib\ShopdbBackupClient.psm1') -Force
if (-not (Test-Path $DNCKEY)) {
# Not an error: plenty of PC types have no NTLARS at all. On those PCs this
# is the permanent state, so it is said once rather than 288 times a day.
Write-ShopdbQuietState -Kind 'ntlars' -State 'no-dnc-key' -Message (
"No DNC key at $DNCKEY - NTLARS is not installed on this PC. Nothing to do.")
exit 0
}
# NTLARS keeps its own MachineNo, used only when pc-config.txt has none. ShopDB
# compares the two and warns on a mismatch rather than silently trusting one.
$ctx = Initialize-ShopdbBackup -Kind 'ntlars' -Force:$Force `
-BaseUrl $(if ($PSBoundParameters.ContainsKey('BaseUrl')) { $BaseUrl } else { '' }) `
-MachineNumberFallback {
try {
$general = Get-ItemProperty -Path (Join-Path $DNCKEY 'General') -EA Stop
if ($general.MachineNo) { return ([string]$general.MachineNo).Trim() }
} catch { }
return ''
}
if (-not $ctx.Proceed) { exit 0 }
try {
$regText = Export-DncToReg -Keys (Get-DncKeys)
} catch {
Write-ShopdbBackupLog -Kind 'ntlars' -Message "Failed to read the DNC key: $_"
exit 0
}
# UTF-16LE + BOM, matching what regedit and NTLARS emit. ShopDB sniffs the BOM,
# so this is belt-and-braces rather than strictly required.
$bytes = [byte[]](0xFF, 0xFE) + [Text.Encoding]::Unicode.GetBytes($regText)
[void](Send-ShopdbBackup -Context $ctx -Bytes $bytes `
-SourceFileName ("{0}.reg" -f $ctx.MachineNumber) -TimeoutSec $TimeoutSec)
Write-ShopdbBackupLog -Kind 'ntlars' -Message '=== Backup-NtlarsSettings end ==='
exit 0