[add] notification-channel "libnotify" [add] notification-channel "file_touch" [add] check-kind "file_timestamp" [mod] threshold + annoy

This commit is contained in:
Christian Fraß 2022-11-30 10:26:27 +01:00
parent f7e3357662
commit 2c2af6bed9
12 changed files with 658 additions and 300 deletions

View file

@ -1,177 +1,38 @@
{ {
"$defs": { "$defs": {
"active": { "active": {
"type": "boolean" "type": "boolean",
"default": true
},
"threshold": {
"description": "how often a condition has to occur in order to be reported",
"type": "integer",
"minimum": 1,
"default": 3
},
"annoy": {
"description": "whether notifications shall be kept sending after the threshold has been surpassed",
"type": "boolean",
"default": false
}, },
"schedule": { "schedule": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"regular_interval": { "regular_interval": {
"description": "in seconds", "description": "in seconds or as text: minute, hour, day, week",
"type": "integer" "type": ["integer", "string"]
}, },
"attentive_interval": { "attentive_interval": {
"description": "in seconds", "description": "in seconds or as text: minute, hour, day, week",
"type": "integer" "type": ["integer", "string"]
} }
}, },
"required": [ "required": [
"regular_interval" "regular_interval"
] ]
}, },
"notifications": { "check_kind_test": {
"type": "array",
"item": {
"anyOf": [
{
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"type": "string",
"const": "console"
},
"parameters": {
"type": "object",
"additionalProperties": false,
"properties": {
},
"required": [
]
}
},
"required": [
"kind",
"parameters"
]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"type": "string",
"const": "email"
},
"parameters": {
"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",
"item": {
"type": "string"
}
},
"tags": {
"description": "list of strings, which will be placed in the e-mail subject",
"type": "array",
"item": {
"type": "string"
},
"default": []
}
},
"required": [
"access",
"sender",
"receivers"
]
}
},
"required": [
"kind",
"parameters"
]
}
]
},
"default": [
{
"kind": "console",
"parameters": {
}
}
]
}
},
"type": "object",
"additionalProperties": false,
"properties": {
"defaults": {
"description": "default values for checks",
"type": "object",
"additionalProperties": false,
"properties": {
"active": {
"$ref": "#/$defs/active"
},
"schedule": {
"$ref": "#/$defs/schedule"
},
"notifications": {
"$ref": "#/$defs/notifications"
}
},
"required": [
]
},
"checks": {
"type": "object",
"additionalProperties": {
"allOf": [
{
"description": "should represent a specific check",
"type": "object",
"additionalProperties": false,
"properties": {
"title": {
"type": "string"
},
"active": {
"$ref": "#/$defs/active"
},
"schedule": {
"$ref": "#/$defs/schedule"
},
"notifications": {
"$ref": "#/$defs/notifications"
}
},
"required": [
]
},
{
"anyOf": [
{
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
@ -207,7 +68,7 @@
"parameters" "parameters"
] ]
}, },
{ "check_kind_script": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
@ -239,7 +100,65 @@
"parameters" "parameters"
] ]
}, },
{ "check_kind_file_timestamp": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"type": "string",
"const": "file_timestamp"
},
"parameters": {
"type": "object",
"additionalProperties": false,
"properties": {
"path": {
"type": "string"
},
"warning_age": {
"type": "integer",
"exclusiveMinimum": 0,
"default": 3600
},
"critical_age": {
"type": "integer",
"exclusiveMinimum": 0,
"default": 86400
},
"condition_on_missing": {
"description": "which condition to report if file is missing",
"type": "string",
"enum": [
"unknown",
"ok",
"warning",
"critical"
],
"default": "warning"
},
"condition_on_implausible": {
"description": "which condition to report if the age is negative, i.e. the file is apparently from the future",
"type": "string",
"enum": [
"unknown",
"ok",
"warning",
"critical"
],
"default": "warning"
}
},
"required": [
"path"
]
}
},
"required": [
"kind",
"parameters"
]
},
"check_kind_http_request": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
@ -317,6 +236,241 @@
"kind", "kind",
"parameters" "parameters"
] ]
},
"notification_channel_console": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"type": "string",
"const": "console"
},
"parameters": {
"type": "object",
"additionalProperties": false,
"properties": {
},
"required": [
]
}
},
"required": [
"kind",
"parameters"
]
},
"notification_channel_file_touch": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"type": "string",
"const": "file_touch"
},
"parameters": {
"type": "object",
"additionalProperties": false,
"properties": {
"path": {
"type": "string"
}
},
"required": [
"path"
]
}
},
"required": [
"kind",
"parameters"
]
},
"notification_channel_libnotify": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"type": "string",
"const": "libnotify"
},
"parameters": {
"type": "object",
"additionalProperties": false,
"properties": {
"icon": {
"type": "string"
}
},
"required": [
]
}
},
"required": [
"kind",
"parameters"
]
},
"notification_channel_email": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"type": "string",
"const": "email"
},
"parameters": {
"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",
"item": {
"type": "string"
}
},
"tags": {
"description": "list of strings, which will be placed in the e-mail subject",
"type": "array",
"item": {
"type": "string"
},
"default": []
}
},
"required": [
"access",
"sender",
"receivers"
]
}
},
"required": [
"kind",
"parameters"
]
},
"notifications": {
"type": "array",
"item": {
"anyOf": [
{
"$ref": "#/$defs/notification_channel_console"
},
{
"$ref": "#/$defs/notification_channel_file_touch"
},
{
"$ref": "#/$defs/notification_channel_email"
}
]
},
"default": [
{
"kind": "console",
"parameters": {
}
}
]
}
},
"type": "object",
"additionalProperties": false,
"properties": {
"defaults": {
"description": "default values for checks",
"type": "object",
"additionalProperties": false,
"properties": {
"active": {
"$ref": "#/$defs/active"
},
"threshold": {
"$ref": "#/$defs/threshold"
},
"annoy": {
"$ref": "#/$defs/annoy"
},
"schedule": {
"$ref": "#/$defs/schedule"
},
"notifications": {
"$ref": "#/$defs/notifications"
}
},
"required": [
]
},
"checks": {
"type": "object",
"additionalProperties": {
"allOf": [
{
"description": "should represent a specific check",
"type": "object",
"additionalProperties": false,
"properties": {
"title": {
"type": "string"
},
"active": {
"$ref": "#/$defs/active"
},
"threshold": {
"$ref": "#/$defs/threshold"
},
"annoy": {
"$ref": "#/$defs/annoy"
},
"schedule": {
"$ref": "#/$defs/schedule"
},
"notifications": {
"$ref": "#/$defs/notifications"
}
},
"required": [
]
},
{
"anyOf": [
{
"$ref": "#/$defs/check_kind_test"
},
{
"$ref": "#/$defs/check_kind_script"
},
{
"$ref": "#/$defs/check_kind_file_timestamp"
},
{
"$ref": "#/$defs/check_kind_http_request"
} }
] ]
} }

View file

@ -0,0 +1,65 @@
class implementation_check_kind_file_timestamp(interface_check_kind):
'''
[implementation]
'''
def normalize_conf_node(self, node):
if ("path" not in node):
raise ValueError("missing mandatory field 'path'")
else:
return dict_merge(
{
"warning_age": (60 * 60),
"critical_age": (60 * 60 * 24),
"condition_on_missing": condition_encode(enum_condition.warning),
"condition_on_implausible": condition_encode(enum_condition.warning),
},
node
)
'''
[implementation]
'''
def run(self, parameters):
if (not _os.path.exists(parameters["path"])):
return {
"condition": condition_decode(parameters["condition_on_missing"]),
"output": "file is missing"
}
else:
result = _os.stat(parameters["path"])
timestamp = get_current_timestamp()
age = (timestamp - result.st_atime)
if (age < 0):
return {
"condition": condition_decode(parameters["condition_on_implausible"]),
"output": string_coin(
"file is apparently from the future; timestamp of checking instance: {{timestamp_this}}; timestamp of file: {{timestamp_that}} (age in seconds: {{age}})",
{
"timestamp_this": timestamp,
"timestamp_that": result.st_atime,
"age": ("%u" % age),
}
),
}
else:
if ((age > 0) and (age <= parameters["warning_age"])):
condition = enum_condition.ok
elif ((age > parameters["warning_age"]) and (age <= parameters["critical_age"])):
condition = enum_condition.warning
elif (age > parameters["critical_age"]):
condition = enum_condition.critical
else:
raise ValueError("impossible state")
return {
"condition": condition,
"output": string_coin(
"age in seconds: {{age}}",
{
"age": ("%u" % age),
}
),
}

View file

@ -1,24 +0,0 @@
class implementation_check_kind_test(interface_check_kind):
'''
[implementation]
'''
def normalize_conf_node(self, node):
return dict_merge(
{
"condition": condition_encode(enum_condition.warning),
"output": "",
},
node
)
'''
[implementation]
'''
def run(self, parameters):
return {
"condition": condition_decode(parameters["condition"]),
"output": parameters["output"]
}

View file

@ -14,6 +14,55 @@ def state_decode(state_encoded):
} }
def conf_normalize_interval(interval_raw):
if (type(interval_raw) == int):
return interval_raw
elif (type(interval_raw) == str):
if (interval_raw == "minute"):
return (60)
elif (interval_raw == "hour"):
return (60 * 60)
elif (interval_raw == "day"):
return (60 * 60 * 24)
elif (interval_raw == "week"):
return (60 * 60 * 24 * 7)
else:
raise ValueError("invalid string interval value: %s" % interval_raw)
else:
raise ValueError("invalid type for interval value")
def conf_normalize_schedule(node):
node_ = dict_merge(
{
"regular_interval": (60 * 60),
"attentive_interval": (60 * 2),
},
node
)
return {
"regular_interval": conf_normalize_interval(node["regular_interval"]),
"attentive_interval": conf_normalize_interval(node["attentive_interval"]),
}
def conf_normalize_defaults(node):
return dict_merge(
{
"active": True,
"threshold": 3,
"annoy": False,
"schedule": {
"regular_interval": (60 * 60),
"attentive_interval": (60 * 2),
},
"notifications": [
],
},
node
)
def conf_normalize_check(check_kind_implementations, defaults, name, node): def conf_normalize_check(check_kind_implementations, defaults, name, node):
if ("kind" not in node): if ("kind" not in node):
raise ValueError("missing mandatory 'check' field 'kind'") raise ValueError("missing mandatory 'check' field 'kind'")
@ -25,6 +74,8 @@ def conf_normalize_check(check_kind_implementations, defaults, name, node):
{ {
"title": name, "title": name,
"active": defaults["active"], "active": defaults["active"],
"threshold": defaults["threshold"],
"annoy": defaults["annoy"],
"schedule": defaults["schedule"], "schedule": defaults["schedule"],
"notifications": defaults["notifications"], "notifications": defaults["notifications"],
"parameters": {}, "parameters": {},
@ -34,28 +85,15 @@ def conf_normalize_check(check_kind_implementations, defaults, name, node):
return { return {
"title": node_["title"], "title": node_["title"],
"active": node_["active"], "active": node_["active"],
"schedule": node_["schedule"], "threshold": node_["threshold"],
"annoy": node_["annoy"],
"schedule": conf_normalize_schedule(node_["schedule"]),
"notifications": node_["notifications"], "notifications": node_["notifications"],
"kind": node_["kind"], "kind": node_["kind"],
"parameters": check_kind_implementations[node_["kind"]].normalize_conf_node(node_["parameters"]), "parameters": check_kind_implementations[node_["kind"]].normalize_conf_node(node_["parameters"]),
} }
def conf_normalize_defaults(node):
return dict_merge(
{
"active": True,
"schedule": {
"regular_interval": (60 * 60),
"attentive_interval": (60 * 2),
},
"notifications": [
],
},
node
)
def conf_normalize_root(check_kind_implementations, node): def conf_normalize_root(check_kind_implementations, node):
return dict( return dict(
map( map(
@ -105,23 +143,6 @@ def main():
dest = "erase_state", dest = "erase_state",
help = "whether the state shall be deleted on start; this will cause that all checks are executed" help = "whether the state shall be deleted on start; this will cause that all checks are executed"
) )
argumentparser.add_argument(
"-t",
"--threshold",
type = int,
default = 3,
dest = "threshold",
metavar = "<threshold>",
help = "how often a condition has to occur in order to be reported"
)
argumentparser.add_argument(
"-k",
"--keep-notifying",
action = "store_true",
default = False,
dest = "keep_notifying",
help = "whether notifications shall be kept sending after the threshold has been surpassed"
)
argumentparser.add_argument( argumentparser.add_argument(
"-e", "-e",
"--expose-full-conf", "--expose-full-conf",
@ -149,15 +170,17 @@ def main():
### load check kind implementations ### load check kind implementations
check_kind_implementations = { check_kind_implementations = {
"test": implementation_check_kind_test(),
"script": implementation_check_kind_script(), "script": implementation_check_kind_script(),
"file_timestamp": implementation_check_kind_file_timestamp(),
"http_request": implementation_check_kind_http_request(), "http_request": implementation_check_kind_http_request(),
} }
### load notification channel implementations ### load notification channel implementations
notification_channel_implementations = { notification_channel_implementations = {
"console": implementation_notification_channel_console(), "console": implementation_notification_channel_console(),
"file_touch": implementation_notification_channel_file_touch(),
"email": implementation_notification_channel_email(), "email": implementation_notification_channel_email(),
"libnotify": implementation_notification_channel_libnotify(),
} }
### get configuration data ### get configuration data
@ -194,16 +217,14 @@ def main():
or or
(old_item_state["condition"] != enum_condition.ok) (old_item_state["condition"] != enum_condition.ok)
or or
((timestamp - old_item_state["timestamp"]) >= check_data["schedule"]["regular_interval"])
or
( (
(old_item_state["count"] is not None) (old_item_state["count"] is not None)
and and
(
((timestamp - old_item_state["timestamp"]) >= check_data["schedule"]["regular_interval"])
or
((timestamp - old_item_state["timestamp"]) >= check_data["schedule"]["attentive_interval"]) ((timestamp - old_item_state["timestamp"]) >= check_data["schedule"]["attentive_interval"])
) )
) )
)
if (not due): if (not due):
pass pass
else: else:
@ -233,7 +254,7 @@ def main():
if ( if (
(old_item_state["count"] is not None) (old_item_state["count"] is not None)
and and
((old_item_state["count"] + 1) <= args.threshold) ((old_item_state["count"] + 1) <= check_data["threshold"])
) else ) else
None None
) )
@ -247,13 +268,13 @@ def main():
( (
(new_item_state["count"] is not None) (new_item_state["count"] is not None)
and and
(new_item_state["count"] == args.threshold) (new_item_state["count"] == check_data["threshold"])
) )
or or
( (
(new_item_state["count"] is None) (new_item_state["count"] is None)
and and
args.keep_notifying check_data["annoy"]
) )
): ):
for notification in check_data["notifications"]: for notification in check_data["notifications"]:

