Configuration policies need a way to attach process/service monitors so drift can be evaluated against a machine's desired state. This wires the policy resolver and routes to the shared monitor catalog and adds an action picker UI that can create process monitors with an optional launch path.
205 lines
8.1 KiB
Python
205 lines
8.1 KiB
Python
"""Configuration and business policy models (RMM-style).
|
|
|
|
DT staff author reusable Configuration Policies (a single rule binding
|
|
allowlisted endpoint actions to endpoints matched by a boolean scope) and group
|
|
them into ordered Business Policies assigned to a fleet. When an endpoint reports
|
|
in, the server folds the matching Business Policies in priority order to compute
|
|
that endpoint's effective policy (first match wins per action).
|
|
|
|
These models live in the computers plugin so policies sit next to ComputerType.
|
|
The action VOCABULARY stays authoritative in plugins/endpoints/commands.py; this
|
|
layer references action NAMES only. The `source` column is the seam for a future
|
|
GE-Enforce sync (local-authored rows vs imported/managed rows).
|
|
|
|
The whole surface is gated by the computers_configpolicies_enabled setting; the
|
|
models register unconditionally (SQLAlchemy needs them), but routes, panels, and
|
|
enforcement stay inert while the flag is off, and the migration downgrade drops
|
|
every table for a clean rollback.
|
|
"""
|
|
|
|
from shopdb.api import db, BaseModel
|
|
from .jsontext import JSONText
|
|
|
|
|
|
# Provenance of a policy row.
|
|
# leaves room for `geenforce` (or another manager) to own rows later without a
|
|
# schema change - an imported row is read-only in the UI and reconciled by its
|
|
# owner. Kept a plain string, not an enum column, so adding a source is data.
|
|
POLICY_SOURCE_LOCAL = 'local'
|
|
|
|
# Product-owned rows we ship (the shopfloor app monitor bundle). Same read-only
|
|
# treatment as any non-local source: the CRUD API refuses to edit or delete
|
|
# them, the seeder reconciles them. Distinct from 'local' so a site's own
|
|
# policies are never confused with what the product shipped.
|
|
POLICY_SOURCE_SHIPPED = 'shipped'
|
|
|
|
|
|
class ConfigurationPolicy(BaseModel):
|
|
"""A single reusable policy: a scope plus the actions it applies.
|
|
|
|
Scope is a boolean expression (AND/OR/NOT over PC types and applications)
|
|
stored as a small typed JSON AST in scopejson; see services/policyscope.py
|
|
for the evaluator. Actions live in ConfigurationPolicyAction rows.
|
|
"""
|
|
__tablename__ = 'configurationpolicies'
|
|
|
|
configurationpolicyid = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(150), unique=True, nullable=False)
|
|
description = db.Column(db.Text)
|
|
|
|
# Boolean target AST: {"type":"and","nodes":[{"type":"pctype","id":3},
|
|
# {"type":"not","node":{"type":"app","id":12}}]}. Validated server-side
|
|
# (known node types, depth cap) before it is stored - see policyscope.
|
|
scopejson = db.Column(JSONText, nullable=True)
|
|
|
|
# GE-Enforce interop seam (see POLICY_SOURCE_LOCAL).
|
|
source = db.Column(db.String(50), default=POLICY_SOURCE_LOCAL, nullable=False)
|
|
|
|
# createddate, modifieddate, isactive come from BaseModel.
|
|
|
|
actions = db.relationship(
|
|
'ConfigurationPolicyAction',
|
|
back_populates='configurationpolicy',
|
|
cascade='all, delete-orphan',
|
|
)
|
|
|
|
__table_args__ = (
|
|
db.Index('idx_configpolicy_source', 'source'),
|
|
db.Index('idx_configpolicy_active', 'isactive'),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<ConfigurationPolicy {self.name}>"
|
|
|
|
|
|
# What a policy action does to a matched endpoint.
|
|
# desired: reconciled desired-state. desiredstate says what it should be
|
|
# (e.g. running=true for mtconnect); the server enqueues the
|
|
# drift-correcting command when telemetry disagrees.
|
|
# gate: allowlist-gating only. The action is PERMITTED (and shown) on
|
|
# matched endpoints; it constrains what may be enqueued, it does not
|
|
# drive anything on its own.
|
|
ACTION_MODE_DESIRED = 'desired'
|
|
ACTION_MODE_GATE = 'gate'
|
|
|
|
|
|
class ConfigurationPolicyAction(BaseModel):
|
|
"""One allowlisted action carried by a ConfigurationPolicy.
|
|
|
|
`action` is a name from plugins/endpoints/commands.py COMMAND_ALLOWLIST;
|
|
it is validated against that allowlist at write time. `mode` selects
|
|
desired-state reconciliation vs allowlist-gating.
|
|
"""
|
|
__tablename__ = 'configurationpolicyactions'
|
|
|
|
configurationpolicyactionid = db.Column(db.Integer, primary_key=True)
|
|
configurationpolicyid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey('configurationpolicies.configurationpolicyid',
|
|
ondelete='CASCADE'),
|
|
nullable=False,
|
|
)
|
|
|
|
# Allowlisted action name (endpoints commands.py). Not an FK - the allowlist
|
|
# is code, not a table - so it is validated in the API layer.
|
|
action = db.Column(db.String(100), nullable=False)
|
|
|
|
mode = db.Column(db.String(20), default=ACTION_MODE_GATE, nullable=False)
|
|
|
|
# Desired-state target for mode=desired, as JSON so different actions can
|
|
# express different targets (e.g. {"running": true}). Null for gate mode.
|
|
desiredstate = db.Column(JSONText, nullable=True)
|
|
|
|
# Optional action parameters passed through to the command (JSON), and an
|
|
# optional reconcile cadence in minutes (null = reconcile on every report).
|
|
params = db.Column(JSONText, nullable=True)
|
|
intervalminutes = db.Column(db.Integer, nullable=True)
|
|
|
|
sortorder = db.Column(db.Integer, default=0, nullable=False)
|
|
|
|
configurationpolicy = db.relationship(
|
|
'ConfigurationPolicy', back_populates='actions')
|
|
|
|
__table_args__ = (
|
|
db.UniqueConstraint('configurationpolicyid', 'action',
|
|
name='uq_configpolicyaction'),
|
|
db.Index('idx_configpolicyaction_policy', 'configurationpolicyid'),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<ConfigurationPolicyAction {self.action} mode={self.mode}>"
|
|
|
|
|
|
class BusinessPolicy(BaseModel):
|
|
"""An ordered collection of Configuration Policies assigned to a fleet.
|
|
|
|
Priority orders Business Policies against each other when several match one
|
|
endpoint: lower priority number is evaluated first, and the first policy to
|
|
speak on a given action name wins (first-match-wins per action).
|
|
"""
|
|
__tablename__ = 'businesspolicies'
|
|
|
|
businesspolicyid = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(150), unique=True, nullable=False)
|
|
description = db.Column(db.Text)
|
|
|
|
# Lower runs first. Ties broken by businesspolicyid for a stable order.
|
|
priority = db.Column(db.Integer, default=100, nullable=False)
|
|
|
|
source = db.Column(db.String(50), default=POLICY_SOURCE_LOCAL, nullable=False)
|
|
|
|
# createddate, modifieddate, isactive come from BaseModel.
|
|
|
|
items = db.relationship(
|
|
'BusinessPolicyItem',
|
|
back_populates='businesspolicy',
|
|
cascade='all, delete-orphan',
|
|
order_by='BusinessPolicyItem.sortorder',
|
|
)
|
|
|
|
__table_args__ = (
|
|
db.Index('idx_businesspolicy_priority', 'priority'),
|
|
db.Index('idx_businesspolicy_active', 'isactive'),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<BusinessPolicy {self.name} priority={self.priority}>"
|
|
|
|
|
|
class BusinessPolicyItem(BaseModel):
|
|
"""Membership of a Configuration Policy in a Business Policy, ordered.
|
|
|
|
sortorder sets the walk order WITHIN a Business Policy; combined with the
|
|
Business Policy priority it gives a fully deterministic fold order for the
|
|
effective-policy resolver.
|
|
"""
|
|
__tablename__ = 'businesspolicyitems'
|
|
|
|
businesspolicyitemid = db.Column(db.Integer, primary_key=True)
|
|
businesspolicyid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey('businesspolicies.businesspolicyid', ondelete='CASCADE'),
|
|
nullable=False,
|
|
)
|
|
configurationpolicyid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey('configurationpolicies.configurationpolicyid',
|
|
ondelete='CASCADE'),
|
|
nullable=False,
|
|
)
|
|
sortorder = db.Column(db.Integer, default=0, nullable=False)
|
|
|
|
businesspolicy = db.relationship('BusinessPolicy', back_populates='items')
|
|
configurationpolicy = db.relationship('ConfigurationPolicy')
|
|
|
|
__table_args__ = (
|
|
db.UniqueConstraint('businesspolicyid', 'configurationpolicyid',
|
|
name='uq_businesspolicyitem'),
|
|
db.Index('idx_bpitem_business', 'businesspolicyid'),
|
|
db.Index('idx_bpitem_config', 'configurationpolicyid'),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return (f"<BusinessPolicyItem bp={self.businesspolicyid} "
|
|
f"cp={self.configurationpolicyid}>")
|