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.
52 lines
2.0 KiB
Python
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()}
|