This commit is contained in:
Christian Fraß 2023-06-19 18:44:32 +02:00
parent 0f6116ddcd
commit 525e59f7cf
9 changed files with 2 additions and 925 deletions

View file

@ -1,13 +0,0 @@
class interface_notification_channel(object):
def parameters_schema(self):
raise NotImplementedError
def normalize_order_node(self, node):
raise NotImplementedError
def notify(self, parameters, name, data, state, info):
raise NotImplementedError

View file

@ -1,41 +0,0 @@
class implementation_notification_channel_console(interface_notification_channel):
'''
[implementation]
'''
def parameters_schema(self):
return {
"type": "object",
"additionalProperties": False,
"properties": {
},
"required": [
]
}
'''
[implementation]
'''
def normalize_order_node(self, node):
return dict_merge(
{
},
node
)
'''
[implementation]
'''
def notify(self, parameters, name, data, state, info):
_sys.stdout.write(
string_coin(
"[{{title}}] <{{condition}}> {{info}}\n",
{
"title": data["title"],
"condition": condition_show(state["condition"]),
"info": _json.dumps(info, indent = "\t", ensure_ascii = False),
}
)
)

View file

@ -1,16 +0,0 @@
class interface_check_kind(object):
def parameters_schema(self):
raise NotImplementedError
def normalize_order_node(self, node):
raise NotImplementedError
'''
return record<condition:enum_condition,info:any>
'''
def run(self, parameters):
raise NotImplementedError

View file

