From 358f84307c9310f1396625e4420835e348992a75 Mon Sep 17 00:00:00 2001 From: cmk-bonobo Date: Sat, 25 Jan 2025 11:30:43 +0100 Subject: [PATCH] removed all Solr modules --- .../base/plugins/agent_based/mailcow_info.py | 91 ++----- .../check_mk/agents/special/agent_mailcow | 227 ++++++++++-------- local/share/check_mk/checkman/mailcow_info | 2 - local/share/check_mk/checks/agent_mailcow | 27 ++- .../web/plugins/metrics/mailcow_metrics.py | 12 - .../plugins/perfometer/mailcow_perfometers.py | 88 +++---- .../web/plugins/wato/mailcow_info_rules.py | 69 ++---- .../web/plugins/wato/mailcow_params.py | 74 ++++-- mkp/Mailcow-1.3.0.mkp | Bin 0 -> 13828 bytes 9 files changed, 290 insertions(+), 300 deletions(-) create mode 100644 mkp/Mailcow-1.3.0.mkp 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 index 2c7568c..3dabad3 100644 --- a/local/lib/python3/cmk/base/plugins/agent_based/mailcow_info.py +++ b/local/lib/python3/cmk/base/plugins/agent_based/mailcow_info.py @@ -22,23 +22,16 @@ def getStateLower(levels, value): def discover_mailcow_info(section): - yield(Service()) + yield (Service()) -def check_mailcow_info(params, section): +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"] - levels_solr_size_kb = params["levels_solr_size"] - # Levels for Solr size are given in MBytes, so convert it to bytes - levels_solr_size_warn = levels_solr_size_kb[0] * 1024 * 1024 - levels_solr_size_crit = levels_solr_size_kb[1] * 1024 * 1024 - levels_solr_size = (levels_solr_size_warn, levels_solr_size_crit) - levels_solr_documents = params["levels_solr_documents"] - version = section[key]["version"] git_version = section[key]["git_version"] check_version_enabled = section[key]["check_version_enabled"] @@ -46,19 +39,19 @@ def check_mailcow_info(params, section): num_domains = section[key]["num_domains"] num_mailboxes = section[key]["num_mailboxes"] num_global_messages = section[key]["num_global_messages"] - solr_enabled = section[key]["solr_enabled"] - solr_size = section[key]["solr_size"] - solr_documents = section[key]["solr_documents"] # 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 graphs for solr size and number of solr documents - if solr_enabled: - yield(Metric("mc_solr_size", solr_size, levels=levels_solr_size)) - yield(Metric("mc_solr_documents", solr_documents, levels=levels_solr_documents)) + 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: @@ -71,7 +64,7 @@ def check_mailcow_info(params, section): 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}\n\nSolr size: {render.bytes(solr_size)}\nNumber of Solr documents: {solr_documents}" + 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 @@ -79,45 +72,27 @@ def check_mailcow_info(params, section): state = getStateUpper((warn, crit), num_domains) notice = f"Number of domains: {num_domains}" if state != State.OK: - yield(Result(state=state, notice=notice)) + 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)) + 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)) + yield (Result(state=state, notice=notice)) - # Create result for solr size and solr number of documents - if solr_enabled: - warn, crit = levels_solr_size - state = getStateUpper((warn, crit), solr_size) - notice = f"Solr size: {render.bytes(solr_size)}" - if state != State.OK: - yield(Result(state=state, notice=notice)) - warn, crit = levels_solr_documents - state = getStateUpper((warn, crit), solr_documents) - notice = f"Number of Solr documents: {solr_documents}" - if state != State.OK: - yield(Result(state=state, notice=notice)) - else: - state = State.WARN - notice = f"Solr is disabled" - yield(Result(state=state, notice=notice)) - - def parse_mailcow_info_section(string_table): - #pprint(string_table) + # pprint(string_table) parsed_data = { - "mailcow" : {}, + "mailcow": {}, } # we expect only one line line = string_table[0] @@ -125,26 +100,13 @@ def parse_mailcow_info_section(string_table): num_domains = int(line[1]) num_mailboxes = int(line[2]) num_global_messages = int(line[3]) - solr_enabled = bool(line[4]) - # solr size comes with value and unit, unfortunately - solr_size_str = line[5] - solr_size_list = solr_size_str.split() - solr_size_unit = solr_size_list[1] - solr_size = float(solr_size_list[0]) - if solr_size_unit == "KB": - solr_size = solr_size * 1024.0 - elif solr_size_unit == "MB": - solr_size = solr_size * 1024.0 * 1024.0 - elif solr_size_unit == "GB": - solr_size = solr_size * 1024.0 * 1024.0 * 1024.0 - solr_documents = int(line[6]) - git_version = line[7] - update_available = line[8] + git_version = line[4] + update_available = line[5] if update_available == "True": update_available = True else: update_available = False - check_version_enabled = line[9] + check_version_enabled = line[6] if check_version_enabled == "True": check_version_enabled = True else: @@ -153,13 +115,10 @@ def parse_mailcow_info_section(string_table): 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"]["solr_enabled"] = solr_enabled - parsed_data["mailcow"]["solr_size"] = solr_size - parsed_data["mailcow"]["solr_documents"] = solr_documents 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) + # pprint(parsed_data) return parsed_data @@ -178,8 +137,6 @@ register.check_plugin( "levels_num_domains": (100, 200), "levels_num_mailboxes": (500, 1000), "levels_num_global_messages": (100000, 250000), - "levels_solr_size": (4096.0, 8192.0), - "levels_solr_documents": (20000, 40000) }, check_ruleset_name="mailcow_info", -) \ No newline at end of file +) diff --git a/local/share/check_mk/agents/special/agent_mailcow b/local/share/check_mk/agents/special/agent_mailcow index c27eed9..88c8b3e 100755 --- a/local/share/check_mk/agents/special/agent_mailcow +++ b/local/share/check_mk/agents/special/agent_mailcow @@ -11,11 +11,15 @@ from pprint import pprint from requests.structures import CaseInsensitiveDict from requests.auth import HTTPBasicAuth from time import ( - time, localtime, strftime, + time, + localtime, + strftime, ) + def showUsage() -> None: - sys.stderr.write("""CheckMK Mailcow Special Agent + sys.stderr.write( + """CheckMK Mailcow Special Agent USAGE: agent_nextcloud -H [hostname] -k [apikey] agent_nextcloud -h @@ -28,10 +32,12 @@ OPTIONS: --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 -""") +""" + ) + # 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 +# be aware: activating this logs very sensitive information to debug files in ~/tmp # !!DO NOT FORGET to delete these files after debugging is done!! DEBUG = False @@ -45,9 +51,15 @@ opt_check_version = False opt_no_https = False opt_no_cert_check = False -short_options = 'hH:k:P:' +short_options = "hH:k:P:" long_options = [ - 'hostname=', 'apikey=', 'port=', 'check-version=', 'no-https=', 'no-cert-check=', 'help' + "hostname=", + "apikey=", + "port=", + "check-version=", + "no-https=", + "no-cert-check=", + "help", ] domain_data = {} @@ -55,11 +67,11 @@ mailbox_data = {} debugLogFilename = "" SEP = "|" -#SEP = "\t" +# 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 @@ -70,7 +82,9 @@ Decorator function for debugging purposes return value of function call type of return value all parameters given to function -''' +""" + + def debugLog(function): def wrapper(*args, **kwargs): # execute function and measure runtime @@ -82,7 +96,7 @@ def debugLog(function): len_kwargs = len(kwargs) # format the output seconds = FLOATFMT.format(end - start) - local_time = strftime(TIMEFMT,localtime(start)) + local_time = strftime(TIMEFMT, localtime(start)) # get function name fname = function.__name__ # create output @@ -103,12 +117,15 @@ def debugLog(function): 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.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 @@ -129,28 +146,28 @@ def getOptions() -> None: opts, args = getopt.getopt(sys.argv[1:], short_options, long_options) for opt, arg in opts: - if opt in ['-H', '--hostname']: + if opt in ["-H", "--hostname"]: opt_hostname = arg - elif opt in ['-k', '--apikey']: + elif opt in ["-k", "--apikey"]: opt_apikey = arg - elif opt in ['-P', '--port']: + elif opt in ["-P", "--port"]: opt_port = arg - elif opt in ['--check-version']: - if arg == 'True': + 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': + 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': + 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']: + elif opt in ["-h", "--help"]: showUsage() sys.exit(0) if DEBUG: @@ -158,10 +175,12 @@ def getOptions() -> None: 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") + file.write( + f"Number of Arguments: {len(sys.argv)}, Argument List: {str(sys.argv)}\n" + ) -#@debugLog +# @debugLog def showOptions() -> None: print(f"Hostname: {opt_hostname}") print(f"Username: {opt_apikey}") @@ -173,24 +192,27 @@ def showOptions() -> None: 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") + file.write( + f"Hostname: {opt_hostname}, Port: {opt_port}, No HTTPS: {opt_no_https}, No Cert Check: {opt_no_cert_check}\n" + ) -#@debugLog + +# @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) - if (response.status_code == 200): + if response.status_code == 200: jsdata = response.text - data = json.loads(jsdata) # returns a list of dictionaries + data = json.loads(jsdata) # returns a list of dictionaries i = 0 while i < len(data): - #pprint(data[i]) + # pprint(data[i]) # get domain name and status (active (1) or not (0)) domain_name = data[i].get("domain_name") active = data[i].get("active") # get creation and last modification date created = data[i].get("created") - modified = data[i].get("modified") # returns "None" or date + modified = data[i].get("modified") # returns "None" or date # get maximum and current number of mailboxes in this domain max_num_mboxes_for_domain = data[i].get("max_num_mboxes_for_domain") mboxes_in_domain = data[i].get("mboxes_in_domain") @@ -214,7 +236,7 @@ def getDomainInfo(headers: str, verify: bool, base_url: str) -> int: "aliases_in_domain": aliases_in_domain, "msgs_total": msgs_total, "bytes_total": bytes_total, - "max_quota_for_domain": max_quota_for_domain + "max_quota_for_domain": max_quota_for_domain, } domain_data[domain_name] = {} domain_data[domain_name] = dom @@ -222,16 +244,19 @@ def getDomainInfo(headers: str, verify: bool, base_url: str) -> int: # return number of email domains return i else: - sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n") + sys.stderr.write( + f"Request response code is {response.status_code} with URL {url}\n" + ) sys.exit(1) -#@debugLog + +# @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) - if (response.status_code == 200): + if response.status_code == 200: jsdata = response.text - data = json.loads(jsdata) # returns a list of dictionaries + data = json.loads(jsdata) # returns a list of dictionaries i = 0 global_num_messages = 0 while i < len(data): @@ -269,7 +294,7 @@ def getMailboxInfo(headers: str, verify: bool, base_url: str) -> tuple: "messages": messages, "last_imap_login": last_imap_login, "last_pop3_login": last_pop3_login, - "last_smtp_login": last_smtp_login + "last_smtp_login": last_smtp_login, } mailbox_data[username] = {} mailbox_data[username] = mb @@ -278,29 +303,35 @@ def getMailboxInfo(headers: str, verify: bool, base_url: str) -> tuple: # 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.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) - if (response.status_code == 200): + if response.status_code == 200: jsdata = response.text - data = json.loads(jsdata) # returns a dictionary + data = json.loads(jsdata) # returns a dictionary git_version = data["tag_name"] - #print(git_version) + # print(git_version) else: - sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n") + sys.stderr.write( + f"Request response code is {response.status_code} with URL {url}\n" + ) sys.exit(1) return git_version + def compareVersions(mc_ver: str, git_ver: str) -> bool: update_available = False try: mc_year, mc_month = mc_ver.split("-") git_year, git_month = git_ver.split("-") - #print(mc_year, mc_month, git_year, git_month) + # print(mc_year, mc_month, git_year, git_month) mc_year_int = int(mc_year) git_year_int = int(git_year) if git_year_int > mc_year_int: @@ -332,23 +363,24 @@ def compareVersions(mc_ver: str, git_ver: str) -> bool: pass return update_available -#@debugLog + +# @debugLog def getMailcowInfo(headers: str, verify: bool, base_url: str) -> dict: info_data = {} url = f"{base_url}/status/version" response = requests.get(url, headers=headers, verify=verify) - if (response.status_code == 200): + if response.status_code == 200: jsdata = response.text - data = json.loads(jsdata) # returns a dictionary - #pprint(data) + data = json.loads(jsdata) # returns a dictionary + # pprint(data) # 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) + # update_available = compareVersions("2023-01a", "2023-02") + # print(update_available) info_data["git_version"] = git_version info_data["update_available"] = update_available else: @@ -358,28 +390,13 @@ def getMailcowInfo(headers: str, verify: bool, base_url: str) -> dict: info_data["check_version_enabled"] = opt_check_version return info_data else: - sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n") - sys.exit(1) - -#@debugLog -def getSolrInfo(headers: str, verify: bool, base_url: str) -> tuple: - url = f"{base_url}/status/solr" - response = requests.get(url, headers=headers, verify=verify) - if (response.status_code == 200): - jsdata = response.text - data = json.loads(jsdata) # returns a dictionary - #pprint(data) - # get Solr infos - solr_enabled = data["solr_enabled"] - solr_size = data["solr_size"] - solr_documents = data["solr_documents"] - return solr_enabled, solr_size, solr_documents - else: - sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n") + sys.stderr.write( + f"Request response code is {response.status_code} with URL {url}\n" + ) sys.exit(1) -''' +""" Output is as follows: 0 mailbox name email address used for login 1 active 1 --> active, 0 --> not active @@ -398,9 +415,10 @@ Example: user1@dom1.de;1;2022-04-29 14:29:34;2022-04-29 14:29:34;Sarah;2433;2;21474836480;495481374;1692520168;0;1692281537 user2@dom1.de;1;2022-04-29 14:38:33;2022-04-29 14:38:33;Tom;271;0;21474836480;25895752;1657394782;1692519758;1681065713 user1@dom2.de;1;2022-04-30 09:55:37;2022-04-30 09:55:37;Melissa;53460;33;19964887040;6677677548;1692520066;0;1692510822 -''' +""" -#@debugLog + +# @debugLog def doCmkOutputMailboxes() -> None: print("<<>>") for mb in mailbox_data: @@ -408,7 +426,7 @@ def doCmkOutputMailboxes() -> None: created = mailbox_data[mb]["created"] modified = mailbox_data[mb]["modified"] name = mailbox_data[mb]["name"] - # strip semicolons, if present, since we use it as delimiter + # strip semicolons, if present, since we use it as delimiter name = name.replace(";", "") num_messages = mailbox_data[mb]["num_messages"] percent_in_use = mailbox_data[mb]["percent_in_use"] @@ -417,21 +435,30 @@ def doCmkOutputMailboxes() -> None: last_imap_login = mailbox_data[mb]["last_imap_login"] last_pop3_login = mailbox_data[mb]["last_pop3_login"] last_smtp_login = mailbox_data[mb]["last_smtp_login"] - print(f"{mb};{active};{created};{modified};{name};{num_messages};{percent_in_use};{quota};{quota_used};{last_imap_login};{last_pop3_login};{last_smtp_login}") + print( + f"{mb};{active};{created};{modified};{name};{num_messages};{percent_in_use};{quota};{quota_used};{last_imap_login};{last_pop3_login};{last_smtp_login}" + ) -#@debugLog -def doCmkOutputMailcow(version: str, - num_domains: int, num_mailboxes: int, num_global_messages: int, - solr_enabled: bool, solr_size: float, solr_documents: int, - git_version: str, update_available: bool, check_version_enabled: bool - ) -> None: + +# @debugLog +def doCmkOutputMailcow( + version: str, + num_domains: int, + num_mailboxes: int, + num_global_messages: int, + git_version: str, + update_available: bool, + check_version_enabled: bool, +) -> None: print("<<>>") # strip semicolons, if present, since we use it as delimiter version = version.replace(";", "") - print(f"{version};{num_domains};{num_mailboxes};{num_global_messages};{solr_enabled};{solr_size};{solr_documents};{git_version};{update_available};{check_version_enabled}") + print( + f"{version};{num_domains};{num_mailboxes};{num_global_messages};{git_version};{update_available};{check_version_enabled}" + ) -''' +""" Output is as follows: 0 domain_name 1 active 1 --> active, 0 --> not active @@ -449,9 +476,10 @@ Example: dom1.de;1;2022-04-23 22:54:57;None;10;0;400;6;0;0;10737418240 dom2.de;1;2022-04-29 13:38:42;2023-08-19 17:21:04;10;0;400;2;0;0;10737418240 dom3.de;1;2022-04-29 13:36:08;2022-04-29 21:26:19;10;1;100;3;28132;12852485367;21474836480 -''' +""" -#@debugLog + +# @debugLog def doCmkOutputDomains() -> None: print("<<>>") for dom in domain_data: @@ -465,7 +493,9 @@ def doCmkOutputDomains() -> None: msgs_total = domain_data[dom]["msgs_total"] bytes_total = domain_data[dom]["bytes_total"] max_quota_for_domain = domain_data[dom]["max_quota_for_domain"] - print(f"{dom};{active};{created};{modified};{max_num_mboxes_for_domain};{mboxes_in_domain};{max_num_aliases_for_domain};{aliases_in_domain};{msgs_total};{bytes_total};{max_quota_for_domain}") + print( + f"{dom};{active};{created};{modified};{max_num_mboxes_for_domain};{mboxes_in_domain};{max_num_aliases_for_domain};{aliases_in_domain};{msgs_total};{bytes_total};{max_quota_for_domain}" + ) def main(): @@ -473,39 +503,39 @@ def main(): cmk.utils.password_store.replace_passwords() getOptions() # do some parameter checks - if (opt_hostname == ""): + if opt_hostname == "": sys.stderr.write(f"No hostname given.\n") showUsage() sys.exit(1) else: hostname = opt_hostname - if (opt_apikey == ""): + if opt_apikey == "": sys.stderr.write(f"No API key given.\n") showUsage() sys.exit(1) - if (opt_no_cert_check): + if opt_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 opt_port == "": + if opt_no_https: protocol = "http" port = "80" else: protocol = "https" port = "443" else: - if (opt_no_https): + if opt_no_https: protocol = "http" else: protocol = "https" port = opt_port - if (protocol == "http" and port == "443"): + if protocol == "http" and port == "443": sys.stderr.write(f"Combining HTTP with port 443 is not supported.\n") sys.exit(1) - if (protocol == "https" and port == "80"): + if protocol == "https" and port == "80": sys.stderr.write(f"Combining HTTPS with port 80 is not supported.\n") sys.exit(1) headers = CaseInsensitiveDict() @@ -519,14 +549,15 @@ def main(): debugLogFilename = getDebugFilename(hostname) if DEBUG: showOptions() - print(f"hostname: {hostname}, protocol: {protocol}, port: {port}, verify: {verify}") + print( + f"hostname: {hostname}, protocol: {protocol}, port: {port}, verify: {verify}" + ) base_url = f"{protocol}://{hostname}:{port}/{mc_api_base}" # get domain data num_domains = getDomainInfo(headers, verify, base_url) # get mailbox data num_mailboxes, num_global_messages = getMailboxInfo(headers, verify, base_url) # get global Mailcow info - solr_enabled, solr_size, solr_documents = getSolrInfo(headers, verify, base_url) mailcow_info = getMailcowInfo(headers, verify, base_url) mailcow_version = mailcow_info["mc_version"] github_version = mailcow_info["git_version"] @@ -535,12 +566,16 @@ def main(): # create agent output doCmkOutputDomains() doCmkOutputMailboxes() - doCmkOutputMailcow(mailcow_version, - num_domains, num_mailboxes, num_global_messages, - solr_enabled, solr_size, solr_documents, - github_version, update_available, check_version_enabled + doCmkOutputMailcow( + mailcow_version, + num_domains, + num_mailboxes, + num_global_messages, + github_version, + update_available, + check_version_enabled, ) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/local/share/check_mk/checkman/mailcow_info b/local/share/check_mk/checkman/mailcow_info index 08415b6..9b117bb 100644 --- a/local/share/check_mk/checkman/mailcow_info +++ b/local/share/check_mk/checkman/mailcow_info @@ -14,7 +14,5 @@ description: The check will raise WARN/CRIT if the number of domains is above the configurable levels. The check will raise WARN/CRIT if the number of mailboxes is above the configurable levels. The check will raise WARN/CRIT if the number of messages is above the configurable levels. - The check will raise WARN/CRIT if the solr size is above the configurable levels. - The check will raise WARN/CRIT if the number of solr documents is above the configurable levels. inventory: one service is created (with several details) \ No newline at end of file diff --git a/local/share/check_mk/checks/agent_mailcow b/local/share/check_mk/checks/agent_mailcow index da216ae..c6607f8 100644 --- a/local/share/check_mk/checks/agent_mailcow +++ b/local/share/check_mk/checks/agent_mailcow @@ -1,12 +1,19 @@ 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, - ] + 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 + +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 index f96d6ee..a9a4e00 100644 --- a/local/share/check_mk/web/plugins/metrics/mailcow_metrics.py +++ b/local/share/check_mk/web/plugins/metrics/mailcow_metrics.py @@ -70,15 +70,3 @@ metric_info["mailcow_mailboxes_messages"] = { "unit": "count", "color": "24/b", } - -metric_info["mc_solr_size"] = { - "title": _("Solr Size"), - "unit": "bytes", - "color": "24/b", -} - -metric_info["mc_solr_documents"] = { - "title": _("Number of Solr Documents"), - "unit": "count", - "color": "24/a", -} \ No newline at end of file diff --git a/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py b/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py index b7bfea5..dc71130 100644 --- a/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py +++ b/local/share/check_mk/web/plugins/perfometer/mailcow_perfometers.py @@ -1,54 +1,44 @@ #!/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": [ + "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_domains_used_quota"], + "total": 100.0, + }, + ], + } +) -perfometer_info.append({ - "type": "stacked", - "perfometers": [ - { - "type": "linear", - "segments": ["mailcow_mailboxes_used_quota"], - "total": 100.0, - }, - ], -}) - -#perfometer_info.append({ -# "type": "stacked", -# "perfometers": [ -# { -# "type": "linear", -# "segments": ["mc_num_mailboxes"], -# }, -# ], -#}) - -#perfometer_info.append({ -# "type": "stacked", -# "perfometers": [ -# { -# "type": "linear", -# "segments": ["mc_num_global_messages"], -# }, -# ], -#}) +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_info_rules.py b/local/share/check_mk/web/plugins/wato/mailcow_info_rules.py index cb1fb52..058877b 100644 --- a/local/share/check_mk/web/plugins/wato/mailcow_info_rules.py +++ b/local/share/check_mk/web/plugins/wato/mailcow_info_rules.py @@ -2,7 +2,7 @@ from cmk.gui.i18n import _ from cmk.gui.plugins.wato import ( CheckParameterRulespecWithoutItem, rulespec_registry, - RulespecGroupCheckParametersApplications + RulespecGroupCheckParametersApplications, ) from cmk.gui.valuespec import ( @@ -14,11 +14,14 @@ from cmk.gui.valuespec import ( Float, ) + def _parameter_spec_mailcow_info(): return Dictionary( elements=[ - ("levels_num_domains", Tuple( - title=_("Number of email domains"), + ( + "levels_num_domains", + Tuple( + title=_("Number of email domains"), elements=[ Integer( title=_("Warning at"), @@ -29,11 +32,14 @@ def _parameter_spec_mailcow_info(): title=_("Critical at"), size=32, default_value=200, - ) + ), ], - )), - ("levels_num_mailboxes", Tuple( - title=_("Number of mailboxes"), + ), + ), + ( + "levels_num_mailboxes", + Tuple( + title=_("Number of mailboxes"), elements=[ Integer( title=_("Warning at"), @@ -44,11 +50,14 @@ def _parameter_spec_mailcow_info(): title=_("Critical at"), size=32, default_value=1000, - ) + ), ], - )), - ("levels_num_global_messages", Tuple( - title=_("Number of messages"), + ), + ), + ( + "levels_num_global_messages", + Tuple( + title=_("Number of messages"), elements=[ Integer( title=_("Warning at"), @@ -59,44 +68,14 @@ def _parameter_spec_mailcow_info(): title=_("Critical at"), size=32, default_value=250000, - ) - ], - )), - ("levels_solr_size", Tuple( - title=_("Solr size"), - elements=[ - Float( - title=_("Warning at"), - size=32, - default_value=4096.0, - unit="MBytes", ), - Float( - title=_("Critical at"), - size=32, - default_value=8192.0, - unit="MBytes", - ) ], - )), - ("levels_solr_documents", Tuple( - title=_("Number of Solr documents"), - elements=[ - Integer( - title=_("Warning at"), - size=32, - default_value=20000, - ), - Integer( - title=_("Critical at"), - size=32, - default_value=40000, - ) - ], - )), + ), + ), ], ) + rulespec_registry.register( CheckParameterRulespecWithoutItem( check_group_name="mailcow_info", @@ -105,4 +84,4 @@ rulespec_registry.register( parameter_valuespec=_parameter_spec_mailcow_info, title=lambda: _("Levels for Mailcow info"), ) -) \ 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 index 6e7052c..c9dfc76 100644 --- a/local/share/check_mk/web/plugins/wato/mailcow_params.py +++ b/local/share/check_mk/web/plugins/wato/mailcow_params.py @@ -27,36 +27,72 @@ from cmk.gui.valuespec import ( Integer, ) + def _valuespec_special_agent_mailcow(): return Dictionary( title=_("Mailcow Server Information"), - help = _("Checking Mailcow instances via API"), + 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"))), + ( + "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, ) -) \ No newline at end of file +) diff --git a/mkp/Mailcow-1.3.0.mkp b/mkp/Mailcow-1.3.0.mkp new file mode 100644 index 0000000000000000000000000000000000000000..47ac9a90bfe94588615f35c596d739da646f949b GIT binary patch literal 13828 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%0APuyw-Z7>}PJnLSgXw kNnZ3to^578