This commit is contained in:
Christian Fraß 2023-07-03 10:37:31 +02:00
commit a2cc43dffb
2 changed files with 278 additions and 312 deletions

View file

@ -28,7 +28,7 @@
"checks.http_request.request_failed": "HTTP request failed", "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.status_code_mismatch": "actual status code {{status_code_actual}} does not match expected value {{status_code_expected}}",
"checks.http_request.header_missing": "header '{{key}}' is unset and hence does not match the expected value '{{value_expected}}'", "checks.http_request.header_missing": "header '{{key}}' is unset and hence does not match the expected value '{{value_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.header_value_mismatch": "actual header value for key '{{key}}' '{{value_actual}}' does not match the expected value {{value_expected}}",
"checks.http_request.body_misses_part": "body does not contain the expected part '{{part}}'", "checks.http_request.body_misses_part": "body does not contain the expected part '{{part}}'",
"misc.state_file_path": "state file path", "misc.state_file_path": "state file path",
"misc.check_procedure_failed": "check procedure failed", "misc.check_procedure_failed": "check procedure failed",

View file

@ -1,7 +1,4 @@
def main(): def main():
## const
version = "0.8"
## setup translation for the first time ## setup translation for the first time
translation_initialize("en", env_get_language()) translation_initialize("en", env_get_language())
@ -11,7 +8,6 @@ def main():
formatter_class = _argparse.ArgumentDefaultsHelpFormatter formatter_class = _argparse.ArgumentDefaultsHelpFormatter
) )
argumentparser.add_argument( argumentparser.add_argument(
nargs = "?",
type = str, type = str,
default = "monitoring.hmdl.json", default = "monitoring.hmdl.json",
dest = "order_path", dest = "order_path",
@ -19,12 +15,12 @@ def main():
help = translation_get("help.args.order_path"), help = translation_get("help.args.order_path"),
) )
argumentparser.add_argument( argumentparser.add_argument(
"-v", "-x",
"--version", "--erase-state",
action = "store_true", action = "store_true",
default = False, default = False,
dest = "show_version", dest = "erase_state",
help = translation_get("help.args.show_version"), help = translation_get("help.args.erase_state"),
) )
argumentparser.add_argument( argumentparser.add_argument(
"-s", "-s",
@ -42,14 +38,6 @@ def main():
dest = "expose_full_order", dest = "expose_full_order",
help = translation_get("help.args.expose_full_order"), help = translation_get("help.args.expose_full_order"),
) )
argumentparser.add_argument(
"-x",
"--erase-state",
action = "store_true",
default = False,
dest = "erase_state",
help = translation_get("help.args.erase_state"),
)
### v conf stuff v ### v conf stuff v
argumentparser.add_argument( argumentparser.add_argument(
"-d", "-d",
@ -98,324 +86,302 @@ def main():
) )
args = argumentparser.parse_args() 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 ## exec
if (args.show_version):
_sys.stdout.write(version + "\n") ### setup translation for the second time
else: if (args.language is not None):
### setup translation for the second time translation_initialize("en", args.language)
if (args.language is not None):
translation_initialize("en", args.language) ### load check kind implementations
check_kind_implementations = {
### load check kind implementations "script": implementation_check_kind_script(),
check_kind_implementations = { "file_state": implementation_check_kind_file_state(),
"script": implementation_check_kind_script(), "tls_certificate": implementation_check_kind_tls_certificate(),
"file_state": implementation_check_kind_file_state(), "http_request": implementation_check_kind_http_request(),
"tls_certificate": implementation_check_kind_tls_certificate(), "generic_remote" : implementation_check_kind_generic_remote(),
"http_request": implementation_check_kind_http_request(), }
"generic_remote" : implementation_check_kind_generic_remote(),
} ### load notification channel implementations
notification_channel_implementations = {
### load notification channel implementations "console": implementation_notification_channel_console(),
notification_channel_implementations = { "email": implementation_notification_channel_email(),
"console": implementation_notification_channel_console(), "libnotify": implementation_notification_channel_libnotify(),
"email": implementation_notification_channel_email(), }
"libnotify": implementation_notification_channel_libnotify(),
} if (args.show_schema):
_sys.stdout.write(
if (args.show_schema): _json.dumps(
_sys.stdout.write( order_schema_root(
_json.dumps( check_kind_implementations,
order_schema_root( notification_channel_implementations
check_kind_implementations, ),
notification_channel_implementations indent = "\t"
),
indent = "\t"
)
+
"\n"
) )
+
"\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: else:
### vars _sys.stderr.write(
if (not _os.path.exists(args.order_path)): 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( _sys.stderr.write(
string_coin( string_coin(
"{{message}}\n", "[error] {{message}} ({{path}})\n",
{ {
"message": translation_get( "message": translation_get("misc.still_running"),
"misc.order_file_not_found", "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",
{ {
"path": args.order_path, "count": ("%u" % result.rowcount),
} }
), ),
} }
) )
) )
_sys.exit(1)
else:
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": _hashlib.sha256(_os.path.abspath(args.order_path).encode("ascii")).hexdigest()[:8]
}
)
)
)
### get order data file_write(args.mutex_path, "", {"append": True})
order = order_load(
check_kind_implementations,
notification_channel_implementations,
_os.path.abspath(args.order_path)
)
if (args.expose_full_order): ### iterate through checks
_sys.stdout.write(_json.dumps(order, indent = "\t") + "\n") for check_data in order["checks"]:
_sys.exit(1) if (not check_data["active"]):
else: pass
_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: else:
### setup database ### get old state and examine whether the check shall be executed
sqlite_query_set( rows1 = sqlite_query_get(
database_path, 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);", "SELECT MAX(timestamp) FROM results WHERE (check_name = :check_name) AND (notification_sent = TRUE);",
{}
)
### 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), "check_name": check_data["name"],
"erase_state": args.erase_state,
} }
) )
_sys.stderr.write( rows2 = sqlite_query_get(
string_coin( database_path,
"[info] {{text}}\n", "SELECT timestamp, condition, notification_sent FROM results WHERE (check_name = :check_name) ORDER BY timestamp DESC LIMIT :limit;",
{ {
"text": translation_get( "check_name": check_data["name"],
"misc.cleanup_info", "limit": (check_data["threshold"] + 1),
{ }
"count": ("%u" % result.rowcount), )
} 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):
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
rows = 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(rows) <= 0):
old_item_state = None
else:
last_notification_timestamp = None
count = 1
for row in rows[1:]:
if (row[1] == rows[0][1]):
count += 1
else:
break
if (count > check_data["threshold"]):
count = None
else:
pass
for row in rows:
if (row[2]):
last_notification_timestamp = row[0]
break
else:
pass
old_item_state = {
"timestamp": rows[0][0],
"condition": condition_decode(rows[0][1]),
"count": count,
"last_notification_timestamp": last_notification_timestamp,
}
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.exists(args.mutex_path)):
pass pass
else: else:
_os.remove(args.mutex_path) _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() main()