Merge pull request #4 from Aignosi/SIENTIAPDE-1049-lidar-com-erros-de-reconexao-no-ingestor-opc

Sientiapde 1049 lidar com erros de reconexao no ingestor opc
This commit is contained in:
Matheus Demoner
2025-05-21 13:09:36 -03:00
committed by GitHub
16 changed files with 321 additions and 196 deletions

3
.coveragerc Normal file
View File

@@ -0,0 +1,3 @@
[run]
omit =
ingestor/app.py

View File

@@ -80,4 +80,5 @@ jobs:
-Dsonar.host.url=$SONAR_HOST_URL \ -Dsonar.host.url=$SONAR_HOST_URL \
-Dsonar.token=$SONAR_TOKEN \ -Dsonar.token=$SONAR_TOKEN \
-Dsonar.python.version=3.11 \ -Dsonar.python.version=3.11 \
-Dsonar.projectVersion=1.0.0 -Dsonar.projectVersion=1.0.1 \
-Dsonar.coverage.exclusions=ingestor/app.py

View File

@@ -1,4 +1,4 @@
# sientia-dataops-opc-gateway # sientia-dataops-opc-ingestor
OPC gateway to manage Scouter pipelines OPC gateway to manage Scouter pipelines
## Local tests ## Local tests

0
app.py
View File

View File

@@ -1,19 +1,19 @@
version: '3.8' version: '3.8'
services: services:
ingestor: # ingestor:
build: # build:
context: . # context: .
environment: # environment:
HOSTNAME: ingestor # HOSTNAME: ingestor
container_name: ingestor # container_name: ingestor
depends_on: # depends_on:
- kafka # - kafka
- redis # - redis
networks: # networks:
- kafka-net # - kafka-net
env_file: # env_file:
- .env # - .env
zookeeper: zookeeper:
image: confluentinc/cp-zookeeper:latest image: confluentinc/cp-zookeeper:latest

View File

@@ -1,19 +1,42 @@
from time import sleep from threading import Event
import signal
import os
import traceback
from ingestor.ingestor import Ingestor from ingestor.ingestor import Ingestor
exit_signal = Event()
def main(): def main():
ingestor = Ingestor() ingestor = Ingestor()
ingestor.prepare_ingestor() ingestor.prepare_ingestor()
ingestor.logger.info("Ingestor prepared. Starting main loop.")
while True: while not exit_signal.is_set():
try:
ingestor.loop()
ingestor.loop() exit_signal.wait(ingestor.poll_interval)
# Sleep for poll interval except Exception:
sleep(ingestor.poll_interval) print("Exception in main loop. Setting exit_signal flag.")
traceback.print_exc()
exit_signal.set()
ingestor.shutdown()
ingestor.logger.info("Main loop exit_signaled.")
os._exit(0)
def signal_handler(_signum, _frame):
print(f"Received signal {_signum}. Setting exit_signal flag.")
exit_signal.set()
if __name__ == "__main__": if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGHUP, signal_handler)
main() main()

View File