@ -1,270 +0,0 @@
class implementation_check_kind_http_request(interface_check_kind):
'''
[implementation]
'''
def parameters_schema(self):
return {
"type": "object",
"additionalProperties": False,
"properties": {
"request": {
"type": "object",
"additionalProperties": False,
"properties": {
"target": {
"description": "URL",
"type": "string"
},
"method": {
"type": "string",
"enum": [
"GET",
"POST"
],
"default": "GET"
}
},
"required": [
"target"
]
},
"timeout": {
"description": "maximum allowed execution time in seconds",
"type": "float",
"default": 5.0
},
"follow_redirects": {
"description": "whether redirect instructions in responses shall be followend instead of being exposed as result",
"type": "boolean",
"default": False
},
"response": {
"type": "object",
"additionalProperties": False,
"properties": {
"status_code": {
"description": "checks whether the response status code is this",
"type": ["null", "integer"],
"default": 200
},
"headers": {
"description": "conjunctively checks header key-value pairs",
"type": "object",
"additionalProperties": {
"description": "header value",
"type": "string"
},
"properties": {
},
"required": [
],
"default": {}
},
"body_part": {
"description": "checks whether the response body contains this string",
"type": "string"
}
},
"required": [
]
},
"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": [
"request",
]
}
'''
[implementation]
'''
def normalize_order_node(self, node):
version = (
"v1"
if (not ("critical" in node)) else
"v2"
)
if (version == "v1"):
node_ = dict_merge(
{
"request": {
"method": "GET"
},
"timeout": 5.0,
"follow_redirects": False,
"response": {
"status_code": 200
},
"strict": True,
},
node,
True
)
allowed_methods = set(["GET", "POST"])
if (node_["request"]["method"] not in allowed_methods):
raise ValueError("invalid HTTP request method: %s" % node_["request"]["method"])
else:
return {
"request": node_["request"],
"timeout": node_["timeout"],
"follow_redirects": node_["follow_redirects"],
"response": node_["response"],
"critical": node_["strict"],
}
elif (version == "v2"):
node_ = dict_merge(
{
"request": {
"method": "GET"
},
"timeout": 5.0,
"follow_redirects": False,
"response": {
"status_code": 200
},
"critical": True,
},
node,
True
)
allowed_methods = set(["GET", "POST"])
if (node_["request"]["method"] not in allowed_methods):
raise ValueError("invalid HTTP request method: %s" % node_["request"]["method"])
else:
return node_
else:
raise ValueError("unhandled")
'''
[implementation]
'''
def run(self, parameters):
if (parameters["request"]["method"] == "GET"):
try:
response = _requests.get(
parameters["request"]["target"],
timeout = parameters["timeout"],
allow_redirects = parameters["follow_redirects"]
)
error = None
except Exception as error_:
error = error_
response = None
elif (parameters["request"]["method"] == "POST"):
try:
response = _requests.post(
parameters["request"]["target"],
timeout = parameters["timeout"],
allow_redirects = parameters["follow_redirects"]
)
error = None
except Exception as error_:
error = error_
response = None
else:
raise ValueError("impossible")
if (response is None):
return {
"condition": (
enum_condition.critical
if parameters["strict"] else
enum_condition.concerning
),
"info": {
"request": parameters["request"],
"faults": [
translation_get("checks.http_request.request_failed"),
],
}
}
else:
faults = []
for (key, value, ) in parameters["response"].items():
if (key == "status_code"):
if ((value is None) or (response.status_code == value)):
pass
else:
faults.append(
translation_get(
"checks.http_request.status_code_mismatch",
{
"status_code_actual": ("%u" % response.status_code),
"status_code_expected": ("%u" % value),
}
)
)
elif (key == "headers"):
for (header_key, header_value, ) in value.items():
if (not (header_key in response.headers)):
faults.append(
translation_get(
"checks.http_request.header_missing",
{
"key": header_key,
"value_expected": header_value,
}
)
)
else:
if (not (response.headers[header_key] == header_value)):
faults.append(
translation_get(
"checks.http_request.header_value_mismatch",
{
"key": header_key,
"value_actual": response.headers[header_key],
"value_expected": header_value,
}
)
)
else:
pass
elif (key == "body_part"):
if (response.text.find(value) >= 0):
pass
else:
faults.append(
translation_get(
"checks.http_request.body_misses_part",
{
"part": value,
}
)
)
else:
raise ValueError("unhandled")
return {
"condition": (
enum_condition.ok
if (len(faults) <= 0) else
(
enum_condition.critical
if parameters["strict"] else
enum_condition.concerning
)
),
"info": {
"request": parameters["request"],
"response": {
"status_code": response.status_code,
"headers": dict(map(lambda pair: pair, response.headers.items())),
# "body": response.text,
},
"faults": faults,
}
}

View file

@ -1,39 +0,0 @@
class enum_condition(_enum.Enum):
unknown = 0
ok = 1
concerning = 2
critical = 3
'''
converts a condition to a human readable string
'''
def condition_show(condition):
return translation_get(
{
enum_condition.unknown: "conditions.unknown",
enum_condition.ok: "conditions.ok",
enum_condition.concerning: "conditions.concerning",
enum_condition.critical: "conditions.critical",
}[condition]
)
def condition_encode(condition):
return {
enum_condition.unknown: "unknown",
enum_condition.ok: "ok",
enum_condition.concerning: "concerning",
enum_condition.critical: "critical",
}[condition]
def condition_decode(condition_encoded):
return {
"unknown": enum_condition.unknown,
"ok": enum_condition.ok,
"warning": enum_condition.concerning, # deprecated
"concerning": enum_condition.concerning,
"critical": enum_condition.critical,
}[condition_encoded]

View file

@ -1,52 +0,0 @@
translation_language_fallback = None
translation_language_shall = None
def translation_initialize(language_fallback, language_shall):
global translation_language_fallback
global translation_language_shall
translation_language_fallback = language_fallback
translation_language_shall = language_shall
def translation_get(key, arguments = None):
global translation_language_fallback
global translation_language_shall
global localization_data
if (arguments is None):
arguments = {}
languages = list(
filter(
lambda language: (language is not None),
[translation_language_shall, translation_language_fallback]
)
)
for language in languages:
if (language not in localization_data):
_sys.stderr.write(
string_coin(
"[notice] missing localization data for language '{{language}}'\n",
{
"language": language,
}
)
)
else:
if (key not in localization_data[language]):
_sys.stderr.write(
string_coin(
"[notice] missing translation in language '{{language}}' for key '{{key}}'\n",
{
"language": language,
"key": key,
}
)
)
else:
return string_coin(
localization_data[language][key],
arguments
)
return ("{{%s}}" % key)

View file

@ -1,476 +0,0 @@
def order_schema_active(
):
return {
"description": "whether the check shall be executed",
"type": "boolean",
"default": True,
}
def order_schema_threshold(
):
return {
"description": "how often a condition has to occur in order to be reported",
"type": "integer",
"minimum": 1,
"default": 3,
}
def order_schema_annoy(
):
return {
"description": "whether notifications about non-ok states shall be kept sending after the threshold has been surpassed",
"type": "boolean",
"default": False,
}
def order_schema_interval(
allow_null,
default
):
return {
"anyOf": [
{
"description": "in seconds",
"type": ("integer" if allow_null else ["null", "integer"]),
"exclusiveMinimum": 0,
},
{
"description": "as text",
"type": "string",
"enum": [
"minute",
"hour",
"day",
"week",
]
},
],
"default": default,
}
def order_schema_schedule(
):
return {
"type": "object",
"additionalProperties": False,
"properties": {
"regular_interval": order_schema_interval(False, (60 * 60)),
"attentive_interval": order_schema_interval(False, (60 * 2)),
"reminding_interval": order_schema_interval(True, (60 * 60 * 24)),
},
"required": [
],
}
def order_schema_notifications(
notification_channel_implementations
):
return {
"type": "array",
"items": {
"anyOf": list(
map(
lambda pair: {
"title": ("notification channel '%s'" % pair[0]),
"type": "object",
"unevaluatedProperties": False,
"properties": {
"kind": {
"type": "string",
"enum": [pair[0]]
},
"parameters": pair[1].parameters_schema(),
},
"required": [
"kind",
"parameters"
],
},
notification_channel_implementations.items()
)
)
},
"default": [
{
"kind": "console",
"parameters": {
}
},
],
}
def order_schema_root(
check_kind_implementations,
notification_channel_implementations
):
return {
"type": "object",
"additionalProperties": False,
"properties": {
"defaults": {
"description": "default values for checks",
"type": "object",
"additionalProperties": False,
"properties": {
"active": order_schema_active(),
"threshold": order_schema_threshold(),
"annoy": order_schema_annoy(),
"schedule": order_schema_schedule(),
"notifications": order_schema_notifications(notification_channel_implementations),
},
"required": [
],
},
"includes": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "list of relative or absolute paths to other hmdl files on the local machine, which shall be subsumed in the overall monitoring task"
},
"checks": {
"type": "array",
"items": {
"allOf": [
{
"description": "should represent a specific check",
"type": "object",
"unevaluatedProperties": False,
"properties": {
"name": {
"type": "string"
},
"title": {
"type": "string"
},
"active": order_schema_active(),
"threshold": order_schema_threshold(),
"annoy": order_schema_annoy(),
"schedule": order_schema_schedule(),
"notifications": order_schema_notifications(notification_channel_implementations),
},
"required": [
"name",
],
},
{
"anyOf": list(
map(
lambda pair: {
"title": ("check '%s'" % pair[0]),
"type": "object",
"unevaluatedProperties": False,
"properties": {
"kind": {
"type": "string",
"enum": [pair[0]]
},
"parameters": pair[1].parameters_schema(),
"custom": {
"description": "custom data, which shall be attached to notifications",
"default": None,
},
},
"required": [
"kind",
"parameters",
]
},
check_kind_implementations.items()
)
),
},
]
}
}
},
"required": [
]
}
def order_normalize_interval(
interval_raw
):
if (interval_raw is None):
return None
else:
if (type(interval_raw) == int):
return interval_raw
elif (type(interval_raw) == str):
map_ = {
"minute": (60),
"hour": (60 * 60),
"day": (60 * 60 * 24),
"week": (60 * 60 * 24 * 7),
}
if (interval_raw not in map_):
raise ValueError("invalid string interval value: %s" % interval_raw)
else:
return map_[interval_raw]
else:
raise ValueError("invalid type for interval value")
def order_normalize_schedule(
node
):
node_ = dict_merge(
{
"regular_interval": (60 * 60),
"attentive_interval": (60 * 2),
"reminding_interval": (60 * 60 * 24),
},
node
)
return {
"regular_interval": order_normalize_interval(node_["regular_interval"]),
"attentive_interval": order_normalize_interval(node_["attentive_interval"]),
"reminding_interval": order_normalize_interval(node_["reminding_interval"]),
}
def order_normalize_notification(
notification_channel_implementations,
node
):
if (node["kind"] not in notification_channel_implementations):
raise ValueError("invalid notification kind: %s" % notification["kind"])
else:
return {
"kind": node["kind"],
"parameters": notification_channel_implementations[node["kind"]].normalize_order_node(node["parameters"]),
}
def order_normalize_defaults(
notification_channel_implementations,
node
):
node_ = dict_merge(
{
"active": True,
"threshold": 3,
"annoy": False,
"schedule": {
"regular_interval": (60 * 60),
"attentive_interval": (60 * 2),
"reminding_interval": (60 * 60 * 24),
},
"notifications": [
],
},
node
)
return {
"active": node_["active"],
"threshold": node_["threshold"],
"annoy": node_["annoy"],
"schedule": node_["schedule"],
"notifications": list(
map(
lambda x: order_normalize_notification(notification_channel_implementations, x),
node_["notifications"]
)
),
}
def order_normalize_check(
check_kind_implementations,
notification_channel_implementations,
defaults,
node
):
if ("name" not in node):
raise ValueError("missing mandatory field in 'check' node: 'name'")
else:
if ("kind" not in node):
raise ValueError("missing mandatory field in 'check' node: 'kind'")
else:
if (node["kind"] not in check_kind_implementations):
raise ValueError("invalid check kind: '%s'" % node["kind"])
else:
node_ = dict_merge(
dict_merge(
defaults,
{
"title": node["name"],
"parameters": {},
"custom": None,
},
),
node
)
node__ = {}
if True:
node__["name"] = node_["name"]
if True:
node__["title"] = node_["title"]
if ("active" in node_):
node__["active"] = node_["active"]
if ("threshold" in node_):
node__["threshold"] = node_["threshold"]
if ("annoy" in node_):
node__["annoy"] = node_["annoy"]
if ("schedule" in node_):
node__["schedule"] = order_normalize_schedule(node_["schedule"])
if ("notifications" in node_):
node__["notifications"] = list(
map(
lambda x: order_normalize_notification(notification_channel_implementations, x),
node_["notifications"]
)
)
if ("kind" in node_):
node__["kind"] = node_["kind"]
if True:
node__["parameters"] = check_kind_implementations[node_["kind"]].normalize_order_node(node_["parameters"])
if ("custom" in node_):
node__["custom"] = node_["custom"]
return node__
def order_normalize_root(
check_kind_implementations,
notification_channel_implementations,
node,
options = None
):
options = dict_merge(
{
"use_implicit_default_values": True,
},
({} if (options is None) else options)
)
counts = {}
checks_raw = (
node["checks"]
if ("checks" in node) else
[]
)
for node_ in checks_raw:
if (node_["name"] not in counts):
counts[node_["name"]] = 0
counts[node_["name"]] += 1
fails = list(filter(lambda pair: (pair[1] > 1), counts.items()))
if (len(fails) > 0):
raise ValueError(
string_coin(
"ambiguous check names: {{names}}",
{
"names": ",".join(map(lambda pair: pair[0], fails)),
}
)
)
else:
defaults_raw = (
node["defaults"]
if ("defaults" in node) else
{}
)
defaults = (
order_normalize_defaults(
notification_channel_implementations,
defaults_raw
)
if options["use_implicit_default_values"] else
defaults_raw
)
includes = (
node["includes"]
if ("includes" in node) else
[]
)
return {
"defaults": defaults,
"includes": includes,
"checks": list(
map(
lambda node_: order_normalize_check(
check_kind_implementations,
notification_channel_implementations,
defaults,
node_
),
checks_raw
)
)
}
def order_load(
check_kind_implementations,
notification_channel_implementations,
path,
options = None
):
options = dict_merge(
{
"root": True,
"already_included": set([]),
},
({} if (options is None) else options)
)
if (path in options["already_included"]):
raise ValueError("circular dependency detected")
else:
order_raw = _json.loads(file_read(path))
includes = (
order_raw["includes"]
if ("includes" in order_raw) else
[]
)
for index in range(len(includes)):
path_ = includes[index]
sub_order = order_load(
check_kind_implementations,
notification_channel_implementations,
(
path_
if _os.path.isabs(path_) else
_os.path.join(_os.path.dirname(path), path_)
),
{
"root": False,
"already_included": (options["already_included"] | {path})
}
)
if (not "checks" in order_raw):
order_raw["checks"] = []
order_raw["checks"].extend(
list(
map(
lambda check: dict_merge(
check,
{
"name": string_coin(
"{{prefix}}.{{original_name}}",
{
"prefix": _os.path.basename(path_).split(".")[0],
"original_name": check["name"],
}
),
}
),
sub_order["checks"]
)
)
)
order_raw["includes"] = []
return order_normalize_root(
check_kind_implementations,
notification_channel_implementations,
order_raw,
{
"use_implicit_default_values": options["root"],
}
)

View file

@ -1,17 +0,0 @@
import sys as _sys
import os as _os
import subprocess as _subprocess
import math as _math
import hashlib as _hashlib
import tempfile as _tempfile
import argparse as _argparse
import json as _json
import requests as _requests
import enum as _enum
import time as _time
import datetime as _datetime
import smtplib as _smtplib
from email.mime.text import MIMEText
import ssl as _ssl
import socket as _socket
import sqlite3 as _sqlite3

View file

@ -4,7 +4,8 @@ async function main(
// consts // consts
const version : string = "0.8"; const version : string = "0.8";
// init // init translations
// TODO: use env language
await lib_plankton.translate.initialize_promise( await lib_plankton.translate.initialize_promise(
{ {
"verbosity": 1, "verbosity": 1,