Add defaults and config parser.

Resolves #4
Resolves #6
This commit is contained in:
Scott Wallace 2020-10-19 18:44:02 +01:00
parent 2d5f17bacb
commit c05d02a16f
4 changed files with 121 additions and 40 deletions

View file

@ -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)
```

View file

@ -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')

6
example.yaml Normal file
View file

@ -0,0 +1,6 @@
---
# verbose: true
gotify_server: gotifyserver.example.net
gotify_key: sOmEsEcReTkEy1
gotify_port: "80"
listen_port: "8080"

View file

@ -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