Developing my first SNMP plugin

I’m trying to make my first plugin work. It’s just a basic SNMP plugin. I have the following SNMP output:

1.3.6.1.4.1.39165.1.1.0 = "TV-IP1319PI" [ASN_OCTET_STR]
1.3.6.1.4.1.39165.1.2.0 = "0" [ASN_OCTET_STR]

I’m trying to create 2 services named “SNMP deviceType” and “SNMP memSize”. Here is what I currently have based on a lot of reading through the doc and other similar plugins:

#!/usr/bin/env python3
from cmk.base.plugins.agent_based.agent_based_api.v1 import exists, register, SNMPTree, Service, Result, State

def parse_trendnet_camera(string_table):
    parsed = {}
    for line in string_table:
        parsed[line[0]] = line[1]
    return parsed

def discover_trendnet_camera(section):
    for service_id in section:
        yield Service(item=service_id)

def check_trendnet_camera(item, params, section):
    yield Result(state=State.OK, summary=f"SNMP {State.OK}: {section[item]}")
    
register.snmp_section(
    name = 'trendnet_camera',
    detect = exists('.1.3.6.1.4.1.39165.1.1.0'),
    fetch = SNMPTree(
        base = '.1.3.6.1.4.1.39165.1.1',
        oids = [
            '1.0',  # deviceType
            '2.0',  # memSize
        ],
    ),
    parse_function=parse_trendnet_camera,
)

register.check_plugin(
    name="trendnet_camera",
    service_name="SNMP %s",
    discovery_function=discover_trendnet_camera,
    check_function=check_trendnet_camera,
    check_default_parameters={}
)

Nothing is discovered. I still can’t exactly figure out how to map the service name to an OID (I have 34 OIDs total but currently just testing the first 2). I’m also not sure the parse_function is required at all for my use case scenario. I know I’m very close to get this working and would appreciate if you could just point me in the right direction. I wish the doc had a completely working basic example.

From your two lines of data i cannot see where data should be for two services with different names.
To see what the system sees with your parse function i would do the following.

Insert a print(string_table) at the start of the parse function. Then do a cmk -vvI hostname and look at the output from the print statement. This is the data you can use in your parse function.

2 Likes

Hi Andreas,
Thank you for your help!

Here is the complete output: OMD[home]:~/local/lib/check_mk/base/plugins/agent_based$ cmk -vvI camera01Disc - Pastebin.com
I’m not sure why there are so many Using cached OID .1.3.6.1.2.1.1.2.0: ''. Other than that here is the result of the print:

+ ANALYSE DISCOVERED HOST LABELS
before print
[]
after print
Trying host label discovery with: trendnet_camera
Trying host label discovery with:
SUCCESS - Found no new host labels
+ ANALYSE DISCOVERED SERVICES
+ EXECUTING DISCOVERY PLUGINS (1)
  Trying discovery with: trendnet_camera
SUCCESS - Found no new services

So, the string_table is basically empty. Why would that be? We do see there is something in the output:

Getting OID .1.3.6.1.4.1.39165.1.1.0: Running 'snmpget -v2c -c public -m "" -M "" -On -OQ -Oe -Ot 10.2.1.11 .1.3.6.1.4.1.39165.1.1.0'
SNMP answer: ==> ["TV-IP1319PI"]

This might be from a different plugin (snmp_check) that I’m running (active check). I disabled it but it might be in cache.

Ok I figured it out, it was looking for .1.3.6.1.4.1.39165.1.1.0 rather than .1.3.6.1.4.1.39165.1.0. I do have a string_table now:

before print
[['TV-IP1319PI', '0']]
after print

I will report back later on my findings, I think I might be able to get it working from there.

Can confirm I got it working. There is still some work to do. If anyone stumbles across this post and has Trendnet IPTV cameras, this should get you started:

/local/lib/check_mk/base/plugins/agent_based/trendnet_camera.py

#!/usr/bin/env python3
from cmk.base.plugins.agent_based.agent_based_api.v1 import exists, register, SNMPTree, Service, Result, State

