def conf_schema_active(): return { "description": "whether the check shall be executed", "type": "boolean", "default": True, } def conf_schema_threshold(): return { "description": "how often a condition has to occur in order to be reported", "type": "integer", "minimum": 1, "default": 3, } def conf_schema_annoy(): return { "description": "whether notifications shall be kept sending after the threshold has been surpassed", "type": "boolean", "default": False, } def conf_schema_interval(default): return { "anyOf": [ { "description": "in seconds", "type": "integer", "exclusiveMinimum": 0, }, { "description": "as text", "type": "string", "enum": [ "minute", "hour", "day", "week", ] }, ], "default": default, } def conf_schema_schedule(): return { "type": "object", "additionalProperties": False, "properties": { "regular_interval": conf_schema_interval(60 * 60), "attentive_interval": conf_schema_interval(60 * 2), }, "required": [ ], } def conf_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 conf_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": conf_schema_active(), "threshold": conf_schema_threshold(), "annoy": conf_schema_annoy(), "schedule": conf_schema_schedule(), "notifications": conf_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": conf_schema_active(), "threshold": conf_schema_threshold(), "annoy": conf_schema_annoy(), "schedule": conf_schema_schedule(), "notifications": conf_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(), }, "required": [ "kind", "parameters", ] }, check_kind_implementations.items() ) ), }, ] } } }, "required": [ ] } def conf_normalize_interval(interval_raw): 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 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_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_conf_node(node["parameters"]), } def conf_normalize_defaults(notification_channel_implementations, node): node_ = dict_merge( { "active": True, "threshold": 3, "annoy": False, "schedule": { "regular_interval": (60 * 60), "attentive_interval": (60 * 2), }, "notifications": [ ], }, 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"] ) ), } def conf_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( { "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 = {} 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(counts.keys()), } ) ) else: defaults = conf_normalize_defaults( notification_channel_implementations, ( node["defaults"] if ("defaults" in node) else {} ) ) includes = ( node["includes"] if ("includes" in node) else [] ) return { "defaults": defaults, "includes": includes, "checks": list( map( lambda node_: conf_normalize_check( check_kind_implementations, notification_channel_implementations, defaults, node_ ), checks_raw ) ) } def conf_load( check_kind_implementations, notification_channel_implementations, path, already_included = None ): if (already_included is None): already_included = set([]) if (path in already_included): raise ValueError("circular dependency detected") else: already_included.add(path) conf_raw = _json.loads(file_read(path)) includes = ( conf_raw["includes"] if ("includes" in conf_raw) else [] ) for index in range(len(includes)): path_ = includes[index] sub_conf = conf_load( check_kind_implementations, notification_channel_implementations, ( path_ if _os.path.isabs(path_) else _os.path.join(_os.path.dirname(path), path_) ), already_included ) if (not "checks" in conf_raw): conf_raw["checks"] = [] conf_raw["checks"].extend( list( map( lambda check: dict_merge( check, { "name": string_coin( "x{{number}}.{{original_name}}", { "number": ("%u" % (index + 1)), "original_name": check["name"], } ), } ), sub_conf["checks"] ) ) ) conf_raw["includes"] = [] return conf_normalize_root( check_kind_implementations, notification_channel_implementations, conf_raw )