From c05d02a16ffa8ca844a6f74160b24ab383dcd321 Mon Sep 17 00:00:00 2001 From: Scott Wallace Date: Mon, 19 Oct 2020 18:44:02 +0100 Subject: [PATCH] Add defaults and config parser. Resolves #4 Resolves #6 --- README.md | 18 ++++--- alertify.py | 136 +++++++++++++++++++++++++++++++++++------------ example.yaml | 6 +++ requirements.txt | 1 + 4 files changed, 121 insertions(+), 40 deletions(-) create mode 100644 example.yaml diff --git a/README.md b/README.md index 2102a40..0b11bf3 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,22 @@ This application bridges [Prometheus Alertmanager](https://prometheus.io/docs/al # Usage ``` -usage: alertify.py [-h] [-H] +usage: alertify.py [-h] [-c CONFIG] [-H] Bridge between Prometheus Alertmanager and Gotify optional arguments: - -h, --help show this help message and exit - -H, --healthcheck Simply exit with 0 for healthy or 1 when unhealthy + -h, --help show this help message and exit + -c CONFIG, --config CONFIG + path to config YAML. (default: alertify.yaml) + -H, --healthcheck simply exit with 0 for healthy or 1 when unhealthy -Three environment variables are required to be set: - * GOTIFY_SERVER: hostname of the Gotify server - * GOTIFY_PORT: port of the Gotify server - * GOTIFY_KEY: app token for alertify +The following environment variables will override any config or default: + * LISTEN_PORT (default: 8080) + * GOTIFY_SERVER (default: localhost) + * GOTIFY_PORT (default: 80) + * GOTIFY_KEY (default: None) + * VERBOSE (default: False) ``` diff --git a/alertify.py b/alertify.py index a26bb83..7e46394 100644 --- a/alertify.py +++ b/alertify.py @@ -5,14 +5,23 @@ Module to act as a Prometheus Exporter for Docker containers with a """ import argparse +import functools import http.client import json import os import sys +from distutils.util import strtobool from http.server import HTTPServer, SimpleHTTPRequestHandler -LISTEN_PORT = 8080 -VERBOSE = int(os.environ.get('ALERTIFY_VERBOSE', 0)) +import yaml + +DEFAULTS = { + 'listen_port': int(8080), + 'gotify_server': str('localhost'), + 'gotify_port': int(80), + 'gotify_key': str(), + 'verbose': bool(False), +} class HTTPHandler(SimpleHTTPRequestHandler): @@ -21,6 +30,15 @@ class HTTPHandler(SimpleHTTPRequestHandler): for Docker containers with a healthcheck configured """ + config = None + + @staticmethod + def set_config(config): + """ + Method + """ + HTTPHandler.config = config + # Override built-in method # pylint: disable=invalid-name def do_GET(self): @@ -28,7 +46,7 @@ class HTTPHandler(SimpleHTTPRequestHandler): Method to handle GET requests """ if self.path == '/healthcheck': - if not healthy(): + if not healthy(self.config): print('ERROR: Check requirements') self._respond(500, 'ERR') @@ -44,6 +62,9 @@ class HTTPHandler(SimpleHTTPRequestHandler): self._alerts() def _respond(self, status, message): + """ + Method to output a simple HTTP status and string to the client + """ self.send_response(int(status) or 500) self.end_headers() self.wfile.write(bytes(str(message).encode())) @@ -52,7 +73,7 @@ class HTTPHandler(SimpleHTTPRequestHandler): """ Method to handle the request for alerts """ - if not healthy(): + if not healthy(self.config): print('ERROR: Check requirements') self._respond(500, 'Server not configured correctly') return @@ -67,7 +88,7 @@ class HTTPHandler(SimpleHTTPRequestHandler): self._respond(400, f'Bad JSON: {error}') return - if VERBOSE: + if self.config.get('verbose'): print('Received from Alertmanager:') print(json.dumps(alert, indent=2)) @@ -94,21 +115,20 @@ class HTTPHandler(SimpleHTTPRequestHandler): self._respond(400, f'Missing field: {error}') return - if VERBOSE: + if self.config.get('verbose'): print('Sending to Gotify:') print(json.dumps(gotify_msg, indent=2)) - response = 'Status: {status}, Reason: {reason}'.format( - **gotify_send( - os.environ['GOTIFY_SERVER'], - os.environ['GOTIFY_PORT'], - os.environ['GOTIFY_KEY'], - gotify_msg - ) + + response = gotify_send( + self.config.get('gotify_server'), + self.config.get('gotify_port'), + self.config.get('gotify_key'), + gotify_msg ) - if VERBOSE: - print(response) - self._respond(200, response) + if self.config.get('verbose'): + print('Status: {status}, Reason: {reason}'.format(**response)) + self._respond(response['status'], response['reason']) def gotify_send(server, port, authkey, payload): @@ -122,8 +142,15 @@ def gotify_send(server, port, authkey, payload): 'Content-type': 'application/json', } - gotify.request('POST', '/message', json.dumps(payload), headers) - response = gotify.getresponse() + try: + gotify.request('POST', '/message', json.dumps(payload), headers) + response = gotify.getresponse() + except ConnectionRefusedError as error: + print(f'ERROR: {error}') + return { + 'status': error.errno, + 'reason': error.strerror + } return { 'status': response.status, @@ -131,35 +158,73 @@ def gotify_send(server, port, authkey, payload): } -def healthy(): +def healthy(config): """ - Simple funtion to return if all the requirements are met + Simple function to return if all the requirements are met """ return all([ - 'GOTIFY_SERVER' in os.environ, - 'GOTIFY_PORT' in os.environ, - 'GOTIFY_KEY' in os.environ, + len(config.get('gotify_key', '')) ]) +@functools.lru_cache +def parse_config(configfile): + """ + Function to parse a configuration file + """ + config = {} + + try: + with open(configfile, 'r') as file: + parsed = yaml.safe_load(file.read()) + except FileNotFoundError as error: + print(f'WARN: {error}') + parsed = {} + + # Iterate over the DEFAULTS dictionary and check for environment variables + # of the same name, then check for any items in the YAML config, otherwise + # use the default values. + # Ensure the types are adhered to. + for key, val in DEFAULTS.items(): + config[key] = os.environ.get(key.upper(), parsed.get(key, val)) + if isinstance(val, bool): + config[key] = strtobool(str(config[key])) + else: + config[key] = type(val)(config[key]) + + if config['verbose']: + print(f'Config:\n{yaml.dump(config, explicit_start=True, default_flow_style=False)}') + return config + + if __name__ == '__main__': - def cli_parse(): + def parse_cli(): """ Function to parse the CLI """ + maxlen = max([len(key) for key in DEFAULTS]) + defaults = [ + f' * {key.upper().ljust(maxlen)} (default: {val if val != "" else "None"})' + for key, val in DEFAULTS.items() + ] + parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description='Bridge between Prometheus Alertmanager and Gotify\n', - epilog='Three environment variables are required to be set:\n' - ' * GOTIFY_SERVER: hostname of the Gotify server\n' - ' * GOTIFY_PORT: port of the Gotify server\n' - ' * GOTIFY_KEY: app token for alertify' + epilog='The following environment variables will override any config or default:\n' + + '\n'.join(defaults) + ) + + parser.add_argument( + '-c', '--config', + default=f'{os.path.splitext(__file__)[0]}.yaml', + help=f'path to config YAML. (default: {os.path.splitext(__file__)[0]}.yaml)', ) parser.add_argument( '-H', '--healthcheck', action='store_true', - help='Simply exit with 0 for healthy or 1 when unhealthy', + help='simply exit with 0 for healthy or 1 when unhealthy', ) return parser.parse_args() @@ -168,15 +233,20 @@ if __name__ == '__main__': """ main() """ - args = cli_parse() + args = parse_cli() + config = parse_config(args.config) if args.healthcheck: # Invert the sense of 'healthy' for Unix CLI usage - return not healthy() + return not healthy(config) - print(f'Starting web server on port {LISTEN_PORT}') + listen_port = config.get('listen_port') + + print(f'Starting web server on port {listen_port}') try: - HTTPServer(('', LISTEN_PORT), HTTPHandler).serve_forever() + with HTTPServer(('', listen_port), HTTPHandler) as webserver: + HTTPHandler.set_config(config) + webserver.serve_forever() except KeyboardInterrupt: print('Exiting') diff --git a/example.yaml b/example.yaml new file mode 100644 index 0000000..a283a42 --- /dev/null +++ b/example.yaml @@ -0,0 +1,6 @@ +--- +# verbose: true +gotify_server: gotifyserver.example.net +gotify_key: sOmEsEcReTkEy1 +gotify_port: "80" +listen_port: "8080" diff --git a/requirements.txt b/requirements.txt index 749d0fd..b06eea0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ # To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file +pyyaml