@@ -1,8 +1,11 @@
from logging import Formatter, StreamHandler, getLogger from logging import Formatter, StreamHandler, getLogger
from os import getenv from os import getenv
from copy import deepcopy
from typing import Dict, Any
from sientia_do.notifications.handlers import NotificationHandler
from ingestor.managers.ingestor_manager import IngestorManager from ingestor.managers.ingestor_manager import IngestorManager
from sientia_do.notifications.handlers import NotificationHandler
class Ingestor: class Ingestor:
@@ -10,7 +13,8 @@ class Ingestor:
""" """
Initializes the ingestor with configuration values retrieved from environment variables. Initializes the ingestor with configuration values retrieved from environment variables.
Environment Variables: Environment Variables:
KAFKA_SERVERS (str): Comma-separated list of Kafka server addresses. Defaults to "localhost:9092". KAFKA_SERVERS (str): Comma-separated list of Kafka server addresses.
Defaults to "localhost:9092".
REDIS_HOST (str): Hostname of the Redis server. Defaults to "localhost". REDIS_HOST (str): Hostname of the Redis server. Defaults to "localhost".
REDIS_PORT (int): Port number of the Redis server. Defaults to 6379. REDIS_PORT (int): Port number of the Redis server. Defaults to 6379.
LEASE_TTL (int): Time-to-live for leases in seconds. Defaults to 10. LEASE_TTL (int): Time-to-live for leases in seconds. Defaults to 10.
@@ -29,13 +33,13 @@ class Ingestor:
kafka_servers = getenv("KAFKA_SERVERS", "localhost:9092") kafka_servers = getenv("KAFKA_SERVERS", "localhost:9092")
self.redis_host = getenv("REDIS_HOST", "localhost") self.redis_host = getenv("REDIS_HOST", "localhost")
self.redis_port = int(getenv("REDIS_PORT", 6379)) self.redis_port = int(getenv("REDIS_PORT", '6379'))
self.redis_username = getenv("REDIS_USERNAME", None) self.redis_username = getenv("REDIS_USERNAME", None)
self.redis_password = getenv("REDIS_PASSWORD", None) self.redis_password = getenv("REDIS_PASSWORD", None)
self.lease_ttl = int(getenv("LEASE_TTL", 10)) self.lease_ttl = int(getenv("LEASE_TTL", '10'))
self.heartbeat_ttl = int(getenv("HEARTBEAT_TTL", 20)) self.heartbeat_ttl = int(getenv("HEARTBEAT_TTL", '20'))
self.pod_id = getenv("HOSTNAME", "localhost") self.pod_id = getenv("HOSTNAME", "localhost")
self.poll_interval = int(getenv("POLL_INTERVAL", 5)) self.poll_interval = int(getenv("POLL_INTERVAL", '5'))
self.kafka_servers = kafka_servers.split(",") self.kafka_servers = kafka_servers.split(",")
self.logger = None self.logger = None
@@ -49,9 +53,15 @@ class Ingestor:
model_name="-", model_name="-",
model="-" model="-"
) )
# build args for build notificarions components
# call build notifications components self.ingestor_manager = None
def shutdown(self):
if self.ingestor_manager:
self.ingestor_manager.shutdown()
def __del__(self):
self.shutdown()
def init_logger(self): def init_logger(self):
""" """
@@ -128,7 +138,7 @@ class Ingestor:
# Get slot lease # Get slot lease
acquired = self.ingestor_manager.get_slot_leases() acquired = self.ingestor_manager.get_slot_leases()
self.logger.info(f"Acquired slots: {acquired}") self.logger.info("Acquired slots: %s", acquired)
self.handle_acquired_tags(acquired) self.handle_acquired_tags(acquired)
@@ -147,9 +157,7 @@ class Ingestor:
# This ingestor is active and has no slots, so we need to try to # This ingestor is active and has no slots, so we need to try to
# Get slot lease # Get slot lease
acquired = self.ingestor_manager.get_slot_leases(1) self.ingestor_manager.get_slot_leases(1)
self.handle_acquired_tags(acquired)
def manage_leases(self, available_slots: int, lacking_ingestors: int, slot_diff: int): def manage_leases(self, available_slots: int, lacking_ingestors: int, slot_diff: int):
""" """
@@ -171,16 +179,14 @@ class Ingestor:
if available_slots > 0 and lacking_ingestors > 0: if available_slots > 0 and lacking_ingestors > 0:
# Some ingestors are innactive, so theres "available_slots" slots available # Some ingestors are innactive, so theres "available_slots" slots available
self.logger.info(f"Slots available: {available_slots}") self.logger.info("Slots available: %s", available_slots)
# Get slot lease # Get slot lease
acquired = self.ingestor_manager.get_slot_leases(available_slots) self.ingestor_manager.get_slot_leases(available_slots)
self.handle_acquired_tags(acquired) elif lacking_ingestors <= 0 and slot_diff > 0:
elif lacking_ingestors == 0 and slot_diff > 0: self.logger.info("Extra slots available: %s", slot_diff)
self.logger.info(f"Extra slots available: {slot_diff}")
# There's enough slots for all ingestors, but this ingestor has more than one slot # There's enough slots for all ingestors, but this ingestor has more than one slot
# So we need to drop the extra leases # So we need to drop the extra leases
@@ -188,6 +194,40 @@ class Ingestor:
self.ingestor_manager.drop_slot_leases(overleases) self.ingestor_manager.drop_slot_leases(overleases)
for lease in overleases:
self.ingestor_manager.unsubscribe_slot(lease)
self.ingestor_manager.managed_tags.pop(lease)
def update_ingestor_manager(self, old_managed_tags: Dict[str, Any]):
"""
Updates the ingestor manager with the new managed tags.
Args:
old_managed_tags (Dict[str, Any]): The old managed tags.
"""
self.logger.debug("Current managed tags: %s",
self.ingestor_manager.managed_tags)
self.ingestor_manager.update_opc_servers()
new_managed_tags = deepcopy(self.ingestor_manager.managed_tags)
self.logger.debug("Comparing new managed tags %s"
"with old managed tags %s",
new_managed_tags, old_managed_tags)
for slot, config in new_managed_tags.items():
if slot not in old_managed_tags:
self.ingestor_manager.subscribe_to_tags({slot: config})
continue
if config != old_managed_tags[slot]:
self.ingestor_manager.unsubscribe_slot(slot)
self.ingestor_manager.subscribe_to_tags({slot: config})
for slot in old_managed_tags.keys():
if slot not in new_managed_tags:
self.ingestor_manager.unsubscribe_slot(slot)
def loop(self): def loop(self):
""" """
Executes the main loop for managing ingestors and slots. Executes the main loop for managing ingestors and slots.
@@ -211,33 +251,50 @@ class Ingestor:
self.logger.info("Polling for slot updates...") self.logger.info("Polling for slot updates...")
# Get active ingestors # Get active ingestors
current_managed_tags = deepcopy(self.ingestor_manager.managed_tags)
ingestors = self.ingestor_manager.get_active_ingestors() ingestors = self.ingestor_manager.get_active_ingestors()
number_of_ingestors = len(ingestors) number_of_ingestors = len(ingestors)
number_of_leases = self.ingestor_manager.get_number_of_leases() number_of_leases = self.ingestor_manager.get_number_of_leases()
number_of_slots = self.ingestor_manager.get_number_of_slots() number_of_slots = self.ingestor_manager.get_number_of_slots()
# Handle no slots # Handle no slots
self.logger.debug("Managing no slots...")
self.manage_no_slots(number_of_slots) self.manage_no_slots(number_of_slots)
available_slots = number_of_slots - number_of_leases available_slots = number_of_slots - number_of_leases
lacking_ingestors = number_of_slots - number_of_ingestors lacking_ingestors = number_of_slots - number_of_ingestors
slot_diff = len(self.ingestor_manager.managed_tags) - 1 slot_diff = len(self.ingestor_manager.managed_tags) - 1
self.logger.debug("Managing leases...")
self.manage_leases(available_slots, lacking_ingestors, slot_diff) self.manage_leases(available_slots, lacking_ingestors, slot_diff)
self.logger.debug( self.logger.debug(
f"Active ingestors: {ingestors}, " "Active ingestors: %s, "
f"Number of slots: {number_of_slots}, " "Number of slots: %s, "
f"Number of leases: {number_of_leases}, " "Number of leases: %s, "
f"Managed tags: {self.ingestor_manager.managed_tags}" "Managed tags: %s, "
f"Managed servers: {self.ingestor_manager.opc_managers}" "Managed servers: %s",
ingestors,
number_of_slots,
number_of_leases,
self.ingestor_manager.managed_tags,
self.ingestor_manager.opc_managers
) )
if not self.ingestor_manager.managed_tags: if not self.ingestor_manager.managed_tags:
# No slots acquired # No slots acquired
self.logger.info("No slots acquired in this loop") self.logger.info("No slots acquired in this loop")
# Update opc servers # Update opc servers
self.logger.debug("Updating slot config...")
self.ingestor_manager.update_slot_config() self.ingestor_manager.update_slot_config()
# Check OPC cycles # Check OPC cycles
self.logger.debug("Checking OPC servers integrity...")
self.ingestor_manager.check_opc_servers_integrity() self.ingestor_manager.check_opc_servers_integrity()
self.logger.debug("Updating managed tags...")
self.update_ingestor_manager(
current_managed_tags
)

