From cf9c4f009dd28fd58bd91f47d9a45649dc0e9a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Fra=C3=9F?= Date: Thu, 27 Jul 2023 17:37:45 +0200 Subject: [PATCH] [mod] source --- source/localization/de.json | 2 + source/localization/en.json | 2 + source/logic.old/channels/email.py | 125 ------ source/logic.old/channels/libnotify.py | 90 ---- .../logic.old/check_kinds/generic_remote.py | 178 -------- source/logic.old/lib.py | 118 ------ source/logic.old/main.py | 388 ----------------- source/logic/base.ts | 6 + source/logic/check_kinds/generic_remote.ts | 45 +- source/logic/check_kinds/http_request.ts | 23 +- source/logic/check_kinds/script.ts | 112 +++-- source/logic/helpers/json_schema.ts | 12 +- source/logic/helpers/misc.ts | 55 +++ source/logic/main.ts | 393 +++++++++--------- source/logic/notification_kinds/console.ts | 2 +- source/logic/notification_kinds/email.ts | 155 +++++++ source/logic/notification_kinds/libnotify.ts | 127 ++++++ source/logic/order.ts | 1 + 18 files changed, 632 insertions(+), 1202 deletions(-) delete mode 100644 source/logic.old/channels/email.py delete mode 100644 source/logic.old/channels/libnotify.py delete mode 100644 source/logic.old/check_kinds/generic_remote.py delete mode 100644 source/logic.old/lib.py delete mode 100644 source/logic.old/main.py create mode 100644 source/logic/notification_kinds/email.ts create mode 100644 source/logic/notification_kinds/libnotify.ts diff --git a/source/localization/de.json b/source/localization/de.json index 5537080..bf03499 100644 --- a/source/localization/de.json +++ b/source/localization/de.json @@ -17,6 +17,8 @@ "help.args.expose_full_order": "nur den Pfad zur Datenbank-Datei zur Standard-Ausgabe schreiben und beenden (nützlich für Fehlersuche)", "help.args.show_version": "nur die Version zur Standard-Ausgabe schreiben und beenden", "help.args.verbosity": "Schwellwert für Log-Ausgaben", + "checks.script.execution_failed": "Ausführung gescheitert", + "checks.script.invalid_return_code": "ungültiger Rückgabe-Code", "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", diff --git a/source/localization/en.json b/source/localization/en.json index 3c78bf9..2020fdc 100644 --- a/source/localization/en.json +++ b/source/localization/en.json @@ -17,6 +17,8 @@ "help.args.expose_full_order": "only print the extended order to stdout and exit (useful for debugging)", "help.args.show_version": "only print the version to stdout and exit", "help.args.verbosity": "threshold for log outputs", + "checks.script.execution_failed": "execution failed", + "checks.script.invalid_return_code": "invalid return code", "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", diff --git a/source/logic.old/channels/email.py b/source/logic.old/channels/email.py deleted file mode 100644 index 5efdfdb..0000000 --- a/source/logic.old/channels/email.py +++ /dev/null @@ -1,125 +0,0 @@ -class implementation_notification_channel_email(interface_notification_channel): - - ''' - [implementation] - ''' - def parameters_schema(self): - return { - "type": "object", - "additionalProperties": False, - "properties": { - "access": { - "type": "object", - "additionalProperties": False, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "username": { - "type": "string" - }, - "password": { - "type": "string" - } - }, - "required": [ - "host", - "port", - "username", - "password" - ] - }, - "sender": { - "type": "string" - }, - "receivers": { - "type": "array", - "items": { - "type": "string" - } - }, - "tags": { - "description": "list of strings, which will be placed in the e-mail subject", - "type": "array", - "items": { - "type": "string" - }, - "default": [] - } - }, - "required": [ - "access", - "sender", - "receivers" - ] - } - - - ''' - [implementation] - ''' - def normalize_order_node(self, node): - return dict_merge( - { - }, - node - ) - - - ''' - [implementation] - ''' - def notify(self, parameters, name, data, state, info): - datetime = _datetime.datetime.utcnow() - smtp_connection = _smtplib.SMTP( - parameters["access"]["host"] - ) - smtp_connection.login( - parameters["access"]["username"], - parameters["access"]["password"] - ) - message = MIMEText( - _json.dumps(info, indent = "\t", ensure_ascii = False) - ) - message["Subject"] = string_coin( - "{{tags}} {{title}}", - { - "tags": " ".join( - map( - lambda tag: ("[%s]" % tag.upper()), - ( - parameters["tags"] - + - [condition_show(state["condition"])] - ) - ) - ), - "title": data["name"], - } - ) - message["From"] = parameters["sender"] - message["To"] = ",".join(parameters["receivers"]) - message["Date"] = string_coin( - "{{day_of_week}}, {{day_of_month}} {{month}} {{year}} {{hour}}:{{minute}}:{{second}} {{time_offset}}", - { - "day_of_week": datetime.strftime("%a"), - "day_of_month": datetime.strftime("%d"), - "month": datetime.strftime("%b"), - "year": datetime.strftime("%Y"), - "hour": datetime.strftime("%H"), - "minute": datetime.strftime("%M"), - "second": datetime.strftime("%S"), - # "time_offset": datetime.strftime("%z"), - "time_offset": "+0000", - } - ) - smtp_connection.sendmail( - parameters["sender"], - parameters["receivers"], - message.as_string() - ) - smtp_connection.quit() - diff --git a/source/logic.old/channels/libnotify.py b/source/logic.old/channels/libnotify.py deleted file mode 100644 index dcb5465..0000000 --- a/source/logic.old/channels/libnotify.py +++ /dev/null @@ -1,90 +0,0 @@ -class implementation_notification_channel_libnotify(interface_notification_channel): - - ''' - [implementation] - ''' - def parameters_schema(self): - return { - "type": "object", - "additionalProperties": False, - "properties": { - "icon": { - "type": "string" - } - }, - "required": [ - ] - } - - - ''' - [implementation] - ''' - def normalize_order_node(self, node): - return dict_merge( - { - "icon": "/usr/local/share/icons/heimdall.png", - }, - node - ) - - - ''' - [implementation] - ''' - def notify(self, parameters, name, data, state, info): - def condition_translate(condition): - return { - enum_condition.unknown: "normal", - enum_condition.ok: "low", - enum_condition.concerning: "normal", - enum_condition.critical: "critical", - }[condition] - parts = [] - parts.append( - "notify-send" - ) - ## app name - parts.append( - string_coin( - "--app-name={{app_name}}", - { - "app_name": "heimdall", - } - ) - ) - ## urgency - parts.append( - string_coin( - "--urgency={{urgency}}", - { - "urgency": condition_translate(state["condition"]), - } - ) - ) - ## icon - if ("icon" not in parameters): - pass - else: - parts.append( - string_coin( - "--icon={{icon}}", - { - "icon": parameters["icon"], - } - ) - ) - ## summary - parts.append( - string_coin( - "{{condition}} | {{title}}", - { - "title": data["title"], - "condition": condition_show(state["condition"]).upper(), - } - ) - ) - ## body - parts.append(_json.dumps(info, ensure_ascii = False)) - _subprocess.run(parts) - diff --git a/source/logic.old/check_kinds/generic_remote.py b/source/logic.old/check_kinds/generic_remote.py deleted file mode 100644 index 5652a27..0000000 --- a/source/logic.old/check_kinds/generic_remote.py +++ /dev/null @@ -1,178 +0,0 @@ -class implementation_check_kind_generic_remote(interface_check_kind): - - ''' - [implementation] - ''' - def parameters_schema(self): - return { - "type": "object", - "additionalProperties": False, - "properties": { - "host" : { - "type" : "string" - }, - "ssh_port": { - "type": ["null", "integer"], - "default": None - }, - "ssh_user" : { - "type" : ["null", "string"], - "default": None, - }, - "ssh_key" : { - "type" : ["null", "string"], - "default": None, - }, - "mount_point" : { - "type" : "string", - "default" : "/" - }, - "threshold" : { - "type" : "integer", - "default" : 95, - "description" : "maximaler Füllstand in Prozent" - }, - "critical": { - "description": "whether a violation of this check shall be leveled as critical instead of concerning", - "type": "boolean", - "default": True - }, - "strict": { - "deprecated": True, - "description": "alias for 'critical'", - "type": "boolean", - "default": True - }, - }, - "required": [ - "host", - ] - } - - - ''' - [implementation] - ''' - def normalize_order_node(self, node): - version = ( - "v1" - if (not ("critical" in node)) else - "v2" - ) - - if (version == "v1"): - if (not "host" in node): - raise ValueError("mandatory parameter \"host\" missing") - else: - node_ = dict_merge( - { - "ssh_port": None, - "ssh_user": None, - "ssh_key": None, - "mount_point": "/", - "threshold": 95, - "strict": False, - }, - node - ) - return { - "ssh_port": node_["ssh_port"], - "ssh_user": node_["ssh_user"], - "ssh_key": node_["ssh_key"], - "mount_point": node_["ssh_path"], - "threshold": node_["ssh_threshold"], - "critical": node_["strict"], - } - elif (version == "v2"): - if (not "host" in node): - raise ValueError("mandatory parameter \"host\" missing") - else: - node_ = dict_merge( - { - "ssh_port": None, - "ssh_user": None, - "ssh_key": None, - "mount_point": "/", - "threshold": 95, - "critical": False, - }, - node - ) - return node_ - else: - raise ValueError("unhandled") - - - ''' - [implementation] - ''' - def run(self, parameters): - inner_command = string_coin( - "df {{mount_point}} | tr -s \" \"", - { - "mount_point": parameters["mount_point"], - } - ) - - outer_command_parts = [] - if True: - outer_command_parts.append("ssh"); - if True: - outer_command_parts.append(string_coin("{{host}}", {"host": parameters["host"]})); - if (parameters["ssh_port"] is not None): - outer_command_parts.append(string_coin("-p {{port}}", {"port": ("%u" % parameters["ssh_port"])})); - if (parameters["ssh_user"] is not None): - outer_command_parts.append(string_coin("-l {{user}}", {"user": parameters["ssh_user"]})); - if (parameters["ssh_key"] is not None): - outer_command_parts.append(string_coin("-i {{key}}", {"key": parameters["ssh_key"]})); - if True: - outer_command_parts.append(string_coin("-o BatchMode=yes", {})) - if True: - outer_command_parts.append(string_coin("'{{inner_command}}'", {"inner_command": inner_command})) - outer_command = " ".join(outer_command_parts) - - result = shell_command(outer_command) - - if (result["return_code"] > 0): - return { - "condition": enum_condition.unknown, - "info": { - "error": result["stderr"], - } - } - else: - stuff = result["stdout"].split("\n")[-2].split(" ") - data = { - "device": stuff[0], - "used": int(stuff[2]), - "avail": int(stuff[3]), - "perc": int(stuff[4][:-1]), - } - faults = [] - if (data["perc"] > parameters["threshold"]): - faults.append(translation_get("checks.generic_remote.overflow")) - else: - pass - return { - "condition": ( - enum_condition.ok - if (len(faults) <= 0) else - ( - enum_condition.critical - if parameters["strict"] else - enum_condition.concerning - ) - ), - "info": { - "data": { - "host": parameters["host"], - "device": data["device"], - "mount_point": parameters["mount_point"], - "used": format_bytes(data["used"]), - "available": format_bytes(data["avail"]), - "percentage": (str(data["perc"]) + "%"), - }, - "faults": faults - } - } - diff --git a/source/logic.old/lib.py b/source/logic.old/lib.py deleted file mode 100644 index 882240a..0000000 --- a/source/logic.old/lib.py +++ /dev/null @@ -1,118 +0,0 @@ -def dict_merge(core_dict, mantle_dict, recursive = False): - result_dict = {} - for current_dict in [core_dict, mantle_dict]: - for (key, value, ) in current_dict.items(): - if (not (key in result_dict)): - result_dict[key] = value - else: - if (recursive and (type(result_dict[key]) == dict) and (type(value) == dict)): - result_dict[key] = dict_merge(result_dict[key], value) - elif (recursive and (type(result_dict[key]) == list) and (type(value) == list)): - result_dict[key] = (result_dict[key] + value) - else: - result_dict[key] = value - return result_dict - - -def file_read(path): - handle = open(path, "r") - content = handle.read() - handle.close() - return content - - -def file_write(path, content, options = None): - options = dict_merge( - { - "append": False, - }, - ({} if (options is None) else options) - ) - handle = open(path, "a" if options["append"] else "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 get_current_timestamp(): - return int(round(_time.time(), 0)) - - -def env_get_language(): - try: - env_lang = _os.environ.get("LANG") - locale = env_lang.split(".")[0] - language = locale.split("_")[0] - return language - except Exception as error: - return None - - -def shell_command(command): - result = _subprocess.run( - command, - capture_output = True, - shell = True, - ) - return { - "return_code": result.returncode, - "stdout": result.stdout.decode(), - "stderr": result.stderr.decode(), - } - - -def format_bytes(bytes_): - units = [ - {"label": "B", "digits": 0}, - {"label": "KB", "digits": 1}, - {"label": "MB", "digits": 1}, - {"label": "GB", "digits": 1}, - {"label": "TB", "digits": 1}, - {"label": "PB", "digits": 1}, - ] - number = bytes_ - index = 0 - while ((number >= 1000) and (index < (len(units) - 1))): - number /= 1000 - index += 1 - return ( - ("%." + ("%u" % units[index]["digits"]) + "f %s") - % ( - number, - units[index]["label"], - ) - ) - - -def sqlite_query_set(database_path, template, arguments): - connection = _sqlite3.connect(database_path) - cursor = connection.cursor() - result = cursor.execute(template, arguments) - connection.commit() - connection.close() - return result - - -def sqlite_query_put(database_path, template, arguments): - connection = _sqlite3.connect(database_path) - cursor = connection.cursor() - result = cursor.execute(template, arguments) - connection.commit() - connection.close() - return result - - -def sqlite_query_get(database_path, template, arguments): - connection = _sqlite3.connect(database_path) - cursor = connection.cursor() - result = cursor.execute(template, arguments) - rows = result.fetchall() - connection.close() - return rows - diff --git a/source/logic.old/main.py b/source/logic.old/main.py deleted file mode 100644 index 1f89705..0000000 --- a/source/logic.old/main.py +++ /dev/null @@ -1,388 +0,0 @@ -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( - type = str, - default = "monitoring.hmdl.json", - dest = "order_path", - metavar = "", - help = translation_get("help.args.order_path"), - ) - 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-order", - action = "store_true", - default = False, - dest = "expose_full_order", - help = translation_get("help.args.expose_full_order"), - ) - ### v conf stuff v - argumentparser.add_argument( - "-d", - "--database-path", - type = str, - default = None, - dest = "database_path", - metavar = "", - help = translation_get("help.args.database_path"), - ) - argumentparser.add_argument( - "-m", - "--mutex-path", - type = str, - default = "/tmp/heimdall.lock", - dest = "mutex_path", - metavar = "", - help = translation_get("help.args.mutex_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( - "-t", - "--time-to-live", - type = int, - default = (60 * 60 * 24 * 7), - dest = "time_to_live", - metavar = "", - help = translation_get("help.args.time_to_live"), - ) - args = argumentparser.parse_args() - - ## vars - id_ = _hashlib.sha256(_os.path.abspath(args.order_path).encode("ascii")).hexdigest()[:8] - database_path = ( - args.database_path - if (args.database_path is not None) else - _os.path.join( - _tempfile.gettempdir(), - string_coin("monitoring-state-{{id}}.sqlite", {"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(), - "tls_certificate": implementation_check_kind_tls_certificate(), - "http_request": implementation_check_kind_http_request(), - "generic_remote" : implementation_check_kind_generic_remote(), - } - - ### 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( - order_schema_root( - check_kind_implementations, - notification_channel_implementations - ), - indent = "\t" - ) - + - "\n" - ) - else: - ### get order data - order = order_load( - check_kind_implementations, - notification_channel_implementations, - _os.path.abspath(args.order_path) - ) - - if (args.expose_full_order): - _sys.stdout.write(_json.dumps(order, indent = "\t") + "\n") - _sys.exit(1) - else: - _sys.stderr.write( - string_coin( - "[info] {{label}}: {{path}}\n", - { - "label": translation_get("misc.state_file_path"), - "path": database_path, - } - ) - ) - - ### mutex check - if (_os.path.exists(args.mutex_path)): - _sys.stderr.write( - string_coin( - "[error] {{message}} ({{path}})\n", - { - "message": translation_get("misc.still_running"), - "path": args.mutex_path, - } - ) - ) - _sys.exit(2) - else: - ### setup database - sqlite_query_set( - database_path, - "CREATE TABLE IF NOT EXISTS results(check_name TEXT NOT NULL, timestamp INTEGER NOT NULL, condition TEXT NOT NULL, notification_sent BOOLEAN NOT NULL, info TEXT NOT NULL);", - {} - ) - - ### clean database - result = sqlite_query_put( - database_path, - "DELETE FROM results WHERE ((timestamp < :timestamp_min) OR :erase_state);", - { - "timestamp_min": (get_current_timestamp() - args.time_to_live), - "erase_state": args.erase_state, - } - ) - _sys.stderr.write( - string_coin( - "[info] {{text}}\n", - { - "text": translation_get( - "misc.cleanup_info", - { - "count": ("%u" % result.rowcount), - } - ), - } - ) - ) - - file_write(args.mutex_path, "", {"append": True}) - - ### iterate through checks - for check_data in order["checks"]: - if (not check_data["active"]): - pass - else: - ### get old state and examine whether the check shall be executed - rows1 = sqlite_query_get( - database_path, - "SELECT MAX(timestamp) FROM results WHERE (check_name = :check_name) AND (notification_sent = TRUE);", - { - "check_name": check_data["name"], - } - ) - rows2 = sqlite_query_get( - database_path, - "SELECT timestamp, condition, notification_sent FROM results WHERE (check_name = :check_name) ORDER BY timestamp DESC LIMIT :limit;", - { - "check_name": check_data["name"], - "limit": (check_data["threshold"] + 1), - } - ) - if (len(rows2) <= 0): - old_item_state = None - else: - count = 1 - for row in rows2[1:]: - if (row[1] == rows2[0][1]): - count += 1 - else: - break - if (count > check_data["threshold"]): - count = None - else: - pass - old_item_state = { - "timestamp": rows2[0][0], - "condition": condition_decode(rows2[0][1]), - "count": count, - "last_notification_timestamp": rows1[0][0], - } - - 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), - }, - } - 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 - ) - ) - shall_send_notification = ( - ( - ( - (count is not None) - and - (count == check_data["threshold"]) - ) - or - ( - (count is None) - and - check_data["annoy"] - ) - or - ( - (count is None) - and - ( - (old_item_state is not None) - and - (old_item_state["last_notification_timestamp"] is not None) - and - (check_data["schedule"]["reminding_interval"] is not None) - and - ( - (timestamp - old_item_state["last_notification_timestamp"]) - >= - check_data["schedule"]["reminding_interval"] - ) - ) - ) - ) - and - ( - (result["condition"] != enum_condition.ok) - or - args.send_ok_notifications - ) - ) - new_item_state = { - "timestamp": timestamp, - "condition": result["condition"], - "count": count, - "last_notification_timestamp": ( - timestamp - if shall_send_notification else - ( - None - if (old_item_state is None) else - old_item_state["last_notification_timestamp"] - ) - ), - } - sqlite_query_put( - database_path, - "INSERT INTO results(check_name, timestamp, condition, notification_sent, info) VALUES (:check_name, :timestamp, :condition, :notification_sent, :info);", - { - "check_name": check_data["name"], - "timestamp": timestamp, - "condition": condition_encode(result["condition"]), - "notification_sent": shall_send_notification, - "info": _json.dumps(result["info"]), - } - ) - - ### send notifications - if (not shall_send_notification): - pass - else: - for notification in check_data["notifications"]: - notification_channel_implementations[notification["kind"]].notify( - notification["parameters"], - check_data["name"], - check_data, - new_item_state, - dict_merge( - ( - {} - if (check_data["custom"] is None) else - {"custom": check_data["custom"]} - ), - result["info"] - ) - ) - - if (not _os.path.exists(args.mutex_path)): - pass - else: - _os.remove(args.mutex_path) - - -main() - diff --git a/source/logic/base.ts b/source/logic/base.ts index 25d5c1a..ca16847 100644 --- a/source/logic/base.ts +++ b/source/logic/base.ts @@ -1,3 +1,9 @@ +/** + * @todo outsource + */ +declare var __dirname; + + namespace _heimdall { diff --git a/source/logic/check_kinds/generic_remote.ts b/source/logic/check_kinds/generic_remote.ts index 2c51b3d..798d7a8 100644 --- a/source/logic/check_kinds/generic_remote.ts +++ b/source/logic/check_kinds/generic_remote.ts @@ -111,11 +111,12 @@ namespace _heimdall.check_kinds.generic_remote true ); return { + "host": node_["host"], "ssh_port": node_["ssh_port"], "ssh_user": node_["ssh_user"], "ssh_key": node_["ssh_key"], - "mount_point": node_["ssh_path"], - "threshold": node_["ssh_threshold"], + "mount_point": node_["mount_point"], + "threshold": node_["threshold"], "critical": node_["strict"], }; } @@ -185,49 +186,21 @@ namespace _heimdall.check_kinds.generic_remote } const outer_command : string = outer_command_parts.join(" "); - type type_result = { - return_code : int; - stdout : string; - stderr : string; - }; - /** - * @see https://nodejs.org/api/child_process.html#child_processspawncommand-args-options - */ - const result : type_result = await new Promise( - (resolve, reject) => { - nm_child_process.spawnSync( - outer_command, - [], - { - }, - (result) => { - if (result.error) { - reject(result.error); - } - else { - resolve( - { - "return_code": result.status, - "stdout": result.stdout, - "stderr": result.stdin, - } - ); - } - } - ); - } + const shell_exec_result : _heimdall.helpers.misc.type_shell_exec_result = await _heimdall.helpers.misc.shell_exec( + outer_command_parts[0], + outer_command_parts.slice(1) ); - if (result.return_code > 0) { + if (shell_exec_result.return_code > 0) { return { "condition": _heimdall.enum_condition.unknown, "info": { - "error": result.stderr, + "error": shell_exec_result.stderr, } }; } else { - const stuff : Array = result.stdout.split("\n").slice(-2)[0].split(" "); + const stuff : Array = shell_exec_result.stdout.split("\n").slice(-2)[0].split(" "); const data = { "device": stuff[0], "used": parseInt(stuff[2]), diff --git a/source/logic/check_kinds/http_request.ts b/source/logic/check_kinds/http_request.ts index da55c4f..728a997 100644 --- a/source/logic/check_kinds/http_request.ts +++ b/source/logic/check_kinds/http_request.ts @@ -194,12 +194,21 @@ namespace _heimdall.check_kinds.http_request ) : Promise<_heimdall.type_result> { let error : (null | Error); + const url : URL = new URL(parameters.request.target); const http_request : lib_plankton.http.type_request = { - "target": parameters.request.target, + "version": "HTTP/1.1", + "scheme": ( + (url.protocol.slice(0, -1) === "https") + ? "https" + : "http" + ), + "host": url.host, "method": { "GET": lib_plankton.http.enum_method.get, "POST": lib_plankton.http.enum_method.post, }[parameters.request.method], + "path": url.pathname, + "query": url.search, "headers": {}, "body": "", }; @@ -208,9 +217,9 @@ namespace _heimdall.check_kinds.http_request http_response = await lib_plankton.http.call( http_request, { - "timeout": /*parameters.timeout*/20.0, + "timeout": parameters.timeout, "follow_redirects": parameters.follow_redirects, - "implementation": "http_module", + "implementation": "fetch", } ); error = null; @@ -247,12 +256,12 @@ namespace _heimdall.check_kinds.http_request } else { const status_code_expected : int = (parameters.response.status_code as int); - if (! (http_response.statuscode === status_code_expected)) { + if (! (http_response.status_code === status_code_expected)) { faults.push( lib_plankton.translate.get( "checks.http_request.status_code_mismatch", { - "status_code_actual": http_response.statuscode.toFixed(0), + "status_code_actual": http_response.status_code.toFixed(0), "status_code_expected": status_code_expected.toFixed(0), } ) @@ -327,7 +336,7 @@ namespace _heimdall.check_kinds.http_request } else { const body_part : string = (parameters.response.body_part as string); - if (! http_response.body.includes(body_part)) { + if (! http_response.body.toString().includes(body_part)) { faults.push( lib_plankton.translate.get( "checks.http_request.body_misses_part", @@ -356,7 +365,7 @@ namespace _heimdall.check_kinds.http_request "info": { "request": parameters.request, "response": { - "status_code": http_response.statuscode, + "status_code": http_response.status_code, "headers": http_response.headers, // "body": http_response.body, }, diff --git a/source/logic/check_kinds/script.ts b/source/logic/check_kinds/script.ts index 5e801f6..d4a6b32 100644 --- a/source/logic/check_kinds/script.ts +++ b/source/logic/check_kinds/script.ts @@ -54,73 +54,63 @@ namespace _heimdall.check_kinds.script parameters ) : Promise<_heimdall.type_result> { - const nm_child_process = require("child_process"); - - type type_result = { - return_code : int; - stdout : string; - stderr : string; - }; - /** - * @see https://nodejs.org/api/child_process.html#child_processspawncommand-args-options - */ - const result : type_result = await new Promise( - (resolve, reject) => { - nm_child_process.spawnSync( - parameters["path"], - parameters["arguments"], - { - }, - (result) => { - if (result.error) { - reject(result.error); - } - else { - resolve( - { - "return_code": result.status, - "stdout": result.stdout, - "stderr": result.stdin, - } - ); - } - } - ); - } - ); + let shell_exec_result : (null | _heimdall.helpers.misc.type_shell_exec_result); + let error : any; + try { + shell_exec_result = await _heimdall.helpers.misc.shell_exec( + parameters["path"], + parameters["arguments"], + ); + error = null; + } + catch (error_) { + shell_exec_result = null; + error = error_; + } let condition : _heimdall.enum_condition; - switch (result.return_code) { - default: { - lib_plankton.log.notice( - lib_plankton.translate.get("check_kind_script_invalid_return_code"), - { - "return_code": result.return_code, - } - ); - condition = _heimdall.enum_condition.unknown; - break; - } - case 0: { - condition = _heimdall.enum_condition.ok; - break; - } - case 1: { - condition = _heimdall.enum_condition.unknown; - break; - } - case 2: { - condition = _heimdall.enum_condition.concerning; - break; - } - case 3: { - condition = _heimdall.enum_condition.critical; - break; + if ((error !== null) || (shell_exec_result === null)) { + lib_plankton.log.notice( + lib_plankton.translate.get("checks.script.execution_failed"), + { + "error": String(error), + } + ); + condition = _heimdall.enum_condition.unknown; + } + else { + switch (shell_exec_result.return_code) { + default: { + lib_plankton.log.notice( + lib_plankton.translate.get("checks.script.invalid_return_code"), + { + "return_code": shell_exec_result.return_code, + } + ); + condition = _heimdall.enum_condition.unknown; + break; + } + case 0: { + condition = _heimdall.enum_condition.ok; + break; + } + case 1: { + condition = _heimdall.enum_condition.unknown; + break; + } + case 2: { + condition = _heimdall.enum_condition.concerning; + break; + } + case 3: { + condition = _heimdall.enum_condition.critical; + break; + } } } return { "condition": condition, "info": { - "result": result, + "result": shell_exec_result, }, }; } diff --git a/source/logic/helpers/json_schema.ts b/source/logic/helpers/json_schema.ts index 7276721..a52b57a 100644 --- a/source/logic/helpers/json_schema.ts +++ b/source/logic/helpers/json_schema.ts @@ -33,8 +33,14 @@ namespace _heimdall.helpers.json_schema } & ( + { + type : "any"; + default ?: any; + } + | { type : "null"; + default ?: null; } | { @@ -97,11 +103,13 @@ namespace _heimdall.helpers.json_schema } | { - anyOf ?: Array; + anyOf : Array; + default ?: any; } | { - allOf ?: Array; + allOf : Array; + default ?: any; } ) ); diff --git a/source/logic/helpers/misc.ts b/source/logic/helpers/misc.ts index d3feaf3..c629ffa 100644 --- a/source/logic/helpers/misc.ts +++ b/source/logic/helpers/misc.ts @@ -29,4 +29,59 @@ namespace _heimdall.helpers.misc } ); } + + + /** + */ + export type type_shell_exec_result = {return_code : int; stdout : string; stderr : string;}; + + + /** + * @see https://nodejs.org/api/child_process.html#child_processspawncommand-args-options + */ + export function shell_exec( + head : string, + args : Array + ) : Promise + { + const nm_child_process = require("child_process"); + + return new Promise( + (resolve, reject) => { + let stdout : string = ""; + let stderr : string = ""; + const proc = nm_child_process.spawn( + head, + args, + { + }, + ); + proc.stdout.on( + "data", + (data) => { + stdout += data; + } + ); + proc.stderr.on( + "data", + (data) => { + stderr += data; + } + ); + proc.on( + "close", + (return_code) => { + resolve( + { + "return_code": return_code, + "stdout": stdout, + "stderr": stderr, + } + ); + } + ); + } + ); + } + } diff --git a/source/logic/main.ts b/source/logic/main.ts index 190ea7c..e1719b1 100644 --- a/source/logic/main.ts +++ b/source/logic/main.ts @@ -3,6 +3,7 @@ async function main( { // consts const version : string = "0.8"; + const workdir : string = __dirname; // init translations // TODO: use env language @@ -12,8 +13,8 @@ async function main( "order": ["de", "en"], "packages": await Promise.all( [ - {"identifier": "de", "path": "localization/de.json"}, - {"identifier": "en", "path": "localization/en.json"}, + {"identifier": "de", "path": (workdir + "/localization/de.json")}, + {"identifier": "en", "path": (workdir + "/localization/en.json")}, ] .map( (entry) => ( @@ -65,7 +66,7 @@ async function main( "default": false, "parameters": { "indicators_long": ["version"], - "indicators_short": ["v"], + "indicators_short": ["r"], }, "info": lib_plankton.translate.get("help.args.show_version"), } @@ -243,6 +244,8 @@ async function main( else { const notification_kind_implementations : Record = { "console": _heimdall.notification_kinds.console.notification_kind_implementation(), + "email": _heimdall.notification_kinds.email.notification_kind_implementation(), + "libnotify": _heimdall.notification_kinds.libnotify.notification_kind_implementation(), }; const check_kind_implementations : Record = { "script": _heimdall.check_kinds.script.check_kind_implementation(), @@ -341,208 +344,206 @@ async function main( // create mutex file await lib_plankton.file.write(args["mutex_path"], ""); - order.checks.forEach( - async (check) => { - if (! check.active) { - // do nothing + for await (const check of order.checks) { + if (! check.active) { + // do nothing + } + else { + let old_item_state : (null | _heimdall.type_item_state); + + const last_notification_timestamp : (null | int) = await _heimdall.state_repository.get_last_notification_timestamp( + database_path, + check.name + ); + + const rows : Array< + { + timestamp : int; + condition : _heimdall.enum_condition; + notification_sent : boolean; + } + > = await _heimdall.state_repository.probe( + database_path, + check.name, + (check.threshold + 1), + ); + if (rows.length <= 0) { + old_item_state = null; } else { - let old_item_state : (null | _heimdall.type_item_state); - - const last_notification_timestamp : (null | int) = await _heimdall.state_repository.get_last_notification_timestamp( - database_path, - check.name - ); - - const rows : Array< - { - timestamp : int; - condition : _heimdall.enum_condition; - notification_sent : boolean; - } - > = await _heimdall.state_repository.probe( - database_path, - check.name, - (check.threshold + 1), - ); - if (rows.length <= 0) { - old_item_state = null; - } - else { - let count : int = 1; - rows.slice(1).some( - (row) => { - if (row.condition === rows[0].condition) { - count += 1; - return true; - } - else { - return false; - } - } - ); - if (count > check.threshold) { - count = null; - } - else { - // do nothing - } - old_item_state = { - "timestamp": rows[0].timestamp, - "condition": rows[0].condition, - "count": count, - "last_notification_timestamp": last_notification_timestamp, - } - - const timestamp : int = _heimdall.get_current_timestamp(); - const due : boolean = ( - (old_item_state === null) - || - (old_item_state.condition !== _heimdall.enum_condition.ok) - || - ((timestamp - old_item_state.timestamp) >= check.schedule.regular_interval) - || - ( - (! (old_item_state.count === null)) - && - ((timestamp - old_item_state.timestamp) >= check.schedule.attentive_interval) - ) - ); - if (! due) { - // do nothing - } - else { - process.stderr.write( - lib_plankton.string.coin( - "-- {{check_name}}\n", - { - "check_name": check.name, - } - ) - ); - - // execute check and set new state - let result; - try { - result = await check_kind_implementations[check.kind].run(check.parameters); - } - catch (error) { - result = { - "condition": _heimdall.enum_condition.unknown, - "info": { - "cause": lib_plankton.translate.get("misc.check_procedure_failed"), - "error": error.toString(), - }, - } - } - const count : (null | int) = ( - ( - (old_item_state === null) - || - (old_item_state.condition !== result.condition) - ) - ? 1 - : ( - ( - (! (old_item_state.count === null)) - && - ((old_item_state.count + 1) <= check.threshold) - ) - ? (old_item_state.count + 1) - : null - ) - ) - const shall_send_notification : boolean = ( - ( - ( - (! (count === null)) - && - (count == check.threshold) - ) - || - ( - (count === null) - && - check.annoy - ) - || - ( - (count === null) - && - ( - (! (old_item_state === null)) - && - (! (old_item_state.last_notification_timestamp === null)) - && - (! (check.schedule.reminding_interval === null)) - && - ( - (timestamp - old_item_state.last_notification_timestamp) - >= - check.schedule.reminding_interval - ) - ) - ) - ) - && - ( - (result.condition !== _heimdall.enum_condition.ok) - || - args["send_ok_notifications"] - ) - ) - const new_item_state : _heimdall.type_item_state = { - "timestamp": timestamp, - "condition": result.condition, - "count": count, - "last_notification_timestamp": ( - shall_send_notification - ? timestamp - : ( - (old_item_state === null) - ? null - : old_item_state.last_notification_timestamp - ) - ), - } - await _heimdall.state_repository.feed( - database_path, - check.name, - timestamp, - result.condition, - shall_send_notification, - result.info - ); - - // send notifications - if (! shall_send_notification) { - // do nothing + let count : int = 1; + rows.slice(1).some( + (row) => { + if (row.condition === rows[0].condition) { + count += 1; + return true; } else { - check.notifications.forEach( - notification => { - notification_kind_implementations[notification.kind].notify( - notification.parameters, - check.name, - check, - new_item_state, - Object.assign( - ( - (check.custom === null) - ? {} - : {"custom": check.custom} - ), - result.info - ) - ) - } - ); + return false; } } + ); + if (count > check.threshold) { + count = null; + } + else { + // do nothing + } + old_item_state = { + "timestamp": rows[0].timestamp, + "condition": rows[0].condition, + "count": count, + "last_notification_timestamp": last_notification_timestamp, + } + + const timestamp : int = _heimdall.get_current_timestamp(); + const due : boolean = ( + (old_item_state === null) + || + (old_item_state.condition !== _heimdall.enum_condition.ok) + || + ((timestamp - old_item_state.timestamp) >= check.schedule.regular_interval) + || + ( + (! (old_item_state.count === null)) + && + ((timestamp - old_item_state.timestamp) >= check.schedule.attentive_interval) + ) + ); + if (! due) { + // do nothing + } + else { + process.stderr.write( + lib_plankton.string.coin( + "-- {{check_name}}\n", + { + "check_name": check.name, + } + ) + ); + + // execute check and set new state + let result; + try { + result = await check_kind_implementations[check.kind].run(check.parameters); + } + catch (error) { + result = { + "condition": _heimdall.enum_condition.unknown, + "info": { + "cause": lib_plankton.translate.get("misc.check_procedure_failed"), + "error": error.toString(), + }, + } + } + const count : (null | int) = ( + ( + (old_item_state === null) + || + (old_item_state.condition !== result.condition) + ) + ? 1 + : ( + ( + (! (old_item_state.count === null)) + && + ((old_item_state.count + 1) <= check.threshold) + ) + ? (old_item_state.count + 1) + : null + ) + ) + const shall_send_notification : boolean = ( + ( + ( + (! (count === null)) + && + (count == check.threshold) + ) + || + ( + (count === null) + && + check.annoy + ) + || + ( + (count === null) + && + ( + (! (old_item_state === null)) + && + (! (old_item_state.last_notification_timestamp === null)) + && + (! (check.schedule.reminding_interval === null)) + && + ( + (timestamp - old_item_state.last_notification_timestamp) + >= + check.schedule.reminding_interval + ) + ) + ) + ) + && + ( + (result.condition !== _heimdall.enum_condition.ok) + || + args["send_ok_notifications"] + ) + ) + const new_item_state : _heimdall.type_item_state = { + "timestamp": timestamp, + "condition": result.condition, + "count": count, + "last_notification_timestamp": ( + shall_send_notification + ? timestamp + : ( + (old_item_state === null) + ? null + : old_item_state.last_notification_timestamp + ) + ), + } + await _heimdall.state_repository.feed( + database_path, + check.name, + timestamp, + result.condition, + shall_send_notification, + result.info + ); + + // send notifications + if (! shall_send_notification) { + // do nothing + } + else { + check.notifications.forEach( + notification => { + notification_kind_implementations[notification.kind].notify( + notification.parameters, + check.name, + check, + new_item_state, + Object.assign( + ( + (check.custom === null) + ? {} + : {"custom": check.custom} + ), + result.info + ) + ) + } + ); + } } } } - ); + } // drop mutex file await ( diff --git a/source/logic/notification_kinds/console.ts b/source/logic/notification_kinds/console.ts index adad472..a026340 100644 --- a/source/logic/notification_kinds/console.ts +++ b/source/logic/notification_kinds/console.ts @@ -45,7 +45,7 @@ namespace _heimdall.notification_kinds.console lib_plankton.string.coin( "[{{title}}] <{{condition}}> {{info}}\n", { - "title": check.title, + "title": check.name, "condition": _heimdall.condition_show(state.condition), "info": lib_plankton.json.encode(info, true), } diff --git a/source/logic/notification_kinds/email.ts b/source/logic/notification_kinds/email.ts new file mode 100644 index 0000000..8987fcd --- /dev/null +++ b/source/logic/notification_kinds/email.ts @@ -0,0 +1,155 @@ +namespace _heimdall.notification_kinds.email +{ + + /** + */ + function parameters_schema( + ) : _heimdall.helpers.json_schema.type_schema + { + return { + "type": "object", + "additionalProperties": false, + "properties": { + "access": { + "type": "object", + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": [ + "host", + "port", + "username", + "password" + ] + }, + "sender": { + "type": "string" + }, + "receivers": { + "type": "array", + "items": { + "type": "string" + } + }, + "tags": { + "description": "list of strings, which will be placed in the e-mail subject", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + } + }, + "required": [ + "access", + "sender", + "receivers" + ] + }; + } + + + /** + */ + function normalize_order_node( + node : any + ) : any + { + return Object.assign( + { + }, + node + ); + } + + + /** + */ + async function notify( + parameters : any, + name : string, + check : type_check, + state : type_item_state, + info : any + ) : Promise + { + const nm_nodemailer = require("nodemailer"); + + // datetime = _datetime.datetime.utcnow() + const now : Date = new Date(Date.now()); + + const transporter = nm_nodemailer.createTransport( + { + "host": parameters.access.host, + "port": parameters.access.port, + "secure": true, + "auth": { + "user": parameters.access.username, + "pass": parameters.access.password, + } + } + ); + const send_result = await transporter.sendMail( + { + "from": parameters.sender, + "to": parameters.receivers.join(","), + "subject": lib_plankton.string.coin( + "{{tags}} {{title}}", + { + "tags": ( + parameters.tags + .concat([_heimdall.condition_show(state.condition)]) + .map(tag => lib_plankton.string.coin("[{{tag}}]", tag.toUpperCase())) + .join(" ") + ), + "title": check.name, + } + ), + "text": JSON.stringify(info, undefined, "\t"), + "headers": { + "Date": lib_plankton.string.coin( + "{{day_of_week}}, {{day_of_month}} {{month}} {{year}} {{hour}}:{{minute}}:{{second}} {{time_offset}}", + { + "day_of_week": now.getDay().toFixed(0), + "day_of_month": now.getDate().toFixed(0), + "month": (now.getMonth() + 1).toFixed(0), + "year": now.getFullYear().toFixed(0), + "hour": now.getHours().toFixed(0), + "minute": now.getMinutes().toFixed(0), + "second": now.getSeconds().toFixed(0), + // "time_offset": (now.getTimezoneOffset() / 60).toFixed(0), + "time_offset": "+0000", + } + ), + } + } + ); + return Promise.resolve(undefined); + } + + + /** + */ + export function notification_kind_implementation( + ) : type_notification_kind + { + return { + "parameters_schema": parameters_schema, + "normalize_order_node": normalize_order_node, + "notify": notify, + }; + } + +} diff --git a/source/logic/notification_kinds/libnotify.ts b/source/logic/notification_kinds/libnotify.ts new file mode 100644 index 0000000..01be855 --- /dev/null +++ b/source/logic/notification_kinds/libnotify.ts @@ -0,0 +1,127 @@ +namespace _heimdall.notification_kinds.libnotify +{ + + /** + */ + function parameters_schema( + ) : _heimdall.helpers.json_schema.type_schema + { + return { + "type": "object", + "additionalProperties": false, + "properties": { + "icon": { + "type": "string" + } + }, + "required": [ + ] + }; + } + + + /** + */ + function normalize_order_node( + node : any + ) : any + { + return Object.assign( + { + "icon": "/usr/local/share/icons/heimdall.png", + }, + node + ); + } + + + /** + */ + async function notify( + parameters : any, + name : string, + check : type_check, + state : type_item_state, + info : any + ) : Promise + { + const condition_translate = function (condition : _heimdall.enum_condition): string + { + switch (condition) { + case _heimdall.enum_condition.unknown: return "normal"; + case _heimdall.enum_condition.ok: return "low"; + case _heimdall.enum_condition.concerning: return "normal"; + case _heimdall.enum_condition.critical: return "critical"; + } + }; + + let parts : Array = []; + parts.push( + "notify-send" + ); + // app name + parts.push( + lib_plankton.string.coin( + "--app-name={{app_name}}", + { + "app_name": "heimdall", + } + ) + ) + // urgency + parts.push( + lib_plankton.string.coin( + "--urgency={{urgency}}", + { + "urgency": condition_translate(state.condition), + } + ) + ) + // icon + if (! ("icon" in parameters)) { + // do nothing + } + else { + parts.push( + lib_plankton.string.coin( + "--icon={{icon}}", + { + "icon": parameters.icon, + } + ) + ); + } + // summary + parts.push( + lib_plankton.string.coin( + "{{condition}} | {{title}}", + { + "title": check.name, + "condition": _heimdall.condition_show(state.condition).toUpperCase(), + } + ) + ); + // body + parts.push( + JSON.stringify(info) + ); + + await _heimdall.helpers.misc.shell_exec(parts[0], parts.slice(1)); + + return Promise.resolve(undefined); + } + + + /** + */ + export function notification_kind_implementation( + ) : type_notification_kind + { + return { + "parameters_schema": parameters_schema, + "normalize_order_node": normalize_order_node, + "notify": notify, + }; + } + +} diff --git a/source/logic/order.ts b/source/logic/order.ts index 2c74233..dd83c1a 100644 --- a/source/logic/order.ts +++ b/source/logic/order.ts @@ -219,6 +219,7 @@ namespace _heimdall.order }, "parameters": value.parameters_schema(), "custom": { + "type": "any", "description": "custom data, which shall be attached to notifications", "default": null, },