diff --git a/doc/hmdl.schema.json b/doc/hmdl.schema.json new file mode 100644 index 0000000..9f0aa70 --- /dev/null +++ b/doc/hmdl.schema.json @@ -0,0 +1,665 @@ +{ + "type": "object", + "additionalProperties": false, + "properties": { + "defaults": { + "description": "default values for checks", + "type": "object", + "additionalProperties": false, + "properties": { + "active": { + "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": { + "type": "object", + "additionalProperties": false, + "properties": { + "regular_interval": { + "description": "in seconds or as text", + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0 + }, + { + "type": "string", + "enum": [ + "minute", + "hour", + "day", + "week" + ] + } + ], + "default": 3600 + }, + "attentive_interval": { + "description": "in seconds or as text", + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0 + }, + { + "type": "string", + "enum": [ + "minute", + "hour", + "day", + "week" + ] + } + ], + "default": 120 + } + }, + "required": [] + }, + "notifications": { + "type": "array", + "items": { + "anyOf": [ + { + "title": "check kind 'console'", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "console" + ] + }, + "parameters": { + "type": "object", + "additionalProperties": false, + "properties": {}, + "required": [] + } + }, + "required": [ + "kind", + "parameters" + ] + }, + { + "title": "check kind 'file_touch'", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "file_touch" + ] + }, + "parameters": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] + } + }, + "required": [ + "kind", + "parameters" + ] + }, + { + "title": "check kind 'email'", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "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", + "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" + ] + } + }, + "required": [ + "kind", + "parameters" + ] + }, + { + "title": "check kind 'libnotify'", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "libnotify" + ] + }, + "parameters": { + "type": "object", + "additionalProperties": false, + "properties": { + "icon": { + "type": "string" + } + }, + "required": [] + } + }, + "required": [ + "kind", + "parameters" + ] + } + ] + }, + "default": [ + { + "kind": "console", + "parameters": {} + } + ] + } + }, + "required": [] + }, + "checks": { + "type": "array", + "items": { + "allOf": [ + { + "description": "should represent a specific check", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "name": { + "type": "string" + }, + "title": { + "type": "string" + }, + "active": { + "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": { + "type": "object", + "additionalProperties": false, + "properties": { + "regular_interval": { + "description": "in seconds or as text", + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0 + }, + { + "type": "string", + "enum": [ + "minute", + "hour", + "day", + "week" + ] + } + ], + "default": 3600 + }, + "attentive_interval": { + "description": "in seconds or as text", + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0 + }, + { + "type": "string", + "enum": [ + "minute", + "hour", + "day", + "week" + ] + } + ], + "default": 120 + } + }, + "required": [] + }, + "notifications": { + "type": "array", + "items": { + "anyOf": [ + { + "title": "check kind 'console'", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "console" + ] + }, + "parameters": { + "type": "object", + "additionalProperties": false, + "properties": {}, + "required": [] + } + }, + "required": [ + "kind", + "parameters" + ] + }, + { + "title": "check kind 'file_touch'", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "file_touch" + ] + }, + "parameters": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] + } + }, + "required": [ + "kind", + "parameters" + ] + }, + { + "title": "check kind 'email'", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "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", + "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" + ] + } + }, + "required": [ + "kind", + "parameters" + ] + }, + { + "title": "check kind 'libnotify'", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "libnotify" + ] + }, + "parameters": { + "type": "object", + "additionalProperties": false, + "properties": { + "icon": { + "type": "string" + } + }, + "required": [] + } + }, + "required": [ + "kind", + "parameters" + ] + } + ] + }, + "default": [ + { + "kind": "console", + "parameters": {} + } + ] + } + }, + "required": [ + "name" + ] + }, + { + "anyOf": [ + { + "title": "notification channel 'script'", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "script" + ] + }, + "parameters": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + }, + "arguments": { + "type": "array", + "item": { + "type": "string" + } + } + }, + "required": [ + "path" + ] + } + }, + "required": [ + "kind", + "parameters" + ] + }, + { + "title": "notification channel 'file_timestamp'", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "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" + ] + }, + { + "title": "notification channel 'http_request'", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "http_request" + ] + }, + "parameters": { + "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" + ] + }, + "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": [] + }, + "as_warning": { + "description": "whether a violation of this check shall be exposed as warning instead of critical; default: false", + "type": "boolean", + "default": false + } + }, + "required": [ + "request" + ] + } + }, + "required": [ + "kind", + "parameters" + ] + } + ] + } + ] + } + } + }, + "required": [ + "defaults", + "checks" + ] +} diff --git a/test.hmdl.json b/examples/test.hmdl.json similarity index 78% rename from test.hmdl.json rename to examples/test.hmdl.json index 6079d54..e7183eb 100644 --- a/test.hmdl.json +++ b/examples/test.hmdl.json @@ -1,12 +1,13 @@ { "defaults": { }, - "checks": { - "test": { + "checks": [ + { + "name": "test", "threshold": 1, "annoy": true, "schedule": { - "regular_interval": 60, + "regular_interval": 15, "attentive_interval": 5 }, "notifications": [ @@ -18,7 +19,6 @@ { "kind": "libnotify", "parameters": { - "icon": "/home/fenris/bilder/zeug/heimdall.png" } } ], @@ -29,5 +29,5 @@ "critical_age": 120 } } - } + ] } diff --git a/hmdl.schema.json b/hmdl.schema.json deleted file mode 100644 index 2a4ff15..0000000 --- a/hmdl.schema.json +++ /dev/null @@ -1,489 +0,0 @@ -{ - "$defs": { - "active": { - "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": { - "type": "object", - "additionalProperties": false, - "properties": { - "regular_interval": { - "description": "in seconds or as text: minute, hour, day, week", - "type": ["integer", "string"] - }, - "attentive_interval": { - "description": "in seconds or as text: minute, hour, day, week", - "type": ["integer", "string"] - } - }, - "required": [ - "regular_interval" - ] - }, - "check_kind_test": { - "type": "object", - "additionalProperties": false, - "properties": { - "kind": { - "type": "string", - "const": "test" - }, - "parameters": { - "type": "object", - "additionalProperties": false, - "properties": { - "condition": { - "type": "string", - "enum": [ - "unknown", - "ok", - "warning", - "critical" - ], - "default": "warning" - }, - "output": { - "type": "string", - "default": "" - } - }, - "required": [ - ] - } - }, - "required": [ - "kind", - "parameters" - ] - }, - "check_kind_script": { - "type": "object", - "additionalProperties": false, - "properties": { - "kind": { - "type": "string", - "const": "script" - }, - "parameters": { - "type": "object", - "additionalProperties": false, - "properties": { - "path": { - "type": "string" - }, - "arguments": { - "type": "array", - "item": { - "type": "string" - } - } - }, - "required": [ - "path" - ] - } - }, - "required": [ - "kind", - "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", - "additionalProperties": false, - "properties": { - "kind": { - "type": "string", - "const": "http_request" - }, - "parameters": { - "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" - ] - }, - "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": [ - ] - }, - "as_warning": { - "description": "whether a violation of this check shall be exposed as warning instead of critical; default: false", - "type": "boolean", - "default": false - } - }, - "required": [ - "request" - ] - } - }, - "required": [ - "kind", - "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" - } - ] - } - ] - }, - "properties": { - }, - "required": [ - ] - } - }, - "required": [ - "defaults", - "groups" - ] -} diff --git a/source/check_kinds/_interface.py b/source/check_kinds/_interface.py index 00943d0..fedecfd 100644 --- a/source/check_kinds/_interface.py +++ b/source/check_kinds/_interface.py @@ -1,5 +1,9 @@ class interface_check_kind(object): + def parameters_schema(self): + raise NotImplementedError + + def normalize_conf_node(self, node): raise NotImplementedError diff --git a/source/check_kinds/file_timestamp.py b/source/check_kinds/file_timestamp.py index 1748acc..f40f2ac 100644 --- a/source/check_kinds/file_timestamp.py +++ b/source/check_kinds/file_timestamp.py @@ -1,5 +1,55 @@ class implementation_check_kind_file_timestamp(interface_check_kind): + ''' + [implementation] + ''' + def parameters_schema(self): + return { + "type": "object", + "additionalProperties": False, + "properties": { + "path": { + "type": "string" + }, + "warning_age": { + "type": "integer", + "exclusiveMinimum": 0, + "default": (60 * 60) + }, + "critical_age": { + "type": "integer", + "exclusiveMinimum": 0, + "default": (60 * 60 * 24) + }, + "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" + ] + } + + ''' [implementation] ''' @@ -62,4 +112,3 @@ class implementation_check_kind_file_timestamp(interface_check_kind): ), } - diff --git a/source/check_kinds/http_request.py b/source/check_kinds/http_request.py index e78f734..02ccdcf 100644 --- a/source/check_kinds/http_request.py +++ b/source/check_kinds/http_request.py @@ -1,5 +1,76 @@ 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" + ] + }, + "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": [ + ] + }, + "as_warning": { + "description": "whether a violation of this check shall be exposed as warning instead of critical; default: false", + "type": "boolean", + "default": False + } + }, + "required": [ + "request" + ] + } + + ''' [implementation] ''' @@ -118,3 +189,4 @@ class implementation_check_kind_http_request(interface_check_kind): ), "output": "\n".join(lines), } + diff --git a/source/check_kinds/script.py b/source/check_kinds/script.py index 7085048..562d4ad 100644 --- a/source/check_kinds/script.py +++ b/source/check_kinds/script.py @@ -1,5 +1,29 @@ class implementation_check_kind_script(interface_check_kind): + ''' + [implementation] + ''' + def parameters_schema(self): + return { + "type": "object", + "additionalProperties": False, + "properties": { + "path": { + "type": "string" + }, + "arguments": { + "type": "array", + "item": { + "type": "string" + } + } + }, + "required": [ + "path" + ] + } + + ''' [implementation] ''' diff --git a/source/main.py b/source/main.py index 87414cb..66c7d91 100644 --- a/source/main.py +++ b/source/main.py @@ -5,7 +5,7 @@ def state_encode(state): "count": state["count"], } - + def state_decode(state_encoded): return { "timestamp": state_encoded["timestamp"], @@ -14,6 +14,179 @@ def state_decode(state_encoded): } +def schema_active(): + return { + "type": "boolean", + "default": True + } + + +def schema_threshold(): + return { + "description": "how often a condition has to occur in order to be reported", + "type": "integer", + "minimum": 1, + "default": 3 + } + + +def schema_annoy(): + return { + "description": "whether notifications shall be kept sending after the threshold has been surpassed", + "type": "boolean", + "default": False + } + + +def schema_interval(default): + return { + "description": "in seconds or as text", + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0 + }, + { + "type": "string", + "enum": [ + "minute", + "hour", + "day", + "week", + ] + }, + ], + "default": default, + } + + +def schema_schedule(): + return { + "type": "object", + "additionalProperties": False, + "properties": { + "regular_interval": schema_interval(60 * 60), + "attentive_interval": schema_interval(60 * 2), + }, + "required": [ + ] + } + + +def schema_notifications(notification_channel_implementations): + return { + "type": "array", + "items": { + "anyOf": list( + map( + lambda pair: { + "title": ("check kind '%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 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": schema_active(), + "threshold": schema_threshold(), + "annoy": schema_annoy(), + "schedule": schema_schedule(), + "notifications": schema_notifications(notification_channel_implementations), + }, + "required": [ + ] + }, + "checks": { + "type": "array", + "items": { + "allOf": [ + { + "description": "should represent a specific check", + "type": "object", + "unevaluatedProperties": False, + "properties": { + "name": { + "type": "string" + }, + "title": { + "type": "string" + }, + "active": schema_active(), + "threshold": schema_threshold(), + "annoy": schema_annoy(), + "schedule": schema_schedule(), + "notifications": schema_notifications(notification_channel_implementations), + }, + "required": [ + "name", + ] + }, + { + "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" + ] + }, + check_kind_implementations.items() + ) + ), + }, + ] + } + } + }, + "required": [ + "defaults", + "checks", + ] + } + + def conf_normalize_interval(interval_raw): if (type(interval_raw) == int): return interval_raw @@ -46,8 +219,15 @@ def conf_normalize_schedule(node): } -def conf_normalize_defaults(node): - return dict_merge( +def conf_normalize_notification(notification_channel_implementations, node): + return { + "kind": node["kind"], + "parameters": notification_channel_implementations[node["kind"]].normalize_conf_node(node["parameters"]), + } + + +def conf_normalize_defaults(notification_channel_implementations, node): + node_ = dict_merge( { "active": True, "threshold": 3, @@ -61,54 +241,88 @@ def conf_normalize_defaults(node): }, node ) - - -def conf_normalize_check(check_kind_implementations, defaults, name, node): - if ("kind" not in node): - raise ValueError("missing mandatory 'check' field 'kind'") - else: - if (node["kind"] not in check_kind_implementations): - raise ValueError("unhandled kind: %s" % node["kind"]) - else: - node_ = dict_merge( - { - "title": name, - "active": defaults["active"], - "threshold": defaults["threshold"], - "annoy": defaults["annoy"], - "schedule": defaults["schedule"], - "notifications": defaults["notifications"], - "parameters": {}, - }, - node + return { + "active": node_["active"], + "threshold": node_["threshold"], + "annoy": node_["annoy"], + "schedule": node_["schedule"], + "notifications": list( + map( + lambda x: conf_normalize_notification(notification_channel_implementations, x), + node_["notifications"] ) - return { - "title": node_["title"], - "active": node_["active"], - "threshold": node_["threshold"], - "annoy": node_["annoy"], - "schedule": conf_normalize_schedule(node_["schedule"]), - "notifications": node_["notifications"], - "kind": node_["kind"], - "parameters": check_kind_implementations[node_["kind"]].normalize_conf_node(node_["parameters"]), - } + ), + } -def conf_normalize_root(check_kind_implementations, node): - return dict( - map( - lambda check_pair: ( - check_pair[0], - conf_normalize_check( - check_kind_implementations, - conf_normalize_defaults(node["defaults"]), - check_pair[0], - check_pair[1] - ), - ), - node["checks"].items() +def conf_normalize_check(check_kind_implementations, notification_channel_implementations, defaults, node): + if ("name" not in node): + raise ValueError("missing mandatory 'check' field 'name'") + else: + if ("kind" not in node): + raise ValueError("missing mandatory 'check' field 'kind'") + else: + if (node["kind"] not in check_kind_implementations): + raise ValueError("unhandled kind: %s" % node["kind"]) + else: + node_ = dict_merge( + { + "title": node["name"], + "active": defaults["active"], + "threshold": defaults["threshold"], + "annoy": defaults["annoy"], + "schedule": defaults["schedule"], + "notifications": defaults["notifications"], + "parameters": {}, + }, + node + ) + return { + "name": node_["name"], + "title": node_["title"], + "active": node_["active"], + "threshold": node_["threshold"], + "annoy": node_["annoy"], + "schedule": conf_normalize_schedule(node_["schedule"]), + "notifications": list( + map( + lambda x: conf_normalize_notification(notification_channel_implementations, x), + node_["notifications"] + ) + ), + "kind": node_["kind"], + "parameters": check_kind_implementations[node_["kind"]].normalize_conf_node(node_["parameters"]), + } + + +def conf_normalize_root(check_kind_implementations, notification_channel_implementations, node): + counts = {} + for node_ in node["checks"]: + 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(counts.keys()), + } + ) + ) + else: + return list( + map( + lambda node_: conf_normalize_check( + check_kind_implementations, + notification_channel_implementations, + conf_normalize_defaults(notification_channel_implementations, node["defaults"]), + node_ + ), + node["checks"] + ) ) - ) def main(): @@ -127,7 +341,7 @@ def main(): help = "path to the configuration file" ) argumentparser.add_argument( - "-s", + "-f", "--state-path", type = str, default = None, @@ -143,6 +357,14 @@ def main(): dest = "erase_state", help = "whether the state shall be deleted on start; this will cause that all checks are executed" ) + argumentparser.add_argument( + "-s", + "--show-schema", + action = "store_true", + default = False, + dest = "show_schema", + help = "print the hmdl JSON schema to stdout and exit" + ) argumentparser.add_argument( "-e", "--expose-full-conf", @@ -166,8 +388,6 @@ def main(): ## exec - _sys.stderr.write(">> state file path: %s\n" % state_path) - ### load check kind implementations check_kind_implementations = { "script": implementation_check_kind_script(), @@ -183,111 +403,131 @@ def main(): "libnotify": implementation_notification_channel_libnotify(), } - ### get configuration data - checks = conf_normalize_root(check_kind_implementations, _json.loads(file_read(args.conf_path))) - if (args.expose_full_conf): - _sys.stdout.write(_json.dumps(checks, indent = "\t") + "\n") - _sys.exit(1) + if (args.show_schema): + _sys.stdout.write( + _json.dumps( + schema_root( + check_kind_implementations, + notification_channel_implementations + ), + indent = "\t" + ) + + + "\n" + ) else: - ### get state data - if ( - (not _os.path.exists(state_path)) - or - args.erase_state - ): - state_data = {} - file_write(state_path, _json.dumps(state_data, indent = "\t")) - else: - state_data = _json.loads(file_read(state_path)) + _sys.stderr.write(">> state file path: %s\n" % state_path) - ### iterate through checks - for (check_name, check_data, ) in checks.items(): - if (not check_data["active"]): - pass + ### get configuration data + checks = conf_normalize_root( + check_kind_implementations, + notification_channel_implementations, + _json.loads(file_read(args.conf_path)) + ) + if (args.expose_full_conf): + _sys.stdout.write(_json.dumps(checks, indent = "\t") + "\n") + _sys.exit(1) + else: + ### get state data + if ( + (not _os.path.exists(state_path)) + or + args.erase_state + ): + state_data = {} + file_write(state_path, _json.dumps(state_data, indent = "\t")) else: - ### get old state and examine whether the check shall be executed - old_item_state = ( - None - if (check_name not in state_data) else - state_decode(state_data[check_name]) - ) - 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): + state_data = _json.loads(file_read(state_path)) + + ### iterate through checks + for check_data in checks: + if (not check_data["active"]): pass else: - _sys.stderr.write( - string_coin( - "-- {{check_name}}\n", - { - "check_name": check_name, - } - ) + ### get old state and examine whether the check shall be executed + old_item_state = ( + None + if (check_data["name"] not in state_data) else + state_decode(state_data[check_data["name"]]) ) - - ### execute check and set new state - result = check_kind_implementations[check_data["kind"]].run(check_data["parameters"]) - new_item_state = { - "timestamp": timestamp, - "condition": result["condition"], - "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 - ) - ), - } - state_data[check_name] = state_encode(new_item_state) - file_write(state_path, _json.dumps(state_data, indent = "\t")) - - ### send notifications - if ( - ( - (new_item_state["count"] is not None) - and - (new_item_state["count"] == check_data["threshold"]) - ) + 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 ( - (new_item_state["count"] is None) + (old_item_state["count"] is not None) and - check_data["annoy"] + ((timestamp - old_item_state["timestamp"]) >= check_data["schedule"]["attentive_interval"]) ) - ): - for notification in check_data["notifications"]: - if (notification["kind"] in notification_channel_implementations): - notification_channel_implementations[notification["kind"]].notify( - notification["parameters"], - check_name, - check_data, - new_item_state, - result["output"] + ) + if (not due): + pass + else: + _sys.stderr.write( + string_coin( + "-- {{check_name}}\n", + { + "check_name": check_data["name"], + } + ) + ) + + ### execute check and set new state + result = check_kind_implementations[check_data["kind"]].run(check_data["parameters"]) + new_item_state = { + "timestamp": timestamp, + "condition": result["condition"], + "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 ) - else: - raise ValueError("invalid notification kind: %s" % notification["kind"]) - + ), + } + state_data[check_data["name"]] = state_encode(new_item_state) + file_write(state_path, _json.dumps(state_data, indent = "\t")) + + ### send notifications + if ( + ( + (new_item_state["count"] is not None) + and + (new_item_state["count"] == check_data["threshold"]) + ) + or + ( + (new_item_state["count"] is None) + and + check_data["annoy"] + ) + ): + for notification in check_data["notifications"]: + if (notification["kind"] in notification_channel_implementations): + notification_channel_implementations[notification["kind"]].notify( + notification["parameters"], + check_data["name"], + check_data, + new_item_state, + result["output"] + ) + else: + raise ValueError("invalid notification kind: %s" % notification["kind"]) + main() + diff --git a/source/notification_channels/_interface.py b/source/notification_channels/_interface.py index 1df1fb9..7a5ca1f 100644 --- a/source/notification_channels/_interface.py +++ b/source/notification_channels/_interface.py @@ -1,5 +1,9 @@ class interface_notification_channel(object): + def parameters_schema(self): + raise NotImplementedError + + def normalize_conf_node(self, node): raise NotImplementedError diff --git a/source/notification_channels/console.py b/source/notification_channels/console.py index a8c2599..df75065 100644 --- a/source/notification_channels/console.py +++ b/source/notification_channels/console.py @@ -1,5 +1,19 @@ class implementation_notification_channel_console(interface_notification_channel): + ''' + [implementation] + ''' + def parameters_schema(self): + return { + "type": "object", + "additionalProperties": False, + "properties": { + }, + "required": [ + ] + } + + ''' [implementation] ''' diff --git a/source/notification_channels/email.py b/source/notification_channels/email.py index 2113a71..3631cd6 100644 --- a/source/notification_channels/email.py +++ b/source/notification_channels/email.py @@ -1,5 +1,63 @@ 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] ''' diff --git a/source/notification_channels/file_touch.py b/source/notification_channels/file_touch.py index f97151f..dc8106b 100644 --- a/source/notification_channels/file_touch.py +++ b/source/notification_channels/file_touch.py @@ -1,5 +1,23 @@ class implementation_notification_channel_file_touch(interface_notification_channel): + ''' + [implementation] + ''' + def parameters_schema(self): + return { + "type": "object", + "additionalProperties": False, + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] + } + + ''' [implementation] ''' diff --git a/source/notification_channels/libnotify.py b/source/notification_channels/libnotify.py index 78ca83c..f751cd1 100644 --- a/source/notification_channels/libnotify.py +++ b/source/notification_channels/libnotify.py @@ -1,12 +1,29 @@ 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_conf_node(self, node): return dict_merge( { - "icon": None, + "icon": "/usr/local/share/icons/heimdall.png", }, node ) diff --git a/todo.md b/todo.md index 30a5f7c..ceec363 100644 --- a/todo.md +++ b/todo.md @@ -3,8 +3,8 @@ - Selbst-Test - Benachrichtigungs-Kanäle: - Matrix -- JSON-Schema für Konfiguration von Programm erzeugen lassen - Möglichkeit dauerhaft laufen zulassen (evtl. als systemd-Dienst) - Versionierung - Umbenennung: `output` zu `info` - Test-Routinen +