Wie schreibt man an besten einen Check für eine REST API Abfrage

Hallo Zusammen nach etwas Abstinenz wieder mal da :wink:

Folgendes wenn ich ein Gerät überwachen will und die Daten nicht via SNMP sondern via REST API abfragen will wie fange ich am besten an ???

Der allgemeine Ablauf ist ja so:
CMK --[runs]–> script --[calls REST API]–> Host --[JSON answer]–> script --[converts to cmk format]–> CMK --[sends output]–> check plugin --[processes output]–> CMK --[creates]–> services <–[Parameters]-- WATO rule plugin

Die Abfrage wäre ja z.B.
curl -v -X GET -u <username>:<password> "http://192.168.1.1/appAll.json"

der Output sieht so aus:

API Response Details
API Response Details:
Response content type: 'application/json'
Response Sample Format:
{
 "data": {
 "all": [{
 "device": {
 "unit": "Device-8106",
 "model": "E-MICRO-T(RHP) ",
 "uptime": "3 days, 2 hours, 8 mins",
 "firmware": "3.1"
 }
 },
 {
 "network": {
 "mac": "00:0c:82:00:00:06",
 "dhcp": 0,
 "addr": "192.168.1.1",
 "mask": "255.255.255.0",
 "gtw": "192.168.1.0",
 "dns1": "192.168.1.52",
 "dns2": "192.168.1.53"
 }
 },
 {
 "isens": [{
 "idx": 0,
 "desc": "temperature",
 "type": 1,
 "unit": 0,
 "val": "30.5 C"
 }, {
 "idx": 1,
 "desc": "Humidity1",
 "type": 2,
NOTE: API commands are case sensitive
NTI Micro Environment Monitoring System
53
 "unit": 0,
 "val": "35.5 %"
 }, {
 "idx": 2,
 "desc": "Dew Point",
 "type": 24,
 "unit": 0,
 "val": "13.6 C"
 }]
 },
 {
 "esens": [{
 "idx": 0,
 "desc": "Temperature #1",
 "type": 1,
 "unit": 0,
 "val": "27.9 C"
 }, {
 "idx": 1,
 "desc": "Humidity #1",
 "type": 2,
 "unit": 0,
 "val": "39.2 %"
 }, {
 "idx": 2,
 "desc": "Dew Point #1",
 "type": 24,
 "unit": 0,
 "val": "12.7 C"
 }, {
 "idx": 3,
 "desc": "Temperature #2",
 "type": 1,
 "unit": 0,
 "val": "27.8 C"
 }, {
 "idx": 4,
 "desc": "Humidity #2",
 "type": 2,
 "unit": 0,
 "val": "39.8 %"
 }, {
 "idx": 5,
 "desc": "Dew Point #2",
 "type": 24,
 "unit": 0,
 "val": "12.9 C"
 }]
 },
 {
 "diginp": [{
 "idx": 0,
 "desc": "Digital Input #1",
 "type": 19,
 "val": "Open"
 }, {
 "idx": 1,
 "desc": "Digital Input #2",
 "type": 19,
 "val": "Open"
 }]
 },
 {
 "ipdev": [{
 "idx": 0,
 "desc": "IP Device #1",
 "ip": "8.8.8.8",
 "val": "Responding",
 "retries": 3,
 "timeout": 5,
 "repeat": 60 
NTI Micro Environment Monitoring System
54
 }]
 },
 {
 "alerts": [{
 "idx": 0,
 "sensor": "Humidity1",
 "status": "2",
 "alertMsg": "Sensor value greater than 25.0",
 "alertStatus": "Alarm",
 "val": "35.5 %",
 "sensorType": 1,
 "sensorClass": 0,
 "sensorId": 1
 }]
 },
 {
 "smalerts": [{
 "idx": 0,
 "status": "Alarm"
 }]
 }
 ]
 },
 "msg": "Request Successful",
 "code": 200
} 

Wie bekomme ich nun die Infos zu einem Check bzw. Metric zugeteilt?

Kennt einer einen Beispiel Check oder so der die REST-API eines Gerätes abfragt und verarbeitet ???

Beschreibung der API ab Seite 52

lG Bernd

1 Like

Das ist witzig. Ich habe gerade vor ein paar Tagen einen Special Agent geschrieben. Der fragt zwar keine REST-Schnittstelle ab, aber was du vorhast, klingt für mich genau danach.

