From 66a1fc0a5af660f4a9f77b80a6970c33b01aa8ee Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Sun, 21 Apr 2024 15:44:07 +0200
Subject: [PATCH] update project

---
 README.md                                     |   2 +-
 mkp/unbound-1.2.0-20240421.mkp                | Bin 0 -> 4853 bytes
 source/agent_based/unbound.py                 | 239 ++++++++++++++++++
 source/agents/plugins/unbound                 |   4 +
 source/checkman/.gitkeep                      |   0
 source/checkman/unbound                       |  45 ----
 source/gui/metrics/unbound.py                 | 126 +++++++++
 source/gui/wato/check_parameters/unbound.py   | 165 ++++++++++++
 .../cmk/base/cee/plugins/bakery/unbound.py    |  36 +++
 source/packages/unbound                       |  19 ++
 10 files changed, 590 insertions(+), 46 deletions(-)
 create mode 100644 mkp/unbound-1.2.0-20240421.mkp
 create mode 100644 source/agent_based/unbound.py
 create mode 100755 source/agents/plugins/unbound
 delete mode 100644 source/checkman/.gitkeep
 delete mode 100644 source/checkman/unbound
 create mode 100644 source/gui/metrics/unbound.py
 create mode 100644 source/gui/wato/check_parameters/unbound.py
 create mode 100644 source/lib/python3/cmk/base/cee/plugins/bakery/unbound.py
 create mode 100644 source/packages/unbound

diff --git a/README.md b/README.md
index 9209cf4..dd1b6d1 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[PACKAGE]: ../../raw/master/packagee-0.1.2-20230706.mkp "package-0.1.2-20230706.mkp"
+[PACKAGE]: ../../raw/master/mkp/unbound-1.2.0-20240421.mkp "unbound-1.2.0-20240421.mkp"
 # Title
 
 A short description about the plugin
diff --git a/mkp/unbound-1.2.0-20240421.mkp b/mkp/unbound-1.2.0-20240421.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..9a1d9420f21bc07886feb932228d2bf5e1a62e90
GIT binary patch
literal 4853
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

literal 0
HcmV?d00001

