diff --git a/examples/test.hmdl.json b/examples/test.hmdl.json index e7183eb..d3e2051 100644 --- a/examples/test.hmdl.json +++ b/examples/test.hmdl.json @@ -4,8 +4,8 @@ "checks": [ { "name": "test", - "threshold": 1, - "annoy": true, + "threshold": 3, + "annoy": false, "schedule": { "regular_interval": 15, "attentive_interval": 5 @@ -22,11 +22,12 @@ } } ], - "kind": "file_timestamp", + "kind": "file_state", "parameters": { "path": "/tmp/test", - "warning_age": 60, - "critical_age": 120 + "exist": true, + "age_threshold": 60, + "size_threshold": 1 } } ] diff --git a/source/localization/de.json b/source/localization/de.json new file mode 100644 index 0000000..3dd8db2 --- /dev/null +++ b/source/localization/de.json @@ -0,0 +1,25 @@ +{ + "conditions.unknown": "unbekannt", + "conditions.ok": "in Ordnung", + "conditions.warning": "bedenklich", + "conditions.critical": "kritisch", + "help.title": "Heimdall — Werkzeug zur System-Überwachungs", + "help.args.conf_path": "Pfad zur Konfigurations-Datei", + "help.args.state_path": "Pfad zur Zustands-Datei, welche Daten über vorherige Prüfungen enthält; Standard-Wert: Pfad im temporären Verzeichnis des Systems mit eindeutigem Namen in Bezug auf den Pfad zur Konfigurations-Datei", + "help.args.send_ok_notifications": "ob '{{condition_name}}'-Zustände gemeldet werden sollen", + "help.args.language": "welche Sprache verwendet werden soll (statt der in den Umgebungs-Variablen gesetzten)", + "help.args.erase_state": "ob der Zustand bei Start gelöscht werden soll; das hat zur Folge, dass alle Prüfungen unmittelbar durchgeführt werden", + "help.args.show_schema": "nur das hmdl-JSON-Schema zur Standard-Ausgabe schreiben und beenden", + "help.args.expose_full_conf": "nur die erweiterte Konfiguration zur Standard-Ausgabe schreiben und beenden (nützlich für Fehlersuche)", + "checks.file_state.exists": "Datei existiert (soll aber nicht)", + "checks.file_state.missing": "Datei existiert nicht (soll aber)", + "checks.file_state.timestamp_implausible": "Datei ist scheinbar aus der Zukunft", + "checks.file_state.too_old": "Datei ist zu alt", + "checks.file_state.too_big": "Datei ist zu groß", + "checks.http_request.request_failed": "HTTP-Abfrage fehlgeschlagen", + "checks.http_request.status_code_mismatch": "Status-Code {{status_code_actual}} stimmt nicht mit dem erwarteten Wert {{status_code_expected}} überein", + "checks.http_request.header_value_mismatch": "Header-Wert für Schlüssel '{{key}}' '{{value_actual}}' stimmt nicht mit erwartetem Wert {{value_expected}} überein", + "checks.http_request.body_misses_part": "Rumpf enthält nicht den erwarteten Teil '{{part}}'", + "misc.state_file_path": "Pfad zur Zustands-Datei", + "misc.check_procedure_failed": "Prüfungs-Prozedur fehlgeschlagen" +} diff --git a/source/localization/en.json b/source/localization/en.json new file mode 100644 index 0000000..676273d --- /dev/null +++ b/source/localization/en.json @@ -0,0 +1,25 @@ +{ + "conditions.unknown": "unknown", + "conditions.ok": "ok", + "conditions.warning": "concerning", + "conditions.critical": "critical", + "help.title": "Heimdall — Monitoring Tool", + "help.args.conf_path": "path to the configuration file", + "help.args.state_path": "path to the state file, which contains information about the recent checks; default: file in temporary directory, unique for the conf-path input", + "help.args.send_ok_notifications": "whether an '{{condition_name}}' condition shall be reported", + "help.args.language": "language to use (instead of the language, set in the environment variables)", + "help.args.erase_state": "whether the state shall be deleted on start; this will cause that all checks are executed immediatly", + "help.args.show_schema": "print the hmdl JSON schema to stdout and exit", + "help.args.expose_full_conf": "only print the extended configuration to stdout and exit (useful for debugging)", + "checks.file_state.exists": "file exists (but shall not)", + "checks.file_state.missing": "file does not exist (but shall)", + "checks.file_state.timestamp_implausible": "file is apparently from the future", + "checks.file_state.too_old": "file is too old", + "checks.file_state.too_big": "file is too big", + "checks.http_request.request_failed": "HTTP request failed", + "checks.http_request.status_code_mismatch": "actual status code {{status_code_actual}} does not match expected value {{status_code_expected}}", + "checks.http_request.header_value_mismatch": "actual header value for key '{{key}}' '{{value_actual}}' and does not match the expected value {{value_expected}}", + "checks.http_request.body_misses_part": "body does not contain the expected part '{{part}}'", + "misc.state_file_path": "state file path", + "misc.check_procedure_failed": "check procedure failed" +} diff --git a/source/notification_channels/_interface.py b/source/logic/channels/_interface.py similarity index 100% rename from source/notification_channels/_interface.py rename to source/logic/channels/_interface.py diff --git a/source/notification_channels/console.py b/source/logic/channels/console.py similarity index 82% rename from source/notification_channels/console.py rename to source/logic/channels/console.py index d69cd3b..1217b73 100644 --- a/source/notification_channels/console.py +++ b/source/logic/channels/console.py @@ -34,8 +34,8 @@ class implementation_notification_channel_console(interface_notification_channel "[{{title}}] <{{condition}}> {{info}}\n", { "title": data["title"], - "condition": condition_encode(state["condition"]), - "info": ("(no info)" if (info is None) else info), + "condition": condition_show(state["condition"]), + "info": _json.dumps(info, indent = "\t", ensure_ascii = False), } ) ) diff --git a/source/notification_channels/email.py b/source/logic/channels/email.py similarity index 93% rename from source/notification_channels/email.py rename to source/logic/channels/email.py index d357bfc..76eb212 100644 --- a/source/notification_channels/email.py +++ b/source/logic/channels/email.py @@ -81,11 +81,7 @@ class implementation_notification_channel_email(interface_notification_channel): parameters["access"]["password"] ) message = MIMEText( - string_coin( - ("(no info)" if (info is None) else info), - { - } - ) + _json.dumps(info, indent = "\t", ensure_ascii = False) ) message["Subject"] = string_coin( "{{tags}} {{title}}", @@ -96,7 +92,7 @@ class implementation_notification_channel_email(interface_notification_channel): ( parameters["tags"] + - [condition_encode(state["condition"])] + [condition_show(state["condition"])] ) ) ), diff --git a/source/notification_channels/libnotify.py b/source/logic/channels/libnotify.py similarity index 73% rename from source/notification_channels/libnotify.py rename to source/logic/channels/libnotify.py index af4c9c7..4b3cb58 100644 --- a/source/notification_channels/libnotify.py +++ b/source/logic/channels/libnotify.py @@ -34,16 +34,12 @@ class implementation_notification_channel_libnotify(interface_notification_chann ''' def notify(self, parameters, name, data, state, info): def condition_translate(condition): - if (condition == enum_condition.unknown): - return "normal" - elif (condition == enum_condition.ok): - return "low" - elif (condition == enum_condition.warning): - return "normal" - elif (condition == enum_condition.critical): - return "critical" - else: - raise ValueError("impossible condition") + return { + enum_condition.unknown: "normal", + enum_condition.ok: "low", + enum_condition.warning: "normal", + enum_condition.critical: "critical", + }[condition] parts = [] parts.append( "notify-send" @@ -84,15 +80,11 @@ class implementation_notification_channel_libnotify(interface_notification_chann "{{condition}} | {{title}}", { "title": data["title"], - "condition": condition_encode(state["condition"]).upper(), + "condition": condition_show(state["condition"]).upper(), } ) ) ## body - parts.append( - "(no info)" - if (info == "") else - info - ) + parts.append(_json.dumps(info, ensure_ascii = False)) _subprocess.run(parts) diff --git a/source/check_kinds/_interface.py b/source/logic/checks/_interface.py similarity index 79% rename from source/check_kinds/_interface.py rename to source/logic/checks/_interface.py index 6dd0f79..c8b3b9e 100644 --- a/source/check_kinds/_interface.py +++ b/source/logic/checks/_interface.py @@ -8,6 +8,9 @@ class interface_check_kind(object): raise NotImplementedError + ''' + return record + ''' def run(self, parameters): raise NotImplementedError diff --git a/source/logic/checks/file_state.py b/source/logic/checks/file_state.py new file mode 100644 index 0000000..d93990a --- /dev/null +++ b/source/logic/checks/file_state.py @@ -0,0 +1,174 @@ +class implementation_check_kind_file_state(interface_check_kind): + + ''' + [implementation] + ''' + def parameters_schema(self): + ''' + - exists (shall, must, must not) + - size (max) + - age (max) + - contains + ''' + return { + "type": "object", + "additionalProperties": False, + "properties": { + "path": { + "type": "string" + }, + "strict": { + "description": "whether a violation of this check shall be leveled as critical instead of concerning", + "type": "boolean", + "default": True + }, + "exist": { + "description": "whether the file is supposed to exist or not", + "type": "boolean", + "default": True + }, + "age_threshold": { + "description": "in seconds; ignored if 'exist' is set to false", + "type": ["null", "integer"], + "exclusiveMinimum": 0, + "default": None, + }, + "size_threshold": { + "description": "in bytes; ignored if 'exist' is set to false", + "type": "integer", + "exclusiveMinimum": 0, + "default": None, + }, + }, + "required": [ + "path" + ] + } + + + ''' + [implementation] + ''' + def normalize_conf_node(self, node): + if ("path" not in node): + raise ValueError("missing mandatory field 'path'") + else: + return dict_merge( + { + "strict": True, + "exist": True, + "age_threshold": None, + "size_threshold": None, + }, + node + ) + + + ''' + [implementation] + ''' + def run(self, parameters): + exists = _os.path.exists(parameters["path"]) + if (parameters["exist"]): + if (parameters["exist"] and not exists): + return { + "condition": ( + enum_condition.critical + if parameters["strict"] else + enum_condition.warning + ), + "info": { + "path": parameters["path"], + "faults": [ + translation_get("checks.file_state.missing"), + ], + "data": { + }, + } + } + else: + faults = [] + data = {} + stat = _os.stat(parameters["path"]) + ## age + if True: + if (parameters["age_threshold"] is None): + pass + else: + timestamp_this = get_current_timestamp() + timestamp_that = int(stat.st_atime) + age = (timestamp_this - timestamp_that) + if (age >= 0): + pass + else: + faults.append(translation_get("checks.file_state.timestamp_implausible")) + if (age <= parameters["age_threshold"]): + pass + else: + faults.append(translation_get("checks.file_state.too_old")) + data = dict_merge( + data, + { + "timestamp_of_checking_instance": timestamp_this, + "timestamp_of_file": timestamp_that, + "age_value_in_seconds": age, + "age_threshold_in_seconds": parameters["age_threshold"], + } + ) + ## size + if True: + if (parameters["size_threshold"] is None): + pass + else: + size = stat.st_size + if (size <= parameters["size_threshold"]): + pass + else: + faults.append(translation_get("checks.file_state.too_big")) + data = dict_merge( + data, + { + "size_value_in_bytes": size, + "size_threshold_in_bytes": parameters["size_threshold_in_bytes"], + } + ) + return { + "condition": ( + enum_condition.ok + if (len(faults) == 0) else + ( + enum_condition.critical + if parameters["strict"] else + enum_condition.warning + ) + ), + "info": { + "path": parameters["path"], + "faults": faults, + "data": data, + } + } + else: + if (not exists): + return { + "condition": ( + enum_condition.critical + if parameters["strict"] else + enum_condition.warning + ), + "info": { + "path": parameters["path"], + "faults": [ + translation_get("checks.file_state.exists") + ], + "data": { + }, + } + } + else: + return { + "condition": enum_condition.ok, + "info": { + } + } + diff --git a/source/check_kinds/file_timestamp.py b/source/logic/checks/file_timestamp.py similarity index 81% rename from source/check_kinds/file_timestamp.py rename to source/logic/checks/file_timestamp.py index ce046dd..8eb4a8b 100644 --- a/source/check_kinds/file_timestamp.py +++ b/source/logic/checks/file_timestamp.py @@ -75,7 +75,10 @@ class implementation_check_kind_file_timestamp(interface_check_kind): if (not _os.path.exists(parameters["path"])): return { "condition": condition_decode(parameters["condition_on_missing"]), - "info": "file is missing" + "info": { + "path": parameters["path"], + "flaw": translation_get("checks.file_timetsamp.missing"), + } } else: result = _os.stat(parameters["path"]) @@ -84,14 +87,12 @@ class implementation_check_kind_file_timestamp(interface_check_kind): if (age < 0): return { "condition": condition_decode(parameters["condition_on_implausible"]), - "info": string_coin( - "file is apparently from the future; timestamp of checking instance: {{timestamp_this}}; timestamp of file: {{timestamp_that}} (age in seconds: {{age}})", - { - "timestamp_this": timestamp, - "timestamp_that": result.st_atime, - "age": ("%u" % age), - } - ), + "info": { + "path": parameters["path"], + "flaw": translation_get("checks.file_timetsamp.implausible"), + "timestamp_of_checking_instance": timestamp, + "timestamp_of_file": result.st_atime, + }, } else: if ((age > 0) and (age <= parameters["warning_age"])): @@ -101,14 +102,13 @@ class implementation_check_kind_file_timestamp(interface_check_kind): elif (age > parameters["critical_age"]): condition = enum_condition.critical else: - raise ValueError("impossible state") + raise ValueError("impossible") return { "condition": condition, - "info": string_coin( - "age in seconds: {{age}}", - { - "age": ("%u" % age), - } - ), + "info": { + "path": parameters["path"], + "flaw": translation_get("checks.file_timetsamp.too_old"), + "age_in_seconds": ("%u" % age), + }, } diff --git a/source/check_kinds/http_request.py b/source/logic/checks/http_request.py similarity index 52% rename from source/check_kinds/http_request.py rename to source/logic/checks/http_request.py index 3c8d80e..3c117d6 100644 --- a/source/check_kinds/http_request.py +++ b/source/logic/checks/http_request.py @@ -59,11 +59,11 @@ class implementation_check_kind_http_request(interface_check_kind): "required": [ ] }, - "as_warning": { - "description": "whether a violation of this check shall be exposed as warning instead of critical; default: false", + "strict": { + "description": "whether a violation of this check shall be leveled as critical instead of concerning", "type": "boolean", - "default": False - } + "default": True + }, }, "required": [ "request" @@ -75,7 +75,7 @@ class implementation_check_kind_http_request(interface_check_kind): [implementation] ''' def normalize_conf_node(self, node): - return dict_merge( + node_ = dict_merge( { "request": { "method": "GET" @@ -83,11 +83,16 @@ class implementation_check_kind_http_request(interface_check_kind): "response": { "status_code": 200 }, - "as_warning": False, + "strict": True, }, node, True ) + allowed_methods = set(["GET", "POST"]) + if (node_["request"]["method"] not in allowed_methods): + raise ValueError("invalid HTTP request method: %s" % node_["request"]["method"]) + else: + return node_ ''' @@ -95,7 +100,6 @@ class implementation_check_kind_http_request(interface_check_kind): ''' def run(self, parameters): if (parameters["request"]["method"] == "GET"): - method_handled = True try: response = _requests.get( parameters["request"]["target"] @@ -105,7 +109,6 @@ class implementation_check_kind_http_request(interface_check_kind): error = error_ response = None elif (parameters["request"]["method"] == "POST"): - method_handled = True try: response = _requests.post( parameters["request"]["target"] @@ -115,78 +118,84 @@ class implementation_check_kind_http_request(interface_check_kind): error = error_ response = None else: - method_handled = False - response = None - if (not method_handled): + raise ValueError("impossible") + faults = [] + if (response is None): return { - "condition": enum_condition.unknown, - "info": ("invalid HTTP request method: %s" % parameters["request"]["method"]) + "condition": ( + enum_condition.critical + if parameters["strict"] else + enum_condition.warning + ), + "info": { + "request": parameters["request"], + "faults": [ + faults.append(translation_get("checks.http_request.request_failed")), + ], + } } else: - if (response is None): - return { - "condition": ( - enum_condition.warning - if parameters["as_warning"] else - enum_condition.critical - ), - "info": "HTTP request failed", - } - else: - lines = [] - for (key, value, ) in parameters["response"].items(): - if (key == "status_code"): - if ((value is None) or (response.status_code == value)): - pass - else: - lines.append( - string_coin( - "actual status code {{status_code_actual}} does not match expected value {{status_code_expected}}", - { - "status_code_actual": ("%u" % response.status_code), - "status_code_expected": ("%u" % value), - } - ) - ) - elif (key == "headers"): - for (header_key, header_value, ) in value.items(): - if (response.headers[header_key] == header_value): - pass - else: - lines.append( - string_coin( - "actual header value for key {{key}} is {{value_actual}} and does not match the expected value {{value_expected}}", - { - "key": header_key, - "value_actual": response.headers[header_key], - "value_expected": header_value, - } - ) - ) - elif (key == "body_part"): - if (response.text.find(value) >= 0): - pass - else: - lines.append( - string_coin( - "body does not contain the expected part '{{part}}'", - { - "part": value, - } - ) - ) + for (key, value, ) in parameters["response"].items(): + if (key == "status_code"): + if ((value is None) or (response.status_code == value)): + pass else: - raise ValueError("unhandled ") - return { - "condition": ( - enum_condition.ok - if (len(lines) <= 0) else - ( - enum_condition.warning - if parameters["as_warning"] else - enum_condition.critical + faults.append( + translation_get( + "checks.http_request.status_code_mismatch", + { + "status_code_actual": ("%u" % response.status_code), + "status_code_expected": ("%u" % value), + } + ) ) - ), - "info": "\n".join(lines), + elif (key == "headers"): + for (header_key, header_value, ) in value.items(): + if (response.headers[header_key] == header_value): + pass + else: + faults.append( + translation_get( + "checks.http_request.header_value_mismatch", + { + "key": header_key, + "value_actual": response.headers[header_key], + "value_expected": header_value, + } + ) + ) + elif (key == "body_part"): + if (response.text.find(value) >= 0): + pass + else: + faults.append( + translation_get( + "checks.http_request.body_misses_part", + { + "part": value, + } + ) + ) + else: + raise ValueError("unhandled") + return { + "condition": ( + enum_condition.ok + if (len(faults) <= 0) else + ( + enum_condition.critical + if parameters["strict"] else + enum_condition.warning + ) + ), + "info": { + "request": parameters["request"], + "response": { + "status_code": response.status_code, + # "headers": dict(map(lambda pair: pair, response.headers.items())), + # "body": response.text, + }, + "faults": faults, } + } diff --git a/source/check_kinds/script.py b/source/logic/checks/script.py similarity index 83% rename from source/check_kinds/script.py rename to source/logic/checks/script.py index 28354e9..3acc4c1 100644 --- a/source/check_kinds/script.py +++ b/source/logic/checks/script.py @@ -52,9 +52,13 @@ class implementation_check_kind_script(interface_check_kind): elif (result.returncode == 3): condition = enum_condition.critical else: - raise ValueError("invalid exit code: %i" % result.returncode) + # raise ValueError("invalid exit code: %i" % result.returncode) + condition = enum_condition.unknown return { "condition": condition, - "info": result.stdout.decode(), + "info": { + "stdout": result.stdout.decode(), + "stderr": result.stderr.decode(), + }, } diff --git a/source/logic/condition.py b/source/logic/condition.py new file mode 100644 index 0000000..8eb1ae8 --- /dev/null +++ b/source/logic/condition.py @@ -0,0 +1,38 @@ +class enum_condition(_enum.Enum): + unknown = 0 + ok = 1 + warning = 2 + critical = 3 + + +''' +converts a condition to a human readable string +''' +def condition_show(condition): + return translation_get( + { + enum_condition.unknown: "conditions.unknown", + enum_condition.ok: "conditions.ok", + enum_condition.warning: "conditions.warning", + enum_condition.critical: "conditions.critical", + }[condition] + ) + + +def condition_encode(condition): + return { + enum_condition.unknown: "unknown", + enum_condition.ok: "ok", + enum_condition.warning: "warning", + enum_condition.critical: "critical", + }[condition] + + +def condition_decode(condition_encoded): + return { + "unknown": enum_condition.unknown, + "ok": enum_condition.ok, + "warning": enum_condition.warning, + "critical": enum_condition.critical, + }[condition_encoded] + diff --git a/source/logic/conf.py b/source/logic/conf.py new file mode 100644 index 0000000..942a69a --- /dev/null +++ b/source/logic/conf.py @@ -0,0 +1,317 @@ +def conf_schema_active(): + return { + "type": "boolean", + "default": True + } + + +def conf_schema_threshold(): + return { + "description": "how often a condition has to occur in order to be reported", + "type": "integer", + "minimum": 1, + "default": 3 + } + + +def conf_schema_annoy(): + return { + "description": "whether notifications shall be kept sending after the threshold has been surpassed", + "type": "boolean", + "default": False + } + + +def conf_schema_interval(default): + return { + "description": "in seconds or as text", + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0 + }, + { + "type": "string", + "enum": [ + "minute", + "hour", + "day", + "week", + ] + }, + ], + "default": default, + } + + +def conf_schema_schedule(): + return { + "type": "object", + "additionalProperties": False, + "properties": { + "regular_interval": conf_schema_interval(60 * 60), + "attentive_interval": conf_schema_interval(60 * 2), + }, + "required": [ + ] + } + + +def conf_schema_notifications(notification_channel_implementations): + return { + "type": "array", + "items": { + "anyOf": list( + map( + lambda pair: { + "title": ("check kind '%s'" % pair[0]), + "type": "object", + "unevaluatedProperties": False, + "properties": { + "kind": { + "type": "string", + "enum": [pair[0]] + }, + "parameters": pair[1].parameters_schema(), + }, + "required": [ + "kind", + "parameters" + ] + }, + notification_channel_implementations.items() + ) + ) + }, + "default": [ + { + "kind": "console", + "parameters": { + } + } + ] + } + + +def conf_schema_root(check_kind_implementations, notification_channel_implementations): + return { + "type": "object", + "additionalProperties": False, + "properties": { + "defaults": { + "description": "default values for checks", + "type": "object", + "additionalProperties": False, + "properties": { + "active": conf_schema_active(), + "threshold": conf_schema_threshold(), + "annoy": conf_schema_annoy(), + "schedule": conf_schema_schedule(), + "notifications": conf_schema_notifications(notification_channel_implementations), + }, + "required": [ + ] + }, + "checks": { + "type": "array", + "items": { + "allOf": [ + { + "description": "should represent a specific check", + "type": "object", + "unevaluatedProperties": False, + "properties": { + "name": { + "type": "string" + }, + "title": { + "type": "string" + }, + "active": conf_schema_active(), + "threshold": conf_schema_threshold(), + "annoy": conf_schema_annoy(), + "schedule": conf_schema_schedule(), + "notifications": conf_schema_notifications(notification_channel_implementations), + }, + "required": [ + "name", + ] + }, + { + "anyOf": list( + map( + lambda pair: { + "title": ("notification channel '%s'" % pair[0]), + "type": "object", + "unevaluatedProperties": False, + "properties": { + "kind": { + "type": "string", + "enum": [pair[0]] + }, + "parameters": pair[1].parameters_schema(), + }, + "required": [ + "kind", + "parameters" + ] + }, + check_kind_implementations.items() + ) + ), + }, + ] + } + } + }, + "required": [ + "defaults", + "checks", + ] + } + + +def conf_normalize_interval(interval_raw): + if (type(interval_raw) == int): + return interval_raw + elif (type(interval_raw) == str): + map_ = { + "minute": (60), + "hour": (60 * 60), + "day": (60 * 60 * 24), + "week": (60 * 60 * 24 * 7), + } + if (interval_raw not in map_): + raise ValueError("invalid string interval value: %s" % interval_raw) + else: + return map_[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_notification(notification_channel_implementations, node): + if (node["kind"] not in notification_channel_implementations): + raise ValueError("invalid notification kind: %s" % notification["kind"]) + else: + return { + "kind": node["kind"], + "parameters": notification_channel_implementations[node["kind"]].normalize_conf_node(node["parameters"]), + } + + +def conf_normalize_defaults(notification_channel_implementations, node): + node_ = dict_merge( + { + "active": True, + "threshold": 3, + "annoy": False, + "schedule": { + "regular_interval": (60 * 60), + "attentive_interval": (60 * 2), + }, + "notifications": [ + ], + }, + node + ) + return { + "active": node_["active"], + "threshold": node_["threshold"], + "annoy": node_["annoy"], + "schedule": node_["schedule"], + "notifications": list( + map( + lambda x: conf_normalize_notification(notification_channel_implementations, x), + node_["notifications"] + ) + ), + } + + +def conf_normalize_check(check_kind_implementations, notification_channel_implementations, defaults, node): + if ("name" not in node): + raise ValueError("missing mandatory field in 'check' node: 'name'") + else: + if ("kind" not in node): + raise ValueError("missing mandatory field in 'check' node: 'kind'") + else: + if (node["kind"] not in check_kind_implementations): + raise ValueError("invalid check kind: '%s'" % node["kind"]) + else: + node_ = dict_merge( + { + "title": node["name"], + "active": defaults["active"], + "threshold": defaults["threshold"], + "annoy": defaults["annoy"], + "schedule": defaults["schedule"], + "notifications": defaults["notifications"], + "parameters": {}, + }, + node + ) + return { + "name": node_["name"], + "title": node_["title"], + "active": node_["active"], + "threshold": node_["threshold"], + "annoy": node_["annoy"], + "schedule": conf_normalize_schedule(node_["schedule"]), + "notifications": list( + map( + lambda x: conf_normalize_notification(notification_channel_implementations, x), + node_["notifications"] + ) + ), + "kind": node_["kind"], + "parameters": check_kind_implementations[node_["kind"]].normalize_conf_node(node_["parameters"]), + } + + +def conf_normalize_root(check_kind_implementations, notification_channel_implementations, node): + counts = {} + for node_ in node["checks"]: + if (node_["name"] not in counts): + counts[node_["name"]] = 0 + counts[node_["name"]] += 1 + fails = list(filter(lambda pair: (pair[1] > 1), counts.items())) + if (len(fails) > 0): + raise ValueError( + string_coin( + "ambiguous check names: {{names}}", + { + "names": ",".join(counts.keys()), + } + ) + ) + else: + defaults = conf_normalize_defaults(notification_channel_implementations, node["defaults"]) + return { + "defaults": defaults, + "checks": list( + map( + lambda node_: conf_normalize_check( + check_kind_implementations, + notification_channel_implementations, + defaults, + node_ + ), + node["checks"] + ) + ) + } + diff --git a/source/lib.py b/source/logic/lib.py similarity index 53% rename from source/lib.py rename to source/logic/lib.py index 4e687a1..d8ff203 100644 --- a/source/lib.py +++ b/source/logic/lib.py @@ -1,7 +1,3 @@ -import enum as _enum -import time as _time - - def file_read(path): handle = open(path, "r") content = handle.read() @@ -42,36 +38,9 @@ def dict_merge(core_dict, mantle_dict, recursive = False): return result_dict -class enum_condition(_enum.Enum): - unknown = 0 - ok = 1 - warning = 2 - critical = 3 +def env_get_language(): + env_lang = _os.environ.get("LANG") + locale = env_lang.split(".")[0] + language = locale.split("_")[0] + return language - -def condition_encode(condition): - if (condition == enum_condition.ok): - return "ok" - elif (condition == enum_condition.unknown): - return "unknown" - elif (condition == enum_condition.warning): - return "warning" - elif (condition == enum_condition.critical): - return "critical" - else: - raise ValueError("unhandled condition: %s" % str(condition)) - - -def condition_decode(condition_encoded): - if (condition_encoded == "ok"): - return enum_condition.ok - elif (condition_encoded == "unknown"): - return enum_condition.unknown - elif (condition_encoded == "warning"): - return enum_condition.warning - elif (condition_encoded == "critical"): - return enum_condition.critical - else: - raise ValueError("unhandled encoded condition: %s" % condition_encoded) - - diff --git a/source/logic/localization.py b/source/logic/localization.py new file mode 100644 index 0000000..f0b3e88 --- /dev/null +++ b/source/logic/localization.py @@ -0,0 +1,46 @@ +translation_language_fallback = None + +translation_language_shall = None + +def translation_initialize(language_fallback, language_shall): + global translation_language_fallback + global translation_language_shall + translation_language_fallback = language_fallback + translation_language_shall = language_shall + + +def translation_get(key, arguments = None): + global translation_language_fallback + global translation_language_shall + global localization_data + if (arguments is None): + arguments = {} + for language in [translation_language_shall, translation_language_fallback]: + if (language not in localization_data): + _sys.stderr.write( + string_coin( + "[notice] missing localization data for language '{{language}}'\n", + { + "language": language, + } + ) + ) + else: + if (key not in localization_data[language]): + _sys.stderr.write( + string_coin( + "[notice] missing translation in language '{{language}}' for key '{{key}}'\n", + { + "language": language, + "key": key, + } + ) + ) + else: + return string_coin( + localization_data[language][key], + arguments + ) + return ("{{%s}}" % key) + + diff --git a/source/logic/main.py b/source/logic/main.py new file mode 100644 index 0000000..79e53fc --- /dev/null +++ b/source/logic/main.py @@ -0,0 +1,268 @@ +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 main(): + ## setup translation for the first time + translation_initialize("en", env_get_language()) + + ## args + argumentparser = _argparse.ArgumentParser( + description = translation_get("help.title"), + formatter_class = _argparse.ArgumentDefaultsHelpFormatter + ) + argumentparser.add_argument( + "-c", + "--conf-path", + type = str, + default = "monitoring.hmdl.json", + dest = "conf_path", + metavar = "", + help = translation_get("help.args.conf_path"), + ) + argumentparser.add_argument( + "-f", + "--state-path", + type = str, + default = None, + dest = "state_path", + metavar = "", + help = translation_get("help.args.state_path"), + ) + argumentparser.add_argument( + "-y", + "--send-ok-notifications", + action = "store_true", + default = False, + dest = "send_ok_notifications", + help = translation_get("help.args.send_ok_notifications", {"condition_name": translation_get("conditions.ok")}), + ) + argumentparser.add_argument( + "-l", + "--language", + type = str, + choices = localization_data.keys(), + default = None, + dest = "language", + metavar = "", + help = translation_get("help.args.language"), + ) + argumentparser.add_argument( + "-x", + "--erase-state", + action = "store_true", + default = False, + dest = "erase_state", + help = translation_get("help.args.erase_state"), + ) + argumentparser.add_argument( + "-s", + "--show-schema", + action = "store_true", + default = False, + dest = "show_schema", + help = translation_get("help.args.show_schema"), + ) + argumentparser.add_argument( + "-e", + "--expose-full-conf", + action = "store_true", + default = False, + dest = "expose_full_conf", + help = translation_get("help.args.expose_full_conf"), + ) + 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 + + ### setup translation for the second time + if (args.language is not None): + translation_initialize("en", args.language) + + ### load check kind implementations + check_kind_implementations = { + "script": implementation_check_kind_script(), + "file_state": implementation_check_kind_file_state(), + "http_request": implementation_check_kind_http_request(), + } + + ### load notification channel implementations + notification_channel_implementations = { + "console": implementation_notification_channel_console(), + "email": implementation_notification_channel_email(), + "libnotify": implementation_notification_channel_libnotify(), + } + + if (args.show_schema): + _sys.stdout.write( + _json.dumps( + conf_schema_root( + check_kind_implementations, + notification_channel_implementations + ), + indent = "\t" + ) + + + "\n" + ) + else: + _sys.stderr.write( + string_coin( + "[info] {{label}}: {{path}}\n", + { + "label": translation_get("misc.state_file_path"), + "path": state_path, + } + ) + ) + + ### get configuration data + conf = conf_normalize_root( + check_kind_implementations, + notification_channel_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_data in conf["checks"]: + if (not check_data["active"]): + pass + else: + ### get old state and examine whether the check shall be executed + old_item_state = ( + None + if (check_data["name"] not in state_data) else + state_decode(state_data[check_data["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_data["name"], + } + ) + ) + + ### execute check and set new state + try: + result = check_kind_implementations[check_data["kind"]].run(check_data["parameters"]) + except Exception as error: + result = { + "condition": enum_condition.unknown, + "info": { + # "cause": translation_get("misc.check_procedure_failed"), + "error": str(error), + }, + } + 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_data["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"] + ) + ) + and + ( + (new_item_state["condition"] != enum_condition.ok) + or + args.send_ok_notifications + ) + ): + for notification in check_data["notifications"]: + notification_channel_implementations[notification["kind"]].notify( + notification["parameters"], + check_data["name"], + check_data, + new_item_state, + result["info"] + ) + + +main() + diff --git a/source/packages.py b/source/logic/packages.py similarity index 86% rename from source/packages.py rename to source/logic/packages.py index a7c4038..d365fc9 100644 --- a/source/packages.py +++ b/source/logic/packages.py @@ -6,5 +6,7 @@ import tempfile as _tempfile import argparse as _argparse import json as _json import requests as _requests +import enum as _enum +import time as _time import smtplib as _smtplib from email.mime.text import MIMEText diff --git a/source/main.py b/source/main.py deleted file mode 100644 index aef841a..0000000 --- a/source/main.py +++ /dev/null @@ -1,533 +0,0 @@ -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 schema_active(): - return { - "type": "boolean", - "default": True - } - - -def schema_threshold(): - return { - "description": "how often a condition has to occur in order to be reported", - "type": "integer", - "minimum": 1, - "default": 3 - } - - -def schema_annoy(): - return { - "description": "whether notifications shall be kept sending after the threshold has been surpassed", - "type": "boolean", - "default": False - } - - -def schema_interval(default): - return { - "description": "in seconds or as text", - "anyOf": [ - { - "type": "integer", - "exclusiveMinimum": 0 - }, - { - "type": "string", - "enum": [ - "minute", - "hour", - "day", - "week", - ] - }, - ], - "default": default, - } - - -def schema_schedule(): - return { - "type": "object", - "additionalProperties": False, - "properties": { - "regular_interval": schema_interval(60 * 60), - "attentive_interval": schema_interval(60 * 2), - }, - "required": [ - ] - } - - -def schema_notifications(notification_channel_implementations): - return { - "type": "array", - "items": { - "anyOf": list( - map( - lambda pair: { - "title": ("check kind '%s'" % pair[0]), - "type": "object", - "unevaluatedProperties": False, - "properties": { - "kind": { - "type": "string", - "enum": [pair[0]] - }, - "parameters": pair[1].parameters_schema(), - }, - "required": [ - "kind", - "parameters" - ] - }, - notification_channel_implementations.items() - ) - ) - }, - "default": [ - { - "kind": "console", - "parameters": { - } - } - ] - } - - -def schema_root(check_kind_implementations, notification_channel_implementations): - return { - "type": "object", - "additionalProperties": False, - "properties": { - "defaults": { - "description": "default values for checks", - "type": "object", - "additionalProperties": False, - "properties": { - "active": schema_active(), - "threshold": schema_threshold(), - "annoy": schema_annoy(), - "schedule": schema_schedule(), - "notifications": schema_notifications(notification_channel_implementations), - }, - "required": [ - ] - }, - "checks": { - "type": "array", - "items": { - "allOf": [ - { - "description": "should represent a specific check", - "type": "object", - "unevaluatedProperties": False, - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "active": schema_active(), - "threshold": schema_threshold(), - "annoy": schema_annoy(), - "schedule": schema_schedule(), - "notifications": schema_notifications(notification_channel_implementations), - }, - "required": [ - "name", - ] - }, - { - "anyOf": list( - map( - lambda pair: { - "title": ("notification channel '%s'" % pair[0]), - "type": "object", - "unevaluatedProperties": False, - "properties": { - "kind": { - "type": "string", - "enum": [pair[0]] - }, - "parameters": pair[1].parameters_schema(), - }, - "required": [ - "kind", - "parameters" - ] - }, - check_kind_implementations.items() - ) - ), - }, - ] - } - } - }, - "required": [ - "defaults", - "checks", - ] - } - - -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_notification(notification_channel_implementations, node): - return { - "kind": node["kind"], - "parameters": notification_channel_implementations[node["kind"]].normalize_conf_node(node["parameters"]), - } - - -def conf_normalize_defaults(notification_channel_implementations, node): - node_ = dict_merge( - { - "active": True, - "threshold": 3, - "annoy": False, - "schedule": { - "regular_interval": (60 * 60), - "attentive_interval": (60 * 2), - }, - "notifications": [ - ], - }, - node - ) - return { - "active": node_["active"], - "threshold": node_["threshold"], - "annoy": node_["annoy"], - "schedule": node_["schedule"], - "notifications": list( - map( - lambda x: conf_normalize_notification(notification_channel_implementations, x), - node_["notifications"] - ) - ), - } - - -def conf_normalize_check(check_kind_implementations, notification_channel_implementations, defaults, node): - if ("name" not in node): - raise ValueError("missing mandatory 'check' field 'name'") - else: - 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": node["name"], - "active": defaults["active"], - "threshold": defaults["threshold"], - "annoy": defaults["annoy"], - "schedule": defaults["schedule"], - "notifications": defaults["notifications"], - "parameters": {}, - }, - node - ) - return { - "name": node_["name"], - "title": node_["title"], - "active": node_["active"], - "threshold": node_["threshold"], - "annoy": node_["annoy"], - "schedule": conf_normalize_schedule(node_["schedule"]), - "notifications": list( - map( - lambda x: conf_normalize_notification(notification_channel_implementations, x), - node_["notifications"] - ) - ), - "kind": node_["kind"], - "parameters": check_kind_implementations[node_["kind"]].normalize_conf_node(node_["parameters"]), - } - - -def conf_normalize_root(check_kind_implementations, notification_channel_implementations, node): - counts = {} - for node_ in node["checks"]: - if (node_["name"] not in counts): - counts[node_["name"]] = 0 - counts[node_["name"]] += 1 - fails = list(filter(lambda pair: (pair[1] > 1), counts.items())) - if (len(fails) > 0): - raise ValueError( - string_coin( - "ambiguous check names: {{names}}", - { - "names": ",".join(counts.keys()), - } - ) - ) - else: - return list( - map( - lambda node_: conf_normalize_check( - check_kind_implementations, - notification_channel_implementations, - conf_normalize_defaults(notification_channel_implementations, node["defaults"]), - node_ - ), - node["checks"] - ) - ) - - -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( - "-f", - "--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( - "-s", - "--show-schema", - action = "store_true", - default = False, - dest = "show_schema", - help = "print the hmdl JSON schema to stdout and exit" - ) - 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 - - ### 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(), - } - - if (args.show_schema): - _sys.stdout.write( - _json.dumps( - schema_root( - check_kind_implementations, - notification_channel_implementations - ), - indent = "\t" - ) - + - "\n" - ) - else: - _sys.stderr.write(">> state file path: %s\n" % state_path) - - ### get configuration data - checks = conf_normalize_root( - check_kind_implementations, - notification_channel_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_data in checks: - if (not check_data["active"]): - pass - else: - ### get old state and examine whether the check shall be executed - old_item_state = ( - None - if (check_data["name"] not in state_data) else - state_decode(state_data[check_data["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_data["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_data["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_data["name"], - check_data, - new_item_state, - result["info"] - ) - else: - raise ValueError("invalid notification kind: %s" % notification["kind"]) - - -main() - diff --git a/source/notification_channels/file_touch.py b/source/notification_channels/file_touch.py deleted file mode 100644 index a0ceb63..0000000 --- a/source/notification_channels/file_touch.py +++ /dev/null @@ -1,37 +0,0 @@ -class implementation_notification_channel_file_touch(interface_notification_channel): - - ''' - [implementation] - ''' - def parameters_schema(self): - return { - "type": "object", - "additionalProperties": False, - "properties": { - "path": { - "type": "string" - } - }, - "required": [ - "path" - ] - } - - - ''' - [implementation] - ''' - def normalize_conf_node(self, node): - return dict_merge( - { - }, - node - ) - - - ''' - [implementation] - ''' - def notify(self, parameters, name, data, state, info): - _os.path.touch(parameters["path"]) - diff --git a/todo.md b/todo.md index 52f964b..c021620 100644 --- a/todo.md +++ b/todo.md @@ -6,4 +6,4 @@ - Möglichkeit dauerhaft laufen zulassen (evtl. als systemd-Dienst) - Versionierung - Test-Routinen - +- neu schreiben in TypeScript (und plankton dafür nutzen)? diff --git a/tools/build b/tools/build index 5cdf2a3..d893ef2 100755 --- a/tools/build +++ b/tools/build @@ -1,19 +1,99 @@ -#!/usr/bin/env sh +#!/usr/bin/env python3 -mkdir -p build -echo "#!/usr/bin/env python3" > build/heimdall -cat \ - source/packages.py \ - source/lib.py \ - source/check_kinds/_interface.py \ - source/check_kinds/script.py \ - source/check_kinds/file_timestamp.py \ - source/check_kinds/http_request.py \ - source/notification_channels/_interface.py \ - source/notification_channels/console.py \ - source/notification_channels/file_touch.py \ - source/notification_channels/email.py \ - source/notification_channels/libnotify.py \ - source/main.py \ - >> build/heimdall -chmod +x build/heimdall +import sys as _sys +import os as _os +import json as _json +import stat as _stat + + +def file_read(path): + handle = open(path, "r") + content = handle.read() + handle.close() + return content + + +def file_write(path, content): + handle = open(path, "w") + handle.write(content) + handle.close() + + +def string_coin(template, arguments): + result = template + for (key, value, ) in arguments.items(): + result = result.replace("{{%s}}" % key, value) + return result + + +def python_data_encode(data): + return _json.dumps(data, indent = "\t") + + +def main(): + ## consts + dir_source = "source" + dir_build = "build" + sources_logic = [ + _os.path.join(dir_source, "logic", "packages.py"), + _os.path.join(dir_source, "logic", "lib.py"), + _os.path.join(dir_source, "logic", "localization.py"), + _os.path.join(dir_source, "logic", "condition.py"), + _os.path.join(dir_source, "logic", "conf.py"), + _os.path.join(dir_source, "logic", "checks", "_interface.py"), + _os.path.join(dir_source, "logic", "checks", "script.py"), + _os.path.join(dir_source, "logic", "checks", "file_state.py"), + _os.path.join(dir_source, "logic", "checks", "http_request.py"), + _os.path.join(dir_source, "logic", "channels", "_interface.py"), + _os.path.join(dir_source, "logic", "channels", "console.py"), + _os.path.join(dir_source, "logic", "channels", "email.py"), + _os.path.join(dir_source, "logic", "channels", "libnotify.py"), + _os.path.join(dir_source, "logic", "main.py"), + ] + path_compilation = _os.path.join(dir_build, "heimdall") + + ## exec + if (not _os.path.exists(dir_build)): + _os.mkdir(dir_build) + + compilation = "" + compilation += "#!/usr/bin/env python3\n\n" + + ### localization + if True: + localization_data = dict( + map( + lambda entry: ( + entry.name.split(".")[0], + _json.loads(file_read(entry.path)), + ), + filter( + lambda entry: (entry.is_file() and entry.name.endswith(".json")), + _os.scandir(_os.path.join(dir_source, "localization")) + ) + ) + ) + compilation += string_coin( + "localization_data = {{data}}\n\n", + { + "data": python_data_encode(localization_data), + } + ) + + ### logic + for path in sources_logic: + compilation += (file_read(path) + "\n") + + ### write to file + if _os.path.exists(path_compilation): + _os.remove(path_compilation) + file_write(path_compilation, compilation) + + ### postproess + _os.chmod( + path_compilation, + (_stat.S_IRWXU | _stat.S_IXGRP | _stat.S_IXOTH) + ) + + +main()