Let the server decide which NIC is a PC's address, and keep the rest
All checks were successful
CI / naming (push) Successful in 4s
CI / frontend (push) Successful in 12s
CI / backend (push) Successful in 4m15s
CI / migrations-mysql (push) Successful in 41s

A CMM reported 100.0.0.1 as its address and never reported 10.48.255.210 at
all. Both halves of that were the same mistake.

THE BAY WAS CHOOSING. The collector schema took a single `ipaddress`, so the
script picked one NIC and dropped the others - preferring an address inside a
configured range, else whichever interface carried a default route. A bay
cannot make that judgement: it does not know the site's addressing. On this
CMM the Marvell AQtion 10Gbit link to its controller is a real hardware NIC,
up, and carries a default route of its own, so it satisfied every test the
script had. It won, and the corporate address was discarded before the request
was even built - which is why it is not in ShopDB even as a second row.

So the bay now reports EVERY connected hardware NIC with a routable IPv4, and
takes no view. The server picks the primary from collector_corporate_ranges, a
new per-site setting, and writes a Communication row for each of the others.
Naming the ranges is what turns the answer from a guess into a decision; with
none set it still falls back to the default-route flag and then to the first
reported, both guesses, both better than before only because nothing is thrown
away now.

Rows are keyed on the MAC where the bay sends one, because an address moves
between adapters and a MAC does not. A NIC that is down at check-in is left
alone rather than deleted - being down is not evidence the record is wrong,
and hand-entered gateway, DNS and serial settings live on those same rows.
Exactly one row ends up flagged primary, and only a row this report touched
can claim it, so a check-in cannot silently demote an address set by hand.

-AllowedRanges and the CollectorRanges registry value are now accepted and
ignored, logging a line that says where the setting moved. The parameter stays
because a scheduled task still passing it would otherwise fail on an unknown
argument.

A bay on an older collector sends one address and no interfaces; that keeps
working and is pinned by a test.

Scope: product
This commit is contained in:
cproudlock
2026-08-31 17:40:21 -04:00
parent 241d8f40fc
commit c8e0de4dc1
3 changed files with 312 additions and 79 deletions

View File

