Device identity, collector enrollment file, and network device fixes
This commit is contained in:
@@ -172,6 +172,7 @@ try {
|
||||
# 2. C:\Enrollment\cmm\cmmid.txt - CMM bay id (e.g. CMM3).
|
||||
# 3. C:\Enrollment\machine-number.txt - imaging value.
|
||||
# keyence / genspect / part-marker have no per-bay id -> no machineNo sent.
|
||||
# Those bays name their DEVICE instead, in asset-id.txt below.
|
||||
$machineNo = ''
|
||||
foreach ($regPath in @(
|
||||
'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General',
|
||||
@@ -236,6 +237,45 @@ try {
|
||||
Log "WARN could not read logged-in user: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# The DEVICE hanging off this PC - the ONE enrollment file for every bay that
|
||||
# has no NTLARS/eDNC MachineNo registry to identify it. It names the asset and
|
||||
# nothing else: it does not say what kind of device that is, because the pc-type
|
||||
# already does, so the same file serves a part marker, a Keyence, a Genspect or
|
||||
# anything added later without a new file or a client change.
|
||||
#
|
||||
# Deliberately NOT machine-number.txt. machinenumber answers "which bay is
|
||||
# this" and is what GE-Enforce TargetMachineNumbers gates on, so naming a device
|
||||
# there would silently stop every bay-gated manifest entry from matching.
|
||||
#
|
||||
# WHY A FILE AT ALL: every other identity the server can fall back to is derived
|
||||
# from the PC - a prior link from this PC, or an asset number built from this
|
||||
# hostname - so none of them survive a PC SWAP. The replacement box mints a
|
||||
# SECOND record for the same physical device while the first keeps its history
|
||||
# under a dead PC's name. This file is what survives.
|
||||
#
|
||||
# The server ADOPTS the named asset, refuses a value it cannot resolve, and
|
||||
# refuses one that resolves to the wrong kind of thing - so a typo warns instead
|
||||
# of inventing a phantom device.
|
||||
$deviceId = ''
|
||||
$idFile = 'C:\Enrollment\asset-id.txt'
|
||||
if (Test-Path -LiteralPath $idFile) {
|
||||
try {
|
||||
$deviceId = ([string](Get-Content -LiteralPath $idFile -First 1 -ErrorAction Stop)).Trim()
|
||||
if ($deviceId) { Log "deviceId from ${idFile}: $deviceId" }
|
||||
} catch { Log "WARN could not read ${idFile}: $($_.Exception.Message)" }
|
||||
}
|
||||
# 0.12.0 shipped measuringtool-id.txt before the file was generalised. Read it
|
||||
# as a fallback so a bay already staged with one keeps reporting.
|
||||
if (-not $deviceId) {
|
||||
$mtFile = 'C:\Enrollment\measuringtool-id.txt'
|
||||
if (Test-Path -LiteralPath $mtFile) {
|
||||
try {
|
||||
$deviceId = ([string](Get-Content -LiteralPath $mtFile -First 1 -ErrorAction Stop)).Trim()
|
||||
if ($deviceId) { Log "deviceId from ${mtFile} (pre-0.13 name): $deviceId" }
|
||||
} catch { Log "WARN could not read ${mtFile}: $($_.Exception.Message)" }
|
||||
}
|
||||
}
|
||||
|
||||
# imaging pc-type (gea-shopfloor-*), read from the enrollment file. Sent only
|
||||
# when present; absent -> server leaves existing pctype untouched (a bare report
|
||||
# never re-types a PC). Unmapped values return a warning, not an error.
|
||||
@@ -302,6 +342,7 @@ $body = @{ hostname = $hostname }
|
||||
if ($serialNumber) { $body['serialnumber'] = $serialNumber }
|
||||
if ($machineNo) { $body['machinenumber'] = $machineNo }
|
||||
if ($pcType) { $body['pctype'] = $pcType }
|
||||
if ($deviceId) { $body['deviceid'] = $deviceId }
|
||||
if ($manufacturer) { $body['vendorname'] = $manufacturer }
|
||||
if ($model) { $body['modelnumber'] = $model }
|
||||
if ($osVersion) { $body['osname'] = $osVersion }
|
||||
@@ -312,8 +353,8 @@ $body['lastcheckin'] = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:s
|
||||
|
||||
$json = $body | ConvertTo-Json -Compress -Depth 4
|
||||
|
||||
Log ("POST {0} host={1} serial={2} pcType={3} make={4} model={5} os={6} boot={7} machineNo={8} user={9} ip={10}" -f `
|
||||
$ApiUrl, $hostname, $serialNumber, $pcType, $manufacturer, $model, $osVersion, $lastBootTime, $machineNo, $loggedInUser, $corpIp)
|
||||
Log ("POST {0} host={1} serial={2} pcType={3} make={4} model={5} os={6} boot={7} machineNo={8} user={9} ip={10} deviceId={11}" -f `
|
||||
$ApiUrl, $hostname, $serialNumber, $pcType, $manufacturer, $model, $osVersion, $lastBootTime, $machineNo, $loggedInUser, $corpIp, $deviceId)
|
||||
|
||||
try {
|
||||
$resp = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $json `
|
||||
|
||||
@@ -106,12 +106,24 @@ class ComputersPlugin(BasePlugin):
|
||||
'fields': {
|
||||
'hostname': {'type': 'string', 'required': True},
|
||||
'machinenumber': {'type': 'string'},
|
||||
# The instrument this PC drives, named by the enrollment file
|
||||
# measuringtool-id.txt. SEPARATE from machinenumber on purpose:
|
||||
# "which bay is this" and "which instrument is this" are
|
||||
# different facts, and machinenumber is what GE-Enforce
|
||||
# TargetMachineNumbers gates on - repointing it at a tool would
|
||||
# silently stop every bay-gated manifest entry matching.
|
||||
# The DEVICE hanging off this PC, named by the enrollment file
|
||||
# asset-id.txt. One file for every bay that has no NTLARS/eDNC
|
||||
# MachineNo registry to identify it, and it does NOT say what
|
||||
# kind of device it is: the pc-type already does, through
|
||||
# SUBORDINATE_DEVICE_MAP.
|
||||
#
|
||||
# SEPARATE from machinenumber on purpose: "which bay is this"
|
||||
# and "which device is this" are different facts, and
|
||||
# machinenumber is what GE-Enforce TargetMachineNumbers gates
|
||||
# on - repointing it at a device would silently stop every
|
||||
# bay-gated manifest entry matching.
|
||||
#
|
||||
# Named deviceid rather than assetid because assetid is this
|
||||
# contract's own RESPONSE field for a PC's integer primary key.
|
||||
'deviceid': {'type': 'string'},
|
||||
# Shipped in 0.12.0, before the file was generalised. Accepted
|
||||
# as an alias so a bay staged with measuringtool-id.txt keeps
|
||||
# reporting; deviceid wins when both arrive.
|
||||
'measuringtoolid': {'type': 'string'},
|
||||
'pctype': {'type': 'string'},
|
||||
'pcsubtype': {'type': 'string'},
|
||||
@@ -394,8 +406,14 @@ class ComputersPlugin(BasePlugin):
|
||||
# marker PC must not also claim the operation directly: several markers
|
||||
# serve one operation number, so direct claims would fight over it. The
|
||||
# marker is partof the operation and control propagates along that rail.
|
||||
# asset-id.txt, falling back to the 0.12.0 field name. One value feeds
|
||||
# BOTH device paths: the file names the device and nothing else, so
|
||||
# which sync consumes it follows from the pc-type.
|
||||
deviceid = (payload.get('deviceid')
|
||||
or payload.get('measuringtoolid') or '').strip() or None
|
||||
|
||||
partmarkers = self._sync_partmarker(comp, pctype, machinenumber,
|
||||
warnings)
|
||||
warnings, deviceid=deviceid)
|
||||
|
||||
# PC -> machine link from the reported machine number.
|
||||
if partmarkers:
|
||||
@@ -410,7 +428,7 @@ class ComputersPlugin(BasePlugin):
|
||||
# get an attached MeasuringTool asset auto-created and linked.
|
||||
measuringtoollinks = self._sync_measuringtool_link(
|
||||
comp.asset, pctype, hostname, warnings,
|
||||
measuringtoolid=payload.get('measuringtoolid'),
|
||||
measuringtoolid=deviceid,
|
||||
machinenumber=machinenumber)
|
||||
|
||||
db.session.commit()
|
||||
@@ -750,7 +768,70 @@ class ComputersPlugin(BasePlugin):
|
||||
'machinenumber': machine.assetnumber,
|
||||
'superseded': len(held)}]
|
||||
|
||||
def _sync_partmarker(self, comp, pctype, machinenumber, warnings):
|
||||
def _is_device_of_type(self, asset, spec):
|
||||
"""Is this asset the kind of device the pc-type's spec describes?
|
||||
|
||||
asset-id.txt names a device and says nothing about its type, so the
|
||||
type is what proves the file points somewhere sensible. A measuring
|
||||
tool is checked against the measuring-tool set; anything else is
|
||||
checked against its machine type, which is where a Part Marker lives.
|
||||
"""
|
||||
if asset is None:
|
||||
return False
|
||||
if spec.get('assettype') == 'measuring_tool':
|
||||
return asset.assetid in self._measuringtool_assetids()
|
||||
try:
|
||||
from plugins.machines.models import Machine, MachineType
|
||||
except ImportError:
|
||||
return False
|
||||
row = (db.session.query(MachineType.machinetype)
|
||||
.join(Machine, Machine.machinetypeid == MachineType.machinetypeid)
|
||||
.filter(Machine.assetid == asset.assetid).first())
|
||||
return bool(row) and row[0] == spec['typename']
|
||||
|
||||
def _ensure_device_rows(self, deviceasset, spec, pcasset, controls, label,
|
||||
warnings):
|
||||
"""Get-or-create the extension row and the PC -> device control link.
|
||||
|
||||
Shared by the named-device and minted paths: an adopted asset may have
|
||||
no extension row, and the control link may already exist under another
|
||||
label, so both are get-or-create rather than insert.
|
||||
"""
|
||||
from shopdb.api import AssetRelationship
|
||||
try:
|
||||
from plugins.machines.models import Machine, MachineType
|
||||
except ImportError:
|
||||
warnings.append('machines plugin unavailable; {} device skipped'
|
||||
.format(spec['typename']))
|
||||
return
|
||||
if not Machine.query.filter_by(assetid=deviceasset.assetid).first():
|
||||
devicetype = MachineType.query.filter_by(
|
||||
machinetype=spec['typename']).first()
|
||||
if not devicetype:
|
||||
devicetype = MachineType(machinetype=spec['typename'],
|
||||
description=spec.get('description'))
|
||||
db.session.add(devicetype)
|
||||
db.session.flush()
|
||||
db.session.add(Machine(assetid=deviceasset.assetid,
|
||||
machinetypeid=devicetype.machinetypeid))
|
||||
controlrow = AssetRelationship.query.filter_by(
|
||||
sourceassetid=pcasset.assetid,
|
||||
targetassetid=deviceasset.assetid,
|
||||
relationshiptypeid=controls.relationshiptypeid).first()
|
||||
if controlrow is not None:
|
||||
controlrow.isactive = True
|
||||
if not controlrow.label:
|
||||
controlrow.label = label
|
||||
else:
|
||||
db.session.add(AssetRelationship(
|
||||
sourceassetid=pcasset.assetid,
|
||||
targetassetid=deviceasset.assetid,
|
||||
relationshiptypeid=controls.relationshiptypeid,
|
||||
label=label,
|
||||
isactive=True))
|
||||
|
||||
def _sync_partmarker(self, comp, pctype, machinenumber, warnings,
|
||||
deviceid=None):
|
||||
"""Give a part-marker PC a marker asset of its own, under its operation.
|
||||
|
||||
Several Telesis markers serve one operation number - 0613, 0615 and
|
||||
@@ -760,10 +841,19 @@ class ComputersPlugin(BasePlugin):
|
||||
question about an individual marker (how many are there, which port,
|
||||
which one failed) could be asked at all.
|
||||
|
||||
One marker per PC, so the PC identifies the marker and the collector can
|
||||
mint it the same way it already mints a CMM or a Keyence unit for a
|
||||
metrology PC. The marker is a machine asset of type Part Marker, the PC
|
||||
`controls` it, and the marker is `partof` the operation it serves.
|
||||
One marker per PC. The marker is a machine asset of type Part Marker,
|
||||
the PC `controls` it, and the marker is `partof` the operation it
|
||||
serves.
|
||||
|
||||
IDENTITY COMES FROM asset-id.txt FIRST, because everything else here is
|
||||
derived from the PC and a PC is not permanent. Reuse looks for a prior
|
||||
link from THIS PC asset and adoption looks up `<PC number>-PARTMARKER`,
|
||||
so both survive a re-image and neither survives a PC SWAP: a new
|
||||
hostname is a new PC asset with no prior link and a predicted number
|
||||
that has never existed, so the same physical Telesis unit gets a second
|
||||
record while the first keeps its config and backup history under a dead
|
||||
PC's name. That is the failure the metrology path already learned from,
|
||||
where minting from the hostname left 43 instruments shadowed by twins.
|
||||
|
||||
That last rail is why the PC does not also claim the operation directly:
|
||||
`controls` propagates through `partof` (seeded in reference-data), so
|
||||
@@ -801,8 +891,26 @@ class ComputersPlugin(BasePlugin):
|
||||
|
||||
label = spec['label']
|
||||
|
||||
# Reuse this PC's existing device before minting one, so a re-image
|
||||
# never leaves a second device behind for the same physical unit.
|
||||
# --- 1. an explicitly named device wins over everything -------------
|
||||
named = (deviceid or '').strip()
|
||||
namedasset = None
|
||||
if named:
|
||||
candidate = self._asset_by_number(named)
|
||||
if candidate is None:
|
||||
# Warn rather than invent: a typo must not mint a phantom
|
||||
# device that nobody can account for.
|
||||
warnings.append(
|
||||
'no asset for device {!r}; not linked'.format(named))
|
||||
elif not self._is_device_of_type(candidate, spec):
|
||||
# The name resolved, but not to this pc-type's device. Refuse
|
||||
# and say which asset it hit, rather than filing an unrelated
|
||||
# asset under a device label the collector also owns.
|
||||
warnings.append(
|
||||
'asset {!r} is not a {}; not linked'.format(
|
||||
candidate.assetnumber, spec['typename']))
|
||||
else:
|
||||
namedasset = candidate
|
||||
|
||||
existing = AssetRelationship.query.filter(
|
||||
AssetRelationship.sourceassetid == pcasset.assetid,
|
||||
AssetRelationship.relationshiptypeid == controls.relationshiptypeid,
|
||||
@@ -811,7 +919,18 @@ class ComputersPlugin(BasePlugin):
|
||||
reuse = next((rel for rel in existing if rel.isactive), None) \
|
||||
or (existing[0] if existing else None)
|
||||
|
||||
if reuse:
|
||||
if namedasset is not None:
|
||||
# The file is authoritative. A PC that previously minted its own
|
||||
# marker and is now told the real one keeps only the named link;
|
||||
# the stale link is archived by the one-marker-per-PC sweep below.
|
||||
markerasset = namedasset
|
||||
reuse = next((rel for rel in existing
|
||||
if rel.targetassetid == namedasset.assetid), None)
|
||||
if reuse is not None:
|
||||
reuse.isactive = True
|
||||
self._ensure_device_rows(markerasset, spec, pcasset, controls,
|
||||
label, warnings)
|
||||
elif reuse:
|
||||
reuse.isactive = True
|
||||
markerasset = db.session.get(Asset, reuse.targetassetid)
|
||||
else:
|
||||
@@ -852,26 +971,8 @@ class ComputersPlugin(BasePlugin):
|
||||
db.session.add(markerasset)
|
||||
db.session.flush()
|
||||
|
||||
# The extension row may be missing on an adopted asset, and the link
|
||||
# may already exist under another label - both get-or-create.
|
||||
if not Machine.query.filter_by(assetid=markerasset.assetid).first():
|
||||
db.session.add(Machine(assetid=markerasset.assetid,
|
||||
machinetypeid=devicetype.machinetypeid))
|
||||
controlrow = AssetRelationship.query.filter_by(
|
||||
sourceassetid=pcasset.assetid,
|
||||
targetassetid=markerasset.assetid,
|
||||
relationshiptypeid=controls.relationshiptypeid).first()
|
||||
if controlrow is not None:
|
||||
controlrow.isactive = True
|
||||
if not controlrow.label:
|
||||
controlrow.label = label
|
||||
else:
|
||||
db.session.add(AssetRelationship(
|
||||
sourceassetid=pcasset.assetid,
|
||||
targetassetid=markerasset.assetid,
|
||||
relationshiptypeid=controls.relationshiptypeid,
|
||||
label=label,
|
||||
isactive=True))
|
||||
self._ensure_device_rows(markerasset, spec, pcasset, controls,
|
||||
label, warnings)
|
||||
|
||||
# One marker per PC: archive any other collector marker link.
|
||||
for rel in existing:
|
||||
|
||||
Reference in New Issue
Block a user