View file

@ -1,5 +1,9 @@
class interface_notification_channel(object): class interface_notification_channel(object):
def normalize_conf_node(self, node):
raise NotImplementedError
def notify(self, parameters, name, data, state, output): def notify(self, parameters, name, data, state, output):
raise NotImplementedError raise NotImplementedError

View file

@ -1,5 +1,16 @@
class implementation_notification_channel_console(interface_notification_channel): class implementation_notification_channel_console(interface_notification_channel):
'''
[implementation]
'''
def normalize_conf_node(self, node):
return dict_merge(
{
},
node
)
''' '''
[implementation] [implementation]
''' '''

View file

@ -1,5 +1,16 @@
class implementation_notification_channel_email(interface_notification_channel): class implementation_notification_channel_email(interface_notification_channel):
'''
[implementation]
'''
def normalize_conf_node(self, node):
return dict_merge(
{
},
node
)
''' '''
[implementation] [implementation]
''' '''

View file

@ -0,0 +1,19 @@
class implementation_notification_channel_file_touch(interface_notification_channel):
'''
[implementation]
'''
def normalize_conf_node(self, node):
return dict_merge(
{
},
node
)
'''
[implementation]
'''
def notify(self, parameters, name, data, state, output):
_os.path.touch(parameters["path"])

