Table of Content

No word about CheckMK's supplied documentation :expressionless:

In this example we add some features for the included SNMP-check kemp_loadmaster_services. For whatever reason, the included check only has hardcoded thresholds, and only takes "Active Connections" into account. Besides it is not yet migrated to CMK v2.0 which we will do in this course.

Plugin Structure

SNMP-plugins only need about three files

└── local
    ├── bin
    ├── lib
    │   ├── check_mk -> python3/cmk
    │   └── python3
    │       └── cmk
    │           └── base
    │               └── plugins
    │                   └── agent_based
    │                       └── kemp_loadmaster_services_extended.py           # Check-File: file where the output of the agent is analyzed
    └── share
        └── check_mk
            ├── checkman
            │   └── kemp_loadmaster_services_extended                          # Checkman-File: file with info about the check
            └── web
                └── plugins
                    └── wato
                        └── kemp_loadmaster_services_extended_parameters.py    # Check-Parameters-UI-File: file which create the WATO-UI page for configuring the parameters of the check

You may notice that the new check-plugin is placed in the agent_based-folder - yes, looks like migrated/new plugins should be placed in there. Looking at ~/lib/check_mk/base/plugins/agent_based/ you'll see that only a handful of SNMP-checks and normal agent-plugin checks are in there.

For the plugin we create here, I orientated on the netscaler_vserver.py plugin.

Check-File

The check-file got a little bit more complicated in my eyes. Parsing the input uses similar functions like the netscaler_vserver.py - a in my eyes "helper"-function to create some dictionary. In the old CMK v1.6 this was a little bit easier achieved - but well, here we go with the new way.

In older CMK-versions it was possible to override included checks by only copying the built-in one and adding additional stuff. CMKv2 seems to complain about that - not sure if it's only because of "overriding" a legacy-check.

#!/usr/bin/env python
# -*- encoding: utf-8; py-indent-offset: 4 -*-

# 2021-10-11 c.steinkogler[at]cashpoint.com
#
# Check_MK kemp_loadmaster_services_extended
# an extended SNMP CheckPlugin with parts reused from the included check
#
# This 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 in version 2.  This file is distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# ails.  You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.

# I use pycharm so that I can use the "Go To" -> "Declaration or Usages"
# Not sure if it's smart to keep those in productive checks else comment
# those if they create problems
import time
from typing import TypedDict, Mapping, List, Iterable, Tuple
from cmk.base.plugins.agent_based.agent_based_api.v1 import *
from cmk.base.item_state import set_item_state, get_item_state

# As documentation about SNMP-checks is scarce I orientated at the code from
# ~/lib/check_mk/base/plugins/agent_based/netscaler_vserver.py which gets
# the normal "string_table" and creates a dictionary from it

# It seems we cannot "overload" the included legacy kemp_loadmaster_service
# check - so we have to disable the check via rule and create this "extended" one

map_states = {
    1: (0, 'in service'),
    2: (2, 'out of service'),
    3: (2, 'failed'),
    4: (0, 'disabled'),
    5: (1, 'sorry'),
    6: (0, 'redirect'),
    7: (2, 'error message'),
}

map_summary_states = {
    0: "(.)",
    1: "(!)",
    2: "(!!)",
    3: "(?)",
}

class KempService(TypedDict, total=False):
    service_name: str
    device_state: int
    conns: int
    active_conns: int
# endclass

Section = Mapping[str, KempService]

# a helper function for parsing the input
def _to_kemp_service_extended(line: Iterable[str]) -> Tuple[str, KempService]:
    # >>> import pprint
    # >>> pprint.pprint(_to_kemp_service_extended(['someservice_http', '1', '38885', '0']))
    # ('someservice_http',
    #  {'device_state': '1',
    #   'conns': 38885,
    #   'active_conns': '0'})

    (service_name, device_state, conns, active_conns) = line
    kempservice: KempService = {
        'device_state': int(device_state),
        'conns': int(conns),
        'active_conns': int(active_conns)
    }

    return service_name or service_name, kempservice
# enddef

def parse_kemp_loadmaster_services_extended(string_table: List[type_defs.StringTable]) -> Section:
    """
    The normal "string_table" looks like:
    [['a_dummy', '1', '420', '0'],
     ['b_dummy', '4', '0', '0']]

    with the parse-function it should create:
    {'a_dummy': {'active_conns': 0, 'conns': 0, 'device_state': '4'},
     'b_dummy': {'active_conns': 0, 'conns': 14, 'device_state': '1'}}
    """

    return dict(_to_kemp_service_extended(line) for line in string_table[0])
