- 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>
194 lines
7.3 KiB
PowerShell
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
|