initial upload of version 3.1.1 which uses the new plugin API

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

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

@@ -0,0 +1,252 @@
#!/usr/bin/env python3
# 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 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
if value >= warn:
return State.WARN
return State.OK
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_info(section) -> DiscoveryResult:
yield Service()
def check_nextcloud_info(params, section) -> CheckResult:
for key in section:
if key == "nextcloud":
_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"
)
update_available = "False"
last_update_human = "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"]
# create graph for number of files and 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 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
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 = "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
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))
notice = f"Remaining free space on disk: {render.bytes(free_space)}"
if state != State.OK:
yield Result(state=state, notice=notice)
# Create result for number of files
warn, crit = levels_number_of_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)
elif key == "users":
num_users = section[key]["number"]
num_active_last1hour = section[key]["active_last1hour"]
num_active_last1day = section[key]["active_last1day"]
num_active_last5min = section[key]["active_last5min"]
# create graphs for number of users
yield Metric("nc_num_users", num_users)
yield Metric("nc_active_users_last_1hour", num_active_last1hour)
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)
elif key == "storages":
num_storages = section[key]["number"]
num_storages_home = section[key]["number_home"]
num_storages_local = section[key]["number_local"]
num_storages_other = section[key]["number_other"]
# create graphs for number of storages
yield Metric("nc_num_storages", num_storages)
yield Metric("nc_num_storages_home", num_storages_home)
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)
elif key == "apps":
# 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"]
if "app_versions" in section[key]:
app_versions = section[key]["app_versions"]
else:
app_versions = ""
# create graphs for number of apps
_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 = 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)
except KeyError:
# TBD
pass
def parse_nextcloud_info(string_table: StringTable) -> dict:
parsed_data = {
"nextcloud": {},
"storages": {},
"apps": {},
"users": {},
}
params_list = [
"NC_Version",
"NC_Freespace",
"NC_Status",
"NC_Last_Update",
"NC_Update_Available",
"NC_Webserver",
"NC_PHP_Version",
"NC_Num_Users",
"NC_Num_Files",
"NC_Num_Shares",
"NC_Num_Storages",
"NC_Num_Storages_Home",
"NC_Num_Storages_Local",
"NC_Num_Storages_Other",
"NC_Num_Apps_Installed",
"NC_Num_Apps_Updates_Available",
"NC_Apps_With_Updates_Available",
"NC_Active_Users_Last_5Min",
"NC_Active_Users_Last_1Hour",
"NC_Active_Users_Last_1Day",
]
for line in string_table:
if line[0] in params_list:
param = line[0]
value = line[1]
if param == "NC_Version":
parsed_data["nextcloud"]["version"] = value
elif param == "NC_Freespace":
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":
parsed_data["nextcloud"]["php_version"] = value
elif param == "NC_Num_Files":
parsed_data["nextcloud"]["number_files"] = int(value)
elif param == "NC_Num_Shares":
parsed_data["nextcloud"]["number_shares"] = int(value)
elif param == "NC_Num_Storages":
parsed_data["storages"]["number"] = int(value)
elif param == "NC_Num_Storages_Home":
parsed_data["storages"]["number_home"] = int(value)
elif param == "NC_Num_Storages_Local":
parsed_data["storages"]["number_local"] = int(value)
elif param == "NC_Num_Storages_Other":
parsed_data["storages"]["number_other"] = int(value)
elif param == "NC_Num_Apps_Installed":
parsed_data["apps"]["installed"] = int(value)
elif param == "NC_Num_Apps_Updates_Available":
parsed_data["apps"]["with_updates_available"] = int(value)
elif param == "NC_Apps_With_Updates_Available":
parsed_data["apps"]["app_versions"] = value
elif param == "NC_Num_Users":
parsed_data["users"]["number"] = int(value)
elif param == "NC_Active_Users_Last_5Min":
parsed_data["users"]["active_last5min"] = int(value)
elif param == "NC_Active_Users_Last_1Hour":
parsed_data["users"]["active_last1hour"] = int(value)
elif param == "NC_Active_Users_Last_1Day":
parsed_data["users"]["active_last1day"] = int(value)
return parsed_data
agent_section_nextcloud_info = AgentSection(
name="nextcloud_info",
parse_function=parse_nextcloud_info,
)
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": ("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

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
# pylint: disable=missing-module-docstring, unused-argument, missing-function-docstring
# pylint: disable=line-too-long, too-many-locals
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
if value >= warn:
return State.WARN
return State.OK
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_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]
quota_total_bytes = section[item][2]
display_name = section[item][3]
last_login_human = section[item][4]
last_login_since = section[item][5]
free_space = quota_total_bytes - quota_used_bytes
# 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)"
)
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 = 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 Metric("nc_users_quota_used", quota_used_percent, levels=levels_quota_used)
state = get_state_upper(levels_quota_used, quota_used_percent)
yield Result(state=state, summary=summary, details=details)
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)
parsed_data = {}
for line in string_table:
userid = line[0]
display_name = line[1]
last_login = int(line[2]) / 1000
if last_login == 0:
# user never logged in
last_login_human = "never"
last_login_since = "never"
else:
# user logged in at least once
curr_time = int(time())
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,
]
return parsed_data
agent_section_nextcloud_users = AgentSection(
name="nextcloud_users",
parse_function=parse_nextcloud_users,
)
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

@@ -0,0 +1,20 @@
title: Nextcloud: Various 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 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 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.
inventory:
one service is created (with several details)

View File

@@ -0,0 +1,20 @@
title: Nextcloud: Quota and Storage Usage of Users
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 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.
The check will raise WARN/CRIT if quota usage is above the configurable levels.
The check will raise WARN/CRIT if free space is below the configurable levels.
item:
userid
inventory:
one service is created for each user

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