From 4edb6adccde4eecf152f0d199d47874e4a056d75 Mon Sep 17 00:00:00 2001 From: cmk-bonobo Date: Sat, 26 Aug 2023 15:02:28 +0200 Subject: [PATCH] MKP 0.2.0, Domain checks added --- .../plugins/agent_based/mailcow_domains.py | 151 ++++++++++++++++++ local/share/check_mk/checkman/mailcow_domains | 20 +++ .../web/plugins/metrics/mailcow_metrics.py | 24 +++ .../plugins/perfometer/mailcow_perfometers.py | 11 ++ .../web/plugins/wato/mailcow_domains_rules.py | 102 ++++++++++++ mkp/Mailcow-0.2.0.mkp | Bin 0 -> 9378 bytes 6 files changed, 308 insertions(+) create mode 100644 local/lib/python3/cmk/base/plugins/agent_based/mailcow_domains.py create mode 100644 local/share/check_mk/checkman/mailcow_domains create mode 100644 local/share/check_mk/web/plugins/wato/mailcow_domains_rules.py create mode 100755 mkp/Mailcow-0.2.0.mkp diff --git a/local/lib/python3/cmk/base/plugins/agent_based/mailcow_domains.py b/local/lib/python3/cmk/base/plugins/agent_based/mailcow_domains.py new file mode 100644 index 0000000..dba2fa6 --- /dev/null +++ b/local/lib/python3/cmk/base/plugins/agent_based/mailcow_domains.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +import time +import random +from pprint import pprint +from .agent_based_api.v1 import get_value_store, get_rate, register, render, Service, Result, State, Metric + +def getStateUpper(levels, value): + warn, crit = levels + if value >= crit: + return State.CRIT + if value >= warn: + return State.WARN + return State.OK + +# services with item --> storageid and usage of value_store (to be able to calculate rates) +def discover_mailcow_domains(section): + for key in section: + yield(Service(item = key)) + +def check_mailcow_domains(item, params, section): + domain = item + # get all values from section + active = section[domain][0] + create_time = section[domain][1] + modify_time = section[domain][2] + max_number_of_mailboxes = section[domain][3] + number_of_mailboxes = section[domain][4] + percent_used_mailboxes = section[domain][5] + max_number_of_aliases = section[domain][6] + number_of_aliases = section[domain][7] + percent_used_aliases = section[domain][8] + total_number_of_messages = section[domain][9] + total_number_of_bytes_used = section[domain][10] + quota = section[domain][11] + percent_storage_used_for_messages = section[domain][12] + + # create (main) service for used storage (domain quota) + warn, crit = params["levels_mailcow_domains_quota_used"] + levels = (warn, crit) + state_quota = getStateUpper(levels, percent_storage_used_for_messages) + + # create graph for used quota + yield Metric("mailcow_domains_used_quota", percent_storage_used_for_messages, levels=levels) + + summary_quota = f"Quota used is {render.percent(percent_storage_used_for_messages)}" + details_quota = f"Quota: {render.bytes(total_number_of_bytes_used)} of {render.bytes(quota)} used" + + # create service + yield(Result(state=state_quota, summary=summary_quota, details=details_quota)) + + # create some additional services and information only details + notice = f"Active: {active}" + yield(Result(state=State.OK, notice=notice)) + notice = f"Creation date: {create_time}" + yield(Result(state=State.OK, notice=notice)) + notice = f"Last modified: {modify_time}" + yield(Result(state=State.OK, notice=notice)) + + # create service for number of configured mailboxes + warn, crit = params["levels_mailcow_domains_mailboxes_used"] + levels = (warn, crit) + state_mailboxes = getStateUpper(levels, percent_used_mailboxes) + yield Metric("mailcow_domains_mailboxes", percent_used_mailboxes, levels=levels) + notice = f"Used mailboxes: {render.percent(percent_used_mailboxes)}, {number_of_mailboxes} of {max_number_of_mailboxes} in use" + yield(Result(state=state_mailboxes, notice=notice)) + + # create service for number of configured aliases + warn, crit = params["levels_mailcow_domains_aliases_used"] + levels = (warn, crit) + state_aliases = getStateUpper(levels, percent_used_aliases) + yield Metric("mailcow_domains_aliases", percent_used_aliases, levels=levels) + notice = f"Used aliases: {render.percent(percent_used_aliases)}, {number_of_aliases} of {max_number_of_aliases} in use" + yield(Result(state=state_aliases, notice=notice)) + + # create service for number of messages + warn, crit = params["levels_mailcow_domains_num_messages"] + levels = (warn, crit) + state_messages = getStateUpper(levels, total_number_of_messages) + yield Metric("mailcow_domains_messages", total_number_of_messages, levels=levels) + notice = f"Number of messages: {total_number_of_messages}" + yield(Result(state=state_messages, notice=notice)) + +def parse_mailcow_domains_section(string_table): + # convert the raw output of the agent section into a meaningful structure, do type conversions and so on + parsed_data = {} + for line in string_table: + domainname = line[0] + value_active = int(line[1]) + if value_active == 1: + active = "yes" + else: + active = "no" + # calculate creation and last modification date in human readable format + create_time_value = line[2] + if create_time_value == "None": + create_time_data = "Not available" + else: + create_time = int(create_time_value) + curr_time = int(time.time()) + diff_time = curr_time - create_time + create_time_data = render.timespan(diff_time) + modify_time_value = line[3] + if modify_time_value == "None": + modify_time_data = "Never" + else: + modify_time = int(modify_time_value) + curr_time = int(time.time()) + diff_time = curr_time - modify_time + modify_time_data = render.timespan(diff_time) + # calculate percentage of used mailboxes + max_number_of_mailboxes = int(line[4]) + number_of_mailboxes = int(line[5]) + percent_used_mailboxes = number_of_mailboxes * 100 / max_number_of_mailboxes + # calculate percentage of used aliases + max_number_of_aliases = int(line[6]) + number_of_aliases = int(line[7]) + percent_used_aliases = number_of_aliases * 100 / max_number_of_aliases + # number of messages within domain + total_number_of_messages = int(line[8]) + # calculate storage used for all messages in domain + total_number_of_bytes_used = int(line[9]) + quota = int(line[10]) + percent_storage_used_for_messages = total_number_of_bytes_used * 100 / quota + # store all (calculated) data + parsed_data[f"{domainname}"] = [active, create_time_data, modify_time_data, + max_number_of_mailboxes, number_of_mailboxes, percent_used_mailboxes, + max_number_of_aliases, number_of_aliases, percent_used_aliases, + total_number_of_messages, + total_number_of_bytes_used, quota, percent_storage_used_for_messages + ] + return parsed_data + +register.agent_section( + name = "mailcow_domains", + parse_function = parse_mailcow_domains_section, +) + +register.check_plugin( + name = "mailcow_domains", + service_name = "Mailcow domain %s", + discovery_function = discover_mailcow_domains, + check_function = check_mailcow_domains, + check_default_parameters = { + "levels_mailcow_domains_quota_used": (65.0, 85.0), + "levels_mailcow_domains_mailboxes_used": (65.0, 85.0), + "levels_mailcow_domains_aliases_used": (65.0, 85.0), + "levels_mailcow_domains_num_messages": (10000, 25000), + "levels_mailcow_domains_num_aliases": (100, 250), + }, + check_ruleset_name="mailcow_domains", +) \ No newline at end of file diff --git a/local/share/check_mk/checkman/mailcow_domains b/local/share/check_mk/checkman/mailcow_domains new file mode 100644 index 0000000..d81eb5f --- /dev/null +++ b/local/share/check_mk/checkman/mailcow_domains @@ -0,0 +1,20 @@ +title: Mailcow: Mailboxes, Aliases and Storage used by Email Domain +agents: linux +catalog: unsorted +license: GPL +distribution: check_mk +description: + Tested with Mailcow versions 2022-07a and higher (use at your own risk with lower versions). + Tested only with fully dockerized Mailcow instances. + You have to provide at least the hostname/IP of your Mailcow server and an API key. + Got to configuration settings and create a new API key (read-only access is sufficient). + Allow API access to at least your CheckMK server IP address. + Shows several information about a Mailcow email domain, e.g. number of configured mailboxes/aliases, used space for all messages etc. + The check will raise WARN/CRIT if quota usage is above the configurable levels. + The check will raise WARN/CRIT if number of configured mailboxes is above the configurable levels. + The check will raise WARN/CRIT if number of configured aliases is above the configurable levels. + The check will raise WARN/CRIT if number of total messages is above the configurable levels. +item: + domainname +inventory: + one service is created for each domain \ No newline at end of file diff --git a/local/share/check_mk/web/plugins/metrics/mailcow_metrics.py b/local/share/check_mk/web/plugins/metrics/mailcow_metrics.py index c3655f9..6f74193 100644 --- a/local/share/check_mk/web/plugins/metrics/mailcow_metrics.py +++ b/local/share/check_mk/web/plugins/metrics/mailcow_metrics.py @@ -21,4 +21,28 @@ metric_info["mc_num_global_messages"] = { "title": _("Number of Messages"), "unit": "count", "color": "42/a", +} + +metric_info["mailcow_domains_used_quota"] = { + "title": _("Domain Quota Used"), + "unit": "%", + "color": "24/a", +} + +metric_info["mailcow_domains_mailboxes"] = { + "title": _("Domain Mailboxes Quota Used"), + "unit": "%", + "color": "44/b", +} + +metric_info["mailcow_domains_aliases"] = { + "title": _("Domain Aliases Quota Used"), + "unit": "%", + "color": "44/a", +} + +metric_info["mailcow_domains_messages"] = { + "title": _("Global Number of Messages"), + "unit": "count", + "color": "24/a", } \ No newline at end of file diff --git a/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py b/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py index d96fac1..9a08574 100644 --- a/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py +++ b/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py @@ -11,6 +11,17 @@ perfometer_info.append({ ], }) +perfometer_info.append({ + "type": "stacked", + "perfometers": [ + { + "type": "linear", + "segments": ["mailcow_domains_used_quota"], + "total": 100.0, + }, + ], +}) + #perfometer_info.append({ # "type": "stacked", # "perfometers": [ diff --git a/local/share/check_mk/web/plugins/wato/mailcow_domains_rules.py b/local/share/check_mk/web/plugins/wato/mailcow_domains_rules.py new file mode 100644 index 0000000..f16ac59 --- /dev/null +++ b/local/share/check_mk/web/plugins/wato/mailcow_domains_rules.py @@ -0,0 +1,102 @@ +from cmk.gui.i18n import _ +from cmk.gui.plugins.wato import ( + CheckParameterRulespecWithItem, + rulespec_registry, + RulespecGroupCheckParametersApplications +) +from cmk.gui.valuespec import ( + Dictionary, + ListChoice, + TextAscii, + Percentage, + Tuple, + Float, + Integer +) + +def _item_spec_mailcow_domains(): + return TextAscii( + title=_("Domain") + ) + +def _parameter_spec_mailcow_domains(): + return Dictionary( + elements=[ + ("levels_mailcow_domains_quota_used", Tuple( + title=_("Mailcow domains quota usage for storage"), + elements=[ + Percentage( + title=_("Warning at"), + default_value=65.0, + ), + Percentage( + title=_("Critical at"), + default_value=85.0, + ) + ], + )), + ("levels_mailcow_domains_mailboxes_used", Tuple( + title=_("Mailcow domains mailboxes usage"), + elements=[ + Percentage( + title=_("Warning at"), + default_value=65.0, + ), + Percentage( + title=_("Critical at"), + default_value=85.0, + ) + ], + )), + ("levels_mailcow_domains_aliases_used", Tuple( + title=_("Mailcow domains aliases usage"), + elements=[ + Percentage( + title=_("Warning at"), + default_value=65.0, + ), + Percentage( + title=_("Critical at"), + default_value=85.0, + ) + ], + )), + ("levels_mailcow_domains_num_messages", Tuple( + title=_("Number of messages"), + elements=[ + Integer( + title=_("Warning at"), + default_value=10000, + ), + Integer( + title=_("Critical at"), + default_value=25000, + ) + ], + )), + ("levels_mailcow_domains_num_aliases", Tuple( + title=_("Number of aliases"), + elements=[ + Integer( + title=_("Warning at"), + default_value=100, + ), + Integer( + title=_("Critical at"), + default_value=250, + ) + ], + )), + ], + ) + +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name="mailcow_domains", + group=RulespecGroupCheckParametersApplications, + match_type="dict", + item_spec=_item_spec_mailcow_domains, + parameter_valuespec=_parameter_spec_mailcow_domains, + title=lambda: _("Levels for Mailcow domains"), + ) +) \ No newline at end of file diff --git a/mkp/Mailcow-0.2.0.mkp b/mkp/Mailcow-0.2.0.mkp new file mode 100755 index 0000000000000000000000000000000000000000..bd0f012a7c6d386be293de0f991bd4a968cce316 GIT binary patch literal 9378 zcmaKxMN}LBlWqwv0TSHZf?IIc;O-8=0u8}wB)HQM+=F{?cXxMh+=6QZ4fOnT-ewkW zW>=e9)Hz=*zI!QSP*JfUr{CdWDi-#RR?c2*ob24}oa_LHpKuqV4sII)iO-W}LHTFn z^Es><91{uh4QWa?wyr$DbGim!y=Em@rQd^JWq&-^?oU1cf_m&r2GcQ(sqT$!=CR<% zNEFW5@y>!;2wD3X&4|bX_xl3hkeFZ>5VU`v-p>gORiALc&bbRHHr`O0xq

!&Lxycu1+&t0(_7LAS+&8)5h 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 literal 0 HcmV?d00001