Merge the wilm work, act on the five-agent review, verify the launch-path policy on Windows
This commit is contained in:
@@ -3,7 +3,10 @@
|
||||
from .endpoint import (
|
||||
EndpointState, EndpointEvent, EndpointCommand, EndpointApplication,
|
||||
)
|
||||
from .monitor import EndpointMonitor
|
||||
from .operatorreport import OperatorReport
|
||||
|
||||
__all__ = [
|
||||
'EndpointState', 'EndpointEvent', 'EndpointCommand', 'EndpointApplication',
|
||||
'EndpointMonitor', 'OperatorReport',
|
||||
]
|
||||
|
||||
@@ -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).
|
||||
|
||||
87
plugins/endpoints/models/monitor.py
Normal file
87
plugins/endpoints/models/monitor.py
Normal 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),
|
||||
}
|
||||
82
plugins/endpoints/models/operatorreport.py
Normal file
82
plugins/endpoints/models/operatorreport.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Operator issue reports (screenshot + free-text notes from the tray).
|
||||
|
||||
An operator on a shop-floor PC opens the tray "Report an issue" dialog, writes a
|
||||
free-text description, and optionally attaches a screenshot of their screen. The
|
||||
agent uploads that to ShopDB, where it becomes one OperatorReport row linked to
|
||||
the reporting PC's asset. The screenshot bytes live on disk under the instance
|
||||
uploads dir (like employee photos); this row holds only metadata plus the
|
||||
relative file path so the image can be served back and purged on retention.
|
||||
|
||||
Reports are transient by design: a retention window (default 7 days) purges old
|
||||
rows and their image files, so this is a rolling operator-signal feed, not a
|
||||
permanent record.
|
||||
"""
|
||||
|
||||
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 OperatorReport(db.Model):
|
||||
"""One operator-submitted issue report for a PC asset."""
|
||||
|
||||
__tablename__ = 'operatorreports'
|
||||
|
||||
operatorreportid = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# The PC this report is about. ON DELETE CASCADE: when the asset goes, its
|
||||
# operator reports go with it.
|
||||
assetid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('assets.assetid', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# The dropdown reason. Only 'Other (free text)' is functional today, but the
|
||||
# column is kept so the richer reason routing can return without a migration.
|
||||
reason = db.Column(db.String(128), nullable=False, default='Other (free text)')
|
||||
|
||||
# The operator's free-text description. Nullable: a screenshot alone is a
|
||||
# valid report.
|
||||
details = db.Column(db.Text, nullable=True)
|
||||
|
||||
# Screenshot metadata. imagefilename is the stored file name under the
|
||||
# operator-reports uploads dir; null when the operator declined a screenshot.
|
||||
imagefilename = db.Column(db.String(255), nullable=True)
|
||||
imagemimetype = db.Column(db.String(64), nullable=True)
|
||||
imagebytesize = db.Column(db.Integer, nullable=True)
|
||||
|
||||
# The hostname the agent reported at upload time, kept for traceability even
|
||||
# if the asset link is later re-pointed.
|
||||
reportedhostname = db.Column(db.String(255), nullable=True)
|
||||
|
||||
createdat = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('idx_operatorreports_asset', 'assetid'),
|
||||
db.Index('idx_operatorreports_createdat', 'createdat'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<OperatorReport {self.operatorreportid} asset={self.assetid}>"
|
||||
|
||||
def has_image(self):
|
||||
return bool(self.imagefilename)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'operatorreportid': self.operatorreportid,
|
||||
'assetid': self.assetid,
|
||||
'reason': self.reason,
|
||||
'details': self.details,
|
||||
'hasimage': self.has_image(),
|
||||
'imagemimetype': self.imagemimetype,
|
||||
'imagebytesize': self.imagebytesize,
|
||||
'reportedhostname': self.reportedhostname,
|
||||
'createdat': _utciso(self.createdat),
|
||||
}
|
||||
Reference in New Issue
Block a user