# enddef

register.snmp_section(
    name="snmp_kemp_loadmaster_services_extended_info",
    parse_function=parse_kemp_loadmaster_services_extended,
    fetch=[
        SNMPTree(
            base=".1.3.6.1.4.1.12196.13.1.1",
            oids=[
                "13",  # B100-MIB::vSname
                "14",  # B100-MIB::vSstate
                "16",  # B100-MIB::vSConns - Total number of connections in the lifetime of Virtual Services
                "21"  # B100-MIB::vSactive_conns - Total number of current active connections of Virtual Services
            ],
        ),
    ],
    detect=exists(".1.3.6.1.2.1.1.2.0"),
)

def discover_kemp_loadmaster_services_extended(section: Section) -> type_defs.DiscoveryResult:
    for service_name in section:
        if section[service_name]['device_state'] not in [4, ""]:
            yield Service(item=service_name)
        # endif
    # endfor
# enddef

def check_kemp_loadmaster_services_extended(item, params, section: Section):
    warn_active_conns, crit_active_conns = params['active_conns']
    warn_connspersec, crit_connspersec = params['connspersec']
    summary = ""

    if item in section:
        data = section[item]
        dev_state = data['device_state']
        # we read the state from the mapping table - if there is an completely unknown one, default to 3 and return the state-number received
        state, state_readable = map_states.get(dev_state, (3, 'unknown[%s]' % dev_state))
        summary += "Status: %s %s" % (state_readable, map_summary_states.get(int(state)))

        active_conns_state = State.OK
        active_conns = data['active_conns']

        if active_conns is not None:
            if active_conns > warn_active_conns and active_conns >= crit_active_conns:
                active_conns_state = State.worst(State.CRIT, active_conns_state)
            elif warn_active_conns <= active_conns < crit_active_conns:
                active_conns_state = State.worst(State.WARN, active_conns_state)
            else:
                active_conns_state = State.worst(State.OK, active_conns_state)
            # endif

            # not completely sure why we have to explicitly tell with `int(state)` to resolve e.g. State.OK to the corresponding number value
            summary += ", Active connections approximately: %s %s" % (active_conns, map_summary_states.get(int(active_conns_state)))
            yield Metric('active_conns', active_conns, levels=(warn_active_conns, crit_active_conns))
        # endif

        conns_state = State.OK
        conns = data['conns']
        # conns value is a counter-value which only gets incremented
        if conns is not None:
            # set_item_state & get_item_state: writes and saves values into some file. We have to do that to later calculate
            # a rough connections/sec value, where conns is showing us a estimation about how many new connections/sec are created
            kemp_loadmaster_service_total_conns = get_item_state('kemp_loadmaster_service_total_conns')
            kemp_loadmaster_service_total_conns_timestamp = get_item_state('kemp_loadmaster_service_total_conns_timestamp')

            if kemp_loadmaster_service_total_conns is None:
                epoch_timestamp_now = int(time.time())
                set_item_state('kemp_loadmaster_service_total_conns', conns)
                set_item_state('kemp_loadmaster_service_total_conns_timestamp', epoch_timestamp_now)
                conns_state = State.worst(State.UNKNOWN, state)
                summary += ", Connections approximately per second: %s %s" % (0, map_summary_states.get(int(conns_state)))
                yield Metric('connspersec', 0, levels=(warn_active_conns, crit_active_conns))
            else:
                epoch_timestamp_now = int(time.time())
                set_item_state('kemp_loadmaster_service_total_conns', conns)
                set_item_state('kemp_loadmaster_service_total_conns_timestamp', epoch_timestamp_now)
                if kemp_loadmaster_service_total_conns <= conns:
                    time_diff = epoch_timestamp_now - kemp_loadmaster_service_total_conns_timestamp
                    conns_diff = conns - kemp_loadmaster_service_total_conns
                    conns_per_second = int(conns_diff / time_diff)

                    if conns_per_second > warn_connspersec and conns_per_second >= crit_connspersec:
                        conns_state = State.worst(State.CRIT, conns_state)
                    elif warn_connspersec <= conns_per_second < crit_connspersec:
                        conns_state = State.worst(State.WARN, conns_state)
                    else:
                        conns_state = State.worst(State.OK, conns_state)
                    # endif

                    summary += ", Connections approximately per second: %s %s" % (conns_per_second, map_summary_states.get(int(conns_state)))
                    yield Metric('connspersec', conns_per_second, levels=(warn_active_conns, crit_active_conns))
                else:
                    conns_state = State.worst(State.UNKNOWN, conns_state)
                    summary += ", Connections approximately per second: %s %s" % (0, map_summary_states.get(int(conns_state)))
                    yield Metric('connspersec', 0, levels=(warn_active_conns, crit_active_conns))
                # endif
            # endif
        # endif

        # we get the worst state from all
        state = State.worst(state, active_conns_state, conns_state)
        yield Result(state=state, summary=summary)
    # endif
