computers: declare subordinate devices instead of coding each one
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 6s

A PC that drives a device which is its own asset had been implemented twice.
METROLOGY_TOOL_MAP covered CMM, Keyence, Genspect and wax-trace, minting a
measuring_tool. A separate path keyed on one hardcoded pc-type minted a Part
Marker machine and filed it under its operation. Both create a device, link the
PC with controls, and archive that link when the PC is re-imaged: one mechanism
with different nouns, written out twice because the second case arrived later.

That is the same trap as the site literals in ADR-015 - a pattern implemented
per instance rather than declared - and it has a known next occurrence. Part
markers already share operation numbers, and any site with two marking lasers
or two wax-trace units on one number needs identical treatment.

One SUBORDINATE_DEVICE_MAP now declares asset type, type name, naming suffix,
whether the device files partof the operation, and the relationship label. The
labels are unchanged per case on purpose: those values are in the production
database and only rows carrying them are archived by a collector push. A site
overrides or adds an entry through subordinatedevice_<pctype> settings, per
ADR-015, so the next case needs no code. A malformed override falls back to the
default rather than failing the push, because a bad setting must not stop a bay
reporting its inventory.

metrology_tool_for stays as a shim over the same map: filters.py and the older
tests read it, and unifying must not change what it returns. A test pins that.

Also adds flask relationships check-shared-machines, which finds the next 0615
rather than waiting for someone to notice duplicate backups. Several devices
legitimately sharing a number and two PCs mis-numbered at imaging look the same
from outside; the difference is whether child assets exist, so that is what it
reports. Read-only.
This commit is contained in:
cproudlock
2026-08-11 11:13:12 -04:00
parent 91143d94fb
commit c90ebcbc7c
5 changed files with 280 additions and 53 deletions

View File

@@ -728,9 +728,13 @@ class ComputersPlugin(BasePlugin):
ordinary machine link to run.
"""
from shopdb.api import AssetRelationship, RelationshipType, Asset
from .pctypemap import drives_partmarker, PARTMARKER_TYPENAME
from .pctypemap import subordinate_device_for
if not drives_partmarker(pctype) or not comp or not comp.asset:
spec = subordinate_device_for(pctype)
if not spec or not spec.get('partof') or not comp or not comp.asset:
# Only a device that FILES UNDER an operation goes through here.
# A measuring tool is a subordinate device too, but it does not
# share a machine number, so it keeps the simpler path.
return []
pcasset = comp.asset
@@ -744,15 +748,18 @@ class ComputersPlugin(BasePlugin):
try:
from plugins.machines.models import Machine, MachineType
except ImportError:
warnings.append('machines plugin unavailable; part marker skipped')
warnings.append('machines plugin unavailable; {} device skipped'
.format(spec['typename']))
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.
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.
existing = AssetRelationship.query.filter(
AssetRelationship.sourceassetid == pcasset.assetid,
AssetRelationship.relationshiptypeid == controls.relationshiptypeid,
AssetRelationship.label == PARTMARKER_LINK_ORIGIN,
AssetRelationship.label == label,
).all()
reuse = next((rel for rel in existing if rel.isactive), None) \
or (existing[0] if existing else None)
@@ -761,35 +768,36 @@ class ComputersPlugin(BasePlugin):
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')
coretype = AssetType.query.filter_by(
assettype=spec['assettype']).first()
if not coretype:
warnings.append('{} asset type missing; {} skipped'.format(
spec['assettype'], spec['typename']))
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)
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()
hostname = comp.hostname
markerasset = Asset(
assetnumber='{}-PARTMARKER'.format(
pcasset.assetnumber or hostname),
name='Part Marker ({})'.format(hostname),
assettypeid=machinetype.assettypeid,
assetnumber='{}-{}'.format(
pcasset.assetnumber or hostname, spec['suffix']),
name='{} ({})'.format(spec['typename'], hostname),
assettypeid=coretype.assettypeid,
statusid=1)
db.session.add(markerasset)
db.session.flush()
db.session.add(Machine(assetid=markerasset.assetid,
machinetypeid=markertype.machinetypeid))
machinetypeid=devicetype.machinetypeid))
db.session.add(AssetRelationship(
sourceassetid=pcasset.assetid,
targetassetid=markerasset.assetid,
relationshiptypeid=controls.relationshiptypeid,
label=PARTMARKER_LINK_ORIGIN,
label=label,
isactive=True))
# One marker per PC: archive any other collector marker link.
@@ -798,14 +806,14 @@ class ComputersPlugin(BasePlugin):
rel.isactive = False
operation = self._link_marker_to_operation(
markerasset, machinenumber, pcasset, warnings)
markerasset, machinenumber, pcasset, label, warnings)
return [{'assetid': markerasset.assetid,
'assetnumber': markerasset.assetnumber,
'operationassetid': operation}]
def _link_marker_to_operation(self, markerasset, machinenumber, pcasset,
warnings):
label, 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
@@ -852,7 +860,7 @@ class ComputersPlugin(BasePlugin):
links = AssetRelationship.query.filter(
AssetRelationship.sourceassetid == markerasset.assetid,
AssetRelationship.relationshiptypeid == partof.relationshiptypeid,
AssetRelationship.label == PARTMARKER_LINK_ORIGIN,
AssetRelationship.label == label,
).all()
found = None
for rel in links:
@@ -866,7 +874,7 @@ class ComputersPlugin(BasePlugin):
sourceassetid=markerasset.assetid,
targetassetid=operation.assetid,
relationshiptypeid=partof.relationshiptypeid,
label=PARTMARKER_LINK_ORIGIN,
label=label,
isactive=True))
return operation.assetid