@@ -31,10 +31,11 @@
#
# WHY ONE NIC ONLY:
# Some bays carry two NICs - a private controller NIC and the routable
# corporate NIC. Only the routable one belongs in ShopDB. A site may name its
# corporate ranges (-AllowedRanges, or the CollectorRanges registry value); with
# none configured the NIC carrying the DEFAULT ROUTE is used, which expresses
# the same intent without knowing any site's addressing.
# corporate NIC. This script reports ALL of them and lets the server decide
# which is the PC's address, from the collector_corporate_ranges setting.
# Choosing here was wrong: a bay cannot know a site's addressing, and a CMM
# whose 10Gbit controller link carries a default route reported the
# machine-side address as the PC's - with the corporate one never sent.
param(
# Flask collector endpoint for the computers plugin. Empty resolves from
@@ -90,18 +91,6 @@ if (-not $ApiUrl) {
exit 0
}
# A site may name its routable ranges rather than rely on the default route.
if (-not $AllowedRanges) {
foreach ($regPath in @('HKLM:\SOFTWARE\GE\ShopDB', 'HKLM:\SOFTWARE\WOW6432Node\GE\ShopDB')) {
try {
if (Test-Path $regPath) {
$v = [string](Get-ItemProperty -Path $regPath -Name CollectorRanges -ErrorAction Stop).CollectorRanges
if ($v -and $v.Trim()) { $AllowedRanges = $v.Trim(); break }
}
} catch {}
}
}
# collector key from the GE-Enforce secret store when not passed via Args.
if (-not $ApiKey) {
foreach ($regPath in @('HKLM:\SOFTWARE\GE\ShopDB', 'HKLM:\SOFTWARE\WOW6432Node\GE\ShopDB')) {
@@ -118,38 +107,13 @@ if (-not $ApiKey) {
exit 0
}
# Routable ranges, if this site named any. NO SITE ADDRESSING IS BAKED IN: an
# unconfigured site falls through to the default-route NIC below (ADR-015).
#
# NOT named $allowedRanges: PowerShell variable names are case-insensitive, so
# that collides with the [string] parameter above and the array is silently
# COERCED to a string. .Count on a string is 1, so the script then believes a
# range is configured and never falls back to the default route.
$rangeList = @()
foreach ($cidr in ($AllowedRanges -split ',')) {
$cidr = $cidr.Trim()
if (-not $cidr) { continue }
$parts = $cidr -split '/'
if ($parts.Count -ne 2) { Log "WARN ignoring malformed range '$cidr'"; continue }
$rangeList += @{ Network = $parts[0].Trim(); PrefixLen = [int]$parts[1] }
}
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 $rangeList) {
$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
# -AllowedRanges and the CollectorRanges registry value are ACCEPTED AND
# IGNORED. Deciding which NIC is the PC's address moved to the server, where
# the site's addressing actually lives: this script now reports every NIC and
# takes no view. The parameter stays so a scheduled task still passing it does
# not fail on an unknown argument.
if ($AllowedRanges) {
Log 'NOTE -AllowedRanges is ignored; set collector_corporate_ranges in ShopDB settings instead.'
}
Log '=== Report asset to ShopDB (collector) ==='
@@ -299,42 +263,48 @@ try {
Log "WARN could not read make/model: $($_.Exception.Message)"
}
# The routable IPv4 - a physical, connected NIC. The collector schema takes a
# single ipaddress string, so report the corporate NIC and drop any
# controller/machine-LAN NIC. With ranges configured the IP must be in one;
# with none, the NIC carrying the default route is the routable one by
# definition, which needs no knowledge of a site's addressing.
$corpIp = ''
# EVERY connected hardware NIC with a routable IPv4, and no opinion about
# which one matters. Choosing used to happen here and this was the wrong place
# for it: a bay cannot know a site's addressing. A CMM whose Marvell AQtion
# 10Gbit link to its controller sits on 100.0.0.1 reported THAT as the PC's
# address - the link carries a default route too - and the corporate address
# was never sent at all, so ShopDB could not even show it as a second row.
#
# The server picks the primary now, from the site's configured corporate
# ranges, and records the rest. Report what is here; let it decide.
$interfaces = @()
$defaultRouteIfIndexes = @()
if ($rangeList.Count -eq 0) {
try {
$defaultRouteIfIndexes = @(Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction Stop |
Sort-Object RouteMetric |
Select-Object -ExpandProperty InterfaceIndex -Unique)
} catch {
Log "WARN could not read the route table: $($_.Exception.Message)"
}
try {
$defaultRouteIfIndexes = @(Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction Stop |
Select-Object -ExpandProperty InterfaceIndex -Unique)
} catch {
Log "WARN could not read the route table: $($_.Exception.Message)"
}
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 ($corpIp) { break }
$adapter = $null
try { $adapter = Get-NetAdapter -InterfaceIndex $ipo.InterfaceIndex -ErrorAction Stop } catch {}
if (-not $adapter) { continue }
if (-not $adapter.HardwareInterface) { continue }
if ($adapter.Status -ne 'Up') { continue }
if ($rangeList.Count -gt 0) {
if (Test-InAllowedRange $ipo.IPAddress) { $corpIp = $ipo.IPAddress }
} elseif ($defaultRouteIfIndexes -contains $ipo.InterfaceIndex) {
$corpIp = $ipo.IPAddress
$interfaces += @{
ipaddress = $ipo.IPAddress
macaddress = $adapter.MacAddress
adaptername = $adapter.InterfaceDescription
isdefaultroute = [bool]($defaultRouteIfIndexes -contains $ipo.InterfaceIndex)
}
}
} catch {
Log "WARN interface enumeration failed: $($_.Exception.Message)"
}
if (-not $corpIp) { Log 'WARN no routable IPv4 NIC found; posting identity without ipaddress.' }
if ($interfaces.Count -eq 0) {
Log 'WARN no routable IPv4 NIC found; posting identity without any interface.'
} else {
Log ("Reporting {0} NIC(s): {1}" -f $interfaces.Count,
(($interfaces | ForEach-Object { $_.ipaddress }) -join ', '))
}
# collector schema fields (lowercase concatenated). hostname is required; the
# rest are sent only when present so a partial read never blanks a good value.
@@ -348,7 +318,7 @@ if ($model) { $body['modelnumber'] = $model }
if ($osVersion) { $body['osname'] = $osVersion }
if ($lastBootTime) { $body['lastboottime'] = $lastBootTime }
if ($loggedInUser) { $body['loggedinuser'] = $loggedInUser }
if ($corpIp) { $body['ipaddress'] = $corpIp }
if ($interfaces.Count -gt 0) { $body['interfaces'] = $interfaces }
$body['lastcheckin'] = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss')
$json = $body | ConvertTo-Json -Compress -Depth 4