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_interval(interval_raw): if (type(interval_raw) == int): return interval_raw elif (type(interval_raw) == str): if (interval_raw == "minute"): return (60) elif (interval_raw == "hour"): return (60 * 60) elif (interval_raw == "day"): return (60 * 60 * 24) elif (interval_raw == "week"): return (60 * 60 * 24 * 7) else: raise ValueError("invalid string interval value: %s" % interval_raw) else: raise ValueError("invalid type for interval value") def conf_normalize_schedule(node): node_ = dict_merge( { "regular_interval": (60 * 60), "attentive_interval": (60 * 2), }, node ) return { "regular_interval": conf_normalize_interval(node["regular_interval"]), "attentive_interval": conf_normalize_interval(node["attentive_interval"]), } def conf_normalize_defaults(node): return dict_merge( { "active": True, "threshold": 3, "annoy": False, "schedule": { "regular_interval": (60 * 60), "attentive_interval": (60 * 2), }, "notifications": [ ], }, node ) def conf_normalize_check(check_kind_implementations, defaults, name, node): if ("kind" not in node): raise ValueError("missing mandatory 'check' 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": defaults["active"], "threshold": defaults["threshold"], "annoy": defaults["annoy"], "schedule": defaults["schedule"], "notifications": defaults["notifications"], "parameters": {}, }, node ) return { "title": node_["title"], "active": node_["active"], "threshold": node_["threshold"], "annoy": node_["annoy"], "schedule": conf_normalize_schedule(node_["schedule"]), "notifications": node_["notifications"], "kind": node_["kind"], "parameters": check_kind_implementations[node_["kind"]].normalize_conf_node(node_["parameters"]), } 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 = "Heimdall-Monitoring-Tool", formatter_class = _argparse.ArgumentDefaultsHelpFormatter ) argumentparser.add_argument( "-c", "--conf-path", type = str, default = "monitoring.hmdl.json", dest = "conf_path", metavar = "", help = "path to the configuration file" ) argumentparser.add_argument( "-s", "--state-path", type = str, default = None, dest = "state_path", metavar = "", 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( "-x", "--erase-state", action = "store_true", default = False, dest = "erase_state", help = "whether the state shall be deleted on start; this will cause that all checks are executed" ) argumentparser.add_argument( "-e", "--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() ## 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(), "file_timestamp": implementation_check_kind_file_timestamp(), "http_request": implementation_check_kind_http_request(), } ### load notification channel implementations notification_channel_implementations = { "console": implementation_notification_channel_console(), "file_touch": implementation_notification_channel_file_touch(), "email": implementation_notification_channel_email(), "libnotify": implementation_notification_channel_libnotify(), } ### 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(state_path)) or args.erase_state ): state_data = {} file_write(state_path, _json.dumps(state_data, indent = "\t")) else: state_data = _json.loads(file_read(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["condition"] != enum_condition.ok) or ((timestamp - old_item_state["timestamp"]) >= check_data["schedule"]["regular_interval"]) or ( (old_item_state["count"] is not None) and ((timestamp - old_item_state["timestamp"]) >= check_data["schedule"]["attentive_interval"]) ) ) 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["parameters"]) 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) <= check_data["threshold"]) ) else None ) ), } state_data[check_name] = state_encode(new_item_state) file_write(state_path, _json.dumps(state_data, indent = "\t")) ### send notifications if ( ( (new_item_state["count"] is not None) and (new_item_state["count"] == check_data["threshold"]) ) or ( (new_item_state["count"] is None) and check_data["annoy"] ) ): 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()