From 287ec86c12d33a51e15c60bbf16fd52a20405bee Mon Sep 17 00:00:00 2001 From: cproudlock Date: Tue, 30 Jun 2026 15:41:36 -0400 Subject: [PATCH] Report-AssetToShopDB: fleet-wide reporting, more fields, dual NIC - collect logged-in user (console user via Win32_ComputerSystem, bare username), pc-type (C:\Enrollment\pc-type.txt), make/model, OS version (caption + DisplayVersion + build), last boot time (for uptime) - report BOTH corp and controller NICs (physical only), each with MAC, tagged IsMachineNetwork; was corp-only before - machine-number sourcing adds C:\Enrollment\cmm\cmmid.txt (CMM bay id) and skips the 9999 placeholder everywhere - intended to run from common\ (every pc-type), not collections-only; api.asp patch-style update keeps it from clobbering other types Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Report-AssetToShopDB.ps1 | 165 +++++++++++++----- 1 file changed, 126 insertions(+), 39 deletions(-) diff --git a/playbook/shopfloor-setup/gea-shopfloor-collections/Report-AssetToShopDB.ps1 b/playbook/shopfloor-setup/gea-shopfloor-collections/Report-AssetToShopDB.ps1 index 3fad122..6c29b72 100644 --- a/playbook/shopfloor-setup/gea-shopfloor-collections/Report-AssetToShopDB.ps1 +++ b/playbook/shopfloor-setup/gea-shopfloor-collections/Report-AssetToShopDB.ps1 @@ -1,21 +1,28 @@ # 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. +# 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, clears+reinserts the interface rows, and (re)creates the -# PC-to-machine relationship from machineNo. Safe to fire repeatedly. +# 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 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. +# 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. @@ -83,15 +90,17 @@ if (-not $serialNumber) { 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. +# 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', @@ -101,43 +110,115 @@ foreach ($regPath in @( try { if (Test-Path $regPath) { $v = [string](Get-ItemProperty -Path $regPath -Name MachineNo -ErrorAction Stop).MachineNo - if ($v) { $machineNo = $v.Trim() } + 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 = Get-Content -LiteralPath $mnFile -First 1 -ErrorAction Stop - if ($v) { $machineNo = ([string]$v).Trim(); Log "machineNo from $mnFile (eDNC registry empty): $machineNo" } + $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 caption for the operatingsystems lookup. +# 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 { - $osVersion = (Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop).Caption - if ($osVersion) { $osVersion = $osVersion.Trim() } + $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 {} -# gather corp NICs only. one networkInterfaces entry per allowed IPv4. +# 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) { - if (-not (Test-InAllowedRange $ipo.IPAddress)) { continue } + $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 = '' + $mac = $adapter.MacAddress $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 @@ -149,13 +230,15 @@ try { [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 = $false # corp NIC, not the controller LAN + IsMachineNetwork = (-not $isCorp) # controller/machine LAN = true; corp = false } } } catch { @@ -163,7 +246,7 @@ try { } if ($interfaces.Count -eq 0) { - Log 'WARN no corp-range IPv4 found; posting identity without interfaces.' + Log 'WARN no physical IPv4 NIC found; posting identity without interfaces.' } $networkInterfacesJson = if ($interfaces.Count -gt 0) { $interfaces | ConvertTo-Json -Compress -Depth 4 } else { '' } @@ -174,14 +257,18 @@ $body = @{ action = 'updateCompleteAsset' hostname = $hostname serialNumber = $serialNumber - pcType = 'Shopfloor' osVersion = $osVersion networkInterfaces = $networkInterfacesJson } -if ($machineNo) { $body['machineNo'] = $machineNo } +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} machineNo={3} ips={4}" -f ` - $ApiUrl, $hostname, $serialNumber, $machineNo, (($interfaces | ForEach-Object { $_.IPAddress }) -join ',')) +Log ("POST {0} host={1} serial={2} pcType={3} make={4} model={5} machineNo={6} user={7} ips={8}" -f ` + $ApiUrl, $hostname, $serialNumber, $pcType, $manufacturer, $model, $machineNo, $loggedInUser, (($interfaces | ForEach-Object { $_.IPAddress }) -join ',')) try { $resp = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $body -TimeoutSec $TimeoutSec -ErrorAction Stop