Device identity, collector enrollment file, and network device fixes
This commit is contained in:
@@ -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