Enforcement reports with asset context and backup state; manifest editor spacing
This commit is contained in:
@@ -909,6 +909,150 @@ def _current_published_version(scopename, phase):
|
||||
return published.versionnumber if published else None
|
||||
|
||||
|
||||
def _asset_facts(hostnames):
|
||||
"""What ShopDB knows about each reporting host, resolved in bulk.
|
||||
|
||||
A report row only carries a hostname; everything an operator wants next -
|
||||
which machine that PC drives, which measuring tool it is, where it sits,
|
||||
whether its config is still being backed up - hangs off the asset behind it.
|
||||
|
||||
Bulk queries, never per row: this table shows the whole fleet, so a lookup
|
||||
inside the loop would be one query per PC. Every plugin here is optional
|
||||
(ADR-013), hence the ImportError guards - a lean build without computers
|
||||
still renders the table, just without these columns.
|
||||
|
||||
Returns {lowercased hostname: {...}}.
|
||||
"""
|
||||
facts = {}
|
||||
if not hostnames:
|
||||
return facts
|
||||
|
||||
try:
|
||||
from plugins.computers.models import Computer
|
||||
except ImportError:
|
||||
return facts
|
||||
|
||||
from shopdb.api import Asset
|
||||
|
||||
rows = (db.session.query(Computer, Asset)
|
||||
.join(Asset, Asset.assetid == Computer.assetid)
|
||||
.filter(Computer.hostname.in_(hostnames))
|
||||
.all())
|
||||
|
||||
assetidtohost = {}
|
||||
for computer, asset in rows:
|
||||
key = (computer.hostname or '').lower()
|
||||
assetidtohost[asset.assetid] = key
|
||||
facts[key] = {
|
||||
'computerid': computer.computerid,
|
||||
'assetid': asset.assetid,
|
||||
'assetnumber': asset.assetnumber,
|
||||
'location': (asset.location.locationname if asset.location else None),
|
||||
'mapx': asset.mapx,
|
||||
'mapy': asset.mapy,
|
||||
'machinenumber': None, 'machineassetid': None,
|
||||
'toolassetnumber': None, 'toolassetid': None,
|
||||
'displayrole': None,
|
||||
'backupkind': None, 'backuplastseen': None,
|
||||
}
|
||||
|
||||
_attach_controlled_assets(facts, assetidtohost)
|
||||
_attach_display_roles(facts, hostnames)
|
||||
_attach_backup_state(facts, hostnames)
|
||||
return facts
|
||||
|
||||
|
||||
def _attach_controlled_assets(facts, assetidtohost):
|
||||
"""Machine number / measuring-tool asset # for the asset each PC controls.
|
||||
|
||||
The collector records this as a 'controls' relationship, so read that rather
|
||||
than re-deriving it - the same resolution living in two places is what put a
|
||||
wrong subtype filter on the map.
|
||||
"""
|
||||
if not assetidtohost:
|
||||
return
|
||||
try:
|
||||
from shopdb.api import Asset, AssetRelationship, RelationshipType
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
controls = RelationshipType.query.filter_by(
|
||||
relationshiptype='controls').first()
|
||||
if not controls:
|
||||
return
|
||||
|
||||
target = db.aliased(Asset)
|
||||
rows = (db.session.query(AssetRelationship.sourceassetid, target)
|
||||
.join(target, target.assetid == AssetRelationship.targetassetid)
|
||||
.filter(AssetRelationship.relationshiptypeid == controls.relationshiptypeid,
|
||||
AssetRelationship.isactive.is_(True),
|
||||
AssetRelationship.sourceassetid.in_(assetidtohost))
|
||||
.all())
|
||||
|
||||
toolassetids = set()
|
||||
try:
|
||||
from plugins.measuringtools.models import MeasuringTool
|
||||
toolassetids = {row.assetid for row in
|
||||
MeasuringTool.query.with_entities(MeasuringTool.assetid)}
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
for sourceassetid, asset in rows:
|
||||
entry = facts.get(assetidtohost.get(sourceassetid))
|
||||
if not entry:
|
||||
continue
|
||||
if asset.assetid in toolassetids:
|
||||
entry['toolassetnumber'] = asset.assetnumber
|
||||
entry['toolassetid'] = asset.assetid
|
||||
else:
|
||||
entry['machinenumber'] = asset.assetnumber
|
||||
entry['machineassetid'] = asset.assetid
|
||||
|
||||
|
||||
def _attach_display_roles(facts, hostnames):
|
||||
"""Dashboard / Lobby / parts kiosk, from the display-role mapping.
|
||||
|
||||
ShopDB's mirror of the display-type.txt each kiosk reads at startup.
|
||||
"""
|
||||
try:
|
||||
from shopdb.core.models.dashboarddefault import DashboardDefault
|
||||
except ImportError:
|
||||
return
|
||||
lowered = {name.lower() for name in hostnames}
|
||||
for default in DashboardDefault.query.filter_by(isactive=True).all():
|
||||
host = (default.fqdn or '').split('.')[0].lower()
|
||||
if host and host in lowered and facts.get(host):
|
||||
facts[host]['displayrole'] = default.displayrole
|
||||
|
||||
|
||||
def _attach_backup_state(facts, hostnames):
|
||||
"""Whether this PC's config is still being backed up, and of what kind.
|
||||
|
||||
Keyed on lastseenat, NOT the newest revision date. Dedup means an unchanged
|
||||
config writes no revision, so a machine stable for six months has a
|
||||
six-month-old newest revision and is perfectly healthy; the one to worry
|
||||
about is the machine whose backup stopped running. sourcehostname is the PC
|
||||
the config was read off, which is exactly the host reporting here.
|
||||
"""
|
||||
try:
|
||||
from plugins.backups.models import BackupRevision
|
||||
except ImportError:
|
||||
return
|
||||
rows = (BackupRevision.query
|
||||
.filter(BackupRevision.sourcehostname.in_(hostnames))
|
||||
.order_by(BackupRevision.lastseenat.desc().nullslast())
|
||||
.all())
|
||||
for revision in rows:
|
||||
key = (revision.sourcehostname or '').lower()
|
||||
entry = facts.get(key)
|
||||
# First row per host wins: ordered newest-confirmed first.
|
||||
if entry and entry['backuplastseen'] is None:
|
||||
entry['backupkind'] = revision.backupkind
|
||||
entry['backuplastseen'] = (
|
||||
revision.lastseenat.isoformat() + 'Z'
|
||||
if revision.lastseenat else None)
|
||||
|
||||
|
||||
@geenforce_bp.route('/reports', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('geenforce.manage')
|
||||
@@ -929,6 +1073,9 @@ def list_reports():
|
||||
reports = query.order_by(
|
||||
ManifestEnforcementReport.receivedat.desc()).all()
|
||||
|
||||
facts = _asset_facts({report.hostname for report in reports
|
||||
if report.hostname})
|
||||
|
||||
latest_cache = {}
|
||||
data = []
|
||||
for report in reports:
|
||||
@@ -936,9 +1083,24 @@ def list_reports():
|
||||
if key not in latest_cache:
|
||||
latest_cache[key] = _current_published_version(*key)
|
||||
latest = latest_cache[key]
|
||||
known = facts.get((report.hostname or '').lower(), {})
|
||||
data.append({
|
||||
'reportid': report.reportid,
|
||||
'hostname': report.hostname,
|
||||
# A host ShopDB has no asset for still shows: the enforcement result
|
||||
# is real even when the inventory is behind.
|
||||
'computerid': known.get('computerid'),
|
||||
'assetnumber': known.get('assetnumber'),
|
||||
'location': known.get('location'),
|
||||
'mapx': known.get('mapx'),
|
||||
'mapy': known.get('mapy'),
|
||||
'machinenumber': known.get('machinenumber'),
|
||||
'machineassetid': known.get('machineassetid'),
|
||||
'toolassetnumber': known.get('toolassetnumber'),
|
||||
'toolassetid': known.get('toolassetid'),
|
||||
'displayrole': known.get('displayrole'),
|
||||
'backupkind': known.get('backupkind'),
|
||||
'backuplastseen': known.get('backuplastseen'),
|
||||
'scopename': report.scopename,
|
||||
'phase': report.phase,
|
||||
'appliedversion': report.appliedversion,
|
||||
|
||||
Reference in New Issue
Block a user