View File

@@ -51,14 +51,21 @@ class DataManager():
self.logger = logger self.logger = logger
self.notification_handler = notification_handler self.notification_handler = notification_handler
def __del__(self): def shutdown(self):
"""Destructor to close the producer connection.""" """Closes the Kafka producer connection."""
print("Closing Kafka producer...")
if self.kafka_producer: if self.kafka_producer:
self.kafka_producer.flush(timeout=10) try:
self.kafka_producer.close() self.kafka_producer.flush(timeout=10)
self.kafka_producer.close()
except Exception as e:
self.logger.error(
f"Error closing Kafka producer: {e}")
else: else:
print("Kafka producer is already closed or not initialized.") self.logger.warning(
"Kafka producer is already closed or not initialized.")
def __del__(self):
self.shutdown()
def delivery_report(self, msg: str): def delivery_report(self, msg: str):
"""Callback for delivery reports from Kafka.""" """Callback for delivery reports from Kafka."""

View File

@@ -2,11 +2,11 @@ from logging import Logger
import traceback import traceback
from typing import Dict, List from typing import Dict, List
from copy import deepcopy from copy import deepcopy
from sientia_do.notifications.handlers import NotificationHandler
from sientia_do.notifications.models import NotificationLevel
from ingestor.managers.data_manager import DataManager from ingestor.managers.data_manager import DataManager
from ingestor.managers.opc_manager import OpcManager from ingestor.managers.opc_manager import OpcManager
from ingestor.managers.resource_manager import ResourceManager from ingestor.managers.resource_manager import ResourceManager
from sientia_do.notifications.models import Notification, NotificationLevel
from sientia_do.notifications.handlers import NotificationHandler
class IngestorManager(): class IngestorManager():
@@ -30,7 +30,8 @@ class IngestorManager():
self.notification_handler = notification_handler self.notification_handler = notification_handler
def initialize_opc_from_config(self, server_config: dict, data_manager: DataManager, logger: Logger) -> OpcManager | None: def initialize_opc_from_config(self, server_config: dict,
data_manager: DataManager, logger: Logger) -> OpcManager | None:
""" """
Initializes an OPC Manager instance using the provided server configuration. Initializes an OPC Manager instance using the provided server configuration.
Args: Args:
@@ -53,9 +54,11 @@ class IngestorManager():
self.logger.info( self.logger.info(
f"Initializing OpcManager at {server_config['url']}") f"Initializing OpcManager at {server_config['url']}")
manager = OpcManager( manager = OpcManager(
server_config['name'], server_config['url'], data_manager, logger, server_config['server_uri'], server_config['name'], server_config['url'],
self.notification_handler, server_config.get('cert_path'), server_config.get( data_manager, logger, server_config['server_uri'],
'private_key_path'), server_config.get('server_cert_path') self.notification_handler, server_config.get('cert_path'),
server_config.get('private_key_path'),
server_config.get('server_cert_path')
) )
manager.config = server_config manager.config = server_config
@@ -76,6 +79,14 @@ class IngestorManager():
return manager return manager
def shutdown(self):
for _server_name, server in self.opc_managers.items():
server.disconnect()
self.data_manager.shutdown()
def __del__(self):
self.shutdown()
def update_opc_servers(self): def update_opc_servers(self):
""" """
Updates the OPC (OLE for Process Control) server connections managed by the ingestor. Updates the OPC (OLE for Process Control) server connections managed by the ingestor.
@@ -91,7 +102,8 @@ class IngestorManager():
4. Disconnects and removes OPC servers that are no longer registered. 4. Disconnects and removes OPC servers that are no longer registered.
Attributes: Attributes:
self.managed_tags (dict): A nested dictionary containing slot and server configurations. self.managed_tags (dict): A nested dictionary containing slot and server configurations.
self.opc_managers (dict): A dictionary mapping server names to their OPC manager instances. self.opc_managers (dict): A dictionary mapping server names to their
OPC manager instances.
self.data_manager: An object responsible for managing data operations. self.data_manager: An object responsible for managing data operations.
self.logger: A logging object for recording warnings and other messages. self.logger: A logging object for recording warnings and other messages.
Raises: Raises:
@@ -101,27 +113,44 @@ class IngestorManager():
""" """
registered_servers = [] registered_servers = []
for slot, slot_config in self.managed_tags.items(): current_managed_tags = deepcopy(self.managed_tags)
for _slot, slot_config in current_managed_tags.items():
for server, server_config in slot_config.items(): for server, server_config in slot_config.items():
registered_servers.append(server) registered_servers.append(server)
server_config = server_config.copy() server_config = deepcopy(server_config)
server_config.pop('tags', None) server_config.pop('tags', None)
server_instance = None server_instance = self.opc_managers.get(server, None)
if server not in self.opc_managers: if server_instance is None:
self.logger.debug(
f"Initializing OPC manager for server {server}"
)
server_instance = self.initialize_opc_from_config( server_instance = self.initialize_opc_from_config(
server_config, self.data_manager, self.logger server_config, self.data_manager, self.logger
) )
elif self.opc_managers[server].config != server_config: elif server_instance.config != server_config:
self.opc_managers[server].disconnect() self.logger.warning(
f"Reinitializing OPC manager for server {server}"
)
server_instance.disconnect()
del self.opc_managers[server] del self.opc_managers[server]
server_instance = self.initialize_opc_from_config( server_instance = self.initialize_opc_from_config(
server_config, self.data_manager, self.logger server_config, self.data_manager, self.logger
) )
else:
self.logger.debug(
f"OPC manager for server {server} is already initialized and up to date"
)
if server_instance is not None: if server_instance is not None:
self.opc_managers[server] = server_instance self.opc_managers[server] = server_instance
else:
self.logger.warning(
f"Failed to initialize OPC manager for server {server}, "
f"removing server from managed tags."
)
_a = [self.managed_tags[slot].pop(server, None)
for slot, _value in current_managed_tags.items()]
for server in list(self.opc_managers.keys()): for server in list(self.opc_managers.keys()):
if server not in registered_servers: if server not in registered_servers:
@@ -143,18 +172,11 @@ class IngestorManager():
if is_lost: if is_lost:
self.logger.warning( self.logger.warning(
f"OPC server {server} is lost. " f"OPC server {server} is lost. "
f"Desconnecting from server." f"Server will be disconnected."
) )
opc_manager.disconnect()
self.opc_managers[server] = self.initialize_opc_from_config( for slot, _config in self.managed_tags.items():
opc_manager.config, self.data_manager, self.logger self.managed_tags[slot].pop(server, None)
)
self.update_opc_servers()
for slot, slot_config in self.managed_tags.items():
if server in slot_config:
self.manage_server(
slot, server, slot_config[server], slot_config[server]['tags']
)
def declare_active(self): def declare_active(self):
""" """
@@ -212,14 +234,16 @@ class IngestorManager():
Dict: A dictionary where the keys are the slot identifiers (as strings) Dict: A dictionary where the keys are the slot identifiers (as strings)
and the values are the leased slot details. and the values are the leased slot details.
Behavior: Behavior:
- Iterates through available slots and attempts to lease them using the resource manager. - Iterates through available slots and attempts to lease
them using the resource manager.
- Logs the leasing of each slot. - Logs the leasing of each slot.
- Updates the `managed_tags` attribute with the acquired slots. - Updates the `managed_tags` attribute with the acquired slots.
- Stops leasing once the specified `max_slots` are acquired. - Stops leasing once the specified `max_slots` are acquired.
- If unable to acquire the requested number of slots, logs a warning and returns the slots that were leased. - If unable to acquire the requested number of slots,
logs a warning and returns the slots that were leased.
Notes: Notes:
- If a slot is leased but its details cannot be retrieved (i.e., `get_tag_slot` returns None), - If a slot is leased but its details cannot be retrieved
that slot is skipped. (i.e., `get_tag_slot` returns None), that slot is skipped.
""" """
acquired = {} acquired = {}
@@ -280,33 +304,17 @@ class IngestorManager():
removed_slots = [] removed_slots = []
update = {} update = {}
for slot, slot_config in self.managed_tags.items(): for slot, _slot_config in self.managed_tags.items():
self.resource_manager.renew_tag_lease(slot) self.resource_manager.renew_tag_lease(slot)
update = self.resource_manager.get_tag_slot(slot) update = self.resource_manager.get_tag_slot(slot)
if update is None: if update is None:
self.logger.warning(
f"Slot {slot} configuration not found. "
f"Removing slot from managed tags."
)
self.unsubscribe_slot(slot)
removed_slots.append(slot) removed_slots.append(slot)
continue continue
if update != slot_config: self.managed_tags[slot] = update
self.logger.info(
f"Slot {slot} configuration updated. "
f"Old: {slot_config}, New: {update}"
)
self.managed_tags[slot] = update
self.unsubscribe_slot(slot)
self.update_opc_servers()
self.subscribe_to_tags({slot: update})
for slot in removed_slots: for slot in removed_slots:
del self.managed_tags[slot] self.managed_tags.pop(slot, None)
self.update_opc_servers()
def drop_slot_leases(self, ids: List[str]) -> None: def drop_slot_leases(self, ids: List[str]) -> None:
""" """
@@ -321,8 +329,8 @@ class IngestorManager():
None None
""" """
for id in ids: for lease_id in ids:
self.resource_manager.drop_tag_lease(id) self.resource_manager.drop_tag_lease(lease_id)
def manage_server(self, slot: str, server: str, server_config: dict, tags: dict) -> int: def manage_server(self, slot: str, server: str, server_config: dict, tags: dict) -> int:
""" """