def parse_trendnet_camera(string_table):
    mib = {
        1:  ('deviceType'),
        2:  ('hardwVersion'),
        3:  ('softwVersion'),
        4:  ('macAddr'),
        5:  ('deviceID'),
        6:  ('manufacturer'),
        7:  ('cpuPercent'),
        8:  ('diskSize'),
        9:  ('diskPercent'),
        10: ('memSize'),
        11: ('memUsed'),
        12: ('restartDev'),
        13: ('dynIpAddr'),
        14: ('dynNetMask'),
        15: ('dynGateway'),
        16: ('staticIpAddr'),
        17: ('staticNetMask'),
        18: ('staticGateway'),
        19: ('sysTime'),
        20: ('videoInChanNum'),
        21: ('videoEncode'),
        22: ('videoNetTrans'),
        23: ('audioAbility'),
        24: ('audioInNum'),
        25: ('videoOutNum'),
        26: ('clarityChanNum'),
        27: ('localStorage'),
        28: ('rtspPlayBack'),
        29: ('netAccessType'),
        30: ('alarmInChanNum'),
        31: ('alarmOutChanNum'),
        32: ('manageServAddr'),
        33: ('ntpServIpAddr'),
        34: ('managePort'),
    }

    parsed = {}
    x = 0

    for value in string_table[0]:
        x += 1
        parsed[mib[x]] = value
    return parsed

def discover_trendnet_camera(section):
    # there are some OIDs which we don't really need such as the network addresses, so we filter what we discover here
    discovery_mibs = [
                'deviceType',
                'hardwVersion',
                'softwVersion',
                'cpuPercent',
                'diskSize',
                'diskPercent',
                'memSize',
                'memUsed',
                'sysTime',
                'videoInChanNum',
                'videoEncode',
                'videoNetTrans',
                'audioAbility',
                'audioInNum',
                'videoOutNum',
                'clarityChanNum',
                'localStorage',
                'rtspPlayBack',
                'netAccessType',
                'alarmInChanNum',
                'alarmOutChanNum',
        ]
    for key in section:
        if key in discovery_mibs: yield Service(item=key)

def check_trendnet_camera(item, params, section):
    if section[item] == '': section[item] = 'No data provided'
    yield Result(state=State.OK, summary=section[item])

register.snmp_section(
    name = 'trendnet_camera',
    detect = exists('.1.3.6.1.4.1.39165.1.1.0'),
    fetch = SNMPTree(
        base = '.1.3.6.1.4.1.39165.1',
        oids = [
            '1.0',  # deviceType
            '2.0',  # hardwVersion
            '3.0',  # softwVersion
            '4.0',  # macAddr
            '5.0',  # deviceID
            '6.0',  # manufacturer
            '7.0',  # cpuPercent
            '8.0',  # diskSize
            '9.0',  # diskPercent
            '10.0', # memSize
            '11.0', # memUsed
            '12.0', # restartDev
            '13.0', # dynIpAddr
            '14.0', # dynNetMask
            '15.0', # dynGateway
            '16.0', # staticIpAddr
            '17.0', # staticNetMask
            '18.0', # staticGateway
            '19.0', # sysTime
            '20.0', # videoInChanNum
            '21.0', # videoEncode
            '22.0', # videoNetTrans
            '23.0', # audioAbility
            '24.0', # audioInNum
            '25.0', # videoOutNum
            '26.0', # clarityChanNum
            '27.0', # localStorage
            '28.0', # rtspPlayBack
            '29.0', # netAccessType
            '30.0', # alarmInChanNum
            '31.0', # alarmOutChanNum
            '32.0', # manageServAddr
            '33.0', # ntpServIpAddr
            '34.0', # managePort
        ],
    ),
    parse_function=parse_trendnet_camera,
)

register.check_plugin(
    name='trendnet_camera',
    service_name='SNMP %s',
    discovery_function=discover_trendnet_camera,
    check_function=check_trendnet_camera,
    check_default_parameters={}
)

Result:

Just don’t forget the Hosts without system description OID rule for the camera.

Exactly the same OIDs are for HIKVISION cameras.

Good to know, maybe there is some standard among the different brands after all!

I added performance charts for the CPU usage, the memory usage and the time offset. I removed most of the (useless) data as well.

Code:

#!/usr/bin/env python3
# http://www.circitor.fr/Mibs/Mib/H/HIK-DEVICE-MIB.mib

from cmk.base.plugins.agent_based.agent_based_api.v1 import exists, register, render, SNMPTree, Service, Result, State, Metric
from datetime import datetime

