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.
173 lines
6.5 KiB
Python
173 lines
6.5 KiB
Python
"""Seed test data for the OPC Gateway e2e validation session (SIENTIAPDE-1972).
|
|
|
|
Populates:
|
|
1. Mongo `connectors` + `connector_tags` (each tag with `metadata: {}` — required
|
|
for documentation, per the feature owner).
|
|
2. Redis slot v2 (`slot:opc_tags:1`, contract C1) so the subscription path collects them.
|
|
3. Postgres test predictions table (with the `write_status` column) + one row the
|
|
NATS write test can UPDATE (correlated by timestamp + model_id).
|
|
|
|
Idempotent: connectors/tags are upserted, the slot is overwritten, the table uses
|
|
CREATE IF NOT EXISTS + ON CONFLICT DO NOTHING.
|
|
|
|
Usage (from the repo root, sientia-dataops conda env):
|
|
python scripts/seed_e2e.py --print-only # dry-run: show docs/SQL, touch nothing
|
|
python scripts/seed_e2e.py # seed Mongo + Redis + Postgres
|
|
|
|
Env (same names the gateway/app use):
|
|
MONGODB_URL/MONGODB_USERNAME/MONGODB_PASSWORD/MONGODB_DATABASE
|
|
REDIS_HOST/REDIS_PORT POSTGRES_HOST/PORT/USER/PASSWORD/DBNAME
|
|
OPC_SERVER_URL (default opc.tcp://localhost:49320 — KepServer default port)
|
|
|
|
# ponytail: plain sync pymongo/redis/sqlalchemy, no sientia_do async plumbing —
|
|
# this is a test fixture, not production code.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
try: # optional: local dev reads .env; in-cluster env comes from Helm
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
except ImportError:
|
|
pass
|
|
|
|
OPC_URL = os.getenv('OPC_SERVER_URL', 'opc.tcp://localhost:49320')
|
|
|
|
# No client certificate: the gateway connects without security (SIENTIAPDE-1974 D5).
|
|
# To test security, point client_certificate_id at an active client certificate of
|
|
# opc_certificates and set mode/policy (e.g. SignAndEncrypt / Basic256Sha256).
|
|
SECURITY = {'mode': 'None', 'policy': 'None', 'client_certificate_id': None}
|
|
|
|
CONNECTOR = {
|
|
'connector_id': 'e2e-kepserver',
|
|
'type': 'opc_ua',
|
|
'name': 'e2e-kepserver',
|
|
'url': OPC_URL,
|
|
'subscription_period_ms': 1000,
|
|
'security': SECURITY,
|
|
'status': 'Active',
|
|
'description': 'OPC Gateway e2e validation connector (SIENTIAPDE-1972)',
|
|
}
|
|
|
|
# metadata: {} on every tag — feature owner asked test tags to carry the field.
|
|
TAGS = [
|
|
{'tag_id': 9101, 'connector_id': 'e2e-kepserver', 'address': 'ns=2;s=Simulation Examples.Functions.Ramp1',
|
|
'tag_name': 'e2e_ramp', 'category': 'Read', 'data_type': 'float', 'unit': '-',
|
|
'status': 'Active', 'description': 'e2e read tag', 'metadata': {}},
|
|
{'tag_id': 9102, 'connector_id': 'e2e-kepserver', 'address': 'ns=2;s=Simulation Examples.Functions.User1',
|
|
'tag_name': 'e2e_write_target', 'category': 'Write', 'data_type': 'float', 'unit': '-',
|
|
'status': 'Active', 'description': 'e2e write tag', 'metadata': {}},
|
|
]
|
|
|
|
# Slot v2 (contract C1), as the Orchestrator writes it for the connector above.
|
|
SLOT = {
|
|
'e2e-kepserver': {
|
|
'name': 'e2e-kepserver',
|
|
'server_id': 'e2e-kepserver',
|
|
'url': OPC_URL,
|
|
'subscription_period_ms': 1000,
|
|
'security': {'mode': SECURITY['mode'], 'policy': SECURITY['policy']},
|
|
'certificate': None,
|
|
'tags': {
|
|
TAGS[0]['address']: {
|
|
'frequency': 5000,
|
|
'active_read': {'enabled': False},
|
|
'names': [
|
|
{
|
|
'collection': 'raw_e2e-gateway-test',
|
|
'tag_name': TAGS[0]['tag_name'],
|
|
'model_id': '9001',
|
|
'model_name': 'e2e-model',
|
|
}
|
|
],
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
PREDICTIONS_SQL = """
|
|
CREATE SCHEMA IF NOT EXISTS sientia_data;
|
|
CREATE TABLE IF NOT EXISTS sientia_data.e2e_predictions (
|
|
model_id text NOT NULL,
|
|
prediction double precision,
|
|
prediction_confidence double precision,
|
|
timestamp text NOT NULL,
|
|
write_status text,
|
|
PRIMARY KEY (model_id, timestamp)
|
|
);
|
|
INSERT INTO sientia_data.e2e_predictions (model_id, prediction, prediction_confidence, timestamp)
|
|
VALUES ('9001', 42.5, 0.97, '2026-07-15T12:00:00Z')
|
|
ON CONFLICT DO NOTHING;
|
|
"""
|
|
|
|
# The matching NATS write command for the row above (paste into `nats pub opc.write '...'`).
|
|
NATS_CMD = {
|
|
'msg_id': 'e2e-0001', 'server_id': 'e2e-kepserver', 'addr': TAGS[1]['address'],
|
|
'value': 42.5, 'data_type': 'float', 'tag_type': 'prediction',
|
|
'model_id': '9001', 'model_name': 'e2e-model',
|
|
'schema': 'sientia_data', 'table_name': 'e2e_predictions',
|
|
'timestamp': '2026-07-15T12:00:00Z',
|
|
}
|
|
|
|
|
|
def seed_mongo():
|
|
from pymongo import MongoClient
|
|
|
|
url = os.getenv('MONGODB_URL', 'localhost:27017')
|
|
user, pwd = os.getenv('MONGODB_USERNAME', 'sientia'), os.getenv('MONGODB_PASSWORD', 'sientia')
|
|
db = MongoClient(f'mongodb://{user}:{pwd}@{url}')[os.getenv('MONGODB_DATABASE', 'sientia')]
|
|
db['connectors'].replace_one({'connector_id': CONNECTOR['connector_id']}, CONNECTOR, upsert=True)
|
|
for tag in TAGS:
|
|
db['connector_tags'].replace_one({'tag_id': tag['tag_id']}, tag, upsert=True)
|
|
print(f'mongo: upserted 1 connector + {len(TAGS)} tags (metadata: {{}})')
|
|
|
|
|
|
def seed_redis():
|
|
import redis
|
|
|
|
r = redis.Redis(host=os.getenv('REDIS_HOST', 'localhost'), port=int(os.getenv('REDIS_PORT', '6379')))
|
|
r.set('slot:opc_tags:1', json.dumps(SLOT))
|
|
print('redis: wrote slot:opc_tags:1 (v2)')
|
|
|
|
|
|
def seed_postgres():
|
|
from sqlalchemy import create_engine, text
|
|
|
|
host, port = os.getenv('POSTGRES_HOST', 'localhost'), os.getenv('POSTGRES_PORT', '5432')
|
|
user, pwd = os.getenv('POSTGRES_USER', 'sientia'), os.getenv('POSTGRES_PASSWORD', 'sientia')
|
|
dbname = os.getenv('POSTGRES_DBNAME', 'sientia')
|
|
engine = create_engine(f'postgresql://{user}:{pwd}@{host}:{port}/{dbname}')
|
|
with engine.begin() as conn:
|
|
for stmt in filter(str.strip, PREDICTIONS_SQL.split(';')):
|
|
conn.execute(text(stmt))
|
|
print('postgres: sientia_data.e2e_predictions ready (write_status column + 1 row)')
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--print-only', action='store_true', help='show docs/SQL, touch nothing')
|
|
args = parser.parse_args()
|
|
|
|
if args.print_only:
|
|
print('connector:', json.dumps(CONNECTOR, indent=2))
|
|
print('tags:', json.dumps(TAGS, indent=2))
|
|
print('slot v2:', json.dumps(SLOT, indent=2))
|
|
print('sql:', PREDICTIONS_SQL)
|
|
print('nats write cmd:', json.dumps(NATS_CMD, indent=2))
|
|
return 0
|
|
|
|
seed_mongo()
|
|
seed_redis()
|
|
seed_postgres()
|
|
print('\nNATS write command for the session:')
|
|
print(f"nats pub opc.write '{json.dumps(NATS_CMD)}'")
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|