diff --git a/.gitmodules b/.gitmodules index e69de29..ae4a4d7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "fritzconnection"] + path = fritzconnection + url = https://github.com/kbr/fritzconnection.git diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/TODO.md b/docs/TODO.md similarity index 100% rename from TODO.md rename to docs/TODO.md diff --git a/fritzconnection b/fritzconnection new file mode 160000 index 0000000..9e41703 --- /dev/null +++ b/fritzconnection @@ -0,0 +1 @@ +Subproject commit 9e41703431330afcbbd9076ce772282f124ed971 diff --git a/test.py b/test.py index f18491c..a55b37e 100644 --- a/test.py +++ b/test.py @@ -6,13 +6,13 @@ Daemon testing file... Daemons in Python are new to me because I only worked on Let's have some fun... :) """ -from daemon import Daemon +from uplink.daemon import Daemon class Test(Daemon): def __init__(self, pid_file, color, size): - # super().__init__(pid_file) + super().__init__(pid_file) self.pid_file = pid_file self.color = color self.size = size @@ -22,5 +22,5 @@ class Test(Daemon): return -Test = Test("/tmp/uplink.pid", "Red", "XXL") -Test.start() +test = Test("/tmp/uplink.pid", "Red", "XXL") +test.start() diff --git a/uplink b/uplink deleted file mode 100755 index b5895b8..0000000 --- a/uplink +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/python3 -d - -# Copyright (c) 2020 Johannes Findeisen -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is furnished -# to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice (including the next -# paragraph) shall be included in all copies or substantial portions of the -# Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF -# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import argparse -import json -import logging -import logging.handlers -from uplink import Uplink - -# Version format: MAJOR.FEATURE.FIXES -__version__ = "0.3.0-development" - -# TODO: Implement logging -# TODO: Implement --no-daemon for a single run e.g. "cron mode" -# TODO: Implement CLI output messages -# TODO: IDEA: Implement a small webserver inline to get statistics and graphs over the network? Or maybe better as a -# separate daemon - - -# TODO: Make use of the args passed to the script -# TODO: Cleanup args and reduce to only what makes sense -def parse_args(): - parser = argparse.ArgumentParser( - description="uplink is a tool to monitor the link status of AVM FRITZ!Box Cable and DSL based routers.", - epilog="uplink is not some program expecting uplinks to work!", - prog="uplink") - - parser.add_argument("config", metavar="CONFIGFILE", help="the configfile to use") - - mode = parser.add_mutually_exclusive_group() - mode.add_argument("--cron", default=False, dest="cron", action="store_true", - help="cron mode executes the script only once without loop (default: false)") - - mode.add_argument("--daemon", default=False, dest="daemon", action="store_true", - help="run as native daemon (default: false)") - - mode.add_argument("--foreground", default=True, dest="foreground", action="store_true", - help="run in foreground (default: true)") - - parser.add_argument("-i", "--interval", default=60, type=int, - help="seconds poll interval (default: 60)") - - parser.add_argument("-b", "--database", metavar="DATABASE", help="database to use") - - parser.add_argument("-u", "--user", type=str, help="database username") - - parser.add_argument("-p", "--password", type=str, help="database password") - - parser.add_argument("-l", "--logfile", metavar="FILE", help="logfile to use") - - parser.add_argument("-c", "--log-count", default=5, type=int, - help="maximum number of logfiles in rotation (default: 5)") - - parser.add_argument("-m", "--log-size", default=10485760, type=int, - help="maximum logfile size in bytes (default: 10485760)") - - parser.add_argument("-s", "--stdout", default=False, dest="stdout", action="store_true", help="log to stdout") - - output = parser.add_mutually_exclusive_group() - output.add_argument("-q", "--quiet", action="store_const", dest="log_level", const=logging.ERROR, - help="output only errors") - - output.add_argument("-w", "--warning", action="store_const", dest="log_level", const=logging.WARNING, - help="output warnings") - - output.add_argument("-v", "--verbose", action="store_const", dest="log_level", const=logging.INFO, - help="output info messages") - - output.add_argument("-d", "--debug", action="store_const", dest="log_level", const=logging.DEBUG, - help="output debug messages") - output.set_defaults(loglevel=logging.ERROR) - - parser.add_argument("--version", action="version", version="%(prog)s " + str(__version__)) - - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - - # TODO: Read --version argument and print program version; then exit. - - # TODO: Cleanup config.json and only define what really is needed - config_path = args.config - try: - with open(config_path, 'r') as configfile: - config_data = configfile.read() - _config = json.loads(config_data) - except Exception as err: - # TODO: Replace all lines like this with generic Python logging - print(str("Uplink: Configuration Error!")) - exit(1) - - uplink = Uplink("/tmp/uplink.pid", _config) - uplink.start() diff --git a/uplink.playground.py b/uplink.playground.py index 33b2f18..dc16602 100644 --- a/uplink.playground.py +++ b/uplink.playground.py @@ -25,16 +25,17 @@ import calendar import json import logging import logging.handlers -import os -import os.path as path import pymysql import socket -import sys import time -from fritzconnection.lib.fritzstatus import FritzStatus +try: + from fritzconnection.fritzconnection.lib.fritzstatus import FritzStatus +except ImportError: + from fritzconnection.lib.fritzstatus import FritzStatus + from threading import Thread -from daemon import Daemon +from uplink.daemon import Daemon # Version format: MAJOR.FEATURE.FIXES __version__ = "0.3.0" diff --git a/uplink.py b/uplink.py old mode 100644 new mode 100755 index 85f2509..3c4e8fd --- a/uplink.py +++ b/uplink.py @@ -1,94 +1,114 @@ -import pymysql -import socket -import time -import calendar +#!/usr/bin/python3 -d -from fritzconnection.lib.fritzstatus import FritzStatus -from threading import Thread -from daemon import Daemon +# Copyright (c) 2020 Johannes Findeisen +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is furnished +# to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice (including the next +# paragraph) shall be included in all copies or substantial portions of the +# Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import argparse +import json +import logging +import logging.handlers +from uplink.uplink import Uplink + +# Version format: MAJOR.FEATURE.FIXES +__version__ = "0.3.0-development" + +# TODO: Implement logging +# TODO: Implement --no-daemon for a single run e.g. "cron mode" +# TODO: Implement CLI output messages +# TODO: IDEA: Implement a small webserver inline to get statistics and graphs over the network? Or maybe better as a +# separate daemon -class Uplink(Daemon): +# TODO: Make use of the args passed to the script +# TODO: Cleanup args and reduce to only what makes sense +def parse_args(): + parser = argparse.ArgumentParser( + description="uplink is a tool to monitor the link status of AVM FRITZ!Box Cable and DSL based routers.", + epilog="uplink is not some program expecting uplinks to work!", + prog="uplink") - def __init__(self, pid_file, config): - self.pid_file = pid_file - self.config = config + parser.add_argument("config", metavar="CONFIGFILE", help="the configfile to use") - @staticmethod - def get_data(config, inc): - try: - # TODO: Think about using a DB ORM (SQLAlchemy?) to make this program supporting different databases like - # sqlite and Postgres - con = pymysql.connect(host=config["database_host"], - user=config["database_user"], - password=config["database_password"], - database=config["database"]) - except Exception as err: - # TODO: Replace all lines like this with generic Python logging - print(str("Uplink: Database connection failed: " + str(err))) - exit(1) + mode = parser.add_mutually_exclusive_group() + mode.add_argument("--cron", default=False, dest="cron", action="store_true", + help="cron mode executes the script only once without loop (default: false)") - timestamp = calendar.timegm(time.gmtime()) - local_time = time.localtime(timestamp) - d = time.strftime("%Y-%m-%d ", local_time) - t = time.strftime("%H:%M:%S", local_time) + mode.add_argument("--daemon", default=False, dest="daemon", action="store_true", + help="run as native daemon (default: false)") - try: - fc = FritzStatus(address=config["uplinks"][inc]["ip"], password=config["uplinks"][inc]["password"]) - except Exception as err: - # TODO: Replace all lines like this with generic Python logging - print(str(str(err) + " on device with ip: " + config["uplinks"][inc]["ip"])) - # TODO: Fix to long lines and make the SQL statement more readable - sql = 'INSERT INTO log (timestamp, date, time, internal_ip, is_linked, is_connected, provider, message, ' \ - 'source_host) VALUES (\"' + str(timestamp) + '\",\"' + str(d) + '\",\"' + str(t) + '\",\"' + \ - str(config["uplinks"][inc]["ip"]) + '\",\"' + "0" + '\",\"' + "0" + '\",\"' + \ - str(config["uplinks"][inc]["provider"]) + '\",\"ERROR: ' + str(err) + '\",\"' + socket.gethostname() + \ - '\")' + mode.add_argument("--foreground", default=True, dest="foreground", action="store_true", + help="run in foreground (default: true)") - # TODO: Replace all lines like this with generic Python logging - print(str(sql)) - with con.cursor() as cur: - cur.execute(sql) - con.commit() - exit(1) + parser.add_argument("-i", "--interval", default=60, type=int, + help="seconds poll interval (default: 60)") - if fc.is_connected: - status = "UP" - else: - status = "DOWN" + parser.add_argument("-b", "--database", metavar="DATABASE", help="database to use") + parser.add_argument("-u", "--user", type=str, help="database username") + + parser.add_argument("-p", "--password", type=str, help="database password") + + parser.add_argument("-l", "--logfile", metavar="FILE", help="logfile to use") + + parser.add_argument("-c", "--log-count", default=5, type=int, + help="maximum number of logfiles in rotation (default: 5)") + + parser.add_argument("-m", "--log-size", default=10485760, type=int, + help="maximum logfile size in bytes (default: 10485760)") + + parser.add_argument("-s", "--stdout", default=False, dest="stdout", action="store_true", help="log to stdout") + + output = parser.add_mutually_exclusive_group() + output.add_argument("-q", "--quiet", action="store_const", dest="log_level", const=logging.ERROR, + help="output only errors") + + output.add_argument("-w", "--warning", action="store_const", dest="log_level", const=logging.WARNING, + help="output warnings") + + output.add_argument("-v", "--verbose", action="store_const", dest="log_level", const=logging.INFO, + help="output info messages") + + output.add_argument("-d", "--debug", action="store_const", dest="log_level", const=logging.DEBUG, + help="output debug messages") + output.set_defaults(loglevel=logging.ERROR) + + parser.add_argument("--version", action="version", version="%(prog)s " + str(__version__)) + + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + + # TODO: Read --version argument and print program version; then exit. + + # TODO: Cleanup config.json and only define what really is needed + config_path = args.config + try: + with open(config_path, 'r') as configfile: + config_data = configfile.read() + _config = json.loads(config_data) + except Exception as err: # TODO: Replace all lines like this with generic Python logging - print(str(config["uplinks"][inc]["provider"] + " " + status)) + print(str("Uplink: Configuration Error!")) + exit(1) - # TODO: Fix to long lines and make the SQL statement more readable - sql = 'INSERT INTO log (timestamp, date, time, uptime, internal_ip, external_ip, external_ipv6, is_linked, ' \ - 'is_connected, str_transmission_rate_up, str_transmission_rate_down, str_max_bit_rate_up, ' \ - 'str_max_bit_rate_down, str_max_linked_bit_rate_up, str_max_linked_bit_rate_down, modelname, ' \ - 'system_version, provider, message, source_host) VALUES (\"' + str(timestamp) + '\",\"' + str(d) + '\",\"' + \ - str(t) + '\",\"' + str(fc.uptime) + '\",\"' + str(config["uplinks"][inc]["ip"]) + '\",\"' + \ - str(fc.external_ip) + '\",\"' + str(fc.external_ipv6) + '\",\"' + str(int(fc.is_linked)) + '\",\"' + \ - str(int(fc.is_connected)) + '\",\"' + str(fc.str_transmission_rate[0]) + '\",\"' + \ - str(fc.str_transmission_rate[1]) + '\",\"' + str(fc.str_max_bit_rate[0]) + '\",\"' + \ - str(fc.str_max_bit_rate[1]) + '\",\"' + str(fc.str_max_linked_bit_rate[0]) + '\",\"' + \ - str(fc.str_max_linked_bit_rate[1]) + '\",\"' + str(fc.modelname) + '\",\"' + \ - str(fc.fc.system_version) + '\",\"' + str(config["uplinks"][inc]["provider"]) + '\",\"' + status + '\",\"' + \ - socket.gethostname() + '\")' - - # TODO: Replace all lines like this with generic Python logging - print(str(sql)) - with con.cursor() as cur: - cur.execute(sql) - con.commit() - con.close() - - def get_config(self): - return self.config - - def run(self): - while True: - for i in range(len(self.config["uplinks"])): - t = Thread(target=self.get_data, args=(self.get_config(), i)) - t.start() - if self.config["cron"]: - break - time.sleep(self.config["interval"]) + uplink = Uplink("/tmp/uplink.pid", _config) + uplink.start() diff --git a/uplink/__init__.py b/uplink/__init__.py new file mode 100644 index 0000000..b5da72c --- /dev/null +++ b/uplink/__init__.py @@ -0,0 +1,5 @@ +""" +uplink + +library for the uplink project +""" diff --git a/daemon.py b/uplink/daemon.py similarity index 100% rename from daemon.py rename to uplink/daemon.py diff --git a/daemon3x.py b/uplink/daemon3x.py similarity index 100% rename from daemon3x.py rename to uplink/daemon3x.py diff --git a/uplink/uplink.py b/uplink/uplink.py new file mode 100644 index 0000000..18e3c7f --- /dev/null +++ b/uplink/uplink.py @@ -0,0 +1,98 @@ +import pymysql +import socket +import time +import calendar + +try: + from fritzconnection.fritzconnection.lib.fritzstatus import FritzStatus +except ImportError: + from fritzconnection.lib.fritzstatus import FritzStatus + +from threading import Thread +from uplink.daemon import Daemon + + +class Uplink(Daemon): + + def __init__(self, pid_file, config): + self.pid_file = pid_file + self.config = config + + @staticmethod + def get_data(config, inc): + try: + # TODO: Think about using a DB ORM (SQLAlchemy?) to make this program supporting different databases like + # sqlite and Postgres + con = pymysql.connect(host=config["database_host"], + user=config["database_user"], + password=config["database_password"], + database=config["database"]) + except Exception as err: + # TODO: Replace all lines like this with generic Python logging + print(str("Uplink: Database connection failed: " + str(err))) + exit(1) + + timestamp = calendar.timegm(time.gmtime()) + local_time = time.localtime(timestamp) + d = time.strftime("%Y-%m-%d ", local_time) + t = time.strftime("%H:%M:%S", local_time) + + try: + fc = FritzStatus(address=config["uplinks"][inc]["ip"], password=config["uplinks"][inc]["password"]) + except Exception as err: + # TODO: Replace all lines like this with generic Python logging + print(str(str(err) + " on device with ip: " + config["uplinks"][inc]["ip"])) + # TODO: Fix to long lines and make the SQL statement more readable + sql = 'INSERT INTO log (timestamp, date, time, internal_ip, is_linked, is_connected, provider, message, ' \ + 'source_host) VALUES (\"' + str(timestamp) + '\",\"' + str(d) + '\",\"' + str(t) + '\",\"' + \ + str(config["uplinks"][inc]["ip"]) + '\",\"' + "0" + '\",\"' + "0" + '\",\"' + \ + str(config["uplinks"][inc]["provider"]) + '\",\"ERROR: ' + str(err) + '\",\"' + socket.gethostname() + \ + '\")' + + # TODO: Replace all lines like this with generic Python logging + print(str(sql)) + with con.cursor() as cur: + cur.execute(sql) + con.commit() + exit(1) + + if fc.is_connected: + status = "UP" + else: + status = "DOWN" + + # TODO: Replace all lines like this with generic Python logging + print(str(config["uplinks"][inc]["provider"] + " " + status)) + + # TODO: Fix to long lines and make the SQL statement more readable + sql = 'INSERT INTO log (timestamp, date, time, uptime, internal_ip, external_ip, external_ipv6, is_linked, ' \ + 'is_connected, str_transmission_rate_up, str_transmission_rate_down, str_max_bit_rate_up, ' \ + 'str_max_bit_rate_down, str_max_linked_bit_rate_up, str_max_linked_bit_rate_down, modelname, ' \ + 'system_version, provider, message, source_host) VALUES (\"' + str(timestamp) + '\",\"' + str(d) + '\",\"' + \ + str(t) + '\",\"' + str(fc.uptime) + '\",\"' + str(config["uplinks"][inc]["ip"]) + '\",\"' + \ + str(fc.external_ip) + '\",\"' + str(fc.external_ipv6) + '\",\"' + str(int(fc.is_linked)) + '\",\"' + \ + str(int(fc.is_connected)) + '\",\"' + str(fc.str_transmission_rate[0]) + '\",\"' + \ + str(fc.str_transmission_rate[1]) + '\",\"' + str(fc.str_max_bit_rate[0]) + '\",\"' + \ + str(fc.str_max_bit_rate[1]) + '\",\"' + str(fc.str_max_linked_bit_rate[0]) + '\",\"' + \ + str(fc.str_max_linked_bit_rate[1]) + '\",\"' + str(fc.modelname) + '\",\"' + \ + str(fc.fc.system_version) + '\",\"' + str(config["uplinks"][inc]["provider"]) + '\",\"' + status + '\",\"' + \ + socket.gethostname() + '\")' + + # TODO: Replace all lines like this with generic Python logging + print(str(sql)) + with con.cursor() as cur: + cur.execute(sql) + con.commit() + con.close() + + def get_config(self): + return self.config + + def run(self): + while True: + for i in range(len(self.config["uplinks"])): + t = Thread(target=self.get_data, args=(self.get_config(), i)) + t.start() + if self.config["cron"]: + break + time.sleep(self.config["interval"])