Added a speedtest thread, lot of refactoring and cleanups
This commit is contained in:
parent
d7f74f3914
commit
c2035d35af
13 changed files with 117 additions and 544 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,6 +10,7 @@ __pycache__
|
|||
config.json
|
||||
log/*.log*
|
||||
uplink.iml
|
||||
_tmp
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
|
|
|||
18
bin/uplink
18
bin/uplink
|
|
@ -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,))
|
||||
|
|
|
|||
|
|
@ -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()."""
|
||||
|
||||
|
|
@ -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/
|
||||
|
|
@ -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())
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class Model:
|
|||
self.__timestamp = None
|
||||
self.__uptime = None
|
||||
|
||||
# set methods:
|
||||
# set methods:
|
||||
def set_date(self, date):
|
||||
self.__date = date
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue