From 9ffe91c13c99f2119ef741a25000d28edaf2a260 Mon Sep 17 00:00:00 2001 From: cmk-bonobo Date: Sun, 27 Aug 2023 11:03:51 +0200 Subject: [PATCH] MKP 0.9.9, section mailcow_mailboxes completed --- .../plugins/agent_based/mailcow_domains.py | 12 +- .../plugins/agent_based/mailcow_mailboxes.py | 148 ++++++++++++++++++ .../check_mk/agents/special/agent_mailcow | 46 +++--- .../share/check_mk/checkman/mailcow_mailboxes | 18 +++ .../web/plugins/metrics/mailcow_metrics.py | 12 ++ .../plugins/perfometer/mailcow_perfometers.py | 11 ++ .../plugins/wato/mailcow_mailboxes_rules.py | 63 ++++++++ mkp/Mailcow-0.9.9.mkp | Bin 0 -> 10205 bytes 8 files changed, 277 insertions(+), 33 deletions(-) create mode 100644 local/lib/python3/cmk/base/plugins/agent_based/mailcow_mailboxes.py create mode 100644 local/share/check_mk/checkman/mailcow_mailboxes create mode 100644 local/share/check_mk/web/plugins/wato/mailcow_mailboxes_rules.py create mode 100755 mkp/Mailcow-0.9.9.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 index 2a02743..f544ad5 100644 --- a/local/lib/python3/cmk/base/plugins/agent_based/mailcow_domains.py +++ b/local/lib/python3/cmk/base/plugins/agent_based/mailcow_domains.py @@ -42,8 +42,8 @@ def check_mailcow_domains(item, params, section): # 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" + summary_quota = f"Storage quota used is {render.percent(percent_storage_used_for_messages)}" + details_quota = f"Storage 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)) @@ -112,19 +112,11 @@ def parse_mailcow_domains_section(string_table): create_time_data = "Not available" else: create_time_data = create_time_value - #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_data = modify_time_value - #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]) diff --git a/local/lib/python3/cmk/base/plugins/agent_based/mailcow_mailboxes.py b/local/lib/python3/cmk/base/plugins/agent_based/mailcow_mailboxes.py new file mode 100644 index 0000000..8eebb78 --- /dev/null +++ b/local/lib/python3/cmk/base/plugins/agent_based/mailcow_mailboxes.py @@ -0,0 +1,148 @@ +#!/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 + +def discover_mailcow_mailboxes(section): + for key in section: + yield(Service(item = key)) + +def check_mailcow_mailboxes(item, params, section): + mailbox = item + # get all values from section + active = section[mailbox][0] + create_time = section[mailbox][1] + modify_time = section[mailbox][2] + display_name = section[mailbox][3] + number_of_messages = section[mailbox][4] + _percent_in_use = section[mailbox][5] + quota = section[mailbox][6] + total_number_of_bytes_used = section[mailbox][7] + percent_storage_used_for_messages = section[mailbox][8] + last_imap_login_data = section[mailbox][9] + last_pop3_login_data = section[mailbox][10] + last_smtp_login_data = section[mailbox][11] + + # create (main) service for used storage (mailbox quota) + warn, crit = params["levels_mailcow_mailboxes_quota_used"] + levels = (warn, crit) + state_quota = getStateUpper(levels, percent_storage_used_for_messages) + # create graph for used quota + yield Metric("mailcow_mailboxes_used_quota", percent_storage_used_for_messages, levels=levels) + summary_quota = f"Storage quota for mailbox of '{display_name}' 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)) + + notice = f"Last IMAP login: {last_imap_login_data} ago" + yield(Result(state=State.OK, notice=notice)) + notice = f"Last POP3 login: {last_pop3_login_data} ago" + yield(Result(state=State.OK, notice=notice)) + notice = f"Last SMTP login: {last_smtp_login_data} ago" + yield(Result(state=State.OK, notice=notice)) + + # create service for number of messages + warn, crit = params["levels_mailcow_mailboxes_num_messages"] + levels = (warn, crit) + state_messages = getStateUpper(levels, number_of_messages) + yield Metric("mailcow_mailboxes_messages", number_of_messages, levels=levels) + notice = f"Number of messages: {number_of_messages}" + yield(Result(state=state_messages, notice=notice)) + + +def parse_mailcow_mailboxes_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: + mailboxname = 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_data = create_time_value + modify_time_value = line[3] + if modify_time_value == "None": + modify_time_data = "Never" + else: + modify_time_data = modify_time_value + # get display name + display_name = line[4] + # number of messages within mailbox + number_of_messages = int(line[5]) + # percent in use, rounded to full percent + percent_in_use = int(line[6]) + # calculate storage used for all messages in mailbox + quota = int(line[7]) + total_number_of_bytes_used = int(line[8]) + percent_storage_used_for_messages = total_number_of_bytes_used * 100 / quota + # get time of last login for IMAP/POP3/SMTP (seconds since epoch) + last_imap_login = int(line[9]) + last_pop3_login = int(line[10]) + last_smtp_login = int(line[11]) + # transfer these times into a human friendly format + if last_imap_login == 0: + last_imap_login_data = "Never" + else: + curr_time = int(time.time()) + diff_time = curr_time - last_imap_login + last_imap_login_data = render.timespan(diff_time) + if last_pop3_login == 0: + last_pop3_login_data = "Never" + else: + curr_time = int(time.time()) + diff_time = curr_time - last_pop3_login + last_pop3_login_data = render.timespan(diff_time) + if last_smtp_login == 0: + last_smtp_login_data = "Never" + else: + curr_time = int(time.time()) + diff_time = curr_time - last_smtp_login + last_smtp_login_data = render.timespan(diff_time) + # store all (calculated) data + parsed_data[f"{mailboxname}"] = [active, create_time_data, modify_time_data, + display_name, number_of_messages, percent_in_use, + quota, total_number_of_bytes_used, percent_storage_used_for_messages, + last_imap_login_data, last_pop3_login_data, last_smtp_login_data + ] + return parsed_data + +register.agent_section( + name = "mailcow_mailboxes", + parse_function = parse_mailcow_mailboxes_section, +) + +register.check_plugin( + name = "mailcow_mailboxes", + service_name = "Mailcow mailbox %s", + discovery_function = discover_mailcow_mailboxes, + check_function = check_mailcow_mailboxes, + check_default_parameters = { + "levels_mailcow_mailboxes_quota_used": (65.0, 85.0), + "levels_mailcow_mailboxes_num_messages": (1000, 2500), + }, + check_ruleset_name="mailcow_mailboxes", +) \ No newline at end of file diff --git a/local/share/check_mk/agents/special/agent_mailcow b/local/share/check_mk/agents/special/agent_mailcow index 7d1b7dc..f12d9d1 100755 --- a/local/share/check_mk/agents/special/agent_mailcow +++ b/local/share/check_mk/agents/special/agent_mailcow @@ -219,18 +219,18 @@ def getMailcowInfo(headers, verify, base_url): ''' Output is as follows: -mailbox name email address used for login -active 1 --> active, 0 --> not active -creation date "None" if ??? -last modified date "None" if never modified -name display name -number of messages -percent in use quota used, rounded to full percents -quota max quota in bytes -quota used quota used in bytes -last imap login seconds since epoch, 0 if never logged inin seconds since epoch -last pop3 login seconds since epoch, 0 if never logged in -last smtp login seconds since epoch, 0 if never logged in +0 mailbox name email address used for login +1 active 1 --> active, 0 --> not active +2 creation date "None" if ??? +3 last modified date "None" if never modified +4 name display name +5 number of messages +6 percent in use quota used, rounded to full percents +7 quota max quota in bytes +8 quota used quota used in bytes +9 last imap login seconds since epoch, 0 if never logged in +10 last pop3 login seconds since epoch, 0 if never logged in +11 last smtp login seconds since epoch, 0 if never logged in Example: user1@dom1.de;1;2022-04-29 14:29:34;2022-04-29 14:29:34;Sarah;2433;2;21474836480;495481374;1692520168;0;1692281537 @@ -266,17 +266,17 @@ def doCmkOutputMailcow(version, num_domains, num_mailboxes, num_global_messages) ''' Output is as follows: -domain_name -active 1 --> active, 0 --> not active -creation date "None" if ??? -last modified date "None" if never modified -max number mailboxes -number of mailboxes -max number of aliases -number of aliases -total number of messages -total number of bytes used in bytes -max quota in bytes +0 domain_name +1 active 1 --> active, 0 --> not active +2 creation date "None" if ??? +3 last modified date "None" if never modified +4 max number mailboxes +5 number of mailboxes +6 max number of aliases +7 number of aliases +8 total number of messages +9 total number of bytes used in bytes +10 max quota in bytes Example: dom1.de;1;2022-04-23 22:54:57;None;10;0;400;6;0;0;10737418240 diff --git a/local/share/check_mk/checkman/mailcow_mailboxes b/local/share/check_mk/checkman/mailcow_mailboxes new file mode 100644 index 0000000..232b32e --- /dev/null +++ b/local/share/check_mk/checkman/mailcow_mailboxes @@ -0,0 +1,18 @@ +title: Mailcow: Usage per Mailbox, Quota and Number of Messages +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 mailbox, e.g. number of messages and quota. + The check will raise WARN/CRIT if quota usage is above the configurable levels. + The check will raise WARN/CRIT if number of messages is above the configurable levels. +item: + mailboxname +inventory: + one service is created for each mailbox \ 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 8e37082..5e87acf 100644 --- a/local/share/check_mk/web/plugins/metrics/mailcow_metrics.py +++ b/local/share/check_mk/web/plugins/metrics/mailcow_metrics.py @@ -57,4 +57,16 @@ metric_info["mailcow_domains_configured_mailboxes"] = { "title": _("Number of Configured Mailboxes"), "unit": "count", "color": "24/b", +} + +metric_info["mailcow_mailboxes_used_quota"] = { + "title": _("Mailbox Quota Used"), + "unit": "%", + "color": "24/a", +} + +metric_info["mailcow_mailboxes_messages"] = { + "title": _("Number of Messages"), + "unit": "count", + "color": "24/b", } \ 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 9a08574..b7bfea5 100644 --- a/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py +++ b/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py @@ -22,6 +22,17 @@ perfometer_info.append({ ], }) +perfometer_info.append({ + "type": "stacked", + "perfometers": [ + { + "type": "linear", + "segments": ["mailcow_mailboxes_used_quota"], + "total": 100.0, + }, + ], +}) + #perfometer_info.append({ # "type": "stacked", # "perfometers": [ diff --git a/local/share/check_mk/web/plugins/wato/mailcow_mailboxes_rules.py b/local/share/check_mk/web/plugins/wato/mailcow_mailboxes_rules.py new file mode 100644 index 0000000..9ea4a38 --- /dev/null +++ b/local/share/check_mk/web/plugins/wato/mailcow_mailboxes_rules.py @@ -0,0 +1,63 @@ +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_mailboxes(): + return TextAscii( + title=_("Domain") + ) + +def _parameter_spec_mailcow_mailboxes(): + return Dictionary( + elements=[ + ("levels_mailcow_mailboxes_quota_used", Tuple( + title=_("Mailcow mailbox quota usage for storage"), + elements=[ + Percentage( + title=_("Warning at"), + default_value=65.0, + ), + Percentage( + title=_("Critical at"), + default_value=85.0, + ) + ], + )), + ("levels_mailcow_mailboxes_num_messages", Tuple( + title=_("Number of messages in mailbox"), + elements=[ + Integer( + title=_("Warning at"), + default_value=1000, + ), + Integer( + title=_("Critical at"), + default_value=2500, + ) + ], + )), + ], + ) + +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name="mailcow_mailboxes", + group=RulespecGroupCheckParametersApplications, + match_type="dict", + item_spec=_item_spec_mailcow_mailboxes, + parameter_valuespec=_parameter_spec_mailcow_mailboxes, + title=lambda: _("Levels for Mailcow mailboxes"), + ) +) \ No newline at end of file diff --git a/mkp/Mailcow-0.9.9.mkp b/mkp/Mailcow-0.9.9.mkp new file mode 100755 index 0000000000000000000000000000000000000000..980f12cf4112608002459ac07c53a36c1dd4ff52 GIT binary patch literal 10205 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