View File

@@ -188,13 +188,21 @@ class OpcManager():
self.logger.warning("Client already disconnected.") self.logger.warning("Client already disconnected.")
return return
try: try:
[self.subscriptions[sub].delete() for sub in self.subscriptions] _a = [self.subscriptions[sub].delete()
for sub in self.subscriptions]
self.logger.warning("Deleted all subscriptions.") self.logger.warning("Deleted all subscriptions.")
except Exception as sub_error:
self.logger.error(f"Failed to clean up subscription: {sub_error}")
try:
self.client.disconnect()
except Exception as conn_error:
self.logger.error(
f"Failed to disconnect from OPC UA server: {conn_error}")
finally:
del self.client del self.client
self.client = None self.client = None
self.logger.warning("Disconnected from OPC UA server.") self.logger.warning("Disconnected from OPC UA server.")
except Exception as sub_error:
self.logger.error(f"Failed to clean up subscription: {sub_error}")
def datachange_notification(self, node, _val, data): def datachange_notification(self, node, _val, data):
""" """
@@ -223,10 +231,8 @@ class OpcManager():
tag = str(node) tag = str(node)
self.logger.debug( self.logger.debug(
f"Data change notification received for tag: {tag} after {self.nodes[tag]['cycle_rule']['cycle_count']} cycles") f"Data change notification received for tag:"
f"{tag} after {self.nodes[tag]['cycle_rule']['cycle_count']} cycles")
self.logger.debug(
f"Resetting cycle count for tag: {tag} after {self.non_receive_count} OPC cycles")
self.nodes[tag]['cycle_rule']['cycle_count'] = 0 self.nodes[tag]['cycle_rule']['cycle_count'] = 0
self.non_receive_count = 0 self.non_receive_count = 0
@@ -238,12 +244,13 @@ class OpcManager():
'value': value 'value': value
} }
[self.data_manager.publish(e, data) _a = [self.data_manager.publish(e, data)
for e in self.nodes[tag]['topics']] for e in self.nodes[tag]['topics']]
def check_cycles(self): def check_cycles(self):
""" """
Checks the cycle counts for all monitored nodes and sends notifications if thresholds are exceeded. Checks the cycle counts for all monitored nodes and sends
notifications if thresholds are exceeded.
This method iterates through all monitored nodes and updates their cycle counts based on This method iterates through all monitored nodes and updates their cycle counts based on
configured increments. If a node's cycle count exceeds a threshold (5 cycles), it triggers configured increments. If a node's cycle count exceeds a threshold (5 cycles), it triggers
@@ -251,7 +258,8 @@ class OpcManager():
""" """
for node, config in self.nodes.items(): for node, config in self.nodes.items():
self.nodes[node]['cycle_rule']['cycle_count'] += self.nodes[node]['cycle_rule']['cycle_increment'] self.nodes[node]['cycle_rule']['cycle_count'] += config[
'cycle_rule']['cycle_increment']
if self.nodes[node]['cycle_rule']['cycle_count'] >= 5: if self.nodes[node]['cycle_rule']['cycle_count'] >= 5:
name = config['tag_name'] name = config['tag_name']
cycles = self.nodes[node]['cycle_rule']['cycle_count'] cycles = self.nodes[node]['cycle_rule']['cycle_count']
@@ -273,7 +281,8 @@ class OpcManager():
if self.non_receive_count >= 5: if self.non_receive_count >= 5:
self.notification_handler.build_and_send_notification( self.notification_handler.build_and_send_notification(
notification_id=f'OPC_LISTENNING_STOPPED__{self.name}', notification_id=f'OPC_LISTENNING_STOPPED__{self.name}',
message=f'{self.non_receive_count} cycles without receive from OPC {self.name}. Tags: {json.dumps(self.nodes)}', message=f'{self.non_receive_count} cycles without '
f'receive from OPC {self.name}. Tags: {json.dumps(self.nodes)}',
block="opc_manager", block="opc_manager",
level=NotificationLevel.ERROR level=NotificationLevel.ERROR
) )

