diff --git a/playbook/shopfloor-setup/gea-shopfloor-collections/Report-AssetToShopDB.ps1 b/playbook/shopfloor-setup/gea-shopfloor-collections/Report-AssetToShopDB.ps1 new file mode 100644 index 0000000..e73bed1 --- /dev/null +++ b/playbook/shopfloor-setup/gea-shopfloor-collections/Report-AssetToShopDB.ps1 @@ -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 diff --git a/playbook/shopfloor-setup/gea-shopfloor-collections/manifest-entry-report-asset.json b/playbook/shopfloor-setup/gea-shopfloor-collections/manifest-entry-report-asset.json new file mode 100644 index 0000000..ee99d76 --- /dev/null +++ b/playbook/shopfloor-setup/gea-shopfloor-collections/manifest-entry-report-asset.json @@ -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" +}