diff --git a/README.md b/README.md index 9209cf4aca73434dbe4fed878d996a1102c59651..dd1b6d18ae9595c094a61e90a42429626afc6663 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 Binary files /dev/null and b/mkp/unbound-1.2.0-20240421.mkp differ diff --git a/source/agent_based/unbound.py b/source/agent_based/unbound.py new file mode 100644 index 0000000000000000000000000000000000000000..4f6e23fc9d5008a30a2d0c9031609a2f2699e3f1 --- /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 0000000000000000000000000000000000000000..52d336ec0382ce2508c119ccc05a8fc6fee50bdc --- /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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/source/checkman/unbound b/source/checkman/unbound deleted file mode 100644 index 08ef898bcf6eb7a7979de60090d54f9719bbbc5b..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..deec35d470fd1f93698eac4e257808afdf9fe597 --- /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 0000000000000000000000000000000000000000..5e52a07f2063f156e8af1d1d31604ab6ca7286fb --- /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 0000000000000000000000000000000000000000..ac4f50de0799d0302c5b5616f4181436bceec7f4 --- /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 0000000000000000000000000000000000000000..5741833e738d9d625c6d90f3deebeda552341e49 --- /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}