# enddef

register.check_plugin(
    name="kemp_loadmaster_services_extended",
    sections=["snmp_kemp_loadmaster_services_extended_info"],
    service_name="Service %s",
    discovery_function=discover_kemp_loadmaster_services_extended,
    check_function=check_kemp_loadmaster_services_extended,
    check_ruleset_name="kemp_loadmaster_services_extended",
    check_default_parameters={
        'active_conns': (2200, 3000),
        'connspersec': (300, 500),
    },
)

The def check_kemp_loadmaster_services_extended didn't change a lot in comparison to the old-version for CMK v1.6. Only the way how the result and metrics are yielded changed. In earlier versions it virtually was possible in on line - now you need two it seems.

Check-Parameters-UI-File

The parameters-file also doesn't look "a lot" different. Only a few lines changed/were added.

#!/usr/env/bin python
# -*- encoding: utf-8; py-indent-offset: 4 -*-

#
# 2021-10-11 c.steinkogler[at]cashpoint.com
#
# Check_MK kemp_loadmaster_services_extended
# an extended SNMP CheckPlugin with parts reused from the included check
#
# 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 3 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, see <http://www.gnu.org/licenses/>.

# As documentation about these parameter-files is scarce I orientated on the code from
# ~/lib/python3/cmk/gui/plugins/wato/check_parameters/netscaler_vserver.py

# for autotranslating stuff it seems
from cmk.gui.i18n import _
# we need Dictionary, Tuple and Integer as we use those later
from cmk.gui.valuespec import (
    Dictionary,
    Tuple,
    Integer,
    TextAscii,
)

from cmk.gui.plugins.wato import (
    CheckParameterRulespecWithItem,
    rulespec_registry,
    RulespecGroupCheckParametersOperatingSystem,
)

def _parameter_valuespec_kemp_loadmaster_services_extended():
    return Dictionary(
        elements=[
            ("active_conns",
             Tuple(
                 title=_("Levels for Active Connections"),
                 help=_("The check detects the active connections"),
                 elements=[
                     Integer(title=_("Warning at"), default_value=1500),
                     Integer(title=_("Critical at"), default_value=2000),
                 ]
             ),
             ),
            ("connspersec",
             Tuple(
                 title=_("Levels for Connections per second"),
                 help=_("The check detects Connections per second"),
                 elements=[
                     Integer(title=_("Warning at"), default_value=1500),
                     Integer(title=_("Critical at"), default_value=2000),
                 ]
             ),
             )
        ]
    )
# enddef

rulespec_registry.register(
    CheckParameterRulespecWithItem(
        check_group_name="kemp_loadmaster_services_extended",
        group=RulespecGroupCheckParametersOperatingSystem,
        item_spec=lambda: TextAscii(title=_("Name of Service")),
        match_type="dict",
        parameter_valuespec=_parameter_valuespec_kemp_loadmaster_services_extended,
        title=lambda: _("Kemp Service Connection Thresholds"),
    )
)

As mentioned in the code - if you want to have a look at the built-in parameter-files, you have to go to ~/lib/python3/cmk/gui/plugins/wato/check_parameters/

Checkman-File

And again the least important file, the checkman file.

You can find the built-in checkman-files in ./share/check_mk/checkman/

title: Kemp Loadmaster: Active Connections (extended)
agents: snmp
catalog: hw/network/kemp
license: GPL
distribution: check_mk
description:
 This check monitors the state and number of active connections of the services (VS)
 running on a Kemp Loadmaster. It is known to run on Kemp Loadmaster LM-2600 and LM-3600.
 It will return OK if the service is in state 'in Service' or 'redirect', WARN if the
 service is in state 'sorry', CRIT if in state 'out of Service',
 'failed' or 'errormsg', 'disabled' services will not be inventorized or if later disabled
 they will be shown as OK - Otherwise it will result in UNKNOWN.

 Additionally thresholds can be set for current active and "new" connections.

inventory:
 One service is created for each VS.

item:
 The name of the VS as found in the oid .1.3.6.1.4.1.12196.13.1.1.13
Zuletzt bearbeitet: Oktober 31, 2021

Autor

Kommentare

Kommentar verfassen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.