239 lines
8.9 KiB
Python
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
|