View File

@@ -1,14 +1,18 @@
import redis import redis
import json import json
import os
# Redis connection settings # Redis connection settings
redis_host = "localhost" REDIS_HOST = "localhost"
redis_port = 6379 REDIS_PORT = 6379
REDIS_USERNAME = None # "default"
REDIS_PASSWORD = None # "bdnZOpcyiL"
OPC_URL = "opc.tcp://sientia-opc-simulator-service.sientia-opc.svc.cluster.local:4840"
OPC_URL = "opc.tcp://localhost:4840"
# Connect to Redis # Connect to Redis
r = redis.Redis(host=redis_host, port=redis_port, r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT,
decode_responses=True, username='default', password='bdnZOpcyiL') decode_responses=True, username=REDIS_USERNAME, password=REDIS_PASSWORD)
# Define the key pattern to target # Define the key pattern to target
pattern = "slot:opc_tags:*" pattern = "slot:opc_tags:*"
@@ -25,7 +29,7 @@ new_data = {
"slot:opc_tags:1": { "slot:opc_tags:1": {
"server1": { "server1": {
"name": "server1", "name": "server1",
"url": "opc.tcp://sientia-opc-simulator-service.sientia-opc.svc.cluster.local:4840", "url": OPC_URL,
"server_uri": "http://opcua-server.simulator", "server_uri": "http://opcua-server.simulator",
"tags": { "tags": {
'ns=2;i=2': { 'ns=2;i=2': {

View File

@@ -108,31 +108,45 @@ def test___init___failure_max_attempts(kafka):
assert False, "Expected NoBrokersAvailable exception was not raised." assert False, "Expected NoBrokersAvailable exception was not raised."
def test___del___has_producer(data_manager): def test_shutdown_has_producer(data_manager):
flush_mock = MagicMock() flush_mock = MagicMock()
close_mock = MagicMock() close_mock = MagicMock()
data_manager.kafka_producer.flush = flush_mock data_manager.kafka_producer.flush = flush_mock
data_manager.kafka_producer.close = close_mock data_manager.kafka_producer.close = close_mock
data_manager.__del__() data_manager.shutdown()
flush_mock.assert_called_once() flush_mock.assert_called_once()
close_mock.assert_called_once() close_mock.assert_called_once()
@patch("ingestor.managers.data_manager.print") def test_shutdown_no_producer(data_manager):
def test___del___no_producer(print, data_manager):
data_manager.kafka_producer = None data_manager.kafka_producer = None
# Call the __del__ method data_manager.shutdown()
data_manager.__del__()
# Check if the print statement was called data_manager.logger.warning.assert_any_call(
print.assert_any_call(
"Kafka producer is already closed or not initialized." "Kafka producer is already closed or not initialized."
) )
def test_shutdown_exception(data_manager):
data_manager.kafka_producer.flush = MagicMock(
side_effect=Exception("Test error"))
data_manager.kafka_producer.close = MagicMock()
data_manager.shutdown()
data_manager.logger.error.assert_called_once_with(
"Error closing Kafka producer: Test error"
)
def test___del__(data_manager):
data_manager.shutdown = MagicMock()
data_manager.__del__()
data_manager.shutdown.assert_called_once()
def test_delivery_report(data_manager): def test_delivery_report(data_manager):
msg = MagicMock() msg = MagicMock()
msg.topic = "test_topic" msg.topic = "test_topic"

View File

@@ -331,14 +331,6 @@ def test_update_slot_config(ingestor_manager):
"config": "new_config"} "config": "new_config"}
assert "slot3" not in ingestor_manager.managed_tags assert "slot3" not in ingestor_manager.managed_tags
assert ingestor_manager.update_opc_servers.call_count == 2
ingestor_manager.subscribe_to_tags.assert_called_once_with(
{'slot1': {"config": "updated_config"}}
)
ingestor_manager.unsubscribe_slot.assert_any_call("slot3")
ingestor_manager.unsubscribe_slot.assert_any_call("slot1")
assert ingestor_manager.unsubscribe_slot.call_count == 2
ingestor_manager.resource_manager.renew_tag_lease.assert_any_call("slot1") ingestor_manager.resource_manager.renew_tag_lease.assert_any_call("slot1")
ingestor_manager.resource_manager.renew_tag_lease.assert_any_call("slot3") ingestor_manager.resource_manager.renew_tag_lease.assert_any_call("slot3")
ingestor_manager.resource_manager.renew_tag_lease.assert_any_call("slot2") ingestor_manager.resource_manager.renew_tag_lease.assert_any_call("slot2")
@@ -546,27 +538,9 @@ def test_check_opc_servers_integrity_server_lost(ingestor_manager):
ingestor_manager.initialize_opc_from_config = MagicMock( ingestor_manager.initialize_opc_from_config = MagicMock(
return_value=new_manager) return_value=new_manager)
# Mock update_opc_servers and manage_server
ingestor_manager.update_opc_servers = MagicMock()
ingestor_manager.manage_server = MagicMock()
# Call the method # Call the method
ingestor_manager.check_opc_servers_integrity() ingestor_manager.check_opc_servers_integrity()
# Verify that the lost server was disconnected
opc_manager.disconnect.assert_called_once()
# Verify that a new manager was initialized
ingestor_manager.initialize_opc_from_config.assert_called_once_with(
opc_manager.config, ingestor_manager.data_manager, ingestor_manager.logger
)
# Verify that the new manager was assigned
assert ingestor_manager.opc_managers["server1"] == new_manager
# Verify that update_opc_servers was called
ingestor_manager.update_opc_servers.assert_called_once()
def test_check_opc_servers_integrity_server_lost_with_tags(ingestor_manager): def test_check_opc_servers_integrity_server_lost_with_tags(ingestor_manager):
# Setup mock OPC manager that will be lost # Setup mock OPC manager that will be lost
@@ -594,30 +568,5 @@ def test_check_opc_servers_integrity_server_lost_with_tags(ingestor_manager):
ingestor_manager.initialize_opc_from_config = MagicMock( ingestor_manager.initialize_opc_from_config = MagicMock(
return_value=new_manager) return_value=new_manager)
# Mock update_opc_servers and manage_server
ingestor_manager.update_opc_servers = MagicMock()
ingestor_manager.manage_server = MagicMock()
# Call the method # Call the method
ingestor_manager.check_opc_servers_integrity() ingestor_manager.check_opc_servers_integrity()
# Verify that the lost server was disconnected
opc_manager.disconnect.assert_called_once()
# Verify that a new manager was initialized
ingestor_manager.initialize_opc_from_config.assert_called_once_with(
opc_manager.config, ingestor_manager.data_manager, ingestor_manager.logger
)
# Verify that the new manager was assigned
assert ingestor_manager.opc_managers["server1"] == new_manager
# Verify that update_opc_servers was called
ingestor_manager.update_opc_servers.assert_called_once()
# Verify that manage_server was called with the correct tags
ingestor_manager.manage_server.assert_called_once_with(
"slot1", "server1",
{"config": "config1", "tags": {"tag1": "value1"}},
{"tag1": "value1"}
)

View File

@@ -188,7 +188,7 @@ def test_disconnect_success(opc_manager_subscribed):
assert opc_manager_subscribed.client is None assert opc_manager_subscribed.client is None
def test_disconnect_error(opc_manager_subscribed): def test_disconnect_error_unsubscribe(opc_manager_subscribed):
opc_manager_subscribed.client = MagicMock() opc_manager_subscribed.client = MagicMock()
opc_manager_subscribed.subscriptions['sub1'] = MagicMock( opc_manager_subscribed.subscriptions['sub1'] = MagicMock(
delete=MagicMock(side_effect=Exception("Test error")) delete=MagicMock(side_effect=Exception("Test error"))
@@ -197,11 +197,24 @@ def test_disconnect_error(opc_manager_subscribed):
opc_manager_subscribed.disconnect() opc_manager_subscribed.disconnect()
opc_manager_subscribed.subscriptions['sub1'].delete.assert_called_once() opc_manager_subscribed.subscriptions['sub1'].delete.assert_called_once()
opc_manager_subscribed.client.disconnect.assert_not_called() opc_manager_subscribed.client = None
opc_manager_subscribed.logger.error.assert_called_once_with( opc_manager_subscribed.logger.error.assert_called_once_with(
"Failed to clean up subscription: Test error") "Failed to clean up subscription: Test error")
def test_disconnect_error(opc_manager_subscribed):
opc_manager_subscribed.client = MagicMock()
opc_manager_subscribed.client.disconnect = MagicMock(
side_effect=Exception("Test error"))
opc_manager_subscribed.disconnect()
opc_manager_subscribed.subscriptions['sub1'].delete.assert_called_once()
opc_manager_subscribed.client = None
opc_manager_subscribed.logger.error.assert_called_once_with(
"Failed to disconnect from OPC UA server: Test error")
def test_datachange_notification(opc_manager_subscribed): def test_datachange_notification(opc_manager_subscribed):
data = MagicMock( data = MagicMock(
monitored_item=MagicMock( monitored_item=MagicMock(

View File

@@ -1,4 +1,5 @@
from unittest.mock import ANY, MagicMock, patch from unittest.mock import ANY, MagicMock, patch, call
from os import getenv
from pytest import fixture from pytest import fixture
from ingestor.ingestor import Ingestor from ingestor.ingestor import Ingestor
@@ -24,13 +25,13 @@ def test___init__(notification_handler, init_logger, getenv):
getenv.assert_any_call("KAFKA_SERVERS", "localhost:9092") getenv.assert_any_call("KAFKA_SERVERS", "localhost:9092")
getenv.assert_any_call("REDIS_HOST", "localhost") getenv.assert_any_call("REDIS_HOST", "localhost")
getenv.assert_any_call("REDIS_PORT", 6379) getenv.assert_any_call("REDIS_PORT", '6379')
getenv.assert_any_call("REDIS_USERNAME", None) getenv.assert_any_call("REDIS_USERNAME", None)
getenv.assert_any_call("REDIS_PASSWORD", None) getenv.assert_any_call("REDIS_PASSWORD", None)
getenv.assert_any_call("LEASE_TTL", 10) getenv.assert_any_call("LEASE_TTL", '10')
getenv.assert_any_call("HEARTBEAT_TTL", 20) getenv.assert_any_call("HEARTBEAT_TTL", '20')
getenv.assert_any_call("HOSTNAME", "localhost") getenv.assert_any_call("HOSTNAME", "localhost")
getenv.assert_any_call("POLL_INTERVAL", 5) getenv.assert_any_call("POLL_INTERVAL", '5')
assert ingestor.kafka_servers == ["localhost:9092", "localhost:35"] assert ingestor.kafka_servers == ["localhost:9092", "localhost:35"]
assert ingestor.redis_host == "localhost1" assert ingestor.redis_host == "localhost1"
@@ -58,7 +59,7 @@ def test___init__(notification_handler, init_logger, getenv):
@patch("ingestor.ingestor.getenv") @patch("ingestor.ingestor.getenv")
@patch("ingestor.ingestor.Ingestor.init_logger") @patch("ingestor.ingestor.Ingestor.init_logger")
@patch("ingestor.ingestor.NotificationHandler") @patch("ingestor.ingestor.NotificationHandler")
def ingestor(notification_handler, init_logger, getenv): def ingestor(_notification_handler, _init_logger, _getenv):
ing = Ingestor() ing = Ingestor()
ing.logger = MagicMock() ing.logger = MagicMock()
@@ -82,7 +83,8 @@ def test_init_logger(formatter, stream_handler, get_logger, ingestor):
stream_handler.assert_called_once() stream_handler.assert_called_once()
formatter.assert_called_once_with( formatter.assert_called_once_with(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s') '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ingestor.logger.setLevel.assert_called_once_with("INFO") ingestor.logger.setLevel.assert_called_once_with(
getenv("LOG_LEVEL", "INFO"))
ingestor.logger.addHandler.assert_called_once_with( ingestor.logger.addHandler.assert_called_once_with(
stream_handler.return_value) stream_handler.return_value)
stream_handler.return_value.setFormatter.assert_called_once_with( stream_handler.return_value.setFormatter.assert_called_once_with(
@@ -172,8 +174,6 @@ def test_manage_slots_none_available_none_available(ingestor_manager_started):
ingestor_manager_started.ingestor_manager.get_slot_leases.assert_called_once_with( ingestor_manager_started.ingestor_manager.get_slot_leases.assert_called_once_with(
1) 1)
ingestor_manager_started.handle_acquired_tags.assert_called_once_with(
ingestor_manager_started.ingestor_manager.get_slot_leases.return_value)
def test_manage_leases_no_available_slots_no_extra_slots(ingestor_manager_started): def test_manage_leases_no_available_slots_no_extra_slots(ingestor_manager_started):
@@ -193,8 +193,6 @@ def test_manage_leases_available_slots_innactive_ingestors(ingestor_manager_star
ingestor_manager_started.ingestor_manager.get_slot_leases.assert_called_once_with( ingestor_manager_started.ingestor_manager.get_slot_leases.assert_called_once_with(
2) 2)
ingestor_manager_started.handle_acquired_tags.assert_called_once_with(
ingestor_manager_started.ingestor_manager.get_slot_leases.return_value)
ingestor_manager_started.ingestor_manager.drop_slot_leases.assert_not_called() ingestor_manager_started.ingestor_manager.drop_slot_leases.assert_not_called()
@@ -267,3 +265,35 @@ def test_loop_no_managed(ingestor_manager_started):
ingestor_manager_started.ingestor_manager.update_slot_config.assert_called_once() ingestor_manager_started.ingestor_manager.update_slot_config.assert_called_once()
ingestor_manager_started.logger.info.assert_any_call( ingestor_manager_started.logger.info.assert_any_call(
"No slots acquired in this loop") "No slots acquired in this loop")
def test_update_ingestor_manager(ingestor_manager_started):
ingestor_manager_started.ingestor_manager.managed_tags = {
"slot_to_create": "new_config",
"slot_to_update": "new_config",
"slot_to_do_nothing": "old_config"
}
old_managed_tags = {
"slot_to_update": "old_config",
"slot_to_delete": "old_config",
"slot_to_do_nothing": "old_config"
}
ingestor_manager_started.update_ingestor_manager(old_managed_tags)
ingestor_manager_started.ingestor_manager.update_opc_servers.assert_called_once()
ingestor_manager_started.ingestor_manager.subscribe_to_tags.assert_has_calls(
[
call(
{"slot_to_create": "new_config"}),
call(
{"slot_to_update": "new_config"})
]
)
ingestor_manager_started.ingestor_manager.unsubscribe_slot.assert_has_calls(
[
call("slot_to_update"),
call("slot_to_delete")
]
)

View File

@@ -11,7 +11,7 @@ image:
# This sets the pull policy for images. # This sets the pull policy for images.
pullPolicy: Always pullPolicy: Always
# Overrides the image tag whose default is the chart appVersion. # Overrides the image tag whose default is the chart appVersion.
tag: "0.0.1" tag: "0.0.2"
# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ # This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
imagePullSecrets: imagePullSecrets:
@@ -19,7 +19,7 @@ imagePullSecrets:
# This is to override the chart name. # This is to override the chart name.
nameOverride: "sientia-opc-ingestor" nameOverride: "sientia-opc-ingestor"
fullnameOverride: "sientia-opc-ingestor" fullnameOverride: "sientia-opc-ingestor"
namespace: sientia-opc namespace: sientia
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ # This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
serviceAccount: serviceAccount:
@@ -123,7 +123,7 @@ env:
- name: GITHUB_REPO_URL - name: GITHUB_REPO_URL
value: "git@github.com:Aignosi/sientia-dataops-opc-ingestor.git" value: "git@github.com:Aignosi/sientia-dataops-opc-ingestor.git"
- name: GITHUB_BRANCH - name: GITHUB_BRANCH
value: "SIENTIAPDE-988-criar-ingestor-opc" value: "SIENTIAPDE-1049-lidar-com-erros-de-reconexao-no-ingestor-opc"
- name: PYTHON_APP - name: PYTHON_APP
value: "ingestor.app" value: "ingestor.app"
@@ -150,13 +150,20 @@ env:
value: "30" value: "30"
- name: POLL_INTERVAL - name: POLL_INTERVAL
value: "10" value: "10"
- name: LOG_LEVEL
value: "DEBUG"
ssh: ssh:
enabled: true enabled: true
secretName: git-ssh-key-temp secretName: git-ssh-key-sientia-opc-ingestor
sshPath: /mnt/.ssh sshPath: /mnt/.ssh
knownHostsPath: /mnt/known_hosts knownHostsPath: /mnt/known_hosts
# kubectl create secret docker-registry docker-hub-secret --namespace sientia-opc --docker-server=http://aignosi.azurecr.io --docker-username=aignosi --docker-password=5I5zpQ6sRaHqX1hD3dr+2mo647yO3FRc359/wu6gsP+ACRDRz5mp # kubectl create secret docker-registry docker-hub-secret --namespace sientia --docker-server=http://aignosi.azurecr.io --docker-username=aignosi --docker-password=5I5zpQ6sRaHqX1hD3dr+2mo647yO3FRc359/wu6gsP+ACRDRz5mp
# helm upgrade --install sientia-dataops-opc-ingestor sientia/sientia-module -n sientia-opc --create-namespace -f ./values.yaml # helm upgrade --install sientia-opc-ingestor sientia/sientia-module -n sientia --create-namespace -f ./values.yaml --version 0.1.0-uat
# kubectl create secret generic git-ssh-key-sientia-opc-ingestor \
# --namespace sientia \
# --from-file=ssh-privatekey=git_key \
# --type=kubernetes.io/ssh-auth