diff --git a/.gitignore b/.gitignore index 18ff89d..2c68f37 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ __pycache__/ *.vsix # Exclude all "private" files (Ralf Mellis) -RaMe* +RM* # Exclude all "private" files (Torsten Behne) TB* diff --git a/README.md b/README.md index aaf6856..7952f4f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # 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. +Tested with Nextcloud 25/26/27/28. Tested only with MySQL/MariaDB as database backend. Feel free to report other working environments. @@ -9,6 +9,7 @@ Version History: -- |Date|Version|Changes| |----|-------|-------| +|2023/01/12|2.4.0|Integrated Password Store for App Password, some changes for compatibility with NC 28 added| |2023/11/26|2.3.4|Fixed agent crash if opcache_get_status is disabled by server settings| |2023/08/16|2.3.3|Fixed some misleading info strings regarding database opcache| |2023/08/12|2.3.2|MKP now compatible with CheckMK 2.2| diff --git a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_info.py b/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_info.py index 4c1fd06..b7d4c2f 100644 --- a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_info.py +++ b/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_info.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 from pprint import pprint +from datetime import datetime from .agent_based_api.v1 import register, render, Service, Result, State, Metric + def getStateUpper(levels, value): warn, crit = levels if value >= crit: @@ -10,6 +12,7 @@ def getStateUpper(levels, value): return State.WARN return State.OK + def getStateLower(levels, value): warn, crit = levels if value < crit: @@ -18,21 +21,32 @@ def getStateLower(levels, value): return State.WARN return State.OK + def discover_nextcloud_info(section): yield(Service()) - + + def check_nextcloud_info(params, section): for key in section: if key == "nextcloud": levels_free_space = params["levels_free_space"] 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" + update_available = "False" + last_update_human = "Update information not available" status = section[key]["status"] free_space = section[key]["freespace"] version = section[key]["version"] 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)) @@ -40,13 +54,23 @@ def check_nextcloud_info(params, section): # create overall result summary = f"Status is {status}" - 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" + details = f"Nextcloud version: {version}\nLast update: {last_update_human}\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 else: state = State.CRIT yield Result(state=state, summary=summary, details=details) + # Create result for available updates + if update_available != "False": + state = State.WARN + notice = f"Update is available" + else: + state = State.OK + notice = "No update available" + if state != State.OK: + 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 @@ -90,15 +114,20 @@ def check_nextcloud_info(params, section): 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)) elif key == "apps": - num_apps_installed = section[key]["installed"] - num_apps_with_updates_available = section[key]["with_updates_available"] - # create graphs for number of apps - 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) - notice = f"Number of installed apps: {num_apps_installed}\nNumber of apps with updates available: {num_apps_with_updates_available}" - yield(Result(state=state, notice=notice)) + # Workaround for Nextcloud 28, "apps" info is not always available + try: + num_apps_installed = section[key]["installed"] + num_apps_with_updates_available = section[key]["with_updates_available"] + # create graphs for number of apps + 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) + notice = f"Number of installed apps: {num_apps_installed}\nNumber of apps with updates available: {num_apps_with_updates_available}" + yield(Result(state=state, notice=notice)) + except KeyError: + pass + def parse_nextcloud_info_section(string_table): parsed_data = { @@ -111,6 +140,8 @@ def parse_nextcloud_info_section(string_table): "NC_Version", "NC_Freespace", "NC_Status", + "NC_Last_Update", + "NC_Update_Available", "NC_Webserver", "NC_PHP_Version", "NC_Num_Users", @@ -136,6 +167,10 @@ def parse_nextcloud_info_section(string_table): parsed_data["nextcloud"]["freespace"] = float(value) elif param == "NC_Status": parsed_data["nextcloud"]["status"] = value + elif param == "NC_Last_Update": + parsed_data["nextcloud"]["last_update"] = int(value) + elif param == "NC_Update_Available": + parsed_data["nextcloud"]["update_available"] = value elif param == "NC_Webserver": parsed_data["nextcloud"]["webserver"] = value elif param == "NC_PHP_Version": @@ -164,13 +199,16 @@ 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( name="nextcloud_info", parse_function=parse_nextcloud_info_section, ) + register.check_plugin( name="nextcloud_info", service_name="Nextcloud Info", diff --git a/local/share/check_mk/agents/special/agent_nextcloud b/local/share/check_mk/agents/special/agent_nextcloud index 67a907c..7a01523 100755 --- a/local/share/check_mk/agents/special/agent_nextcloud +++ b/local/share/check_mk/agents/special/agent_nextcloud @@ -9,6 +9,7 @@ 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 @@ -34,7 +35,7 @@ OPTIONS: # 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 +DEBUG = True 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" @@ -54,6 +55,15 @@ long_options = [ 'hostname=', 'username=', 'password=', 'port=', 'token=', '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 @@ -91,12 +101,7 @@ def getOptions(): elif opt in ['-h', '--help']: showUsage() sys.exit(0) - if DEBUG: - home_path = os.getenv("HOME") - tmp_path = f"{home_path}/tmp" - help_file = f"{tmp_path}/nextcloud_{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") + logDebug(f"getOptions - Number of Arguments: {len(sys.argv)}, Argument List: {str(sys.argv)}\n") def showOptions(): print(f"Hostname: {opt_hostname}") @@ -107,48 +112,41 @@ def showOptions(): print(f"Token: {opt_token}") print(f"No HTTPS: {opt_no_https}") print(f"No TLS Check: {opt_no_cert_check}") - home_path = os.getenv("HOME") - tmp_path = f"{home_path}/tmp" - help_file = f"{tmp_path}/nextcloud_{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") + 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 not 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 DEBUG: - home_path = os.getenv("HOME") - tmp_path = f"{home_path}/tmp" - help_file = f"{tmp_path}/nextcloud_{hostname}_debug.txt" - with open(help_file, "a") as file: - file.write(f"Data URL: {url}\n") + 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}?format=json" + url = f"{protocol}://{hostname}:{port}/{endpoint}/{user}?{params}" else: - url = f"{protocol}://{hostname}:{port}/{folder}/{endpoint}/{user}?format=json" - if DEBUG: - home_path = os.getenv("HOME") - tmp_path = f"{home_path}/tmp" - help_file = f"{tmp_path}/nextcloud_{hostname}_debug.txt" - with open(help_file, "a") as file: - file.write(f"User URL: {url}\n") + url = f"{protocol}://{hostname}:{port}/{folder}/{endpoint}/{user}?{params}" + logDebug(f"createUrlUser - User URL: {url}\n") return url -def getData(url, verify): +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): headers = CaseInsensitiveDict() headers["Accept"] = "application/json" - if (opt_token == '0'): - # authenticate with username and password - pwd = opt_password - else: - # authenticate with token - pwd = opt_token - response = requests.get(url, auth=HTTPBasicAuth(opt_username, pwd), headers=headers, verify=verify) + cookies = {"nc_sameSiteCookiestrict": "true"} + response = session.get(url, headers=headers, cookies=cookies, verify=verify) status = response.status_code if (status == 200): jsdata = response.text @@ -162,17 +160,12 @@ def getData(url, verify): sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n") sys.exit(1) -def getDataAllUsers(url, verify): +def getDataAllUsers(session, url, verify): headers = CaseInsensitiveDict() headers["Accept"] = "application/json" headers["OCS-APIRequest"] = "true" - if (opt_token == '0'): - # authenticate with username and password - pwd = opt_password - else: - # authenticate with token - pwd = opt_token - response = requests.get(url, auth=HTTPBasicAuth(opt_username, pwd), headers=headers, verify=verify) + cookies = {"nc_sameSiteCookiestrict": "true"} + response = session.get(url, headers=headers, cookies=cookies, verify=verify) status = response.status_code if (status == 200): jsdata = response.text @@ -182,18 +175,12 @@ def getDataAllUsers(url, verify): sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n") sys.exit(1) -def getDataUser(url, verify): - #print(url) +def getDataUser(session, url, verify): headers = CaseInsensitiveDict() headers["Accept"] = "application/json" headers["OCS-APIRequest"] = "true" - if (opt_token == '0'): - # authenticate with username and password - pwd = opt_password - else: - # authenticate with token - pwd = opt_token - response = requests.get(url, auth=HTTPBasicAuth(opt_username, pwd), headers=headers, verify=verify) + cookies = {"nc_sameSiteCookiestrict": "true"} + response = session.get(url, headers=headers, cookies=cookies, verify=verify) status = response.status_code if (status == 200): jsdata = response.text @@ -208,6 +195,12 @@ def doCmkOutput(data): 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']}") @@ -215,8 +208,12 @@ def doCmkOutput(data): 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']}") - 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']}") + # 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']}") + 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']}") @@ -234,11 +231,11 @@ def doCmkOutput(data): else: print(f"NC_OPCache_Hit_Rate;0") -def doCmkOutputAllUsers(data, verify, hostname, protocol, port, folder): +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(nc_url, verify) + 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']) @@ -260,6 +257,8 @@ def doCmkOutputAllUsers(data, verify, hostname, protocol, port, folder): 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") @@ -290,13 +289,22 @@ def main(): 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) + sys.exit(1) + if (opt_token == '0'): + # authenticate with username and password + pwd = opt_password + else: + # authenticate with token + pwd = opt_token + + # create session + session = getSession(opt_username, pwd) nc_url = createUrl(nc_api_endpoint, opt_hostname, protocol, port, opt_folder) - nc_data = getData(nc_url, verify) + 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(nc_url, verify) - doCmkOutputAllUsers(nc_data, verify, 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/checks/agent_nextcloud b/local/share/check_mk/checks/agent_nextcloud index 9d864e6..9f653a1 100644 --- a/local/share/check_mk/checks/agent_nextcloud +++ b/local/share/check_mk/checks/agent_nextcloud @@ -3,7 +3,7 @@ def agent_nextcloud_arguments(params, hostname, ipaddress): "--hostname", params["hostname"], "--username", params["username"], "--password", params["password"], - "--token", params["token"], + "--token", passwordstore_get_cmdline("%s", params["token"]), "--port", params["port"], "--folder", params["folder"], "--no-https", params["no_https"], diff --git a/local/share/check_mk/web/plugins/wato/nextcloud_params.py b/local/share/check_mk/web/plugins/wato/nextcloud_params.py index 32c524d..081d0d1 100644 --- a/local/share/check_mk/web/plugins/wato/nextcloud_params.py +++ b/local/share/check_mk/web/plugins/wato/nextcloud_params.py @@ -1,10 +1,13 @@ -from cmk.gui.i18n import _ -from cmk.gui.plugins.wato import ( - CheckParameterRulespecWithItem, - rulespec_registry, - RulespecGroupCheckParametersOperatingSystem, -) +#!/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, @@ -13,11 +16,8 @@ from cmk.gui.valuespec import ( Password, ) -from cmk.gui.plugins.wato import ( - HostRulespec, -) - -from cmk.gui.plugins.wato.datasource_programs import RulespecGroupDatasourceProgramsCustom +def _factory_default_special_agent_nextcloud(): + return Rulespec.FACTORY_DEFAULT_UNUSED def _valuespec_special_agent_nextcloud(): return Dictionary( @@ -33,7 +33,7 @@ def _valuespec_special_agent_nextcloud(): ("password", Password(title=_("Password"), allow_empty=True, help=_("Specify password OR token, not both, token recommended"))), - ("token", Password(title=_("Token"), + ("token", IndividualOrStoredPassword(title=_("App Password"), allow_empty=True, help=_("Specify password OR token, not both, token recommended"))), ("port", TextAscii(title=_("Port"), @@ -52,8 +52,9 @@ def _valuespec_special_agent_nextcloud(): rulespec_registry.register( HostRulespec( - group=RulespecGroupDatasourceProgramsCustom, - name="special_agents:nextcloud", - valuespec=_valuespec_special_agent_nextcloud, + 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/mkp/Nextcloud-2.4.0.mkp b/mkp/Nextcloud-2.4.0.mkp new file mode 100644 index 0000000..466145f Binary files /dev/null and b/mkp/Nextcloud-2.4.0.mkp differ