From cacdd574852d4cc84b1e27f2b5a12193357e72f5 Mon Sep 17 00:00:00 2001 From: Mark Coleman Date: Fri, 23 Feb 2024 15:22:04 +0000 Subject: [PATCH] Added faux monitoring service which pulls devices from NetBox and pings --- .../agents/monitor_network/monitor_network.py | 104 ++++++++++++++++++ .../agents/monitor_network/netbox.py | 60 ++++++++++ .../requirements.txt | 2 + 3 files changed, 166 insertions(+) create mode 100644 netbox-event-driven-architectures/agents/monitor_network/monitor_network.py create mode 100644 netbox-event-driven-architectures/agents/monitor_network/netbox.py diff --git a/netbox-event-driven-architectures/agents/monitor_network/monitor_network.py b/netbox-event-driven-architectures/agents/monitor_network/monitor_network.py new file mode 100644 index 0000000..8065829 --- /dev/null +++ b/netbox-event-driven-architectures/agents/monitor_network/monitor_network.py @@ -0,0 +1,104 @@ +import os, asyncio, nmap, time, sys, ipaddress, json +from nats.aio.client import Client as NATS +from dotenv import load_dotenv +from netbox import NetBoxHelper +from pythonping import ping +from pythonping.executor import SuccessOn +from prettytable import PrettyTable + +class MonitorNetwork(): + + # Class Variables + nats_server = "" + subscribe_subject = "" + publish_subject = "" + network_devices = {} + netbox_url = "" + netbox_token = "" + nc = None + + # Class Functions + def __init__(self): + # Load Environment Variables + load_dotenv() + self.nats_server = os.getenv("NATS_SERVER") + self.publish_subject = os.getenv("PUBLISH_SUBJECT") + self.subscribe_subject = os.getenv("SUBSCRIBE_SUBJECT") + self.netbox_url = os.getenv("NETBOX_URL") + self.netbox_token = os.getenv("NETBOX_TOKEN") + + # Load devices from netbox + self.network_devices = self.load_devices_from_netbox() + + # Report environment + print(f"""Loaded environment for {os.path.basename(__file__)} +NATs Server: {self.nats_server} +Publishing to subject: {self.publish_subject} +Monitoring Devices: {json.dumps(self.network_devices, indent=4)}""") + + def load_devices_from_netbox(self) -> {str, str}: + # Create NetBox Helper object + nb = NetBoxHelper(self.netbox_url, self.netbox_token) + + # Get all active devices with an IPv4 management IP + print(f"Loading devices from NetBox instance at {self.netbox_url}") + total_devices_count, elligible_devices_count, devices = nb.get_active_devices_with_a_mgmt_ipv4() + print(f"Found {total_devices_count} devices. {elligible_devices_count} of which are elligible for monitoring.") + + network_devices = {} + + # Write each of the IPs into network_devices[] + for device in devices: + mgmt_ip = ipaddress.ip_interface(str(device.primary_ip4)).ip + network_devices[f"{device.name}"] = str(mgmt_ip) + + return network_devices + + async def message_handler(self, msg) -> None: + subject = msg.subject + data = msg.data.decode() + print(f"Received a message on '{subject}': {data}") + + table = PrettyTable(["Device Name", "IP", "Pingable?"]) + + + # Ping all devices and output the results + for device, ip in self.network_devices.items(): + device_failed = False + ping_status = ping(ip, + verbose=False, + timeout=1, + count=1).success(option=SuccessOn.Most) + table.add_row([device, ip, ping_status]) + if ping_status == False: + device_failed = True + + + await self.nc.publish(self.publish_subject, f"Device monitoring issues ⚠️ \n {table}".encode()) + + print(table) + + async def main_loop(self) -> None: + # Create a NATS client + self.nc = NATS() + + # Connect to the NATS server + await self.nc.connect(self.nats_server) + + # Subscribe to subject + await self.nc.subscribe(self.subscribe_subject, cb=self.message_handler) + print(f"Subscribed to {self.subscribe_subject}") + + # Keep the script running to receive messages + try: + await asyncio.Future() + except KeyboardInterrupt: + print("Disconnecting...") + await self.nc.close() + except Exception as e: + print(e) + +# Run the subscriber +if __name__ == "__main__": + network_monitor = MonitorNetwork() + asyncio.run(network_monitor.main_loop()) \ No newline at end of file diff --git a/netbox-event-driven-architectures/agents/monitor_network/netbox.py b/netbox-event-driven-architectures/agents/monitor_network/netbox.py new file mode 100644 index 0000000..2caba81 --- /dev/null +++ b/netbox-event-driven-architectures/agents/monitor_network/netbox.py @@ -0,0 +1,60 @@ +import pynetbox +import logging +from requests.exceptions import MissingSchema + +class NetBoxHelper: + + netbox = None + logger = None + netbox_url = "" + netbox_token = "" + __instance = None + + # Singleton --- + @staticmethod + def getInstance(netbox_url: str, netbox_token: str): + if NetBoxHelper.__instance == None: + NetBoxHelper(netbox_url, netbox_token) + return NetBoxHelper.__instance + + def __init__(self, netbox_url: str, netbox_token: str): + if NetBoxHelper.__instance != None: + raise Exception(f"{NetBoxHelper.__name__} singleton already created. Please use {NetBoxHelper.__name__}.getInstance() instead") + else: + NetBoxHelper.__instance = self + + logging.basicConfig(format='%(asctime)s,%(msecs)03d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s', + datefmt='%Y-%m-%d:%H:%M:%S', + level=logging.INFO) + + self.logger = logging.getLogger(__name__) + + self.netbox_url = netbox_url + self.netbox_token = netbox_token + + try: + self.netbox = pynetbox.api(self.netbox_url, self.netbox_token) + except Exception as e: + self.logger.debug(f"Unable to connect to NetBox instance: {e}") + return None + # --- Singleton + + # Functions + def get_active_devices_with_a_mgmt_ipv4(self) -> (int, int, pynetbox.core.response.RecordSet): + total_devices_count = 0 + elligible_devices_count = 0 + elligible_devices = None + + self.logger.info("Retrieving devices from NetBox...") + + try: + total_devices_count = len(self.netbox.dcim.devices.all()) + elligible_devices = self.netbox.dcim.devices.filter(status='active', has_primary_ip='True') + elligible_devices_count = len(elligible_devices) + + except (pynetbox.core.query.RequestError, MissingSchema) as e: + self.logger.info(f"Could not establish connection with NetBox API. Please verify details. Exception details: {e}") + except AttributeError as e: + self.logger.critical(f"Attribute error: {e}. Did you specify an invalid API endpoint?") + + return total_devices_count, elligible_devices_count, elligible_devices \ No newline at end of file diff --git a/netbox-event-driven-architectures/requirements.txt b/netbox-event-driven-architectures/requirements.txt index 8cddb6b..c773ba4 100644 --- a/netbox-event-driven-architectures/requirements.txt +++ b/netbox-event-driven-architectures/requirements.txt @@ -5,3 +5,5 @@ napalm pythonping ipaddress python-nmap +pynetbox +prettytable