Part markers become their own assets under the operation they serve
This commit is contained in:
@@ -34,6 +34,10 @@ MEASURINGTOOL_LINK_ORIGIN = 'collector:measuringtool'
|
||||
# touched.
|
||||
MACHINE_LINK_ORIGIN = 'collector:machine'
|
||||
|
||||
# Marker stamped on both links a part-marker PC produces: PC controls marker,
|
||||
# and marker partof the operation it serves. Same archive discipline again.
|
||||
PARTMARKER_LINK_ORIGIN = 'collector:partmarker'
|
||||
|
||||
# How long a PC holding a machine may go without reporting before a second PC
|
||||
# claiming that machine is treated as its replacement. A swap resolves itself
|
||||
# within a day; a PC off overnight or behind a network outage keeps its bay.
|
||||
@@ -338,8 +342,19 @@ class ComputersPlugin(BasePlugin):
|
||||
# Remote-access protocol sync (only when the payload carried the key).
|
||||
accessprotocols = self._sync_access_protocols(comp, payload, warnings)
|
||||
|
||||
# Part-marker PCs get a marker asset of their own, which is what the
|
||||
# machine number then hangs off. Done BEFORE the machine link because a
|
||||
# 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.
|
||||
partmarkers = self._sync_partmarker(comp, pctype, machinenumber,
|
||||
warnings)
|
||||
|
||||
# PC -> machine link from the reported machine number.
|
||||
machinelinks = self._sync_machine_link(comp, machinenumber, warnings)
|
||||
if partmarkers:
|
||||
machinelinks = []
|
||||
else:
|
||||
machinelinks = self._sync_machine_link(comp, machinenumber, warnings)
|
||||
|
||||
# Printer relationship sync (only when the payload carried printer data).
|
||||
printerlinks = self._sync_printer_links(comp.asset, payload, warnings)
|
||||
@@ -362,6 +377,7 @@ class ComputersPlugin(BasePlugin):
|
||||
'measuringtoollinkcount': len(measuringtoollinks),
|
||||
'accessprotocols': accessprotocols,
|
||||
'machinelinks': machinelinks,
|
||||
'partmarkers': partmarkers,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -687,6 +703,159 @@ class ComputersPlugin(BasePlugin):
|
||||
'machinenumber': machine.assetnumber,
|
||||
'superseded': len(held)}]
|
||||
|
||||
def _sync_partmarker(self, comp, pctype, machinenumber, warnings):
|
||||
"""Give a part-marker PC a marker asset of its own, under its operation.
|
||||
|
||||
Several Telesis markers serve one operation number - 0613, 0615 and
|
||||
WJPRT each have more than one - so treating the operation as the marker
|
||||
collapsed separate devices into one record. Their configs, which differ
|
||||
by COM port, then overwrote each other in the backup history, and no
|
||||
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.
|
||||
|
||||
That last rail is why the PC does not also claim the operation directly:
|
||||
`controls` propagates through `partof` (seeded in reference-data), so
|
||||
control of the operation follows from controlling its marker, and two
|
||||
markers on one operation no longer contest a link that can only have one
|
||||
holder.
|
||||
|
||||
Returns [] for any PC that does not drive a marker, which leaves the
|
||||
ordinary machine link to run.
|
||||
"""
|
||||
from shopdb.api import AssetRelationship, RelationshipType, Asset
|
||||
from .pctypemap import drives_partmarker, PARTMARKER_TYPENAME
|
||||
|
||||
if not drives_partmarker(pctype) or not comp or not comp.asset:
|
||||
return []
|
||||
|
||||
pcasset = comp.asset
|
||||
controls = RelationshipType.query.filter_by(
|
||||
relationshiptype='controls').first()
|
||||
if not controls:
|
||||
warnings.append("'controls' relationship type missing; "
|
||||
'run flask seed reference-data')
|
||||
return []
|
||||
|
||||
try:
|
||||
from plugins.machines.models import Machine, MachineType
|
||||
except ImportError:
|
||||
warnings.append('machines plugin unavailable; part marker skipped')
|
||||
return []
|
||||
|
||||
# Reuse this PC's existing marker before minting one, so a re-image
|
||||
# never leaves a second marker behind for the same physical device.
|
||||
existing = AssetRelationship.query.filter(
|
||||
AssetRelationship.sourceassetid == pcasset.assetid,
|
||||
AssetRelationship.relationshiptypeid == controls.relationshiptypeid,
|
||||
AssetRelationship.label == PARTMARKER_LINK_ORIGIN,
|
||||
).all()
|
||||
reuse = next((rel for rel in existing if rel.isactive), None) \
|
||||
or (existing[0] if existing else None)
|
||||
|
||||
if reuse:
|
||||
reuse.isactive = True
|
||||
markerasset = db.session.get(Asset, reuse.targetassetid)
|
||||
else:
|
||||
machinetype = AssetType.query.filter_by(assettype='machine').first()
|
||||
if not machinetype:
|
||||
warnings.append('machine asset type missing; part marker '
|
||||
'skipped')
|
||||
return []
|
||||
markertype = MachineType.query.filter_by(
|
||||
machinetype=PARTMARKER_TYPENAME).first()
|
||||
if not markertype:
|
||||
markertype = MachineType(machinetype=PARTMARKER_TYPENAME,
|
||||
description='Telesis part marker')
|
||||
db.session.add(markertype)
|
||||
db.session.flush()
|
||||
|
||||
hostname = comp.hostname
|
||||
markerasset = Asset(
|
||||
assetnumber='{}-PARTMARKER'.format(
|
||||
pcasset.assetnumber or hostname),
|
||||
name='Part Marker ({})'.format(hostname),
|
||||
assettypeid=machinetype.assettypeid,
|
||||
statusid=1)
|
||||
db.session.add(markerasset)
|
||||
db.session.flush()
|
||||
db.session.add(Machine(assetid=markerasset.assetid,
|
||||
machinetypeid=markertype.machinetypeid))
|
||||
db.session.add(AssetRelationship(
|
||||
sourceassetid=pcasset.assetid,
|
||||
targetassetid=markerasset.assetid,
|
||||
relationshiptypeid=controls.relationshiptypeid,
|
||||
label=PARTMARKER_LINK_ORIGIN,
|
||||
isactive=True))
|
||||
|
||||
# One marker per PC: archive any other collector marker link.
|
||||
for rel in existing:
|
||||
if rel is not reuse and rel.isactive:
|
||||
rel.isactive = False
|
||||
|
||||
operation = self._link_marker_to_operation(
|
||||
markerasset, machinenumber, warnings)
|
||||
|
||||
return [{'assetid': markerasset.assetid,
|
||||
'assetnumber': markerasset.assetnumber,
|
||||
'operationassetid': operation}]
|
||||
|
||||
def _link_marker_to_operation(self, markerasset, machinenumber, warnings):
|
||||
"""Make a marker `partof` the operation whose number its PC reports.
|
||||
|
||||
Unlike the PC-to-machine link this does NOT contest: an operation can
|
||||
hold any number of markers, which is the whole point. Moving a marker to
|
||||
another operation archives the old membership rather than deleting it,
|
||||
so where a marker used to live stays answerable.
|
||||
"""
|
||||
from shopdb.api import AssetRelationship, RelationshipType, Asset
|
||||
|
||||
if not machinenumber:
|
||||
return None
|
||||
|
||||
partof = RelationshipType.query.filter_by(
|
||||
relationshiptype='partof').first()
|
||||
if not partof:
|
||||
warnings.append("'partof' relationship type missing; "
|
||||
'run flask seed reference-data')
|
||||
return None
|
||||
|
||||
operation = Asset.query.filter(
|
||||
Asset.assetnumber.ilike(machinenumber),
|
||||
Asset.isactive.is_(True)).first()
|
||||
if not operation:
|
||||
warnings.append(
|
||||
'no asset for machine number {!r}; marker not filed under an '
|
||||
'operation'.format(machinenumber))
|
||||
return None
|
||||
if operation.assetid == markerasset.assetid:
|
||||
return None
|
||||
|
||||
links = AssetRelationship.query.filter(
|
||||
AssetRelationship.sourceassetid == markerasset.assetid,
|
||||
AssetRelationship.relationshiptypeid == partof.relationshiptypeid,
|
||||
AssetRelationship.label == PARTMARKER_LINK_ORIGIN,
|
||||
).all()
|
||||
found = None
|
||||
for rel in links:
|
||||
if rel.targetassetid == operation.assetid:
|
||||
rel.isactive = True
|
||||
found = rel
|
||||
else:
|
||||
rel.isactive = False
|
||||
if found is None:
|
||||
db.session.add(AssetRelationship(
|
||||
sourceassetid=markerasset.assetid,
|
||||
targetassetid=operation.assetid,
|
||||
relationshiptypeid=partof.relationshiptypeid,
|
||||
label=PARTMARKER_LINK_ORIGIN,
|
||||
isactive=True))
|
||||
return operation.assetid
|
||||
|
||||
def _assetname(self, assetid):
|
||||
"""Readable name for an asset id, for warnings and alerts."""
|
||||
from shopdb.api import Asset
|
||||
|
||||
Reference in New Issue
Block a user