From c0f4490b89a7eb9558dc6692d2641a6e2b810f18 Mon Sep 17 00:00:00 2001
From: cmk-bonobo <cmk.bonobo@arschkrebs.org>
Date: Sat, 5 Apr 2025 12:00:39 +0200
Subject: [PATCH] initial upload of version 3.1.1 which uses the new plugin API

---
 README.md                                     |  15 +-
 .../plugins/agent_based/nextcloud_database.py |  82 -----
 .../check_mk/agents/special/agent_nextcloud   | 311 -----------------
 .../check_mk/checkman/nextcloud_database      |  16 -
 local/share/check_mk/checks/agent_nextcloud   |  13 -
 .../web/plugins/metrics/nextcloud_metrics.py  | 135 --------
 .../perfometer/nextcloud_perfometers.py       |  34 --
 .../plugins/wato/nextcloud_database_rules.py  |  43 ---
 .../web/plugins/wato/nextcloud_info_rules.py  |  74 -----
 .../web/plugins/wato/nextcloud_params.py      |  60 ----
 .../web/plugins/wato/nextcloud_users_rules.py |  68 ----
 mkp/.DS_Store                                 | Bin 0 -> 6148 bytes
 mkp/Nextcloud-3.1.1.mkp                       | Bin 0 -> 12512 bytes
 nextcloud/agent_based/nextcloud_database.py   | 105 ++++++
 .../agent_based/nextcloud_info.py             | 109 +++---
 .../agent_based/nextcloud_users.py            | 101 ++++--
 nextcloud/checkman/nextcloud_database         |  16 +
 .../checkman/nextcloud_info                   |  12 +-
 .../checkman/nextcloud_users                  |   9 +-
 nextcloud/graphing/graph_nextcloud.py         | 198 +++++++++++
 nextcloud/libexec/agent_nextcloud             | 312 ++++++++++++++++++
 .../rulesets/nextcloud_database_rules.py      |  45 +++
 nextcloud/rulesets/nextcloud_info_rules.py    |  66 ++++
 nextcloud/rulesets/nextcloud_params.py        | 105 ++++++
 nextcloud/rulesets/nextcloud_users_rules.py   |  54 +++
 .../server_side_calls/agent_nextcloud.py      |  54 +++
 26 files changed, 1106 insertions(+), 931 deletions(-)
 delete mode 100644 local/lib/python3/cmk/base/plugins/agent_based/nextcloud_database.py
 delete mode 100755 local/share/check_mk/agents/special/agent_nextcloud
 delete mode 100644 local/share/check_mk/checkman/nextcloud_database
 delete mode 100644 local/share/check_mk/checks/agent_nextcloud
 delete mode 100644 local/share/check_mk/web/plugins/metrics/nextcloud_metrics.py
 delete mode 100644 local/share/check_mk/web/plugins/perfometer/nextcloud_perfometers.py
 delete mode 100644 local/share/check_mk/web/plugins/wato/nextcloud_database_rules.py
 delete mode 100644 local/share/check_mk/web/plugins/wato/nextcloud_info_rules.py
 delete mode 100644 local/share/check_mk/web/plugins/wato/nextcloud_params.py
 delete mode 100644 local/share/check_mk/web/plugins/wato/nextcloud_users_rules.py
 create mode 100644 mkp/.DS_Store
 create mode 100644 mkp/Nextcloud-3.1.1.mkp
 create mode 100644 nextcloud/agent_based/nextcloud_database.py
 rename {local/lib/python3/cmk/base/plugins => nextcloud}/agent_based/nextcloud_info.py (75%)
 rename {local/lib/python3/cmk/base/plugins => nextcloud}/agent_based/nextcloud_users.py (54%)
 create mode 100644 nextcloud/checkman/nextcloud_database
 rename {local/share/check_mk => nextcloud}/checkman/nextcloud_info (60%)
 rename {local/share/check_mk => nextcloud}/checkman/nextcloud_users (59%)
 create mode 100644 nextcloud/graphing/graph_nextcloud.py
 create mode 100755 nextcloud/libexec/agent_nextcloud
 create mode 100644 nextcloud/rulesets/nextcloud_database_rules.py
 create mode 100644 nextcloud/rulesets/nextcloud_info_rules.py
 create mode 100644 nextcloud/rulesets/nextcloud_params.py
 create mode 100644 nextcloud/rulesets/nextcloud_users_rules.py
 create mode 100644 nextcloud/server_side_calls/agent_nextcloud.py

diff --git a/README.md b/README.md
index 7c8de27..65c7dc1 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,18 @@
 # Nextcloud CheckMK Special Agent
 Monitors various aspects of Nextcloud instances like state, quota and disk usage of all users, number of apps with available updates, database php opcache hit rate and so on.
 Gives additional information regarding versions of Nextcloud, database, number of storages and active users etc.
-Tested with Nextcloud 25/26/27/28.
-Tested only with MySQL/MariaDB as database backend.
+Tested only with Nextcloud versions 29/30
+Tested only with MariaDB as database backend
+The whole plugin is now migrated to the new plugin API version, introduced with CheckMK 2.3
 Feel free to report other working environments.
 
 
-Upgrade from older MKPs (before 2.4.0):
-If you upgrade from a already installed version before 2.4.0, you have to re-create your rules for the "Nextcloud Server Information" (reason: massive parameter changes).
-Steps to accomplish this without problems:
+Upgrade from older MKPs (before 3.1.1):
 
-1. Take a screenshot of your settings in the above mentioned ruleset
+If you upgrade from a already installed MKP version before 3.1.0, you have to re-create your rules for the "Nextcloud Server Information" (reason: some parameter changes combined with the migration to the new plugin API).
+Hint: It is always a good idea to create screenshots from all other Nextcloud rules as well.
+
+1. Take a screenshot of your settings in the above mentioned rulesets
 2. Assure that you have access to the passwords/tokens you used within the current rules
 3. Delete all rules for "Nextcloud Server Information"
 4. Install and enable the new MKP
@@ -39,6 +41,7 @@ Version History:
 --
 |Date|Version|Changes|
 |----|-------|-------|
+|2025/03/30|3.1.1|Migration to the new plugin API version finished, (hopefully) ready for CheckMK version 2.4
 |2023/01/13|2.5.2|Repackaged only to set the minimum required version back to 2.1.0p1, due to serveral user requests|
 |2023/01/12|2.5.1|Added versions for apps with available updates|
 |2023/01/12|2.4.1|Removed Parameter "token", switched to parameter "app password" only|