Du brauchst vier Dateien:

  • Ein WATO-Plugin, in dem du die Aufruf-Parameter für den Special Agent festlegen kannst (Username, Passwort, URL)
  • Ein Check-Plugin, das aus diesen WATO-Parametern eine Argumentliste zusammenbaut, mit der dein Special Agent aufgerufen wird.
  • Den Special Agent selbst. Das ist ein beliebiges Skript (oder Binary), das wie ein Agent-Plugin irgendwelche Abfragen macht und dann seine Ausgabe in einer oder mehreren <<<sections>>> nach STDOUT schreibt
  • Das/die eigentliche(n) Check-Plugin(s). Sie unterscheiden sich nicht von anderen Check-Plugins, d.h. sie kriegen die Daten ganz normal in ihre parse/inventory/check-Funktionen reingereicht.

WATO-Plugin
#!/usr/bin/env python3
# -*- mode: Python; encoding: utf-8; indent-offset: 4; autowrap: nil -*-

from cmk.gui.i18n import _
from cmk.gui.valuespec import (
    Dictionary,
    TextAscii,
    PasswordSpec,
)

from cmk.gui.plugins.wato import (
    rulespec_registry,
    HostRulespec,
)

from cmk.gui.plugins.wato.datasource_programs import (
    RulespecGroupDatasourcePrograms,
)

