MKP 2.4.0, added password store for app password, some changes for compatibility with NC 28

This commit is contained in:
mellis 2024-01-12 12:23:21 +01:00
parent 1fbeae93a1
commit 714990bffb
7 changed files with 137 additions and 89 deletions

2
.gitignore vendored
View File

@ -16,7 +16,7 @@ __pycache__/
*.vsix
# Exclude all "private" files (Ralf Mellis)
RaMe*
RM*
# Exclude all "private" files (Torsten Behne)
TB*

View File

@ -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|

View File

@ -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",

View File

@ -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("<<<nextcloud_users:sep(59)>>>")
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()

View File

@ -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"],

View File

@ -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,
)
)

BIN
mkp/Nextcloud-2.4.0.mkp Normal file

Binary file not shown.