Added a speedtest thread, lot of refactoring and cleanups

This commit is contained in:
Johannes Findeisen 2022-08-31 00:19:15 +02:00
commit c2035d35af
13 changed files with 117 additions and 544 deletions

1
.gitignore vendored
View file

@ -10,6 +10,7 @@ __pycache__
config.json config.json
log/*.log* log/*.log*
uplink.iml uplink.iml
_tmp
# ---> Python # ---> Python
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files

View file

@ -21,11 +21,8 @@
# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# TODO: CHECK ALL ERROR HANDLING!!! # TODO: CHECK ALL ERROR HANDLING!!!
# TODO: Make use of less args passed to the script and set values in the config file # TODO: become more verbose in each log level and log only to error when it really
# TODO: Implement a speedtest/ping that will also run regularly but in an individual interval # is an error else log to info and even add debug messages in DEBUG level
# TODO: become more verbose in each log level and log only to error when it really is an error
# else log to info and even add debug messages in DEBUG level
# TODO: make all args consistent and clean... there is some stuff to do!
import argparse import argparse
import calendar import calendar
@ -72,7 +69,7 @@ def parse_args():
help='run looped in foreground (default: false)') help='run looped in foreground (default: false)')
parser.add_argument('--httpserver', default=False, dest='httpserver', action='store_true', parser.add_argument('--httpserver', default=False, dest='httpserver', action='store_true',
help='start http server for status information and statistics') help='enable the http server for status information and statistics')
parser.add_argument('-i', '--interval', type=int, help='poll interval in seconds. this ' parser.add_argument('-i', '--interval', type=int, help='poll interval in seconds. this '
'overrides config file settings (default: 60)') 'overrides config file settings (default: 60)')
@ -103,7 +100,8 @@ def main():
configuration.set_env_var('_start_date', time.strftime('%Y-%m-%d %H:%M:%S', configuration.set_env_var('_start_date', time.strftime('%Y-%m-%d %H:%M:%S',
time.localtime( time.localtime(
calendar.timegm(time.gmtime())))) calendar.timegm(time.gmtime()))))
configuration.set_env_var('_start_timestamp', calendar.timegm(time.gmtime()))
# interval set in args is overriding configuration and default # interval set in args is overriding configuration and default
if args.interval: if args.interval:
@ -169,6 +167,12 @@ def main():
st = Thread(target=s.run_server, daemon=True) st = Thread(target=s.run_server, daemon=True)
st.start() st.start()
if configuration.get_speedtest():
from uplink.speedtest import Speedtest
se = Speedtest(configuration)
ste = Thread(target=se.run_speedtest, daemon=True)
ste.start()
while True: while True:
for i in range(len(configuration.get_uplinks())): for i in range(len(configuration.get_uplinks())):
ft = Thread(target=u.fetch_data, daemon=True, args=(i,)) ft = Thread(target=u.fetch_data, daemon=True, args=(i,))

View file

@ -1,133 +0,0 @@
"""
Generic linux daemon base class for python 3.x.
Original Source:
https://web.archive.org/web/20131017130434/http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
https://web.archive.org/web/20131017130434/http://www.jejik.com/files/examples/daemon3x.py
See files/daemon3x.py in the uplink source tree.
More Information:
https://franklingu.github.io/programming/2016/03/01/creating-daemon-process-python-example-explanation/
https://dpbl.wordpress.com/2017/02/12/a-tutorial-on-python-daemon/
"""
import sys, os, time, atexit, signal
class daemon:
"""A generic daemon class.
Usage: subclass the daemon class and override the run() method."""
def __init__(self, pidfile): self.pidfile = pidfile
def daemonize(self):
"""Deamonize class. UNIX double fork mechanism."""
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #1 failed: {0}\n'.format(err))
sys.exit(1)
# decouple from parent environment
os.chdir('/')
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #2 failed: {0}\n'.format(err))
sys.exit(1)
# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = open(os.devnull, 'r')
so = open(os.devnull, 'a+')
se = open(os.devnull, 'a+')
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
# write pidfile
atexit.register(self.delpid)
pid = str(os.getpid())
with open(self.pidfile,'w+') as f:
f.write(pid + '\n')
def delpid(self):
os.remove(self.pidfile)
def start(self):
"""Start the daemon."""
# Check for a pidfile to see if the daemon already runs
try:
with open(self.pidfile,'r') as pf:
pid = int(pf.read().strip())
except IOError:
pid = None
if pid:
message = "pidfile {0} already exist. " + \
"Daemon already running?\n"
sys.stderr.write(message.format(self.pidfile))
sys.exit(1)
# Start the daemon
self.daemonize()
self.run()
def stop(self):
"""Stop the daemon."""
# Get the pid from the pidfile
try:
with open(self.pidfile,'r') as pf:
pid = int(pf.read().strip())
except IOError:
pid = None
if not pid:
message = "pidfile {0} does not exist. " + \
"Daemon not running?\n"
sys.stderr.write(message.format(self.pidfile))
return # not an error in a restart
# Try killing the daemon process
try:
while 1:
os.kill(pid, signal.SIGTERM)
time.sleep(0.1)
except OSError as err:
e = str(err.args)
if e.find("No such process") > 0:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
print (str(err.args))
sys.exit(1)
def restart(self):
"""Restart the daemon."""
self.stop()
self.start()
def run(self):
"""You should override this method when you subclass Daemon.
It will be called after the process has been daemonized by
start() or restart()."""

View file

@ -1,11 +0,0 @@
- https://cherrypy.dev/
- https://docs.cherrypy.dev/en/latest/index.html
- https://docs.cherrypy.dev/en/latest/tutorials.html
- https://cherrypydocrework.readthedocs.io/index.html
- https://helpful.knobs-dials.com/index.php/CherryPy
- https://bottlepy.org/docs/dev/index.html
- https://www.sqlalchemy.org/
- https://docs.sqlalchemy.org/en/14/

View file

@ -1,47 +0,0 @@
# Copyright (c) 2022 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 json
from bottle import route
from bottle import run
from bottle import template
from logging import getLogger
logger = getLogger(__name__)
class Server:
def __init__(self, configuration):
self.__configuration = configuration
@route('/hello/<name>')
def index(self, name):
return template('<b>Hello {{name}}</b>!', name=name)
@route('/config')
def config(self):
return template('<pre>{{config}}</pre>!', config=json.dumps(vars(self.__configuration),
sort_keys=True, indent=4))
def run_server(self):
run(host=self.__configuration.get_httpserver_host(),
port=self.__configuration.get_httpserver_port())

View file

@ -1,46 +0,0 @@
# Copyright (c) 2022 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 json
from logging import getLogger
from pyramid.config import Configurator
from pyramid.response import Response
from wsgiref.simple_server import make_server
logger = getLogger(__name__)
class Server:
def __init__(self, configuration):
self.__configuration = configuration
def config(self, request):
return Response(config=json.dumps(vars(self.__configuration), sort_keys=True, indent=4))
def run_server(self):
with Configurator() as config:
config.add_route('config', '/')
config.add_view(self.config, route_name='config')
app = config.make_wsgi_app()
server = make_server(self.__configuration.get_httpserver_host(),
self.__configuration.get_httpserver_port(), app)
server.serve_forever()

View file

@ -1,46 +0,0 @@
#!/usr/bin/python3 -d
# Copyright (c) 2021 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.
"""
Daemon testing file... Daemons in Python are new to me because I only worked on CLI, Gtk+ and Django projects before.
Let's have some fun... :)
"""
from uplink.daemon import Daemon
class Test(Daemon):
def __init__(self, pid_file, color, size):
super().__init__(pid_file)
self.pid_file = pid_file
self.color = color
self.size = size
def run(self):
# Write my daemon test code here:
return
test = Test("/tmp/uplink.pid", "Red", "XXL")
test.start()

View file

@ -1,228 +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 calendar
import json
import logging
import logging.handlers
import pymysql
import socket
import time
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
# Version format: MAJOR.FEATURE.FIXES
__version__ = "0.3.0"
# logger = logging.getLogger(__name__)
# PURPLE = "\033[95m"
# BLUE = "\033[94m"
# YELLOW = "\033[93m"
# GRAY = "\033[90m"
# END = "\033[0m"
class Uplink(Daemon):
def __init__(self, pid_file, config):
self.pid_file = pid_file
self.config = config
@staticmethod
def get_data(config, inc):
try:
con = pymysql.connect(host=config["database_host"],
user=config["database_user"],
password=config["database_password"],
database=config["database"])
except Exception as err:
# logger.error("Database connection failed: " + str(err))
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:
# logger.error(str(err) + " on device with ip: " + config["uplinks"][inc]["ip"])
print(str(str(err) + " on device with ip: " + config["uplinks"][inc]["ip"]))
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() + \
'\")'
# logger.debug(sql)
print(str(sql))
with con.cursor() as cur:
cur.execute(sql)
con.commit()
exit(1)
if fc.is_connected:
status = "UP"
else:
status = "DOWN"
# logger.info(config["uplinks"][inc]["provider"] + " " + status)
print(str(config["uplinks"][inc]["provider"] + " " + status))
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() + '\")'
# logger.debug(sql)
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"])
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", "--logcount", default=5, type=int,
help="maximum number of logfiles in rotation (default: 5)")
parser.add_argument("-m", "--logsize", 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="loglevel", const=logging.ERROR,
help="output only errors")
output.add_argument("-w", "--warning", action="store_const", dest="loglevel", const=logging.WARNING,
help="output warnings")
output.add_argument("-v", "--verbose", action="store_const", dest="loglevel", const=logging.INFO,
help="output info messages")
output.add_argument("-d", "--debug", action="store_const", dest="loglevel", 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()
# root_logger = logging.getLogger()
# if args.stdout:
# formatter = logging.Formatter(
# PURPLE + "%(asctime)s" + END + ":" + BLUE + "%(levelname)s" + END + ":" + YELLOW + "%(name)s" + END + ":" + GRAY + "%(message)s" + END)
# handler1 = logging.StreamHandler(sys.stdout)
# handler1.setFormatter(formatter)
# root_logger.addHandler(handler1)
# if args.logfile:
# logfile = path.expanduser(args.logfile)
# if not path.exists(path.dirname(logfile)):
# os.makedirs(path.dirname(logfile))
# formatter = logging.Formatter("%(asctime)s:%(levelname)s:%(name)s:%(message)s")
# handler2 = logging.handlers.RotatingFileHandler(args.logfile, maxBytes=args.logsize, backupCount=args.logcount)
# handler2.setFormatter(formatter)
# root_logger.addHandler(handler2)
# root_logger.setLevel(args.loglevel)
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:
# logger.error("Reading configuration file '" + config_path + "' failed: " + str(err))
print(str("Uplink: Configuration Error!"))
exit(1)
# try:
# run(_config)
# except KeyboardInterrupt:
# logger.info("Program terminated!")
# exit(0)
Uplink = Uplink("/tmp/uplink.pid", _config)
Uplink.start()

View file

@ -28,15 +28,17 @@ class Configuration:
def __init__(self, configuration): def __init__(self, configuration):
self.__configuration = configuration self.__configuration = configuration
self.__base_configuration = None
self.__database_host = None self.__database_host = None
self.__database_name = None self.__database_name = None
self.__database_password = None self.__database_password = None
self.__database_user = None self.__database_user = None
# environment vars which can be set dynamically at runtime. # environment vars which can be set dynamically at runtime. these vars are shared across all
# objects and readable and writable by all of them.
self.__env = {} self.__env = {}
self.__httpserver = False self.__httpserver = False
self.__httpserver_host = '0.0.0.0' self.__httpserver_host = '0.0.0.0'
self.__httpserver_port = 8080 self.__httpserver_port = 1042
self.__interval = 60 self.__interval = 60
self.__log_count = 1 self.__log_count = 1
self.__log_file = None self.__log_file = None
@ -44,6 +46,9 @@ class Configuration:
self.__log_size = 0 self.__log_size = 0
self.__pid_file = '/tmp/uplink.pid' self.__pid_file = '/tmp/uplink.pid'
self.__run_mode = 'cron' self.__run_mode = 'cron'
self.__speedtest = False
self.__speedtest_interval = 3600
self.__speedtest_url = "https://unixpeople.org/uplink.test"
self.__uplinks = None self.__uplinks = None
if 'database_host' in self.__configuration: if 'database_host' in self.__configuration:
@ -96,12 +101,24 @@ class Configuration:
if 'run_mode' in self.__configuration: if 'run_mode' in self.__configuration:
self.__run_mode = self.__configuration['run_mode'] self.__run_mode = self.__configuration['run_mode']
if 'speedtest' in self.__configuration:
self.__speedtest = self.__configuration['speedtest']
if 'speedtest_interval' in self.__configuration:
self.__speedtest_interval = self.__configuration['speedtest_interval']
if 'speedtest_url' in self.__configuration:
self.__speedtest_url = self.__configuration['speedtest_url']
if 'uplinks' in self.__configuration: if 'uplinks' in self.__configuration:
self.__uplinks = self.__configuration['uplinks'] self.__uplinks = self.__configuration['uplinks']
else: else:
raise Exception('uplinks required!') raise Exception('uplinks required!')
# get methods # get methods
def get_base_configuration(self):
return self
def get_database_host(self): def get_database_host(self):
return self.__database_host return self.__database_host
@ -117,9 +134,9 @@ class Configuration:
def get_env(self): def get_env(self):
return self.__env return self.__env
def get_env_var(self, name): def get_env_var(self, key):
if name in self.__env: if key in self.__env:
return self.__env[name] return self.__env[key]
def get_httpserver(self): def get_httpserver(self):
return self.__httpserver return self.__httpserver
@ -151,6 +168,15 @@ class Configuration:
def get_mode(self): def get_mode(self):
return self.__run_mode return self.__run_mode
def get_speedtest(self):
return self.__speedtest
def get_speedtest_interval(self):
return self.__speedtest_interval
def get_speedtest_url(self):
return self.__speedtest_url
def get_uplink(self, uplink): def get_uplink(self, uplink):
return self.__uplinks[uplink] return self.__uplinks[uplink]
@ -158,8 +184,8 @@ class Configuration:
return self.__uplinks return self.__uplinks
# set methods # set methods
def set_env_var(self, name, value): def set_env_var(self, key, value):
self.__env[name] = value self.__env[key] = value
def set_httpserver(self, value): def set_httpserver(self, value):
self.__httpserver = value self.__httpserver = value
@ -167,5 +193,5 @@ class Configuration:
def set_interval(self, interval): def set_interval(self, interval):
self.__interval = interval self.__interval = interval
def set_log_file(self, value): def set_log_file(self, log_file):
self.__log_file = value self.__log_file = log_file

View file

@ -30,7 +30,7 @@ class Database:
def __init__(self, configuration): def __init__(self, configuration):
self.__configuration = configuration self.__configuration = configuration
def write_model_to_db(self, model): def write_log_to_db(self, model):
try: try:
con = pymysql.connect(host=self.__configuration.get_database_host(), con = pymysql.connect(host=self.__configuration.get_database_host(),
user=self.__configuration.get_database_user(), user=self.__configuration.get_database_user(),
@ -57,7 +57,7 @@ class Database:
'system_version, ' \ 'system_version, ' \
'provider, ' \ 'provider, ' \
'message, ' \ 'message, ' \
'source_host '\ 'source_host ' \
') VALUES (\"' + \ ') VALUES (\"' + \
str(model.get_timestamp()) + '\",\"' + \ str(model.get_timestamp()) + '\",\"' + \
str(model.get_date()) + '\",\"' + \ str(model.get_date()) + '\",\"' + \
@ -78,7 +78,7 @@ class Database:
str(model.get_system_version()) + '\",\"' + \ str(model.get_system_version()) + '\",\"' + \
str(model.get_provider()) + '\",\"' + \ str(model.get_provider()) + '\",\"' + \
str(model.get_message()) + '\",\"' + \ str(model.get_message()) + '\",\"' + \
str(model.get_source_host() + '\")') str(model.get_source_host()) + '\")'
with con.cursor() as cur: with con.cursor() as cur:
cur.execute(sql) cur.execute(sql)

View file

@ -52,7 +52,7 @@ class Model:
self.__timestamp = None self.__timestamp = None
self.__uptime = None self.__uptime = None
# set methods: # set methods:
def set_date(self, date): def set_date(self, date):
self.__date = date self.__date = date

View file

@ -18,7 +18,9 @@
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF # 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. # OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import speedtest import calendar
import requests
import time
from logging import getLogger from logging import getLogger
@ -30,13 +32,53 @@ class Speedtest:
def __init__(self, configuration): def __init__(self, configuration):
self.__configuration = configuration self.__configuration = configuration
self.download = None self.speedtest_maximum_speed = None
self.upload = None self.speedtest_average_speed = None
self.speedtest_time_elapsed = None
def run(self): def run_speedtest(self):
speed = speedtest.Speedtest() while True:
self.download = speed.download() tmp_time = time.localtime(calendar.timegm(time.gmtime()))
self.upload = speed.upload() self.__configuration.set_env_var('_speedtest_last_run_date',
time.strftime('%Y-%m-%d %H:%M:%S',
tmp_time))
self.__configuration.set_env_var('_speedtest_last_run_timestamp',
calendar.timegm(time.gmtime()))
start = time.perf_counter()
request = requests.get(self.__configuration.get_speedtest_url(), stream=True)
size = int(request.headers.get('Content-Length'))
downloaded = 0.0
total_mbps = 0.0
maximum_speed = 0.0
total_chunks = 0.0
if size is not None:
for chunk in request.iter_content(1024 * 1024):
downloaded += len(chunk)
# megabytes per second
mbps = downloaded / (time.perf_counter() - start) / (1024 * 1024)
if mbps > maximum_speed:
maximum_speed = mbps
total_chunks += 1
total_mbps += mbps
self.speedtest_maximum_speed = maximum_speed
self.__configuration.set_env_var('_speedtest_maximum_speed_megabyte_per_second',
str(round(self.speedtest_maximum_speed)))
self.speedtest_average_speed = total_mbps / total_chunks
self.__configuration.set_env_var('_speedtest_average_speed_megabyte_per_second',
str(round(self.speedtest_average_speed)))
self.speedtest_time_elapsed = time.perf_counter() - start
self.__configuration.set_env_var('_speedtest_time_elapsed',
str(self.speedtest_time_elapsed))
else:
logger.warning("could not calculate download speed!")
time.sleep(self.__configuration.get_speedtest_interval())
def write_to_db(self): def write_to_db(self):
return return

View file

@ -53,7 +53,8 @@ class Uplink(Daemon):
self.date = time.strftime('%Y-%m-%d', local_time) self.date = time.strftime('%Y-%m-%d', local_time)
self.time = time.strftime('%H:%M:%S', local_time) self.time = time.strftime('%H:%M:%S', local_time)
self.__configuration.set_env_var('_last_run', self.date + ' ' + self.time) self.__configuration.set_env_var('_last_run_date', self.date + ' ' + self.time)
self.__configuration.set_env_var('_last_run_timestamp', timestamp)
uplink = self.__configuration.get_uplink(i) uplink = self.__configuration.get_uplink(i)
try: try:
@ -61,12 +62,16 @@ class Uplink(Daemon):
password=uplink['password']) password=uplink['password'])
if fc.is_connected: if fc.is_connected:
self.status = 'UP' self.status = 'UP'
self.__configuration.set_env_var('_last_success_' + uplink['identifier'], self.__configuration.set_env_var('_last_success_' + uplink['identifier'] +
self.date + ' ' + self.time) '_date', self.date + ' ' + self.time)
self.__configuration.set_env_var('_last_success_' + uplink['identifier'] +
'_timestamp', timestamp)
else: else:
self.status = 'DOWN' self.status = 'DOWN'
self.__configuration.set_env_var('_last_fail_' + uplink['identifier'], self.__configuration.set_env_var('_last_fail_' + uplink['identifier'] +
self.date + ' ' + self.time) '_date', self.date + ' ' + self.time)
self.__configuration.set_env_var('_last_fail_' + uplink['identifier'] +
'_timestamp', timestamp)
logger.info(str(uplink['provider'] + ' (IP: ' + fc.external_ip + ') ' + logger.info(str(uplink['provider'] + ' (IP: ' + fc.external_ip + ') ' +
self.status)) self.status))
@ -94,7 +99,7 @@ class Uplink(Daemon):
model.set_uptime(fc.connection_uptime) model.set_uptime(fc.connection_uptime)
database = Database(self.__configuration) database = Database(self.__configuration)
database.write_model_to_db(model) database.write_log_to_db(model)
except Exception as err: except Exception as err:
message = str('error when getting data from ' + uplink['ip'] + ': {0}') message = str('error when getting data from ' + uplink['ip'] + ': {0}')
@ -107,6 +112,12 @@ class Uplink(Daemon):
st = Thread(target=s.run_server, daemon=True) st = Thread(target=s.run_server, daemon=True)
st.start() st.start()
if self.__configuration.get_speedtest():
from uplink.speedtest import Speedtest
se = Speedtest(self.__configuration)
ste = Thread(target=se.run_speedtest, daemon=True)
ste.start()
while True: while True:
for i in range(len(self.__configuration.get_uplinks())): for i in range(len(self.__configuration.get_uplinks())):
ut = Thread(target=self.fetch_data, daemon=True, args=(i,)) ut = Thread(target=self.fetch_data, daemon=True, args=(i,))