computers: collect remote-access protocols (RealVNC and friends)

The accessprotocols / computeraccess tables replaced the old isvnc/iswinrm
booleans and the PC page already badges what a machine exposes, but nothing
kept them current: the 574 rows in place all came from the legacy migration and
have not moved since. The collector schema had no field for them.

Adds 'accessprotocols', a list of catalog names, synced with the same
discipline as the printer links. A reported protocol is activated; a
catalogued one the PC did NOT report is deactivated rather than deleted, so a
manual portoverride survives a service being briefly down. An unknown name
warns and is skipped: the catalog is admin-managed, and a typo on one bay must
not invent a protocol for the whole site.

Presence of the key is what drives the sync. A payload without it leaves every
existing row untouched, which is what protects the migrated rows from a
collector that does not report protocols yet.

Six tests cover recording, case-insensitive matching, deactivation on removal,
the omitted-key no-op, an explicit empty list meaning "exposes nothing", and
that an unknown name never creates a protocol.
This commit is contained in:
cproudlock
2026-08-10 08:34:19 -04:00
parent 939cdd0882
commit efe34034e3
2 changed files with 175 additions and 0 deletions

View File

@@ -124,6 +124,18 @@ class ComputersPlugin(BasePlugin):
'(Win32_Printer): windows name / share / '
'hostname / IP. Unresolved -> warning.'),
},
'accessprotocols': {
'type': 'array',
'items': {'type': 'string'},
'description': ('Remote-access protocols this PC actually '
'exposes, by catalog name (VNC, WinRM, '
'RDP). Presence of the key drives the sync: '
'reported protocols are activated and '
'catalogued ones not reported are '
'deactivated. Omit the key entirely to '
'leave existing rows alone - most came from '
'the legacy isvnc/iswinrm migration.'),
},
},
}
@@ -262,6 +274,9 @@ class ComputersPlugin(BasePlugin):
computerid=comp.computerid, appid=app.appid,
installedversion=version))
# Remote-access protocol sync (only when the payload carried the key).
accessprotocols = self._sync_access_protocols(comp, payload, warnings)
# Printer relationship sync (only when the payload carried printer data).
printerlinks = self._sync_printer_links(comp.asset, payload, warnings)
@@ -281,11 +296,75 @@ class ComputersPlugin(BasePlugin):
'printerlinkcount': len(printerlinks),
'measuringtoollinks': measuringtoollinks,
'measuringtoollinkcount': len(measuringtoollinks),
'accessprotocols': accessprotocols,
},
}
# -- printer relationship sync -----------------------------------------
def _sync_access_protocols(self, comp, payload, warnings):
"""Idempotently sync a PC's remote-access protocols from the collector.
Payload key 'accessprotocols' is a list of protocol NAMES as they appear
in the accessprotocols catalog ('VNC', 'WinRM', 'RDP'), matched
case-insensitively. An unknown name warns and is skipped rather than
creating a protocol: the catalog is admin-managed on purpose, so a
typo on one bay must not invent a protocol for the whole site.
Presence of the key drives the sync, exactly like the printer links: a
reported protocol is activated, and a catalogued protocol the PC did
NOT report is deactivated (not deleted, so a port override survives a
temporary outage). A payload with no 'accessprotocols' key leaves every
existing row untouched - most PCs' rows came from the legacy
isvnc/iswinrm migration and must not be wiped by a collector that
simply does not report them yet.
Returns the list of active protocol names after the sync.
"""
from plugins.computers.models import AccessProtocol, ComputerAccess
if 'accessprotocols' not in payload:
return []
reported = payload.get('accessprotocols') or []
if not isinstance(reported, list):
warnings.append('accessprotocols must be a list of protocol names')
return []
wanted = set()
for name in reported:
name = str(name or '').strip()
if not name:
continue
protocol = AccessProtocol.query.filter(
AccessProtocol.name.ilike(name)).first()
if not protocol:
warnings.append('unknown access protocol: {}'.format(name))
continue
wanted.add(protocol.protocolid)
existing = {row.protocolid: row for row in
ComputerAccess.query.filter_by(computerid=comp.computerid).all()}
for protocolid in wanted:
row = existing.get(protocolid)
if row:
row.isactive = True
else:
db.session.add(ComputerAccess(
computerid=comp.computerid, protocolid=protocolid,
isactive=True))
# Deactivate what the PC no longer exposes. Kept as rows so a manual
# portoverride is not lost the first time a service is briefly down.
for protocolid, row in existing.items():
if protocolid not in wanted:
row.isactive = False
names = [p.name for p in AccessProtocol.query.filter(
AccessProtocol.protocolid.in_(wanted)).all()] if wanted else []
return sorted(names)
def _sync_printer_links(self, pcasset, payload, warnings):
"""Idempotently sync PC->printer relationships from collector printer data.