From ed220154d888b8f67424d908f2a8852079aa8e74 Mon Sep 17 00:00:00 2001 From: askiiart Date: Tue, 30 Jan 2024 13:35:56 -0600 Subject: [PATCH] Add support for other extensions --- .gitignore | 4 +- README.md | 6 ++- services-example.json | 21 ++++++++++ timer.py | 54 ++++++++++++++++++++++++++ updog.py | 90 +++++++++++++++++++++++++++++++++++++------ 5 files changed, 161 insertions(+), 14 deletions(-) create mode 100644 services-example.json create mode 100644 timer.py diff --git a/.gitignore b/.gitignore index a3ebfc4..86a0441 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -extensions/* \ No newline at end of file +extensions/ +__pycache__/ +logs/ \ No newline at end of file diff --git a/README.md b/README.md index 8b32c4c..4435c0f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Not much, you? Docs are in the `./docs/` folder, but here's a quick overview on how to use this: -1. Clone this repository - or just download `updog.py` +1. Clone this repository - `git clone --depth 1 https://git.askiiart.net/askiiart/updog` 2. Install your extensions in the appropriate folders: `./extensions/checkers`, `./extensions/alerts` (optional), and `./extensions/logging` 3. Create your `services.json` file, example below. @@ -49,6 +49,10 @@ Docs are in the `./docs/` folder, but here's a quick overview on how to use this - Call the extensions - Add support for logging and alert extensions - Add maintenance windows (optionally recurring) +- Add ability to set default extensions that can be overridden on a per-service basis + - And groups (lower priority) +- Add ability for checkers to have preferred alerts and logging extensions - will print a warning if the preferred extension(s) are not used +- Add "keywords" to be replaced in the config (for current directory, service name, maybe some others) --- diff --git a/services-example.json b/services-example.json new file mode 100644 index 0000000..f06059a --- /dev/null +++ b/services-example.json @@ -0,0 +1,21 @@ +{ + "site": { + "name": "A Website", + "checker": "CheckerTemplate", + "checker-args": { + "url": "https://example.net", + "port": 443, + "lol": "CheckerTemplate ignores these options lol" + }, + "rate": 60, + "alerts": "AlertsTemplate", + "alerts-args": { + "url": "https://example.com/webhook-url-or-whatever-goes-here", + "lol": "irrelevant, AlertsTemplate ignores these options lol" + }, + "logging": "LoggingTemplate", + "logging-args": { + "file": "/home/askiiart/Documents/updog/logs/site-log" + } + } +} \ No newline at end of file diff --git a/timer.py b/timer.py new file mode 100644 index 0000000..09eaa56 --- /dev/null +++ b/timer.py @@ -0,0 +1,54 @@ +import threading +import time + +# Threaded repeating timer thing +# From https://stackoverflow.com/a/40965385/16432246 +# It shouldn't drift, but that's untested +# Keep in mind you can't catch Exceptions for this, it just crashes the thread. +# Some more scheduling stuff: https://www.redwood.com/article/python-job-scheduling/ +class RepeatedTimer(object): + ''' + Run stuff repeatedly every x seconds + Example usage (from SO and ported to Python 3): + from time import sleep + + def hello(name): + print "Hello %s!" % name + + print()"starting...") + rt = RepeatedTimer(1, hello, "World") # it auto-starts, no need of rt.start() + try: + sleep(5) # your long-running job goes here... + catch: + rt.stop() # better in a try/catch block to make sure the program ends! + ''' + + def __init__(self, interval, function, *args, **kwargs): + ''' + Run a functions with arguments every + ''' + self._timer = None + self.interval = interval + self.function = function + self.args = args + self.kwargs = kwargs + self.is_running = False + self.next_call = time.time() + self.start() + + def _run(self): + self.is_running = False + self.start() + self.function(*self.args, **self.kwargs) + + def start(self): + if not self.is_running: + self.next_call += self.interval + self._timer = threading.Timer( + self.next_call - time.time(), self._run) + self._timer.start() + self.is_running = True + + def stop(self): + self._timer.cancel() + self.is_running = False diff --git a/updog.py b/updog.py index 4d192d4..b526a8f 100644 --- a/updog.py +++ b/updog.py @@ -1,31 +1,97 @@ +from timer import RepeatedTimer import sys import os import importlib import inspect +import json -DEBUG = False - -# relative imports suck - https://gideonbrimleaf.github.io/2021/01/26/relative-imports-python.html path = os.path.realpath(__file__) path = path[:path.rfind('/')] + +##################### +# Import extensions # +##################### + +# Import checkers # +# relative imports suck - https://gideonbrimleaf.github.io/2021/01/26/relative-imports-python.html sys.path.insert(1, f'{path}/extensions/checkers') # importlib used to import stuff programmatically, rather than using the hardcoded import keyword, basically the same but it seems it can't import *just* a class -uptime_extension_imports = [] +checker_extension_imports = [] for ext_name in os.listdir(f'{path}/extensions/checkers'): if ext_name[0] != '.': - uptime_extension_imports.append(f'{ext_name}.{ext_name}') + checker_extension_imports.append(f'{ext_name}.{ext_name}') -# uptime_checkers contains all the classes for the checker extensions +# checkers contains all the classes for the checker extensions # e.g. {'CheckerTemplate': checker_template.checker_template.CheckerTemplate} -checkers = dict() -for ext in [importlib.import_module(ext) for ext in uptime_extension_imports]: +checkers = {} +for ext in [importlib.import_module(ext) for ext in checker_extension_imports]: for name, obj in inspect.getmembers(ext): if inspect.isclass(obj): checkers[name] = obj -if DEBUG: - print('uptime_extension_imports:', uptime_extension_imports) - print('checkers:', checkers) +# Import alerts # +# same as above, just for alerts +sys.path.insert(1, f'{path}/extensions/alerts') -print(checkers['CheckerTemplate'].get_status()) +alerts_extension_imports = [] +for ext_name in os.listdir(f'{path}/extensions/alerts'): + if ext_name[0] != '.': + alerts_extension_imports.append(f'{ext_name}.{ext_name}') + +# alerts contains all the classes for the checker extensions +# e.g. {'AlertsTemplate': alerts_template.alerts_template.AlertsTemplate} +alerts = {} +for ext in [importlib.import_module(ext) for ext in alerts_extension_imports]: + for name, obj in inspect.getmembers(ext): + if inspect.isclass(obj): + alerts[name] = obj + +# Import logging # +# same as above, just for logging +sys.path.insert(1, f'{path}/extensions/logging') + +logging_extension_imports = [] +for ext_name in os.listdir(f'{path}/extensions/logging'): + if ext_name[0] != '.': + logging_extension_imports.append(f'{ext_name}.{ext_name}') + +# logging contains all the classes for the checker extensions +# e.g. {'LoggingTemplate': logging_template.logging_template.LoggingTemplate} +logging = {} +for ext in [importlib.import_module(ext) for ext in logging_extension_imports]: + for name, obj in inspect.getmembers(ext): + if inspect.isclass(obj): + logging[name] = obj + +# get config from services.json +if 'services.json' in os.listdir(): + config_filename = 'services.json' +elif 'services-example.json' in os.listdir(): + config_filename = 'services-example.json' + +with open(config_filename, 'rt') as config_file: + config = json.loads(''.join(config_file.readlines())) + + +def create_instances(config): + ''' + Creates instances of all the extensions according to config + Parameters: + config: the dictionary containing the config + Returns: + instances (dict): A dictionary containing instances of the extensions + example: {'site': {'checker': instanceOfCheckerTemplate}} + ''' + instances = {} + for service in config.keys(): + instances[service] = {} + # just creates an instance of the checker with the arguments for it + instances[service]['checker'] = checkers[config[service] + ['checker']](config[service]['checker-args']) + instances[service]['alerts'] = alerts[config[service] + ['alerts']](config[service]['alerts-args']) + instances[service]['logging'] = logging[config[service] + ['logging']](config[service]['logging-args']) + + return instances