geenforce: the fleet table links where it says, and judges backups instead of dating them
Two fixes to the same table, in the same regions of the same files. ASSET LINK POINTED AT THE WRONG RECORD. The Asset chip linked /machines/<assetid>, but /machines/:id keys on machineid - the plugin extension id - as MachineDetail itself does everywhere. So the link landed on whichever machine happened to carry that number: a wrong page that looks right, which is worse than a 404. Same for /measuringtools/. The API now returns machinepluginid / toolpluginid beside the asset ids and the view links on those. Both lookups are import-guarded, and with no plugin id the number renders as plain text rather than a link that misleads. AssetRelationships already resolved this correctly; this brings the reports table in line. BACKUP COLUMN READ AS NEGLECT. It showed a raw date, and a revision is only written when the config CHANGES - dedup means a machine stable for months has a months-old newest revision and is perfectly healthy. The column already used lastseenat, the last time the collector CONFIRMED the config, but a bare timestamp says "nothing has happened since", which at the default 24h collection interval IS the healthy steady state. It made a working system look stalled and made the reader do arithmetic against a setting they would have to go and find. It now returns backupok and shows a badge naming the kind, green when confirmed recently, red when not, with the date and an explanation in the hover. backupok is tri-state on purpose: null means no revision at all, and renders as NO badge rather than a green one, because "never seen" must not read as healthy. The threshold is the backups plugin's own backups_staledays, read through its service so there is one definition of stale rather than a second drifting here. Nothing in the badge is kind-specific, so a backup kind added later inherits it by existing. docs/BACKUP-KINDS.md records that, the BackupKind contract, and why the rule is time-based rather than per-kind.
This commit is contained in:
@@ -336,3 +336,128 @@ def test_a_future_checkin_cannot_mask_server_side_silence(client, db, app,
|
||||
row = client.get('/api/geenforce/reports',
|
||||
headers=auth_headers).get_json()['data'][0]
|
||||
assert row['isstale'] is True
|
||||
|
||||
|
||||
def test_asset_link_uses_the_plugin_id_not_the_assetid(client, db, app, auth_headers):
|
||||
"""/machines/:id keys on machineid, NOT the core assetid.
|
||||
|
||||
Returning the assetid made the fleet table link to whichever machine
|
||||
happened to carry that number - a wrong page that looks right rather than a
|
||||
404. The frontend links on machinepluginid for exactly this reason.
|
||||
"""
|
||||
from shopdb.core.models import Asset, AssetType
|
||||
from shopdb.core.models.relationship import RelationshipType, AssetRelationship
|
||||
from plugins.computers.models import Computer
|
||||
from plugins.machines.models import Machine
|
||||
|
||||
_seed_and_publish(app)
|
||||
secret = _token(client, auth_headers, ['geenforce.report'])
|
||||
|
||||
computertype = AssetType.query.filter_by(assettype='computer').first() \
|
||||
or AssetType(assettype='computer')
|
||||
machinetype = AssetType.query.filter_by(assettype='machine').first() \
|
||||
or AssetType(assettype='machine')
|
||||
db.session.add_all([computertype, machinetype])
|
||||
db.session.flush()
|
||||
|
||||
pcasset = Asset(assetnumber='WJBAY01', assettypeid=computertype.assettypeid)
|
||||
machineasset = Asset(assetnumber='6905', assettypeid=machinetype.assettypeid)
|
||||
db.session.add_all([pcasset, machineasset])
|
||||
db.session.flush()
|
||||
db.session.add(Computer(assetid=pcasset.assetid, hostname='WJBAY01'))
|
||||
machine = Machine(assetid=machineasset.assetid)
|
||||
db.session.add(machine)
|
||||
db.session.flush()
|
||||
|
||||
controls = RelationshipType.query.filter_by(relationshiptype='controls').first()
|
||||
if not controls:
|
||||
controls = RelationshipType(relationshiptype='controls')
|
||||
db.session.add(controls)
|
||||
db.session.flush()
|
||||
db.session.add(AssetRelationship(
|
||||
sourceassetid=pcasset.assetid, targetassetid=machineasset.assetid,
|
||||
relationshiptypeid=controls.relationshiptypeid, label='collector:machine'))
|
||||
db.session.commit()
|
||||
|
||||
client.post('/api/geenforce/report', json={
|
||||
'hostname': 'WJBAY01', 'scopename': 'gea-shopfloor-cmm', 'counts': {},
|
||||
}, headers={'X-API-Key': secret})
|
||||
|
||||
row = [r for r in client.get('/api/geenforce/reports',
|
||||
headers=auth_headers).get_json()['data']
|
||||
if r['hostname'] == 'WJBAY01'][0]
|
||||
assert row['machinenumber'] == '6905'
|
||||
assert row['machineassetid'] == machineasset.assetid
|
||||
assert row['machinepluginid'] == machine.machineid
|
||||
|
||||
|
||||
# -- backup verdict ----------------------------------------------------------
|
||||
#
|
||||
# The column used to show a raw date, which read as neglect: dedup means an
|
||||
# unchanged config writes no new revision, so a day-old confirmation IS the
|
||||
# healthy steady state at the default 24h collection interval.
|
||||
|
||||
def _backup_revision(db, hostname, kind, ageda):
|
||||
from datetime import timedelta
|
||||
from plugins.backups.models import BackupRevision
|
||||
from plugins.geenforce.service import _utcnow
|
||||
from shopdb.core.models import Asset, AssetType
|
||||
from plugins.computers.models import Computer
|
||||
|
||||
computertype = AssetType.query.filter_by(assettype='computer').first() \
|
||||
or AssetType(assettype='computer')
|
||||
db.session.add(computertype)
|
||||
db.session.flush()
|
||||
asset = Asset(assetnumber=hostname, assettypeid=computertype.assettypeid)
|
||||
db.session.add(asset)
|
||||
db.session.flush()
|
||||
db.session.add(Computer(assetid=asset.assetid, hostname=hostname))
|
||||
db.session.add(BackupRevision(assetid=asset.assetid, backupkind=kind,
|
||||
sourcehostname=hostname,
|
||||
contenthash='0' * 64,
|
||||
lastseenat=_utcnow() - timedelta(days=ageda)))
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def _row_for(client, auth_headers, hostname):
|
||||
return [r for r in client.get('/api/geenforce/reports',
|
||||
headers=auth_headers).get_json()['data']
|
||||
if r['hostname'] == hostname][0]
|
||||
|
||||
|
||||
def test_a_recent_check_is_good_even_though_nothing_changed(client, db, app, auth_headers):
|
||||
_seed_and_publish(app)
|
||||
secret = _token(client, auth_headers, ['geenforce.report'])
|
||||
_backup_revision(db, 'WJBAK01', 'ntlars', ageda=1)
|
||||
client.post('/api/geenforce/report', json={
|
||||
'hostname': 'WJBAK01', 'scopename': 'gea-shopfloor-cmm', 'counts': {}},
|
||||
headers={'X-API-Key': secret})
|
||||
|
||||
row = _row_for(client, auth_headers, 'WJBAK01')
|
||||
assert row['backupkind'] == 'ntlars'
|
||||
assert row['backupok'] is True, 'a day old is the healthy steady state'
|
||||
assert row['backupstaleafterdays'] == 3
|
||||
|
||||
|
||||
def test_a_check_older_than_the_threshold_is_stale(client, db, app, auth_headers):
|
||||
_seed_and_publish(app)
|
||||
secret = _token(client, auth_headers, ['geenforce.report'])
|
||||
_backup_revision(db, 'WJBAK02', 'ntlars', ageda=9)
|
||||
client.post('/api/geenforce/report', json={
|
||||
'hostname': 'WJBAK02', 'scopename': 'gea-shopfloor-cmm', 'counts': {}},
|
||||
headers={'X-API-Key': secret})
|
||||
|
||||
assert _row_for(client, auth_headers, 'WJBAK02')['backupok'] is False
|
||||
|
||||
|
||||
def test_no_backup_at_all_is_unknown_not_good(client, db, app, auth_headers):
|
||||
"""None, never True: 'never seen' must not render as a green badge."""
|
||||
_seed_and_publish(app)
|
||||
secret = _token(client, auth_headers, ['geenforce.report'])
|
||||
client.post('/api/geenforce/report', json={
|
||||
'hostname': 'WJBAK03', 'scopename': 'gea-shopfloor-cmm', 'counts': {}},
|
||||
headers={'X-API-Key': secret})
|
||||
|
||||
row = _row_for(client, auth_headers, 'WJBAK03')
|
||||
assert row['backupkind'] is None
|
||||
assert row['backupok'] is None
|
||||
|
||||
Reference in New Issue
Block a user