Merge the wilm work, act on the five-agent review, verify the launch-path policy on Windows
This commit is contained in:
238
tests/test_plugins/test_endpoints_reconciler.py
Normal file
238
tests/test_plugins/test_endpoints_reconciler.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user