From eaff8b05c20b19c59e32e2646ca17d7bc75250aa Mon Sep 17 00:00:00 2001 From: jwansek Date: Sun, 18 May 2025 22:41:07 +0100 Subject: Added fetching mikrotik POE usage to InfluxDB too --- switch-snmp/Dockerfile | 4 +- switch-snmp/mikrotik-switches.conf | 9 +++ switch-snmp/mikrotik.py | 131 +++++++++++++++++++++++++++++++++++++ switch-snmp/omada-switches.conf | 18 +++++ switch-snmp/port-names.conf | 10 --- switch-snmp/requirements.txt | 3 +- switch-snmp/snmp-omada.py | 100 ---------------------------- switch-snmp/snmpOmada.py | 96 +++++++++++++++++++++++++++ switch-snmp/switches.py | 7 ++ 9 files changed, 265 insertions(+), 113 deletions(-) create mode 100644 switch-snmp/mikrotik-switches.conf create mode 100644 switch-snmp/mikrotik.py create mode 100644 switch-snmp/omada-switches.conf delete mode 100644 switch-snmp/port-names.conf delete mode 100644 switch-snmp/snmp-omada.py create mode 100644 switch-snmp/snmpOmada.py create mode 100644 switch-snmp/switches.py (limited to 'switch-snmp') diff --git a/switch-snmp/Dockerfile b/switch-snmp/Dockerfile index a056214..d09e341 100644 --- a/switch-snmp/Dockerfile +++ b/switch-snmp/Dockerfile @@ -11,7 +11,7 @@ RUN unzip -j "privateMibs(20220831).zip" -d /usr/share/snmp/mibs RUN pip3 install -r requirements.txt RUN rm "privateMibs(20220831).zip" -RUN echo "*/1 * * * * root python3 /app/snmp-omada.py > /proc/1/fd/1 2>/proc/1/fd/2" > /etc/crontab -RUN echo "*/1 * * * * root sh -c 'sleep 30 && python3 /app/snmp-omada.py' > /proc/1/fd/1 2>/proc/1/fd/2" >> /etc/crontab +RUN echo "*/1 * * * * root python3 /app/switches.py > /proc/1/fd/1 2>/proc/1/fd/2" > /etc/crontab +RUN echo "*/1 * * * * root sh -c 'sleep 30 && python3 /app/switches.py' > /proc/1/fd/1 2>/proc/1/fd/2" >> /etc/crontab ENTRYPOINT ["bash"] CMD ["entrypoint.sh"] diff --git a/switch-snmp/mikrotik-switches.conf b/switch-snmp/mikrotik-switches.conf new file mode 100644 index 0000000..b777b53 --- /dev/null +++ b/switch-snmp/mikrotik-switches.conf @@ -0,0 +1,9 @@ +[192.168.69.22] +ether1 = Modem +ether2 = 2 +ether3 = EAP110 Wifi +ether4 = Amazon Firestick +ether5 = 5 +ether6 = 6 +ether7 = 7 +ether8 = 8 \ No newline at end of file diff --git a/switch-snmp/mikrotik.py b/switch-snmp/mikrotik.py new file mode 100644 index 0000000..afd3cfa --- /dev/null +++ b/switch-snmp/mikrotik.py @@ -0,0 +1,131 @@ +from dataclasses import dataclass +import configparser +import threading +import fabric +import time +import os +import re + +from influxdb_client import InfluxDBClient, Point, WritePrecision +from influxdb_client.client.write_api import SYNCHRONOUS + +INFLUXDB_MAPPINGS = { + "poe-out-voltage": "tpPoeVoltage", + "poe-out-current": "tpPoeCurrent", + "poe-out-power": "tpPoePower", +} + +@dataclass +class MikroTikSSHDevice: + + host: str + ssh_key_path: str + user: str = "admin" + + def __post_init__(self): + self.is_being_polled = threading.Event() + + def _get_conn(self): + return fabric.Connection( + user = self.user, + host = self.host, + connect_kwargs = {"key_filename": self.ssh_key_path} + ) + + def _poll_four_interfaces(self, four_interfaces): + # only poll four interfaces at the same time since we can only get a certain amount of information through SSH at the same time + self.is_being_polled.set() + result = self._get_conn().run("/interface/ethernet/poe/monitor %s once" % ",".join(four_interfaces), hide = True) + self.is_being_polled.clear() + return self._parse_result(result) + + def _parse_result(self, result): + r = result.stdout + # print(r) + s = [re.split(r" +", row.rstrip())[1:] for row in r.split("\r\n")][:-2] + out = {i: {} for i in s[0][1:]} + off_interfaces = set() + for row in s[1:]: + column_decrimator = 0 + output_name = row[0][:-1] + # print(output_name) + + for i, interface_name in enumerate(out.keys(), 0): + # print("off_interfaces:", off_interfaces) + # print(i, interface_name, row[1:][i]) + if interface_name in off_interfaces: + # print("Skipping '%s' for %s..." % (output_name, interface_name)) + column_decrimator += 1 + else: + out[interface_name][output_name] = row[1:][i - column_decrimator] + + if output_name == "poe-out-status": + if row[1:][i] != "powered-on": + # print("Adding %s to off interfaces" % interface_name) + off_interfaces.add(interface_name) + return out + + def get_poe_interfaces(self, interface_names): + out = {} + for four_interfaces in [interface_names[i:i + 4] for i in range(0, len(interface_names), 4)]: + out |= self._poll_four_interfaces(four_interfaces) + return out + +def remove_measurement_type(type_str): + type_str = "".join([s for s in type_str if s.isdigit() or s == "."]) + if "." in type_str: + return float(type_str) + else: + return int(type_str) + +def fields_to_points(fields, switch_host, config): + return [{ + "measurement": "switch_status", + "tags": {"port": port, "port_name": config.get(switch_host, port), "switch_host": switch_host, "type": "MikroTik"}, + "fields": {INFLUXDB_MAPPINGS[k]: remove_measurement_type(v) for k, v in values.items() if k in INFLUXDB_MAPPINGS} + } for port, values in fields.items()] + +def get_points(): + mikrotik_switches = configparser.ConfigParser() + mikrotik_switches.read(os.path.join(os.path.dirname(__file__), "mikrotik-switches.conf")) + points = [] + for mikrotik_switch in mikrotik_switches.sections(): + mikrotik_device = MikroTikSSHDevice(mikrotik_switch, os.path.join(os.path.dirname(__file__), "mikrotik.pem")) + points += fields_to_points(mikrotik_device.get_poe_interfaces(list(mikrotik_switches[mikrotik_switch].keys())), mikrotik_switch, mikrotik_switches) + return points + +def append(points): + env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "config.env") + if os.path.exists(env_path): + import dotenv + dotenv.load_dotenv(dotenv_path = env_path) + INFLUXDB_HOST = "dns.athome" + else: + INFLUXDB_HOST = "influxdb" + + influxc = InfluxDBClient( + url = "http://%s:8086" % INFLUXDB_HOST, + token = os.environ["DOCKER_INFLUXDB_INIT_ADMIN_TOKEN"], + org = os.environ["DOCKER_INFLUXDB_INIT_ORG"] + ) + influxc.ping() + + write_api = influxc.write_api(write_options = SYNCHRONOUS) + write_api.write( + os.environ["DOCKER_INFLUXDB_INIT_BUCKET"], + os.environ["DOCKER_INFLUXDB_INIT_ORG"], + points, + write_precision = WritePrecision.S + ) + +if __name__ == "__main__": + if not os.path.exists(os.path.join(os.path.dirname(__file__), "mikrotik-switches.conf")): + raise FileNotFoundError("Couldn't find mikrotik config file") + if not os.path.exists(os.path.join(os.path.dirname(__file__), "mikrotik.pem")): + raise FileNotFoundError("Couldn't find mikrotik public key file") + + import json + points = get_points() + print(json.dumps(points, indent = 4)) + append(points) + diff --git a/switch-snmp/omada-switches.conf b/switch-snmp/omada-switches.conf new file mode 100644 index 0000000..3f3f3f0 --- /dev/null +++ b/switch-snmp/omada-switches.conf @@ -0,0 +1,18 @@ +[192.168.69.26] +1 = EAP225 Wifi +2 = Tasmota Zigbee +4 = Mikrotik CRS310-8G+2S+ +6 = Routerbox +16 = Intel Compute Stick +24 = Frigate Pi +23 = Modem & ES205G +8 = PiKVM +10 = TL-RP108GE & EAP110 +22 = Cluster Pi 9 +19 = Cluster Pi 5 +20 = Cluster Pi 7 +21 = Cluster Pi 8 +18 = Cluster Pi 6 +17 = Cluster Pi 4 +9 = Jetson Orin Nano + diff --git a/switch-snmp/port-names.conf b/switch-snmp/port-names.conf deleted file mode 100644 index 8f8d089..0000000 --- a/switch-snmp/port-names.conf +++ /dev/null @@ -1,10 +0,0 @@ -1 = EAP225 Wifi -2 = Tasmota Zigbee -4 = 2.5Gb Switch -6 = Routerbox -13 = Intel Compute Stick -24 = Frigate Pi -23 = Modem & ES205G -8 = PiKVM -10 = TL-RP108GE & EAP110 - diff --git a/switch-snmp/requirements.txt b/switch-snmp/requirements.txt index e7e63fd..aaf744d 100644 --- a/switch-snmp/requirements.txt +++ b/switch-snmp/requirements.txt @@ -1,3 +1,4 @@ python-dotenv influxdb-client -pandas \ No newline at end of file +pandas +fabric \ No newline at end of file diff --git a/switch-snmp/snmp-omada.py b/switch-snmp/snmp-omada.py deleted file mode 100644 index aaa340e..0000000 --- a/switch-snmp/snmp-omada.py +++ /dev/null @@ -1,100 +0,0 @@ -# wget https://static.tp-link.com/upload/software/2022/202209/20220915/privateMibs(20220831).zip -# cp -v *.mib /home/eden/.snmp/mibs -# sudo apt install snmp -# sudo apt-get install snmp-mibs-downloader - -import subprocess -from dataclasses import dataclass -import dotenv -import os -import pandas - -from influxdb_client import InfluxDBClient, Point, WritePrecision -from influxdb_client.client.write_api import SYNCHRONOUS - -DIVIDE_BY_10_ENDPOINTS = ["tpPoePower", "tpPoeVoltage"] - -PORT_NAMES = dotenv.dotenv_values(os.path.join(os.path.dirname(__file__), "port-names.conf")) -PORT_NAMES = {int(k): v for k, v in PORT_NAMES.items()} - -@dataclass -class SNMPReading: - endpoint: str - port: int - reading: float - - @classmethod - def from_string(cls, str_): - s = str_.split() - if len(s) != 4: - raise Exception("Couldn't parse") - endpoint_and_port, _, type_, reading = s - endpoint, port = endpoint_and_port.split(".") - - if reading.isdigit(): - reading = int(reading) - if endpoint in DIVIDE_BY_10_ENDPOINTS: - reading = reading / 10 - - return cls(endpoint, int(port), reading) - -def get_alternate_name(port): - try: - return PORT_NAMES[port] - except KeyError: - return port - -def snmp_walk(host): - proc = subprocess.Popen( - ["snmpwalk", "-Os", "-c", "tplink", "-v", "2c", "-m", "TPLINK-POWER-OVER-ETHERNET-MIB", host, "tplinkPowerOverEthernetMIB"], - stdout = subprocess.PIPE - ) - out = [] - while True: - line = proc.stdout.readline() - if not line: - break - try: - out.append(SNMPReading.from_string(line.rstrip().decode())) - except Exception: - pass - - return out - -def readings_to_points(readings, switch_host): - points = [] - df = pandas.DataFrame(readings) - df["port_name"] = df["port"].apply(get_alternate_name) - for p, group_df in df.groupby(["port", "port_name"]): - port, port_name = p - fields = dict(zip(group_df['endpoint'], group_df['reading'])) - - points.append({"measurement": "switch_status", "tags": {"port": port, "port_name": port_name, "switch_host": switch_host}, "fields": fields}) - - return points - -if __name__ == "__main__": - env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "config.env") - if os.path.exists(env_path): - import dotenv - dotenv.load_dotenv(dotenv_path = env_path) - INFLUXDB_HOST = "dns.athome" - else: - INFLUXDB_HOST = "influxdb" - - influxc = InfluxDBClient( - url = "http://%s:8086" % INFLUXDB_HOST, - token = os.environ["DOCKER_INFLUXDB_INIT_ADMIN_TOKEN"], - org = os.environ["DOCKER_INFLUXDB_INIT_ORG"] - ) - influxc.ping() - - for switch_host in os.environ["OMADA_SWITCHES"].split(","): - points = readings_to_points(snmp_walk(switch_host), switch_host) - write_api = influxc.write_api(write_options = SYNCHRONOUS) - write_api.write( - os.environ["DOCKER_INFLUXDB_INIT_BUCKET"], - os.environ["DOCKER_INFLUXDB_INIT_ORG"], - points, - write_precision = WritePrecision.S - ) \ No newline at end of file diff --git a/switch-snmp/snmpOmada.py b/switch-snmp/snmpOmada.py new file mode 100644 index 0000000..f1546a7 --- /dev/null +++ b/switch-snmp/snmpOmada.py @@ -0,0 +1,96 @@ +# wget https://static.tp-link.com/upload/software/2022/202209/20220915/privateMibs(20220831).zip +# cp -v *.mib /home/eden/.snmp/mibs +# sudo apt install snmp +# sudo apt-get install snmp-mibs-downloader + +import subprocess +from dataclasses import dataclass +import dotenv +import os +import pandas +import configparser + +from influxdb_client import InfluxDBClient, Point, WritePrecision +from influxdb_client.client.write_api import SYNCHRONOUS + +DIVIDE_BY_10_ENDPOINTS = ["tpPoePower", "tpPoeVoltage"] + +@dataclass +class SNMPReading: + endpoint: str + port: int + reading: float + + @classmethod + def from_string(cls, str_): + s = str_.split() + if len(s) != 4: + raise Exception("Couldn't parse") + endpoint_and_port, _, type_, reading = s + endpoint, port = endpoint_and_port.split(".") + + if reading.isdigit(): + reading = int(reading) + if endpoint in DIVIDE_BY_10_ENDPOINTS: + reading = reading / 10 + + return cls(endpoint, int(port), reading) + +def get_alternate_name(port, host): + port_names = configparser.ConfigParser() + port_names.read(os.path.join(os.path.dirname(__file__), "omada-switches.conf")) + port_names = {int(k): v for k, v in port_names[host].items()} + + try: + return port_names[port] + except KeyError: + return port + +def snmp_walk(host): + proc = subprocess.Popen( + ["snmpwalk", "-Os", "-c", "tplink", "-v", "2c", "-m", "TPLINK-POWER-OVER-ETHERNET-MIB", host, "tplinkPowerOverEthernetMIB"], + stdout = subprocess.PIPE + ) + out = [] + while True: + line = proc.stdout.readline() + if not line: + break + try: + out.append(SNMPReading.from_string(line.rstrip().decode())) + except Exception: + pass + + return out + +def readings_to_points(readings, switch_host): + points = [] + df = pandas.DataFrame(readings) + df["port_name"] = df["port"].apply(get_alternate_name, args = (switch_host, )) + for p, group_df in df.groupby(["port", "port_name"]): + port, port_name = p + fields = dict(zip(group_df['endpoint'], group_df['reading'])) + + points.append({ + "measurement": "switch_status", + "tags": {"port": port, "port_name": port_name, "switch_host": switch_host, "type": "Omada"}, + "fields": fields + }) + + return points + +def get_points(): + if not os.path.exists(os.path.join(os.path.dirname(__file__), "omada-switches.conf")): + raise FileNotFoundError("Couldn't find config file") + switches = configparser.ConfigParser() + switches.read(os.path.join(os.path.dirname(__file__), "omada-switches.conf")) + points = [] + for switch_host in switches.sections(): + points += readings_to_points(snmp_walk(switch_host), switch_host) + return points + +if __name__ == "__main__": + import mikrotik + points = get_points() + print(points) + mikrotik.append(points) \ No newline at end of file diff --git a/switch-snmp/switches.py b/switch-snmp/switches.py new file mode 100644 index 0000000..52be19a --- /dev/null +++ b/switch-snmp/switches.py @@ -0,0 +1,7 @@ +import snmpOmada +import mikrotik + +if __name__ == "__main__": + points = snmpOmada.get_points() + mikrotik.get_points() + print(points) + mikrotik.append(points) \ No newline at end of file -- cgit v1.2.3