Make the server authoritative for what endpoints watch and launch
Fleet health showed dncMain.exe red on endpoints that never ran eDNC. Three
layers each re-added the monitor, so fixing any one of them in isolation left
the others to put it back.
The installer baked a watch list into appsettings.json on every package, and
appsettings.json is never rewritten on the box, so an entry outlived whatever
policy justified it. The server auto-attached shipped monitors by PC type name.
The agent then UNIONED the server's resolved list with its local one, so an
entry could never be removed from the server side at all.
Now: the installer seeds nothing, auto-attach by PC type is gone, and
MonitorConfig.Merge replaces the local list with the server's. Null and empty
stay distinct - null means the fetch failed so keep what we have, empty means
watch nothing. Collapsing those blanks monitoring fleet-wide on one failed HTTP
call, or makes detaching the last monitor silently do nothing forever.
Ingest filters reported services against the resolved monitors as well. The
agent config sits on the endpoint, so it is not a trustworthy account of what
that endpoint should be reporting.
Launch paths move the same way, which exposed an older bug. MTConnect installs
to ProgramData and starts through run.bat, but LaunchPathPolicy approved only
the Program Files roots, so the launchpath on its shipped monitor row - seeded
in 0006, present from the start - was rejected every time as outside every
approved root. That was invisible because a local launchpaths entry wins in
ResolveLaunchPath and never reaches validation; remote start worked only
through the bypass. The same precedence meant editing an eDNC, UDC or MTConnect
path in the WebUI could never reach an existing endpoint.
So ProgramData is approved and the local launch map ships empty. ProgramData is
world-writable and the agent launches as SYSTEM, which is why it was excluded
originally; it is deliberately too broad for now and carries a REVISIT comment.
The remaining gates still hold - admin-configured monitor row, exists,
launchable extension, re-checked after symlink resolution. Narrowing it to
ProgramData\udc needs the fleet's non-Program-Files install locations
confirmed first.
Also seeds the Office ProPlus LTSC monitor templates, x64 and x86 as separate
rows since they differ only by path, and adds an agent test project. It links
the platform-pure sources rather than referencing the net8.0-windows assembly
so the semantics run on the Linux CI runners. Writing those tests caught a
merge bug where only the dedupe key was trimmed and the stored name was not.
Note for the wj merge: plugins/usb/frontend/views/usbSettings.spec.js fails on
this branch and is untouched here. Commit da91a29 added four kiosk settings the
save assertion does not expect. The spec looks like the stale side, but that is
the usb author's call - flagging rather than guessing.
This commit is contained in:
207
tests/test_plugins/test_endpoints_ingest_authority.py
Normal file
207
tests/test_plugins/test_endpoints_ingest_authority.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""The server decides what is monitored, not the endpoint.
|
||||
|
||||
The agent merges its local appsettings.json watch list into the server-resolved
|
||||
one, so what an endpoint reports is not by itself evidence that the server ever
|
||||
asked for it. Anyone able to edit that file on the box - or an old package with
|
||||
monitors baked in by the installer - can put entries into the report that no
|
||||
site policy attached.
|
||||
|
||||
The interesting failure is not noise. It is a never-running entry that parks an
|
||||
asset at a permanent condition: an operator who learns to ignore a perpetually
|
||||
amber endpoint stops reading the signal that says a real service died. So the
|
||||
endpoint can blind the fleet view by TALKING, not only by going quiet.
|
||||
|
||||
These tests pin that reported entries are filtered against the server's own
|
||||
resolved monitor list before they are stored.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from shopdb.core.models import Asset, AssetType, Setting
|
||||
|
||||
|
||||
def _computer(db, hostname, computertype):
|
||||
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):
|
||||
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='Site {}'.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 _stored_services(db, assetid):
|
||||
from plugins.endpoints.models import EndpointState
|
||||
|
||||
state = EndpointState.query.filter_by(assetid=assetid).first()
|
||||
if state is None or not state.servicesjson:
|
||||
return []
|
||||
return json.loads(state.servicesjson)
|
||||
|
||||
|
||||
def _report(app, hostname, services):
|
||||
from plugins.endpoints.plugin import EndpointsPlugin
|
||||
|
||||
plugin = EndpointsPlugin()
|
||||
return plugin.apply_collector_payload({
|
||||
'hostname': hostname,
|
||||
'services': services,
|
||||
})
|
||||
|
||||
|
||||
def test_an_unmonitored_reported_service_is_not_stored(client, db, app):
|
||||
"""The baked-in installer case: the endpoint reports dncMain.exe, but no
|
||||
site policy attaches it, so it never reaches the stored snapshot."""
|
||||
asset, _ctype = _computer(db, 'INGEST1', 'Shopfloor PC')
|
||||
db.session.commit()
|
||||
|
||||
_report(app, 'INGEST1', [
|
||||
{'name': 'dncMain.exe', 'kind': 'process', 'state': 'stopped'},
|
||||
])
|
||||
db.session.commit()
|
||||
|
||||
assert _stored_services(db, asset.assetid) == []
|
||||
|
||||
|
||||
def test_a_policy_attached_service_is_stored(client, db, app):
|
||||
"""The control case: filtering must not throw away legitimate reports."""
|
||||
from plugins.endpoints.models import EndpointMonitor
|
||||
|
||||
asset, ctype = _computer(db, 'INGEST2', 'Shopfloor PC')
|
||||
monitor = EndpointMonitor(
|
||||
kind='process', displayname='eDNC', processname='dncMain.exe',
|
||||
source='shipped', isactive=True,
|
||||
)
|
||||
db.session.add(monitor)
|
||||
db.session.flush()
|
||||
_attach_monitor_to_pctype(db, monitor, ctype.computertypeid)
|
||||
|
||||
_report(app, 'INGEST2', [
|
||||
{'name': 'dncMain.exe', 'kind': 'process', 'state': 'stopped'},
|
||||
])
|
||||
db.session.commit()
|
||||
|
||||
stored = _stored_services(db, asset.assetid)
|
||||
assert [entry['name'] for entry in stored] == ['dncMain.exe']
|
||||
|
||||
|
||||
def test_a_spoofed_never_running_service_cannot_park_the_asset_amber(
|
||||
client, db, app):
|
||||
"""The attack that matters: an entry the server never attached cannot
|
||||
manufacture a standing health condition."""
|
||||
from plugins.endpoints.services.health import _service_conditions
|
||||
from plugins.endpoints.models import EndpointState
|
||||
|
||||
asset, _ctype = _computer(db, 'INGEST3', 'Shopfloor PC')
|
||||
db.session.commit()
|
||||
|
||||
_report(app, 'INGEST3', [
|
||||
{'name': 'totally-made-up.exe', 'kind': 'process', 'state': 'stopped'},
|
||||
])
|
||||
db.session.commit()
|
||||
|
||||
state = EndpointState.query.filter_by(assetid=asset.assetid).first()
|
||||
assert _service_conditions(state, None) == []
|
||||
|
||||
|
||||
def test_matching_ignores_case_on_the_wire_name(client, db, app):
|
||||
"""The agent reports whatever Windows hands it; 0006 seeds 'dncMain.exe'
|
||||
while the launch path says 'DncMain.exe'. Identity is case-insensitive or
|
||||
the filter would silently drop real monitors."""
|
||||
from plugins.endpoints.models import EndpointMonitor
|
||||
|
||||
asset, ctype = _computer(db, 'INGEST4', 'Shopfloor PC')
|
||||
monitor = EndpointMonitor(
|
||||
kind='process', displayname='eDNC', processname='dncMain.exe',
|
||||
source='shipped', isactive=True,
|
||||
)
|
||||
db.session.add(monitor)
|
||||
db.session.flush()
|
||||
_attach_monitor_to_pctype(db, monitor, ctype.computertypeid)
|
||||
|
||||
_report(app, 'INGEST4', [
|
||||
{'name': 'DNCMAIN.EXE', 'kind': 'process', 'state': 'running'},
|
||||
])
|
||||
db.session.commit()
|
||||
|
||||
stored = _stored_services(db, asset.assetid)
|
||||
assert [entry['name'] for entry in stored] == ['DNCMAIN.EXE']
|
||||
|
||||
|
||||
def test_an_omitted_services_key_does_not_wipe_the_last_snapshot(
|
||||
client, db, app):
|
||||
"""Filtering sits inside the existing presence guard: a report that omits
|
||||
services leaves the previous snapshot standing."""
|
||||
from plugins.endpoints.models import EndpointMonitor
|
||||
|
||||
asset, ctype = _computer(db, 'INGEST5', 'Shopfloor PC')
|
||||
monitor = EndpointMonitor(
|
||||
kind='service', displayname='MTConnect', servicename='MTConnect UDC Agent',
|
||||
source='shipped', isactive=True,
|
||||
)
|
||||
db.session.add(monitor)
|
||||
db.session.flush()
|
||||
_attach_monitor_to_pctype(db, monitor, ctype.computertypeid)
|
||||
|
||||
_report(app, 'INGEST5', [
|
||||
{'name': 'MTConnect UDC Agent', 'kind': 'service', 'state': 'running'},
|
||||
])
|
||||
db.session.commit()
|
||||
|
||||
from plugins.endpoints.plugin import EndpointsPlugin
|
||||
EndpointsPlugin().apply_collector_payload({'hostname': 'INGEST5'})
|
||||
db.session.commit()
|
||||
|
||||
stored = _stored_services(db, asset.assetid)
|
||||
assert [entry['name'] for entry in stored] == ['MTConnect UDC Agent']
|
||||
172
tests/test_plugins/test_endpoints_office_monitor_templates.py
Normal file
172
tests/test_plugins/test_endpoints_office_monitor_templates.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""The Office ProPlus LTSC monitor templates seeded by endpoints 0025.
|
||||
|
||||
Runs the migration against a bare engine and checks the rows it produces, rather
|
||||
than asserting against the module's constants - a test that reads the same list
|
||||
the code writes proves nothing about what lands in the table.
|
||||
|
||||
The behaviour worth pinning here is that 0025 seeds on a site that ALREADY has
|
||||
monitors. 0006 deliberately bails out when the catalog is non-empty (it is a
|
||||
first-install seed), and copying that guard into 0025 would have made it a no-op
|
||||
everywhere the templates are actually wanted.
|
||||
"""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import create_engine, text
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
_ENDPOINTS_VERSIONS = _ROOT / 'plugins' / 'endpoints' / 'migrations' / 'versions'
|
||||
|
||||
OFFICE_MIGRATION = '0025_office_monitor_templates.py'
|
||||
|
||||
_EXPECTED_NAMES = {
|
||||
'Office Word (x64)', 'Office Word (x86)',
|
||||
'Office Excel (x64)', 'Office Excel (x86)',
|
||||
'Office PowerPoint (x64)', 'Office PowerPoint (x86)',
|
||||
'Office Outlook (x64)', 'Office Outlook (x86)',
|
||||
'Office Access (x64)', 'Office Access (x86)',
|
||||
}
|
||||
|
||||
|
||||
def _load(path, prefix):
|
||||
spec = importlib.util.spec_from_file_location(prefix + path.stem, str(path))
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _run(connection, *loaded):
|
||||
operations = Operations(MigrationContext.configure(connection))
|
||||
operations._install_proxy()
|
||||
try:
|
||||
for module in loaded:
|
||||
module.upgrade()
|
||||
finally:
|
||||
operations._remove_proxy()
|
||||
connection.commit()
|
||||
|
||||
|
||||
def _office_module():
|
||||
return _load(_ENDPOINTS_VERSIONS / OFFICE_MIGRATION, 'endpoints_mig_0025_')
|
||||
|
||||
|
||||
def _seeded(connection):
|
||||
_run(
|
||||
connection,
|
||||
_load(_ENDPOINTS_VERSIONS / '0006_endpoint_monitors.py',
|
||||
'endpoints_mig_0006_'),
|
||||
_office_module(),
|
||||
)
|
||||
|
||||
|
||||
def _monitors(connection):
|
||||
rows = connection.execute(text(
|
||||
'SELECT displayname, kind, processname, launchpath, source '
|
||||
'FROM endpointmonitors')).all()
|
||||
return {row[0]: row for row in rows}
|
||||
|
||||
|
||||
def _connection():
|
||||
return create_engine('sqlite://').connect()
|
||||
|
||||
|
||||
def test_every_office_app_is_seeded_at_both_install_roots():
|
||||
with _connection() as connection:
|
||||
_seeded(connection)
|
||||
by_name = _monitors(connection)
|
||||
|
||||
assert _EXPECTED_NAMES.issubset(set(by_name))
|
||||
|
||||
|
||||
def test_access_is_included():
|
||||
"""Called out explicitly because it is the one that gets forgotten."""
|
||||
with _connection() as connection:
|
||||
_seeded(connection)
|
||||
by_name = _monitors(connection)
|
||||
|
||||
assert by_name['Office Access (x64)'][2] == 'MSACCESS.EXE'
|
||||
assert by_name['Office Access (x86)'][2] == 'MSACCESS.EXE'
|
||||
|
||||
|
||||
def test_the_architectures_point_at_different_install_roots():
|
||||
with _connection() as connection:
|
||||
_seeded(connection)
|
||||
by_name = _monitors(connection)
|
||||
|
||||
x64 = by_name['Office Excel (x64)'][3]
|
||||
x86 = by_name['Office Excel (x86)'][3]
|
||||
|
||||
assert x64 == r'C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE'
|
||||
assert x86 == (r'C:\Program Files (x86)\Microsoft Office\root\Office16'
|
||||
r'\EXCEL.EXE')
|
||||
|
||||
|
||||
def test_the_rows_are_shipped_process_monitors():
|
||||
with _connection() as connection:
|
||||
_seeded(connection)
|
||||
by_name = _monitors(connection)
|
||||
|
||||
for name in _EXPECTED_NAMES:
|
||||
assert by_name[name][1] == 'process', name
|
||||
assert by_name[name][4] == 'shipped', name
|
||||
|
||||
|
||||
def test_seeding_is_idempotent():
|
||||
"""The kind/displayname unique constraint would raise on a re-run."""
|
||||
with _connection() as connection:
|
||||
_seeded(connection)
|
||||
_run(connection, _office_module())
|
||||
by_name = _monitors(connection)
|
||||
|
||||
assert len([name for name in by_name if name.startswith('Office ')]) == 10
|
||||
|
||||
|
||||
def test_templates_are_seeded_on_a_site_that_already_has_monitors():
|
||||
"""0006 bails out when the catalog is non-empty; 0025 must not.
|
||||
|
||||
Every existing site has the four shipped monitors from 0006, so copying that
|
||||
guard would make this migration a no-op exactly where it is needed.
|
||||
"""
|
||||
with _connection() as connection:
|
||||
_run(connection,
|
||||
_load(_ENDPOINTS_VERSIONS / '0006_endpoint_monitors.py',
|
||||
'endpoints_mig_0006_'))
|
||||
before = _monitors(connection)
|
||||
assert before, 'expected the 0006 shipped rows to be present'
|
||||
|
||||
_run(connection, _office_module())
|
||||
after = _monitors(connection)
|
||||
|
||||
assert _EXPECTED_NAMES.issubset(set(after))
|
||||
assert 'eDNC' in after
|
||||
|
||||
|
||||
def test_seeding_does_not_disturb_the_existing_shipped_monitors():
|
||||
with _connection() as connection:
|
||||
_seeded(connection)
|
||||
by_name = _monitors(connection)
|
||||
|
||||
assert by_name['eDNC'][2] == 'dncMain.exe'
|
||||
assert by_name['Universal Data Collector'][2] == 'udc.exe'
|
||||
|
||||
|
||||
def test_downgrade_removes_the_seeded_templates():
|
||||
module = _office_module()
|
||||
with _connection() as connection:
|
||||
_seeded(connection)
|
||||
|
||||
operations = Operations(MigrationContext.configure(connection))
|
||||
operations._install_proxy()
|
||||
try:
|
||||
module.downgrade()
|
||||
finally:
|
||||
operations._remove_proxy()
|
||||
connection.commit()
|
||||
|
||||
by_name = _monitors(connection)
|
||||
|
||||
assert not _EXPECTED_NAMES & set(by_name)
|
||||
assert 'eDNC' in by_name
|
||||
@@ -253,30 +253,69 @@ def test_a_shipped_monitor_keeps_the_identity_the_policy_matches_on(
|
||||
assert monitor.displayname == 'eDNC'
|
||||
|
||||
|
||||
# --- which PC type the shipped bundle attaches to is a setting ---------------
|
||||
# --- enabling a monitor must never require a new agent ----------------------
|
||||
|
||||
def test_shipped_monitors_follow_the_configured_pctype(client, db, app):
|
||||
from plugins.endpoints.api.monitors import (
|
||||
SHIPPED_PCTYPE_SETTING, _resolve_pctype_monitors)
|
||||
def test_a_monitor_created_today_reaches_the_agent_watch_list(client, db, app,
|
||||
auth_headers):
|
||||
"""Create a monitor over the API, attach it by policy, and it is in the
|
||||
agent's watch list on the next fetch.
|
||||
|
||||
This is the no-redeploy contract, and it is the property most easily lost
|
||||
in a refactor. The agent reads this route every cycle and merges the result
|
||||
over appsettings.json, so a monitor that appears here is live on the floor
|
||||
without shipping a new exe. Nothing in this test touches the agent.
|
||||
"""
|
||||
asset, ctype = _computer(db, 'NOREDEPLOY1', 'CMM PC')
|
||||
|
||||
created = client.post('/api/endpoints/monitors', json={
|
||||
'kind': 'service', 'displayname': 'Print Spooler',
|
||||
'servicename': 'Spooler',
|
||||
}, headers=auth_headers)
|
||||
assert created.status_code in (200, 201)
|
||||
|
||||
from plugins.endpoints.models import EndpointMonitor
|
||||
monitor = EndpointMonitor.query.filter_by(servicename='Spooler').first()
|
||||
assert monitor is not None
|
||||
_attach_monitor_to_pctype(db, monitor, ctype.computertypeid)
|
||||
|
||||
response = client.get(RESOLVED_URL, query_string={'hostname': 'NOREDEPLOY1'},
|
||||
headers=_collector_headers(app))
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()['data']
|
||||
assert {'name': 'Spooler', 'app': 'Print Spooler'} in data['services']
|
||||
|
||||
|
||||
# --- shipped monitors attach by POLICY ONLY ---------------------------------
|
||||
|
||||
def test_shipped_monitors_are_not_attached_by_pc_type_name(client, db, app):
|
||||
"""The auto-attach path is gone: a shipped monitor is not expected anywhere
|
||||
until a Site Policy composes it.
|
||||
|
||||
Replaces two tests that pinned the old behaviour - that shipped monitors
|
||||
followed a configurable PC type name, and that blanking the setting turned
|
||||
them off. Both described a route that should not exist. Keeping them would
|
||||
have meant keeping the bug they documented: every endpoint typed
|
||||
'Shopfloor PC' silently expected eDNC, UDC and MTConnect, which is what put
|
||||
dncMain.exe in Fleet health for all endpoints.
|
||||
"""
|
||||
from plugins.endpoints.api.monitors import _resolve_pctype_monitors
|
||||
|
||||
_shipped(db)
|
||||
Setting.set(SHIPPED_PCTYPE_SETTING, 'Shop Floor Workstation')
|
||||
db.session.commit()
|
||||
|
||||
# The plant's own name for the type resolves the bundle...
|
||||
assert _resolve_pctype_monitors('Shopfloor PC') == []
|
||||
assert _resolve_pctype_monitors('Shop Floor Workstation') == []
|
||||
|
||||
|
||||
def test_a_shipped_monitor_still_resolves_once_policy_attaches_it(
|
||||
client, db, app):
|
||||
"""Removing auto-attach must not make shipped monitors unusable - a site
|
||||
composes them like any other monitor."""
|
||||
from plugins.endpoints.api.monitors import _resolve_pctype_monitors
|
||||
|
||||
asset, ctype = _computer(db, 'SHIPPEDPC1', 'Shopfloor PC')
|
||||
monitor = _shipped(db)
|
||||
_attach_monitor_to_pctype(db, monitor, ctype.computertypeid)
|
||||
|
||||
assert [m.displayname for m in
|
||||
_resolve_pctype_monitors('Shop Floor Workstation')] == ['eDNC']
|
||||
# ...and the name this shipped with no longer does.
|
||||
assert _resolve_pctype_monitors('Shopfloor PC') == []
|
||||
|
||||
|
||||
def test_a_blank_shipped_pctype_attaches_the_bundle_to_nothing(client, db, app):
|
||||
"""The neutral behaviour for a site that does not want the bundle: off,
|
||||
rather than silently attached to whatever happens to match."""
|
||||
from plugins.endpoints.api.monitors import (
|
||||
SHIPPED_PCTYPE_SETTING, _resolve_pctype_monitors)
|
||||
|
||||
_shipped(db)
|
||||
Setting.set(SHIPPED_PCTYPE_SETTING, '')
|
||||
db.session.commit()
|
||||
assert _resolve_pctype_monitors('Shopfloor PC') == []
|
||||
_resolve_pctype_monitors('Shopfloor PC')] == ['eDNC']
|
||||
|
||||
@@ -351,3 +351,33 @@ def test_install_script_helper_without_pins_or_key():
|
||||
assert '[string[]] $ServerPins = @()' in script
|
||||
assert 'CollectorKey' not in script
|
||||
assert 'ServerPins left as is' in script
|
||||
|
||||
|
||||
def test_the_package_ships_no_baked_in_monitors(client, db, auth_headers, servercert):
|
||||
"""The server owns the watch list, so the installer must seed nothing.
|
||||
|
||||
Seeding monitors here is what put dncMain.exe on every endpoint in Fleet
|
||||
health: the entry rode into appsettings.json on every package, and
|
||||
appsettings.json is never rewritten on the box, so it outlived any policy
|
||||
that justified it. The agent now replaces its watch list from
|
||||
/api/endpoints/monitors/resolved, which makes a seeded entry pointless as
|
||||
well as harmful.
|
||||
"""
|
||||
settings = json.loads(_files(_build(
|
||||
client, auth_headers, collectorkey=SECRET))['appsettings.json'])
|
||||
|
||||
assert settings['monitors'] == {'processes': [], 'services': []}
|
||||
|
||||
|
||||
def test_the_package_ships_no_baked_in_launch_paths(client, db, auth_headers, servercert):
|
||||
"""Launch paths are server authoritative, so the installer seeds none.
|
||||
|
||||
A local launchpaths entry wins over the server-provisioned path AND skips
|
||||
LaunchPathPolicy validation, so a seeded entry both outlives the config that
|
||||
justified it and makes WebUI edits never reach the endpoint. eDNC, UDC and
|
||||
MTConnect carry their launchpath on the shipped monitor rows instead.
|
||||
"""
|
||||
settings = json.loads(_files(_build(
|
||||
client, auth_headers, collectorkey=SECRET))['appsettings.json'])
|
||||
|
||||
assert settings['launchpaths'] == {}
|
||||
|
||||
Reference in New Issue
Block a user