diff --git a/source/interface_check_kind.py b/source/check_kinds/_interface.py similarity index 100% rename from source/interface_check_kind.py rename to source/check_kinds/_interface.py diff --git a/source/implementation_check_kind_http_request.py b/source/check_kinds/http_request.py similarity index 100% rename from source/implementation_check_kind_http_request.py rename to source/check_kinds/http_request.py diff --git a/source/implementation_check_kind_script.py b/source/check_kinds/script.py similarity index 100% rename from source/implementation_check_kind_script.py rename to source/check_kinds/script.py diff --git a/source/heimdall.py b/source/heimdall.py deleted file mode 100755 index feb7219..0000000 --- a/source/heimdall.py +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env python3 - -import sys as _sys -import os as _os -import json as _json -import argparse as _argparse - -from lib import * -from implementation_check_kind_script import * -from implementation_check_kind_http_request import * -from implementation_notification_channel_console import * -from implementation_notification_channel_email import * - - -def state_encode(state): - return { - "timestamp": state["timestamp"], - "condition": condition_encode(state["condition"]), - "count": state["count"], - } - - -def state_decode(state_encoded): - return { - "timestamp": state_encoded["timestamp"], - "condition": condition_decode(state_encoded["condition"]), - "count": state_encoded["count"], - } - - -def conf_normalize_check(check_kind_implementations, defaults, name, node): - if ("kind" not in node): - raise ValueError("missing mandatory 'member' field 'kind'") - else: - if (node["kind"] not in check_kind_implementations): - raise ValueError("unhandled kind: %s" % node["kind"]) - else: - node_ = dict_merge( - { - "title": name, - "active": True, - "schedule": defaults["schedule"], - "notifications": defaults["notifications"], - "parameters": {}, - }, - node - ) - return { - "title": node_["title"], - "active": node_["active"], - "schedule": node_["schedule"], - "notifications": node_["notifications"], - "kind": node_["kind"], - "parameters": check_kind_implementations[node_["kind"]].normalize_conf_node(node_["parameters"]), - } - - -def conf_normalize_defaults(node): - return dict_merge( - { - "active": True, - "schedule": {"kind": "hourly"}, - "notifications": [], - }, - node - ) - - -def conf_normalize_root(check_kind_implementations, node): - return dict( - map( - lambda check_pair: ( - check_pair[0], - conf_normalize_check( - check_kind_implementations, - conf_normalize_defaults(node["defaults"]), - check_pair[0], - check_pair[1] - ), - ), - node["checks"].items() - ) - ) - - -def main(): - ## args - argumentparser = _argparse.ArgumentParser( - description = "monitoring processor", - formatter_class = _argparse.ArgumentDefaultsHelpFormatter - ) - argumentparser.add_argument( - "-c", - "--conf-path", - type = str, - default = "conf.json", - dest = "conf_path", - metavar = "", - help = "path to the configuration file" - ) - argumentparser.add_argument( - "-s", - "--state-path", - type = str, - default = "/tmp/monitoring-state.json", - dest = "state_path", - metavar = "", - help = "path to the state file, which contains information about the recent checks" - ) - argumentparser.add_argument( - "-t", - "--threshold", - type = int, - default = 3, - dest = "threshold", - metavar = "", - help = "how often a condition has to occur in order to be reported" - ) - argumentparser.add_argument( - "-k", - "--keep-notifying", - action = "store_true", - default = False, - dest = "keep_notifying", - help = "whether notifications shall be kept sending after the threshold has been surpassed" - ) - argumentparser.add_argument( - "-x", - "--expose-full-conf", - action = "store_true", - default = False, - dest = "expose_full_conf", - help = "only print the extended configuration to stdout and exit (useful for debug purposes)" - ) - args = argumentparser.parse_args() - - ## exec - - ### load check kind implementations - check_kind_implementations = { - "script": implementation_check_kind_script(), - "http_request": implementation_check_kind_http_request(), - } - - ### load notification channel implementations - notification_channel_implementations = { - "console": implementation_notification_channel_console(), - "email": implementation_notification_channel_email(), - } - - ### get configuration data - checks = conf_normalize_root(check_kind_implementations, _json.loads(file_read(args.conf_path))) - if (args.expose_full_conf): - _sys.stdout.write(_json.dumps(checks, indent = "\t") + "\n") - _sys.exit(1) - else: - ### get state data - if (not _os.path.exists(args.state_path)): - state_data = {} - file_write(args.state_path, _json.dumps(state_data, indent = "\t")) - else: - state_data = _json.loads(file_read(args.state_path)) - - ### iterate through checks - for (check_name, check_data, ) in checks.items(): - if (not check_data["active"]): - pass - else: - ### get old state and examine whether the check shall be executed - old_item_state = ( - None - if (check_name not in state_data) else - state_decode(state_data[check_name]) - ) - timestamp = get_current_timestamp() - due = ( - (old_item_state is None) - or - ( - (old_item_state["count"] is not None) - and - ((timestamp - old_item_state["timestamp"]) >= (1 * 5)) - ) - or - ( - ( - (check_data["schedule"]["kind"] == "minutely") - and - ((timestamp - old_item_state["timestamp"]) >= (60)) - ) - or - ( - (check_data["schedule"]["kind"] == "hourly") - and - ((timestamp - old_item_state["timestamp"]) >= (60 * 60)) - ) - or - ( - (check_data["schedule"]["kind"] == "daily") - and - ((timestamp - old_item_state["timestamp"]) >= (60 * 60 * 24)) - ) - ) - ) - if (not due): - pass - else: - _sys.stderr.write( - string_coin( - "-- {{check_name}}\n", - { - "check_name": check_name, - } - ) - ) - - ### execute check and set new state - result = check_kind_implementations[check_data["kind"]].run(check_data) - new_item_state = { - "timestamp": timestamp, - "condition": result["condition"], - "count": ( - 1 - if ( - (old_item_state is None) - or - (old_item_state["condition"] != result["condition"]) - ) else - ( - (old_item_state["count"] + 1) - if ( - (old_item_state["count"] is not None) - and - ((old_item_state["count"] + 1) <= args.threshold) - ) else - None - ) - ), - } - state_data[check_name] = state_encode(new_item_state) - file_write(args.state_path, _json.dumps(state_data, indent = "\t")) - - ### send notifications - if ( - ( - (new_item_state["count"] is not None) - and - (new_item_state["count"] == args.threshold) - ) - or - ( - (new_item_state["count"] is None) - and - args.keep_notifying - ) - ): - for notification in check_data["notifications"]: - if (notification["kind"] in notification_channel_implementations): - notification_channel_implementations[notification["kind"]].notify( - notification["parameters"], - check_name, - check_data, - new_item_state, - result["output"] - ) - else: - raise ValueError("invalid notification kind: %s" % notification["kind"]) - - -main() diff --git a/source/main.py b/source/main.py index a328672..afe0305 100644 --- a/source/main.py +++ b/source/main.py @@ -72,14 +72,14 @@ def conf_normalize_root(check_kind_implementations, node): def main(): ## args argumentparser = _argparse.ArgumentParser( - description = "monitoring processor", + description = "Heimdall-Monitoring-Tool", formatter_class = _argparse.ArgumentDefaultsHelpFormatter ) argumentparser.add_argument( "-c", "--conf-path", type = str, - default = "conf.json", + default = "monitoring.hmdl.json", dest = "conf_path", metavar = "", help = "path to the configuration file" @@ -88,10 +88,10 @@ def main(): "-s", "--state-path", type = str, - default = "/tmp/monitoring-state.json", + default = None, dest = "state_path", metavar = "", - help = "path to the state file, which contains information about the recent checks" + help = "path to the state file, which contains information about the recent checks; default: file in temporary directory, unique for the conf-path input" ) argumentparser.add_argument( "-t", @@ -102,6 +102,15 @@ def main(): metavar = "", help = "how often a condition has to occur in order to be reported" ) + argumentparser.add_argument( + "-a", + "--awareness-interval", + type = int, + default = 120, + dest = "awareness_interval", + metavar = "", + help = "seconds to wait until starting the next run of a check, for which the condition has changed recently" + ) argumentparser.add_argument( "-k", "--keep-notifying", @@ -120,8 +129,21 @@ def main(): ) args = argumentparser.parse_args() + ## vars + id_ = _hashlib.sha256(_os.path.abspath(args.conf_path).encode("ascii")).hexdigest()[:8] + state_path = ( + args.state_path + if (args.state_path is not None) else + _os.path.join( + _tempfile.gettempdir(), + string_coin("monitoring-state-{{id}}.json", {"id": id_}) + ) + ) + ## exec + _sys.stderr.write(">> state file path: %s\n" % state_path) + ### load check kind implementations check_kind_implementations = { "script": implementation_check_kind_script(), @@ -141,11 +163,11 @@ def main(): _sys.exit(1) else: ### get state data - if (not _os.path.exists(args.state_path)): + if (not _os.path.exists(state_path)): state_data = {} - file_write(args.state_path, _json.dumps(state_data, indent = "\t")) + file_write(state_path, _json.dumps(state_data, indent = "\t")) else: - state_data = _json.loads(file_read(args.state_path)) + state_data = _json.loads(file_read(state_path)) ### iterate through checks for (check_name, check_data, ) in checks.items(): @@ -165,7 +187,7 @@ def main(): ( (old_item_state["count"] is not None) and - ((timestamp - old_item_state["timestamp"]) >= (1 * 5)) + ((timestamp - old_item_state["timestamp"]) >= args.awareness_interval) ) or ( @@ -224,7 +246,7 @@ def main(): ), } state_data[check_name] = state_encode(new_item_state) - file_write(args.state_path, _json.dumps(state_data, indent = "\t")) + file_write(state_path, _json.dumps(state_data, indent = "\t")) ### send notifications if ( diff --git a/source/interface_notification_channel.py b/source/notification_channels/_interface.py similarity index 100% rename from source/interface_notification_channel.py rename to source/notification_channels/_interface.py diff --git a/source/implementation_notification_channel_console.py b/source/notification_channels/console.py similarity index 100% rename from source/implementation_notification_channel_console.py rename to source/notification_channels/console.py diff --git a/source/implementation_notification_channel_email.py b/source/notification_channels/email.py similarity index 100% rename from source/implementation_notification_channel_email.py rename to source/notification_channels/email.py diff --git a/source/packages.py b/source/packages.py index bb7c76e..a7c4038 100644 --- a/source/packages.py +++ b/source/packages.py @@ -1,9 +1,10 @@ import sys as _sys import os as _os import subprocess as _subprocess +import hashlib as _hashlib +import tempfile as _tempfile import argparse as _argparse import json as _json import requests as _requests import smtplib as _smtplib from email.mime.text import MIMEText - diff --git a/tools/build b/tools/build index e9695f5..7bc5afa 100755 --- a/tools/build +++ b/tools/build @@ -5,12 +5,12 @@ echo "#!/usr/bin/env python3" > build/heimdall cat \ source/packages.py \ source/lib.py \ - source/interface_check_kind.py \ - source/implementation_check_kind_script.py \ - source/implementation_check_kind_http_request.py \ - source/interface_notification_channel.py \ - source/implementation_notification_channel_console.py \ - source/implementation_notification_channel_email.py \ + source/check_kinds/_interface.py \ + source/check_kinds/script.py \ + source/check_kinds/http_request.py \ + source/notification_channels/_interface.py \ + source/notification_channels/console.py \ + source/notification_channels/email.py \ source/main.py \ >> build/heimdall chmod +x build/heimdall