diff --git a/source/agent_based/unbound.py b/source/agent_based/unbound.py
new file mode 100644
index 0000000..4f6e23f
--- /dev/null
+++ b/source/agent_based/unbound.py
@@ -0,0 +1,239 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2022, Jan-Philipp Litza (PLUTEX) <jpl@plutex.de>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# https://nlnetlabs.nl/projects/unbound/about/
+
+# changes by thl-cmk[at]outlook[dot]com
+# 2024-04-21: removed Union -> no need "int | float" should do
+#             added levels_upper_NOERROR to default parameters -> show up in info line
+
+from typing import (
+    Any,
+    Mapping,
+    # Union,
+)
+
+from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
+    CheckResult,
+    DiscoveryResult,
+    StringTable,
+)
+
+from cmk.base.plugins.agent_based.agent_based_api.v1 import (
+    GetRateError,
+    Service,
+    check_levels,
+    get_rate,
+    get_value_store,
+    register,
+    render,
+)
+
+
+# UnboundSection = Mapping[str, Union[int, float, "UnboundSection"]]
+UnboundSection = Mapping[str, int | float]
+
+
+def render_qps(x: float) -> str:
+    return f'{x:.2f}/s'
+
+
+def parse_unbound(string_table: StringTable) -> UnboundSection:
+    section = {}
+    for key, value in string_table:
+        try:
+            section[key] = int(value)
+        except ValueError:
+            section[key] = float(value)
+    return section
+
+
+register.agent_section(
+    name="unbound",
+    parse_function=parse_unbound,
+)
+
+
+def discover_unbound_cache(section: UnboundSection) -> DiscoveryResult:
+    if 'total.num.cachehits' in section and 'total.num.cachemiss' in section:
+        yield Service()
+
+
+def check_unbound_cache(
+    params: Mapping[str, Any],
+    section: UnboundSection,
+) -> CheckResult:
+    cumulative_cache_hits = section.get('total.num.cachehits')
+    cumulative_cache_miss = section.get('total.num.cachemiss')
+    now = section.get('time.now')
+
+    if None in (cumulative_cache_hits, cumulative_cache_miss, now):
+        return
+
+    cache_hits = get_rate(
+        get_value_store(),
+        'unbound_cache_hits',
+        now,
+        cumulative_cache_hits,
+        raise_overflow=True,
+    )
+    cache_miss = get_rate(
+        get_value_store(),
+        'unbound_cache_miss',
+        now,
+        cumulative_cache_miss,
+        raise_overflow=True,
+    )
+    total = cache_hits + cache_miss
+    hit_perc = (cache_hits / float(total)) * 100.0 if total != 0 else 100.0
+
+    yield from check_levels(
+        value=cache_miss,
+        metric_name="cache_misses_rate",
+        levels_upper=params.get("cache_misses"),
+        render_func=render_qps,
+        label='Cache Misses',
+        notice_only=True,
+    )
+
+    yield from check_levels(
+        value=cache_hits,
+        metric_name="cache_hit_rate",
+        render_func=render_qps,
+        label='Cache Hits',
+        notice_only=True,
+    )
+
+    yield from check_levels(
+        value=hit_perc,
+        metric_name="cache_hit_ratio",
+        levels_lower=params.get("cache_hits"),
+        render_func=render.percent,
+        label='Cache Hit Ratio',
+    )
+
+
+register.check_plugin(
+    name="unbound_cache",
+    service_name="Unbound Cache",
+    sections=["unbound"],
+    discovery_function=discover_unbound_cache,
+    check_function=check_unbound_cache,
+    check_default_parameters={},
+    check_ruleset_name="unbound_cache",
+)
+
+
+def discover_unbound_answers(section: UnboundSection) -> DiscoveryResult:
+    if 'time.now' in section and 'num.answer.rcode.SERVFAIL' in section:
+        yield Service()
+
+
+def check_unbound_answers(params: Mapping, section: UnboundSection) -> CheckResult:
+    key_prefix = 'num.answer.rcode.'
+    if 'time.now' not in section:
+        return
+
+    now = section['time.now']
+
+    total = sum(
+        value for key, value in section.items()
+        if key.startswith(key_prefix)
+    )
+
+    for key, value in section.items():
+        if not key.startswith(key_prefix):
+            continue
+        answer = key[len(key_prefix):]
+
+        try:
+            rate = get_rate(
+                get_value_store(),
+                f'unbound_answers_{answer}',
+                now,
+                value,
+                raise_overflow=True,
+            )
+        except GetRateError:
+            pass
+        else:
+            levels_upper = params.get(f'levels_upper_{answer}')
+            if levels_upper is not None and len(levels_upper) == 3:
+                # levels on the ratio of answers
+                levels_upper = (
+                    levels_upper[0] * total,
+                    levels_upper[1] * total,
+                )
+            yield from check_levels(
+                value=rate,
+                levels_upper=levels_upper,
+                metric_name=f'unbound_answers_{answer}',
+                render_func=render_qps,
+                label=answer,
+                notice_only=f'levels_upper_{answer}' not in params,
+            )
+
+
+register.check_plugin(
+    name="unbound_answers",
+    service_name="Unbound Answers",
+    sections=["unbound"],
+    discovery_function=discover_unbound_answers,
+    check_function=check_unbound_answers,
+    check_default_parameters={
+        'levels_upper_NOERROR': (101,101),
+        'levels_upper_SERVFAIL': (10, 100),
+        'levels_upper_REFUSED': (10, 100),
+    },
+    check_ruleset_name="unbound_answers",
+)
+
+
+def discover_unbound_unwanted_replies(section: UnboundSection) -> DiscoveryResult:
+    if 'time.now' in section and 'unwanted.replies' in section:
+        yield Service()
+
+
+def check_unbound_unwanted_replies(section: UnboundSection) -> CheckResult:
+    if 'time.now' not in section or 'unwanted.replies' not in section:
+        return
+
+    rate = get_rate(
+        get_value_store(),
+        'unbound_unwanted_replies',
+        section['time.now'],
+        section['unwanted.replies'],
+        raise_overflow=True,
+    )
+
+    yield from check_levels(
+        value=rate,
+        levels_upper=(10, 100),
+        metric_name='unbound_unwanted_replies',
+        render_func=render_qps,
+        label='Unwanted Replies',
+    )
+
+
+register.check_plugin(
+    name="unbound_unwanted_replies",
+    service_name="Unbound Unwanted Replies",
+    sections=["unbound"],
+    discovery_function=discover_unbound_unwanted_replies,
+    check_function=check_unbound_unwanted_replies,
+)
diff --git a/source/agents/plugins/unbound b/source/agents/plugins/unbound
new file mode 100755
index 0000000..52d336e
--- /dev/null
+++ b/source/agents/plugins/unbound
@@ -0,0 +1,4 @@
+#!/bin/sh
+echo '<<<unbound:sep(61)>>>'
+unbound-control stats_noreset
+echo '<<<>>>'
diff --git a/source/checkman/.gitkeep b/source/checkman/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/source/checkman/unbound b/source/checkman/unbound
deleted file mode 100644
index 08ef898..0000000
--- a/source/checkman/unbound
+++ /dev/null
@@ -1,45 +0,0 @@
-title: Dummy check man page - used as template for new check manuals
-agents: linux, windows, aix, solaris, hpux, vms, freebsd, snmp
-catalog: see modules/catalog.py for possible values
-license: GPL
-distribution: check_mk
-description:
- Describe here: (1) what the check actually does, (2) under which
- circumstances it goes warning/critical, (3) which devices are supported
- by the check, (4) if the check requires a separated plugin or
- tool or separate configuration on the target host.
-
-item:
- Describe the syntax and meaning of the check's item here. Provide all
- information one needs if coding a manual check with {checks +=} in {main.mk}.
- Give an example.  If the check uses {None} as sole item,
- then leave out this section.
-
-examples:
- # Give examples for configuration in {main.mk} here. If the check has
- # configuration variable, then give example for them here.
-
- # set default levels to 40 and 60 percent:
- foo_default_values = (40, 60)
-
- # another configuration variable here:
- inventory_foo_filter = [ "superfoo", "superfoo2" ]
-
-perfdata:
- Describe precisely the number and meaning of performance variables
- the check sends. If it outputs no performance data, then leave out this
- section.
-
-inventory:
- Describe how the inventory for the check works. Which items
- will it find? Describe the influence of check specific
- configuration parameters to the inventory.
-
-[parameters]
-foofirst(int): describe the first parameter here (if parameters are grouped
-        as tuple)
-fooother(string): describe another parameter here.
-
-[configuration]
-foo_default_levels(int, int): Describe global configuration variable of
-    foo here. Important: also tell the user how they are preset.
diff --git a/source/gui/metrics/unbound.py b/source/gui/metrics/unbound.py
new file mode 100644
index 0000000..deec35d
--- /dev/null
+++ b/source/gui/metrics/unbound.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+# -*- encoding: utf-8; py-indent-offset: 4 -*-
+
+# Copyright (C) 2022, Jan-Philipp Litza (PLUTEX) <jpl@plutex.de>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# modifications by thl-cmk[at]outlook[dot]com
+# 2024-02-21: changed import path form mk.gui.plugins.metrics to mk.gui.plugins.metrics.utils
+#             added metrics/graph for unbound_unwanted_replies
+#             moved to ~/local/lib/check_mk/gui/plugins/metrics (from local/share/check_mk/web/plugins/metrics)
+#             added perfometer for unbound_answers (NOERROR/SERVFAIL) and unbound_unwanted_replies
+
+from cmk.gui.i18n import _
+
+from cmk.gui.plugins.metrics.utils import (
+    metric_info,
+    graph_info,
+    perfometer_info,
+)
+
+metric_info['unbound_answers_NOERROR'] = {
+    'title': _('Rate of NOERROR answers'),
+    'unit': '1/s',
+    'color': '31/a',
+}
+
+metric_info['unbound_answers_FORMERR'] = {
+    'title': _('Rate of FORMERR answers'),
+    'unit': '1/s',
+    'color': '21/a',
+}
+
+metric_info['unbound_answers_SERVFAIL'] = {
+    'title': _('Rate of SERVFAIL answers'),
+    'unit': '1/s',
+    'color': '11/a',
+}
+
+metric_info['unbound_answers_NXDOMAIN'] = {
+    'title': _('Rate of NXDOMAIN answers'),
+    'unit': '1/s',
+    'color': '51/a',
+}
+
+metric_info['unbound_answers_NOTIMPL'] = {
+    'title': _('Rate of NOTIMPL answers'),
+    'unit': '1/s',
+    'color': '41/a',
+}
+
+metric_info['unbound_answers_REFUSED'] = {
+    'title': _('Rate of REFUSED answers'),
+    'unit': '1/s',
+    'color': '26/a',
+}
+
+metric_info['unbound_answers_nodata'] = {
+    'title': _('Rate of answers without data'),
+    'unit': '1/s',
+    'color': '52/a',
+}
+
+graph_info['unbound_answers'] = {
+    'title': _('Rate of answers'),
+    'metrics': [
+        (f'unbound_answers_{answer}', 'line')
+        for answer in ('NOERROR', 'FORMERR', 'SERVFAIL', 'NXDOMAIN', 'NOTIMPL', 'REFUSED', 'nodata')
+    ],
+}
+
+perfometer_info.append(('stacked', [
+    {
+        'type': 'logarithmic',
+        'metric': 'unbound_answers_NOERROR',
+        'half_value': 100.0,  # ome year
+        'exponent': 2,
+    },
+    {
+        'type': 'logarithmic',
+        'metric': 'unbound_answers_SERVFAIL',
+        'half_value': 50.0,
+        'exponent': 2,
+    },
+]))
+
+metric_info['cache_hit_rate'] = {
+    'title': _('Cache hits per second'),
+    'unit': '1/s',
+    'color': '26/a',
+}
+
+graph_info['cache_hit_misses'] = {
+    'title': _('Cache Hits and Misses'),
+    'metrics': [('cache_hit_rate', 'line'), ('cache_misses_rate', 'line')],
+}
+
+metric_info['unbound_unwanted_replies'] = {
+    'title': _('Unwanted replies'),
+    'unit': '1/s',
+    'color': '26/a',
+}
+
+graph_info['unbound_unwanted_replies'] = {
+    'title': _('Unwanted replies'),
+    'metrics': [('unbound_unwanted_replies', 'area')],
+}
+
+perfometer_info.append({
+    'type': 'logarithmic',
+    'metric': 'unbound_unwanted_replies',
+    'half_value': 100.0,  # ome year
+    'exponent': 2,
+})
diff --git a/source/gui/wato/check_parameters/unbound.py b/source/gui/wato/check_parameters/unbound.py
new file mode 100644
index 0000000..5e52a07
--- /dev/null
+++ b/source/gui/wato/check_parameters/unbound.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2022, Jan-Philipp Litza (PLUTEX) <jpl@plutex.de>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# modifications by thl-cmk[at]outlook[dot]com
+# 2024-04-21: fixed missing FixedValue in import
+#             changed Alternative titles to "Upper levels in qps"/"Upper levels in %" to better differentiate between them
+#             added explicit unit "%2
+#             added ruleset for bakery
+#             renamed to unbound.py (from unbound_parameters.py)
+#             moved to ~/local/lib/check_mk/gui/plugins/wato/check_parameters (from local/share/check_mk/web/plugins/wato)
+#
+
+from cmk.gui.i18n import _
+from cmk.gui.plugins.wato.utils import (
+    CheckParameterRulespecWithoutItem,
+    rulespec_registry,
+    RulespecGroupCheckParametersApplications,
+    HostRulespec,
+)
+from cmk.gui.cee.plugins.wato.agent_bakery.rulespecs.utils import (
+    RulespecGroupMonitoringAgentsAgentPlugins,
+)
+
+from cmk.gui.valuespec import Dictionary, Float, Percentage, Tuple, Alternative, FixedValue
+
+
+def _parameter_valuespec_unbound_cache():
+    return Dictionary(
+        title=_("Unbound: Cache"),
+        elements=[
+            (
+                "cache_misses",
+                Tuple(
+                    title="Levels on cache misses per second",
+                    elements=[
+                        Float(
+                            title="warn",
+                        ),
+                        Float(
+                            title="crit",
+                        ),
+                    ],
+                ),
+            ),
+            (
+                "cache_hits",
+                Tuple(
+                    title="Lower levels for hits in %",
+                    elements=[
+                        Percentage(
+                            title="warn",
+                        ),
+                        Percentage(
+                            title="crit",
+                        ),
+                    ],
+                ),
+            ),
+        ],
+    )
+
+
+rulespec_registry.register(
+    CheckParameterRulespecWithoutItem(
+        check_group_name="unbound_cache",
+        group=RulespecGroupCheckParametersApplications,
+        match_type="dict",
+        parameter_valuespec=_parameter_valuespec_unbound_cache,
+        title=lambda: _("Unbound Cache"),
+    )
+)
+
+
+def _parameter_valuespec_unbound_answers():
+    return Dictionary(
+        elements=[
+            (
+                f"levels_upper_{answer}",
+                Alternative(
+                    title=f'Upper levels for {answer} answers',
+                    show_alternative_title=True,
+                    elements=[
+                        Tuple(
+                            elements=[
+                                Float(title=_("Warning at"), unit=_("qps")),
+                                Float(title=_("Critical at"), unit=_("qps")),
+                            ],
+                            title=f'Upper levels in qps',
+                        ),
+                        Tuple(
+                            elements=[
+                                Percentage(title=_("Warning at"), unit=_("%")),
+                                Percentage(title=_("Critical at"), unit=_("%")),
+                                FixedValue(value="%", totext=""),  # need to decide between both variants
+                            ],
+                            title=f'Upper levels in %',
+                        ),
+                    ]
+                )
+            )
+            for answer in (
+                'NOERROR',
+                'FORMERR',
+                'SERVFAIL',
+                'NXDOMAIN',
+                'NOTIMPL',
+                'REFUSED',
+                'nodata',
+            )
+        ],
+    )
+
+
+rulespec_registry.register(
+    CheckParameterRulespecWithoutItem(
+        check_group_name="unbound_answers",
+        group=RulespecGroupCheckParametersApplications,
+        match_type="dict",
+        parameter_valuespec=_parameter_valuespec_unbound_answers,
+        title=lambda: _("Unbound Answers"),
+    )
+)
+
+
+def _parameter_valuespec_unbound_bakery():
+    return Alternative(
+        title=_('unbound'),
+        elements=[
+            FixedValue(
+                True,
+                title=_('Deploy the unbound agent plugin'),
+                totext=_('The unbound agent plugin will be deployed')
+            ),
+            FixedValue(
+                None,
+                title=_('Do not deploy the unbound agent plugin'),
+                totext=_('The unbound agent plugin will not be deployed')
+            ),
+        ],
+    )
+
+
+rulespec_registry.register(
+    HostRulespec(
+        group=RulespecGroupMonitoringAgentsAgentPlugins,
+        name='agent_config:unbound',
+        valuespec=_parameter_valuespec_unbound_bakery,
+    )
+)
diff --git a/source/lib/python3/cmk/base/cee/plugins/bakery/unbound.py b/source/lib/python3/cmk/base/cee/plugins/bakery/unbound.py
new file mode 100644
index 0000000..ac4f50d
--- /dev/null
+++ b/source/lib/python3/cmk/base/cee/plugins/bakery/unbound.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2024-04-21
+# File  : share/check_mk/agents/unbound.py
+#
+# bakery unbound plugin
+#
+
+
+from pathlib import Path
+
+from cmk.base.cee.plugins.bakery.bakery_api.v1 import (
+    FileGenerator,
+    OS,
+    Plugin,
+    register
+)
+
+
+def get_unbound_files(conf) -> FileGenerator:
+    if conf is True:
+        yield Plugin(
+            base_os=OS.LINUX,
+            source=Path('unbound'),
+        )
+
+
+register.bakery_plugin(
+    name='unbound',
+    files_function=get_unbound_files,
+)
diff --git a/source/packages/unbound b/source/packages/unbound
new file mode 100644
index 0000000..5741833
--- /dev/null
+++ b/source/packages/unbound
@@ -0,0 +1,19 @@
+{'author': 'Jan-Philipp Litza <jpl@plutex.de>',
+ 'description': 'Plugin to gather statistics from unbound caching DNS resolver '
+                'via agent plugin. It monitors answer types, cache hit ratio '
+                'and miss rate as well as unwanted reply rate.\n'
+                '\n'
+                'needs in server config:\n'
+                '  server:\n'
+                '      extended-statistics: yes\n',
+ 'download_url': 'https://github.com/PLUTEX/checkmk-unbound/',
+ 'files': {'agent_based': ['unbound.py'],
+           'agents': ['plugins/unbound'],
+           'gui': ['metrics/unbound.py', 'wato/check_parameters/unbound.py'],
+           'lib': ['python3/cmk/base/cee/plugins/bakery/unbound.py']},
+ 'name': 'unbound',
+ 'title': 'Unbound',
+ 'version': '1.2.0-20240421',
+ 'version.min_required': '2.2.0b1',
+ 'version.packaged': '2.2.0p24',
+ 'version.usable_until': None}
-- 
GitLab