Add endpoint monitor catalog and warn-only drift reconciler

Operators need to see when a machine's running services or processes fall outside the desired state defined by policy. This adds a reusable monitor catalog (shipped read-only rows plus user-built local rows), computes drift server-side on agent check-in, and surfaces a warning on the endpoint panels. Remediation stays manual for now; automation can follow once demand is proven. Includes migrations 0006-0008 and the monitors API client.
This commit is contained in:
benjamin.baker
2026-09-01 15:44:03 -04:00
parent 1a1bf0640f
commit 42db980ca9
23 changed files with 2054 additions and 51 deletions

View File

@@ -3,7 +3,9 @@
from .endpoint import (
EndpointState, EndpointEvent, EndpointCommand, EndpointApplication,
)
from .monitor import EndpointMonitor
__all__ = [
'EndpointState', 'EndpointEvent', 'EndpointCommand', 'EndpointApplication',
'EndpointMonitor',
]

View File

@@ -75,6 +75,13 @@ class EndpointState(db.Model):
interfacesjson = db.Column(db.Text(length=16777215), nullable=True)
scheduledtasksjson = db.Column(db.Text(length=16777215), nullable=True)
# Warn-only reconciler output, refreshed on every agent check-in in the same
# commit as servicesjson. Each entry names a policy/shipped-attached monitor
# the endpoint is expected to be running but the latest snapshot does not
# report as running, so an operator sees the drift and intervenes manually.
# This is warn-only: nothing here is auto-remediated. Null = never computed.
driftjson = db.Column(db.Text(length=16777215), nullable=True)
# Last-known MTConnect agent run state, cached so the WebUI toggle can show
# a real state between command polls. Set by the agent's mtconnect_status
# result. Null = never reported (unknown).

View File

@@ -0,0 +1,87 @@
"""Endpoint monitor definitions (the reusable monitor catalog).
A monitor definition is the "what": a named process or service the agent should
watch, with a bespoke display label separate from the raw OS name, plus optional
launch and collector paths. Definitions are the noun that a configuration policy
action points at; attaching a monitor to a PC type is a policy concern, not a
property of the definition itself.
Two sources:
shipped Internal apps we ship (eDNC, MTConnect, UDC). Seeded by migration.
Read-only in the UI: name and identity are fixed by the product.
local User-defined monitors built on the Settings page.
A definition is watched on an endpoint when a site policy in effect for that
endpoint's PC type includes a monitor action pointing at it.
"""
from datetime import datetime
from shopdb.api import db
def _utciso(value):
"""Naive-UTC datetime -> ISO string marked UTC, or None."""
return value.isoformat() + 'Z' if value else None
class EndpointMonitor(db.Model):
"""One reusable process or service monitor definition."""
__tablename__ = 'endpointmonitors'
endpointmonitorid = db.Column(db.Integer, primary_key=True)
# 'process' or 'service'. Decides which identity column is authoritative and
# which start/stop/status actions the row exposes.
kind = db.Column(db.String(16), nullable=False)
# The bespoke label operators see (e.g. 'MTConnect'). Distinct from the raw
# OS name because agent.exe is not obviously MTConnect.
displayname = db.Column(db.String(128), nullable=False)
# OS identity. For a process monitor, processname is the executable the agent
# matches (e.g. 'agent.exe'); for a service monitor, servicename is the
# Windows service name (e.g. 'MTConnect UDC Agent'). Exactly one is the
# authoritative identity for a given kind; the other stays null.
processname = db.Column(db.String(255), nullable=True)
servicename = db.Column(db.String(255), nullable=True)
# Optional paths supplied per monitor. launchpath starts the app; the
# collector paths point the agent at where the app writes data and logs.
launchpath = db.Column(db.String(512), nullable=True)
datacollectorpath = db.Column(db.String(512), nullable=True)
logcollectorpath = db.Column(db.String(512), nullable=True)
# 'shipped' (product-owned, seeded) or 'local' (user-built). Shipped rows are
# not editable or deletable through the CRUD API.
source = db.Column(db.String(16), nullable=False, default='local')
# Soft on/off so a monitor can be retired without losing policy references.
isactive = db.Column(db.Boolean, nullable=False, default=True)
createdat = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
__table_args__ = (
db.UniqueConstraint('kind', 'displayname', name='uq_endpointmonitors_kinddisplay'),
db.Index('idx_endpointmonitors_source', 'source'),
)
def __repr__(self):
return f"<EndpointMonitor {self.displayname!r} kind={self.kind} source={self.source}>"
def to_dict(self):
return {
'endpointmonitorid': self.endpointmonitorid,
'kind': self.kind,
'displayname': self.displayname,
'processname': self.processname,
'servicename': self.servicename,
'launchpath': self.launchpath,
'datacollectorpath': self.datacollectorpath,
'logcollectorpath': self.logcollectorpath,
'source': self.source,
'isactive': self.isactive,
'createdat': _utciso(self.createdat),
}