View file

@ -0,0 +1,81 @@
class implementation_notification_channel_libnotify(interface_notification_channel):
'''
[implementation]
'''
def normalize_conf_node(self, node):
return dict_merge(
{
"icon": None,
},
node
)
'''
[implementation]
'''
def notify(self, parameters, name, data, state, output):
def condition_translate(condition):
if (condition == enum_condition.unknown):
return "normal"
elif (condition == enum_condition.ok):
return "low"
elif (condition == enum_condition.warning):
return "normal"
elif (condition == enum_condition.critical):
return "critical"
else:
raise ValueError("impossible condition")
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_encode(state["condition"]).upper(),
}
)
)
## body
parts.append(
"(no infos)"
if (output == "") else
output
)
_subprocess.run(parts)

View file

@ -3,8 +3,10 @@
}, },
"checks": { "checks": {
"test": { "test": {
"threshold": 1,
"annoy": true,
"schedule": { "schedule": {
"regular_interval": 12, "regular_interval": 60,
"attentive_interval": 5 "attentive_interval": 5
}, },
"notifications": [ "notifications": [
@ -12,10 +14,19 @@
"kind": "console", "kind": "console",
"parameters": { "parameters": {
} }
},
{
"kind": "libnotify",
"parameters": {
"icon": "/home/fenris/bilder/zeug/heimdall.png"
}
} }
], ],
"kind": "test", "kind": "file_timestamp",
"parameters": { "parameters": {
"path": "/tmp/test",
"warning_age": 60,
"critical_age": 120
} }
} }
} }

