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
This commit is contained in:
0
plugins/endpoints/services/__init__.py
Normal file
0
plugins/endpoints/services/__init__.py
Normal file
153
plugins/endpoints/services/reconciler.py
Normal file
153
plugins/endpoints/services/reconciler.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""Warn-only monitor reconciler.
|
||||
|
||||
The reconciler answers one question on every agent check-in: for the monitors a
|
||||
site policy (or the shipped bundle) attaches to this endpoint, is each one the
|
||||
endpoint is EXPECTED to be running actually reported as running in the latest
|
||||
snapshot? Every monitor that is expected-running but not observed-running is
|
||||
DRIFT, and drift is surfaced to an operator for MANUAL intervention.
|
||||
|
||||
This is warn-only. The reconciler NEVER starts, stops, or enqueues anything - it
|
||||
compares observed state against expected state and records the difference. Whether
|
||||
to later automate remediation (a keep-alive that starts a process when it drifts)
|
||||
is a separate, deliberate opt-in the product does not ship switched on.
|
||||
|
||||
Two seams are kept deliberately narrow so this stays easy to change:
|
||||
|
||||
monitor_expects_running(monitor)
|
||||
The single place that decides "should this monitor be up?". Today every
|
||||
attached monitor is treated as expected-running (attachment implies intent
|
||||
to run). When an observe-only monitor kind arrives - one that only collects
|
||||
general state and must never be flagged as drift - this is the one function
|
||||
that branches on it. Nothing else in the reconciler encodes that policy.
|
||||
|
||||
compute_drift(asset_id, reported_services)
|
||||
Pure given its inputs: it resolves the attached monitors and diffs them
|
||||
against the reported running set. It reads no request state and writes no
|
||||
rows, so the ingest path can call it inside its own transaction and tests
|
||||
can call it directly.
|
||||
"""
|
||||
|
||||
# The running-state vocabulary a watched process/service can report. Kept in
|
||||
# lockstep with plugins/endpoints/api/routes.py::_RUNNING_STATES so a service the
|
||||
# panel paints green is never flagged as drift here. Both read from the same
|
||||
# agent snapshot; disagreeing would show a running app as out-of-desired-state.
|
||||
_RUNNING_STATES = ('running', 'started', 'up', 'ok', 'active')
|
||||
|
||||
|
||||
def monitor_expects_running(monitor):
|
||||
"""Whether an attached monitor is expected to be running on an endpoint.
|
||||
|
||||
The seam for the "should be up" decision. Today attachment implies intent to
|
||||
run, so every active monitor returns True. A future observe-only monitor kind
|
||||
(general state collection, never keep-alive) branches HERE and returns False
|
||||
so it is never reported as drift - no other part of the reconciler needs to
|
||||
change.
|
||||
"""
|
||||
return bool(getattr(monitor, 'isactive', True))
|
||||
|
||||
|
||||
def _monitor_identity(monitor):
|
||||
"""The (kind, osname) a monitor watches, or None when it has no identity.
|
||||
|
||||
Mirrors how the services panel reads a monitor's OS identity: a process
|
||||
monitor is keyed by its processname, a service monitor by its servicename.
|
||||
"""
|
||||
kind = (getattr(monitor, 'kind', '') or '').lower() or None
|
||||
if kind == 'process':
|
||||
osname = getattr(monitor, 'processname', None)
|
||||
elif kind == 'service':
|
||||
osname = getattr(monitor, 'servicename', None)
|
||||
else:
|
||||
osname = None
|
||||
osname = (osname or '').strip()
|
||||
if not kind or not osname:
|
||||
return None
|
||||
return kind, osname
|
||||
|
||||
|
||||
def _reported_running_keys(reported_services):
|
||||
"""Set of (kind, lowercased-name) the snapshot reports as RUNNING.
|
||||
|
||||
Tolerant of the two shapes the agent snapshot uses: dict entries with
|
||||
name/service and state/status (and an optional kind), or bare strings.
|
||||
Only running entries land in the set; a stopped or unknown entry is absent,
|
||||
which is what makes an expected-running monitor read as drift.
|
||||
"""
|
||||
running = set()
|
||||
for entry in reported_services or ():
|
||||
if isinstance(entry, dict):
|
||||
name = entry.get('name') or entry.get('service') or ''
|
||||
raw = (entry.get('state') or entry.get('status') or '').strip().lower()
|
||||
kind = (entry.get('kind') or '').lower() or None
|
||||
else:
|
||||
name, raw, kind = str(entry), '', None
|
||||
name = name.strip().lower()
|
||||
if not name or raw not in _RUNNING_STATES:
|
||||
continue
|
||||
running.add((kind, name))
|
||||
return running
|
||||
|
||||
|
||||
def _is_running(kind, osname, running_keys):
|
||||
"""Whether a monitor's identity appears running in the snapshot.
|
||||
|
||||
A snapshot entry may omit kind (older agents, or a plain string list), so a
|
||||
match on name alone under an unknown kind still counts as running - the
|
||||
reconciler must not invent drift just because the agent did not label a row.
|
||||
"""
|
||||
needle = osname.strip().lower()
|
||||
if (kind, needle) in running_keys:
|
||||
return True
|
||||
if (None, needle) in running_keys:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def compute_drift(asset_id, reported_services):
|
||||
"""Compute the current drift list for an endpoint. Pure, no writes.
|
||||
|
||||
Resolves the monitors attached to the asset's PC type (shipped defaults plus
|
||||
site policy), keeps only those that are expected-running, and returns one
|
||||
entry per monitor whose identity is not reported as running. Returns an empty
|
||||
list when nothing is attached or nothing has drifted, so the caller can store
|
||||
it as "computed, no drift" distinct from a null "never computed".
|
||||
|
||||
Isolated from Flask request state: it reads the monitor catalog and the
|
||||
passed-in snapshot only, so the ingest path calls it inside its own
|
||||
transaction and tests call it directly.
|
||||
"""
|
||||
try:
|
||||
from plugins.endpoints.api.routes import _resolved_monitors_for_asset
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
monitors = _resolved_monitors_for_asset(asset_id) or []
|
||||
if not monitors:
|
||||
return []
|
||||
|
||||
running_keys = _reported_running_keys(reported_services)
|
||||
|
||||
drift = []
|
||||
seen = set()
|
||||
for monitor in monitors:
|
||||
if not monitor_expects_running(monitor):
|
||||
continue
|
||||
identity = _monitor_identity(monitor)
|
||||
if identity is None:
|
||||
continue
|
||||
kind, osname = identity
|
||||
key = (kind, osname.lower())
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
if _is_running(kind, osname, running_keys):
|
||||
continue
|
||||
drift.append({
|
||||
'monitorid': getattr(monitor, 'endpointmonitorid', None),
|
||||
'kind': kind,
|
||||
'name': osname,
|
||||
'displayname': getattr(monitor, 'displayname', None) or osname,
|
||||
'expected': 'running',
|
||||
'observed': 'notrunning',
|
||||
})
|
||||
return drift
|
||||
Reference in New Issue
Block a user