[mod] source

This commit is contained in:
Christian Fraß 2023-07-27 17:37:45 +02:00
parent e168077ef9
commit cf9c4f009d
18 changed files with 632 additions and 1202 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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()

View file

@ -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)

View file

@ -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
}
}

View file

@ -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

View file

@ -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 = "<order-path>",
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 = "<database-path>",
help = translation_get("help.args.database_path"),
)
argumentparser.add_argument(
"-m",
"--mutex-path",
type = str,
default = "/tmp/heimdall.lock",
dest = "mutex_path",
metavar = "<mutex-path>",
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 = "<language>",
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 = "<time-to-live>",
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()

View file

@ -1,3 +1,9 @@
/**
* @todo outsource
*/
declare var __dirname;
namespace _heimdall
{

View file

@ -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<type_result>(
(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<string> = result.stdout.split("\n").slice(-2)[0].split(" ");
const stuff : Array<string> = shell_exec_result.stdout.split("\n").slice(-2)[0].split(" ");
const data = {
"device": stuff[0],
"used": parseInt(stuff[2]),

View file

@ -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,
},

View file

@ -54,47 +54,36 @@ 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<type_result>(
(resolve, reject) => {
nm_child_process.spawnSync(
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;
if ((error !== null) || (shell_exec_result === null)) {
lib_plankton.log.notice(
lib_plankton.translate.get("checks.script.execution_failed"),
{
},
(result) => {
if (result.error) {
reject(result.error);
"error": String(error),
}
);
condition = _heimdall.enum_condition.unknown;
}
else {
resolve(
{
"return_code": result.status,
"stdout": result.stdout,
"stderr": result.stdin,
}
);
}
}
);
}
);
let condition : _heimdall.enum_condition;
switch (result.return_code) {
switch (shell_exec_result.return_code) {
default: {
lib_plankton.log.notice(
lib_plankton.translate.get("check_kind_script_invalid_return_code"),
lib_plankton.translate.get("checks.script.invalid_return_code"),
{
"return_code": result.return_code,
"return_code": shell_exec_result.return_code,
}
);
condition = _heimdall.enum_condition.unknown;
@ -117,10 +106,11 @@ namespace _heimdall.check_kinds.script
break;
}
}
}
return {
"condition": condition,
"info": {
"result": result,
"result": shell_exec_result,
},
};
}

View file

@ -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<type_schema>;
anyOf : Array<type_schema>;
default ?: any;
}
|
{
allOf ?: Array<type_schema>;
allOf : Array<type_schema>;
default ?: any;
}
)
);

View file

@ -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<string>
) : Promise<type_shell_exec_result>
{
const nm_child_process = require("child_process");
return new Promise<type_shell_exec_result>(
(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,
}
);
}
);
}
);
}
}

View file

@ -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<string, _heimdall.notification_kinds.type_notification_kind> = {
"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<string, _heimdall.check_kinds.type_check_kind> = {
"script": _heimdall.check_kinds.script.check_kind_implementation(),
@ -341,8 +344,7 @@ async function main(
// create mutex file
await lib_plankton.file.write(args["mutex_path"], "");
order.checks.forEach(
async (check) => {
for await (const check of order.checks) {
if (! check.active) {
// do nothing
}
@ -542,7 +544,6 @@ async function main(
}
}
}
);
// drop mutex file
await (

View file

@ -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),
}

View file

@ -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<void>
{
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<void>(undefined);
}
/**
*/
export function notification_kind_implementation(
) : type_notification_kind
{
return {
"parameters_schema": parameters_schema,
"normalize_order_node": normalize_order_node,
"notify": notify,
};
}
}

View file

@ -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<void>
{
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<string> = [];
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<void>(undefined);
}
/**
*/
export function notification_kind_implementation(
) : type_notification_kind
{
return {
"parameters_schema": parameters_schema,
"normalize_order_node": normalize_order_node,
"notify": notify,
};
}
}

View file

@ -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,
},