initial upload of version 3.1.1 which uses the new plugin API

This commit is contained in:
Ralf Mellis 2025-04-05 12:00:39 +02:00
parent 0152860b55
commit c0f4490b89
26 changed files with 1106 additions and 931 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

BIN
mkp/.DS_Store vendored Normal file

Binary file not shown.

BIN
mkp/Nextcloud-3.1.1.mkp Normal file

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

312
nextcloud/libexec/agent_nextcloud Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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