collections: report host + IP + machine number to ShopDB each enforce cycle

Adds Report-AssetToShopDB.ps1 (Type=PS1, DetectionMethod=Always manifest entry)
for collections PCs. Reads hostname, BIOS serial, eDNC MachineNo and the corp
NIC IPv4 (filtered to WJ corp ranges, controller NIC dropped) and POSTs
action=updateCompleteAsset to ShopDB api.asp, which upserts the machine, stores
the IP, and links the PC to its machine-number equipment. manifest-entry-report-asset.json
is the snippet to merge into the SFLD share collections manifest (+ stage the
script under apps/). Note: relies on the ShopDB api.asp LogToFile Err-leak fix
(separate shopdb repo commit) to create the relationship reliably.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-03 14:04:33 -04:00
parent 5211861409
commit a380b17112
2 changed files with 179 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
# 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 from eDNC reg (2001, 2002, ...). optional - sent only if present.
$machineNo = ''
$edncRegPath = 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General'
try {
if (Test-Path $edncRegPath) {
$machineNo = [string](Get-ItemProperty -Path $edncRegPath -Name MachineNo -ErrorAction Stop).MachineNo
$machineNo = $machineNo.Trim()
}
} catch {
Log "WARN could not read eDNC MachineNo: $($_.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

View File

@@ -0,0 +1,8 @@
{
"_comment": "Drop this entry into the SFLD share at \\tsgwp00525\\sfld$\\v2\\shared\\dt\\shopfloor\\gea-shopfloor-collections\\manifest.json (Applications array). Place Report-AssetToShopDB.ps1 in the apps/ dir on the share. Type=PS1 + DetectionMethod=Always means GE-Enforce runs it straight off the share every cycle (at-logon, periodic 5-min, shift-change) under SYSTEM, no local copy. The script reads hostname, BIOS serial, eDNC MachineNo and the corp/AESFMA IPv4 (filtered to WJ corp ranges 10.134.48.0/23 + 10.48.249.0/26) and POSTs action=updateCompleteAsset to ShopDB. api.asp upserts the machines row by hostname, clears+reinserts interface rows, and recreates the PC-to-machine relationship from machineNo, so repeat fires are safe. Override the ShopDB URL with the Args field if the host/path moves, e.g. \"Args\": \"-ApiUrl https://newhost/shopdb/api.asp\".",
"Name": "Report asset (host + IP + machine number) to ShopDB",
"PCTypes": ["gea-shopfloor-collections"],
"Script": "apps/Report-AssetToShopDB.ps1",
"Type": "PS1",
"DetectionMethod": "Always"
}