def _valuespec_special_agents_my_rest_api():
    return Dictionary(
        elements=[
            ("uid", TextAscii(title=_("Username"), allow_empty=False)),
            ("pwd", PasswordSpec(title=_("Password"), allow_empty=False, hidden=True)),
            ("url, TextAscii(title=_("URL"), allow_empty=False)),
        ],
        optional_keys=False,
        title=_("REST Parameters"),
    )

rulespec_registry.register(
    HostRulespec(
        group=RulespecGroupDatasourcePrograms,
        name="special_agents:my_rest_api",
        valuespec=_valuespec_special_agents_my_rest_api,
    ))
Check-Plugin für den Aufruf
#!/usr/bin/env python3
# -*- encoding: utf-8; py-indent-offset: 4 -*-

def agent_my_rest_api_arguments(params, hostname, ipaddress):
    args = []
    args.append(params['uid'])
    args.append(params['pwd'])
    args.append(params['url'])
    args.append('--test')
    return args

special_agent_info['my_rest_api'] = agent_my_rest_api_arguments
Special-Agent
#!/usr/bin/env bash

uid=$1;
pwd=$2;
url=$3;
test=$4;

OUTPUT=$(curl -v -X GET -u $uid:$pwd "$url")
do_something_with $OUTPUT;

echo "<<<my_rest_api>>>"
echo $OUTPUT
Check-Plugin(s)

Hier brauchst du nur ein ganz normales Plugin für die Section <<<my_rest_api>>>. Dein Special Agent kann auch mehrere <<<sections>>> schreiben, dann brauchst du halt mehrere Check-Plugins.


Du würdest den Special-Agent natürlich nicht in bash schreiben, sondern z.B. in Python, denn dann kannst du leicht über die zurückgegebene JSON-Struktur iterieren und daraus sowas wie

<<<my_rest_api>>>
network mac=00:0c:82:00:00:06 dhcp=0 addr=192.168.1.1 ...
...

machen. Wichtig ist die Namensgebung der Dateien, denn manche müssen agent_ davor heißen:

  • Check-Plugin: ~/local/share/check_mk/checks/my_rest_api
  • Parameter-Bauer: ~/local/share/check_mk/checks/agent_my_rest_api
  • Special Agent: ~/local/share/check_mk/agents/special/agent_my_rest_api
  • WATO: ~/local/share/check_mk/web/plugins/wato/my_rest_api.py

Ist das ungefähr das, was Du meinst?

Was dann noch fehlt, ist natürlich eine Regel, wonach dein Host nicht per checkmk-Agent sondern mit deiner neuen Datasource (aka Special Agent) abgefragt wird.


EDIT: Ich habe oben geschrieben, der Special Agent soll eine Section <<<my_rest_api>>> ausgeben. Das ist nicht ganz richtig: er kann so viele Sections ausgeben wie er will und die können auch heißen, wie sie wollen (von bereits vergebenen Namen mal abgesehen). Du brauchst dann halt für jede Section ein eigenes “normales” Check-Plugin.

11 Likes

WOW danke für den Anschub !!!

Ich werde das mal die Woche in Angriff nehmen … und dann Rückmeldung geben

Gruß Bernd

1 Like

Genau sowas muss ins Handbuch. Mega!

3 Likes

Ich denke das es auch ein Thema ist was in Zukunft öfter der Fall ist da SNMP oft schlecht implementiert ist bzw auch recht langsam.

1 Like

Ich hatte mal einen Wrapper für check_http geschrieben: https://github.com/HeinleinSupport/check_mk_extensions/tree/master/check_restapi
Dabei wird das Ergebnis per Regex geprüft, also kein JSON interpretiert.

1 Like

Von mir gibts auch einen schon etwas älteren Special Agent für die HP iLO REST API.
Kannst dir ja mal anschauen was ich da gebastelt hab.

1 Like

nur mal so als Status … Die Woche komme ich nicht mehr zum basteln und programmieren Rückmeldung dann nächste Woche.

Gruß Bernd

So mal einen zwischen Stand da ich noch nicht so richtig dazu gekommen bin aber schon mal die die Ideen gesichtet und teils ausprobiert habe wird es wohl auf die Lösung via Spezial Agent hinauslaufen.

Das Hauptproblem ist noch das umwandeln des Output (wrappen), da ich hier bisher noch keine Erfahrung habe wie man es am elegantesten löst nehme ich gerne Vorschläge an :wink:

der Output sieht so aus:

Output
*   Trying 10.122.xxx.xxx...
* TCP_NODELAY set
* Connected to 10.122.xxx.xxx (10.122.xxx.xxx) port 80 (#0)
* Server auth using Basic with user 'xxxx'
> GET /appAll.json HTTP/1.1
> Host: 10.122.xxx.xxx
> Authorization: Basic cm9vdDprdXNibw==
> User-Agent: curl/7.55.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Connection: close
< Content-Type: application/json
< Cache-Control: no-cache
<
{"data":{"all":[
 {"device":{"unit":"xxxxxxxx", "model":"ENVIROMUX-MICRO-T(RHP) ", "uptime":"75 days, 8 hours, 41 mins", "firmware":"3.10"}},
 {"network":{"mac":"00:0c:82:15:0d:68","dhcp":1,"addr":"10.122.xxx.xxx","mask":"255.255.254.0","gtw":"10.122.xxx.xxx","dns1":"10.122.xxx.xxx","dns2":"10.124.xxx.xxx"}},
 {"isens":[{"idx":0,"desc":"UPS BO Temperatur", "type":1, "unit":0, "val":"23.5 C"},{"idx":1,"desc":"UPS BO Feuchte", "type":2, "unit":0, "val":"50.0 %"},{"idx":2,"desc":"UPS BO Taupunkt", "type":24, "unit":0, "val":"12.5 C"} ]},
 {"esens":[                         ]},
 {"diginp":[{"idx":0,"desc":"Digital Input #1", "type":19, "val":"Open"},{"idx":1,"desc":"Digital Input #2", "type":19, "val":"Open"} ]},
 {"ipdev":[{"idx":0,"desc":"USV", "ip":"10.122.xxx.xxx", "val":"Responding", "retries":3, "timeout":2, "repeat":600} ]},
 {"alerts":[{"idx":0,"sensor":"UPS BO Temperatur", "status":"0", "alertMsg":"", "alertStatus":"Normal", "val":"23.5 C", "sensorType":1,"sensorClass":0,"sensorId":0},{"idx":1,"sensor":"Digital Input #1", "status":"0", "alertMsg":"", "alertStatus":"Normal", "val":"Open", "sensorType":19,"sensorClass":2,"sensorId":0},{"idx":2,"sensor":"USV", "status":"0", "alertMsg":"", "alertStatus":"Normal", "val":"Responding", "sensorType":0,"sensorClass":3,"sensorId":0},{"idx":3,"sensor":"Temperatur USV-RAUM BO", "status":"0", "alertMsg":"", "alertStatus":"Normal", "val":"--", "sensorType":0,"sensorClass":1,"sensorId":0}                             ]},
 {"smalerts":[  ]}
]}, "msg": "Request Successful", "code": 200}
* Closing connection 0

Du könntest z.B. ungefähr folgenden Code für den Special Agent als Vorlage benutzen:

Special Agent
#!/usr/bin/env python3
# -*- mode: Python; encoding: utf-8; indent-offset: 4; autowrap: nil -*-

import requests
import json

def print_sections(response):
    if response['code'] != 200:
        return
        
    separator = 124 # '|'; for more fun use separator=7 (which is a bell)
    lines = []
    for d1 in response['data']['all']:
        for k, v in d1.items():
            if isinstance(v, dict):     # single dictionary
                lines.append(chr(separator).join([k] + ['{}={}'.format(k2,v2) for k2, v2 in sorted(v.items())]))
            elif isinstance(v, list):   # list of dictionaries
                for d2 in v:
                    lines.append(chr(separator).join([k] + ['{}={}'.format(k2,v2) for k2, v2 in sorted(d2.items())]))
    if lines:
        print('<<<my_section:sep({})>>>'.format(separator))
        print('\n'.join(lines))

def main():
    try:
        url = 'http://192.168.1.1/appAll.json'
        raw_response = requests.get(url,auth=('user', 'pass'))
        response = raw_response.json()
    except Exception as exc:
        print('cannot GET data from URL {}: {}'.format(url, exc))
        sys.exit(1)

    try:
        print_sections(response)
    except KeyError:
        # some of the expected keys did not exist in the response
        pass    

Die Variablennamen sind zugegebenermaßen schrecklich (v1, v2, …), aber ich wusste keine passenden, weil ich mich in der Problem-Domäne zu wenig auskenne.

Bedeutung der Variablen
For this sample data:
    {'code': 200,
     'data': {'all': [{'device': {'firmware': '3.10',
                                  'model': 'ENVIROMUX-MICRO-T(RHP) ',
                                  'unit': 'xxxxxxxx',
                                  'uptime': '75 days, 8 hours, 41 mins'}},
                      {'isens': [{'desc': 'UPS BO Temperatur',
                                  'idx': 0,
                                  'type': 1,
                                  'unit': 0,
                                  'val': '23.5 C'},
                                  ...
                                 {'desc': 'UPS BO Taupunkt',
                                  'idx': 2,
                                  'type': 24,
                                  'unit': 0,
                                  'val': '12.5 C'}]},
        ...
the variables refer to the following data:
    d1: the dictionaries {device:...}, {isens:...}, ...
    k:  'device', 'isens', ...
    v:  either a {} or a [] (the value of d1[k])
    d2: the {} within the [] if v is a list
    k2: 'firmware', 'model', ..., 'desc', 'idx', ...
    v2: '3.10', 'ENVIROMUX-MICRO-T(RHP) ', ..., 'UPS BO Temperatur', 0, ...

Mit deinen Beispieldaten kommt damit folgendes raus:

<<<my_section:sep(124)>>>
device|firmware=3.10|model=ENVIROMUX-MICRO-T(RHP) |unit=xxxxxxxx|uptime=75 days, 8 hours, 41 mins
network|addr=10.122.xxx.xxx|dhcp=1|dns1=10.122.xxx.xxx|dns2=10.124.xxx.xxx|gtw=10.122.xxx.xxx|mac=00:0c:82:15:0d:68|mask=255.255.254.0
isens|desc=UPS BO Temperatur|idx=0|type=1|unit=0|val=23.5 C
isens|desc=UPS BO Feuchte|idx=1|type=2|unit=0|val=50.0 %
isens|desc=UPS BO Taupunkt|idx=2|type=24|unit=0|val=12.5 C
diginp|desc=Digital Input #1|idx=0|type=19|val=Open
diginp|desc=Digital Input #2|idx=1|type=19|val=Open
ipdev|desc=USV|idx=0|ip=10.122.xxx.xxx|repeat=600|retries=3|timeout=2|val=Responding
alerts|alertMsg=|alertStatus=Normal|idx=0|sensor=UPS BO Temperatur|sensorClass=0|sensorId=0|sensorType=1|status=0|val=23.5 C
alerts|alertMsg=|alertStatus=Normal|idx=1|sensor=Digital Input #1|sensorClass=2|sensorId=0|sensorType=19|status=0|val=Open
alerts|alertMsg=|alertStatus=Normal|idx=2|sensor=USV|sensorClass=3|sensorId=0|sensorType=0|status=0|val=Responding
alerts|alertMsg=|alertStatus=Normal|idx=3|sensor=Temperatur USV-RAUM BO|sensorClass=1|sensorId=0|sensorType=0|status=0|val=--

Wie gesagt, ist nur ein Vorschlag. Viele Agent-Plugins erstellen auch eine Art “Subsection”, etwa so:

<<<my_section>>>
[[[ENVIROMUX-MICRO-T(RHP)]]]
(Daten für dieses Device)
[[[ENVIROMUX-Nano]]]
(Daten für jenes Device)

Was natürlich noch fehlt, ist die Übergabe der drei Parameter URL, uid, pwd.

4 Likes

Hi Zusamen,

komme zwar aktuell nicht dazu das fertig zu machen aber wenn ich den Agent fertig habe kommt er auf jeden Fall in den Exchange

THX euch allen

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.