Files
pxe-server/playbook/shopfloor-setup/gea-shopfloor-collections/Report-AssetToShopDB.ps1
cproudlock e97e5bd049 shopfloor: CMM PC-DMIS version gate, ShopDB reporter fixes, staging self-heal
- lib Install-FromManifest 2.5->2.6: add _CmmVersion per-entry filter (reads
  C:\Enrollment\cmm\version.txt). Lifted the version gate out of 09-Setup-CMM
  into the shared lib so imaging and GE-Enforce apply it identically and cannot
  drift (root cause of PC-DMIS 2016 installing on every CMM).
- Install-goCMMSettings: canonicalize the part-group share host to the FQDN in
  both the registry and ApplicationSettings.xml. Handles bare \\tsgwp00525\ and
  the legacy rd.ds.ge.com domain; idempotent. VM-tested.
- Report-AssetToShopDB: resolve the machine number eDNC registry first, then fall
  back to C:\Enrollment\machine-number.txt (matches the lib resolution order) so
  a freshly imaged PC still reports its number for the PC-machine relationship.
- Add Update-CMMEnforcer.ps1/.bat: update one CMM's local lib to the gated
  version and self-heal its PC-DMIS version.
- Add Debug-ShopDBReporting.ps1/.bat: one-shot reporter triage (preconditions,
  client log, live test POST, verdict).
- Add Verify-And-Heal-Staging.ps1/.bat: post-boot check that every imaging
  payload arrived and re-pull anything missing from the share, including the CMM
  bundle and the selected bay's backup (the payload that times out in WinPE).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:14:54 -04:00

194 lines
7.3 KiB
PowerShell

# Report-AssetToShopDB.ps1
#
# Reports a collections bay's identity to ShopDB so the machines record stays
# current with whatever the bay actually is right now: hostname, BIOS serial,
# DNC machine number (2001, 2002, ...) and its corp/AESFMA IPv4 address.
#
# Runs every GE-Enforce cycle as a Type=PS1 manifest entry (DetectionMethod
# Always) under the SYSTEM scheduled task. Idempotent on the server side:
# ShopDB api.asp action=updateCompleteAsset upserts the machines row keyed by
# hostname, clears+reinserts the interface rows, and (re)creates the
# PC-to-machine relationship from machineNo. Safe to fire repeatedly.
#
# WHY collections-only and corp-NIC-only:
# Collections (controller-NIC) bays carry two NICs - a private controller
# NIC (e.g. 192.168.x / 10.x stray) and the routable corp/AESFMA NIC. Only
# the corp NIC belongs in ShopDB, so we filter to the WJ corp ranges and
# drop everything else. Mirrors the allowed-range gate in
# Invoke-FilteredReportIP.ps1.
#
# Always exits 0 so the GE-Enforce "last run result" stays clean; failures are
# logged, never thrown.
param(
# ShopDB asset endpoint. Override via the manifest entry's "Args" field if
# the host or path ever moves.
[string]$ApiUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb/api.asp',
[int]$TimeoutSec = 30
)
$ErrorActionPreference = 'Continue'
$logDir = 'C:\Logs\Shopfloor'
if (-not (Test-Path $logDir)) {
New-Item -ItemType Directory -Path $logDir -Force -ErrorAction SilentlyContinue | Out-Null
}
$logFile = Join-Path $logDir ('report-asset-{0}.log' -f (Get-Date -Format 'yyyyMMdd'))
function Log([string]$msg) {
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
"$ts $msg" | Tee-Object -FilePath $logFile -Append | Out-Null
}
# corp ranges - same gate as Invoke-FilteredReportIP. update if site re-VLANs.
$allowedRanges = @(
@{ Network = '10.134.48.0'; PrefixLen = 23 },
@{ Network = '10.48.249.0'; PrefixLen = 26 }
)
function ConvertTo-Uint32([string]$ip) {
$bytes = ([System.Net.IPAddress]::Parse($ip)).GetAddressBytes()
[Array]::Reverse($bytes)
return [BitConverter]::ToUInt32($bytes, 0)
}
function Test-InAllowedRange([string]$ip) {
try {
$ipInt = ConvertTo-Uint32 $ip
foreach ($r in $allowedRanges) {
$netInt = ConvertTo-Uint32 $r.Network
$mask = [uint32]([math]::Pow(2, 32) - [math]::Pow(2, 32 - $r.PrefixLen))
if (($ipInt -band $mask) -eq ($netInt -band $mask)) { return $true }
}
} catch {}
return $false
}
Log '=== Report asset to ShopDB ==='
# hostname
$hostname = $env:COMPUTERNAME
# BIOS serial - required by api.asp. bail if missing.
$serialNumber = ''
try {
$serialNumber = (Get-CimInstance -ClassName Win32_BIOS -ErrorAction Stop).SerialNumber
if ($serialNumber) { $serialNumber = $serialNumber.Trim() }
} catch {
Log "WARN could not read BIOS serial: $($_.Exception.Message)"
}
if (-not $serialNumber) {
Log 'ERROR no BIOS serial number; api.asp requires hostname + serialNumber. Skipping.'
exit 0
}
# DNC machine number (2001, 2002, ...). optional - sent only if found.
# Resolution order mirrors the GE-Enforce lib Get-CurrentMachineNumber so the
# reporter and manifest gating agree:
# 1. eDNC registry (WOW6432Node, then native) - follows bay reassignment, which
# Set-MachineNumber rewrites here.
# 2. C:\Enrollment\machine-number.txt - the imaging-time value written once by
# startnet.cmd. Used when eDNC has not populated the registry yet (fresh
# image, or a bay where eDNC has not run), so the PC still reports its number
# and api.asp can build the relationship.
$machineNo = ''
foreach ($regPath in @(
'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General',
'HKLM:\SOFTWARE\GE Aircraft Engines\DNC\General'
)) {
if ($machineNo) { break }
try {
if (Test-Path $regPath) {
$v = [string](Get-ItemProperty -Path $regPath -Name MachineNo -ErrorAction Stop).MachineNo
if ($v) { $machineNo = $v.Trim() }
}
} catch {
Log "WARN could not read MachineNo from ${regPath}: $($_.Exception.Message)"
}
}
if (-not $machineNo) {
$mnFile = 'C:\Enrollment\machine-number.txt'
if (Test-Path -LiteralPath $mnFile) {
try {
$v = Get-Content -LiteralPath $mnFile -First 1 -ErrorAction Stop
if ($v) { $machineNo = ([string]$v).Trim(); Log "machineNo from $mnFile (eDNC registry empty): $machineNo" }
} catch { Log "WARN could not read ${mnFile}: $($_.Exception.Message)" }
}
}
# OS caption for the operatingsystems lookup.
$osVersion = ''
try {
$osVersion = (Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop).Caption
if ($osVersion) { $osVersion = $osVersion.Trim() }
} catch {}
# gather corp NICs only. one networkInterfaces entry per allowed IPv4.
$interfaces = @()
try {
$ipObjs = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop |
Where-Object { $_.IPAddress -notmatch '^169\.254' -and $_.IPAddress -ne '127.0.0.1' }
foreach ($ipo in $ipObjs) {
if (-not (Test-InAllowedRange $ipo.IPAddress)) { continue }
$mac = ''
$gw = ''
try {
$adapter = Get-NetAdapter -InterfaceIndex $ipo.InterfaceIndex -ErrorAction Stop
$mac = $adapter.MacAddress
} catch {}
try {
$gw = (Get-NetRoute -InterfaceIndex $ipo.InterfaceIndex -DestinationPrefix '0.0.0.0/0' -ErrorAction Stop |
Select-Object -First 1).NextHop
} catch {}
# CIDR prefix -> dotted subnet mask
$maskInt = [uint32]([math]::Pow(2, 32) - [math]::Pow(2, 32 - $ipo.PrefixLength))
$maskBytes = [BitConverter]::GetBytes($maskInt)
[Array]::Reverse($maskBytes)
$subnetMask = ($maskBytes | ForEach-Object { $_ }) -join '.'
$interfaces += [pscustomobject]@{
IPAddress = $ipo.IPAddress
MACAddress = $mac
SubnetMask = $subnetMask
DefaultGateway = $gw
InterfaceName = $ipo.InterfaceAlias
IsMachineNetwork = $false # corp NIC, not the controller LAN
}
}
} catch {
Log "WARN interface enumeration failed: $($_.Exception.Message)"
}
if ($interfaces.Count -eq 0) {
Log 'WARN no corp-range IPv4 found; posting identity without interfaces.'
}
$networkInterfacesJson = if ($interfaces.Count -gt 0) { $interfaces | ConvertTo-Json -Compress -Depth 4 } else { '' }
# ConvertTo-Json emits a bare object (not an array) for a single element; force array shape for api.asp ParseJSONArray.
if ($interfaces.Count -eq 1) { $networkInterfacesJson = '[' + $networkInterfacesJson + ']' }
$body = @{
action = 'updateCompleteAsset'
hostname = $hostname
serialNumber = $serialNumber
pcType = 'Shopfloor'
osVersion = $osVersion
networkInterfaces = $networkInterfacesJson
}
if ($machineNo) { $body['machineNo'] = $machineNo }
Log ("POST {0} host={1} serial={2} machineNo={3} ips={4}" -f `
$ApiUrl, $hostname, $serialNumber, $machineNo, (($interfaces | ForEach-Object { $_.IPAddress }) -join ','))
try {
$resp = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $body -TimeoutSec $TimeoutSec -ErrorAction Stop
Log ("RESPONSE {0}" -f ($resp | ConvertTo-Json -Compress -Depth 4))
} catch {
Log "ERROR POST failed: $($_.Exception.Message)"
}
exit 0