Totally refactored. Split everything in classes. Integrated HTTP server support for a status page. bottle, celery and pyramid supported but I will choose only one. Currently cherrypy looks perfect. Writing results to a database will become optionally and will use SQLAlchemy in the next days. This all is a work in progress and the code should not really be used. I am refreshing my Python skills and I hope the next release will be modular and usable for everybody... stay tuned

This commit is contained in:
Johannes Findeisen 2022-08-27 18:46:47 +02:00
commit a14fccfb71
16 changed files with 780 additions and 167 deletions

View file

@ -1,7 +1,5 @@
MIT License
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

View file

@ -3,7 +3,7 @@
# 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
# 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
@ -13,14 +13,24 @@
# 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
# 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.
# 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: Maybe make path to pid_file an arg to use /var/run/user/$uid/uplink.pid instead of
# /tmp/uplink.pid or check user id and write it to /var/run or var/run/user/$uid automatically
# TODO: make all args consistent and clean... there is some stuff to do!
import argparse
import calendar
import json
import logging
import logging.handlers
@ -28,87 +38,73 @@ import os
import sys
import time
from uplink.configuration import Configuration
from uplink.uplink import Uplink
# Version format: MAJOR.FEATURE.FIXES
__version__ = "0.6.1"
# TODO: CHECK ALL ERROR HANDLING!!!
# 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 more args passed to the script
# TODO: Implement a speedtest 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
# TODO: Maybe make path to pid_file an arg to use /var/run/$user/uplink.pid instead of
# /tmp/uplink.pid
__version__ = '0.7.0'
__author__ = 'Johannes Findeisen <you@hanez.org>'
logger = logging.getLogger(__name__)
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")
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")
parser.add_argument('configuration_file', metavar='CONFIGFILE',
help='the configuration file to use')
mode = parser.add_mutually_exclusive_group()
mode.add_argument("-c", "--cron", default=False, dest="cron", action="store_true",
help="cron mode executes the script only once without loop (default: false)")
mode.add_argument('-c', '--cron', default=False, dest='cron', action='store_true',
help='cron mode executes the script only once without loop (default: false)')
mode.add_argument("-d", "--daemon", default=False, dest="daemon", action="store_true",
help="run as native daemon (default: false)")
mode.add_argument('-d', '--daemon', default=False, dest='daemon', action='store_true',
help='run as native daemon (default: false)')
parser.add_argument("-k", "--kill", default=False, dest="kill", action="store_true",
help="kill the daemon if it is running (default: false)")
parser.add_argument('-k', '--kill', default=False, dest='kill', action='store_true',
help='kill the daemon if it is running (default: false)')
parser.add_argument("-r", "--restart", default=False, dest="restart", action="store_true",
help="restart the daemon if it is running(default: false)")
parser.add_argument('-r', '--restart', default=False, dest='restart', action='store_true',
help='restart the daemon if it is running(default: false)')
mode.add_argument("-f", "--foreground", default=False, dest="foreground", action="store_true",
help="run looped in foreground (default: false)")
mode.add_argument('-f', '--foreground', default=False, dest='foreground', action='store_true',
help='run looped in foreground (default: false)')
parser.add_argument("-i", "--interval", type=int, help="poll interval in seconds. this "
"overrides config file settings. (default: 60)")
parser.add_argument('--server', default=False, dest='server', action='store_true',
help='start http server for status information and statistics')
"""
TODO: maybe make database connection information as args... not sure.
parser.add_argument("-b", "--database", metavar="DATABASE", help="database to use")
parser.add_argument('-i', '--interval', type=int, help='poll interval in seconds. this '
'overrides config file settings. (default: 60)')
parser.add_argument("-u", "--user", type=str, help="database username")
parser.add_argument('-l', '--logfile', metavar='LOGFILE', help='logfile to use')
parser.add_argument("-p", "--password", type=str, help="database password")
"""
parser.add_argument('-n', '--logcount', default=5, type=int,
help='maximum number of logfiles in rotation (default: 5)')
parser.add_argument("-l", "--logfile", metavar="LOGFILE", help="logfile to use")
parser.add_argument('-m', '--logsize', default=10485760, type=int,
help='maximum logfile size in bytes (default: 1048576)')
parser.add_argument("-n", "--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: 1048576)")
parser.add_argument("-s", "--stdout", default=False, dest="stdout", action="store_true",
help="log to stdout")
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('-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('-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('-v', '--verbose', action='store_const', dest='loglevel',
const=logging.INFO, help='output info messages')
output.add_argument("-e", "--debug", action="store_const", dest="loglevel",
const=logging.DEBUG, help="output debug messages")
output.add_argument('-e', '--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__))
parser.add_argument('--version', action='version', version='%(prog)s ' + str(__version__))
return parser.parse_args()
@ -117,53 +113,57 @@ def main():
args = parse_args()
root_logger = logging.getLogger()
logger.error("TEST")
if args.logfile:
logfile = os.path.expanduser(args.logfile)
if not os.path.exists(os.path.dirname(logfile)):
os.makedirs(os.path.dirname(logfile))
formatter1 = logging.Formatter("%(asctime)s:%(levelname)s:%(name)s:%(message)s")
formatter1 = logging.Formatter('%(asctime)s:%(levelname)s:%(name)s:%(message)s')
handler1 = logging.handlers.RotatingFileHandler(args.logfile, maxBytes=args.logsize,
backupCount=args.logcount)
handler1.setFormatter(formatter1)
root_logger.addHandler(handler1)
if args.stdout:
formatter2 = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s")
formatter2 = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s')
handler2 = logging.StreamHandler(sys.stdout)
handler2.setFormatter(formatter2)
root_logger.addHandler(handler2)
root_logger.setLevel(args.loglevel)
config_path = args.config
try:
with open(config_path, 'r', encoding='utf-8') as configfile:
config_data = configfile.read()
config = json.loads(config_data)
with open(args.configuration_file, 'r', encoding='utf-8') as configuration_file:
configuration_data = configuration_file.read()
configuration = Configuration(json.loads(configuration_data))
except Exception as err:
logger.error(str("uplink: configuration error! {0}".format(err)))
logger.error(str('[uplink] configuration error! {0}'.format(err)))
sys.exit(1)
try:
config["interval"]
except KeyError:
# interval not configured in configuration; using default value
config["interval"] = 60
configuration.set_env_var('_start_date', time.strftime('%Y-%m-%d %H:%M:%S',
time.localtime(
calendar.timegm(time.gmtime()))))
# interval set in args is overriding configuration and default
if args.interval:
# interval set in args is overriding configuration and default
config["interval"] = args.interval
configuration.set_interval(args.interval)
pid_file = "/tmp/uplink.pid"
u = Uplink(pid_file, config)
configuration.set_env_var('_server', False)
if args.server:
configuration.set_env_var('_server', True)
try:
u = Uplink(configuration.get_pid_file(), configuration)
except Exception as err:
logger.error(str('[uplink] core error! {0}'.format(err)))
sys.exit(1)
if args.cron:
from threading import Thread
for i in range(len(config["uplinks"])):
t = Thread(target=u.fetch_data, args=(config, i))
t.start()
for i in range(len(configuration.get_uplinks())):
ct = Thread(target=u.fetch_data, daemon=True, args=(i,))
ct.start()
elif args.daemon:
if args.kill:
u.stop()
@ -173,19 +173,39 @@ def main():
u.start()
elif args.foreground:
from threading import Thread
if configuration.get_env_var('_server'):
if configuration.get_httpserver_framework() == 'bottle':
from uplink.server_bottle import Server
s = Server(configuration)
st = Thread(target=s.run, daemon=True)
st.start()
elif configuration.get_httpserver_framework() == 'cherrypy':
from uplink.server_cherrypy import Server
s = Server(configuration)
st = Thread(target=s.run, daemon=True)
st.start()
elif configuration.get_httpserver_framework() == 'pyramid':
from uplink.server_pyramid import Server
s = Server(configuration)
st = Thread(target=s.run, daemon=True)
st.start()
else:
logger.error('[uplink] no http framework configured! '
'(bottle, cherrypy and pyramid are supported)')
while True:
for i in range(len(config["uplinks"])):
t = Thread(target=u.fetch_data, args=(config, i))
t.start()
for i in range(len(configuration.get_uplinks())):
ft = Thread(target=u.fetch_data, daemon=True, args=(i,))
ft.start()
try:
time.sleep(config["interval"])
time.sleep(configuration.get_interval())
except KeyboardInterrupt:
logger.info(str("uplink: program terminated by user!"))
logger.info(str('[uplink] program terminated by user!'))
sys.exit(0)
else:
logger.error("uplink: no run mode selected; use --cron (-c), --daemon (-d) or "
"--foreground (-f) to run uplink. use --help for more information")
logger.error('uplink: no run mode selected; use --cron (-c), --daemon (-d) or '
'--foreground (-f) to run uplink. use --help for more information')
if __name__ == "__main__":
if __name__ == '__main__':
main()

View file

@ -1,13 +0,0 @@
{
"interval": 60,
"database": "uplink",
"database_host": "127.0.0.1",
"database_user": "USER",
"database_password": "PASSWORD",
"mode": "cron",
"log_level": "info",
"uplinks": [
{ "provider": "Cable Provider", "ip": "192.168.0.1", "password": "1234" },
{ "provider": "DSL Provider", "ip": "192.168.1.1", "password": "1234" }
]
}

24
etc/config.example.json Normal file
View file

@ -0,0 +1,24 @@
{
"interval": 60,
"database_name": "uplink",
"database_host": "127.0.0.1",
"database_user": "USER",
"database_password": "PASSWORD",
"httpserver_host": "0.0.0.0",
"httpserver_port": 8080,
"httpserver_framework": "cherrypy",
"mode": "cron",
"log_level": "info",
"uplinks": [
{
"provider": "Cable Provider",
"ip": "192.168.0.1",
"password": "1234"
},
{
"provider": "DSL Provider",
"ip": "192.168.1.1",
"password": "1234"
}
]
}

11
files/devhelp.md Normal file
View file

@ -0,0 +1,11 @@
- 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/

0
templates/.gitkeep Normal file
View file

141
uplink/configuration.py Normal file
View file

@ -0,0 +1,141 @@
# 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.
from logging import getLogger
logger = getLogger(__name__)
class Configuration:
def __init__(self, configuration):
self.__configuration = configuration
self.__database_host = None
self.__database_name = None
self.__database_password = None
self.__database_user = None
self.__httpserver_framework = None
self.__httpserver_host = '0.0.0.0'
self.__httpserver_port = 8080
self.__interval = '60'
self.__log_count = 5
self.__log_file = None
self.__log_level = 'info'
self.__log_size = 10485760,
self.__pid_file = '/tmp/uplink.pid'
self.__run_mode = None
self.__uplinks = None
self.__env = {}
if'database_host' in self.__configuration:
self.__database_host = self.__configuration['database_host']
else:
raise Exception('database_host required!')
if 'database_name' in self.__configuration:
self.__database_name = self.__configuration['database_name']
else:
raise Exception('database_name required!')
if 'database_password' in self.__configuration:
self.__database_password = self.__configuration['database_password']
else:
raise Exception('database_password required!')
if 'database_user' in self.__configuration:
self.__database_user = self.__configuration['database_user']
else:
raise Exception('database_user required!')
if 'httpserver_framework' in self.__configuration:
self.__httpserver_framework = self.__configuration['httpserver_framework']
if 'httpserver_host' in self.__configuration:
self.__httpserver_host = self.__configuration['httpserver_host']
if 'httpserver_port' in self.__configuration:
self.__httpserver_port = self.__configuration['httpserver_port']
if 'interval' in self.__configuration:
self.__interval = self.__configuration['interval']
if 'log_level' in self.__configuration:
self.__log_level = self.__configuration['log_level']
if 'run_mode' in self.__configuration:
self.__run_mode = self.__configuration['run_mode']
if 'uplinks' in self.__configuration:
self.__uplinks = self.__configuration['uplinks']
else:
raise Exception('uplinks required!')
def get_database_host(self):
return self.__database_host
def get_database_name(self):
return self.__database_name
def get_database_password(self):
return self.__database_password
def get_database_user(self):
return self.__database_user
def get_env(self):
return self.__env
def get_env_var(self, name):
return self.__env[name]
def set_env_var(self, name, value):
self.__env[name] = value
def get_httpserver_framework(self):
return self.__httpserver_framework
def get_httpserver_host(self):
return self.__httpserver_host
def get_httpserver_port(self):
return self.__httpserver_port
def get_interval(self):
return self.__interval
def set_interval(self, interval):
self.__interval = interval
def get_log_level(self):
return self.__log_level
def get_pid_file(self):
return self.__pid_file
def get_mode(self):
return self.__run_mode
def get_uplink(self, uplink):
return self.__uplinks[uplink]
def get_uplinks(self):
return self.__uplinks

View file

@ -49,7 +49,7 @@ class Daemon:
# Exit first parent
sys.exit(0)
except OSError as err:
logger.error(str("fork #1 failed: {0}".format(err)))
logger.error(str('fork #1 failed: {0}'.format(err)))
sys.exit(1)
# Decouple from parent environment
@ -64,7 +64,7 @@ class Daemon:
# Exit from second parent
sys.exit(0)
except OSError as err:
logger.error(str("fork #2 failed: {0}".format(err)))
logger.error(str('fork #2 failed: {0}'.format(err)))
sys.exit(1)
# Redirect standard file descriptors
@ -101,7 +101,7 @@ class Daemon:
pid = None
if pid:
message = "pid_file {0} already exist. daemon already running?"
message = 'pid_file {0} already exist. daemon already running?'
logger.error(str(message.format(self.pid_file)))
sys.exit(1)
@ -122,7 +122,7 @@ class Daemon:
pid = None
if not pid:
message = "pid_file {0} does not exist. daemon not running?"
message = 'pid_file {0} does not exist. daemon not running?'
logger.error(str(message.format(self.pid_file)))
return # not an error in a restart
@ -133,7 +133,7 @@ class Daemon:
time.sleep(0.1)
except OSError as err:
e = str(err.args)
if e.find("No such process") > 0:
if e.find('no such process') > 0:
if os.path.exists(self.pid_file):
os.remove(self.pid_file)
else:

72
uplink/database.py Normal file
View file

@ -0,0 +1,72 @@
# 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 calendar
import pymysql
import socket
import time
from logging import getLogger
from sqlalchemy import create_engine
logger = getLogger(__name__)
class Database:
def __init__(self, configuration):
self.configuration = configuration
self.date = None
self.time = None
self.status = None
def write_to_db(self, fc, ip, provider, status):
timestamp = calendar.timegm(time.gmtime())
local_time = time.localtime(timestamp)
self.date = time.strftime('%Y-%m-%d', local_time)
self.time = time.strftime('%H:%M:%S', local_time)
try:
con = pymysql.connect(host=self.configuration.get_database_host(),
user=self.configuration.get_database_user(),
password=self.configuration.get_database_password(),
database=self.configuration.get_database_name())
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(self.date) + '\",\"' + \
str(self.time) + '\",\"' + str(fc.connection_uptime) + '\",\"' + str(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(provider) + '\",\"' + str(status) + '\",\"' + \
socket.gethostname() + '\")'
with con.cursor() as cur:
cur.execute(sql)
con.commit()
con.close()
except Exception as err:
logger.error(str("database connection failed: {0}".format(err)))
return

29
uplink/model.py Normal file
View file

@ -0,0 +1,29 @@
# 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.
from logging import getLogger
logger = getLogger(__name__)
class Model:
def __init__(self, configuration):
self.configuration = configuration

47
uplink/server_bottle.py Normal file
View file

@ -0,0 +1,47 @@
# 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 configuration(self):
return template('<pre>{{config}}</pre>!', config=json.dumps(vars(self.configuration),
sort_keys=True, indent=4))
def run(self):
run(host=self.configuration.get_httpserver_host(),
port=self.configuration.get_httpserver_port())

83
uplink/server_cherrypy.py Normal file
View file

@ -0,0 +1,83 @@
# 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.
# TODO: Select a template engine for easy output creation. Mako preferred but need to
# take a look at Jinja.
import cherrypy
import json
from logging import getLogger
logger = getLogger(__name__)
class Server:
def __init__(self, configuration):
self.configuration = configuration
@cherrypy.expose
def index(self):
return "Hello world!"
@cherrypy.expose
def config(self):
return '<pre style="border:2px solid black;background:#1d2021;color:#f0751a;">' + \
json.dumps(vars(self.configuration), sort_keys=True, indent=4) + \
'</pre>'
@cherrypy.expose
def playground(self):
return "My Playground!"
def run(self):
conf = {
'/': {
# 'tools.sessions.on': True,
# 'tools.staticdir.root': os.path.abspath(os.getcwd())
'tools.response_headers.on': True,
'tools.response_headers.headers': [('Content-Type', 'text/plain')],
},
'/config': {
# 'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
'tools.response_headers.on': True,
'tools.response_headers.headers': [('Content-Type', 'text/html')],
},
'/playground': {
# 'tools.staticdir.on': True,
# 'tools.staticdir.dir': './public'
}
}
#cherrypy.config.update({
# 'global': {
# 'engine.autoreload.on': False
# }
#})
cherrypy.config.update({
'global': {
'server.socket_host': self.configuration.get_httpserver_host(),
'server.socket_port': self.configuration.get_httpserver_port(),
'environment': 'production'
}
})
cherrypy.tree.mount(root=None, config=conf)
cherrypy.quickstart(self, '/', conf)
#cherrypy.server.bus.exit(self)

46
uplink/server_pyramid.py Normal file
View file

@ -0,0 +1,46 @@
# 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(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()

41
uplink/speedtest.py Normal file
View file

@ -0,0 +1,41 @@
# 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 speedtest
from logging import getLogger
logger = getLogger(__name__)
class Speedtest:
def __init__(self, configuration):
self.configuration = configuration
self.download = None
self.upload = None
def run(self):
speed = speedtest.Speedtest()
self.download = speed.download()
self.upload = speed.upload()
def write_to_db(self):
return

View file

@ -1,7 +1,7 @@
# 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
# 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
@ -11,101 +11,86 @@
# 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
# 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.
# TODO: Switch all SQL stuff to SQLAlchemy using the uplink.database Database class
# TODO: Make the use of a database optional and only output to stdout and/or in a logfile (use rich
# for colorizing the std output (https://github.com/Textualize/rich)); Output to a CSV file should
# be optional too.
import calendar
import pymysql
import socket
import sys
import time
from uplink.daemon import Daemon
from fritzconnection.lib.fritzstatus import FritzStatus
from logging import getLogger
from threading import Thread
from uplink.daemon import Daemon
from uplink.database import Database
from uplink.model import Model
logger = getLogger(__name__)
class Uplink(Daemon):
def __init__(self, pid_file, config):
def __init__(self, pid_file, configuration):
super().__init__(pid_file)
self.config = config
self.configuration = configuration
self.date = None
self.time = None
self.status = None
def fetch_data(self, 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:
logger.error(str("uplink: Database connection failed: {0}".format(err)))
sys.exit(1)
def fetch_data(self, i):
timestamp = calendar.timegm(time.gmtime())
local_time = time.localtime(timestamp)
self.date = time.strftime("%Y-%m-%d", local_time)
self.time = time.strftime("%H:%M:%S", local_time)
self.date = time.strftime('%Y-%m-%d', local_time)
self.time = time.strftime('%H:%M:%S', local_time)
uplink = self.configuration.get_uplink(i)
try:
fc = FritzStatus(address=config["uplinks"][inc]["ip"],
password=config["uplinks"][inc]["password"])
fc = FritzStatus(address=uplink['ip'],
password=uplink['password'])
if fc.is_connected:
self.status = 'UP'
else:
self.status = 'DOWN'
logger.info(str(uplink['provider'] + ' ' + self.status + ' (ip: ' +
fc.external_ip + ')'))
model = Model(self.configuration)
database = Database(self.configuration)
database.write_to_db(fc, uplink['ip'], uplink['provider'], self.status)
except Exception as err:
logger.error(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(self.date) + '\",\"' + str(self.time) + '\",\"' + \
str(config["uplinks"][inc]["ip"]) + '\",\"' + "0" + '\",\"' + "0" + '\",\"' + \
str(config["uplinks"][inc]["provider"]) + '\",\"ERROR: ' + str(err) + '\",\"' + socket.gethostname() + \
'\")'
logger.debug(str(sql))
with con.cursor() as cur:
cur.execute(sql)
con.commit()
sys.exit(1)
if fc.is_connected:
self.status = "UP"
else:
self.status = "DOWN"
logger.info(str(config["uplinks"][inc]["provider"] + " " + self.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(self.date) + '\",\"' + \
str(self.time) + '\",\"' + 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"]) + '\",\"' + self.status + '\",\"' + \
socket.gethostname() + '\")'
logger.debug(str(sql))
with con.cursor() as cur:
cur.execute(sql)
con.commit()
con.close()
message = str('error when getting data from ' + uplink['ip'] + ': {0}')
logger.error(str(message.format(err)))
def run(self):
if self.configuration.get_env_var('_server'):
if self.configuration.get_httpserver_framework() == 'bottle':
from uplink.server_bottle import Server
s = Server(self.configuration)
st = Thread(target=s.run, daemon=True)
st.start()
elif self.configuration.get_httpserver_framework() == 'cherrypy':
from uplink.server_cherrypy import Server
s = Server(self.configuration)
st = Thread(target=s.run, daemon=True)
st.start()
elif self.configuration.get_httpserver_framework() == 'pyramid':
from uplink.server_pyramid import Server
s = Server(self.configuration)
st = Thread(target=s.run, daemon=True)
st.start()
while True:
for i in range(len(self.config["uplinks"])):
t = Thread(target=self.fetch_data, args=(self.config, i))
t.start()
time.sleep(self.config["interval"])
for i in range(len(self.configuration.get_uplinks())):
ut = Thread(target=self.fetch_data, daemon=True, args=(i,))
ut.start()
time.sleep(self.configuration.get_interval())

129
uplink/uplink_old.py Normal file
View file

@ -0,0 +1,129 @@
# 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.
# TODO: Switch all SQL stuff to SQLAlchemy using the uplink.database Database class
# TODO: Make the use of a database optional and only output to stdout and/or in a logfile (use rich
# for colorizing the std output (https://github.com/Textualize/rich))
import calendar
import pymysql
import socket
import sys
import time
from uplink.configuration import Configuration
from uplink.daemon import Daemon
from uplink.database import Database
from fritzconnection.lib.fritzstatus import FritzStatus
from logging import getLogger
from threading import Thread
logger = getLogger(__name__)
class Uplink(Daemon):
def __init__(self, pid_file, config, configuration):
super().__init__(pid_file)
#self.config = config
self.configuration = configuration
self.date = None
self.time = None
self.status = None
def get_time(self):
return self.time
def get_status(self):
return self.status
def fetch_data(self, config, uplink):
try:
con = pymysql.connect(host=self.configuration.get_database_host(),
user=self.configuration.get_database_user(),
password=self.configuration.get_database_password(),
database=self.configuration.get_database_name())
except Exception as err:
logger.error(str("database connection failed: {0}".format(err)))
timestamp = calendar.timegm(time.gmtime())
local_time = time.localtime(timestamp)
self.date = time.strftime("%Y-%m-%d", local_time)
self.time = time.strftime("%H:%M:%S", local_time)
try:
fc = FritzStatus(address=config["uplinks"][uplink]["ip"],
password=config["uplinks"][uplink]["password"])
except Exception as err:
logger.error(str(str(err) + " on device with ip: " + config["uplinks"][uplink]["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(self.date) + '\",\"' + str(self.time) + '\",\"' + \
str(config["uplinks"][uplink]["ip"]) + '\",\"' + "0" + '\",\"' + "0" + '\",\"' + \
str(config["uplinks"][uplink]["provider"]) + '\",\"ERROR: ' + str(err) + '\",\"' + socket.gethostname() + \
'\")'
logger.debug(str(sql))
with con.cursor() as cur:
cur.execute(sql)
con.commit()
sys.exit(1)
if fc.is_connected:
self.status = "UP"
else:
self.status = "DOWN"
logger.info(str(config["uplinks"][uplink]["provider"] + " " + self.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(self.date) + '\",\"' + \
str(self.time) + '\",\"' + str(fc.connection_uptime) + '\",\"' + str(config["uplinks"][uplink]["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"][uplink]["provider"]) + '\",\"' + self.status + '\",\"' + \
socket.gethostname() + '\")'
logger.debug(str(sql))
with con.cursor() as cur:
cur.execute(sql)
con.commit()
con.close()
def run(self):
if self.configuration.get_env_var("_server"):
if self.config["httpserver_framework"] == "bottle":
from uplink.server_bottle import Server
else:
from uplink.server_cherrypy import Server
s = Server(self.config)
st = Thread(target=s.run, daemon=True)
st.start()
while True:
for i in range(len(self.config["uplinks"])):
ut = Thread(target=self.fetch_data, daemon=True, args=(self.config, i))
ut.start()
time.sleep(self.configuration.get_interval())