From a14fccfb71901b221a8c0fd5a61759481f2fd833 Mon Sep 17 00:00:00 2001 From: Johannes Findeisen Date: Sat, 27 Aug 2022 18:46:47 +0200 Subject: [PATCH] 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 --- LICENSE | 2 - bin/uplink | 186 +++++++++++++++++++++----------------- config.example.json | 13 --- etc/config.example.json | 24 +++++ files/devhelp.md | 11 +++ templates/.gitkeep | 0 uplink/configuration.py | 141 +++++++++++++++++++++++++++++ uplink/daemon.py | 10 +- uplink/database.py | 72 +++++++++++++++ uplink/model.py | 29 ++++++ uplink/server_bottle.py | 47 ++++++++++ uplink/server_cherrypy.py | 83 +++++++++++++++++ uplink/server_pyramid.py | 46 ++++++++++ uplink/speedtest.py | 41 +++++++++ uplink/uplink.py | 119 +++++++++++------------- uplink/uplink_old.py | 129 ++++++++++++++++++++++++++ 16 files changed, 783 insertions(+), 170 deletions(-) delete mode 100644 config.example.json create mode 100644 etc/config.example.json create mode 100644 files/devhelp.md create mode 100644 templates/.gitkeep create mode 100644 uplink/configuration.py create mode 100644 uplink/database.py create mode 100644 uplink/model.py create mode 100644 uplink/server_bottle.py create mode 100644 uplink/server_cherrypy.py create mode 100644 uplink/server_pyramid.py create mode 100644 uplink/speedtest.py create mode 100644 uplink/uplink_old.py diff --git a/LICENSE b/LICENSE index be2f3b0..69ddf28 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,5 @@ MIT License -Copyright (c) 2020 Johannes Findeisen - 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 diff --git a/bin/uplink b/bin/uplink index f87b47e..53bd041 100755 --- a/bin/uplink +++ b/bin/uplink @@ -3,7 +3,7 @@ # Copyright (c) 2020 Johannes Findeisen # # 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 ' 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() diff --git a/config.example.json b/config.example.json deleted file mode 100644 index 3cf3b89..0000000 --- a/config.example.json +++ /dev/null @@ -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" } - ] -} diff --git a/etc/config.example.json b/etc/config.example.json new file mode 100644 index 0000000..b5451b8 --- /dev/null +++ b/etc/config.example.json @@ -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" + } + ] +} diff --git a/files/devhelp.md b/files/devhelp.md new file mode 100644 index 0000000..39cc732 --- /dev/null +++ b/files/devhelp.md @@ -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/ \ No newline at end of file diff --git a/templates/.gitkeep b/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/uplink/configuration.py b/uplink/configuration.py new file mode 100644 index 0000000..518b4ed --- /dev/null +++ b/uplink/configuration.py @@ -0,0 +1,141 @@ +# Copyright (c) 2022 Johannes Findeisen +# +# 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 diff --git a/uplink/daemon.py b/uplink/daemon.py index 620c4f3..1f1f689 100644 --- a/uplink/daemon.py +++ b/uplink/daemon.py @@ -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: diff --git a/uplink/database.py b/uplink/database.py new file mode 100644 index 0000000..b7a4d96 --- /dev/null +++ b/uplink/database.py @@ -0,0 +1,72 @@ +# Copyright (c) 2022 Johannes Findeisen +# +# 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 diff --git a/uplink/model.py b/uplink/model.py new file mode 100644 index 0000000..2dc12c4 --- /dev/null +++ b/uplink/model.py @@ -0,0 +1,29 @@ +# Copyright (c) 2022 Johannes Findeisen +# +# 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 diff --git a/uplink/server_bottle.py b/uplink/server_bottle.py new file mode 100644 index 0000000..1c78618 --- /dev/null +++ b/uplink/server_bottle.py @@ -0,0 +1,47 @@ +# Copyright (c) 2022 Johannes Findeisen +# +# 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/') + def index(self, name): + return template('Hello {{name}}!', name=name) + + @route('/config') + def configuration(self): + return template('
{{config}}
!', 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()) diff --git a/uplink/server_cherrypy.py b/uplink/server_cherrypy.py new file mode 100644 index 0000000..6a6dd01 --- /dev/null +++ b/uplink/server_cherrypy.py @@ -0,0 +1,83 @@ +# Copyright (c) 2022 Johannes Findeisen +# +# 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 '
' + \
+               json.dumps(vars(self.configuration), sort_keys=True, indent=4) + \
+               '
' + + @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) diff --git a/uplink/server_pyramid.py b/uplink/server_pyramid.py new file mode 100644 index 0000000..dec714a --- /dev/null +++ b/uplink/server_pyramid.py @@ -0,0 +1,46 @@ +# Copyright (c) 2022 Johannes Findeisen +# +# 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() diff --git a/uplink/speedtest.py b/uplink/speedtest.py new file mode 100644 index 0000000..aaaceed --- /dev/null +++ b/uplink/speedtest.py @@ -0,0 +1,41 @@ +# Copyright (c) 2022 Johannes Findeisen +# +# 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 diff --git a/uplink/uplink.py b/uplink/uplink.py index 9649051..3b17512 100644 --- a/uplink/uplink.py +++ b/uplink/uplink.py @@ -1,7 +1,7 @@ # Copyright (c) 2021 Johannes Findeisen # # 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()) diff --git a/uplink/uplink_old.py b/uplink/uplink_old.py new file mode 100644 index 0000000..ef5d70c --- /dev/null +++ b/uplink/uplink_old.py @@ -0,0 +1,129 @@ +# Copyright (c) 2021 Johannes Findeisen +# +# 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())