View file

@ -1,7 +1,10 @@
- parallele Zugriffe auf die Zustands-Datei verhindern - parallele Zugriffe auf die Zustands-Datei verhindern
- fehlertolerantere Implementierung - fehlertolerantere Implementierung
- Selbst-Test - Selbst-Test
- Matrix als Benachrichtigungs-Kanal - Benachrichtigungs-Kanäle:
- Matrix
- JSON-Schema für Konfiguration von Programm erzeugen lassen - JSON-Schema für Konfiguration von Programm erzeugen lassen
- Möglichkeit dauerhaft laufen zulassen (evtl. als systemd-Dienst) - Möglichkeit dauerhaft laufen zulassen (evtl. als systemd-Dienst)
- Versionierung - Versionierung
- Umbenennung: `output` zu `info`
- Test-Routinen

View file

@ -6,12 +6,14 @@ cat \
source/packages.py \ source/packages.py \
source/lib.py \ source/lib.py \
source/check_kinds/_interface.py \ source/check_kinds/_interface.py \
source/check_kinds/test.py \
source/check_kinds/script.py \ source/check_kinds/script.py \
source/check_kinds/file_timestamp.py \
source/check_kinds/http_request.py \ source/check_kinds/http_request.py \
source/notification_channels/_interface.py \ source/notification_channels/_interface.py \
source/notification_channels/console.py \ source/notification_channels/console.py \
source/notification_channels/file_touch.py \
source/notification_channels/email.py \ source/notification_channels/email.py \
source/notification_channels/libnotify.py \
source/main.py \ source/main.py \
>> build/heimdall >> build/heimdall
chmod +x build/heimdall chmod +x build/heimdall