def parse_trendnet_camera(string_table):
    mib = {
        1:  ('deviceType'),
        2:  ('hardwVersion'),
        3:  ('softwVersion'),
        4:  ('macAddr'),
        5:  ('deviceID'),
        6:  ('manufacturer'),
        7:  ('cpuPercent'),
        8:  ('diskSize'),
        9:  ('diskPercent'),
        10: ('memSize'),
        11: ('memUsed'),
        12: ('restartDev'),
        13: ('dynIpAddr'),
        14: ('dynNetMask'),
        15: ('dynGateway'),
        16: ('staticIpAddr'),
        17: ('staticNetMask'),
        18: ('staticGateway'),
        19: ('sysTime'),
        20: ('videoInChanNum'),
        21: ('videoEncode'),
        22: ('videoNetTrans'),
        23: ('audioAbility'),
        24: ('audioInNum'),
        25: ('videoOutNum'),
        26: ('clarityChanNum'),
        27: ('localStorage'),
        28: ('rtspPlayBack'),
        29: ('netAccessType'),
        30: ('alarmInChanNum'),
        31: ('alarmOutChanNum'),
        32: ('manageServAddr'),
        33: ('ntpServIpAddr'),
        34: ('managePort'),
    }

    parsed = {}
    x = 0

    for value in string_table[0]:
        x += 1
        parsed[mib[x]] = value
    return parsed

def discover_trendnet_camera(section):
    # there are some OIDs which we don't really need such as the network addresses, so we filter what we discover here
    discovery_mibs = [
                'deviceType',
                'softwVersion',
                'cpuPercent',
                'memUsed',
                'sysTime',
        ]
    for key in section:
        if key in discovery_mibs: yield Service(item=key)

def check_trendnet_camera(item, params, section):
    if section[item] == '': section[item] = 'No data provided'
    if item == 'cpuPercent':
        yield Metric(name='util', value=int(section[item].split()[0]))
        summary = f"Total CPU : {render.percent(int(section[item].split()[0]))}"
    elif item == 'memUsed':
        yield Metric(name='mem_used_percent', value=int(section[item].split()[0]))
        summary = f"Total : {render.percent(int(section[item].split()[0]))}, {int(int(section[item].split()[0])/100*int(section['memSize'].split()[0]))} MB of {section['memSize']}"
    elif item == 'sysTime':
        diff = int((datetime.now() - datetime.strptime(section[item],'%Y-%m-%d %H:%M:%S')).total_seconds())
        yield Metric(name='time_offset', value=diff)
        if abs(diff) > 120: # tolerance offset
            state=State.CRIT
        else:
            state=State.OK
        yield Result(state=state, summary=f"Offset: {diff} s")
        return
    else:
        summary = section[item]
    yield Result(state=State.OK, summary=summary)

register.snmp_section(
    name = 'trendnet_camera',
    detect = exists('.1.3.6.1.4.1.39165.1.1.0'),
    fetch = SNMPTree(
        base = '.1.3.6.1.4.1.39165.1',
        oids = [
            '1.0',  # deviceType
            '2.0',  # hardwVersion
            '3.0',  # softwVersion
            '4.0',  # macAddr
            '5.0',  # deviceID
            '6.0',  # manufacturer
            '7.0',  # cpuPercent
            '8.0',  # diskSize
            '9.0',  # diskPercent
            '10.0', # memSize
            '11.0', # memUsed
            '12.0', # restartDev
            '13.0', # dynIpAddr
            '14.0', # dynNetMask
            '15.0', # dynGateway
            '16.0', # staticIpAddr
            '17.0', # staticNetMask
            '18.0', # staticGateway
            '19.0', # sysTime
            '20.0', # videoInChanNum
            '21.0', # videoEncode
            '22.0', # videoNetTrans
            '23.0', # audioAbility
            '24.0', # audioInNum
            '25.0', # videoOutNum
            '26.0', # clarityChanNum
            '27.0', # localStorage
            '28.0', # rtspPlayBack
            '29.0', # netAccessType
            '30.0', # alarmInChanNum
            '31.0', # alarmOutChanNum
            '32.0', # manageServAddr
            '33.0', # ntpServIpAddr
            '34.0', # managePort
        ],
    ),
    parse_function=parse_trendnet_camera,
)

register.check_plugin(
    name='trendnet_camera',
    service_name='IPTV %s',
    discovery_function=discover_trendnet_camera,
    check_function=check_trendnet_camera,
    check_default_parameters={}
)

Yes there is, most of these cameras are Hikvision devices with different branding.

One thing i would do a little bit different is the following.
Your parsed section i would use in different checks.
One check for CPU, Memory and so on. Why?
If you want to use the parameters with your CPU or Memory check then you can use the already existing generic parameters from setup (WATO).
Also you don’t need the “if elif” with the different items in your check function.

So you are saying there are default checks I could use so that the thresholds could be defined with what exists in WATO? Any example or documentation on this? I’m very new to plugin programming.

As for the elif are you suggesting they should just all be ifs?

Any help would be appreciated!

Yes
This here

Is an example where i use the generic temperature WATO rule to define parameters for the check.

Inside this folder

You find all the check parameters already existing.
The “memory” parameters are one your can easily use in your checks or the parameters for CPU usage.