From cc2aaee90832a3867dbf843ca6c39ecfc1b620b9 Mon Sep 17 00:00:00 2001 From: cmk-bonobo Date: Sat, 5 Apr 2025 11:47:59 +0200 Subject: [PATCH] initial upload of version 2.1.0 which uses the new plugin API --- README.md | 23 +- .../plugins/agent_based/mailcow_domains.py | 162 ------- .../base/plugins/agent_based/mailcow_info.py | 142 ------- .../plugins/agent_based/mailcow_mailboxes.py | 155 ------- local/share/check_mk/checks/agent_mailcow | 19 - .../web/plugins/metrics/mailcow_metrics.py | 72 ---- .../plugins/perfometer/mailcow_perfometers.py | 44 -- .../web/plugins/wato/mailcow_domains_rules.py | 115 ----- .../web/plugins/wato/mailcow_info_rules.py | 87 ---- .../plugins/wato/mailcow_mailboxes_rules.py | 65 --- .../web/plugins/wato/mailcow_params.py | 98 ----- .../mailcow_domains.cpython-312.pyc | Bin 0 -> 6499 bytes .../__pycache__/mailcow_info.cpython-312.pyc | Bin 0 -> 4439 bytes .../mailcow_mailboxes.cpython-312.pyc | Bin 0 -> 6058 bytes mailcow/agent_based/mailcow_domains.py | 219 ++++++++++ mailcow/agent_based/mailcow_info.py | 155 +++++++ mailcow/agent_based/mailcow_mailboxes.py | 211 +++++++++ .../checkman/mailcow_domains | 2 +- .../checkman/mailcow_info | 0 .../checkman/mailcow_mailboxes | 0 .../__pycache__/graph_mailcow.cpython-312.pyc | Bin 0 -> 3330 bytes mailcow/graphing/graph_mailcow.py | 112 +++++ .../special => mailcow/libexec}/agent_mailcow | 400 ++++++------------ .../rs_mailcow_domains.cpython-312.pyc | Bin 0 -> 2782 bytes .../rs_mailcow_info.cpython-312.pyc | Bin 0 -> 2144 bytes .../rs_mailcow_mailboxes.cpython-312.pyc | Bin 0 -> 1770 bytes .../rs_mailcow_params.cpython-312.pyc | Bin 0 -> 3199 bytes mailcow/rulesets/rs_mailcow_domains.py | 93 ++++ mailcow/rulesets/rs_mailcow_info.py | 65 +++ mailcow/rulesets/rs_mailcow_mailboxes.py | 57 +++ mailcow/rulesets/rs_mailcow_params.py | 98 +++++ .../__pycache__/agent_mailcow.cpython-312.pyc | Bin 0 -> 2084 bytes mailcow/server_side_calls/agent_mailcow.py | 48 +++ mkp/Mailcow-2.1.0.mkp | Bin 0 -> 13310 bytes 34 files changed, 1204 insertions(+), 1238 deletions(-) delete mode 100644 local/lib/python3/cmk/base/plugins/agent_based/mailcow_domains.py delete mode 100644 local/lib/python3/cmk/base/plugins/agent_based/mailcow_info.py delete mode 100644 local/lib/python3/cmk/base/plugins/agent_based/mailcow_mailboxes.py delete mode 100644 local/share/check_mk/checks/agent_mailcow delete mode 100644 local/share/check_mk/web/plugins/metrics/mailcow_metrics.py delete mode 100644 local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py delete mode 100644 local/share/check_mk/web/plugins/wato/mailcow_domains_rules.py delete mode 100644 local/share/check_mk/web/plugins/wato/mailcow_info_rules.py delete mode 100644 local/share/check_mk/web/plugins/wato/mailcow_mailboxes_rules.py delete mode 100644 local/share/check_mk/web/plugins/wato/mailcow_params.py create mode 100644 mailcow/agent_based/__pycache__/mailcow_domains.cpython-312.pyc create mode 100644 mailcow/agent_based/__pycache__/mailcow_info.cpython-312.pyc create mode 100644 mailcow/agent_based/__pycache__/mailcow_mailboxes.cpython-312.pyc create mode 100644 mailcow/agent_based/mailcow_domains.py create mode 100644 mailcow/agent_based/mailcow_info.py create mode 100644 mailcow/agent_based/mailcow_mailboxes.py rename {local/share/check_mk => mailcow}/checkman/mailcow_domains (95%) rename {local/share/check_mk => mailcow}/checkman/mailcow_info (100%) rename {local/share/check_mk => mailcow}/checkman/mailcow_mailboxes (100%) create mode 100644 mailcow/graphing/__pycache__/graph_mailcow.cpython-312.pyc create mode 100644 mailcow/graphing/graph_mailcow.py rename {local/share/check_mk/agents/special => mailcow/libexec}/agent_mailcow (58%) create mode 100644 mailcow/rulesets/__pycache__/rs_mailcow_domains.cpython-312.pyc create mode 100644 mailcow/rulesets/__pycache__/rs_mailcow_info.cpython-312.pyc create mode 100644 mailcow/rulesets/__pycache__/rs_mailcow_mailboxes.cpython-312.pyc create mode 100644 mailcow/rulesets/__pycache__/rs_mailcow_params.cpython-312.pyc create mode 100644 mailcow/rulesets/rs_mailcow_domains.py create mode 100644 mailcow/rulesets/rs_mailcow_info.py create mode 100644 mailcow/rulesets/rs_mailcow_mailboxes.py create mode 100644 mailcow/rulesets/rs_mailcow_params.py create mode 100644 mailcow/server_side_calls/__pycache__/agent_mailcow.cpython-312.pyc create mode 100644 mailcow/server_side_calls/agent_mailcow.py create mode 100644 mkp/Mailcow-2.1.0.mkp diff --git a/README.md b/README.md index b9745d3..3ef82a7 100644 --- a/README.md +++ b/README.md @@ -12,22 +12,21 @@ Mailboxes with quota usage, number of messages, last logins via IMAP/POP3/SMTP ## Caveats: Tested only with dockerized versions of Mailcow -Tested with Mailcow version 2022-07a and higher +Tested with Mailcow version 2023-01 and higher Please feel free to add other working environments via review/email. -## Upgrade from older MKPs (before 1.2.0): -If you upgrade from a already installed version before 1.2.0, you have to re-create your rules for the "Mailcow Server Information" (reason: massive parameter changes). +Upgrade from older MKPs (before 2.1.0): If you upgrade from a already installed version before 2.1.0, you have to re-create your rules for the "Mailcow Server Information" (reason: parameter handling changes and migration to the new plugin API). +HINT: It is a good idea to take a screenshot from all other Mailcow related rules as well. -## Steps to accomplish this without problems: - -1. Take a screenshot of your settings in the above mentioned ruleset -2. Assure that you have access to the API keys you used within the current rules -3. Delete all rules for "Mailcow Server Information" -4. Install and enable the new MKP -5. Re-create your rules with the previously saved information from steps 1 and 2 -6. Apply your changes +Steps to accomplish this without problems: +Take a screenshot of your settings in the above mentioned ruleset +Assure that you have access to the API keys you used within the current rules +Delete all rules for "Mailcow Server Information" +Install and enable the new MKP +Re-create your rules with the previously saved information from steps 1 and 2 +Apply your changes ## General installation instructions: @@ -57,6 +56,8 @@ If you upgrade from a already installed version before 1.2.0, you have to re-cre ## Version history: +2025/03/30 2.1.0 Migration to new plugin API done + 2024/01/26 1.2.0 Added password store option for the API key, added version check 2023/09/22: 1.1.4 Bugfix for the last bugfix :-) 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 deleted file mode 100644 index f544ad5..0000000 --- a/local/lib/python3/cmk/base/plugins/agent_based/mailcow_domains.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/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"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)) - - # 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 (percent) - 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 (percent) - 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)) - - # create service for number of configured aliases (absolute) - warn, crit = params["levels_mailcow_domains_num_aliases"] - levels = (warn, crit) - state_aliases = getStateUpper(levels, number_of_aliases) - yield Metric("mailcow_domains_configured_aliases", number_of_aliases, levels=levels) - notice = f"Number of aliases: {number_of_aliases}, max {max_number_of_aliases} allowed" - yield(Result(state=state_aliases, notice=notice)) - - # create service for number of configured mailboxes (absolute) - warn, crit = params["levels_mailcow_domains_num_mailboxes"] - levels = (warn, crit) - state_mailboxes = getStateUpper(levels, number_of_mailboxes) - yield Metric("mailcow_domains_configured_mailboxes", number_of_mailboxes, levels=levels) - notice = f"Number of mailboxes: {number_of_mailboxes}, max {max_number_of_mailboxes} allowed" - yield(Result(state=state_mailboxes, 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_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 - # 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), - "levels_mailcow_domains_num_mailboxes": (100, 250), - }, - check_ruleset_name="mailcow_domains", -) \ No newline at end of file diff --git a/local/lib/python3/cmk/base/plugins/agent_based/mailcow_info.py b/local/lib/python3/cmk/base/plugins/agent_based/mailcow_info.py deleted file mode 100644 index 3dabad3..0000000 --- a/local/lib/python3/cmk/base/plugins/agent_based/mailcow_info.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python3 -from pprint import pprint -from .agent_based_api.v1 import 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 getStateLower(levels, value): - warn, crit = levels - if value < crit: - return State.CRIT - if value < warn: - return State.WARN - return State.OK - - -def discover_mailcow_info(section): - yield (Service()) - - -def check_mailcow_info(params, section): - for key in section: - if key == "mailcow": - # get thresholds - levels_num_domains = params["levels_num_domains"] - levels_num_mailboxes = params["levels_num_mailboxes"] - levels_num_global_messages = params["levels_num_global_messages"] - version = section[key]["version"] - git_version = section[key]["git_version"] - check_version_enabled = section[key]["check_version_enabled"] - update_available = section[key]["update_available"] - num_domains = section[key]["num_domains"] - num_mailboxes = section[key]["num_mailboxes"] - num_global_messages = section[key]["num_global_messages"] - - # create graphs for number of domains, mailboxes and messages - yield (Metric("mc_num_domains", num_domains, levels=levels_num_domains)) - yield ( - Metric("mc_num_mailboxes", num_mailboxes, levels=levels_num_mailboxes) - ) - yield ( - Metric( - "mc_num_global_messages", - num_global_messages, - levels=levels_num_global_messages, - ) - ) - - # create overall result - if check_version_enabled: - if update_available: - summary = f"Update available: Running version is {version}, Github version is: {git_version}" - state = State.WARN - else: - summary = f"No update available: Running version is {version}, Github version is: {git_version}" - state = State.OK - else: - summary = f"Version is {version}, Update check is disabled" - state = State.OK - details = f"Mailcow version: {version}\nNumber of domains: {num_domains}\nNumber of mailboxes: {num_mailboxes}\nNumber of messages: {num_global_messages}" - yield Result(state=state, summary=summary, details=details) - - # Create result for number of domains - warn, crit = levels_num_domains - state = getStateUpper((warn, crit), num_domains) - notice = f"Number of domains: {num_domains}" - if state != State.OK: - yield (Result(state=state, notice=notice)) - - # Create result for number of mailboxes - warn, crit = levels_num_mailboxes - state = getStateUpper((warn, crit), num_mailboxes) - notice = f"Number of mailboxes: {num_mailboxes}" - if state != State.OK: - yield (Result(state=state, notice=notice)) - - # Create result for number of global messages - warn, crit = levels_num_global_messages - state = getStateUpper((warn, crit), num_global_messages) - notice = f"Number of messages: {num_global_messages}" - if state != State.OK: - yield (Result(state=state, notice=notice)) - - -def parse_mailcow_info_section(string_table): - # pprint(string_table) - parsed_data = { - "mailcow": {}, - } - # we expect only one line - line = string_table[0] - version = line[0] - num_domains = int(line[1]) - num_mailboxes = int(line[2]) - num_global_messages = int(line[3]) - git_version = line[4] - update_available = line[5] - if update_available == "True": - update_available = True - else: - update_available = False - check_version_enabled = line[6] - if check_version_enabled == "True": - check_version_enabled = True - else: - check_version_enabled = False - parsed_data["mailcow"]["version"] = version - parsed_data["mailcow"]["num_domains"] = num_domains - parsed_data["mailcow"]["num_mailboxes"] = num_mailboxes - parsed_data["mailcow"]["num_global_messages"] = num_global_messages - parsed_data["mailcow"]["git_version"] = git_version - parsed_data["mailcow"]["update_available"] = update_available - parsed_data["mailcow"]["check_version_enabled"] = check_version_enabled - # pprint(parsed_data) - return parsed_data - - -register.agent_section( - name="mailcow_info", - parse_function=parse_mailcow_info_section, -) - - -register.check_plugin( - name="mailcow_info", - service_name="Mailcow Info", - discovery_function=discover_mailcow_info, - check_function=check_mailcow_info, - check_default_parameters={ - "levels_num_domains": (100, 200), - "levels_num_mailboxes": (500, 1000), - "levels_num_global_messages": (100000, 250000), - }, - check_ruleset_name="mailcow_info", -) 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 deleted file mode 100644 index 0213e18..0000000 --- a/local/lib/python3/cmk/base/plugins/agent_based/mailcow_mailboxes.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/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]) - # calculate storage used for all messages in mailbox - quota = int(line[7]) - total_number_of_bytes_used = int(line[8]) - if quota == 0: - # quota is not set, if this is the case, line[6] contains no numeric value, but the char "-" - # so set all usage counters to zero - percent_in_use = 0 - percent_storage_used_for_messages = 0 - else: - # percent in use, rounded to full percent (calculated by Mailcow) - percent_in_use = int(line[6]) - # let's calculate our own value - 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/checks/agent_mailcow b/local/share/check_mk/checks/agent_mailcow deleted file mode 100644 index c6607f8..0000000 --- a/local/share/check_mk/checks/agent_mailcow +++ /dev/null @@ -1,19 +0,0 @@ -def agent_mailcow_arguments(params, hostname, ipaddress): - return [ - "--hostname", - params["hostname"], - "--apikey", - passwordstore_get_cmdline("%s", params["apikey"]), - "--port", - params["port"], - "--check-version", - params["check_version"], - "--no-https", - params["no_https"], - "--no-cert-check", - params["no_cert_check"], - ipaddress, - ] - - -special_agent_info["mailcow"] = agent_mailcow_arguments diff --git a/local/share/check_mk/web/plugins/metrics/mailcow_metrics.py b/local/share/check_mk/web/plugins/metrics/mailcow_metrics.py deleted file mode 100644 index a9a4e00..0000000 --- a/local/share/check_mk/web/plugins/metrics/mailcow_metrics.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -from cmk.gui.i18n import _ -from cmk.gui.plugins.metrics import ( - metric_info, - graph_info, -) - -metric_info["mc_num_domains"] = { - "title": _("Number of Domains"), - "unit": "count", - "color": "44/a", -} - -metric_info["mc_num_mailboxes"] = { - "title": _("Number of Mailboxes"), - "unit": "count", - "color": "44/b", -} - -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", -} - -metric_info["mailcow_domains_configured_aliases"] = { - "title": _("Number of Configured Aliases"), - "unit": "count", - "color": "24/a", -} - -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", -} diff --git a/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py b/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py deleted file mode 100644 index dc71130..0000000 --- a/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -from cmk.gui.plugins.metrics import perfometer_info - -perfometer_info.append( - { - "type": "stacked", - "perfometers": [ - { - "type": "linear", - "segments": [ - "mc_num_domains", - "mc_num_mailboxes", - "mc_num_global_messages", - ], - }, - ], - } -) - -perfometer_info.append( - { - "type": "stacked", - "perfometers": [ - { - "type": "linear", - "segments": ["mailcow_domains_used_quota"], - "total": 100.0, - }, - ], - } -) - -perfometer_info.append( - { - "type": "stacked", - "perfometers": [ - { - "type": "linear", - "segments": ["mailcow_mailboxes_used_quota"], - "total": 100.0, - }, - ], - } -) 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 deleted file mode 100644 index 3ba6467..0000000 --- a/local/share/check_mk/web/plugins/wato/mailcow_domains_rules.py +++ /dev/null @@ -1,115 +0,0 @@ -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 name") - ) - -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 configured aliases"), - elements=[ - Integer( - title=_("Warning at"), - default_value=100, - ), - Integer( - title=_("Critical at"), - default_value=250, - ) - ], - )), - ("levels_mailcow_domains_num_mailboxes", Tuple( - title=_("Number of configured mailboxes"), - 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/local/share/check_mk/web/plugins/wato/mailcow_info_rules.py b/local/share/check_mk/web/plugins/wato/mailcow_info_rules.py deleted file mode 100644 index 058877b..0000000 --- a/local/share/check_mk/web/plugins/wato/mailcow_info_rules.py +++ /dev/null @@ -1,87 +0,0 @@ -from cmk.gui.i18n import _ -from cmk.gui.plugins.wato import ( - CheckParameterRulespecWithoutItem, - rulespec_registry, - RulespecGroupCheckParametersApplications, -) - -from cmk.gui.valuespec import ( - Dictionary, - ListChoice, - Tuple, - Percentage, - Integer, - Float, -) - - -def _parameter_spec_mailcow_info(): - return Dictionary( - elements=[ - ( - "levels_num_domains", - Tuple( - title=_("Number of email domains"), - elements=[ - Integer( - title=_("Warning at"), - size=32, - default_value=100, - ), - Integer( - title=_("Critical at"), - size=32, - default_value=200, - ), - ], - ), - ), - ( - "levels_num_mailboxes", - Tuple( - title=_("Number of mailboxes"), - elements=[ - Integer( - title=_("Warning at"), - size=32, - default_value=500, - ), - Integer( - title=_("Critical at"), - size=32, - default_value=1000, - ), - ], - ), - ), - ( - "levels_num_global_messages", - Tuple( - title=_("Number of messages"), - elements=[ - Integer( - title=_("Warning at"), - size=32, - default_value=100000, - ), - Integer( - title=_("Critical at"), - size=32, - default_value=250000, - ), - ], - ), - ), - ], - ) - - -rulespec_registry.register( - CheckParameterRulespecWithoutItem( - check_group_name="mailcow_info", - group=RulespecGroupCheckParametersApplications, - match_type="dict", - parameter_valuespec=_parameter_spec_mailcow_info, - title=lambda: _("Levels for Mailcow info"), - ) -) 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 deleted file mode 100644 index 85c5f6d..0000000 --- a/local/share/check_mk/web/plugins/wato/mailcow_mailboxes_rules.py +++ /dev/null @@ -1,65 +0,0 @@ -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=_("Mailbox name") - ) - -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"), - size=32, - default_value=1000, - ), - Integer( - title=_("Critical at"), - size=32, - 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/local/share/check_mk/web/plugins/wato/mailcow_params.py b/local/share/check_mk/web/plugins/wato/mailcow_params.py deleted file mode 100644 index c9dfc76..0000000 --- a/local/share/check_mk/web/plugins/wato/mailcow_params.py +++ /dev/null @@ -1,98 +0,0 @@ -from cmk.gui.i18n import _ - -from cmk.gui.plugins.wato.special_agents.common import ( - RulespecGroupDatasourceProgramsApps, - RulespecGroupDatasourceProgramsCustom, - RulespecGroupDatasourceProgramsHardware, - RulespecGroupDatasourceProgramsOS, - RulespecGroupDatasourceProgramsTesting, -) - -from cmk.gui.plugins.wato.utils import ( - IndividualOrStoredPassword, - rulespec_registry, - CheckParameterRulespecWithItem, - CheckParameterRulespecWithoutItem, - HostRulespec, - Rulespec, -) - -from cmk.gui.valuespec import ( - Dictionary, - ListChoice, - Checkbox, - TextAscii, - Password, - NetworkPort, - Integer, -) - - -def _valuespec_special_agent_mailcow(): - return Dictionary( - title=_("Mailcow Server Information"), - help=_("Checking Mailcow instances via API"), - elements=[ - ( - "hostname", - TextAscii( - title=_("Hostname"), - size=32, - allow_empty=False, - help=_("Hostname of Mailcow server (bare FQDN or IP), mandatory"), - ), - ), - ( - "apikey", - IndividualOrStoredPassword( - title=_("API Key"), - size=32, - allow_empty=False, - help=_("API Key, mandatory"), - ), - ), - ( - "port", - TextAscii( - title=_("Port"), - allow_empty=True, - help=_("Specify port if not listening to HTTPS/HTTP, optional"), - ), - ), - ( - "check_version", - Checkbox( - title=_("Check version"), - help=_("Checks the running version against the public Github repo"), - ), - ), - ( - "no_https", - Checkbox( - title=_("Disable HTTPS"), - help=_( - "Activate to disable TLS encryption (not recommended), optional" - ), - ), - ), - ( - "no_cert_check", - Checkbox( - title=_("Disable certificate validation"), - help=_( - "Activate to disable certificate validation (not recommended), optional" - ), - ), - ), - ], - optional_keys=[], - ) - - -rulespec_registry.register( - HostRulespec( - group=RulespecGroupDatasourceProgramsApps, - name="special_agents:mailcow", - valuespec=_valuespec_special_agent_mailcow, - ) -) diff --git a/mailcow/agent_based/__pycache__/mailcow_domains.cpython-312.pyc b/mailcow/agent_based/__pycache__/mailcow_domains.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cbabcb2165e54212cd2b6ae010b0254a8c3574d7 GIT binary patch literal 6499 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 literal 0 HcmV?d00001 diff --git a/mailcow/agent_based/__pycache__/mailcow_mailboxes.cpython-312.pyc b/mailcow/agent_based/__pycache__/mailcow_mailboxes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3ec3d3ee3d12d46e0dd3cb420702d390b7cea5a GIT binary patch literal 6058 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 literal 0 HcmV?d00001 diff --git a/mailcow/agent_based/mailcow_domains.py b/mailcow/agent_based/mailcow_domains.py new file mode 100644 index 0000000..6e3b871 --- /dev/null +++ b/mailcow/agent_based/mailcow_domains.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# pylint: disable=too-many-locals, line-too-long, too-many-statements +"""Mailcow check for domains""" + + +# import necessary elements from API version 2 +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + Service, + Result, + State, + Metric, + render, +) + + +def get_state_upper( + levels: tuple[int | float, int | float], value: int | float +) -> State: + """returns OK/WARN/CRIT depending on the given parameters""" + warn, crit = levels + if value >= crit: + return State.CRIT + if value >= warn: + return State.WARN + return State.OK + + +def parse_mailcow_domains(string_table): + """the parse function""" + parsed_data = {} + # convert the raw output of the agent section to an meaningful structure + # do type conversions and so on + 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_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 + # 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 + + +def discover_mailcow_domains(section): + """the discover function""" + # since we have a service with item here we must create one service per item + for key in section: + yield Service(item=key) + + +def check_mailcow_domains(item, params, section): + """the check function""" + domain = section.get(item) + if not domain: + # if a previously found domain does not exist anymore, create a meaningful result + yield Result( + state=State.UNKNOWN, + summary="Domain not found anymore, maybe it was deleted?", + ) + return + + # get all infos regarding the domain + active = domain[0] + create_time = domain[1] + modify_time = domain[2] + max_number_of_mailboxes = domain[3] + number_of_mailboxes = domain[4] + percent_used_mailboxes = domain[5] + max_number_of_aliases = domain[6] + number_of_aliases = domain[7] + percent_used_aliases = domain[8] + total_number_of_messages = domain[9] + total_number_of_bytes_used = domain[10] + quota = domain[11] + percent_storage_used_for_messages = domain[12] + + # create (main) service for used storage (domain quota) + _type, levels = params["levels_mailcow_domains_quota_used"] + state_quota = get_state_upper(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"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) + + # create some additional services and information only in service 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 (percent) + _type, levels = params["levels_mailcow_domains_mailboxes_used"] + state_mailboxes = get_state_upper(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 (percent) + _type, levels = params["levels_mailcow_domains_aliases_used"] + state_aliases = get_state_upper(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 + _type, levels = params["levels_mailcow_domains_num_messages"] + state_messages = get_state_upper(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) + + # create service for number of configured aliases (absolute) + _type, levels = params["levels_mailcow_domains_num_aliases"] + state_aliases = get_state_upper(levels, number_of_aliases) + yield Metric("mailcow_domains_configured_aliases", number_of_aliases, levels=levels) + notice = ( + f"Number of aliases: {number_of_aliases}, max {max_number_of_aliases} allowed" + ) + yield Result(state=state_aliases, notice=notice) + + # create service for number of configured mailboxes (absolute) + _type, levels = params["levels_mailcow_domains_num_mailboxes"] + state_mailboxes = get_state_upper(levels, number_of_mailboxes) + yield Metric( + "mailcow_domains_configured_mailboxes", number_of_mailboxes, levels=levels + ) + notice = f"Number of mailboxes: {number_of_mailboxes}, max {max_number_of_mailboxes} allowed" + yield Result(state=state_mailboxes, notice=notice) + + +# create the new agent section, must begin with "agent_section_" +# and must be an instance of "AgentSection" +agent_section_mailcow_domains = AgentSection( + # "name" must exactly match the section name within the agent output + name="mailcow_domains", + # define the parse function, name is arbitrary, a good choice is to choose + # "parse_" as prefix and append the section name + parse_function=parse_mailcow_domains, +) + +# create the new check plugin, must begin with "check_plugin_" +# and must be an instance of "CheckPlugin" +check_plugin_mailcow_domains = CheckPlugin( + # "name" should be the same as the corresponding section within the agent output + name="mailcow_domains", + # this is a service with item, so you have to include a place holder for the item id + service_name="Mailcow domain %s", + # define the discovery function, name is arbitrary, a good choice is to choose + # "discover_" as prefix and append the section name + discovery_function=discover_mailcow_domains, + # define the check function, name is arbitrary, a good choice is to choose + # "check_" as prefix and append the section name + check_function=check_mailcow_domains, + # define the default parameters + check_default_parameters={ + "levels_mailcow_domains_quota_used": ("fixed", (65.0, 85.0)), + "levels_mailcow_domains_mailboxes_used": ("fixed", (65.0, 85.0)), + "levels_mailcow_domains_aliases_used": ("fixed", (65.0, 85.0)), + "levels_mailcow_domains_num_messages": ("fixed", (10_000, 25_000)), + "levels_mailcow_domains_num_aliases": ("fixed", (100, 250)), + "levels_mailcow_domains_num_mailboxes": ("fixed", (100, 250)), + }, + # connect to the ruleset where parameters can be defined + # must match the name of the ruleset exactly + check_ruleset_name="mailcow_domains", +) diff --git a/mailcow/agent_based/mailcow_info.py b/mailcow/agent_based/mailcow_info.py new file mode 100644 index 0000000..a7c208f --- /dev/null +++ b/mailcow/agent_based/mailcow_info.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# pylint: disable=line-too-long, simplifiable-if-statement, missing-module-docstring, too-many-locals + +# import necessary elements from API version 2 +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + Service, + Result, + State, + Metric, +) + + +def get_state_upper( + levels: tuple[int | float, int | float], value: int | float +) -> State: + """returns OK/WARN/CRIT depending on the given parameters""" + warn, crit = levels + if value >= crit: + return State.CRIT + if value >= warn: + return State.WARN + return State.OK + + +def parse_mailcow_info(string_table) -> dict: + """the parse function""" + parsed_data = {} + # we only expect one line with 7 entries + line = string_table[0] + version = line[0] + num_domains = int(line[1]) + num_mailboxes = int(line[2]) + num_global_messages = int(line[3]) + git_version = line[4] + update_available = line[5] + if update_available == "True": + update_available = True + else: + update_available = False + check_version_enabled = line[6] + if check_version_enabled == "True": + check_version_enabled = True + else: + check_version_enabled = False + parsed_data["version"] = version + parsed_data["num_domains"] = num_domains + parsed_data["num_mailboxes"] = num_mailboxes + parsed_data["num_global_messages"] = num_global_messages + parsed_data["git_version"] = git_version + parsed_data["update_available"] = update_available + parsed_data["check_version_enabled"] = check_version_enabled + return parsed_data + + +def discover_mailcow_info(section): + """the discover function""" + yield Service() + + +def check_mailcow_info(params, section): + """the check function""" + # get thresholds + _type, levels_num_domains = params["levels_num_domains"] + _type, levels_num_mailboxes = params["levels_num_mailboxes"] + _type, levels_num_global_messages = params["levels_num_global_messages"] + # get all section data + version: str = section["version"] + git_version: str = section["git_version"] + check_version_enabled: bool = section["check_version_enabled"] + update_available: bool = section["update_available"] + num_domains: int = section["num_domains"] + num_mailboxes: int = section["num_mailboxes"] + num_global_messages: int = section["num_global_messages"] + + # create graphs for number of domains, mailboxes and messages + yield Metric(name="mc_num_domains", value=num_domains, levels=levels_num_domains) + yield Metric( + name="mc_num_mailboxes", value=num_mailboxes, levels=levels_num_mailboxes + ) + yield Metric( + name="mc_num_global_messages", + value=num_global_messages, + levels=levels_num_global_messages, + ) + + # create overall result + if check_version_enabled: + if update_available: + summary = f"Update available: Running version is {version}, Github version is: {git_version}" + state = State.WARN + else: + summary = f"No update available: Running version is {version}, Github version is: {git_version}" + state = State.OK + else: + summary = f"Version is {version}, Update check is disabled" + state = State.OK + details = f"Mailcow version: {version}\nNumber of domains: {num_domains}\nNumber of mailboxes: {num_mailboxes}\nNumber of messages: {num_global_messages}" + yield Result(state=state, summary=summary, details=details) + + # Create result for number of domains + warn, crit = levels_num_domains + state = get_state_upper((warn, crit), num_domains) + notice = f"Number of domains: {num_domains}" + if state != State.OK: + yield Result(state=state, notice=notice) + + # Create result for number of mailboxes + warn, crit = levels_num_mailboxes + state = get_state_upper((warn, crit), num_mailboxes) + notice = f"Number of mailboxes: {num_mailboxes}" + if state != State.OK: + yield Result(state=state, notice=notice) + + # Create result for number of global messages + warn, crit = levels_num_global_messages + state = get_state_upper((warn, crit), num_global_messages) + notice = f"Number of messages: {num_global_messages}" + if state != State.OK: + yield Result(state=state, notice=notice) + + +# create the new agent section, must begin with "agent_section_" +# and must be an instance of "AgentSection" +agent_section_mailcow_info = AgentSection( + # "name" must exactly match the section name within the agent output + name="mailcow_info", + # define the parse function, name is arbitrary, a good choice is to choose + # "parse_" as prefix and append the section name + parse_function=parse_mailcow_info, +) + +# create the new check plugin, must begin with "check_plugin_" +# and must be an instance of "CheckPlugin" +check_plugin_mailcow_info = CheckPlugin( + # "name" should be the same as the corresponding section within the agent output + name="mailcow_info", + service_name="Mailcow info", + # define the discovery function, name is arbitrary, a good choice is to choose + # "discover_" as prefix and append the section name + discovery_function=discover_mailcow_info, + # define the check function, name is arbitrary, a good choice is to choose + # "check_" as prefix and append the section name + check_function=check_mailcow_info, + # define the default parameters + check_default_parameters={ + "levels_num_domains": ("fixed", (100, 200)), + "levels_num_mailboxes": ("fixed", (500, 1000)), + "levels_num_global_messages": ("fixed", (100_000, 250_000)), + }, + # connect to the ruleset where parameters can be defined + # must match the name of the ruleset exactly + check_ruleset_name="mailcow_info", +) diff --git a/mailcow/agent_based/mailcow_mailboxes.py b/mailcow/agent_based/mailcow_mailboxes.py new file mode 100644 index 0000000..a48cdb3 --- /dev/null +++ b/mailcow/agent_based/mailcow_mailboxes.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +# pylint: disable=too-many-locals, line-too-long, too-many-statements, too-many-branches +"""Mailcow check for mailboxes""" + +import time + +# import necessary elements from API version 2 +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + Service, + Result, + State, + Metric, + render, +) + + +def get_state_upper( + levels: tuple[int | float, int | float], value: int | float +) -> State: + """returns OK/WARN/CRIT depending on the given parameters""" + warn, crit = levels + if value >= crit: + return State.CRIT + if value >= warn: + return State.WARN + return State.OK + + +def parse_mailcow_mailboxes(string_table): + """the parse function""" + parsed_data = {} + # convert the raw output of the agent section to an meaningful structure + # do type conversions and so on + 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]) + # calculate storage used for all messages in mailbox + quota = int(line[7]) + total_number_of_bytes_used = int(line[8]) + if quota == 0: + # quota is not set, if this is the case, line[6] contains no numeric value, but the char "-" + # so set all usage counters to zero + percent_in_use = 0 + percent_storage_used_for_messages = 0 + else: + # percent in use, rounded to full percent (calculated by Mailcow) + percent_in_use = int(line[6]) + # let's calculate our own value + 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 + + +def discover_mailcow_mailboxes(section): + """the discover function""" + # since we have a service with item here we must create one service per item + for key in section: + yield Service(item=key) + + +def check_mailcow_mailboxes(item, params, section): + """the check function""" + mailbox = section.get(item) + if not mailbox: + # if a previously found domain does not exist anymore, create a meaningful result + yield Result( + state=State.UNKNOWN, + summary="Mailbox not found anymore, maybe it was deleted?", + ) + return + + # get all infos regarding the mailbox + active = mailbox[0] + create_time = mailbox[1] + modify_time = mailbox[2] + display_name = mailbox[3] + number_of_messages = mailbox[4] + _percent_in_use = mailbox[5] + quota = mailbox[6] + total_number_of_bytes_used = mailbox[7] + percent_storage_used_for_messages = mailbox[8] + last_imap_login_data = mailbox[9] + last_pop3_login_data = mailbox[10] + last_smtp_login_data = mailbox[11] + + # create (main) service for used storage (mailbox quota) + _type, levels = params["levels_mailcow_mailboxes_quota_used"] + state_quota = get_state_upper(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 + _type, levels = params["levels_mailcow_mailboxes_num_messages"] + state_messages = get_state_upper(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) + + +# create the new agent section, must begin with "agent_section_" +# and must be an instance of "AgentSection" +agent_section_mailcow_mailboxes = AgentSection( + # "name" must exactly match the section name within the agent output + name="mailcow_mailboxes", + # define the parse function, name is arbitrary, a good choice is to choose + # "parse_" as prefix and append the section name + parse_function=parse_mailcow_mailboxes, +) + +# create the new check plugin, must begin with "check_plugin_" +# and must be an instance of "CheckPlugin" +check_plugin_mailcow_mailboxes = CheckPlugin( + # "name" should be the same as the corresponding section within the agent output + name="mailcow_mailboxes", + # this is a service with item, so you have to include a place holder for the item id + service_name="Mailcow mailbox %s", + # define the discovery function, name is arbitrary, a good choice is to choose + # "discover_" as prefix and append the section name + discovery_function=discover_mailcow_mailboxes, + # define the check function, name is arbitrary, a good choice is to choose + # "check_" as prefix and append the section name + check_function=check_mailcow_mailboxes, + # define the default parameters + check_default_parameters={ + "levels_mailcow_mailboxes_quota_used": ("fixed", (65.0, 85.0)), + "levels_mailcow_mailboxes_num_messages": ("fixed", (1000, 2500)), + }, + # connect to the ruleset where parameters can be defined + # must match the name of the ruleset exactly + check_ruleset_name="mailcow_mailboxes", +) diff --git a/local/share/check_mk/checkman/mailcow_domains b/mailcow/checkman/mailcow_domains similarity index 95% rename from local/share/check_mk/checkman/mailcow_domains rename to mailcow/checkman/mailcow_domains index d81eb5f..b6eea3e 100644 --- a/local/share/check_mk/checkman/mailcow_domains +++ b/mailcow/checkman/mailcow_domains @@ -17,4 +17,4 @@ description: item: domainname inventory: - one service is created for each domain \ No newline at end of file + one service is created for each email domain \ No newline at end of file diff --git a/local/share/check_mk/checkman/mailcow_info b/mailcow/checkman/mailcow_info similarity index 100% rename from local/share/check_mk/checkman/mailcow_info rename to mailcow/checkman/mailcow_info diff --git a/local/share/check_mk/checkman/mailcow_mailboxes b/mailcow/checkman/mailcow_mailboxes similarity index 100% rename from local/share/check_mk/checkman/mailcow_mailboxes rename to mailcow/checkman/mailcow_mailboxes diff --git a/mailcow/graphing/__pycache__/graph_mailcow.cpython-312.pyc b/mailcow/graphing/__pycache__/graph_mailcow.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..21ba26ce258ff43df092f042f7da17c5ddf8535e GIT binary patch literal 3330 zcmb7`&vVm86vtPJ?O2NKqyZ9aCys0!g9BJ0P&(}}ouMRz4#j2CGM(z8Q7px%NLDJ@ zl;l6)#If!4(oQe!gukPgUXsxP*hBG5rw0z5X$DT5`gZlhik-NrX1vnw`@YY6yZcu1 z@98v;;QHnSq!NvIxudl@r`WC53LMo%O24k`k%aG?0W5kR}F*7d3X+LHp z%u#97OiD?UlQ@%?csd?8#>|wIGSgDp%t#pqu}I_81hlL=y2E&JpvS{6sV0Mw;I55+ zRS1&m-JfzhkQce63CbhhtU4BW4dsJie?o$Z*|i zE5sis?ocL<-2-5C1Sf77mZPbdziBl(4)w4ZjMU@8r*)rMdsWnUvuqd;_T znR%QWL}o%-t5@0)Irplo?p)@_*PsVLLeq@ zj|+z~Li;O~l>dkg-(ZBtHb~9NHYs_WbhM@kJMGk~B|Iua!8T+Wk2lN*wct$E9$vD^ zVZ~E@qn^UC$#>F@6UB7D!as_3f=EV5lOzrK_TzlNcr!ddj!phI*khzg5$)LW^|jmb zr)$^it1I}FN0kqrG0aiTjW4g)@$5lG-|@jbRj#hBtkj_x=*bRX;gaD(&kznq>J%p_ zWNl>`&QgfzDTA_zT7!b^oiv>9`3BaI;m_!!PwuTi(T6btT~HypK~osKJZS6HSWtm} zoVy>_Z{x~PhhAzzZ?nkK1vy1#RywE%T_bDwD|!u^i{5@ATBa&GdRucuReQK}Sv0JM zVu*&mE^cnM?^~_QA{2|Ps4Be6#Z9BrgqNyFknjo3zTAm;2}sQ40HeTi0TX!=>mn%Kz$Di@}JwE?tERkh*(W(6mDxU3fMJhk<@)!E$PsGO}h>IYe1#!OE zWK@A8#?@iRWR$LBzv2l@2=NmM%5<<3(1lFMKA{DODEBj;19~1(nLX3xN>qNu z<=^d>`>Qew;w*>@ None: - sys.stderr.write( - """CheckMK Mailcow Special Agent - -USAGE: agent_nextcloud -H [hostname] -k [apikey] - agent_nextcloud -h - -OPTIONS: - -H, --hostname Hostname (FQDN or IP) of Nextcloud server - -k, --apikey API Key - -P, --port Port - --check-version True|False If "True": Running version will be checked against official Github repo - --no-https True|False If "True": Disable HTTPS, use HTTP (not recommended!) - --no-cert-check True|False If "True": Disable TLS certificate check (not recommended!) - -h, --help Show this help message and exit -""" +def get_args() -> argparse.Namespace: + parser: argparse.ArgumentParser = argparse.ArgumentParser( + description="Mailcow server parameters" ) + parser.add_argument( + "--hostname", required=True, type=str, help="Hostname or IP of mailcow server" + ) + parser.add_argument("--apikey", required=True, type=str, help="API key") + parser.add_argument( + "--port", + required=False, + type=int, + help="Port where the server is listening on", + ) + parser.add_argument( + "--no-https", + required=False, + default=False, + help="Disable HTTPS", + action="store_true", + ) + parser.add_argument( + "--no-cert-check", + required=False, + default=False, + help="Disable certificate checks", + action="store_true", + ) + parser.add_argument( + "--check-version", + required=False, + default=False, + help="Enable version check", + action="store_true", + ) + args = parser.parse_args() + return args -# set this to True to produce debug output (this clutters the agent output) -# be aware: activating this logs very sensitive information to debug files in ~/tmp -# !!DO NOT FORGET to delete these files after debugging is done!! +MC_API_BASE: str = "api/v1/get" +TIMEOUT: int = 30 -DEBUG = False - -mc_api_base = "api/v1/get" - -opt_hostname = "" -opt_apikey = "" -opt_port = "" -opt_check_version = False -opt_no_https = False -opt_no_cert_check = False - -short_options = "hH:k:P:" -long_options = [ - "hostname=", - "apikey=", - "port=", - "check-version=", - "no-https=", - "no-cert-check=", - "help", -] - -domain_data = {} -mailbox_data = {} - -debugLogFilename = "" -SEP = "|" -# SEP = "\t" -TIMEFMT = "%Y-%m-%d %H:%M:%S" -FLOATFMT = "{:.4f}" - -""" -Decorator function for debugging purposes - creates a file with many information regarding the function call, like: - timestamp - name of function - runtime - number of arguments - number of keyword arguments - return value of function call - type of return value - all parameters given to function -""" +domain_data: dict = {} +mailbox_data: dict = {} -def debugLog(function): - def wrapper(*args, **kwargs): - # execute function and measure runtime - start = time() - value = function(*args, **kwargs) - end = time() - # get number of args and kwargs - len_args = len(args) - len_kwargs = len(kwargs) - # format the output - seconds = FLOATFMT.format(end - start) - local_time = strftime(TIMEFMT, localtime(start)) - # get function name - fname = function.__name__ - # create output - # out1: Timestamp|Name of Function|Runtime|Num Args|Num Kwargs|Return Value|Return Value Type - out1 = f"{local_time}{SEP}{fname}{SEP}{seconds}{SEP}{len_args}{SEP}{len_kwargs}{SEP}{value}{SEP}{type(value)}" - # out2: all arguments - out2 = "" - for arg in args: - out2 = out2 + SEP + str(arg) - # out3: all keyword arguments - out3 = "" - if len_kwargs > 0: - for key, val in kwargs.items(): - out3 = out3 + SEP + key + ":" + str(val) - # write output to file - if debugLogFilename != "": - try: - with open(debugLogFilename, "a+") as f: - f.write(f"{out1}{out2}{out3}\n") - except: - sys.stderr.write( - f"Something went wrong when writing to file {debugLogFilename}\n" - ) - sys.exit(1) - else: - sys.stderr.write(f"Debug activated, but no debug filename given.\n") - sys.exit(1) - return value - - return wrapper - - -def getDebugFilename(hostname: str) -> str: - home_path = os.getenv("HOME") - tmp_path = f"{home_path}/tmp" - file_name = f"{tmp_path}/mailcow_{hostname}_debug.log" - return file_name - - -def getOptions() -> None: - global opt_hostname - global opt_apikey - global opt_port - global opt_check_version - global opt_no_https - global opt_no_cert_check - - opts, args = getopt.getopt(sys.argv[1:], short_options, long_options) - for opt, arg in opts: - if opt in ["-H", "--hostname"]: - opt_hostname = arg - elif opt in ["-k", "--apikey"]: - opt_apikey = arg - elif opt in ["-P", "--port"]: - opt_port = arg - elif opt in ["--check-version"]: - if arg == "True": - opt_check_version = True - else: - opt_check_version = False - elif opt in ["--no-https"]: - if arg == "True": - opt_no_https = True - else: - opt_no_https = False - elif opt in ["--no-cert-check"]: - if arg == "True": - opt_no_cert_check = True - else: - opt_no_cert_check = False - elif opt in ["-h", "--help"]: - showUsage() - sys.exit(0) - if DEBUG: - home_path = os.getenv("HOME") - tmp_path = f"{home_path}/tmp" - help_file = f"{tmp_path}/mailcow_{opt_hostname}_debug.txt" - with open(help_file, "a") as file: - file.write( - f"Number of Arguments: {len(sys.argv)}, Argument List: {str(sys.argv)}\n" - ) - - -# @debugLog -def showOptions() -> None: - print(f"Hostname: {opt_hostname}") - print(f"Username: {opt_apikey}") - print(f"Port: {opt_port}") - print(f"Check Version: {opt_check_version}") - print(f"No HTTPS: {opt_no_https}") - print(f"No TLS Check: {opt_no_cert_check}") - home_path = os.getenv("HOME") - tmp_path = f"{home_path}/tmp" - help_file = f"{tmp_path}/mailcow_{opt_hostname}_debug.txt" - with open(help_file, "a") as file: - file.write( - f"Hostname: {opt_hostname}, Port: {opt_port}, No HTTPS: {opt_no_https}, No Cert Check: {opt_no_cert_check}\n" - ) - - -# @debugLog -def getDomainInfo(headers: str, verify: bool, base_url: str) -> int: - url = f"{base_url}/domain/all" - response = requests.get(url, headers=headers, verify=verify) +def get_domain_info(headers: str, verify: bool, base_url: str) -> int: + """retrieves info about all configured domains""" + url: str = f"{base_url}/domain/all" + response = requests.get(url=url, headers=headers, verify=verify, timeout=TIMEOUT) if response.status_code == 200: jsdata = response.text data = json.loads(jsdata) # returns a list of dictionaries @@ -243,22 +107,21 @@ def getDomainInfo(headers: str, verify: bool, base_url: str) -> int: i += 1 # return number of email domains return i - else: - sys.stderr.write( - f"Request response code is {response.status_code} with URL {url}\n" - ) - sys.exit(1) + sys.stderr.write( + f"Request response code is {response.status_code} with URL {url}\n" + ) + sys.exit(1) -# @debugLog -def getMailboxInfo(headers: str, verify: bool, base_url: str) -> tuple: - url = f"{base_url}/mailbox/all" - response = requests.get(url, headers=headers, verify=verify) +def get_mailbox_info(headers: str, verify: bool, base_url: str) -> tuple: + """retrieves info about all configured mailboxes""" + url: str = f"{base_url}/mailbox/all" + response = requests.get(url=url, headers=headers, verify=verify, timeout=TIMEOUT) if response.status_code == 200: jsdata = response.text data = json.loads(jsdata) # returns a list of dictionaries - i = 0 - global_num_messages = 0 + i: int = 0 + global_num_messages: int = 0 while i < len(data): # get status of mailbox (0-->inactive or 1-->active) active = data[i].get("active") @@ -282,7 +145,7 @@ def getMailboxInfo(headers: str, verify: bool, base_url: str) -> tuple: last_imap_login = data[i].get("last_imap_login") last_pop3_login = data[i].get("last_pop3_login") last_smtp_login = data[i].get("last_smtp_login") - mb = { + mailbox = { "active": active, "created": created, "modified": modified, @@ -297,27 +160,26 @@ def getMailboxInfo(headers: str, verify: bool, base_url: str) -> tuple: "last_smtp_login": last_smtp_login, } mailbox_data[username] = {} - mailbox_data[username] = mb + mailbox_data[username] = mailbox i += 1 global_num_messages += num_messages # return number of mailboxes and global number of messages return i, global_num_messages - else: - sys.stderr.write( - f"Request response code is {response.status_code} with URL {url}\n" - ) - sys.exit(1) + sys.stderr.write( + f"Request response code is {response.status_code} with URL {url}\n" + ) + sys.exit(1) -def getGitVersion() -> str: - url = "https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest" - git_version = "" - response = requests.get(url) +def get_git_version() -> str: + """gets the actual version of Mailcow from Github""" + url: str = "https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest" + git_version: str = "" + response = requests.get(url=url, timeout=TIMEOUT) if response.status_code == 200: jsdata = response.text data = json.loads(jsdata) # returns a dictionary git_version = data["tag_name"] - # print(git_version) else: sys.stderr.write( f"Request response code is {response.status_code} with URL {url}\n" @@ -326,8 +188,9 @@ def getGitVersion() -> str: return git_version -def compareVersions(mc_ver: str, git_ver: str) -> bool: - update_available = False +def compare_versions(mc_ver: str, git_ver: str) -> bool: + """compares the actual Github version with the version of the running Mailcow system""" + update_available: bool = False try: mc_year, mc_month = mc_ver.split("-") git_year, git_month = git_ver.split("-") @@ -360,15 +223,18 @@ def compareVersions(mc_ver: str, git_ver: str) -> bool: elif git_month_ver > mc_month_ver: update_available = True except: + # TBD pass return update_available -# @debugLog -def getMailcowInfo(headers: str, verify: bool, base_url: str) -> dict: +def get_mailcow_info( + headers: str, verify: bool, base_url: str, check_version: bool +) -> dict: + """retrieves several global information about the mailcow system""" info_data = {} url = f"{base_url}/status/version" - response = requests.get(url, headers=headers, verify=verify) + response = requests.get(url=url, headers=headers, verify=verify, timeout=TIMEOUT) if response.status_code == 200: jsdata = response.text data = json.loads(jsdata) # returns a dictionary @@ -376,24 +242,21 @@ def getMailcowInfo(headers: str, verify: bool, base_url: str) -> dict: # get Mailcow version mc_version = data["version"] # if enabled, check this version against the official Mailcow repo on Github - if opt_check_version: - git_version = getGitVersion() - update_available = compareVersions(mc_version, git_version) - # update_available = compareVersions("2023-01a", "2023-02") - # print(update_available) + if check_version: + git_version = get_git_version() + update_available = compare_versions(mc_version, git_version) info_data["git_version"] = git_version info_data["update_available"] = update_available else: info_data["git_version"] = "Version check disabled" info_data["update_available"] = False info_data["mc_version"] = mc_version - info_data["check_version_enabled"] = opt_check_version + info_data["check_version_enabled"] = check_version return info_data - else: - sys.stderr.write( - f"Request response code is {response.status_code} with URL {url}\n" - ) - sys.exit(1) + sys.stderr.write( + f"Request response code is {response.status_code} with URL {url}\n" + ) + sys.exit(1) """ @@ -418,8 +281,8 @@ user1@dom2.de;1;2022-04-30 09:55:37;2022-04-30 09:55:37;Melissa;53460;33;1996488 """ -# @debugLog -def doCmkOutputMailboxes() -> None: +def do_cmk_output_mailboxes() -> None: + """prints out agent section for mailboxes""" print("<<>>") for mb in mailbox_data: active = mailbox_data[mb]["active"] @@ -440,8 +303,7 @@ def doCmkOutputMailboxes() -> None: ) -# @debugLog -def doCmkOutputMailcow( +def do_cmk_output_mailcow( version: str, num_domains: int, num_mailboxes: int, @@ -450,6 +312,7 @@ def doCmkOutputMailcow( update_available: bool, check_version_enabled: bool, ) -> None: + """prints out agent section for global mailcow info""" print("<<>>") # strip semicolons, if present, since we use it as delimiter version = version.replace(";", "") @@ -460,11 +323,11 @@ def doCmkOutputMailcow( """ Output is as follows: -0 domain_name +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 +4 max number of mailboxes 5 number of mailboxes 6 max number of aliases 7 number of aliases @@ -479,8 +342,8 @@ dom3.de;1;2022-04-29 13:36:08;2022-04-29 21:26:19;10;1;100;3;28132;12852485367;2 """ -# @debugLog -def doCmkOutputDomains() -> None: +def do_cmk_output_domains() -> None: + """print out agent section for domains""" print("<<>>") for dom in domain_data: active = domain_data[dom]["active"] @@ -499,74 +362,77 @@ def doCmkOutputDomains() -> None: def main(): - global debugLogFilename - cmk.utils.password_store.replace_passwords() - getOptions() + """main function""" + # get and check parameters + params: argparse.Namespace = get_args() # do some parameter checks - if opt_hostname == "": - sys.stderr.write(f"No hostname given.\n") - showUsage() + if params.hostname is None: + sys.stderr.write("No hostname given.\n") sys.exit(1) else: - hostname = opt_hostname - if opt_apikey == "": - sys.stderr.write(f"No API key given.\n") - showUsage() + hostname = params.hostname + if params.apikey is None: + sys.stderr.write("No API key given.\n") sys.exit(1) - if opt_no_cert_check: + else: + apikey = params.apikey + if params.no_cert_check: # disable certificate warnings urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) verify = False else: verify = True - if opt_port == "": - if opt_no_https: + if params.check_version: + check_version = True + else: + check_version = False + if params.port is None: + if params.no_https: protocol = "http" port = "80" else: protocol = "https" port = "443" else: - if opt_no_https: + if params.no_https: protocol = "http" else: protocol = "https" - port = opt_port + port = params.port if protocol == "http" and port == "443": - sys.stderr.write(f"Combining HTTP with port 443 is not supported.\n") + sys.stderr.write("Combining HTTP with port 443 is not supported.\n") sys.exit(1) if protocol == "https" and port == "80": - sys.stderr.write(f"Combining HTTPS with port 80 is not supported.\n") + sys.stderr.write("Combining HTTPS with port 80 is not supported.\n") sys.exit(1) headers = CaseInsensitiveDict() headers["Accept"] = "application/json" - headers["X-API-Key"] = opt_apikey + headers["X-API-Key"] = apikey # now "hostname" contains the FQDN of the host running Mailcow # now "protocol" is http or https # now "port" contains the port number to use # now "verify" signals whether certificate checking will be done (True) or not (False) + # now "check_version" signals whether the Mailcow version will be checked against the Github version # now "headers" contains the accepted format (JSON) and the API key to use - debugLogFilename = getDebugFilename(hostname) - if DEBUG: - showOptions() - print( - f"hostname: {hostname}, protocol: {protocol}, port: {port}, verify: {verify}" - ) - base_url = f"{protocol}://{hostname}:{port}/{mc_api_base}" + base_url = f"{protocol}://{hostname}:{port}/{MC_API_BASE}" # get domain data - num_domains = getDomainInfo(headers, verify, base_url) + num_domains = get_domain_info(headers=headers, verify=verify, base_url=base_url) # get mailbox data - num_mailboxes, num_global_messages = getMailboxInfo(headers, verify, base_url) + num_mailboxes, num_global_messages = get_mailbox_info( + headers=headers, verify=verify, base_url=base_url + ) # get global Mailcow info - mailcow_info = getMailcowInfo(headers, verify, base_url) + mailcow_info = get_mailcow_info( + headers=headers, verify=verify, base_url=base_url, check_version=check_version + ) mailcow_version = mailcow_info["mc_version"] github_version = mailcow_info["git_version"] update_available = mailcow_info["update_available"] check_version_enabled = mailcow_info["check_version_enabled"] # create agent output - doCmkOutputDomains() - doCmkOutputMailboxes() - doCmkOutputMailcow( + do_cmk_output_domains() + do_cmk_output_mailboxes() + do_cmk_output_mailcow( mailcow_version, num_domains, num_mailboxes, diff --git a/mailcow/rulesets/__pycache__/rs_mailcow_domains.cpython-312.pyc b/mailcow/rulesets/__pycache__/rs_mailcow_domains.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9438dcaa243c36026110f07ff1ec61f8b134200 GIT binary patch literal 2782 zcmc&$%TF6e7@u8x{aC+zuskZdZH2=tO)3PY5G9U)T7~qmO0`_nMjC z{C0kg#dtuTpErIh{t^e^cRFyNNJ}~VfhZe51rkshmDL%8l~{)AKHX>dCBMN*oDq-$ zMoQCmSQ`*P6rR8?z>n2 zcEZ(FcGKr(+4R@^O}ol%25M|m|Cb)Ty?u*4c&K@cJ$QJv>25mDf8GoJf_o8hck%)~ zL@(Gw?EmONjej55Ot_Vnf!MWF9bRld`@-XjX)8j-BFEI>2Fo@P(M7iC>`o?^X({(I zlp@wBC5V2^*_11Al5&e|m0?~c$G5C2HpB^?(t@mdywF`nu%PL>T+p6CRd!xmDiRfSd#k=;|tOlp*Q$=jgdVM!6x@xVi*O;k|0X5 zC`1J%UPM_HQM$$4yUgSnOF3*uC1oa z>-Gb)G@i~Ik7Pwv&61Ta>y;w;ETvlunr?l%tTeJZ7kQ+-juUb_jL;ydnIu0;BAe6P)uTjc zEPj*(*Jl|wY_^{I^c-Q=Y1qV!8#Pl;WzG>b(H=EXPfa$4N1m>qZgFdh^tjm8qZ2kh z?G~G^r)FLYdwzFEyxkTxPK!-uyqRU{so8UcO~wztWIj&vN4}fUz&^Oi1yDa(W1Q%K zef9EU8^Z`ql;Oay&Iqy z6%<-HI-AQaWanmAvY#z2<13zbCpeKnL+C1@0U}O9Q--OM)YIr184_#)>pl_tK89gl zf@^<*{$n=8u*ZOi11gS_fLUR-!SY|BPnf{LHw@_gXg8dC$tSkDhU@(MyL{pW-?Pp4 f?7lN_5CDn6XYi*xHFGCEee`a$m)!`b2gHR7w{qwKi4#ih@E?H0#Z@Y`9hnmx=q*tqMe2!}U9Xdn3M618znOXS-n`lO zzVG>$Qb`A5{BZx*##9b~-{dA4Vjqp&2V%MpAg}>cpo$e=wN*c5rxdwQ;k2*WnxC;V ze%8+VIXma)?YytsdUT$~1;1z){gPes4cibp4P{nTnE4`YmsNm@Fe_vV<~Ec#94g&4 zn9ihCbiLkN|JWgPbdIT9q6)&9m@|^2fnF+T_HJ53dD+u=B|;f z4rX>DS;O?EmfW(L*+|EFnBB~6sPUTp{TuR$H~aT*(EsBb3jf0!ivRQmSbC7!G&cI# zzYVPNV&`lOrU>eNjNTN$t%sip$V(Eq5*r;ef{;Xda!T8jA)lISo@>r5Ep#RhU39<( zBw3Jf!cw0>QITgq3t=f-Ta!Sw(p+N__`D*+IkbuDj<~srU50oWORPI^0P>M05iG^n zS@G5pbfRN=M{vk!Cir&bJv>Lyr!CAd`6+nBXfn11`E z0=%!gDIj=NiK9E>bDciz`N$#NAOx=-(#BbK`kjDUDKaeb!Sk!42Z#?}XFP{u?o%sI zGUAL#OcPl)A{j;`oQZ6@MD|y%eYCW6?L#8z^*z`X5eo!{Ab_?%#7<2*ZHZnz+>vJ*y@kEGl0kEsQ8)v_`1Y*Etdm)6F(7J4X{u zCPct7@uNQp@j2)fPWFu9?#Me^#+6=Wv^)0JR^{#ACoc5Hr+U@#t~s|=z1BN7{aw2^ zdGYb&<%g4(d)6C2UVmi0zn9m{%61V{Pw$m;uNZq3aQ3yt>*`i@=D61fkF5D4UT3A( z)SHRddt243$Go~%79Ux+j<`)3JLgVS^u4TE%xnWw%Meq13ojo2_Rh(heO~lw5|`A{ zcsdY$H7=sYWMd^V9Xwx8I_siU;aNvOr0zJpEb=~`BzSu5^|-FG!o2#Iqv-)5Xk7RR z3{mxE;iHO2I5_Sp14LA!L^d@ddnqYNA(RUIA6>Nq+39UhWQ^+tSZJ(aVqZQxEl1PlhL+8ZUhn?iDgs^&uFIV*d%Oe=Vp0 literal 0 HcmV?d00001 diff --git a/mailcow/rulesets/__pycache__/rs_mailcow_mailboxes.cpython-312.pyc b/mailcow/rulesets/__pycache__/rs_mailcow_mailboxes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89bc7cbc420756212fb5cff6eb196c51c602fddf GIT binary patch literal 1770 zcmb7F&u`pB6drrm`@`$qZMI2~HYqVQD7Z*#+CbC;N)siOSd@kpNjX>u%kfN7w;p>t zGaH%+2@d=Zxp1q5R3!&)@CR_?(kPWGjTR|YL4sRCB1P(n8Si>2jiADk-@JK#@6DU{ zeKY>KT-Jeje%StH-6{a^Oit3L)K`wb6UsJ#zyVN!YN%kz^SSL889cL3iCTkZzWW@WA@j4VL;Ni zoOl*e&)woc(|5FX2j4sB36c1=WiqbZYwR0^5G9-%H|^2WC+j45e#H{QA^woeE)_0 zgR+=p#Ctp;`m_yLzgqw>X-yv1?}4!8QyWy$9S>+Jz$z_mP2TKdEiqq>@0<5J zG4sq06|f{+pe!arx778du-VpZU%53aDX}3bU}d?+BsX|Xa>=D_QU|XJs>W+3MpPC@%^*sM zDvGw$;CF#>@RI;gq`B0~==N!xjKT|1H`$!qp$PJ~{?#e!U^iVvY_WiP)TUOEg;~`cK>8jo6xgcYP2_b)%|DR$VIVgO}z9gA-)FP z(#WqME(Ydyd{Xdh!PCAG1qD>Hs|M7njbGM z^U;C#SsY1S2FYHLF(IB8KEN>)$!ExAF_l*t{Y;3%jG`z{!MneKH%`@pqMiaFj-)uP z0cAzm1IvFFE-1O9X)xY6D2_eVtB=mTy{}sbdi9Auwx^FBoPYBu2dbAIqVJpgjhV-l P>nE=nleVC#|KOjFxe-T9)fa>{HITHtyuK9GypDtP(PnFC*GSNP^>L zjE-S5zAyLV!`z*VgG@#qaN;1bAL$FUd^4%0TCbY0V!6-Y0atI)2K=(dGzNTTcd4z$%INv&80zGXuP zx|k%{#PVDnHHor?5p_?Jd%A6zI`)uH(yx1-4R!Zs&9e+hVrwpjRftG@Q}_K(J!Fyz z$EqS7!!q{Dbs%g03(01=aZsL3G0#CPY1GLltAx)MVSda4);qxK3xB zjvucfuzU<`IzX2d*mFm`TR+f!KNg}O;0Tx;wIgGJ!gIk(%b>$hBe$A)5M;P~R8bUw8=pii5P zCtOhGhRB5&_!fq$8Xi`OL~YhcTIm(b=RQ}rw>P(1uZ-qx4wj&6peD}&Ty#AQ5Hvi8 z_8r=`C~|`A=7lQ*B5YMGgKd9s)~qJCPx5`T?mxv*6%wxvP)-!RZc!+Z2s>7i=9MlF zDox_9SFT}Pr~Qw<_%R1}OLKiL499XOHzo3}@T=QtcXJK0mD3roK&TsvI2_M`ms!kg{$>)qLf_J!-6 z+1GmM7|8Xa$(c-VQkr_2`*?==NOTI9sE_u!*E;hzx(lTz?MuBxlv&27GQFIX2S+pW z-Ln^%rSkQ2JotMe_?-IfL}&In?&Rv>!qv{gwf6ZSa-Kz=SGoNnwQqwLJB63H2UmDF z1CFLYpp!F`y;PhrNg86 ztBz+0-uQgY~_6b#dneD%I^OO};yFC9zl r`Cv07<)1oA1 literal 0 HcmV?d00001 diff --git a/mailcow/rulesets/rs_mailcow_domains.py b/mailcow/rulesets/rs_mailcow_domains.py new file mode 100644 index 0000000..028272f --- /dev/null +++ b/mailcow/rulesets/rs_mailcow_domains.py @@ -0,0 +1,93 @@ +#!/user/bin/env python3 +"""parameter form ruleset for mailcow domains""" + +from cmk.rulesets.v1 import Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + LevelDirection, + SimpleLevels, + Percentage, + Integer, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, HostAndItemCondition, Topic + + +# function name should begin with an underscore to limit it's visibility +def _parameter_form(): + return Dictionary( + elements={ + "levels_mailcow_domains_quota_used": DictElement( + parameter_form=SimpleLevels( + title=Title("Mailcow domains quota usage for storage"), + form_spec_template=Percentage(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(65.0, 85.0)), + ), + required=True, + ), + "levels_mailcow_domains_mailboxes_used": DictElement( + parameter_form=SimpleLevels( + title=Title("Mailcow domains mailboxes usage"), + form_spec_template=Percentage(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(65.0, 85.0)), + ), + required=True, + ), + "levels_mailcow_domains_aliases_used": DictElement( + parameter_form=SimpleLevels( + title=Title("Mailcow domains aliases usage"), + form_spec_template=Percentage(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(65.0, 85.0)), + ), + required=True, + ), + "levels_mailcow_domains_num_messages": DictElement( + parameter_form=SimpleLevels( + title=Title("Number of messages"), + form_spec_template=Integer(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(10_000, 25_000)), + ), + required=True, + ), + "levels_mailcow_domains_num_aliases": DictElement( + parameter_form=SimpleLevels( + title=Title("Number of configured aliases"), + form_spec_template=Integer(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(100, 250)), + ), + required=True, + ), + "levels_mailcow_domains_num_mailboxes": DictElement( + parameter_form=SimpleLevels( + title=Title("Number of configured mailboxes"), + form_spec_template=Integer(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(100, 250)), + ), + required=True, + ), + } + ) + + +# name must begin with "rule_spec_", should refer to the used check plugin +# must be an instance of "CheckParameters" +rule_spec_mailcow_domains = CheckParameters( + # "name" should be the same as the check plugin + name="mailcow_domains", + # the title is shown in the GUI + title=Title("Mailcow domain levels"), + # this ruleset can be found under Setup|Service monitoring rules|Various + topic=Topic.APPLICATIONS, + # define the name of the function which creates the GUI elements + parameter_form=_parameter_form, + # define the label in the GUI where you can restrict the + # settings to one or mor specific users (item) + condition=HostAndItemCondition(item_title=Title("Domain")), +) diff --git a/mailcow/rulesets/rs_mailcow_info.py b/mailcow/rulesets/rs_mailcow_info.py new file mode 100644 index 0000000..6aa17c4 --- /dev/null +++ b/mailcow/rulesets/rs_mailcow_info.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +defines the form for typing in all needed levels +regarding number of domains, mailboxes and global message count +""" + +from cmk.rulesets.v1 import Help, Title +from cmk.rulesets.v1.form_specs import ( + DictElement, + Dictionary, + SimpleLevels, + LevelDirection, + DefaultValue, + Integer, +) +from cmk.rulesets.v1.rule_specs import ( + CheckParameters, + Topic, + HostCondition, +) + + +def _parameter_form() -> Dictionary: + return Dictionary( + title=Title("Levels for global Mailcow information"), + help_text=Help("Checking Mailcow systems via API"), + elements={ + "levels_num_domains": DictElement( + parameter_form=SimpleLevels( + title=Title("Levels for number of email domains"), + form_spec_template=Integer(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(100, 200)), + ), + required=True, + ), + "levels_num_mailboxes": DictElement( + parameter_form=SimpleLevels( + title=Title("Levels for number of mailboxes"), + form_spec_template=Integer(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(500, 1000)), + ), + required=True, + ), + "levels_num_global_messages": DictElement( + parameter_form=SimpleLevels( + title=Title("Levels for number of messages"), + form_spec_template=Integer(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(100_000, 250_000)), + ), + required=True, + ), + }, + ) + + +rule_spec_mailcow_info = CheckParameters( + name="mailcow_info", + title=Title("Mailcow global levels"), + topic=Topic.APPLICATIONS, + parameter_form=_parameter_form, + condition=HostCondition(), +) diff --git a/mailcow/rulesets/rs_mailcow_mailboxes.py b/mailcow/rulesets/rs_mailcow_mailboxes.py new file mode 100644 index 0000000..31581d0 --- /dev/null +++ b/mailcow/rulesets/rs_mailcow_mailboxes.py @@ -0,0 +1,57 @@ +#!/user/bin/env python3 +"""parameter form ruleset for mailcow domains""" + +from cmk.rulesets.v1 import Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + LevelDirection, + SimpleLevels, + Percentage, + Integer, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, HostAndItemCondition, Topic + + +# function name should begin with an underscore to limit it's visibility +def _parameter_form(): + return Dictionary( + elements={ + "levels_mailcow_mailboxes_quota_used": DictElement( + parameter_form=SimpleLevels( + title=Title("Mailcow mailbox quota usage for storage"), + form_spec_template=Percentage(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(65.0, 85.0)), + ), + required=True, + ), + "levels_mailcow_mailboxes_num_messages": DictElement( + parameter_form=SimpleLevels( + title=Title("Number of messages in mailbox"), + form_spec_template=Integer(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(1000, 2500)), + ), + required=True, + ), + } + ) + + +# name must begin with "rule_spec_", should refer to the used check plugin +# must be an instance of "CheckParameters" +rule_spec_mailcow_mailboxes = CheckParameters( + # "name" should be the same as the check plugin + name="mailcow_mailboxes", + # the title is shown in the GUI + title=Title("Mailcow mailbox levels"), + # this ruleset can be found under Setup|Service monitoring rules|Various + topic=Topic.APPLICATIONS, + # define the name of the function which creates the GUI elements + parameter_form=_parameter_form, + # define the label in the GUI where you can restrict the + # settings to one or mor specific users (item) + condition=HostAndItemCondition(item_title=Title("Mailbox")), +) diff --git a/mailcow/rulesets/rs_mailcow_params.py b/mailcow/rulesets/rs_mailcow_params.py new file mode 100644 index 0000000..ff60490 --- /dev/null +++ b/mailcow/rulesets/rs_mailcow_params.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# pylint: disable=line-too-long +"""defines the form for typing in all needed HAL9002 parameters""" + +from cmk.rulesets.v1 import Help, Title, Label +from cmk.rulesets.v1.form_specs import ( + DictElement, + Dictionary, + String, + validators, + BooleanChoice, + Integer, + Password, + migrate_to_password, + InputHint, + DefaultValue, +) +from cmk.rulesets.v1.rule_specs import SpecialAgent, Topic + + +def _form_spec_special_agent_mailcow() -> Dictionary: + return Dictionary( + title=Title("Mailcow Server Information"), + help_text=Help("Checking Mailcow systems via API"), + elements={ + "hostname": DictElement( + required=True, + parameter_form=String( + title=Title("Hostname"), + help_text=Help( + "Hostname of Mailcow server (bare FQDN or IP), mandatory, eg. mailcow.yourdomain.tld" + ), + custom_validate=(validators.LengthInRange(min_value=1),), + prefill=InputHint("mailcow.yourdomain.tld"), + ), + ), + "apikey": DictElement( + required=True, + parameter_form=Password( + title=Title("API key"), + help_text=Help("Specify API key, mandatory"), + custom_validate=(validators.LengthInRange(min_value=1),), + migrate=migrate_to_password, + ), + ), + "port": DictElement( + required=False, + parameter_form=Integer( + title=Title("Port"), + help_text=Help( + "Specify port the Mailcow system ist listening on, mandatory" + ), + prefill=DefaultValue(443), + custom_validate=(validators.NetworkPort(),), + ), + ), + "check_version": DictElement( + required=False, + parameter_form=BooleanChoice( + title=Title("Check Mailcow version"), + help_text=Help( + "Activate to check version of running Mailcow against actual Github version, optional" + ), + label=Label( + "Enable version check (requires access to Github internet site)" + ), + ), + ), + "no_https": DictElement( + required=False, + parameter_form=BooleanChoice( + title=Title("Disable HTTPS"), + help_text=Help( + "Activate to disable encryption (not recommended), optional" + ), + label=Label("Disable HTTPS"), + ), + ), + "no_cert_check": DictElement( + required=False, + parameter_form=BooleanChoice( + title=Title("Disable certificate validation"), + help_text=Help( + "Activate to disable certificate validation (not recommended), optional" + ), + label=Label("Disable certificate validation"), + ), + ), + }, + ) + + +rule_spec_mailcow = SpecialAgent( + name="mailcow", + title=Title("Mailcow connection parameters"), + topic=Topic.APPLICATIONS, + parameter_form=_form_spec_special_agent_mailcow, +) diff --git a/mailcow/server_side_calls/__pycache__/agent_mailcow.cpython-312.pyc b/mailcow/server_side_calls/__pycache__/agent_mailcow.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f70a3682ced3e246f8128d3e8511d8c0f9dc099 GIT binary patch literal 2084 zcmZ`)&2Jk;6rb5$@2=fANn703Nz=HgsBmCo+Hjy!R7g{WmMAS!;o{4%*_}8}_ruJt zQ#oywq$;Alpqwa2E=Uat>WzPa0~Z@diZ$YZgplYhC`CX|yjic)B(x*>&HJ96dGGi9 zQ#zeOu)cfmY3bVpLca;2v$UAm`4X5%$VN7Hkc*2L3tn<0S1!sCFFT5xC?;IBsJdEF zbM>N*k%T@#wsIfY2`aD4?OesAt;obN6gC zwQ*=SM^~*YpMEN=dCc{zA!X)_LxE3TG6Op8+tkS`Jaxqn!U^A-Etj}@gIbJ+{OFAe zwaTV5UZNh@T-Wq$eq_H=1Xz2HM)9Ykoe;E-5Jg37qoVW)JV#Ncij5cHq2NN&7}!Cl zQf*f!$2uJ@-SJZ*U80zxhagIgrLIWQVksR-x{zu(pXBLjv+P*@t&dD*y1_NB&%xz9 z)1_QBE9INC#+8cC!bz@sKA8){O2E^=S(JsunxocDo{l!MKv_`sy?i&<2@weh;VD8~ z->y1>?;_;0s_C>X>=2yKAfTHK07JSz(1m%zh1)DCMA1ye(WjY0?JX5ID!a3g?Em-bNV%jzwP^dW7QWtAO+*JBonLhBen@V4+Z*X~f^&R2g<~Ef17gJ;2JKa4o+W`_X91qj8Tjq|fDxKP7@DVq?n-yie8*$YzH<1EWaF3HO41@c zaum5Ye;mCs2$3S|OKkJv8xE}XSl*LW)bYSt>4U6tnCz$_|4Y2q;bvm($ra6$wzfkV z+-+X8$$@1nop_8Ozi6KW>y?p-eUYN#2i7Yi)fbWB(N54EX+!Jizrv4T!qw3C@-3X# zuPvqwql30Z-7wmCTT~50AWm34--sZ$*c0&rbVh_Wp@qeiV|}pme^6C&wlMZo_unB?RtG+tf7z9^{lYo zUAbG=a>6?Q`0Nv7&Da{g*ciUp9Dct(Fy7F|W4GHYx9i#|;WoD+Lz=0zspcE+)(0*$ z^b4`e(!C|HZ|S*)p8MwHral7cx3vDBwfdOLof;klCtdiJ^8apXNQ5?>ICPgAsGaV&Xly zUa8rp7nUui2=N4rsgeK9qmhMkOn|LD6Ge5HE=V+$#Dnv07_!tM3#L=H&5-g;(Ec+b z`^v;lh^T1Xh$24M$J(>`B6|x&0k>cl$hM3z{!2&r_21E{zm)`*b`X$fy(-SFUVVmO z+dk5ZCvdAPyDdjdezeW}nn>5XPd5@nI|-rx^DN4~*-9R1Wrnsg=Ng%Ftz*VBP030P Il!?CMKiE(BT>t<8 literal 0 HcmV?d00001 diff --git a/mailcow/server_side_calls/agent_mailcow.py b/mailcow/server_side_calls/agent_mailcow.py new file mode 100644 index 0000000..3f134fe --- /dev/null +++ b/mailcow/server_side_calls/agent_mailcow.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# pylint: disable=missing-class-docstring, missing-module-docstring, missing-function-docstring + +from collections.abc import Iterable + +from pydantic import BaseModel + +from cmk.server_side_calls.v1 import ( + HostConfig, + Secret, + SpecialAgentCommand, + SpecialAgentConfig, +) + + +class MailcowParams(BaseModel): + hostname: str | None = None + apikey: Secret | None = None + port: int | None = None + no_https: bool = False + no_cert_check: bool = False + check_version: bool = False + + +def agent_mailcow_arguments( + params: MailcowParams, _host_config: HostConfig +) -> Iterable[SpecialAgentCommand]: + command_arguments: list[str | Secret] = [] + if params.hostname is not None: + command_arguments += ["--hostname", params.hostname] + if params.apikey is not None: + command_arguments += ["--apikey", params.apikey.unsafe()] + if params.port is not None: + command_arguments += ["--port", str(params.port)] + if params.no_https: + command_arguments.append("--no-https") + if params.no_cert_check: + command_arguments.append("--no-cert-check") + if params.check_version: + command_arguments.append("--check-version") + yield SpecialAgentCommand(command_arguments=command_arguments) + + +special_agent_mailcow = SpecialAgentConfig( + name="mailcow", + parameter_parser=MailcowParams.model_validate, + commands_function=agent_mailcow_arguments, +) diff --git a/mkp/Mailcow-2.1.0.mkp b/mkp/Mailcow-2.1.0.mkp new file mode 100644 index 0000000000000000000000000000000000000000..9f589e36285e028fe2ba6c78ae78abc676c314cb GIT binary patch literal 13310 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 q-P-)1&|lf(5S-$QgAT+36y$$nejf7x literal 0 HcmV?d00001