# Report-AssetToShopDB.ps1 # # Reports a PC's identity to ShopDB so the machines record stays current with # whatever the PC actually is right now: hostname, BIOS serial, pc-type, logged- # in user, DNC machine number (2001, 2002, ... when present) and its corp/AESFMA # IPv4 address. # # Deployed in common\ (runs on EVERY shopfloor pc-type: collections, cmm, # keyence, waxtrace, genspect, heattreat, partmarker, nocollections, ...), not # collections-only. Non-DNC bays (cmm/keyence/waxtrace) simply report identity # with no machineNo, so no PC-to-machine relationship is built - by design. # # 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 (patch-style: only the fields posted here are updated, so it never # clobbers model/VNC/WinRM), clears+reinserts the interface rows, and (re)creates # the PC-to-machine relationship from machineNo. Safe to fire repeatedly. # # WHY corp-NIC-only: # Some bays (collections/controller) 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 the rest. # Single-NIC PCs just pass their one corp IP through the same gate. 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 } # Machine identifier - optional, sent only if found. api.asp matches it against # the equipment machinenumber OR alias column, so one value covers every type: # 1. eDNC registry (WOW6432Node, then native) - DNC/collections bays (2001...). # Follows bay reassignment, which Set-MachineNumber rewrites here. # 2. C:\Enrollment\cmm\cmmid.txt - CMM bay id (e.g. CMM3), written by # select-cmm-bay.ps1 at imaging. Matches the equipment machinenumber. # 3. C:\Enrollment\machine-number.txt - imaging value. For wax&trace this is the # asset tag from select-waxtrace-asset.ps1 (matches the equipment alias); # for DNC bays it is the digit number. 9999 is the placeholder = skip. # keyence / genspect / part-marker have no per-bay id on the PC -> no machineNo # is sent and the link is assigned manually in ShopDB. $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 -and $v.Trim() -ne '9999') { $machineNo = $v.Trim() } } } catch { Log "WARN could not read MachineNo from ${regPath}: $($_.Exception.Message)" } } if (-not $machineNo) { $cmmFile = 'C:\Enrollment\cmm\cmmid.txt' if (Test-Path -LiteralPath $cmmFile) { try { $v = ([string](Get-Content -LiteralPath $cmmFile -First 1 -ErrorAction Stop)).Trim() if ($v -and $v -ne '9999') { $machineNo = $v; Log "machineNo from $cmmFile (CMM bay id): $machineNo" } } catch { Log "WARN could not read ${cmmFile}: $($_.Exception.Message)" } } } if (-not $machineNo) { $mnFile = 'C:\Enrollment\machine-number.txt' if (Test-Path -LiteralPath $mnFile) { try { $v = ([string](Get-Content -LiteralPath $mnFile -First 1 -ErrorAction Stop)).Trim() if ($v -and $v -ne '9999') { $machineNo = $v; Log "machineNo from $mnFile (imaging value): $machineNo" } } catch { Log "WARN could not read ${mnFile}: $($_.Exception.Message)" } } } # OS for the operatingsystems lookup: caption + feature-update (e.g. 23H2, read # from the registry since WMI does not expose it) + major build number. Yields a # string like "Microsoft Windows 11 Enterprise 23H2 (build 22631)" so the fleet # OS-version breakdown is visible everywhere osid is shown - no schema change, # api.asp upserts each distinct string into operatingsystems. $osVersion = '' # last boot time - api.asp stores it as machines.lastboottime; uptime is derived # server-side (DATEDIFF(NOW(), lastboottime)). MySQL datetime format. $lastBootTime = '' try { $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop $osVersion = "$($os.Caption)".Trim() $displayVersion = '' try { $displayVersion = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name DisplayVersion -ErrorAction Stop).DisplayVersion } catch {} if ($displayVersion) { $osVersion += " $displayVersion" } if ($os.BuildNumber) { $osVersion += " (build $($os.BuildNumber))" } $osVersion = $osVersion.Trim() try { $lastBootTime = $os.LastBootUpTime.ToString('yyyy-MM-dd HH:mm:ss') } catch {} } catch {} # interactive console user (DOMAIN\user). this script runs as SYSTEM so we # cannot use $env:USERNAME; Win32_ComputerSystem.UserName is the console # session owner and works from SYSTEM context. empty when nobody is logged on, # in which case we omit it from the post so an unattended bay does not blank # the last-known user on the server. $loggedInUser = '' try { $loggedInUser = (Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop).UserName # Win32 returns COMPUTERNAME\user (local account) or DOMAIN\user. Keep only # the username part: matches the legacy bare-username convention and avoids # the backslash, which the inline api.asp SQL does not escape so MySQL eats # it (FB90238\ShopFloor was being stored as FB90238ShopFloor). if ($loggedInUser) { $loggedInUser = ($loggedInUser -split '\\')[-1].Trim() } } catch { Log "WARN could not read logged-in user: $($_.Exception.Message)" } # imaging pc-type (gea-shopfloor-*), read from the enrollment file written at # image time. api.asp maps both the gea-shopfloor-* values and the legacy # display strings to the right pctypeid. Sent only when present; when absent the # server's patch-style update leaves the existing pctype untouched (so a bare # report never re-types a PC). This is what lets the reporter run fleet-wide # from common\ instead of collections-only. $pcType = '' $ptFile = 'C:\Enrollment\pc-type.txt' if (Test-Path -LiteralPath $ptFile) { try { $pcType = (Get-Content -LiteralPath $ptFile -First 1 -ErrorAction Stop).Trim() } catch { Log "WARN could not read ${ptFile}: $($_.Exception.Message)" } } # PC make/model from WMI (e.g. "Dell Inc." / "OptiPlex 7090"). api.asp resolves # or creates the vendor + model rows and links modelnumberid. Sent only when # present so a WMI read failure does not blank the model on the row. $manufacturer = '' $model = '' try { $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop $manufacturer = "$($cs.Manufacturer)".Trim() $model = "$($cs.Model)".Trim() } catch { Log "WARN could not read make/model: $($_.Exception.Message)" } # gather IPv4 NICs - BOTH the corp/AESFMA NIC and the controller/machine LAN NIC, # each with its MAC. Physical adapters only (drop Hyper-V/VPN/WSL/virtual plus # link-local 169.254 and loopback). Each NIC is tagged IsMachineNetwork: true for # the controller LAN (any IP outside the corp ranges), false for the corp NIC. $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) { $adapter = $null try { $adapter = Get-NetAdapter -InterfaceIndex $ipo.InterfaceIndex -ErrorAction Stop } catch {} # physical + connected only; skip virtual adapters (Hyper-V/VPN/WSL/etc) if (-not $adapter) { continue } if (-not $adapter.HardwareInterface) { continue } if ($adapter.Status -ne 'Up') { continue } $mac = $adapter.MacAddress $gw = '' 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 '.' $isCorp = Test-InAllowedRange $ipo.IPAddress $interfaces += [pscustomobject]@{ IPAddress = $ipo.IPAddress MACAddress = $mac SubnetMask = $subnetMask DefaultGateway = $gw InterfaceName = $ipo.InterfaceAlias IsMachineNetwork = (-not $isCorp) # controller/machine LAN = true; corp = false } } } catch { Log "WARN interface enumeration failed: $($_.Exception.Message)" } if ($interfaces.Count -eq 0) { Log 'WARN no physical IPv4 NIC 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 osVersion = $osVersion networkInterfaces = $networkInterfacesJson } if ($machineNo) { $body['machineNo'] = $machineNo } if ($loggedInUser) { $body['loggedInUser'] = $loggedInUser } if ($pcType) { $body['pcType'] = $pcType } if ($manufacturer) { $body['manufacturer'] = $manufacturer } if ($model) { $body['model'] = $model } if ($lastBootTime) { $body['lastBootTime'] = $lastBootTime } Log ("POST {0} host={1} serial={2} pcType={3} make={4} model={5} os={6} boot={7} machineNo={8} user={9} ips={10}" -f ` $ApiUrl, $hostname, $serialNumber, $pcType, $manufacturer, $model, $osVersion, $lastBootTime, $machineNo, $loggedInUser, (($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