Files
sientia-dataops-opc-ingestor/ingestor/io/slots.py
vitor-aignosi ecb055eefb SIENTIAPDE-1973
SIENTIAPDE-1973: Implement per-connector OPC client certificates and remove legacy read engine.
This update overhauls certificate management, transitioning from a single platform certificate to individual client certificates defined in the slot configuration. The CertProvider now downloads certificates from MinIO based on certificate_id, applying dynamic security policies (mode, policy) from the slot. The legacy READ_ENGINE=legacy option and OpcManager have been entirely removed, standardizing on the ServerReader with strict slot v2 enforcement. Write command resolution (ServerConfigResolver) now consistently uses connectors joined with opc_certificates. The CERT_BUCKET environment variable is no longer used.
2026-10-29 12:39:21 -03:00

52 lines
2.0 KiB
Python

"""Parse the slot v2 the Orchestrator writes to Redis (contract C1, SIENTIAPDE-1974).
Each server carries its connection (`url`, `subscription_period_ms`), its OPC security
(`security`: mode + policy) and its client certificate (`certificate`: certificate_id,
application_uri and the MinIO objects of the cert and the private key; None = connect
without security, D5). Each tag carries `names`: one entry per collection / tag name /
model the reading is written to.
"""
SERVER_FIELDS = ('server_id', 'url', 'subscription_period_ms', 'security', 'certificate')
# What a connection/subscription needs (the pool keys clients by server_id itself).
CONNECTION_FIELDS = tuple(f for f in SERVER_FIELDS if f != 'server_id')
NAME_FIELDS = ('collection', 'tag_name', 'model_id')
def _parse_names(address: str, tag: dict) -> list[dict]:
names = tag.get('names')
if not names:
raise ValueError(f'tag {address!r} has no names')
for name in names:
missing = [field for field in NAME_FIELDS if field not in name]
if missing:
raise ValueError(f'tag {address!r} has a name without {missing}')
return names
def _parse_tag(address: str, tag: dict) -> dict:
return {
'frequency': tag['frequency'],
'names': _parse_names(address, tag),
'active_read': tag.get('active_read', {'enabled': False}),
}
def _parse_server(name: str, server: dict) -> dict:
if not isinstance(server.get('security'), dict):
raise ValueError(f'server {name!r} has no security')
parsed = {field: server.get(field) for field in SERVER_FIELDS}
parsed['name'] = server.get('name', name)
parsed['tags'] = {addr: _parse_tag(addr, tag) for addr, tag in server['tags'].items()}
return parsed
def parse_slot(raw: dict) -> dict:
"""Normalize a raw slot v2 into `{server_name: {..., tags: {addr: tag}}}`.
Raises:
KeyError | TypeError | ValueError: When the slot is not a valid slot v2 (e.g. a
slot v1 with `topics`).
"""
return {name: _parse_server(name, server) for name, server in raw.items()}