Lot of refactoring and tests. Maybe all this will be veverted but the code still runs

This commit is contained in:
Johannes Findeisen 2021-11-14 01:09:39 +01:00
commit 6ec071287f
12 changed files with 228 additions and 214 deletions

3
.gitmodules vendored
View file

@ -0,0 +1,3 @@
[submodule "fritzconnection"]
path = fritzconnection
url = https://github.com/kbr/fritzconnection.git

0
docs/.gitkeep Normal file
View file

1
fritzconnection Submodule

@ -0,0 +1 @@
Subproject commit 9e41703431330afcbbd9076ce772282f124ed971

View file

@ -6,13 +6,13 @@ Daemon testing file... Daemons in Python are new to me because I only worked on
Let's have some fun... :) Let's have some fun... :)
""" """
from daemon import Daemon from uplink.daemon import Daemon
class Test(Daemon): class Test(Daemon):
def __init__(self, pid_file, color, size): def __init__(self, pid_file, color, size):
# super().__init__(pid_file) super().__init__(pid_file)
self.pid_file = pid_file self.pid_file = pid_file
self.color = color self.color = color
self.size = size self.size = size
@ -22,5 +22,5 @@ class Test(Daemon):
return return
Test = Test("/tmp/uplink.pid", "Red", "XXL") test = Test("/tmp/uplink.pid", "Red", "XXL")
Test.start() test.start()

114
uplink
View file

@ -1,114 +0,0 @@
#!/usr/bin/python3 -d
# Copyright (c) 2020 Johannes Findeisen <you@hanez.org>
#
# 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()

View file

@ -25,16 +25,17 @@ import calendar
import json import json
import logging import logging
import logging.handlers import logging.handlers
import os
import os.path as path
import pymysql import pymysql
import socket import socket
import sys
import time 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 threading import Thread
from daemon import Daemon from uplink.daemon import Daemon
# Version format: MAJOR.FEATURE.FIXES # Version format: MAJOR.FEATURE.FIXES
__version__ = "0.3.0" __version__ = "0.3.0"

184
uplink.py Normal file → Executable file
View file

@ -1,94 +1,114 @@
import pymysql #!/usr/bin/python3 -d
import socket
import time
import calendar
from fritzconnection.lib.fritzstatus import FritzStatus # Copyright (c) 2020 Johannes Findeisen <you@hanez.org>
from threading import Thread #
from daemon import Daemon # 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): parser.add_argument("config", metavar="CONFIGFILE", help="the configfile to use")
self.pid_file = pid_file
self.config = config
@staticmethod mode = parser.add_mutually_exclusive_group()
def get_data(config, inc): mode.add_argument("--cron", default=False, dest="cron", action="store_true",
try: help="cron mode executes the script only once without loop (default: false)")
# 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()) mode.add_argument("--daemon", default=False, dest="daemon", action="store_true",
local_time = time.localtime(timestamp) help="run as native daemon (default: false)")
d = time.strftime("%Y-%m-%d ", local_time)
t = time.strftime("%H:%M:%S", local_time)
try: mode.add_argument("--foreground", default=True, dest="foreground", action="store_true",
fc = FritzStatus(address=config["uplinks"][inc]["ip"], password=config["uplinks"][inc]["password"]) help="run in foreground (default: true)")
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 parser.add_argument("-i", "--interval", default=60, type=int,
print(str(sql)) help="seconds poll interval (default: 60)")
with con.cursor() as cur:
cur.execute(sql)
con.commit()
exit(1)
if fc.is_connected: parser.add_argument("-b", "--database", metavar="DATABASE", help="database to use")
status = "UP"
else:
status = "DOWN"
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 # 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 uplink = Uplink("/tmp/uplink.pid", _config)
sql = 'INSERT INTO log (timestamp, date, time, uptime, internal_ip, external_ip, external_ipv6, is_linked, ' \ uplink.start()
'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"])

5
uplink/__init__.py Normal file
View file

@ -0,0 +1,5 @@
"""
uplink
library for the uplink project
"""

98
uplink/uplink.py Normal file
View file

@ -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"])