diff --git a/README.md b/README.md index 7c8de27..65c7dc1 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ # Nextcloud CheckMK Special Agent Monitors various aspects of Nextcloud instances like state, quota and disk usage of all users, number of apps with available updates, database php opcache hit rate and so on. Gives additional information regarding versions of Nextcloud, database, number of storages and active users etc. -Tested with Nextcloud 25/26/27/28. -Tested only with MySQL/MariaDB as database backend. +Tested only with Nextcloud versions 29/30 +Tested only with MariaDB as database backend +The whole plugin is now migrated to the new plugin API version, introduced with CheckMK 2.3 Feel free to report other working environments. -Upgrade from older MKPs (before 2.4.0): -If you upgrade from a already installed version before 2.4.0, you have to re-create your rules for the "Nextcloud Server Information" (reason: massive parameter changes). -Steps to accomplish this without problems: +Upgrade from older MKPs (before 3.1.1): -1. Take a screenshot of your settings in the above mentioned ruleset +If you upgrade from a already installed MKP version before 3.1.0, you have to re-create your rules for the "Nextcloud Server Information" (reason: some parameter changes combined with the migration to the new plugin API). +Hint: It is always a good idea to create screenshots from all other Nextcloud rules as well. + +1. Take a screenshot of your settings in the above mentioned rulesets 2. Assure that you have access to the passwords/tokens you used within the current rules 3. Delete all rules for "Nextcloud Server Information" 4. Install and enable the new MKP @@ -39,6 +41,7 @@ Version History: -- |Date|Version|Changes| |----|-------|-------| +|2025/03/30|3.1.1|Migration to the new plugin API version finished, (hopefully) ready for CheckMK version 2.4 |2023/01/13|2.5.2|Repackaged only to set the minimum required version back to 2.1.0p1, due to serveral user requests| |2023/01/12|2.5.1|Added versions for apps with available updates| |2023/01/12|2.4.1|Removed Parameter "token", switched to parameter "app password" only| diff --git a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_database.py b/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_database.py deleted file mode 100644 index 7a179c0..0000000 --- a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_database.py +++ /dev/null @@ -1,82 +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_nextcloud_database(section): - yield(Service()) - -def check_nextcloud_database(params, section): - for key in section: - if key == "database": - opcache_hit_rate = section[key]["opcache_hit_rate"] - size = section[key]["size"] - type = section[key]["type"] - version = section[key]["version"] - levels = params["levels_database_opcache_hit_rate"] - # create graph for opcache hit rate - yield Metric("nc_database_opcache_hit_rate", opcache_hit_rate, levels=levels) - # create graph for database size - yield Metric("nc_database_size", size) - state = getStateLower(levels, opcache_hit_rate) - summary = f"PHP OPCache hit rate: {render.percent(opcache_hit_rate)}" - if opcache_hit_rate == 0: - state = State.UNKNOWN - summary = f"PHP OPCache hit rate: 0% - please check if opcache_get_status is enabled" - details = f"\nDatabase type: {type}\nDatabase version: {version}\nDatabase size: {render.bytes(size)}" - yield(Result(state=state, summary=summary, details=details)) - -def parse_nextcloud_database_section(string_table): - parsed_data = { - "database" : {} - } - params_list = [ - "NC_Database_Type", - "NC_Database_Version", - "NC_Database_Size", - "NC_OPCache_Hit_Rate" - ] - for line in string_table: - if line[0] in params_list: - param = line[0] - value = line[1] - if param == "NC_Database_Type": - parsed_data["database"]["type"] = value - elif param == "NC_Database_Version": - parsed_data["database"]["version"] = value - elif param == "NC_Database_Size": - parsed_data["database"]["size"] = int(value) - elif param == "NC_OPCache_Hit_Rate": - parsed_data["database"]["opcache_hit_rate"] = float(value) - return parsed_data - -register.agent_section( - name="nextcloud_database", - parse_function=parse_nextcloud_database_section, -) - -register.check_plugin( - name="nextcloud_database", - service_name="Nextcloud Database", - discovery_function=discover_nextcloud_database, - check_function=check_nextcloud_database, - check_default_parameters={ - "levels_database_opcache_hit_rate": (99.7, 99.1), - }, - check_ruleset_name="nextcloud_database", -) \ No newline at end of file diff --git a/local/share/check_mk/agents/special/agent_nextcloud b/local/share/check_mk/agents/special/agent_nextcloud deleted file mode 100755 index 3470fdf..0000000 --- a/local/share/check_mk/agents/special/agent_nextcloud +++ /dev/null @@ -1,311 +0,0 @@ -#!/usr/bin/env python3 - -import getopt -import sys -import requests -import urllib3 -import json -import os -from pprint import pprint -from requests.structures import CaseInsensitiveDict -from requests.auth import HTTPBasicAuth -import cmk.utils.password_store - -def showUsage(): - sys.stderr.write("""CheckMK Nextcloud Special Agent - -USAGE: agent_nextcloud_info -u [username] -p [password] - OR - agent_nextcloud_info -h - -OPTIONS: - -H, --hostname Hostname (FQDN or IP) of Nextcloud server - -u, --username Username - -p, --password App Password - -P, --port Port - -f, --folder Subfolder if not installed in web root - --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 flag logs very sensitive information to debug files in ~/tmp -# !!DO NOT FORGET to delete these files after debugging is done!! - -DEBUG = False - -nc_api_endpoint = "ocs/v2.php/apps/serverinfo/api/v1/info?format=json" -nc_api_endpoint_all_users = "ocs/v1.php/cloud/users?format=json" -nc_api_endpoint_user = "ocs/v1.php/cloud/users" - -opt_hostname = "" -opt_username = "" -opt_password = "" -opt_port = "" -opt_folder = "" -opt_no_https = False -opt_no_cert_check = False - -short_options = 'hH:u:p:P:f:' -long_options = [ - 'hostname=', 'username=', 'password=', 'port=', 'folder=', 'no-https=', 'no-cert-check=', 'help' -] - -def logDebug(line): - if DEBUG: - home_path = os.getenv("HOME") - tmp_path = f"{home_path}/tmp" - help_file = f"{tmp_path}/nextcloud_{opt_hostname}_{opt_port}_debug.txt" - with open(help_file, "a") as file: - file.write(line) - - -def getOptions(): - global opt_hostname - global opt_username - global opt_password - global opt_port - global opt_folder - 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 ['-u', '--username']: - opt_username = arg - elif opt in ['-p', '--password']: - opt_password = arg - elif opt in ['-P', '--port']: - opt_port = arg - elif opt in ['-f', '--folder']: - opt_folder = arg - 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) - logDebug(f"getOptions - Number of Arguments: {len(sys.argv)}, Argument List: {str(sys.argv)}\n") - -def showOptions(): - print(f"Hostname: {opt_hostname}") - print(f"Username: {opt_username}") - print(f"Password: {opt_password}") - print(f"Port: {opt_port}") - print(f"Folder: {opt_folder}") - print(f"No HTTPS: {opt_no_https}") - print(f"No TLS Check: {opt_no_cert_check}") - logDebug(f"showOptions - Hostname: {opt_hostname}, Port: {opt_port}, No HTTPS: {opt_no_https}, No Cert Check: {opt_no_cert_check}\n") - -def createUrl(endpoint, hostname, protocol, port, folder): - # these parameters are needed, otherwise no information about updates regarding apps and Nextcloud itself are reported (since version 28) - params = "skipApps=false&skipUpdate=false" - if folder == "": - url = f"{protocol}://{hostname}:{port}/{endpoint}" - else: - url = f"{protocol}://{hostname}:{port}/{folder}/{endpoint}" - if endpoint == nc_api_endpoint: - url = f"{url}&{params}" - logDebug(f"createUrl - Data URL: {url}\n") - return url - -def createUrlUser(user, endpoint, hostname, protocol, port, folder): - params = "format=json" - if folder == "": - url = f"{protocol}://{hostname}:{port}/{endpoint}/{user}?{params}" - else: - url = f"{protocol}://{hostname}:{port}/{folder}/{endpoint}/{user}?{params}" - logDebug(f"createUrlUser - User URL: {url}\n") - return url - -def getSession(username, secret): - session = requests.session() - session.cookies.set("SameSite", "Strict") - session.auth = (username, secret) - session.headers['Accept'] = "application/json" - return session - -def getData(session, url, verify): - data = {} - headers = CaseInsensitiveDict() - headers["Accept"] = "application/json" - response = session.get(url, headers=headers, verify=verify) - status = response.status_code - if (status == 200): - jsdata = response.text - data = json.loads(jsdata) # returns a dictionary - return data - elif (status == 503): - # this code is reported when maintenance mode is on - sys.stderr.write(f"Request response code is {response.status_code} with URL {url}, maybe maintenance mode is on?\n") - sys.exit(1) - else: - sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n") - sys.exit(1) - -def getDataAllUsers(session, url, verify): - headers = CaseInsensitiveDict() - headers["Accept"] = "application/json" - headers["OCS-APIRequest"] = "true" - response = session.get(url, headers=headers, verify=verify) - status = response.status_code - if (status == 200): - jsdata = response.text - data = json.loads(jsdata) # returns a dictionary - return data - else: - sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n") - sys.exit(1) - -def getDataUser(session, url, verify): - headers = CaseInsensitiveDict() - headers["Accept"] = "application/json" - headers["OCS-APIRequest"] = "true" - response = session.get(url, headers=headers, verify=verify) - status = response.status_code - if (status == 200): - jsdata = response.text - data = json.loads(jsdata) # returns a dictionary - return data - else: - sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n") - sys.exit(1) - -def doCmkOutput(data): - apps_with_updates_available = {} - str_apps_with_updates_available = "" - print("<<>>") - print(f"NC_Version;{data['ocs']['data']['nextcloud']['system']['version']}") - print(f"NC_Freespace;{data['ocs']['data']['nextcloud']['system']['freespace']}") - print(f"NC_Status;{data['ocs']['meta']['status']}") - # This update info is available only from version 28 onwards, so the key "update" does not exist in all versions before - try: - print(f"NC_Last_Update;{data['ocs']['data']['nextcloud']['system']['update']['lastupdatedat']}") - print(f"NC_Update_Available;{data['ocs']['data']['nextcloud']['system']['update']['available']}") - except KeyError: - pass - print(f"NC_Num_Users;{data['ocs']['data']['nextcloud']['storage']['num_users']}") - print(f"NC_Num_Files;{data['ocs']['data']['nextcloud']['storage']['num_files']}") - print(f"NC_Num_Shares;{data['ocs']['data']['nextcloud']['shares']['num_shares']}") - print(f"NC_Num_Storages;{data['ocs']['data']['nextcloud']['storage']['num_storages']}") - print(f"NC_Num_Storages_Home;{data['ocs']['data']['nextcloud']['storage']['num_storages_home']}") - print(f"NC_Num_Storages_Local;{data['ocs']['data']['nextcloud']['storage']['num_storages_local']}") - print(f"NC_Num_Storages_Other;{data['ocs']['data']['nextcloud']['storage']['num_storages_other']}") - # Workaround for Nextcloud 28.0.1 (KeyError "apps") - try: - print(f"NC_Num_Apps_Installed;{data['ocs']['data']['nextcloud']['system']['apps']['num_installed']}") - print(f"NC_Num_Apps_Updates_Available;{data['ocs']['data']['nextcloud']['system']['apps']['num_updates_available']}") - apps_with_updates_available = data['ocs']['data']['nextcloud']['system']['apps']['app_updates'] - if apps_with_updates_available: - for app, version in apps_with_updates_available.items(): - str_apps_with_updates_available = str_apps_with_updates_available + app + "/" + version + " " - print(f"NC_Apps_With_Updates_Available;{str_apps_with_updates_available}") - except KeyError: - pass - print(f"NC_Active_Users_Last_5Min;{data['ocs']['data']['activeUsers']['last5minutes']}") - print(f"NC_Active_Users_Last_1Hour;{data['ocs']['data']['activeUsers']['last1hour']}") - print(f"NC_Active_Users_Last_1Day;{data['ocs']['data']['activeUsers']['last24hours']}") - print(f"NC_Webserver;{data['ocs']['data']['server']['webserver']}") - print(f"NC_PHP_Version;{data['ocs']['data']['server']['php']['version']}") - - print("<<>>") - print(f"NC_Database_Type;{data['ocs']['data']['server']['database']['type']}") - print(f"NC_Database_Version;{data['ocs']['data']['server']['database']['version']}") - print(f"NC_Database_Size;{data['ocs']['data']['server']['database']['size']}") - # opcache entry does not exist if opcache_get_status is disabled by server settings - # thanks to Marcus Klein from Iteratio to report (and solve!) this bug - if (data['ocs']['data']['server']['php']['opcache']): - print(f"NC_OPCache_Hit_Rate;{data['ocs']['data']['server']['php']['opcache']['opcache_statistics']['opcache_hit_rate']}") - else: - print(f"NC_OPCache_Hit_Rate;0") - -def doCmkOutputAllUsers(session, data, verify, hostname, protocol, port, folder): - print("<<>>") - for user in data['ocs']['data']['users']: - nc_url = createUrlUser(user, nc_api_endpoint_user, hostname, protocol, port, folder) - user_data = getDataUser(session, nc_url, verify) - userid = user_data['ocs']['data']['id'] - displayname = user_data['ocs']['data']['displayname'] - lastlogin = int(user_data['ocs']['data']['lastLogin']) - if lastlogin == 0: - # user has never logged in - quota_free = -1 - quota_quota = -1 - quota_relative = -1 - quota_total = -1 - quota_used = -1 - else: - quota_free = user_data['ocs']['data']['quota']['free'] - # quota_quota == -3 --> unlimited - quota_quota = user_data['ocs']['data']['quota']['quota'] - # quota_relative = used * 100 / (free + used) - quota_relative = user_data['ocs']['data']['quota']['relative'] - quota_total = user_data['ocs']['data']['quota']['total'] - quota_used = user_data['ocs']['data']['quota']['used'] - print(f"{userid};{displayname};{lastlogin};{quota_free};{quota_quota};{quota_relative};{quota_total};{quota_used}") - -def main(): - # replace password from pwd store - cmk.utils.password_store.replace_passwords() - getOptions() - if (opt_hostname == ""): - sys.stderr.write(f"No hostname given.\n") - showUsage() - sys.exit(1) - if (opt_username == ""): - sys.stderr.write(f"No username given.\n") - showUsage() - sys.exit(1) - if (opt_password == ""): - sys.stderr.write(f"No password given.\n") - showUsage() - sys.exit(1) - if (opt_no_cert_check): - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - verify = False - else: - verify = True - if (opt_port == ""): - if (opt_no_https): - protocol = "http" - port = "80" - else: - protocol = "https" - port = "443" - else: - if (opt_no_https): - protocol = "http" - else: - protocol = "https" - port = opt_port - if DEBUG: - showOptions() - 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"): - sys.stderr.write(f"Combining HTTPS with port 80 is not supported.\n") - sys.exit(1) - pwd = opt_password - - # create session - session = getSession(opt_username, pwd) - nc_url = createUrl(nc_api_endpoint, opt_hostname, protocol, port, opt_folder) - nc_data = getData(session, nc_url, verify) - doCmkOutput(nc_data) - nc_url = createUrl(nc_api_endpoint_all_users, opt_hostname, protocol, port, opt_folder) - nc_data = getDataAllUsers(session, nc_url, verify) - doCmkOutputAllUsers(session, nc_data, verify, opt_hostname, protocol, port, opt_folder) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/local/share/check_mk/checkman/nextcloud_database b/local/share/check_mk/checkman/nextcloud_database deleted file mode 100644 index ad96286..0000000 --- a/local/share/check_mk/checkman/nextcloud_database +++ /dev/null @@ -1,16 +0,0 @@ -title: Nextcloud: Database information -agents: linux -catalog: unsorted -license: GPL -distribution: check_mk -description: - Works with Nextcloud version 25/26/27 (use at your own risk with lower versions). - Tested only with mysql/mariab as underlying database. - You may use a username/password or username/token combination to get access to the Nextcloud API. - You can create this token within the personal settings of an administrative user in Nextcloud. - Got to security settings and create a new app password, use this for the token. - The user must not be secured with 2FA. - Shows several information about a Nextcloud database (type/version/size). - The check will raise WARN/CRIT if Database PHP OPcache hit rate is below 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_nextcloud b/local/share/check_mk/checks/agent_nextcloud deleted file mode 100644 index b59e9ae..0000000 --- a/local/share/check_mk/checks/agent_nextcloud +++ /dev/null @@ -1,13 +0,0 @@ -def agent_nextcloud_arguments(params, hostname, ipaddress): - return [ - "--hostname", params["hostname"], - "--username", params["username"], - "--password", passwordstore_get_cmdline("%s", params["password"]), - "--port", params["port"], - "--folder", params["folder"], - "--no-https", params["no_https"], - "--no-cert-check", params["no_cert_check"], - ipaddress, - ] - -special_agent_info['nextcloud'] = agent_nextcloud_arguments diff --git a/local/share/check_mk/web/plugins/metrics/nextcloud_metrics.py b/local/share/check_mk/web/plugins/metrics/nextcloud_metrics.py deleted file mode 100644 index d7cb0f9..0000000 --- a/local/share/check_mk/web/plugins/metrics/nextcloud_metrics.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python3 -from cmk.gui.i18n import _ -from cmk.gui.plugins.metrics import ( - metric_info, - graph_info, -) - -metric_info["nc_num_users"] = { - "title": _("Number of Users"), - "unit": "count", - "color": "44/a", -} - -metric_info["nc_num_shares"] = { - "title": _("Number of Shares"), - "unit": "count", - "color": "44/b", -} - -metric_info["nc_num_storages"] = { - "title": _("Number of Storages"), - "unit": "count", - "color": "42/a", -} - -metric_info["nc_num_storages_home"] = { - "title": _("Number of Home Storages"), - "unit": "count", - "color": "44/a", -} - -metric_info["nc_num_storages_local"] = { - "title": _("Number of Local Storages"), - "unit": "count", - "color": "44/b", -} - -metric_info["nc_num_storages_other"] = { - "title": _("Number of Other Storages"), - "unit": "count", - "color": "42/a", -} - -metric_info["nc_num_files"] = { - "title": _("Number of Files"), - "unit": "count", - "color": "42/a", -} - -metric_info["nc_num_apps_installed"] = { - "title": _("Number of installed Apps"), - "unit": "count", - "color": "43/a", -} - -metric_info["nc_apps_with_updates_available"] = { - "title": _("Number of installed Apps with Updates Available"), - "unit": "count", - "color": "24/a", -} - -metric_info["nc_active_users_last_5min"] = { - "title": _("Number of Active Users in the last 5 minutes"), - "unit": "count", - "color": "24/a", -} - -metric_info["nc_active_users_last_1hour"] = { - "title": _("Number of Active Users in the last 1 hour"), - "unit": "count", - "color": "41/a", -} - -metric_info["nc_active_users_last_1day"] = { - "title": _("Number of Active Users in the last 1 day"), - "unit": "count", - "color": "42/a", -} - -metric_info["nc_users_free_space"] = { - "title": _("Free Space of User"), - "unit": "bytes", - "color": "22/a", -} - -metric_info["nc_free_space"] = { - "title": _("Free Space on Disk"), - "unit": "bytes", - "color": "22/a", -} - -metric_info["nc_database_size"] = { - "title": _("Database Size"), - "unit": "bytes", - "color": "24/a", -} - -metric_info["nc_database_opcache_hit_rate"] = { - "title": _("Database PHP OPCache Hit Rate"), - "unit": "%", - "color": "24/a", -} - -metric_info["nc_users_quota_used"] = { - "title": _("Quota used"), - "unit": "%", - "color": "16/a", -} - -graph_info["number_of_users_shares_storages_combined"] = { - "title": _("Number of Users, Shares and Storages"), - "metrics": [ - ("nc_num_users", "area"), - ("nc_num_shares", "stack"), - ("nc_num_storages", "stack"), - ], -} - -graph_info["number_of_storage_types_combined"] = { - "title": _("Number of Storage Types"), - "metrics": [ - ("nc_num_storages_home", "area"), - ("nc_num_storages_local", "stack"), - ("nc_num_storages_other", "stack"), - ], -} - -graph_info["number_of_active_users_combined"] = { - "title": _("Number of Active Users"), - "metrics": [ - ("nc_active_users_last_5min", "area"), - ("nc_active_users_last_1hour", "stack"), - ("nc_active_users_last_1day", "stack"), - ], -} diff --git a/local/share/check_mk/web/plugins/perfometer/nextcloud_perfometers.py b/local/share/check_mk/web/plugins/perfometer/nextcloud_perfometers.py deleted file mode 100644 index 9774e06..0000000 --- a/local/share/check_mk/web/plugins/perfometer/nextcloud_perfometers.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -from cmk.gui.plugins.metrics import perfometer_info - -perfometer_info.append({ - "type": "stacked", - "perfometers": [ - { - "type": "linear", - "segments": ["nc_database_opcache_hit_rate"], - "total": 100.0, - }, - ], -}) - -perfometer_info.append({ - "type": "stacked", - "perfometers": [ - { - "type": "linear", - "segments": ["nc_database_size"], - }, - ], -}) - -perfometer_info.append({ - "type": "stacked", - "perfometers": [ - { - "type": "linear", - "segments": ["nc_users_quota_used"], - "total": 100.0, - }, - ], -}) diff --git a/local/share/check_mk/web/plugins/wato/nextcloud_database_rules.py b/local/share/check_mk/web/plugins/wato/nextcloud_database_rules.py deleted file mode 100644 index 5d5c005..0000000 --- a/local/share/check_mk/web/plugins/wato/nextcloud_database_rules.py +++ /dev/null @@ -1,43 +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_nextcloud_database(): - return Dictionary( - elements=[ - ("levels_database_opcache_hit_rate", Tuple( - title=_("Nextcloud levels for database php opcache hit rate"), - elements=[ - Percentage( - title=_("Warning below"), - default_value=99.7, - ), - Percentage( - title=_("Critical below"), - default_value=99.1, - ) - ], - )), - ], - ) - -rulespec_registry.register( - CheckParameterRulespecWithoutItem( - check_group_name="nextcloud_database", - group=RulespecGroupCheckParametersApplications, - match_type="dict", - parameter_valuespec=_parameter_spec_nextcloud_database, - title=lambda: _("Nextcloud Database"), - ) -) diff --git a/local/share/check_mk/web/plugins/wato/nextcloud_info_rules.py b/local/share/check_mk/web/plugins/wato/nextcloud_info_rules.py deleted file mode 100644 index 29e8c53..0000000 --- a/local/share/check_mk/web/plugins/wato/nextcloud_info_rules.py +++ /dev/null @@ -1,74 +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_nextcloud_info(): - return Dictionary( - elements=[ - ("levels_apps_with_updates_available", Tuple( - title=_("Nextcloud number of installed apps with updates available"), - elements=[ - Integer( - title=_("Warning at"), - default_value=1, - ), - Integer( - title=_("Critical at"), - default_value=2, - ) - ], - )), - ("levels_free_space", Tuple( - title=_("Nextcloud levels for free disk space overall"), - elements=[ - Float( - title=_("Warning below"), - default_value=8.0, - unit="GBytes", - ), - Float( - title=_("Critical below"), - default_value=4.0, - unit="GBytes", - ) - ], - )), - ("levels_number_of_files", Tuple( - title=_("Nextcloud number of files"), - elements=[ - Integer( - title=_("Warning at"), - default_value=100000, - size=32, - ), - Integer( - title=_("Critical at"), - default_value=250000, - size=32, - ) - ], - )), - ], - ) - -rulespec_registry.register( - CheckParameterRulespecWithoutItem( - check_group_name="nextcloud_info", - group=RulespecGroupCheckParametersApplications, - match_type="dict", - parameter_valuespec=_parameter_spec_nextcloud_info, - title=lambda: _("Nextcloud Info"), - ) -) \ No newline at end of file diff --git a/local/share/check_mk/web/plugins/wato/nextcloud_params.py b/local/share/check_mk/web/plugins/wato/nextcloud_params.py deleted file mode 100644 index ae0923e..0000000 --- a/local/share/check_mk/web/plugins/wato/nextcloud_params.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 - -from cmk.gui.i18n import _ -from cmk.gui.plugins.wato.special_agents.common import RulespecGroupDatasourceProgramsApps -from cmk.gui.plugins.wato.utils import ( - HostRulespec, - Rulespec, - IndividualOrStoredPassword, - rulespec_registry, -) -from cmk.gui.valuespec import ( - Dictionary, - ListChoice, - Checkbox, - TextAscii, - Password, -) - -def _factory_default_special_agent_nextcloud(): - return Rulespec.FACTORY_DEFAULT_UNUSED - -def _valuespec_special_agent_nextcloud(): - return Dictionary( - title=_("Nextcloud Server Information"), - help = _("Checking Nextcloud servers via API"), - elements=[ - ("hostname", TextAscii(title=_("Hostname"), - allow_empty=False, - size = 40, - help=_("Hostname of Nextcloud server (bare FQDN or IP), mandatory, eg. nextcloud.yourdomain.tld"))), - ("username", TextAscii(title=_("Username"), - size = 40, - allow_empty=False, - help=_("Username with administrative rights, mandatory"))), - ("password", IndividualOrStoredPassword(title=_("App Password"), - size = 40, - allow_empty=False, - help=_("Specify app password, mandatory, use Personal Settings|Security|Devices and Sessions within the NC UI to create one for the given user"))), - ("port", TextAscii(title=_("Port"), - allow_empty=True, - help=_("Specify port if not listening to HTTP(S), optional"))), - ("folder", TextAscii(title=_("Folder"), - allow_empty=True, - help=_("Specify subfolder if your Nextcloud instance is not installed in the web root, no trailing/leading slashes, optional"))), - ("no_https", Checkbox(title=_("Disable HTTPS"), - help=_("Activate to disable 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( - factory_default = _factory_default_special_agent_nextcloud(), - group = RulespecGroupDatasourceProgramsApps, - name = "special_agents:nextcloud", - valuespec = _valuespec_special_agent_nextcloud, - ) -) \ No newline at end of file diff --git a/local/share/check_mk/web/plugins/wato/nextcloud_users_rules.py b/local/share/check_mk/web/plugins/wato/nextcloud_users_rules.py deleted file mode 100644 index 9eff766..0000000 --- a/local/share/check_mk/web/plugins/wato/nextcloud_users_rules.py +++ /dev/null @@ -1,68 +0,0 @@ -from cmk.gui.i18n import _ -from cmk.gui.plugins.wato import ( - CheckParameterRulespecWithItem, - rulespec_registry, - RulespecGroupCheckParametersApplications -) - -from cmk.gui.valuespec import ( - Dictionary, - ListChoice, - Tuple, - Percentage, - Integer, - TextAscii, - Float, -) - -def _item_spec_nextcloud_users(): - return TextAscii( - title=_("User ID") - ) - -def _parameter_spec_nextcloud_users(): - return Dictionary( - elements=[ - ("levels_users_quota_used", Tuple( - title=_("Nextcloud levels for quota usage of users"), - elements=[ - Percentage( - title=_("Warning at"), - default_value=65.0, - unit="%", - ), - Percentage( - title=_("Critical at"), - default_value=85.0, - unit="%", - ) - ], - )), - ("levels_users_free_space", Tuple( - title=_("Nextcloud levels for free disk space of users"), - elements=[ - Float( - title=_("Warning below"), - default_value=256.0, - unit="MBytes", - ), - Float( - title=_("Critical below"), - default_value=128.0, - unit="MBytes", - ) - ], - )), - ], - ) - -rulespec_registry.register( - CheckParameterRulespecWithItem( - check_group_name="nextcloud_users", - group=RulespecGroupCheckParametersApplications, - match_type="dict", - item_spec=_item_spec_nextcloud_users, - parameter_valuespec=_parameter_spec_nextcloud_users, - title=lambda: _("Nextcloud Users"), - ) -) \ No newline at end of file diff --git a/mkp/.DS_Store b/mkp/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/mkp/.DS_Store differ diff --git a/mkp/Nextcloud-3.1.1.mkp b/mkp/Nextcloud-3.1.1.mkp new file mode 100644 index 0000000..b1441ea Binary files /dev/null and b/mkp/Nextcloud-3.1.1.mkp differ diff --git a/nextcloud/agent_based/nextcloud_database.py b/nextcloud/agent_based/nextcloud_database.py new file mode 100644 index 0000000..db64233 --- /dev/null +++ b/nextcloud/agent_based/nextcloud_database.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# pylint: disable=missing-module-docstring, unused-argument, missing-function-docstring +# pylint: disable=line-too-long + +from collections.abc import Mapping +from typing import NotRequired, TypedDict + +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + CheckResult, + Metric, + render, + Result, + Service, + State, + StringTable, + DiscoveryResult, +) + + +class _ItemData(TypedDict): + dbtype: NotRequired[str] + version: NotRequired[str] + size: NotRequired[int] + opcache_hit_rate: NotRequired[float] + + +Section = Mapping[str, _ItemData] + + +def get_state_lower(levels, value): + warn, crit = levels + if value < crit: + return State.CRIT + if value < warn: + return State.WARN + return State.OK + + +def discover_nextcloud_database(section) -> DiscoveryResult: + yield Service() + + +def check_nextcloud_database(params, section) -> CheckResult: + for key in section: + if key == "database": + opcache_hit_rate = section[key]["opcache_hit_rate"] + size = section[key]["size"] + dbtype = section[key]["dbtype"] + version = section[key]["version"] + _level_type, levels = params["levels_database_opcache_hit_rate"] + # create graph for opcache hit rate + yield Metric( + "nc_database_opcache_hit_rate", opcache_hit_rate, levels=levels + ) + # create graph for database size + yield Metric("nc_database_size", size) + state = get_state_lower(levels, opcache_hit_rate) + summary = f"PHP OPCache hit rate: {render.percent(opcache_hit_rate)}" + if opcache_hit_rate == 0: + state = State.UNKNOWN + summary = "PHP OPCache hit rate: 0% - please check if opcache_get_status is enabled" + details = f"\nDatabase type: {dbtype}\nDatabase version: {version}\nDatabase size: {render.bytes(size)}" + yield Result(state=state, summary=summary, details=details) + + +def parse_nextcloud_database(string_table: StringTable) -> Section: + parsed_data = {"database": {}} + params_list = [ + "NC_Database_Type", + "NC_Database_Version", + "NC_Database_Size", + "NC_OPCache_Hit_Rate", + ] + for line in string_table: + if line[0] in params_list: + param = line[0] + value = line[1] + if param == "NC_Database_Type": + parsed_data["database"]["dbtype"] = value + elif param == "NC_Database_Version": + parsed_data["database"]["version"] = value + elif param == "NC_Database_Size": + parsed_data["database"]["size"] = int(value) + elif param == "NC_OPCache_Hit_Rate": + parsed_data["database"]["opcache_hit_rate"] = float(value) + return parsed_data + + +agent_section_nextcloud_database = AgentSection( + name="nextcloud_database", + parse_function=parse_nextcloud_database, +) + +check_plugin_nextcloud_database = CheckPlugin( + name="nextcloud_database", + service_name="Nextcloud Database", + discovery_function=discover_nextcloud_database, + check_function=check_nextcloud_database, + check_default_parameters={ + "levels_database_opcache_hit_rate": ("fixed", (95.0, 85.0)), + }, + check_ruleset_name="nextcloud_database", +) diff --git a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_info.py b/nextcloud/agent_based/nextcloud_info.py similarity index 75% rename from local/lib/python3/cmk/base/plugins/agent_based/nextcloud_info.py rename to nextcloud/agent_based/nextcloud_info.py index 00848f4..6cd8621 100644 --- a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_info.py +++ b/nextcloud/agent_based/nextcloud_info.py @@ -1,10 +1,24 @@ #!/usr/bin/env python3 -from pprint import pprint +# pylint: disable=missing-module-docstring, unused-argument, missing-function-docstring +# pylint: disable=line-too-long, too-many-branches, too-many-locals, too-many-statements + from datetime import datetime -from .agent_based_api.v1 import register, render, Service, Result, State, Metric + +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + CheckResult, + Metric, + render, + Result, + Service, + State, + StringTable, + DiscoveryResult, +) -def getStateUpper(levels, value): +def get_state_upper(levels, value): warn, crit = levels if value >= crit: return State.CRIT @@ -13,7 +27,7 @@ def getStateUpper(levels, value): return State.OK -def getStateLower(levels, value): +def get_state_lower(levels, value): warn, crit = levels if value < crit: return State.CRIT @@ -22,22 +36,24 @@ def getStateLower(levels, value): return State.OK -def discover_nextcloud_info(section): - yield(Service()) +def discover_nextcloud_info(section) -> DiscoveryResult: + yield Service() -def check_nextcloud_info(params, section): +def check_nextcloud_info(params, section) -> CheckResult: for key in section: if key == "nextcloud": - levels_free_space = params["levels_free_space"] - levels_number_of_files = params["levels_number_of_files"] + _level_type, levels_free_space = params["levels_free_space"] + _level_type, levels_number_of_files = params["levels_number_of_files"] # update infos are available only from Nextcloud version 28 onwards try: last_update = section[key]["last_update"] update_available = section[key]["update_available"] last_update_human = datetime.fromtimestamp(last_update) except KeyError: - last_update = "Update information not available, update to at least version 28" + last_update = ( + "Update information not available, update to at least version 28" + ) update_available = "False" last_update_human = "Information not available" status = section[key]["status"] @@ -46,14 +62,14 @@ def check_nextcloud_info(params, section): php_version = section[key]["php_version"] webserver = section[key]["webserver"] num_files = section[key]["number_files"] - num_shares = section[key]["number_shares"] + num_shares = section[key]["number_shares"] # create graph for number of files and shares - yield(Metric("nc_num_files", num_files)) - yield(Metric("nc_num_shares", num_shares)) + yield Metric("nc_num_files", num_files) + yield Metric("nc_num_shares", num_shares) # create overall result - summary = f"Status is {status}, Last update: {last_update_human}" + summary = f"Status is {status}, Last update check: {last_update_human}" details = f"Nextcloud version: {version}\nPHP version: {php_version}\nWebserver: {webserver}\n\nNumber of files: {num_files}\nNumber of shares: {num_shares}\n\nFree space on disk: {render.bytes(free_space)}\n" if status == "ok": state = State.OK @@ -64,31 +80,31 @@ def check_nextcloud_info(params, section): # Create result for available updates if update_available != "False": state = State.WARN - notice = f"Update is available" + notice = "Update is available" else: state = State.OK notice = "No update available" if state != State.OK: - yield(Result(state=state, notice=notice)) + yield Result(state=state, notice=notice) # Create result for free space on disk # Levels for free space are given in GBytes, we have to adjust this here warn, crit = levels_free_space - warn = warn*1024*1024*1024 - crit = crit*1024*1024*1024 - state = getStateLower((warn, crit), free_space) + warn = warn * 1024 * 1024 * 1024 + crit = crit * 1024 * 1024 * 1024 + state = get_state_lower((warn, crit), free_space) # create graph for free space on disk - yield(Metric("nc_free_space", free_space, levels=(warn,crit))) + yield Metric("nc_free_space", free_space, levels=(warn, crit)) notice = f"Remaining free space on disk: {render.bytes(free_space)}" if state != State.OK: - yield(Result(state=state, notice=notice)) + yield Result(state=state, notice=notice) # Create result for number of files warn, crit = levels_number_of_files - state = getStateUpper((warn, crit), num_files) + state = get_state_upper((warn, crit), num_files) notice = f"Number of files: {num_files}" if state != State.OK: - yield(Result(state=state, notice=notice)) + yield Result(state=state, notice=notice) elif key == "users": num_users = section[key]["number"] num_active_last1hour = section[key]["active_last1hour"] @@ -100,7 +116,7 @@ def check_nextcloud_info(params, section): yield Metric("nc_active_users_last_1day", num_active_last1day) yield Metric("nc_active_users_last_5min", num_active_last5min) notice = f"Number of users: {num_users}\n\nActive users last 5 min: {num_active_last5min}\nActive user since last hour: {num_active_last1hour}\nActive users since last day: {num_active_last1day}" - yield(Result(state=State.OK, notice=notice)) + yield Result(state=State.OK, notice=notice) elif key == "storages": num_storages = section[key]["number"] num_storages_home = section[key]["number_home"] @@ -112,7 +128,7 @@ def check_nextcloud_info(params, section): yield Metric("nc_num_storages_local", num_storages_local) yield Metric("nc_num_storages_other", num_storages_other) notice = f"Number of storages: {num_storages}\nNumber of home/local/other storages: {num_storages_home}/{num_storages_local}/{num_storages_other}" - yield(Result(state=State.OK, notice=notice)) + yield Result(state=State.OK, notice=notice) elif key == "apps": # Workaround for Nextcloud 28, "apps" info is not always available try: @@ -123,25 +139,30 @@ def check_nextcloud_info(params, section): else: app_versions = "" # create graphs for number of apps - levels = params["levels_apps_with_updates_available"] + _level_type, levels = params["levels_apps_with_updates_available"] yield Metric("nc_num_apps_installed", num_apps_installed) - yield Metric("nc_apps_with_updates_available", num_apps_with_updates_available, levels=levels) - state = getStateUpper(levels, num_apps_with_updates_available) - if (app_versions == ""): + yield Metric( + "nc_apps_with_updates_available", + num_apps_with_updates_available, + levels=levels, + ) + state = get_state_upper(levels, num_apps_with_updates_available) + if app_versions == "": notice = f"Number of installed apps: {num_apps_installed}\nNumber of apps with updates available: {num_apps_with_updates_available}" else: notice = f"Number of installed apps: {num_apps_installed}\nNumber of apps with updates available: {num_apps_with_updates_available}\nNew app versions available: {app_versions}" - yield(Result(state=state, notice=notice)) + yield Result(state=state, notice=notice) except KeyError: + # TBD pass -def parse_nextcloud_info_section(string_table): +def parse_nextcloud_info(string_table: StringTable) -> dict: parsed_data = { - "nextcloud" : {}, - "storages" : {}, - "apps" : {}, - "users" : {}, + "nextcloud": {}, + "storages": {}, + "apps": {}, + "users": {}, } params_list = [ "NC_Version", @@ -163,7 +184,7 @@ def parse_nextcloud_info_section(string_table): "NC_Apps_With_Updates_Available", "NC_Active_Users_Last_5Min", "NC_Active_Users_Last_1Hour", - "NC_Active_Users_Last_1Day" + "NC_Active_Users_Last_1Day", ] for line in string_table: if line[0] in params_list: @@ -209,25 +230,23 @@ def parse_nextcloud_info_section(string_table): parsed_data["users"]["active_last1hour"] = int(value) elif param == "NC_Active_Users_Last_1Day": parsed_data["users"]["active_last1day"] = int(value) - #pprint(parsed_data) return parsed_data -register.agent_section( +agent_section_nextcloud_info = AgentSection( name="nextcloud_info", - parse_function=parse_nextcloud_info_section, + parse_function=parse_nextcloud_info, ) - -register.check_plugin( +check_plugin_nextcloud_info = CheckPlugin( name="nextcloud_info", service_name="Nextcloud Info", discovery_function=discover_nextcloud_info, check_function=check_nextcloud_info, check_default_parameters={ - "levels_apps_with_updates_available": (1, 2), - "levels_free_space": (8.0, 4.0), - "levels_number_of_files": (100000, 250000), + "levels_apps_with_updates_available": ("fixed", (1, 2)), + "levels_free_space": ("fixed", (8.0, 4.0)), + "levels_number_of_files": ("fixed", (100_000, 250_000)), }, check_ruleset_name="nextcloud_info", -) \ No newline at end of file +) diff --git a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_users.py b/nextcloud/agent_based/nextcloud_users.py similarity index 54% rename from local/lib/python3/cmk/base/plugins/agent_based/nextcloud_users.py rename to nextcloud/agent_based/nextcloud_users.py index 223dbd5..5a21c03 100644 --- a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_users.py +++ b/nextcloud/agent_based/nextcloud_users.py @@ -1,9 +1,24 @@ #!/usr/bin/env python3 -from pprint import pprint -from time import time -from .agent_based_api.v1 import register, render, Service, Result, State, Metric +# pylint: disable=missing-module-docstring, unused-argument, missing-function-docstring +# pylint: disable=line-too-long, too-many-locals -def getStateUpper(levels, value): +from time import time + +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + CheckResult, + Metric, + render, + Result, + Service, + State, + StringTable, + DiscoveryResult, +) + + +def get_state_upper(levels, value): warn, crit = levels if value >= crit: return State.CRIT @@ -11,7 +26,8 @@ def getStateUpper(levels, value): return State.WARN return State.OK -def getStateLower(levels, value): + +def get_state_lower(levels, value): warn, crit = levels if value < crit: return State.CRIT @@ -19,11 +35,13 @@ def getStateLower(levels, value): return State.WARN return State.OK -def discover_nextcloud_users(section): - for key in section: - yield(Service(item = key)) -def check_nextcloud_users(item, params, section): +def discover_nextcloud_users(section) -> DiscoveryResult: + for key in section: + yield Service(item=key) + + +def check_nextcloud_users(item, params, section) -> CheckResult: userid = item quota_used_percent = section[item][0] quota_used_bytes = section[item][1] @@ -32,30 +50,33 @@ def check_nextcloud_users(item, params, section): last_login_human = section[item][4] last_login_since = section[item][5] free_space = quota_total_bytes - quota_used_bytes - #print(free_space) - levels_quota_used = params["levels_users_quota_used"] - levels_free_space = params["levels_users_free_space"] + # print(free_space) + _level_type, levels_quota_used = params["levels_users_quota_used"] + _level_type, levels_free_space = params["levels_users_free_space"] if last_login_human == "never": quota_used_percent = 0 details = f"User ID is '{userid}', Last login: {last_login_human}" - summary = f"Used quota of '{display_name}' can't be calculated yet (never logged in)" + summary = ( + f"Used quota of '{display_name}' can't be calculated yet (never logged in)" + ) else: # Levels are given in MBytes, we have to adjust this here warn, crit = levels_free_space - warn = warn*1024*1024 - crit = crit*1024*1024 - state = getStateLower((warn, crit), free_space) + warn = warn * 1024 * 1024 + crit = crit * 1024 * 1024 + state = get_state_lower((warn, crit), free_space) details = f"User ID is '{userid}'\nLast login: {last_login_human} ({last_login_since} ago)\nFree space: {render.bytes(free_space)}" summary = f"Used quota of '{display_name}' is {render.percent(quota_used_percent)}, {render.bytes(quota_used_bytes)}/{render.bytes(quota_total_bytes)} used" notice = f"Remaining free space: {render.bytes(free_space)}" yield Metric("nc_users_free_space", free_space, levels=(warn, crit)) if state != State.OK: - yield(Result(state=state, notice=notice)) + yield Result(state=state, notice=notice) yield Metric("nc_users_quota_used", quota_used_percent, levels=levels_quota_used) - state = getStateUpper(levels_quota_used, quota_used_percent) - yield(Result(state=state, summary=summary, details=details)) + state = get_state_upper(levels_quota_used, quota_used_percent) + yield Result(state=state, summary=summary, details=details) -def parse_nextcloud_users_section(string_table): + +def parse_nextcloud_users(string_table: StringTable) -> dict: # Raw output from check: # userid;displayname;lastLogin;quota_free;quota_quota;quota_relative;quota_total;quota_used # str;str;int(milli seconds since epoch);int(bytes);int(bytes);float(percent);int(bytes);int(bytes) @@ -63,7 +84,7 @@ def parse_nextcloud_users_section(string_table): for line in string_table: userid = line[0] display_name = line[1] - last_login = int(line[2])/1000 + last_login = int(line[2]) / 1000 if last_login == 0: # user never logged in last_login_human = "never" @@ -74,25 +95,37 @@ def parse_nextcloud_users_section(string_table): login_diff = curr_time - last_login last_login_human = render.datetime(last_login) last_login_since = render.timespan(login_diff) + quota_quota = int(line[4]) + if quota_quota == -3: + # TBD, no quota set for user + pass quota_relative = float(line[5]) quota_total = float(line[6]) quota_used = float(line[7]) - parsed_data[f"{userid}"] = [quota_relative, quota_used, quota_total, display_name, last_login_human, last_login_since] + parsed_data[f"{userid}"] = [ + quota_relative, + quota_used, + quota_total, + display_name, + last_login_human, + last_login_since, + ] return parsed_data - -register.agent_section( - name = "nextcloud_users", - parse_function = parse_nextcloud_users_section, + + +agent_section_nextcloud_users = AgentSection( + name="nextcloud_users", + parse_function=parse_nextcloud_users, ) -register.check_plugin( - name = "nextcloud_users", - service_name = "Nextcloud User %s", - discovery_function = discover_nextcloud_users, - check_function = check_nextcloud_users, - check_default_parameters = { - "levels_users_quota_used": (65.0, 85.00), - "levels_users_free_space": (256.0, 128.0) +check_plugin_nextcloud_users = CheckPlugin( + name="nextcloud_users", + service_name="Nextcloud User %s", + discovery_function=discover_nextcloud_users, + check_function=check_nextcloud_users, + check_default_parameters={ + "levels_users_quota_used": ("fixed", (65.0, 85.0)), + "levels_users_free_space": ("fixed", (256.0, 128.0)), }, check_ruleset_name="nextcloud_users", ) diff --git a/nextcloud/checkman/nextcloud_database b/nextcloud/checkman/nextcloud_database new file mode 100644 index 0000000..5c61c6d --- /dev/null +++ b/nextcloud/checkman/nextcloud_database @@ -0,0 +1,16 @@ +title: Nextcloud: Database information +agents: linux +catalog: unsorted +license: GPL +distribution: check_mk +description: + Works with Nextcloud version 25/26/27/28/29/30 (use at your own risk with lower versions). + Tested only with mariab as underlying database. + You have to use a username/app password combination to get access to the Nextcloud API. + You can create this app password within the personal settings of an administrative user in Nextcloud. + Got to security settings and create a new app password. + The user must not be secured with 2FA. + Shows several information about a Nextcloud database (type/version/size). + The check will raise WARN/CRIT if Database PHP OPcache hit rate is below the configurable levels. +inventory: + one service is created (with several details) \ No newline at end of file diff --git a/local/share/check_mk/checkman/nextcloud_info b/nextcloud/checkman/nextcloud_info similarity index 60% rename from local/share/check_mk/checkman/nextcloud_info rename to nextcloud/checkman/nextcloud_info index 86f6cbb..3e00f08 100644 --- a/local/share/check_mk/checkman/nextcloud_info +++ b/nextcloud/checkman/nextcloud_info @@ -4,15 +4,15 @@ catalog: unsorted license: GPL distribution: check_mk description: - Works with Nextcloud version 25/26/27 (use at your own risk with lower versions). - Tested only with mysql/mariab as underlying database. - You may use a username/password or username/token combination to get access to the Nextcloud API. - You can create this token within the personal settings of an administrative user in Nextcloud. - Got to security settings and create a new app password, use this for the token. + Works with Nextcloud version 25/26/27/28/29/30 (use at your own risk with lower versions). + Tested only with mariab as underlying database. + You have to use a username/app password combination to get access to the Nextcloud API. + You can create this app password within the personal settings of an administrative user in Nextcloud. + Got to security settings and create a new app password. The user must not be secured with 2FA. Shows several information about a Nextcloud instance, e.g. number of files/storages/(active)users, free space on disk. The check will raise CRIT if the Nextcloud instance is not in "ok" state. - The check will raise WARN if there is an update availbale for Nextcloud. + The check will raise WARN if there is an update available for Nextcloud. The check will raise WARN/CRIT if free space on disk is below the configurable levels. The check will raise WARN/CRIT if the number of installed apps with available updates is above the configurable levels. The check will raise WARN/CRIT if the number of files is above the configurable levels. diff --git a/local/share/check_mk/checkman/nextcloud_users b/nextcloud/checkman/nextcloud_users similarity index 59% rename from local/share/check_mk/checkman/nextcloud_users rename to nextcloud/checkman/nextcloud_users index 4345347..be2c4ad 100644 --- a/local/share/check_mk/checkman/nextcloud_users +++ b/nextcloud/checkman/nextcloud_users @@ -4,10 +4,11 @@ catalog: unsorted license: GPL distribution: check_mk description: - Works with Nextcloud version 25/26/27 (use at your own risk with lower versions). - You may use a username/password or username/token combination to get access to the Nextcloud API. - You can create this token within the personal settings of an administrative user in Nextcloud. - Got to security settings and create a new app password, use this for the token. + Works with Nextcloud version 25/26/27/28/29/30 (use at your own risk with lower versions). + Tested only with mariab as underlying database. + You have to use a username/app password combination to get access to the Nextcloud API. + You can create this app password within the personal settings of an administrative user in Nextcloud. + Got to security settings and create a new app password. The user must not be secured with 2FA. Shows the usage of storage quota used by each user in percent of the maximum allowed quota. Shows the remaining free space for each user based on his/her max quota and used space. diff --git a/nextcloud/graphing/graph_nextcloud.py b/nextcloud/graphing/graph_nextcloud.py new file mode 100644 index 0000000..b37b6ff --- /dev/null +++ b/nextcloud/graphing/graph_nextcloud.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""graphing and metrics definitions""" + +from cmk.graphing.v1 import Title +from cmk.graphing.v1.graphs import Graph, MinimalRange +from cmk.graphing.v1.metrics import Color, DecimalNotation, SINotation, Metric, Unit +from cmk.graphing.v1.perfometers import Open, Closed, FocusRange, Perfometer, Stacked + + +metric_nc_num_users = Metric( + name="nc_num_users", + title=Title("Number of Users"), + unit=Unit(DecimalNotation("")), + color=Color.PURPLE, +) + +metric_nc_num_shares = Metric( + name="nc_num_shares", + title=Title("Number of Shares"), + unit=Unit(DecimalNotation("")), + color=Color.BROWN, +) + +metric_nc_num_storages = Metric( + name="nc_num_storages", + title=Title("Number of Storages"), + unit=Unit(DecimalNotation("")), + color=Color.BLUE, +) + +metric_nc_num_storages_home = Metric( + name="nc_num_storages_home", + title=Title("Number of Home Storages"), + unit=Unit(DecimalNotation("")), + color=Color.GREEN, +) + +metric_nc_num_storages_local = Metric( + name="nc_num_storages_local", + title=Title("Number of Local Storages"), + unit=Unit(DecimalNotation("")), + color=Color.DARK_GREEN, +) + + +metric_nc_num_storages_other = Metric( + name="nc_num_storages_other", + title=Title("Number of Other Storages"), + unit=Unit(DecimalNotation("")), + color=Color.LIGHT_GREEN, +) + +metric_nc_num_files = Metric( + name="nc_num_files", + title=Title("Number of Files"), + unit=Unit(SINotation("")), + color=Color.RED, +) + +metric_nc_num_apps_installed = Metric( + name="nc_num_apps_installed", + title=Title("Number of installed Apps"), + unit=Unit(DecimalNotation("")), + color=Color.GREEN, +) + +metric_nc_apps_with_updates_available = Metric( + name="nc_apps_with_updates_available", + title=Title("Number of installed Apps with Updates Available"), + unit=Unit(DecimalNotation("")), + color=Color.RED, +) + +metric_nc_active_users_last_5min = Metric( + name="nc_active_users_last_5min", + title=Title("Number of Active Users in the last 5 minutes"), + unit=Unit(DecimalNotation("")), + color=Color.LIGHT_RED, +) + +metric_nc_active_users_last_1hour = Metric( + name="nc_active_users_last_1hour", + title=Title("Number of Active Users in the last 1 hour"), + unit=Unit(DecimalNotation("")), + color=Color.LIGHT_GREEN, +) + +metric_nc_active_users_last_1day = Metric( + name="nc_active_users_last_1day", + title=Title("Number of Active Users in the last 1 day"), + unit=Unit(DecimalNotation("")), + color=Color.LIGHT_BLUE, +) + +metric_nc_users_free_space = Metric( + name="nc_users_free_space", + title=Title("Free Space of User"), + unit=Unit(SINotation("bytes")), + color=Color.LIGHT_BLUE, +) + +metric_nc_free_space = Metric( + name="nc_free_space", + title=Title("Free Space on Disk"), + unit=Unit(SINotation("bytes")), + color=Color.DARK_BLUE, +) + +metric_nc_database_size = Metric( + name="nc_database_size", + title=Title("Database Size"), + unit=Unit(SINotation("bytes")), + color=Color.GREEN, +) + +metric_nc_database_opcache_hit_rate = Metric( + name="nc_database_opcache_hit_rate", + title=Title("Database PHP OPCache Hit Rate"), + unit=Unit(DecimalNotation("%")), + color=Color.LIGHT_BLUE, +) + + +metric_nc_users_quota_used = Metric( + name="nc_users_quota_used", + title=Title("Quota used"), + unit=Unit(DecimalNotation("%")), + color=Color.BLUE, +) + + +graph_nc_number_of_users_shares_storages = Graph( + name="number_of_users_shares_storages_combined", + title=Title("Number of Users, Shares and Storages"), + # names here refer to the metric names above + simple_lines=["nc_num_users", "nc_num_shares", "nc_num_storages"], + minimal_range=MinimalRange(0, 200), +) + +graph_nc_number_of_storage_types = Graph( + name="number_of_storage_types_combined", + title=Title("Number of Storage Types"), + # names here refer to the metric names above + simple_lines=[ + "nc_num_storages_home", + "nc_num_storages_local", + "nc_num_storages_other", + ], + minimal_range=MinimalRange(0, 100), +) + +graph_nc_number_of_active_users = Graph( + name="number_of_active_users_combined", + title=Title("Number of Active Users"), + # names here refer to the metric names above + simple_lines=[ + "nc_active_users_last_5min", + "nc_active_users_last_1hour", + "nc_active_users_last_1day", + ], + minimal_range=MinimalRange(0, 100), +) + + +perfometer_nc_database_op_cache_hit_rate = Perfometer( + name="nc_database_opcache_hit_rate", + focus_range=FocusRange( + Closed(0), + Closed(100), + ), + segments=("nc_database_opcache_hit_rate",), +) + +perfometer_nc_database_size = Perfometer( + name="nc_database_size", + focus_range=FocusRange(Open(0.0), Open(500.0)), + segments=["nc_database_size"], +) + +perfometer_nc_quota_used = Perfometer( + name="nc_users_quota_used", + focus_range=FocusRange(Closed(0), Closed(100)), + segments=["nc_users_quota_used"], +) + +perfometer_nc_info = Stacked( + name="nc_info", + lower=Perfometer( + name="nc_num_users", + focus_range=FocusRange(Closed(0), Open(200)), + segments=["nc_num_users"], + ), + upper=Perfometer( + name="nc_num_apps_installed", + focus_range=FocusRange(Closed(0), Open(100)), + segments=["nc_num_apps_installed"], + ), +) diff --git a/nextcloud/libexec/agent_nextcloud b/nextcloud/libexec/agent_nextcloud new file mode 100755 index 0000000..a8f2d5c --- /dev/null +++ b/nextcloud/libexec/agent_nextcloud @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +# pylint: disable=missing-module-docstring, missing-class-docstring, missing-function-docstring +# pylint: disable=too-many-branches, line-too-long, too-many-statements +# pylint: disable=too-many-arguments, too-many-positional-arguments, too-many-locals + +import json +import argparse +import sys +import requests +import urllib3 +from requests.structures import CaseInsensitiveDict + +NC_API_ENDPOINT = "ocs/v2.php/apps/serverinfo/api/v1/info?format=json" +NC_API_ENDPOINT_ALL_USERS = "ocs/v1.php/cloud/users?format=json" +NC_API_ENDPOINT_USER = "ocs/v1.php/cloud/users" + + +def get_args() -> argparse.Namespace: + parser: argparse.ArgumentParser = argparse.ArgumentParser( + description="Nextcloud server parameters" + ) + parser.add_argument( + "--hostname", required=True, type=str, help="Hostname or IP of nextcloud server" + ) + parser.add_argument( + "--username", required=True, type=str, help="User with admin privileges" + ) + parser.add_argument( + "--password", required=True, type=str, help="App password for the user" + ) + parser.add_argument( + "--port", + required=False, + type=int, + help="Port where the server is listening on (if not HTTP/HTTPS)", + ) + parser.add_argument( + "--folder", + required=False, + type=str, + help="Folder if not installed in web root directory", + ) + 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", + ) + args = parser.parse_args() + return args + + +def create_url(endpoint, hostname, protocol, port, folder): + # these parameters are needed, otherwise no information about updates regarding apps and Nextcloud itself are reported (since version 28) + params = "skipApps=false&skipUpdate=false" + if folder == "": + url = f"{protocol}://{hostname}:{port}/{endpoint}" + else: + url = f"{protocol}://{hostname}:{port}/{folder}/{endpoint}" + if endpoint == NC_API_ENDPOINT: + url = f"{url}&{params}" + return url + + +def create_url_user(user, endpoint, hostname, protocol, port, folder): + params = "format=json" + if folder == "": + url = f"{protocol}://{hostname}:{port}/{endpoint}/{user}?{params}" + else: + url = f"{protocol}://{hostname}:{port}/{folder}/{endpoint}/{user}?{params}" + return url + + +def get_session(username, secret): + session = requests.session() + session.cookies.set("SameSite", "Strict") + session.auth = (username, secret) + session.headers["Accept"] = "application/json" + return session + + +def get_data(session, url, verify): + data = {} + headers = CaseInsensitiveDict() + headers["Accept"] = "application/json" + response = session.get(url, headers=headers, verify=verify) + status = response.status_code + if status == 200: + jsdata = response.text + data = json.loads(jsdata) # returns a dictionary + return data + if status == 503: + # this code is reported when maintenance mode is on + sys.stderr.write( + f"Request response code is {response.status_code} with URL {url}, maybe maintenance mode is on?\n" + ) + sys.exit(1) + else: + sys.stderr.write( + f"Request response code is {response.status_code} with URL {url}\n" + ) + sys.exit(1) + + +def get_data_all_users(session, url, verify): + headers = CaseInsensitiveDict() + headers["Accept"] = "application/json" + headers["OCS-APIRequest"] = "true" + response = session.get(url, headers=headers, verify=verify) + status = response.status_code + if status == 200: + jsdata = response.text + data = json.loads(jsdata) # returns a dictionary + return data + sys.stderr.write( + f"Request response code is {response.status_code} with URL {url}\n" + ) + sys.exit(1) + + +def get_data_user(session, url, verify): + headers = CaseInsensitiveDict() + headers["Accept"] = "application/json" + headers["OCS-APIRequest"] = "true" + response = session.get(url, headers=headers, verify=verify) + status = response.status_code + if status == 200: + jsdata = response.text + data = json.loads(jsdata) # returns a dictionary + return data + + sys.stderr.write( + f"Request response code is {response.status_code} with URL {url}\n" + ) + sys.exit(1) + + +def do_cmk_output(data): + apps_with_updates_available = {} + str_apps_with_updates_available = "" + print("<<>>") + print(f"NC_Version;{data['ocs']['data']['nextcloud']['system']['version']}") + print(f"NC_Freespace;{data['ocs']['data']['nextcloud']['system']['freespace']}") + print(f"NC_Status;{data['ocs']['meta']['status']}") + # This update info is available only from version 28 onwards, so the key "update" does not exist in all versions before + try: + print( + f"NC_Last_Update;{data['ocs']['data']['nextcloud']['system']['update']['lastupdatedat']}" + ) + print( + f"NC_Update_Available;{data['ocs']['data']['nextcloud']['system']['update']['available']}" + ) + except KeyError: + # TBD + pass + print(f"NC_Num_Users;{data['ocs']['data']['nextcloud']['storage']['num_users']}") + print(f"NC_Num_Files;{data['ocs']['data']['nextcloud']['storage']['num_files']}") + print(f"NC_Num_Shares;{data['ocs']['data']['nextcloud']['shares']['num_shares']}") + print( + f"NC_Num_Storages;{data['ocs']['data']['nextcloud']['storage']['num_storages']}" + ) + print( + f"NC_Num_Storages_Home;{data['ocs']['data']['nextcloud']['storage']['num_storages_home']}" + ) + print( + f"NC_Num_Storages_Local;{data['ocs']['data']['nextcloud']['storage']['num_storages_local']}" + ) + print( + f"NC_Num_Storages_Other;{data['ocs']['data']['nextcloud']['storage']['num_storages_other']}" + ) + # Workaround for Nextcloud 28.0.1 (KeyError "apps") + try: + print( + f"NC_Num_Apps_Installed;{data['ocs']['data']['nextcloud']['system']['apps']['num_installed']}" + ) + print( + f"NC_Num_Apps_Updates_Available;{data['ocs']['data']['nextcloud']['system']['apps']['num_updates_available']}" + ) + apps_with_updates_available = data["ocs"]["data"]["nextcloud"]["system"][ + "apps" + ]["app_updates"] + if apps_with_updates_available: + for app, version in apps_with_updates_available.items(): + str_apps_with_updates_available = ( + str_apps_with_updates_available + app + "/" + version + " " + ) + print( + f"NC_Apps_With_Updates_Available;{str_apps_with_updates_available}" + ) + except KeyError: + # TBD + pass + print( + f"NC_Active_Users_Last_5Min;{data['ocs']['data']['activeUsers']['last5minutes']}" + ) + print( + f"NC_Active_Users_Last_1Hour;{data['ocs']['data']['activeUsers']['last1hour']}" + ) + print( + f"NC_Active_Users_Last_1Day;{data['ocs']['data']['activeUsers']['last24hours']}" + ) + print(f"NC_Webserver;{data['ocs']['data']['server']['webserver']}") + print(f"NC_PHP_Version;{data['ocs']['data']['server']['php']['version']}") + + print("<<>>") + print(f"NC_Database_Type;{data['ocs']['data']['server']['database']['type']}") + print(f"NC_Database_Version;{data['ocs']['data']['server']['database']['version']}") + print(f"NC_Database_Size;{data['ocs']['data']['server']['database']['size']}") + # opcache entry does not exist if opcache_get_status is disabled by server settings + # thanks to Marcus Klein from Iteratio to report (and solve!) this bug + if data["ocs"]["data"]["server"]["php"]["opcache"]: + print( + f"NC_OPCache_Hit_Rate;{data['ocs']['data']['server']['php']['opcache']['opcache_statistics']['opcache_hit_rate']}" + ) + else: + print("NC_OPCache_Hit_Rate;0") + + +def do_cmk_output_all_users(session, data, verify, hostname, protocol, port, folder): + print("<<>>") + for user in data["ocs"]["data"]["users"]: + nc_url = create_url_user( + user, NC_API_ENDPOINT_USER, hostname, protocol, port, folder + ) + user_data = get_data_user(session, nc_url, verify) + userid = user_data["ocs"]["data"]["id"] + displayname = user_data["ocs"]["data"]["displayname"] + lastlogin = int(user_data["ocs"]["data"]["lastLogin"]) + if lastlogin == 0: + # user has never logged in + quota_free = -1 + quota_quota = -1 + quota_relative = -1 + quota_total = -1 + quota_used = -1 + else: + quota_free = user_data["ocs"]["data"]["quota"]["free"] + # quota_quota == -3 --> unlimited + quota_quota = user_data["ocs"]["data"]["quota"]["quota"] + # quota_relative = used * 100 / (free + used) + quota_relative = user_data["ocs"]["data"]["quota"]["relative"] + quota_total = user_data["ocs"]["data"]["quota"]["total"] + quota_used = user_data["ocs"]["data"]["quota"]["used"] + print( + f"{userid};{displayname};{lastlogin};{quota_free};{quota_quota};{quota_relative};{quota_total};{quota_used}" + ) + + +def main(): + # get and check parameters + params: argparse.Namespace = get_args() + if params.hostname is None: + sys.stderr.write("No hostname given.\n") + sys.exit(1) + if params.username is None: + sys.stderr.write("No username given.\n") + sys.exit(1) + if params.password is None: + sys.stderr.write("No app password given.\n") + sys.exit(1) + hostname = params.hostname + username = params.username + pwd = params.password + if params.folder is None: + folder = "" + else: + folder = params.folder + if params.no_cert_check: + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + verify = False + else: + verify = True + if params.port is None: + if params.no_https: + protocol = "http" + port = "80" + else: + protocol = "https" + port = "443" + else: + if params.no_https: + protocol = "http" + else: + protocol = "https" + port = params.port + if protocol == "http" and port == "443": + sys.stderr.write("Combining HTTP with port 443 is not supported.\n") + sys.exit(1) + if protocol == "https" and port == "80": + sys.stderr.write("Combining HTTPS with port 80 is not supported.\n") + sys.exit(1) + + # create session + session = get_session(username, pwd) + nc_url = create_url(NC_API_ENDPOINT, hostname, protocol, port, folder) + nc_data = get_data(session, nc_url, verify) + do_cmk_output(nc_data) + nc_url = create_url(NC_API_ENDPOINT_ALL_USERS, hostname, protocol, port, folder) + nc_data = get_data_all_users(session, nc_url, verify) + do_cmk_output_all_users(session, nc_data, verify, hostname, protocol, port, folder) + + +if __name__ == "__main__": + main() diff --git a/nextcloud/rulesets/nextcloud_database_rules.py b/nextcloud/rulesets/nextcloud_database_rules.py new file mode 100644 index 0000000..c57f638 --- /dev/null +++ b/nextcloud/rulesets/nextcloud_database_rules.py @@ -0,0 +1,45 @@ +#!/user/bin/env python3 +"""parameter form ruleset for Nextcloud databases""" + +from cmk.rulesets.v1 import Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + Float, + LevelDirection, + SimpleLevels, +) +from cmk.rulesets.v1.rule_specs import HostCondition, CheckParameters, Topic + + +# function name should begin with an underscore to limit it's visibility +def _parameter_form(): + return Dictionary( + elements={ + "levels_database_opcache_hit_rate": DictElement( + parameter_form=SimpleLevels( + title=Title("Levels for database PHP op cache hit rate"), + form_spec_template=Float(), + level_direction=LevelDirection.LOWER, + prefill_fixed_levels=DefaultValue(value=(95.0, 85.0)), + ), + required=True, + ), + } + ) + + +# name must begin with "rule_spec_", should refer to the used check plugin +# must be an instance of "CheckParameters" +rule_spec_nextcloud_database = CheckParameters( + # "name" should be the same as the check plugin + name="nextcloud_database", + # the title is shown in the GUI + title=Title("Levels for database PHP op cache hit rate"), + # this ruleset can be found under Setup|Service monitoring rules|Applications... + topic=Topic.APPLICATIONS, + # define the name of the function which creates the GUI elements + parameter_form=_parameter_form, + condition=HostCondition(), +) diff --git a/nextcloud/rulesets/nextcloud_info_rules.py b/nextcloud/rulesets/nextcloud_info_rules.py new file mode 100644 index 0000000..cc0a132 --- /dev/null +++ b/nextcloud/rulesets/nextcloud_info_rules.py @@ -0,0 +1,66 @@ +#!/user/bin/env python3 +"""parameter form ruleset for Nextcloud systems""" + +from cmk.rulesets.v1 import Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + Float, + LevelDirection, + SimpleLevels, + Integer, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, Topic, HostCondition + + +# function name should begin with an underscore to limit it's visibility +def _parameter_form(): + return Dictionary( + elements={ + "levels_apps_with_updates_available": DictElement( + parameter_form=SimpleLevels( + title=Title("Number of installed apps with updates available"), + form_spec_template=Integer(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(1, 2)), + ), + required=True, + ), + "levels_free_space": DictElement( + parameter_form=SimpleLevels( + title=Title("Levels for free disk space overall"), + form_spec_template=Float(unit_symbol="GBytes"), + level_direction=LevelDirection.LOWER, + prefill_fixed_levels=DefaultValue(value=(8.0, 4.0)), + ), + required=True, + ), + "levels_number_of_files": DictElement( + parameter_form=SimpleLevels( + title=Title("Number of files overall"), + form_spec_template=Integer(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(100_000, 250_000)), + ), + required=True, + ), + } + ) + + +# name must begin with "rule_spec_", should refer to the used check plugin +# must be an instance of "CheckParameters" +rule_spec_nextcloud_info = CheckParameters( + # "name" should be the same as the check plugin + name="nextcloud_info", + # the title is shown in the GUI + title=Title("Various parameters for Nextcloud systems"), + # this ruleset can be found under Setup|Service monitoring rules|Applications... + 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 storages (item) + condition=HostCondition(), +) diff --git a/nextcloud/rulesets/nextcloud_params.py b/nextcloud/rulesets/nextcloud_params.py new file mode 100644 index 0000000..7689c3f --- /dev/null +++ b/nextcloud/rulesets/nextcloud_params.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# pylint: disable=line-too-long, missing-module-docstring + +from cmk.rulesets.v1 import Help, Label, Title +from cmk.rulesets.v1.form_specs import ( + BooleanChoice, + DictElement, + Dictionary, + migrate_to_password, + Password, + String, + Integer, + validators, + InputHint, +) +from cmk.rulesets.v1.rule_specs import SpecialAgent, Topic + + +def _form_spec_special_agent_nextcloud() -> Dictionary: + return Dictionary( + title=Title("Nextcloud Server Information"), + help_text=Help("Checking Nextcloud servers via API"), + elements={ + "hostname": DictElement( + required=True, + parameter_form=String( + title=Title("Hostname"), + help_text=Help( + "Hostname of Nextcloud server (bare FQDN or IP), mandatory, eg. nextcloud.yourdomain.tld" + ), + custom_validate=(validators.LengthInRange(min_value=1),), + prefill=InputHint("nextcloud.yourdomain.tld"), + ), + ), + "username": DictElement( + required=True, + parameter_form=String( + title=Title("Username"), + help_text=Help("Username with administrative rights, mandatory"), + custom_validate=(validators.LengthInRange(min_value=1),), + prefill=InputHint("admin"), + ), + ), + "password": DictElement( + required=True, + parameter_form=Password( + title=Title("App Password"), + help_text=Help( + "Specify app password, mandatory, use Personal Settings|Security|Devices and Sessions within the NC UI to create one for the given user" + ), + 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 if not listening to HTTP(S), optional" + ), + prefill=InputHint(8080), + custom_validate=(validators.NetworkPort(),), + ), + ), + "folder": DictElement( + required=False, + parameter_form=String( + title=Title("Folder"), + help_text=Help( + "Specify subfolder if your Nextcloud instance is not installed in the web root, no trailing/leading slashes, optional" + ), + prefill=InputHint("nextcloud"), + ), + ), + "no_https": DictElement( + required=False, + parameter_form=BooleanChoice( + title=Title("Protocol handling"), + label=Label("Disable HTTPS"), + help_text=Help( + "Activate to disable encryption (not recommended), optional" + ), + ), + ), + "no_cert_check": DictElement( + required=False, + parameter_form=BooleanChoice( + title=Title("Certificate handling"), + label=Label("Disable certificate validation"), + help_text=Help( + "Activate to disable certificate validation (not recommended), optional" + ), + ), + ), + }, + ) + + +rule_spec_nextcloud = SpecialAgent( + name="nextcloud", + title=Title("Nextcloud connection parameters"), + topic=Topic.APPLICATIONS, + parameter_form=_form_spec_special_agent_nextcloud, +) diff --git a/nextcloud/rulesets/nextcloud_users_rules.py b/nextcloud/rulesets/nextcloud_users_rules.py new file mode 100644 index 0000000..711af54 --- /dev/null +++ b/nextcloud/rulesets/nextcloud_users_rules.py @@ -0,0 +1,54 @@ +#!/user/bin/env python3 +"""parameter form ruleset for Nextcloud users""" + +from cmk.rulesets.v1 import Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + Float, + LevelDirection, + SimpleLevels, +) +from cmk.rulesets.v1.rule_specs import HostAndItemCondition, CheckParameters, Topic + + +# function name should begin with an underscore to limit it's visibility +def _parameter_form(): + return Dictionary( + elements={ + "levels_users_quota_used": DictElement( + parameter_form=SimpleLevels( + title=Title("Levels for quota usage of users"), + form_spec_template=Float(unit_symbol="%"), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(65.0, 85.0)), + ), + required=True, + ), + "levels_users_free_space": DictElement( + parameter_form=SimpleLevels( + title=Title("Levels for free disk space of users"), + form_spec_template=Float(unit_symbol="MBytes"), + level_direction=LevelDirection.LOWER, + prefill_fixed_levels=DefaultValue(value=(256.0, 128.0)), + ), + required=True, + ), + } + ) + + +# name must begin with "rule_spec_", should refer to the used check plugin +# must be an instance of "CheckParameters" +rule_spec_nextcloud_users = CheckParameters( + # "name" should be the same as the check plugin + name="nextcloud_users", + # the title is shown in the GUI + title=Title("Levels for Nextcloud users"), + # this ruleset can be found under Setup|Service monitoring rules|Applications... + topic=Topic.APPLICATIONS, + # define the name of the function which creates the GUI elements + parameter_form=_parameter_form, + condition=HostAndItemCondition(item_title=Title("User ID")), +) diff --git a/nextcloud/server_side_calls/agent_nextcloud.py b/nextcloud/server_side_calls/agent_nextcloud.py new file mode 100644 index 0000000..acd3f54 --- /dev/null +++ b/nextcloud/server_side_calls/agent_nextcloud.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# pylint: disable=missing-module-docstring, missing-class-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 NextcloudParams(BaseModel): + hostname: str | None = None + username: str | None = None + password: Secret | None = None + port: int | None = None + folder: str | None = None + no_https: bool = False + no_cert_check: bool = False + + +def agent_nextcloud_arguments( + params: NextcloudParams, _host_config: HostConfig +) -> Iterable[SpecialAgentCommand]: + # print(f"Params: {params}") + command_arguments: list[str | Secret] = [] + if params.hostname is not None: + command_arguments += ["--hostname", params.hostname] + if params.username is not None: + command_arguments += ["--username", params.username] + if params.password is not None: + command_arguments += ["--password", params.password.unsafe()] + if params.port is not None: + command_arguments += ["--port", str(params.port)] + if params.folder is not None: + command_arguments += ["--folder", params.folder] + if params.no_https: + command_arguments.append("--no-https") + if params.no_cert_check: + command_arguments.append("--no-cert-check") + # command_arguments.append(host_config.name) + # print(f"Command Args: {command_arguments}") + yield SpecialAgentCommand(command_arguments=command_arguments) + + +special_agent_nextcloud = SpecialAgentConfig( + name="nextcloud", + parameter_parser=NextcloudParams.model_validate, + commands_function=agent_nextcloud_arguments, +)