One enrollment file names the device, for every bay without a registry
A bay with an NTLARS/eDNC MachineNo registry is identified by it, and a CMM by cmmid.txt. Everything else - Keyence, Genspect, wax-trace, part markers, and whatever a site declares next - had no stable identity at all, and the two mechanisms that stood in for one both key off the PC: reuse looks for a prior link from THIS PC asset, and minting builds `<PC number>-<SUFFIX>` where a PC's number is its hostname. Both survive a re-image. 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 device gets a second record while the first keeps its config and its backup history under a dead PC's name. That is how 43 legacy MT-#### tools ended up shadowed by minted twins, three records deep in places. The metrology path learned this and gained an explicit id file; the part-marker path was modelled on the metrology path as it stood BEFORE that fix, so it inherited the defect - and its own docstring said so, describing minting "the same way it already mints a CMM". C:\Enrollment\asset-id.txt is now that identity for both, and for anything declared later through subordinatedevice_<pctype>. It holds one line, the device's assetnumber, and deliberately does NOT record what kind of device it is: the pc-type already does, so a new device type needs no new file and no client change. Resolution puts it first, ahead of everything PC-derived. 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. A part marker still files partof the operation from machinenumber; asset-id.txt changes which marker the PC controls, not which operation that marker belongs to. The wire field is `deviceid` rather than `assetid`, because assetid is already this contract's RESPONSE field for a PC's integer primary key - the two would have sat side by side meaning different things. measuringtoolid, shipped in 0.12.0, is accepted as an alias and the client still reads the older file, so a bay staged in the last day keeps reporting; deviceid wins when both arrive. Two guards, shared by both device families: a value that resolves to nothing warns instead of minting a phantom, and a value that resolves to the WRONG KIND of asset is refused with the asset named, so a machine number pasted into the file cannot be filed under a device label the collector also owns. Row creation is now one helper instead of a copy per path, because the named and minted branches both need get-or-create on the extension row and the control link. 1744 tests green, including a new file that pins the swap case both ways - with the file one device, without it two. Five of its tests failed first time because the fixtures built bare assets with no extension row and the type guard refused them, which is the guard working. VERIFIED ON WINDOWS 11 (build 26200), five cases: the new file alone; both files present, new winning; the legacy file alone; neither, sending no field; and a padded value with a trailing line.
This commit is contained in:
@@ -172,7 +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.
|
||||
# A metrology bay names its INSTRUMENT instead, in measuringtool-id.txt below.
|
||||
# Those bays name their DEVICE instead, in asset-id.txt below.
|
||||
$machineNo = ''
|
||||
foreach ($regPath in @(
|
||||
'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General',
|
||||
@@ -237,23 +237,43 @@ try {
|
||||
Log "WARN could not read logged-in user: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# The INSTRUMENT this PC drives, named by its own enrollment file. Deliberately
|
||||
# NOT machine-number.txt: machinenumber answers "which bay is this" and is what
|
||||
# GE-Enforce TargetMachineNumbers gates on, so pointing it at a tool would
|
||||
# silently stop every bay-gated manifest entry from matching. A Keyence or
|
||||
# Genspect bay has an instrument and no bay id, which is the case this exists
|
||||
# for; a CMM already reports its bay id through cmmid.txt and the server falls
|
||||
# back to that.
|
||||
# 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.
|
||||
#
|
||||
# The server ADOPTS the named instrument and refuses to mint one from a value it
|
||||
# cannot resolve, so a typo here warns instead of inventing a phantom tool.
|
||||
$measuringToolId = ''
|
||||
$mtFile = 'C:\Enrollment\measuringtool-id.txt'
|
||||
if (Test-Path -LiteralPath $mtFile) {
|
||||
# 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 {
|
||||
$measuringToolId = ([string](Get-Content -LiteralPath $mtFile -First 1 -ErrorAction Stop)).Trim()
|
||||
if ($measuringToolId) { Log "measuringToolId from ${mtFile}: $measuringToolId" }
|
||||
} catch { Log "WARN could not read ${mtFile}: $($_.Exception.Message)" }
|
||||
$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
|
||||
@@ -322,7 +342,7 @@ $body = @{ hostname = $hostname }
|
||||
if ($serialNumber) { $body['serialnumber'] = $serialNumber }
|
||||
if ($machineNo) { $body['machinenumber'] = $machineNo }
|
||||
if ($pcType) { $body['pctype'] = $pcType }
|
||||
if ($measuringToolId) { $body['measuringtoolid'] = $measuringToolId }
|
||||
if ($deviceId) { $body['deviceid'] = $deviceId }
|
||||
if ($manufacturer) { $body['vendorname'] = $manufacturer }
|
||||
if ($model) { $body['modelnumber'] = $model }
|
||||
if ($osVersion) { $body['osname'] = $osVersion }
|
||||
@@ -333,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} toolId={11}" -f `
|
||||
$ApiUrl, $hostname, $serialNumber, $pcType, $manufacturer, $model, $osVersion, $lastBootTime, $machineNo, $loggedInUser, $corpIp, $measuringToolId)
|
||||
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