diff --git a/core/services/cable_guy/api/manager.py b/core/services/cable_guy/api/manager.py index 757fbe58a3..7d5b66b633 100644 --- a/core/services/cable_guy/api/manager.py +++ b/core/services/cable_guy/api/manager.py @@ -1,8 +1,9 @@ +import asyncio import re import subprocess import time from socket import AddressFamily -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Set, Tuple import psutil from commonwealth.utils.decorators import temporary_cache @@ -84,13 +85,15 @@ def save(self) -> None: result = [interface.dict(exclude={"info"}) for interface in self.result] self.settings.save(result) - def set_configuration(self, interface: NetworkInterface) -> None: + def set_configuration(self, interface: NetworkInterface, watchdog_call: bool = False) -> None: """Modify hardware based in the configuration Args: interface: NetworkInterface + watchdog_call: Whether this is a watchdog call """ - self.network_handler.cleanup_interface_connections(interface.name) + if not watchdog_call: + self.network_handler.cleanup_interface_connections(interface.name) interfaces = self.get_ethernet_interfaces() valid_names = [interface.name for interface in interfaces] if interface.name not in valid_names: @@ -533,3 +536,72 @@ def stop(self) -> None: def __del__(self) -> None: self.stop() + + def priorities_mismatch(self) -> bool: + """Check if the current interface priorities differ from the saved ones. + Uses sets for order-independent comparison of NetworkInterfaceMetric objects, + which compare only name and priority fields. + + Returns: + bool: True if priorities don't match, False if they do + """ + if "priorities" not in self.settings.root: + return False + + current = set(self.get_interfaces_priority()) + # Convert saved priorities to NetworkInterfaceMetric, index value doesn't matter for comparison + saved = {NetworkInterfaceMetric(index=0, **iface) for iface in self.settings.root["priorities"]} + + return current != saved + + def config_mismatch(self) -> Set[NetworkInterface]: + """Check if the current interface config differs from the saved ones. + + Returns: + bool: True if config doesn't match, False if it does + """ + + mismatches: Set[NetworkInterface] = set() + current = self.get_ethernet_interfaces() + if "content" not in self.settings.root: + logger.debug("No saved configuration found") + logger.debug(f"Current configuration: {self.settings.root}") + return mismatches + + saved = self.settings.root["content"] + saved_interfaces = {interface["name"]: NetworkInterface(**interface) for interface in saved} + + for interface in current: + if interface.name not in saved_interfaces: + logger.debug(f"Interface {interface.name} not in saved configuration, skipping") + continue + + for address in saved_interfaces[interface.name].addresses: + if address not in interface.addresses: + logger.info( + f"Mismatch detected for {interface.name}: " + f"saved address {address.ip} ({address.mode}) not found in current addresses " + f"[{', '.join(f'{addr.ip} ({addr.mode})' for addr in interface.addresses)}]" + ) + mismatches.add(saved_interfaces[interface.name]) + return mismatches + + async def watchdog(self) -> None: + """ + periodically checks the interfaces states against the saved settings, + if there is a mismatch, it will apply the saved settings + """ + while True: + if self.priorities_mismatch(): + logger.warning("Interface priorities mismatch, applying saved settings.") + try: + self.set_interfaces_priority(self.settings.root["priorities"]) + except Exception as error: + logger.error(f"Failed to set interface priorities: {error}") + mismatches = self.config_mismatch() + if mismatches: + logger.warning("Interface config mismatch, applying saved settings.") + logger.debug(f"Mismatches: {mismatches}") + for interface in mismatches: + self.set_configuration(interface, watchdog_call=True) + await asyncio.sleep(5) diff --git a/core/services/cable_guy/main.py b/core/services/cable_guy/main.py index ca9a847bf2..11f59e5936 100755 --- a/core/services/cable_guy/main.py +++ b/core/services/cable_guy/main.py @@ -179,8 +179,8 @@ async def root() -> HTMLResponse: loop = asyncio.new_event_loop() - # # Running uvicorn with log disabled so loguru can handle it + # Running uvicorn with log disabled so loguru can handle it config = Config(app=app, loop=loop, host="0.0.0.0", port=9090, log_config=None) server = Server(config) - + loop.create_task(manager.watchdog()) loop.run_until_complete(server.serve()) diff --git a/core/services/cable_guy/typedefs.py b/core/services/cable_guy/typedefs.py index 2188c05a84..7caa0205ee 100755 --- a/core/services/cable_guy/typedefs.py +++ b/core/services/cable_guy/typedefs.py @@ -9,11 +9,17 @@ class AddressMode(str, Enum): Server = "server" Unmanaged = "unmanaged" + def __hash__(self) -> int: + return hash(self.value) + class InterfaceAddress(BaseModel): ip: str mode: AddressMode + def __hash__(self) -> int: + return hash(self.ip) + hash(self.mode) + class InterfaceInfo(BaseModel): connected: bool @@ -26,6 +32,9 @@ class NetworkInterface(BaseModel): addresses: List[InterfaceAddress] info: Optional[InterfaceInfo] + def __hash__(self) -> int: + return hash(self.name) + sum(hash(address) for address in self.addresses) + class NetworkInterfaceMetric(BaseModel): name: str