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
log/*.log*
uplink.iml
_tmp
# ---> Python
# 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.
# TODO: CHECK ALL ERROR HANDLING!!!
# TODO: Make use of less args passed to the script and set values in the config file
# TODO: Implement a speedtest/ping that will also run regularly but in an individual interval
# 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!
# 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
import argparse
import calendar
@ -72,7 +69,7 @@ def parse_args():
help='run looped in foreground (default: false)')
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 '
'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',
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
if args.interval:
@ -169,6 +167,12 @@ def main():
st = Thread(target=s.run_server, daemon=True)
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:
for i in range(len(configuration.get_uplinks())):
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):
self.__configuration = configuration
self.__base_configuration = None
self.__database_host = None
self.__database_name = None
self.__database_password = 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.__httpserver = False
self.__httpserver_host = '0.0.0.0'
self.__httpserver_port = 8080
self.__httpserver_port = 1042
self.__interval = 60
self.__log_count = 1
self.__log_file = None
@ -44,6 +46,9 @@ class Configuration:
self.__log_size = 0
self.__pid_file = '/tmp/uplink.pid'
self.__run_mode = 'cron'
self.__speedtest = False
self.__speedtest_interval = 3600
self.__speedtest_url = "https://unixpeople.org/uplink.test"
self.__uplinks = None
if 'database_host' in self.__configuration:
@ -96,12 +101,24 @@ class Configuration:
if 'run_mode' in self.__configuration:
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:
self.__uplinks = self.__configuration['uplinks']
else:
raise Exception('uplinks required!')
# get methods
def get_base_configuration(self):
return self
def get_database_host(self):
return self.__database_host
@ -117,9 +134,9 @@ class Configuration:
def get_env(self):
return self.__env
def get_env_var(self, name):
if name in self.__env:
return self.__env[name]
def get_env_var(self, key):
if key in self.__env:
return self.__env[key]
def get_httpserver(self):
return self.__httpserver
@ -151,6 +168,15 @@ class Configuration:
def get_mode(self):
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):
return self.__uplinks[uplink]
@ -158,8 +184,8 @@ class Configuration:
return self.__uplinks
# set methods
def set_env_var(self, name, value):
self.__env[name] = value
def set_env_var(self, key, value):
self.__env[key] = value
def set_httpserver(self, value):
self.__httpserver = value
@ -167,5 +193,5 @@ class Configuration:
def set_interval(self, interval):
self.__interval = interval
def set_log_file(self, value):
self.__log_file = value
def set_log_file(self, log_file):
self.__log_file = log_file

View file

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

View file

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

View file

@ -18,7 +18,9 @@
# 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 speedtest
import calendar
import requests
import time
from logging import getLogger
@ -30,13 +32,53 @@ class Speedtest:
def __init__(self, configuration):
self.__configuration = configuration
self.download = None
self.upload = None
self.speedtest_maximum_speed = None
self.speedtest_average_speed = None
self.speedtest_time_elapsed = None
def run(self):
speed = speedtest.Speedtest()
self.download = speed.download()
self.upload = speed.upload()
def run_speedtest(self):
while True:
tmp_time = time.localtime(calendar.timegm(time.gmtime()))
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):
return

View file

@ -53,7 +53,8 @@ class Uplink(Daemon):
self.date = time.strftime('%Y-%m-%d', 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)
try:
@ -61,12 +62,16 @@ class Uplink(Daemon):
password=uplink['password'])
if fc.is_connected:
self.status = 'UP'
self.__configuration.set_env_var('_last_success_' + uplink['identifier'],
self.date + ' ' + self.time)
self.__configuration.set_env_var('_last_success_' + uplink['identifier'] +
'_date', self.date + ' ' + self.time)
self.__configuration.set_env_var('_last_success_' + uplink['identifier'] +
'_timestamp', timestamp)
else:
self.status = 'DOWN'
self.__configuration.set_env_var('_last_fail_' + uplink['identifier'],
self.date + ' ' + self.time)
self.__configuration.set_env_var('_last_fail_' + uplink['identifier'] +
'_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 + ') ' +
self.status))
@ -94,7 +99,7 @@ class Uplink(Daemon):
model.set_uptime(fc.connection_uptime)
database = Database(self.__configuration)
database.write_model_to_db(model)
database.write_log_to_db(model)
except Exception as err:
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.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:
for i in range(len(self.__configuration.get_uplinks())):
ut = Thread(target=self.fetch_data, daemon=True, args=(i,))