diff --git a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_database.py b/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_database.py
deleted file mode 100644
index 7a179c0..0000000
--- a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_database.py
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/usr/bin/env python3
-from pprint import pprint
-from .agent_based_api.v1 import register, render, Service, Result, State, Metric
-
-def getStateUpper(levels, value):
-    warn, crit = levels
-    if value >= crit:
-        return State.CRIT
-    if value >= warn:
-        return State.WARN
-    return State.OK
-
-def getStateLower(levels, value):
-    warn, crit = levels
-    if value < crit:
-        return State.CRIT
-    if value < warn:
-        return State.WARN
-    return State.OK
-
-def discover_nextcloud_database(section):
-    yield(Service())
-    
-def check_nextcloud_database(params, section): 
-    for key in section:
-        if key == "database":
-            opcache_hit_rate = section[key]["opcache_hit_rate"]
-            size = section[key]["size"]
-            type = section[key]["type"]
-            version = section[key]["version"]
-            levels = params["levels_database_opcache_hit_rate"]
-            # create graph for opcache hit rate
-            yield Metric("nc_database_opcache_hit_rate", opcache_hit_rate, levels=levels)
-            # create graph for database size
-            yield Metric("nc_database_size", size)
-            state = getStateLower(levels, opcache_hit_rate)
-            summary = f"PHP OPCache hit rate: {render.percent(opcache_hit_rate)}"
-            if opcache_hit_rate == 0:
-                state = State.UNKNOWN
-                summary = f"PHP OPCache hit rate: 0% - please check if opcache_get_status is enabled"
-            details = f"\nDatabase type: {type}\nDatabase version: {version}\nDatabase size: {render.bytes(size)}"
-            yield(Result(state=state, summary=summary, details=details))
-
-def parse_nextcloud_database_section(string_table):
-    parsed_data = {
-        "database" : {}
-    }
-    params_list = [
-        "NC_Database_Type",
-        "NC_Database_Version",
-        "NC_Database_Size",
-        "NC_OPCache_Hit_Rate"
-    ]
-    for line in string_table:
-        if line[0] in params_list:
-            param = line[0]
-            value = line[1]
-            if param == "NC_Database_Type":
-                parsed_data["database"]["type"] = value
-            elif param == "NC_Database_Version":
-                parsed_data["database"]["version"] = value
-            elif param == "NC_Database_Size":
-                parsed_data["database"]["size"] = int(value)
-            elif param == "NC_OPCache_Hit_Rate":
-                parsed_data["database"]["opcache_hit_rate"] = float(value)
-    return parsed_data
-
-register.agent_section(
-    name="nextcloud_database",
-    parse_function=parse_nextcloud_database_section,
-)
-
-register.check_plugin(
-    name="nextcloud_database",
-    service_name="Nextcloud Database",
-    discovery_function=discover_nextcloud_database,
-    check_function=check_nextcloud_database,
-    check_default_parameters={
-        "levels_database_opcache_hit_rate": (99.7, 99.1),
-    },
-    check_ruleset_name="nextcloud_database",
-)
\ No newline at end of file
diff --git a/local/share/check_mk/agents/special/agent_nextcloud b/local/share/check_mk/agents/special/agent_nextcloud
deleted file mode 100755
index 3470fdf..0000000
--- a/local/share/check_mk/agents/special/agent_nextcloud
+++ /dev/null
@@ -1,311 +0,0 @@
-#!/usr/bin/env python3
-
-import getopt
-import sys
-import requests
-import urllib3
-import json
-import os
-from pprint import pprint
-from requests.structures import CaseInsensitiveDict
-from requests.auth import HTTPBasicAuth
-import cmk.utils.password_store
-
-def showUsage():
-    sys.stderr.write("""CheckMK Nextcloud Special Agent
-
-USAGE: agent_nextcloud_info -u [username] -p [password]
-        OR
-       agent_nextcloud_info -h
-
-OPTIONS:
-  -H, --hostname                Hostname (FQDN or IP) of Nextcloud server
-  -u, --username                Username
-  -p, --password                App Password
-  -P, --port                    Port
-  -f, --folder                  Subfolder if not installed in web root
-  --no-https True|False         If "True": Disable HTTPS, use HTTP (not recommended!)
-  --no-cert-check True|False    If "True": Disable TLS certificate check (not recommended!)
-  -h, --help                    Show this help message and exit
-""")
-
-# set this to true to produce debug output (this clutters the agent output)
-# be aware: activating this flag logs very sensitive information to debug files in ~/tmp 
-# !!DO NOT FORGET to delete these files after debugging is done!!
-
-DEBUG = False
-
-nc_api_endpoint = "ocs/v2.php/apps/serverinfo/api/v1/info?format=json"
-nc_api_endpoint_all_users = "ocs/v1.php/cloud/users?format=json"
-nc_api_endpoint_user = "ocs/v1.php/cloud/users"
-
-opt_hostname = ""
-opt_username = ""
-opt_password = ""
-opt_port = ""
-opt_folder = ""
-opt_no_https = False
-opt_no_cert_check = False
-
-short_options = 'hH:u:p:P:f:'
-long_options = [
-    'hostname=', 'username=', 'password=', 'port=', 'folder=', 'no-https=', 'no-cert-check=', 'help'
-]
-
-def logDebug(line):
-    if DEBUG:
-        home_path = os.getenv("HOME")
-        tmp_path = f"{home_path}/tmp"
-        help_file = f"{tmp_path}/nextcloud_{opt_hostname}_{opt_port}_debug.txt"
-        with open(help_file, "a") as file:
-            file.write(line)
-
-
-def getOptions():
-    global opt_hostname
-    global opt_username
-    global opt_password
-    global opt_port
-    global opt_folder
-    global opt_no_https
-    global opt_no_cert_check
-
-    opts, args = getopt.getopt(sys.argv[1:], short_options, long_options)
-    for opt, arg in opts:
-        if opt in ['-H', '--hostname']:
-            opt_hostname = arg
-        elif opt in ['-u', '--username']:
-            opt_username = arg
-        elif opt in ['-p', '--password']:
-            opt_password = arg
-        elif opt in ['-P', '--port']:
-            opt_port = arg
-        elif opt in ['-f', '--folder']:
-            opt_folder = arg
-        elif opt in ['--no-https']:
-            if arg == 'True':
-                opt_no_https = True
-            else:
-                opt_no_https = False
-        elif opt in ['--no-cert-check']:
-            if arg == 'True':
-                opt_no_cert_check = True
-            else:
-                opt_no_cert_check = False
-        elif opt in ['-h', '--help']:
-            showUsage()
-            sys.exit(0)
-    logDebug(f"getOptions - Number of Arguments: {len(sys.argv)}, Argument List: {str(sys.argv)}\n")
-
-def showOptions():
-    print(f"Hostname: {opt_hostname}")
-    print(f"Username: {opt_username}")
-    print(f"Password: {opt_password}")
-    print(f"Port: {opt_port}")
-    print(f"Folder: {opt_folder}")
-    print(f"No HTTPS: {opt_no_https}")
-    print(f"No TLS Check: {opt_no_cert_check}")
-    logDebug(f"showOptions - Hostname: {opt_hostname}, Port: {opt_port}, No HTTPS: {opt_no_https}, No Cert Check: {opt_no_cert_check}\n")
-
-def createUrl(endpoint, hostname, protocol, port, folder):
-    # these parameters are needed, otherwise no information about updates regarding apps and Nextcloud itself are reported (since version 28)
-    params = "skipApps=false&skipUpdate=false"
-    if folder == "":
-        url = f"{protocol}://{hostname}:{port}/{endpoint}"
-    else:
-        url = f"{protocol}://{hostname}:{port}/{folder}/{endpoint}"
-    if endpoint == nc_api_endpoint:
-        url = f"{url}&{params}"
-    logDebug(f"createUrl - Data URL: {url}\n")
-    return url
-
-def createUrlUser(user, endpoint, hostname, protocol, port, folder):
-    params = "format=json"
-    if folder == "":
-        url = f"{protocol}://{hostname}:{port}/{endpoint}/{user}?{params}"
-    else:
-        url = f"{protocol}://{hostname}:{port}/{folder}/{endpoint}/{user}?{params}"
-    logDebug(f"createUrlUser - User URL: {url}\n")
-    return url
-
-def getSession(username, secret):
-    session = requests.session()
-    session.cookies.set("SameSite", "Strict")
-    session.auth = (username, secret)
-    session.headers['Accept'] = "application/json"
-    return session
-
-def getData(session, url, verify):
-    data = {}
-    headers = CaseInsensitiveDict()
-    headers["Accept"] = "application/json"
-    response = session.get(url, headers=headers, verify=verify)
-    status = response.status_code
-    if (status == 200):
-        jsdata = response.text
-        data = json.loads(jsdata) # returns  a dictionary
-        return data
-    elif (status == 503):
-        # this code is reported when maintenance mode is on
-        sys.stderr.write(f"Request response code is {response.status_code} with URL {url}, maybe maintenance mode is on?\n")
-        sys.exit(1)
-    else:
-        sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n")
-        sys.exit(1)
-
-def getDataAllUsers(session, url, verify):
-    headers = CaseInsensitiveDict()
-    headers["Accept"] = "application/json"
-    headers["OCS-APIRequest"] = "true"
-    response = session.get(url, headers=headers, verify=verify)
-    status = response.status_code
-    if (status == 200):
-        jsdata = response.text
-        data = json.loads(jsdata) # returns  a dictionary
-        return data
-    else:
-        sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n")
-        sys.exit(1)
-
-def getDataUser(session, url, verify):
-    headers = CaseInsensitiveDict()
-    headers["Accept"] = "application/json"
-    headers["OCS-APIRequest"] = "true"
-    response = session.get(url, headers=headers, verify=verify)
-    status = response.status_code
-    if (status == 200):
-        jsdata = response.text
-        data = json.loads(jsdata) # returns  a dictionary
-        return data
-    else:
-        sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n")
-        sys.exit(1)
-    
-def doCmkOutput(data):
-    apps_with_updates_available = {}
-    str_apps_with_updates_available = ""
-    print("<<<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()
\ No newline at end of file
diff --git a/local/share/check_mk/checkman/nextcloud_database b/local/share/check_mk/checkman/nextcloud_database
deleted file mode 100644
index ad96286..0000000
--- a/local/share/check_mk/checkman/nextcloud_database
+++ /dev/null
@@ -1,16 +0,0 @@
-title: Nextcloud: Database information
-agents: linux
-catalog: unsorted
-license: GPL
-distribution: check_mk
-description:
- Works with Nextcloud version 25/26/27 (use at your own risk with lower versions).
- Tested only with mysql/mariab as underlying database.
- You may use a username/password or username/token combination to get access to the Nextcloud API.
- You can create this token within the personal settings of an administrative user in Nextcloud.
- Got to security settings and create a new app password, use this for the token.
- The user must not be secured with 2FA.
- Shows several information about a Nextcloud database (type/version/size).
- The check will raise WARN/CRIT if Database PHP OPcache hit rate is below the configurable levels. 
-inventory:
- one service is created (with several details)
\ No newline at end of file
diff --git a/local/share/check_mk/checks/agent_nextcloud b/local/share/check_mk/checks/agent_nextcloud
deleted file mode 100644
index b59e9ae..0000000
--- a/local/share/check_mk/checks/agent_nextcloud
+++ /dev/null
@@ -1,13 +0,0 @@
-def agent_nextcloud_arguments(params, hostname, ipaddress):
-        return [
-                "--hostname", params["hostname"],
-                "--username", params["username"],
-                "--password", passwordstore_get_cmdline("%s", params["password"]),
-                "--port", params["port"],
-                "--folder", params["folder"],
-                "--no-https", params["no_https"],
-                "--no-cert-check", params["no_cert_check"],
-                ipaddress,
-        ]
-
-special_agent_info['nextcloud'] = agent_nextcloud_arguments
diff --git a/local/share/check_mk/web/plugins/metrics/nextcloud_metrics.py b/local/share/check_mk/web/plugins/metrics/nextcloud_metrics.py
deleted file mode 100644
index d7cb0f9..0000000
--- a/local/share/check_mk/web/plugins/metrics/nextcloud_metrics.py
+++ /dev/null
@@ -1,135 +0,0 @@
-#!/usr/bin/env python3
-from cmk.gui.i18n import _
-from cmk.gui.plugins.metrics import (
-    metric_info,
-    graph_info,
-)
-
-metric_info["nc_num_users"] = {
-    "title": _("Number of Users"),
-    "unit": "count",
-    "color": "44/a",
-}
-
-metric_info["nc_num_shares"] = {
-    "title": _("Number of Shares"),
-    "unit": "count",
-    "color": "44/b",
-}
-
-metric_info["nc_num_storages"] = {
-    "title": _("Number of Storages"),
-    "unit": "count",
-    "color": "42/a",
-}
-
-metric_info["nc_num_storages_home"] = {
-    "title": _("Number of Home Storages"),
-    "unit": "count",
-    "color": "44/a",
-}
-
-metric_info["nc_num_storages_local"] = {
-    "title": _("Number of Local Storages"),
-    "unit": "count",
-    "color": "44/b",
-}
-
-metric_info["nc_num_storages_other"] = {
-    "title": _("Number of Other Storages"),
-    "unit": "count",
-    "color": "42/a",
-}
-
-metric_info["nc_num_files"] = {
-    "title": _("Number of Files"),
-    "unit": "count",
-    "color": "42/a",
-}
-
-metric_info["nc_num_apps_installed"] = {
-    "title": _("Number of installed Apps"),
-    "unit": "count",
-    "color": "43/a",
-}
-
-metric_info["nc_apps_with_updates_available"] = {
-    "title": _("Number of installed Apps with Updates Available"),
-    "unit": "count",
-    "color": "24/a",
-}
-
-metric_info["nc_active_users_last_5min"] = {
-    "title": _("Number of Active Users in the last 5 minutes"),
-    "unit": "count",
-    "color": "24/a",
-}
-
-metric_info["nc_active_users_last_1hour"] = {
-    "title": _("Number of Active Users in the last 1 hour"),
-    "unit": "count",
-    "color": "41/a",
-}
-
-metric_info["nc_active_users_last_1day"] = {
-    "title": _("Number of Active Users in the last 1 day"),
-    "unit": "count",
-    "color": "42/a",
-}
-
-metric_info["nc_users_free_space"] = {
-    "title": _("Free Space of User"),
-    "unit": "bytes",
-    "color": "22/a",
-}
-
-metric_info["nc_free_space"] = {
-    "title": _("Free Space on Disk"),
-    "unit": "bytes",
-    "color": "22/a",
-}
-
-metric_info["nc_database_size"] = {
-    "title": _("Database Size"),
-    "unit": "bytes",
-    "color": "24/a",
-}
-
-metric_info["nc_database_opcache_hit_rate"] = {
-    "title": _("Database PHP OPCache Hit Rate"),
-    "unit": "%",
-    "color": "24/a",
-}
-
-metric_info["nc_users_quota_used"] = {
-    "title": _("Quota used"),
-    "unit": "%",
-    "color": "16/a",
-}
-
-graph_info["number_of_users_shares_storages_combined"] = {
-    "title": _("Number of Users, Shares and Storages"),
-    "metrics": [
-        ("nc_num_users", "area"),
-        ("nc_num_shares", "stack"),
-        ("nc_num_storages", "stack"),
-    ],
-}
-
-graph_info["number_of_storage_types_combined"] = {
-    "title": _("Number of Storage Types"),
-    "metrics": [
-        ("nc_num_storages_home", "area"),
-        ("nc_num_storages_local", "stack"),
-        ("nc_num_storages_other", "stack"),
-    ],
-}
-
-graph_info["number_of_active_users_combined"] = {
-    "title": _("Number of Active Users"),
-    "metrics": [
-        ("nc_active_users_last_5min", "area"),
-        ("nc_active_users_last_1hour", "stack"),
-        ("nc_active_users_last_1day", "stack"),
-    ],
-}
diff --git a/local/share/check_mk/web/plugins/perfometer/nextcloud_perfometers.py b/local/share/check_mk/web/plugins/perfometer/nextcloud_perfometers.py
deleted file mode 100644
index 9774e06..0000000
--- a/local/share/check_mk/web/plugins/perfometer/nextcloud_perfometers.py
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env python3
-from cmk.gui.plugins.metrics import perfometer_info
-
-perfometer_info.append({
-    "type": "stacked",
-    "perfometers": [
-        {
-            "type": "linear",
-            "segments": ["nc_database_opcache_hit_rate"],
-            "total": 100.0,
-        },
-    ],
-})
-
-perfometer_info.append({
-    "type": "stacked",
-    "perfometers": [
-        {
-            "type": "linear",
-            "segments": ["nc_database_size"],
-        },
-    ],
-})
-
-perfometer_info.append({
-    "type": "stacked",
-    "perfometers": [
-        {
-            "type": "linear",
-            "segments": ["nc_users_quota_used"],
-            "total": 100.0,
-        },
-    ],
-})
diff --git a/local/share/check_mk/web/plugins/wato/nextcloud_database_rules.py b/local/share/check_mk/web/plugins/wato/nextcloud_database_rules.py
deleted file mode 100644
index 5d5c005..0000000
--- a/local/share/check_mk/web/plugins/wato/nextcloud_database_rules.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from cmk.gui.i18n import _
-from cmk.gui.plugins.wato import (
-    CheckParameterRulespecWithoutItem,
-    rulespec_registry,
-    RulespecGroupCheckParametersApplications
-)
-from cmk.gui.valuespec import (
-    Dictionary,
-    ListChoice,
-    Tuple,
-    Percentage,
-    Integer,
-    Float,
-)
-
-def _parameter_spec_nextcloud_database():
-    return Dictionary(
-        elements=[
-            ("levels_database_opcache_hit_rate", Tuple(
-                title=_("Nextcloud levels for database php opcache hit rate"),
-                    elements=[
-                        Percentage(
-                            title=_("Warning below"),
-                            default_value=99.7,
-                        ),
-                        Percentage(
-                            title=_("Critical below"),
-                            default_value=99.1,
-                        )
-                    ],
-            )),
-        ],
-    )
-
-rulespec_registry.register(
-    CheckParameterRulespecWithoutItem(
-        check_group_name="nextcloud_database",
-        group=RulespecGroupCheckParametersApplications,
-        match_type="dict",
-        parameter_valuespec=_parameter_spec_nextcloud_database,
-        title=lambda: _("Nextcloud Database"),
-    )
-)
diff --git a/local/share/check_mk/web/plugins/wato/nextcloud_info_rules.py b/local/share/check_mk/web/plugins/wato/nextcloud_info_rules.py
deleted file mode 100644
index 29e8c53..0000000
--- a/local/share/check_mk/web/plugins/wato/nextcloud_info_rules.py
+++ /dev/null
@@ -1,74 +0,0 @@
-from cmk.gui.i18n import _
-from cmk.gui.plugins.wato import (
-    CheckParameterRulespecWithoutItem,
-    rulespec_registry,
-    RulespecGroupCheckParametersApplications
-)
-
-from cmk.gui.valuespec import (
-    Dictionary,
-    ListChoice,
-    Tuple,
-    Percentage,
-    Integer,
-    Float,
-)
-
-def _parameter_spec_nextcloud_info():
-    return Dictionary(
-        elements=[
-            ("levels_apps_with_updates_available", Tuple(
-                title=_("Nextcloud number of installed apps with updates available"),
-                    elements=[
-                        Integer(
-                            title=_("Warning at"),
-                            default_value=1,
-                        ),
-                        Integer(
-                            title=_("Critical at"),
-                            default_value=2,
-                        )
-                    ],
-            )),
-            ("levels_free_space", Tuple(
-                title=_("Nextcloud levels for free disk space overall"),
-                    elements=[
-                        Float(
-                            title=_("Warning below"),
-                            default_value=8.0,
-                            unit="GBytes",
-                        ),
-                        Float(
-                            title=_("Critical below"),
-                            default_value=4.0,
-                            unit="GBytes",
-                        )
-                    ],
-            )),
-            ("levels_number_of_files", Tuple(
-                title=_("Nextcloud number of files"),
-                    elements=[
-                        Integer(
-                            title=_("Warning at"),
-                            default_value=100000,
-                            size=32,
-                        ),
-                        Integer(
-                            title=_("Critical at"),
-                            default_value=250000,
-                            size=32,
-                        )
-                    ],
-            )),
-        ],
-    )
-
-rulespec_registry.register(
-    CheckParameterRulespecWithoutItem(
-        check_group_name="nextcloud_info",
-        group=RulespecGroupCheckParametersApplications,
-        match_type="dict",
-        parameter_valuespec=_parameter_spec_nextcloud_info,
-        title=lambda: _("Nextcloud Info"),
-    )
-)
\ No newline at end of file
diff --git a/local/share/check_mk/web/plugins/wato/nextcloud_params.py b/local/share/check_mk/web/plugins/wato/nextcloud_params.py
deleted file mode 100644
index ae0923e..0000000
--- a/local/share/check_mk/web/plugins/wato/nextcloud_params.py
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/usr/bin/env python3
-
-from cmk.gui.i18n import _
-from cmk.gui.plugins.wato.special_agents.common import RulespecGroupDatasourceProgramsApps
-from cmk.gui.plugins.wato.utils import (
-    HostRulespec,
-    Rulespec,
-    IndividualOrStoredPassword,
-    rulespec_registry,
-)
-from cmk.gui.valuespec import (
-    Dictionary,
-    ListChoice,
-    Checkbox,
-    TextAscii,
-    Password,
-)
-
-def _factory_default_special_agent_nextcloud():
-    return Rulespec.FACTORY_DEFAULT_UNUSED
-
-def _valuespec_special_agent_nextcloud():
-    return Dictionary(
-        title=_("Nextcloud Server Information"),
-        help = _("Checking Nextcloud servers via API"),
-        elements=[
-            ("hostname", TextAscii(title=_("Hostname"),
-                allow_empty=False,
-                size = 40,
-                help=_("Hostname of Nextcloud server (bare FQDN or IP), mandatory, eg. nextcloud.yourdomain.tld"))),
-            ("username", TextAscii(title=_("Username"),
-                size = 40,
-                allow_empty=False,
-                help=_("Username with administrative rights, mandatory"))),
-            ("password", IndividualOrStoredPassword(title=_("App Password"),
-                size = 40,
-                allow_empty=False,
-                help=_("Specify app password, mandatory, use Personal Settings|Security|Devices and Sessions within the NC UI to create one for the given user"))),
-            ("port", TextAscii(title=_("Port"),
-                allow_empty=True,
-                help=_("Specify port if not listening to HTTP(S), optional"))),
-            ("folder", TextAscii(title=_("Folder"),
-                allow_empty=True,
-                help=_("Specify subfolder if your Nextcloud instance is not installed in the web root, no trailing/leading slashes, optional"))),
-            ("no_https", Checkbox(title=_("Disable HTTPS"),
-                help=_("Activate to disable encryption (not recommended), optional"))),
-            ("no_cert_check", Checkbox(title=_("Disable certificate validation"),
-                help=_("Activate to disable certificate validation (not recommended), optional"))),
-        ],
-        optional_keys=[],
-    )
-
-rulespec_registry.register(
-    HostRulespec(
-        factory_default = _factory_default_special_agent_nextcloud(),
-        group = RulespecGroupDatasourceProgramsApps,
-        name = "special_agents:nextcloud",
-        valuespec = _valuespec_special_agent_nextcloud,
-    )
-)
\ No newline at end of file
diff --git a/local/share/check_mk/web/plugins/wato/nextcloud_users_rules.py b/local/share/check_mk/web/plugins/wato/nextcloud_users_rules.py
deleted file mode 100644
index 9eff766..0000000
--- a/local/share/check_mk/web/plugins/wato/nextcloud_users_rules.py
+++ /dev/null
@@ -1,68 +0,0 @@
-from cmk.gui.i18n import _
-from cmk.gui.plugins.wato import (
-    CheckParameterRulespecWithItem,
-    rulespec_registry,
-    RulespecGroupCheckParametersApplications
-)
-
-from cmk.gui.valuespec import (
-    Dictionary,
-    ListChoice,
-    Tuple,
-    Percentage,
-    Integer,
-    TextAscii,
-    Float,
-)
-
-def _item_spec_nextcloud_users():
-    return TextAscii(
-        title=_("User ID")
-    )
-
-def _parameter_spec_nextcloud_users():
-    return Dictionary(
-        elements=[
-            ("levels_users_quota_used", Tuple(
-                title=_("Nextcloud levels for quota usage of users"),
-                    elements=[
-                        Percentage(
-                            title=_("Warning at"),
-                            default_value=65.0,
-                            unit="%",
-                        ),
-                        Percentage(
-                            title=_("Critical at"),
-                            default_value=85.0,
-                            unit="%",
-                        )
-                    ],
-            )),
-            ("levels_users_free_space", Tuple(
-                title=_("Nextcloud levels for free disk space of users"),
-                    elements=[
-                        Float(
-                            title=_("Warning below"),
-                            default_value=256.0,
-                            unit="MBytes",
-                        ),
-                        Float(
-                            title=_("Critical below"),
-                            default_value=128.0,
-                            unit="MBytes",
-                        )
-                    ],
-            )),
-        ],
-    )
-
-rulespec_registry.register(
-    CheckParameterRulespecWithItem(
-        check_group_name="nextcloud_users",
-        group=RulespecGroupCheckParametersApplications,
-        match_type="dict",
-        item_spec=_item_spec_nextcloud_users,
-        parameter_valuespec=_parameter_spec_nextcloud_users,
-        title=lambda: _("Nextcloud Users"),
-    )
-)
\ No newline at end of file
diff --git a/mkp/.DS_Store b/mkp/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6
GIT binary patch
literal 6148
zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3
zem<@ulZcFPQ@L2!n>{z**<q8>++&mCkOWA81W14cNZ<zv;LbK1Poaz?KmsK2CSc!(
z0ynLxE!0092;Krf2c+FF_Fe*7ECH>lEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ
zLs35+`xjp>T0<F0fCPF1$Cyrb|F7^5{eNG?83~ZUUlGt@xh*qZDeu<Z%US-OSsOPv
j)R!Z4KLME7ReXlK;d!wEw5GODWMKRea10D2@KpjYNUI8I

literal 0
HcmV?d00001

diff --git a/mkp/Nextcloud-3.1.1.mkp b/mkp/Nextcloud-3.1.1.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..b1441ea90fba190bedf088e4087dfc8b1ced5814
GIT binary patch
literal 12512
zcmb7~<9a2GqJwL@Q`<JCwr$())b46Kb*Hv%+qP}nHrM{nC7fTmgFJbY5Jka&Kz`p@
zf&s7nzFjss7ybZV3<FJ185dk4>BkMreJ=Q|v)FSeW0(|b9_6m~=g`a^<>Gk~8ZK8)
zf#2KuAmma(h7E~Ln&pdHq=CaG^;-1qSn#X4kh{JovmgSUo10Hq0`0wn+4Fy1GxuM1
zc3%ECdid0V&w9K<b?(;n|2Euf{AWTHOxgQx>VK2NHTlL0lY7-jbxYtlvg17KLbj4V
zRr_%oNR$N)ybPW{dZ0ZzRThL{>QMB?yBbfDOq|mna@aFnZVkL05yq~!rE>jiK9ptE
zPvtZegu{E#JA%%nrI3CJUjGdnn|5m81SM#)W2(rhb@O<%J4x)GcBr_%6kW5%>deR$
z<jjZcf5&{m0^k-rCE?ejCv`s@Nu`vUX+HFxX&+s8W(hgTqx^E3$qsKEJ`RF}=X9b>
z&C>5+fzJnN#)YK6`z-AN-BY~b*t?Z0WqjGb&r@wA&f<NG3i%p<Ev*6c*MLfUCQt;_
z*TQ8J^7{L2ng0ID5=PD!IJ_VF_xFAu{xpNvsmamzJy4P*SERH{SGdAe2~AFN*WLj!
zW7!|R@CyG6rZ7w8aVuTV319;S0@2fsVDt-f=V9O(=T92ZR$aSKAbV~Dmk~?S{d+X+
z{ny)^7(;|T@)%)Qzbkj(MDRkCtU$Z>CmY^#YsTW$+F|&|KbP2Pkr`BEY#mGxR+3KG
z0Xlry8^ccHz1enT1ixz@$}SQA>xWFt!DlB>{alevV`j_1-yttMKfVa)w^`tVPA$ts
zFVOmrR)jNa_H__3D?)v)_aZlPj>Ghie8dPKYJ-#HTHx$9<&r2&fnjk_Bmf>zBWAok
z>#>1iXg=g;>$s{U4}>`dh+D#v#sL$801U_<fvHOFx&U+@7edke@!~^6kRxekK@UHN
zV9CEx({Kj;HQ4FJwk6-Tdx?JT*kq&JJJBxifHiTH0Oky5XyNggAg;r$SVnMcQ(>hz
z4yb@2ju_9cO5(sgM7dx*lq?H;1rsDRIBi=ne*FN?VZwG1E6B+-h5-=rr7p7@Tq2~;
ztH0o^OY<m_+M<UT_gqjT(E%9YDl=Xnhml|KM{J4d9sQ=Jx`hHhgyOP$Id2OQsuF=W
zcho9_peER%oRLN=a*#d}$7Hmqne>Awb)=;31vT~0)dIYGgp%V3WC;xgBcV_^Y(X10
zqBt4&<j3ub@P7{|(ilc`c>P{>*on|)^!ARz#F2|kAzaffQXs&EOlrf^Or*m9{ky&a
zTgQS2k+Gj>U&s=LI0fc%R+?ks3-W4EhsWoca4GNM<e?%BCwI*n#H;;z{JMtZV%o=1
zX9tlp9$$_bi)fQSt+|z8QE>0WOZ5TkdO8&Nq!CJk`m@3^#;p^pNd5>s4F>mVzpoZ&
zk8x^w6PELah|z)gkRAuohGxzyEBS)V6K~?t@p}=8tL?jNBbDRxa8?)Lsru9N&(vx%
zK{Vj1WDw+xOUdOF^ONq(Iv|OC8~{nHxTRGy>dZ-^%ZeRCEg_BkF*Su)s0ABH>lx2R
z)-fjDG|c*f{^}*AgECFm|M*6xhvp?~h8tcR3(xeY<UK-*6IRlORc#s^1%v4iaU66?
z6d^)Ax&ouiLV+54cm<aXjiUnCZmRDA<)wyMzs*H|RsGJqi4h32OYGz^ZNo%Sq#1SC
zS|Aj11d9tlIhAxLH{Ty4cE?LGnNv5EVS&6=QzGMma7Q6#clxk4U0Rb8U=RaJGi6Fk
zK+*?wdZ)mtDqH*O$CV^jDP{xRe(gmOL>EMt)}J3HV$#dIAMswfzy<@EoD$l2bBQ+`
zsXOfsuy^4S(6EnrSqOqm#?wb#Wr*}XE^enr(FW)M0>Z^FPzFe<<&d=!1WBK7(68UD
zg-`m%ZV+B>!W||JNV7?CQ%1HLqm-ak5mMFMG4RYrS0KN&n7{xh210DAD$#5R77}(`
zR$T!I#sWQG7)KP!h>)z|f?GcFLQWtz;3aJT=&II+>Pzggswkqg7^(vXeo@XqaQMM?
zK_I7{)HY0TIZ+KL^G>?r$~1)HClNW~E}!9^D=@1I9pi%F6H|n`k$yS^aA5i99l@)B
z80n|O5>uPsIVsG!W=72eWEcoMAxeGV#s9ElU)v$Ia7yk*Ko?C9?zS#pI1lf@k)u@g
zrsB6$W=|URTguLMUbEJ>_c2yfr_c&sIHu;ebaP`nAW0tdm-UL4@#iYEx7?C%7cLX_
z7~5`slYIHjo`TYvkB*n=I)U)2!>>SFp}zb`AVF0W`<j?z+HS!79s%|7rM9HYCt2M>
z*D0=lq%M`uRMl>vYLT~T4CYXp4(jLk;Gc6m3jKk^;~$)+%T38|Y39avh;hfT(fzi6
z`<(d3s3Q%8bY28qp3cU3<%e%bT*KmPng0XMhI)h(4uhXY!`rLworUJjpA`0^u2xXj
z9U=|R?Ek{WO#VW16h%CgQAH-lyu<PJrm6`u^LQ-o)e1Gs8P@qJ-P)4ifVzL;0I})Y
zFtJoskk=g7rZCMF;dc1;>|{b$A!c?*FAmYZ2lE8Mq1V^Xtvg%>hB<ZK{cDB!`d2Sy
zGQ5-sZ&#8$VxAF93E@*wM>fOqO!x*W4=8(bP)+dQg@DP3V%?^QRDy3TW{md2z**k*
ztO>ztuBTr+TpYw*-lM^|LOlsCq?LI`VDbjGFwG%Fq#lKN*kDovHB8Y0JQ%0DF$~tE
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

literal 0
HcmV?d00001

diff --git a/nextcloud/agent_based/nextcloud_database.py b/nextcloud/agent_based/nextcloud_database.py
new file mode 100644
index 0000000..db64233
--- /dev/null
+++ b/nextcloud/agent_based/nextcloud_database.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+# pylint: disable=missing-module-docstring, unused-argument, missing-function-docstring
+# pylint: disable=line-too-long
+
+from collections.abc import Mapping
+from typing import NotRequired, TypedDict
+
+from cmk.agent_based.v2 import (
+    AgentSection,
+    CheckPlugin,
+    CheckResult,
+    Metric,
+    render,
+    Result,
+    Service,
+    State,
+    StringTable,
+    DiscoveryResult,
+)
+
+
+class _ItemData(TypedDict):
+    dbtype: NotRequired[str]
+    version: NotRequired[str]
+    size: NotRequired[int]
+    opcache_hit_rate: NotRequired[float]
+
+
+Section = Mapping[str, _ItemData]
+
+
+def get_state_lower(levels, value):
+    warn, crit = levels
+    if value < crit:
+        return State.CRIT
+    if value < warn:
+        return State.WARN
+    return State.OK
+
+
+def discover_nextcloud_database(section) -> DiscoveryResult:
+    yield Service()
+
+
+def check_nextcloud_database(params, section) -> CheckResult:
+    for key in section:
+        if key == "database":
+            opcache_hit_rate = section[key]["opcache_hit_rate"]
+            size = section[key]["size"]
+            dbtype = section[key]["dbtype"]
+            version = section[key]["version"]
+            _level_type, levels = params["levels_database_opcache_hit_rate"]
+            # create graph for opcache hit rate
+            yield Metric(
+                "nc_database_opcache_hit_rate", opcache_hit_rate, levels=levels
+            )
+            # create graph for database size
+            yield Metric("nc_database_size", size)
+            state = get_state_lower(levels, opcache_hit_rate)
+            summary = f"PHP OPCache hit rate: {render.percent(opcache_hit_rate)}"
+            if opcache_hit_rate == 0:
+                state = State.UNKNOWN
+                summary = "PHP OPCache hit rate: 0% - please check if opcache_get_status is enabled"
+            details = f"\nDatabase type: {dbtype}\nDatabase version: {version}\nDatabase size: {render.bytes(size)}"
+            yield Result(state=state, summary=summary, details=details)
+
+
+def parse_nextcloud_database(string_table: StringTable) -> Section:
+    parsed_data = {"database": {}}
+    params_list = [
+        "NC_Database_Type",
+        "NC_Database_Version",
+        "NC_Database_Size",
+        "NC_OPCache_Hit_Rate",
+    ]
+    for line in string_table:
+        if line[0] in params_list:
+            param = line[0]
+            value = line[1]
+            if param == "NC_Database_Type":
+                parsed_data["database"]["dbtype"] = value
+            elif param == "NC_Database_Version":
+                parsed_data["database"]["version"] = value
+            elif param == "NC_Database_Size":
+                parsed_data["database"]["size"] = int(value)
+            elif param == "NC_OPCache_Hit_Rate":
+                parsed_data["database"]["opcache_hit_rate"] = float(value)
+    return parsed_data
+
+
+agent_section_nextcloud_database = AgentSection(
+    name="nextcloud_database",
+    parse_function=parse_nextcloud_database,
+)
+
+check_plugin_nextcloud_database = CheckPlugin(
+    name="nextcloud_database",
+    service_name="Nextcloud Database",
+    discovery_function=discover_nextcloud_database,
+    check_function=check_nextcloud_database,
+    check_default_parameters={
+        "levels_database_opcache_hit_rate": ("fixed", (95.0, 85.0)),
+    },
+    check_ruleset_name="nextcloud_database",
+)
diff --git a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_info.py b/nextcloud/agent_based/nextcloud_info.py
similarity index 75%
rename from local/lib/python3/cmk/base/plugins/agent_based/nextcloud_info.py
rename to nextcloud/agent_based/nextcloud_info.py
index 00848f4..6cd8621 100644
--- a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_info.py
+++ b/nextcloud/agent_based/nextcloud_info.py
@@ -1,10 +1,24 @@
 #!/usr/bin/env python3
-from pprint import pprint
+# pylint: disable=missing-module-docstring, unused-argument, missing-function-docstring
+# pylint: disable=line-too-long, too-many-branches, too-many-locals, too-many-statements
+
 from datetime import datetime
-from .agent_based_api.v1 import register, render, Service, Result, State, Metric
+
+from cmk.agent_based.v2 import (
+    AgentSection,
+    CheckPlugin,
+    CheckResult,
+    Metric,
+    render,
+    Result,
+    Service,
+    State,
+    StringTable,
+    DiscoveryResult,
+)
 
 
-def getStateUpper(levels, value):
+def get_state_upper(levels, value):
     warn, crit = levels
     if value >= crit:
         return State.CRIT
@@ -13,7 +27,7 @@ def getStateUpper(levels, value):
     return State.OK
 
 
-def getStateLower(levels, value):
+def get_state_lower(levels, value):
     warn, crit = levels
     if value < crit:
         return State.CRIT
@@ -22,22 +36,24 @@ def getStateLower(levels, value):
     return State.OK
 
 
-def discover_nextcloud_info(section):
-    yield(Service())
+def discover_nextcloud_info(section) -> DiscoveryResult:
+    yield Service()
 
 
-def check_nextcloud_info(params, section): 
+def check_nextcloud_info(params, section) -> CheckResult:
     for key in section:
         if key == "nextcloud":
-            levels_free_space = params["levels_free_space"]
-            levels_number_of_files = params["levels_number_of_files"]
+            _level_type, levels_free_space = params["levels_free_space"]
+            _level_type, levels_number_of_files = params["levels_number_of_files"]
             # update infos are available only from Nextcloud version 28 onwards
             try:
                 last_update = section[key]["last_update"]
                 update_available = section[key]["update_available"]
                 last_update_human = datetime.fromtimestamp(last_update)
             except KeyError:
-                last_update = "Update information not available, update to at least version 28"
+                last_update = (
+                    "Update information not available, update to at least version 28"
+                )
                 update_available = "False"
                 last_update_human = "Information not available"
             status = section[key]["status"]
@@ -46,14 +62,14 @@ def check_nextcloud_info(params, section):
             php_version = section[key]["php_version"]
             webserver = section[key]["webserver"]
             num_files = section[key]["number_files"]
-            num_shares = section[key]["number_shares"]         
+            num_shares = section[key]["number_shares"]
 
             # create graph for number of files and shares
-            yield(Metric("nc_num_files", num_files))
-            yield(Metric("nc_num_shares", num_shares))
+            yield Metric("nc_num_files", num_files)
+            yield Metric("nc_num_shares", num_shares)
 
             # create overall result
-            summary = f"Status is {status}, Last update: {last_update_human}"
+            summary = f"Status is {status}, Last update check: {last_update_human}"
             details = f"Nextcloud version: {version}\nPHP version: {php_version}\nWebserver: {webserver}\n\nNumber of files: {num_files}\nNumber of shares: {num_shares}\n\nFree space on disk: {render.bytes(free_space)}\n"
             if status == "ok":
                 state = State.OK
@@ -64,31 +80,31 @@ def check_nextcloud_info(params, section):
             # Create result for available updates
             if update_available != "False":
                 state = State.WARN
-                notice = f"Update is available"
+                notice = "Update is available"
             else:
                 state = State.OK
                 notice = "No update available"
             if state != State.OK:
-                yield(Result(state=state, notice=notice))
+                yield Result(state=state, notice=notice)
 
             # Create result for free space on disk
             # Levels for free space are given in GBytes, we have to adjust this here
             warn, crit = levels_free_space
-            warn = warn*1024*1024*1024
-            crit = crit*1024*1024*1024
-            state = getStateLower((warn, crit), free_space)
+            warn = warn * 1024 * 1024 * 1024
+            crit = crit * 1024 * 1024 * 1024
+            state = get_state_lower((warn, crit), free_space)
             # create graph for free space on disk
-            yield(Metric("nc_free_space", free_space, levels=(warn,crit)))
+            yield Metric("nc_free_space", free_space, levels=(warn, crit))
             notice = f"Remaining free space on disk: {render.bytes(free_space)}"
             if state != State.OK:
-                yield(Result(state=state, notice=notice))
+                yield Result(state=state, notice=notice)
 
             # Create result for number of files
             warn, crit = levels_number_of_files
-            state = getStateUpper((warn, crit), num_files)
+            state = get_state_upper((warn, crit), num_files)
             notice = f"Number of files: {num_files}"
             if state != State.OK:
-                yield(Result(state=state, notice=notice))
+                yield Result(state=state, notice=notice)
         elif key == "users":
             num_users = section[key]["number"]
             num_active_last1hour = section[key]["active_last1hour"]
@@ -100,7 +116,7 @@ def check_nextcloud_info(params, section):
             yield Metric("nc_active_users_last_1day", num_active_last1day)
             yield Metric("nc_active_users_last_5min", num_active_last5min)
             notice = f"Number of users: {num_users}\n\nActive users last 5 min: {num_active_last5min}\nActive user since last hour: {num_active_last1hour}\nActive users since last day: {num_active_last1day}"
-            yield(Result(state=State.OK, notice=notice))
+            yield Result(state=State.OK, notice=notice)
         elif key == "storages":
             num_storages = section[key]["number"]
             num_storages_home = section[key]["number_home"]
@@ -112,7 +128,7 @@ def check_nextcloud_info(params, section):
             yield Metric("nc_num_storages_local", num_storages_local)
             yield Metric("nc_num_storages_other", num_storages_other)
             notice = f"Number of storages: {num_storages}\nNumber of home/local/other storages: {num_storages_home}/{num_storages_local}/{num_storages_other}"
-            yield(Result(state=State.OK, notice=notice))
+            yield Result(state=State.OK, notice=notice)
         elif key == "apps":
             # Workaround for Nextcloud 28, "apps" info is not always available
             try:
@@ -123,25 +139,30 @@ def check_nextcloud_info(params, section):
                 else:
                     app_versions = ""
                 # create graphs for number of apps
-                levels = params["levels_apps_with_updates_available"]
+                _level_type, levels = params["levels_apps_with_updates_available"]
                 yield Metric("nc_num_apps_installed", num_apps_installed)
-                yield Metric("nc_apps_with_updates_available", num_apps_with_updates_available, levels=levels)
-                state = getStateUpper(levels, num_apps_with_updates_available)
-                if (app_versions == ""):
+                yield Metric(
+                    "nc_apps_with_updates_available",
+                    num_apps_with_updates_available,
+                    levels=levels,
+                )
+                state = get_state_upper(levels, num_apps_with_updates_available)
+                if app_versions == "":
                     notice = f"Number of installed apps: {num_apps_installed}\nNumber of apps with updates available: {num_apps_with_updates_available}"
                 else:
                     notice = f"Number of installed apps: {num_apps_installed}\nNumber of apps with updates available: {num_apps_with_updates_available}\nNew app versions available: {app_versions}"
-                yield(Result(state=state, notice=notice))
+                yield Result(state=state, notice=notice)
             except KeyError:
+                # TBD
                 pass
 
 
-def parse_nextcloud_info_section(string_table):
+def parse_nextcloud_info(string_table: StringTable) -> dict:
     parsed_data = {
-        "nextcloud" : {},
-        "storages" : {},
-        "apps" : {},
-        "users" : {},
+        "nextcloud": {},
+        "storages": {},
+        "apps": {},
+        "users": {},
     }
     params_list = [
         "NC_Version",
@@ -163,7 +184,7 @@ def parse_nextcloud_info_section(string_table):
         "NC_Apps_With_Updates_Available",
         "NC_Active_Users_Last_5Min",
         "NC_Active_Users_Last_1Hour",
-        "NC_Active_Users_Last_1Day"
+        "NC_Active_Users_Last_1Day",
     ]
     for line in string_table:
         if line[0] in params_list:
@@ -209,25 +230,23 @@ def parse_nextcloud_info_section(string_table):
                 parsed_data["users"]["active_last1hour"] = int(value)
             elif param == "NC_Active_Users_Last_1Day":
                 parsed_data["users"]["active_last1day"] = int(value)
-    #pprint(parsed_data)
     return parsed_data
 
 
-register.agent_section(
+agent_section_nextcloud_info = AgentSection(
     name="nextcloud_info",
-    parse_function=parse_nextcloud_info_section,
+    parse_function=parse_nextcloud_info,
 )
 
-
-register.check_plugin(
+check_plugin_nextcloud_info = CheckPlugin(
     name="nextcloud_info",
     service_name="Nextcloud Info",
     discovery_function=discover_nextcloud_info,
     check_function=check_nextcloud_info,
     check_default_parameters={
-        "levels_apps_with_updates_available": (1, 2),
-        "levels_free_space": (8.0, 4.0),
-        "levels_number_of_files": (100000, 250000),
+        "levels_apps_with_updates_available": ("fixed", (1, 2)),
+        "levels_free_space": ("fixed", (8.0, 4.0)),
+        "levels_number_of_files": ("fixed", (100_000, 250_000)),
     },
     check_ruleset_name="nextcloud_info",
-)
\ No newline at end of file
+)
diff --git a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_users.py b/nextcloud/agent_based/nextcloud_users.py
similarity index 54%
rename from local/lib/python3/cmk/base/plugins/agent_based/nextcloud_users.py
rename to nextcloud/agent_based/nextcloud_users.py
index 223dbd5..5a21c03 100644
--- a/local/lib/python3/cmk/base/plugins/agent_based/nextcloud_users.py
+++ b/nextcloud/agent_based/nextcloud_users.py
@@ -1,9 +1,24 @@
 #!/usr/bin/env python3
-from pprint import pprint
-from time import time
-from .agent_based_api.v1 import register, render, Service, Result, State, Metric
+# pylint: disable=missing-module-docstring, unused-argument, missing-function-docstring
+# pylint: disable=line-too-long, too-many-locals
 
-def getStateUpper(levels, value):
+from time import time
+
+from cmk.agent_based.v2 import (
+    AgentSection,
+    CheckPlugin,
+    CheckResult,
+    Metric,
+    render,
+    Result,
+    Service,
+    State,
+    StringTable,
+    DiscoveryResult,
+)
+
+
+def get_state_upper(levels, value):
     warn, crit = levels
     if value >= crit:
         return State.CRIT
@@ -11,7 +26,8 @@ def getStateUpper(levels, value):
         return State.WARN
     return State.OK
 
-def getStateLower(levels, value):
+
+def get_state_lower(levels, value):
     warn, crit = levels
     if value < crit:
         return State.CRIT
@@ -19,11 +35,13 @@ def getStateLower(levels, value):
         return State.WARN
     return State.OK
 
-def discover_nextcloud_users(section):
-    for key in section:
-        yield(Service(item = key))
 
-def check_nextcloud_users(item, params, section):
+def discover_nextcloud_users(section) -> DiscoveryResult:
+    for key in section:
+        yield Service(item=key)
+
+
+def check_nextcloud_users(item, params, section) -> CheckResult:
     userid = item
     quota_used_percent = section[item][0]
     quota_used_bytes = section[item][1]
@@ -32,30 +50,33 @@ def check_nextcloud_users(item, params, section):
     last_login_human = section[item][4]
     last_login_since = section[item][5]
     free_space = quota_total_bytes - quota_used_bytes
-    #print(free_space)
-    levels_quota_used = params["levels_users_quota_used"]
-    levels_free_space = params["levels_users_free_space"]   
+    # print(free_space)
+    _level_type, levels_quota_used = params["levels_users_quota_used"]
+    _level_type, levels_free_space = params["levels_users_free_space"]
     if last_login_human == "never":
         quota_used_percent = 0
         details = f"User ID is '{userid}', Last login: {last_login_human}"
-        summary = f"Used quota of '{display_name}' can't be calculated yet (never logged in)"
+        summary = (
+            f"Used quota of '{display_name}' can't be calculated yet (never logged in)"
+        )
     else:
         # Levels are given in MBytes, we have to adjust this here
         warn, crit = levels_free_space
-        warn = warn*1024*1024
-        crit = crit*1024*1024
-        state = getStateLower((warn, crit), free_space)
+        warn = warn * 1024 * 1024
+        crit = crit * 1024 * 1024
+        state = get_state_lower((warn, crit), free_space)
         details = f"User ID is '{userid}'\nLast login: {last_login_human} ({last_login_since} ago)\nFree space: {render.bytes(free_space)}"
         summary = f"Used quota of '{display_name}' is {render.percent(quota_used_percent)}, {render.bytes(quota_used_bytes)}/{render.bytes(quota_total_bytes)} used"
         notice = f"Remaining free space: {render.bytes(free_space)}"
         yield Metric("nc_users_free_space", free_space, levels=(warn, crit))
         if state != State.OK:
-            yield(Result(state=state, notice=notice))
+            yield Result(state=state, notice=notice)
     yield Metric("nc_users_quota_used", quota_used_percent, levels=levels_quota_used)
-    state = getStateUpper(levels_quota_used, quota_used_percent)
-    yield(Result(state=state, summary=summary, details=details))
+    state = get_state_upper(levels_quota_used, quota_used_percent)
+    yield Result(state=state, summary=summary, details=details)
 
-def parse_nextcloud_users_section(string_table):
+
+def parse_nextcloud_users(string_table: StringTable) -> dict:
     # Raw output from check:
     # userid;displayname;lastLogin;quota_free;quota_quota;quota_relative;quota_total;quota_used
     # str;str;int(milli seconds since epoch);int(bytes);int(bytes);float(percent);int(bytes);int(bytes)
@@ -63,7 +84,7 @@ def parse_nextcloud_users_section(string_table):
     for line in string_table:
         userid = line[0]
         display_name = line[1]
-        last_login = int(line[2])/1000
+        last_login = int(line[2]) / 1000
         if last_login == 0:
             # user never logged in
             last_login_human = "never"
@@ -74,25 +95,37 @@ def parse_nextcloud_users_section(string_table):
             login_diff = curr_time - last_login
             last_login_human = render.datetime(last_login)
             last_login_since = render.timespan(login_diff)
+        quota_quota = int(line[4])
+        if quota_quota == -3:
+            # TBD, no quota set for user
+            pass
         quota_relative = float(line[5])
         quota_total = float(line[6])
         quota_used = float(line[7])
-        parsed_data[f"{userid}"] = [quota_relative, quota_used, quota_total, display_name, last_login_human, last_login_since]
+        parsed_data[f"{userid}"] = [
+            quota_relative,
+            quota_used,
+            quota_total,
+            display_name,
+            last_login_human,
+            last_login_since,
+        ]
     return parsed_data
-      
-register.agent_section(
-    name = "nextcloud_users",
-    parse_function = parse_nextcloud_users_section,
+
+
+agent_section_nextcloud_users = AgentSection(
+    name="nextcloud_users",
+    parse_function=parse_nextcloud_users,
 )
 
-register.check_plugin(
-    name = "nextcloud_users",
-    service_name = "Nextcloud User %s",
-    discovery_function = discover_nextcloud_users,
-    check_function = check_nextcloud_users,
-    check_default_parameters = {
-        "levels_users_quota_used": (65.0, 85.00),
-        "levels_users_free_space": (256.0, 128.0)
+check_plugin_nextcloud_users = CheckPlugin(
+    name="nextcloud_users",
+    service_name="Nextcloud User %s",
+    discovery_function=discover_nextcloud_users,
+    check_function=check_nextcloud_users,
+    check_default_parameters={
+        "levels_users_quota_used": ("fixed", (65.0, 85.0)),
+        "levels_users_free_space": ("fixed", (256.0, 128.0)),
     },
     check_ruleset_name="nextcloud_users",
 )
diff --git a/nextcloud/checkman/nextcloud_database b/nextcloud/checkman/nextcloud_database
new file mode 100644
index 0000000..5c61c6d
--- /dev/null
+++ b/nextcloud/checkman/nextcloud_database
@@ -0,0 +1,16 @@
+title: Nextcloud: Database information
+agents: linux
+catalog: unsorted
+license: GPL
+distribution: check_mk
+description:
+ Works with Nextcloud version 25/26/27/28/29/30 (use at your own risk with lower versions).
+ Tested only with mariab as underlying database.
+ You have to use a username/app password combination to get access to the Nextcloud API.
+ You can create this app password within the personal settings of an administrative user in Nextcloud.
+ Got to security settings and create a new app password.
+ The user must not be secured with 2FA.
+ Shows several information about a Nextcloud database (type/version/size).
+ The check will raise WARN/CRIT if Database PHP OPcache hit rate is below the configurable levels. 
+inventory:
+ one service is created (with several details)
\ No newline at end of file
diff --git a/local/share/check_mk/checkman/nextcloud_info b/nextcloud/checkman/nextcloud_info
similarity index 60%
rename from local/share/check_mk/checkman/nextcloud_info
rename to nextcloud/checkman/nextcloud_info
index 86f6cbb..3e00f08 100644
--- a/local/share/check_mk/checkman/nextcloud_info
+++ b/nextcloud/checkman/nextcloud_info
@@ -4,15 +4,15 @@ catalog: unsorted
 license: GPL
 distribution: check_mk
 description:
- Works with Nextcloud version 25/26/27 (use at your own risk with lower versions).
- Tested only with mysql/mariab as underlying database.
- You may use a username/password or username/token combination to get access to the Nextcloud API.
- You can create this token within the personal settings of an administrative user in Nextcloud.
- Got to security settings and create a new app password, use this for the token.
+ Works with Nextcloud version 25/26/27/28/29/30 (use at your own risk with lower versions).
+ Tested only with mariab as underlying database.
+ You have to use a username/app password combination to get access to the Nextcloud API.
+ You can create this app password within the personal settings of an administrative user in Nextcloud.
+ Got to security settings and create a new app password.
  The user must not be secured with 2FA.
  Shows several information about a Nextcloud instance, e.g. number of files/storages/(active)users, free space on disk.
  The check will raise CRIT if the Nextcloud instance is not in "ok" state.
- The check will raise WARN if there is an update availbale for Nextcloud.
+ The check will raise WARN if there is an update available for Nextcloud.
  The check will raise WARN/CRIT if free space on disk is below the configurable levels.
  The check will raise WARN/CRIT if the number of installed apps with available updates is above the configurable levels. 
  The check will raise WARN/CRIT if the number of files is above the configurable levels.
diff --git a/local/share/check_mk/checkman/nextcloud_users b/nextcloud/checkman/nextcloud_users
similarity index 59%
rename from local/share/check_mk/checkman/nextcloud_users
rename to nextcloud/checkman/nextcloud_users
index 4345347..be2c4ad 100644
--- a/local/share/check_mk/checkman/nextcloud_users
+++ b/nextcloud/checkman/nextcloud_users
@@ -4,10 +4,11 @@ catalog: unsorted
 license: GPL
 distribution: check_mk
 description:
- Works with Nextcloud version 25/26/27 (use at your own risk with lower versions).
- You may use a username/password or username/token combination to get access to the Nextcloud API.
- You can create this token within the personal settings of an administrative user in Nextcloud.
- Got to security settings and create a new app password, use this for the token.
+ Works with Nextcloud version 25/26/27/28/29/30 (use at your own risk with lower versions).
+ Tested only with mariab as underlying database.
+ You have to use a username/app password combination to get access to the Nextcloud API.
+ You can create this app password within the personal settings of an administrative user in Nextcloud.
+ Got to security settings and create a new app password.
  The user must not be secured with 2FA.
  Shows the usage of storage quota used by each user in percent of the maximum allowed quota.
  Shows the remaining free space for each user based on his/her max quota and used space.
diff --git a/nextcloud/graphing/graph_nextcloud.py b/nextcloud/graphing/graph_nextcloud.py
new file mode 100644
index 0000000..b37b6ff
--- /dev/null
+++ b/nextcloud/graphing/graph_nextcloud.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python3
+"""graphing and metrics definitions"""
+
+from cmk.graphing.v1 import Title
+from cmk.graphing.v1.graphs import Graph, MinimalRange
+from cmk.graphing.v1.metrics import Color, DecimalNotation, SINotation, Metric, Unit
+from cmk.graphing.v1.perfometers import Open, Closed, FocusRange, Perfometer, Stacked
+
+
+metric_nc_num_users = Metric(
+    name="nc_num_users",
+    title=Title("Number of Users"),
+    unit=Unit(DecimalNotation("")),
+    color=Color.PURPLE,
+)
+
+metric_nc_num_shares = Metric(
+    name="nc_num_shares",
+    title=Title("Number of Shares"),
+    unit=Unit(DecimalNotation("")),
+    color=Color.BROWN,
+)
+
+metric_nc_num_storages = Metric(
+    name="nc_num_storages",
+    title=Title("Number of Storages"),
+    unit=Unit(DecimalNotation("")),
+    color=Color.BLUE,
+)
+
+metric_nc_num_storages_home = Metric(
+    name="nc_num_storages_home",
+    title=Title("Number of Home Storages"),
+    unit=Unit(DecimalNotation("")),
+    color=Color.GREEN,
+)
+
+metric_nc_num_storages_local = Metric(
+    name="nc_num_storages_local",
+    title=Title("Number of Local Storages"),
+    unit=Unit(DecimalNotation("")),
+    color=Color.DARK_GREEN,
+)
+
+
+metric_nc_num_storages_other = Metric(
+    name="nc_num_storages_other",
+    title=Title("Number of Other Storages"),
+    unit=Unit(DecimalNotation("")),
+    color=Color.LIGHT_GREEN,
+)
+
+metric_nc_num_files = Metric(
+    name="nc_num_files",
+    title=Title("Number of Files"),
+    unit=Unit(SINotation("")),
+    color=Color.RED,
+)
+
+metric_nc_num_apps_installed = Metric(
+    name="nc_num_apps_installed",
+    title=Title("Number of installed Apps"),
+    unit=Unit(DecimalNotation("")),
+    color=Color.GREEN,
+)
+
+metric_nc_apps_with_updates_available = Metric(
+    name="nc_apps_with_updates_available",
+    title=Title("Number of installed Apps with Updates Available"),
+    unit=Unit(DecimalNotation("")),
+    color=Color.RED,
+)
+
+metric_nc_active_users_last_5min = Metric(
+    name="nc_active_users_last_5min",
+    title=Title("Number of Active Users in the last 5 minutes"),
+    unit=Unit(DecimalNotation("")),
+    color=Color.LIGHT_RED,
+)
+
+metric_nc_active_users_last_1hour = Metric(
+    name="nc_active_users_last_1hour",
+    title=Title("Number of Active Users in the last 1 hour"),
+    unit=Unit(DecimalNotation("")),
+    color=Color.LIGHT_GREEN,
+)
+
+metric_nc_active_users_last_1day = Metric(
+    name="nc_active_users_last_1day",
+    title=Title("Number of Active Users in the last 1 day"),
+    unit=Unit(DecimalNotation("")),
+    color=Color.LIGHT_BLUE,
+)
+
+metric_nc_users_free_space = Metric(
+    name="nc_users_free_space",
+    title=Title("Free Space of User"),
+    unit=Unit(SINotation("bytes")),
+    color=Color.LIGHT_BLUE,
+)
+
+metric_nc_free_space = Metric(
+    name="nc_free_space",
+    title=Title("Free Space on Disk"),
+    unit=Unit(SINotation("bytes")),
+    color=Color.DARK_BLUE,
+)
+
+metric_nc_database_size = Metric(
+    name="nc_database_size",
+    title=Title("Database Size"),
+    unit=Unit(SINotation("bytes")),
+    color=Color.GREEN,
+)
+
+metric_nc_database_opcache_hit_rate = Metric(
+    name="nc_database_opcache_hit_rate",
+    title=Title("Database PHP OPCache Hit Rate"),
+    unit=Unit(DecimalNotation("%")),
+    color=Color.LIGHT_BLUE,
+)
+
+
+metric_nc_users_quota_used = Metric(
+    name="nc_users_quota_used",
+    title=Title("Quota used"),
+    unit=Unit(DecimalNotation("%")),
+    color=Color.BLUE,
+)
+
+
+graph_nc_number_of_users_shares_storages = Graph(
+    name="number_of_users_shares_storages_combined",
+    title=Title("Number of Users, Shares and Storages"),
+    # names here refer to the metric names above
+    simple_lines=["nc_num_users", "nc_num_shares", "nc_num_storages"],
+    minimal_range=MinimalRange(0, 200),
+)
+
+graph_nc_number_of_storage_types = Graph(
+    name="number_of_storage_types_combined",
+    title=Title("Number of Storage Types"),
+    # names here refer to the metric names above
+    simple_lines=[
+        "nc_num_storages_home",
+        "nc_num_storages_local",
+        "nc_num_storages_other",
+    ],
+    minimal_range=MinimalRange(0, 100),
+)
+
+graph_nc_number_of_active_users = Graph(
+    name="number_of_active_users_combined",
+    title=Title("Number of Active Users"),
+    # names here refer to the metric names above
+    simple_lines=[
+        "nc_active_users_last_5min",
+        "nc_active_users_last_1hour",
+        "nc_active_users_last_1day",
+    ],
+    minimal_range=MinimalRange(0, 100),
+)
+
+
+perfometer_nc_database_op_cache_hit_rate = Perfometer(
+    name="nc_database_opcache_hit_rate",
+    focus_range=FocusRange(
+        Closed(0),
+        Closed(100),
+    ),
+    segments=("nc_database_opcache_hit_rate",),
+)
+
+perfometer_nc_database_size = Perfometer(
+    name="nc_database_size",
+    focus_range=FocusRange(Open(0.0), Open(500.0)),
+    segments=["nc_database_size"],
+)
+
+perfometer_nc_quota_used = Perfometer(
+    name="nc_users_quota_used",
+    focus_range=FocusRange(Closed(0), Closed(100)),
+    segments=["nc_users_quota_used"],
+)
+
+perfometer_nc_info = Stacked(
+    name="nc_info",
+    lower=Perfometer(
+        name="nc_num_users",
+        focus_range=FocusRange(Closed(0), Open(200)),
+        segments=["nc_num_users"],
+    ),
+    upper=Perfometer(
+        name="nc_num_apps_installed",
+        focus_range=FocusRange(Closed(0), Open(100)),
+        segments=["nc_num_apps_installed"],
+    ),
+)
diff --git a/nextcloud/libexec/agent_nextcloud b/nextcloud/libexec/agent_nextcloud
new file mode 100755
index 0000000..a8f2d5c
--- /dev/null
+++ b/nextcloud/libexec/agent_nextcloud
@@ -0,0 +1,312 @@
+#!/usr/bin/env python3
+# pylint: disable=missing-module-docstring, missing-class-docstring, missing-function-docstring
+# pylint: disable=too-many-branches, line-too-long, too-many-statements
+# pylint: disable=too-many-arguments, too-many-positional-arguments, too-many-locals
+
+import json
+import argparse
+import sys
+import requests
+import urllib3
+from requests.structures import CaseInsensitiveDict
+
+NC_API_ENDPOINT = "ocs/v2.php/apps/serverinfo/api/v1/info?format=json"
+NC_API_ENDPOINT_ALL_USERS = "ocs/v1.php/cloud/users?format=json"
+NC_API_ENDPOINT_USER = "ocs/v1.php/cloud/users"
+
+
+def get_args() -> argparse.Namespace:
+    parser: argparse.ArgumentParser = argparse.ArgumentParser(
+        description="Nextcloud server parameters"
+    )
+    parser.add_argument(
+        "--hostname", required=True, type=str, help="Hostname or IP of nextcloud server"
+    )
+    parser.add_argument(
+        "--username", required=True, type=str, help="User with admin privileges"
+    )
+    parser.add_argument(
+        "--password", required=True, type=str, help="App password for the user"
+    )
+    parser.add_argument(
+        "--port",
+        required=False,
+        type=int,
+        help="Port where the server is listening on (if not HTTP/HTTPS)",
+    )
+    parser.add_argument(
+        "--folder",
+        required=False,
+        type=str,
+        help="Folder if not installed in web root directory",
+    )
+    parser.add_argument(
+        "--no-https",
+        required=False,
+        default=False,
+        help="Disable HTTPS",
+        action="store_true",
+    )
+    parser.add_argument(
+        "--no-cert-check",
+        required=False,
+        default=False,
+        help="Disable certificate checks",
+        action="store_true",
+    )
+    args = parser.parse_args()
+    return args
+
+
+def create_url(endpoint, hostname, protocol, port, folder):
+    # these parameters are needed, otherwise no information about updates regarding apps and Nextcloud itself are reported (since version 28)
+    params = "skipApps=false&skipUpdate=false"
+    if folder == "":
+        url = f"{protocol}://{hostname}:{port}/{endpoint}"
+    else:
+        url = f"{protocol}://{hostname}:{port}/{folder}/{endpoint}"
+    if endpoint == NC_API_ENDPOINT:
+        url = f"{url}&{params}"
+    return url
+
+
+def create_url_user(user, endpoint, hostname, protocol, port, folder):
+    params = "format=json"
+    if folder == "":
+        url = f"{protocol}://{hostname}:{port}/{endpoint}/{user}?{params}"
+    else:
+        url = f"{protocol}://{hostname}:{port}/{folder}/{endpoint}/{user}?{params}"
+    return url
+
+
+def get_session(username, secret):
+    session = requests.session()
+    session.cookies.set("SameSite", "Strict")
+    session.auth = (username, secret)
+    session.headers["Accept"] = "application/json"
+    return session
+
+
+def get_data(session, url, verify):
+    data = {}
+    headers = CaseInsensitiveDict()
+    headers["Accept"] = "application/json"
+    response = session.get(url, headers=headers, verify=verify)
+    status = response.status_code
+    if status == 200:
+        jsdata = response.text
+        data = json.loads(jsdata)  # returns  a dictionary
+        return data
+    if status == 503:
+        # this code is reported when maintenance mode is on
+        sys.stderr.write(
+            f"Request response code is {response.status_code} with URL {url}, maybe maintenance mode is on?\n"
+        )
+        sys.exit(1)
+    else:
+        sys.stderr.write(
+            f"Request response code is {response.status_code} with URL {url}\n"
+        )
+        sys.exit(1)
+
+
+def get_data_all_users(session, url, verify):
+    headers = CaseInsensitiveDict()
+    headers["Accept"] = "application/json"
+    headers["OCS-APIRequest"] = "true"
+    response = session.get(url, headers=headers, verify=verify)
+    status = response.status_code
+    if status == 200:
+        jsdata = response.text
+        data = json.loads(jsdata)  # returns  a dictionary
+        return data
+    sys.stderr.write(
+        f"Request response code is {response.status_code} with URL {url}\n"
+    )
+    sys.exit(1)
+
+
+def get_data_user(session, url, verify):
+    headers = CaseInsensitiveDict()
+    headers["Accept"] = "application/json"
+    headers["OCS-APIRequest"] = "true"
+    response = session.get(url, headers=headers, verify=verify)
+    status = response.status_code
+    if status == 200:
+        jsdata = response.text
+        data = json.loads(jsdata)  # returns  a dictionary
+        return data
+
+    sys.stderr.write(
+        f"Request response code is {response.status_code} with URL {url}\n"
+    )
+    sys.exit(1)
+
+
+def do_cmk_output(data):
+    apps_with_updates_available = {}
+    str_apps_with_updates_available = ""
+    print("<<<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()
diff --git a/nextcloud/rulesets/nextcloud_database_rules.py b/nextcloud/rulesets/nextcloud_database_rules.py
new file mode 100644
index 0000000..c57f638
--- /dev/null
+++ b/nextcloud/rulesets/nextcloud_database_rules.py
@@ -0,0 +1,45 @@
+#!/user/bin/env python3
+"""parameter form ruleset for Nextcloud databases"""
+
+from cmk.rulesets.v1 import Title
+from cmk.rulesets.v1.form_specs import (
+    DefaultValue,
+    DictElement,
+    Dictionary,
+    Float,
+    LevelDirection,
+    SimpleLevels,
+)
+from cmk.rulesets.v1.rule_specs import HostCondition, CheckParameters, Topic
+
+
+# function name should begin with an underscore to limit it's visibility
+def _parameter_form():
+    return Dictionary(
+        elements={
+            "levels_database_opcache_hit_rate": DictElement(
+                parameter_form=SimpleLevels(
+                    title=Title("Levels for database PHP op cache hit rate"),
+                    form_spec_template=Float(),
+                    level_direction=LevelDirection.LOWER,
+                    prefill_fixed_levels=DefaultValue(value=(95.0, 85.0)),
+                ),
+                required=True,
+            ),
+        }
+    )
+
+
+# name must begin with "rule_spec_", should refer to the used check plugin
+# must be an instance of "CheckParameters"
+rule_spec_nextcloud_database = CheckParameters(
+    # "name" should be the same as the check plugin
+    name="nextcloud_database",
+    # the title is shown in the GUI
+    title=Title("Levels for database PHP op cache hit rate"),
+    # this ruleset can be found under Setup|Service monitoring rules|Applications...
+    topic=Topic.APPLICATIONS,
+    # define the name of the function which creates the GUI elements
+    parameter_form=_parameter_form,
+    condition=HostCondition(),
+)
diff --git a/nextcloud/rulesets/nextcloud_info_rules.py b/nextcloud/rulesets/nextcloud_info_rules.py
new file mode 100644
index 0000000..cc0a132
--- /dev/null
+++ b/nextcloud/rulesets/nextcloud_info_rules.py
@@ -0,0 +1,66 @@
+#!/user/bin/env python3
+"""parameter form ruleset for Nextcloud systems"""
+
+from cmk.rulesets.v1 import Title
+from cmk.rulesets.v1.form_specs import (
+    DefaultValue,
+    DictElement,
+    Dictionary,
+    Float,
+    LevelDirection,
+    SimpleLevels,
+    Integer,
+)
+from cmk.rulesets.v1.rule_specs import CheckParameters, Topic, HostCondition
+
+
+# function name should begin with an underscore to limit it's visibility
+def _parameter_form():
+    return Dictionary(
+        elements={
+            "levels_apps_with_updates_available": DictElement(
+                parameter_form=SimpleLevels(
+                    title=Title("Number of installed apps with updates available"),
+                    form_spec_template=Integer(),
+                    level_direction=LevelDirection.UPPER,
+                    prefill_fixed_levels=DefaultValue(value=(1, 2)),
+                ),
+                required=True,
+            ),
+            "levels_free_space": DictElement(
+                parameter_form=SimpleLevels(
+                    title=Title("Levels for free disk space overall"),
+                    form_spec_template=Float(unit_symbol="GBytes"),
+                    level_direction=LevelDirection.LOWER,
+                    prefill_fixed_levels=DefaultValue(value=(8.0, 4.0)),
+                ),
+                required=True,
+            ),
+            "levels_number_of_files": DictElement(
+                parameter_form=SimpleLevels(
+                    title=Title("Number of files overall"),
+                    form_spec_template=Integer(),
+                    level_direction=LevelDirection.UPPER,
+                    prefill_fixed_levels=DefaultValue(value=(100_000, 250_000)),
+                ),
+                required=True,
+            ),
+        }
+    )
+
+
+# name must begin with "rule_spec_", should refer to the used check plugin
+# must be an instance of "CheckParameters"
+rule_spec_nextcloud_info = CheckParameters(
+    # "name" should be the same as the check plugin
+    name="nextcloud_info",
+    # the title is shown in the GUI
+    title=Title("Various parameters for Nextcloud systems"),
+    # this ruleset can be found under Setup|Service monitoring rules|Applications...
+    topic=Topic.APPLICATIONS,
+    # define the name of the function which creates the GUI elements
+    parameter_form=_parameter_form,
+    # define the label in the GUI where you can restrict the
+    # settings to one or mor specific storages (item)
+    condition=HostCondition(),
+)
diff --git a/nextcloud/rulesets/nextcloud_params.py b/nextcloud/rulesets/nextcloud_params.py
new file mode 100644
index 0000000..7689c3f
--- /dev/null
+++ b/nextcloud/rulesets/nextcloud_params.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+# pylint: disable=line-too-long, missing-module-docstring
+
+from cmk.rulesets.v1 import Help, Label, Title
+from cmk.rulesets.v1.form_specs import (
+    BooleanChoice,
+    DictElement,
+    Dictionary,
+    migrate_to_password,
+    Password,
+    String,
+    Integer,
+    validators,
+    InputHint,
+)
+from cmk.rulesets.v1.rule_specs import SpecialAgent, Topic
+
+
+def _form_spec_special_agent_nextcloud() -> Dictionary:
+    return Dictionary(
+        title=Title("Nextcloud Server Information"),
+        help_text=Help("Checking Nextcloud servers via API"),
+        elements={
+            "hostname": DictElement(
+                required=True,
+                parameter_form=String(
+                    title=Title("Hostname"),
+                    help_text=Help(
+                        "Hostname of Nextcloud server (bare FQDN or IP), mandatory, eg. nextcloud.yourdomain.tld"
+                    ),
+                    custom_validate=(validators.LengthInRange(min_value=1),),
+                    prefill=InputHint("nextcloud.yourdomain.tld"),
+                ),
+            ),
+            "username": DictElement(
+                required=True,
+                parameter_form=String(
+                    title=Title("Username"),
+                    help_text=Help("Username with administrative rights, mandatory"),
+                    custom_validate=(validators.LengthInRange(min_value=1),),
+                    prefill=InputHint("admin"),
+                ),
+            ),
+            "password": DictElement(
+                required=True,
+                parameter_form=Password(
+                    title=Title("App Password"),
+                    help_text=Help(
+                        "Specify app password, mandatory, use Personal Settings|Security|Devices and Sessions within the NC UI to create one for the given user"
+                    ),
+                    custom_validate=(validators.LengthInRange(min_value=1),),
+                    migrate=migrate_to_password,
+                ),
+            ),
+            "port": DictElement(
+                required=False,
+                parameter_form=Integer(
+                    title=Title("Port"),
+                    help_text=Help(
+                        "Specify port if not listening to HTTP(S), optional"
+                    ),
+                    prefill=InputHint(8080),
+                    custom_validate=(validators.NetworkPort(),),
+                ),
+            ),
+            "folder": DictElement(
+                required=False,
+                parameter_form=String(
+                    title=Title("Folder"),
+                    help_text=Help(
+                        "Specify subfolder if your Nextcloud instance is not installed in the web root, no trailing/leading slashes, optional"
+                    ),
+                    prefill=InputHint("nextcloud"),
+                ),
+            ),
+            "no_https": DictElement(
+                required=False,
+                parameter_form=BooleanChoice(
+                    title=Title("Protocol handling"),
+                    label=Label("Disable HTTPS"),
+                    help_text=Help(
+                        "Activate to disable encryption (not recommended), optional"
+                    ),
+                ),
+            ),
+            "no_cert_check": DictElement(
+                required=False,
+                parameter_form=BooleanChoice(
+                    title=Title("Certificate handling"),
+                    label=Label("Disable certificate validation"),
+                    help_text=Help(
+                        "Activate to disable certificate validation (not recommended), optional"
+                    ),
+                ),
+            ),
+        },
+    )
+
+
+rule_spec_nextcloud = SpecialAgent(
+    name="nextcloud",
+    title=Title("Nextcloud connection parameters"),
+    topic=Topic.APPLICATIONS,
+    parameter_form=_form_spec_special_agent_nextcloud,
+)
diff --git a/nextcloud/rulesets/nextcloud_users_rules.py b/nextcloud/rulesets/nextcloud_users_rules.py
new file mode 100644
index 0000000..711af54
--- /dev/null
+++ b/nextcloud/rulesets/nextcloud_users_rules.py
@@ -0,0 +1,54 @@
+#!/user/bin/env python3
+"""parameter form ruleset for Nextcloud users"""
+
+from cmk.rulesets.v1 import Title
+from cmk.rulesets.v1.form_specs import (
+    DefaultValue,
+    DictElement,
+    Dictionary,
+    Float,
+    LevelDirection,
+    SimpleLevels,
+)
+from cmk.rulesets.v1.rule_specs import HostAndItemCondition, CheckParameters, Topic
+
+
+# function name should begin with an underscore to limit it's visibility
+def _parameter_form():
+    return Dictionary(
+        elements={
+            "levels_users_quota_used": DictElement(
+                parameter_form=SimpleLevels(
+                    title=Title("Levels for quota usage of users"),
+                    form_spec_template=Float(unit_symbol="%"),
+                    level_direction=LevelDirection.UPPER,
+                    prefill_fixed_levels=DefaultValue(value=(65.0, 85.0)),
+                ),
+                required=True,
+            ),
+            "levels_users_free_space": DictElement(
+                parameter_form=SimpleLevels(
+                    title=Title("Levels for free disk space of users"),
+                    form_spec_template=Float(unit_symbol="MBytes"),
+                    level_direction=LevelDirection.LOWER,
+                    prefill_fixed_levels=DefaultValue(value=(256.0, 128.0)),
+                ),
+                required=True,
+            ),
+        }
+    )
+
+
+# name must begin with "rule_spec_", should refer to the used check plugin
+# must be an instance of "CheckParameters"
+rule_spec_nextcloud_users = CheckParameters(
+    # "name" should be the same as the check plugin
+    name="nextcloud_users",
+    # the title is shown in the GUI
+    title=Title("Levels for Nextcloud users"),
+    # this ruleset can be found under Setup|Service monitoring rules|Applications...
+    topic=Topic.APPLICATIONS,
+    # define the name of the function which creates the GUI elements
+    parameter_form=_parameter_form,
+    condition=HostAndItemCondition(item_title=Title("User ID")),
+)
diff --git a/nextcloud/server_side_calls/agent_nextcloud.py b/nextcloud/server_side_calls/agent_nextcloud.py
new file mode 100644
index 0000000..acd3f54
--- /dev/null
+++ b/nextcloud/server_side_calls/agent_nextcloud.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+# pylint: disable=missing-module-docstring, missing-class-docstring, missing-function-docstring
+
+from collections.abc import Iterable
+
+from pydantic import BaseModel
+
+from cmk.server_side_calls.v1 import (
+    HostConfig,
+    Secret,
+    SpecialAgentCommand,
+    SpecialAgentConfig,
+)
+
+
+class NextcloudParams(BaseModel):
+    hostname: str | None = None
+    username: str | None = None
+    password: Secret | None = None
+    port: int | None = None
+    folder: str | None = None
+    no_https: bool = False
+    no_cert_check: bool = False
+
+
+def agent_nextcloud_arguments(
+    params: NextcloudParams, _host_config: HostConfig
+) -> Iterable[SpecialAgentCommand]:
+    # print(f"Params: {params}")
+    command_arguments: list[str | Secret] = []
+    if params.hostname is not None:
+        command_arguments += ["--hostname", params.hostname]
+    if params.username is not None:
+        command_arguments += ["--username", params.username]
+    if params.password is not None:
+        command_arguments += ["--password", params.password.unsafe()]
+    if params.port is not None:
+        command_arguments += ["--port", str(params.port)]
+    if params.folder is not None:
+        command_arguments += ["--folder", params.folder]
+    if params.no_https:
+        command_arguments.append("--no-https")
+    if params.no_cert_check:
+        command_arguments.append("--no-cert-check")
+    # command_arguments.append(host_config.name)
+    # print(f"Command Args: {command_arguments}")
+    yield SpecialAgentCommand(command_arguments=command_arguments)
+
+
+special_agent_nextcloud = SpecialAgentConfig(
+    name="nextcloud",
+    parameter_parser=NextcloudParams.model_validate,
+    commands_function=agent_nextcloud_arguments,
+)