Files
shopdb-flask/tests/test_plugins/test_endpoints_reconciler.py
cproudlock 27b2817fd7 Import the wilm endpoint-monitor and channel-distribution work
Everything on origin/wilm that is not on the published main, applied as one
change because the published history has rewritten hashes: per-commit
cherry-pick conflicts against our main even though the squashed diff applies
clean.

Contents: endpoint monitor catalog + policy attachment + warn-only drift
reconciler, operator issue reports from the tray, agent launch-path allowlist,
and the GHCR channel-image distribution path for dev workstations.

NOT READY FOR main. Open items:
  - 10 new routes are absent from docs/api-inventory.json, so
    test_api_inventory_parity fails. That gate SKIPS on the published mirror
    (the test ships, its data file is scrubbed), which is why wilm never saw it.
  - dev-bootstrap.ps1 and Start-ShopDBDev.ps1 hardcode the ge-aero GitHub org.
  - plugins/endpoints/plugin.py carries a whitespace-only re-indent.

Scope: product
2026-09-02 10:05:44 -04:00

239 lines
8.9 KiB
Python

"""Warn-only reconciler: drift computation and its exposure on the panels.
The reconciler runs server-side on agent check-in. For every monitor a site
policy attaches to an endpoint's PC type that is EXPECTED to be running, it flags
DRIFT when the latest snapshot does not report it running. It never starts or
stops anything - drift is surfaced for an operator to act on manually.
These tests cover the pure computer (compute_drift), the check-in write path
(apply_collector_payload persisting driftjson), the panel exposure, and the
observe-only seam (monitor_expects_running) that keeps a non-keepalive monitor
out of the drift list.
"""
from shopdb.core.models import Asset, AssetType, Setting
SERVICES_URL = '/api/endpoints/asset/{}/services'
STATUS_URL = '/api/endpoints/asset/{}/status'
def _computer(db, hostname, computertype):
"""Create a computer Asset + Computer of the given PC type; return the pair."""
from plugins.computers.models import Computer, ComputerType
atype = AssetType.query.filter_by(assettype='computer').first()
if not atype:
atype = AssetType(assettype='computer')
db.session.add(atype)
db.session.flush()
ctype = ComputerType.query.filter_by(computertype=computertype).first()
if not ctype:
ctype = ComputerType(computertype=computertype)
db.session.add(ctype)
db.session.flush()
asset = Asset(assetnumber=hostname, assettypeid=atype.assettypeid,
isactive=True)
db.session.add(asset)
db.session.flush()
comp = Computer(assetid=asset.assetid, hostname=hostname,
computertypeid=ctype.computertypeid)
db.session.add(comp)
db.session.flush()
return asset, ctype
def _attach_monitor_to_pctype(db, monitor, computertypeid):
"""Attach a monitor to a PC type via an active Business/Configuration Policy."""
from plugins.computers.models import (
ConfigurationPolicy, ConfigurationPolicyAction,
BusinessPolicy, BusinessPolicyItem,
)
if not Setting.query.filter_by(
key='computers_configpolicies_enabled').first():
db.session.add(
Setting(key='computers_configpolicies_enabled', value='true'))
policy = ConfigurationPolicy(
name='Coverage {}'.format(monitor.endpointmonitorid),
scopejson={'type': 'pctype', 'id': computertypeid},
)
db.session.add(policy)
db.session.flush()
db.session.add(ConfigurationPolicyAction(
configurationpolicyid=policy.configurationpolicyid,
action='monitor:{}'.format(monitor.endpointmonitorid),
mode='gate',
))
business = BusinessPolicy(name='BP {}'.format(monitor.endpointmonitorid),
priority=100)
db.session.add(business)
db.session.flush()
db.session.add(BusinessPolicyItem(
businesspolicyid=business.businesspolicyid,
configurationpolicyid=policy.configurationpolicyid,
sortorder=0,
))
db.session.commit()
def _process_monitor(db, displayname, processname):
from plugins.endpoints.models import EndpointMonitor
monitor = EndpointMonitor(
kind='process', displayname=displayname, processname=processname,
source='local', isactive=True,
)
db.session.add(monitor)
db.session.flush()
return monitor
# ---------------------------------------------------------------------------
# Pure computer
# ---------------------------------------------------------------------------
def test_compute_drift_flags_attached_monitor_not_reported_running(
client, db, app):
"""An expected-running monitor absent from the snapshot is drift."""
from plugins.endpoints.services.reconciler import compute_drift
asset, ctype = _computer(db, 'DRIFT01', 'CMM PC')
monitor = _process_monitor(db, 'eDNC', 'dncMain.exe')
_attach_monitor_to_pctype(db, monitor, ctype.computertypeid)
drift = compute_drift(asset.assetid, [])
assert len(drift) == 1
assert drift[0]['name'] == 'dncMain.exe'
assert drift[0]['kind'] == 'process'
assert drift[0]['expected'] == 'running'
assert drift[0]['observed'] == 'notrunning'
def test_compute_drift_empty_when_monitor_reported_running(client, db, app):
"""A monitor the snapshot reports running is not drift."""
from plugins.endpoints.services.reconciler import compute_drift
asset, ctype = _computer(db, 'DRIFT02', 'CMM PC')
monitor = _process_monitor(db, 'eDNC', 'dncMain.exe')
_attach_monitor_to_pctype(db, monitor, ctype.computertypeid)
reported = [{'name': 'dncMain.exe', 'kind': 'process', 'state': 'running'}]
assert compute_drift(asset.assetid, reported) == []
def test_compute_drift_empty_when_no_monitor_attached(client, db, app):
"""No attached monitors means nothing to reconcile."""
from plugins.endpoints.services.reconciler import compute_drift
asset, _ = _computer(db, 'DRIFT03', 'CMM PC')
assert compute_drift(asset.assetid, []) == []
def test_monitor_expects_running_is_the_observe_only_seam():
"""The 'should be up' decision lives in one function; inactive is excluded."""
from plugins.endpoints.services.reconciler import monitor_expects_running
class _M:
isactive = True
active = _M()
inactive = _M()
inactive.isactive = False
assert monitor_expects_running(active) is True
assert monitor_expects_running(inactive) is False
# ---------------------------------------------------------------------------
# Check-in write path
# ---------------------------------------------------------------------------
def test_checkin_persists_drift_on_endpoint_state(client, db, app):
"""apply_collector_payload caches drift on the state in the same commit."""
from plugins.endpoints.plugin import EndpointsPlugin
from plugins.endpoints.models import EndpointState
asset, ctype = _computer(db, 'CHKIN01', 'CMM PC')
monitor = _process_monitor(db, 'eDNC', 'dncMain.exe')
_attach_monitor_to_pctype(db, monitor, ctype.computertypeid)
plugin = EndpointsPlugin()
plugin.apply_collector_payload({'hostname': 'CHKIN01', 'services': []})
db.session.commit()
state = EndpointState.query.filter_by(assetid=asset.assetid).first()
assert state.driftjson is not None
import json
drift = json.loads(state.driftjson)
assert [d['name'] for d in drift] == ['dncMain.exe']
def test_checkin_clears_drift_when_process_returns(client, db, app):
"""A later report that shows the process running clears the cached drift."""
from plugins.endpoints.plugin import EndpointsPlugin
from plugins.endpoints.models import EndpointState
import json
asset, ctype = _computer(db, 'CHKIN02', 'CMM PC')
monitor = _process_monitor(db, 'eDNC', 'dncMain.exe')
_attach_monitor_to_pctype(db, monitor, ctype.computertypeid)
plugin = EndpointsPlugin()
plugin.apply_collector_payload({'hostname': 'CHKIN02', 'services': []})
db.session.commit()
plugin.apply_collector_payload({
'hostname': 'CHKIN02',
'services': [{'name': 'dncMain.exe', 'kind': 'process',
'state': 'running'}],
})
db.session.commit()
state = EndpointState.query.filter_by(assetid=asset.assetid).first()
assert json.loads(state.driftjson) == []
# ---------------------------------------------------------------------------
# Panel exposure
# ---------------------------------------------------------------------------
def test_status_panel_surfaces_drift_warning(client, db, app, auth_headers):
"""The status panel adds a Needs attention field when drift is cached."""
from plugins.endpoints.plugin import EndpointsPlugin
asset, ctype = _computer(db, 'PANEL01', 'CMM PC')
monitor = _process_monitor(db, 'eDNC', 'dncMain.exe')
_attach_monitor_to_pctype(db, monitor, ctype.computertypeid)
plugin = EndpointsPlugin()
plugin.apply_collector_payload({'hostname': 'PANEL01', 'services': []})
db.session.commit()
response = client.get(STATUS_URL.format(asset.assetid), headers=auth_headers)
assert response.status_code == 200
labels = [f['label'] for f in response.get_json()['data']['fields']]
assert 'Needs attention' in labels
def test_services_panel_marks_drifted_row(client, db, app, auth_headers):
"""The synthetic Unknown row for an expected-running monitor is drift-flagged."""
from plugins.endpoints.plugin import EndpointsPlugin
asset, ctype = _computer(db, 'PANEL02', 'CMM PC')
monitor = _process_monitor(db, 'eDNC', 'dncMain.exe')
_attach_monitor_to_pctype(db, monitor, ctype.computertypeid)
plugin = EndpointsPlugin()
plugin.apply_collector_payload({'hostname': 'PANEL02', 'services': []})
db.session.commit()
response = client.get(SERVICES_URL.format(asset.assetid), headers=auth_headers)
assert response.status_code == 200
rows = response.get_json()['data']
edenc = next((r for r in rows if r['name'] == 'eDNC'), None)
assert edenc is not None
assert edenc.get('drift') is True