Collector: machine number identifies the machine, not the PC; ntlars empty-value fix
This commit is contained in:
@@ -28,6 +28,12 @@ PRINTER_LINK_ORIGIN = 'collector:printers'
|
||||
# by a collector push, so hand-made tool links survive.
|
||||
MEASURINGTOOL_LINK_ORIGIN = 'collector:measuringtool'
|
||||
|
||||
# Marker stamped on the PC->machine "controls" link built from the reported
|
||||
# machine number. Same discipline as the two above: only rows carrying this
|
||||
# label are archived by a collector push, so a link made by hand is never
|
||||
# touched.
|
||||
MACHINE_LINK_ORIGIN = 'collector:machine'
|
||||
|
||||
|
||||
class ComputersPlugin(BasePlugin):
|
||||
"""
|
||||
@@ -153,8 +159,15 @@ class ComputersPlugin(BasePlugin):
|
||||
if not hostname:
|
||||
raise ValueError('hostname is required')
|
||||
|
||||
# Machine number is the business identifier (Asset.assetnumber). Skip
|
||||
# the imaging-time placeholder '9999' and fall back to hostname.
|
||||
# The machine number identifies the MACHINE, not this PC. It is
|
||||
# reported so the PC can be related to its machine; it is deliberately
|
||||
# NOT used as the PC's assetnumber. assets.assetnumber is uniquely
|
||||
# indexed and the machine already owns that value, so assigning it here
|
||||
# raised "Duplicate entry '3015' for key 'ix_assets_assetnumber'" and
|
||||
# returned 500 to the bay - forever, since every retry did the same
|
||||
# thing. The convention this restores is what the data already shows:
|
||||
# of 289 computers, none has a numeric assetnumber and 214 use their
|
||||
# hostname.
|
||||
machinenumber = (payload.get('machinenumber') or '').strip()
|
||||
if machinenumber in ('', '9999'):
|
||||
machinenumber = None
|
||||
@@ -169,7 +182,7 @@ class ComputersPlugin(BasePlugin):
|
||||
atype = AssetType.query.filter_by(assettype='computer').first()
|
||||
# statusid=1 is the first seeded asset status ("In Use"); a
|
||||
# collector-discovered PC is by definition in use.
|
||||
asset = Asset(assetnumber=machinenumber or hostname,
|
||||
asset = Asset(assetnumber=hostname,
|
||||
assettypeid=atype.assettypeid, statusid=1)
|
||||
db.session.add(asset)
|
||||
db.session.flush()
|
||||
@@ -177,8 +190,10 @@ class ComputersPlugin(BasePlugin):
|
||||
db.session.add(comp)
|
||||
db.session.flush()
|
||||
action = 'created'
|
||||
elif machinenumber and comp.asset:
|
||||
comp.asset.assetnumber = machinenumber
|
||||
# NOTE: an existing PC's assetnumber is left alone. Overwriting it with
|
||||
# the machine number renamed the PC onto the machine's identifier, which
|
||||
# either collided with the unique index or silently changed how that PC
|
||||
# is identified everywhere else.
|
||||
|
||||
comp.lastreporteddate = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
if payload.get('lastboottime'):
|
||||
@@ -277,6 +292,9 @@ class ComputersPlugin(BasePlugin):
|
||||
# Remote-access protocol sync (only when the payload carried the key).
|
||||
accessprotocols = self._sync_access_protocols(comp, payload, warnings)
|
||||
|
||||
# PC -> machine link from the reported machine number.
|
||||
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)
|
||||
|
||||
@@ -297,6 +315,7 @@ class ComputersPlugin(BasePlugin):
|
||||
'measuringtoollinks': measuringtoollinks,
|
||||
'measuringtoollinkcount': len(measuringtoollinks),
|
||||
'accessprotocols': accessprotocols,
|
||||
'machinelinks': machinelinks,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -494,6 +513,140 @@ class ComputersPlugin(BasePlugin):
|
||||
|
||||
# -- measuring-tool sync -----------------------------------------------
|
||||
|
||||
def _sync_machine_link(self, comp, machinenumber, warnings):
|
||||
"""Link a PC to the machine it drives, from the reported machine number.
|
||||
|
||||
Until this existed the machine number was collected and then discarded,
|
||||
so a replaced PC never took over its bay: the retired PC kept the link
|
||||
and the new one got none. Everything that walks PC->machine (the
|
||||
warranty machine column, the DNC info card) therefore pointed at
|
||||
hardware that had been pulled out.
|
||||
|
||||
ARCHIVES, never deletes. A superseded link stays with isactive=False so
|
||||
"which PC ran 3015 in June" is still answerable. Status on the old PC is
|
||||
deliberately NOT changed: the collector cannot tell whether it was
|
||||
shelved, broken or re-imaged for another bay, and guessing would
|
||||
overwrite whatever a person deliberately set.
|
||||
"""
|
||||
from shopdb.api import AssetRelationship, RelationshipType, Asset
|
||||
|
||||
pcasset = comp.asset if comp else None
|
||||
if not machinenumber or not pcasset:
|
||||
return []
|
||||
|
||||
controls = RelationshipType.query.filter_by(
|
||||
relationshiptype='controls').first()
|
||||
if not controls:
|
||||
warnings.append("'controls' relationship type missing; "
|
||||
'run flask seed reference-data')
|
||||
return []
|
||||
|
||||
machine = Asset.query.filter(
|
||||
Asset.assetnumber.ilike(machinenumber),
|
||||
Asset.isactive.is_(True)).first()
|
||||
if not machine:
|
||||
# Reported a machine ShopDB does not know. Warn rather than invent
|
||||
# an asset: a mistyped number would create a machine nobody can
|
||||
# account for.
|
||||
warnings.append(
|
||||
'no asset for machine number {!r}; PC not linked'.format(
|
||||
machinenumber))
|
||||
return []
|
||||
if machine.assetid == pcasset.assetid:
|
||||
return []
|
||||
|
||||
# This PC's own collector links: keep the one for the reported machine,
|
||||
# archive any other (the PC moved bays).
|
||||
mine = AssetRelationship.query.filter(
|
||||
AssetRelationship.sourceassetid == pcasset.assetid,
|
||||
AssetRelationship.relationshiptypeid == controls.relationshiptypeid,
|
||||
AssetRelationship.label == MACHINE_LINK_ORIGIN,
|
||||
).all()
|
||||
|
||||
link = None
|
||||
for rel in mine:
|
||||
if rel.targetassetid == machine.assetid:
|
||||
rel.isactive = True
|
||||
link = rel
|
||||
else:
|
||||
rel.isactive = False
|
||||
if link is None:
|
||||
link = AssetRelationship(
|
||||
sourceassetid=pcasset.assetid,
|
||||
targetassetid=machine.assetid,
|
||||
relationshiptypeid=controls.relationshiptypeid,
|
||||
label=MACHINE_LINK_ORIGIN,
|
||||
isactive=True)
|
||||
db.session.add(link)
|
||||
|
||||
# Any OTHER pc still collector-linked to this machine has been replaced.
|
||||
superseded = AssetRelationship.query.filter(
|
||||
AssetRelationship.targetassetid == machine.assetid,
|
||||
AssetRelationship.relationshiptypeid == controls.relationshiptypeid,
|
||||
AssetRelationship.label == MACHINE_LINK_ORIGIN,
|
||||
AssetRelationship.sourceassetid != pcasset.assetid,
|
||||
AssetRelationship.isactive.is_(True),
|
||||
).all()
|
||||
for rel in superseded:
|
||||
rel.isactive = False
|
||||
old = db.session.get(Asset, rel.sourceassetid)
|
||||
oldname = old.assetnumber if old else str(rel.sourceassetid)
|
||||
warnings.append(
|
||||
'machine {} was taken over from {}; check that PC'.format(
|
||||
machine.assetnumber, oldname))
|
||||
self._alert_pc_superseded(oldname, comp, machine)
|
||||
|
||||
return [{'assetid': machine.assetid,
|
||||
'machinenumber': machine.assetnumber,
|
||||
'superseded': len(superseded)}]
|
||||
|
||||
def _alert_pc_superseded(self, oldname, newcomp, machine):
|
||||
"""Tell a human a PC was replaced on a machine. Best effort, never raises.
|
||||
|
||||
Deliberately an ALERT and not a status change: the collector cannot tell
|
||||
whether the old PC was shelved, sent for repair or re-imaged for another
|
||||
bay, so it says what happened and lets a person decide. Uses the same
|
||||
email + webhook path as the toner alerts, falling back to the site-wide
|
||||
alert_recipients when nothing more specific is set.
|
||||
|
||||
NOT a shopfloor notification: that board is for operators (General,
|
||||
Recertification, Recognition), and an IT asset message does not belong
|
||||
in front of the floor.
|
||||
"""
|
||||
import logging as _logging
|
||||
try:
|
||||
from shopdb.api import send_email, send_webhook, Setting
|
||||
|
||||
newname = newcomp.hostname if newcomp else 'a new PC'
|
||||
machinename = machine.assetnumber or machine.name
|
||||
subject = 'PC replaced on machine {}: {} to {}'.format(
|
||||
machinename, oldname, newname)
|
||||
# /pcs/:id is keyed on computerid, not assetid - an assetid here
|
||||
# opens someone else's PC or a 404.
|
||||
base = (Setting.get('site_base_url') or '').rstrip('/')
|
||||
link = '{}/pcs/{}'.format(base, newcomp.computerid) if base else ''
|
||||
linkhtml = ('<p><a href="{0}">View {1}</a></p>'.format(link, newname)
|
||||
if link else '')
|
||||
html = (
|
||||
'<p><strong>{0}</strong> is now reporting machine '
|
||||
'<strong>{1}</strong>, which was previously driven by '
|
||||
'<strong>{2}</strong>.</p>'
|
||||
'<p>The old link has been archived. {2} has NOT had its status '
|
||||
'changed - set it to Inventory, In Repair or Retired as '
|
||||
'appropriate.</p>{3}'.format(newname, machinename, oldname,
|
||||
linkhtml))
|
||||
send_webhook(subject, subject)
|
||||
|
||||
raw = Setting.get('alert_recipients') or ''
|
||||
recipients = [a.strip() for a in raw.replace(';', ',').split(',')
|
||||
if a.strip()]
|
||||
if recipients:
|
||||
send_email(recipients, subject, html)
|
||||
except Exception:
|
||||
_logging.getLogger(__name__).exception(
|
||||
'PC-superseded alert failed for machine %s',
|
||||
getattr(machine, 'assetnumber', '?'))
|
||||
|
||||
def _sync_measuringtool_link(self, pcasset, pctype, hostname, warnings):
|
||||
"""Auto-create + link the MeasuringTool a metrology PC drives.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user