Get started on custom HTTP parser

I have a managed power distribution unit (PDU) on my network, from which I can fetch http://ip/status.xml which contains temperature and humidity and whether an outlet in the PDU is on or off.
It feels pretty straight-forward to parse and I can probably write something in Python to do all the file fetching and such.
What I am after however, I haven’t been able to find in the documentation (yet), which is separate services for each outlet and humidity value and temperature value, not unlike a network switch with its separate interfaces and such.

Using Checkmk Raw Edition 2.3.0p20 on Ubuntu 22.04 VM in Proxmox.

Thus I have two questions:

  1. Is there a ruleset I can use somewhere that does exactly this?
  2. If I write my own Python check, how do I make it into separate services and that? Should I write a local check or a special agent for this use case? I get a bit lost in the WATO at times so please bear with me. <3

Thanks!

1 Like

To answer question 2: think what you are looking for is a Datasource program

I use this to pull data from a P1-meter (in a very simple form)

The howto is on the forum at https://forum.checkmk.com/t/howto-p1-meter-monitoring-in-cmk/49317
Hope this can act as an example for your case.

  • Glowsome
1 Like

That is a mighty quick response my friend, I will absolutely have a look at this when I get home. It looks very promising, hopefully can apply to my use case as well!
Will update on how it goes.

1 Like

That actually did it!
I had to come to some conclusions myself, such as having to enclose multi-word service names within double brackets, but besides that, it went positively smoothly.

If anyone wishes to implement the exact same fringe case as me, enclosed is the full code that I put under ~/local/bin in accordance with the linked documentation.

pdu_status_check.py
#!/usr/bin/env python3

import requests
import xml.etree.ElementTree as ET
from bs4 import BeautifulSoup

# === Config ===
PDU_HOST = "http://192.168.1.41"  # Change to your PDU IP
AUTH = ("username", "password")  # Credentials for control_outlet.htm

# === Fetch XML status ===
def fetch_status_xml():
    try:
        response = requests.get(f"{PDU_HOST}/status.xml", timeout=2)
        response.raise_for_status()
        return ET.fromstring(response.content)
    except Exception as e:
        print(f"2 PDU_Status - Error fetching status.xml: {e}")
        exit(1)

# === Fetch outlet names from control_outlet.htm ===
def fetch_outlet_names():
    try:
        response = requests.get(f"{PDU_HOST}/control_outlet.htm", auth=AUTH, timeout=2)
        response.raise_for_status()
        soup = BeautifulSoup(response.text, 'html.parser')
        names = [td.text.strip() for td in soup.find_all('td')[::3]][1:]  # Skip header
        return {f"outletStat{i}": names[i] for i in range(len(names))}
    except Exception as e:
        print(f"2 PDU_Outlets - Error fetching outlet names: {e}")
        return {}

# === Check Logic ===
def parse_services(status_xml, outlet_name_map):
    services = []

    # Load
    current = status_xml.findtext("curBan")
    if current is not None:
        current = float(current)
        state = 0 if current <= 9.0 else 1 if current <= 9.5 else 2
        services.append((state, f'"PDU Load" - Current Load: {current} A'))

        # Temperature
    temp = status_xml.findtext("tempBan")
    if temp is not None:
        temp = int(temp)
        state = 0 if temp < 35 else 1 if temp <= 45 else 2
        services.append((state, f'"PDU Temperature" - Temperature: {temp} °C'))

    # Humidity
    hum = status_xml.findtext("humBan")
    if hum is not None:
        hum = int(hum)
        state = 0 if hum < 70 else 1 if hum < 85 else 2
        services.append((state, f'"PDU Humidity" - Humidity: {hum}%'))

    # Outlets
    for i in range(8):
        key = f"outletStat{i}"
        val = status_xml.findtext(key)
        name = outlet_name_map.get(key, f"Outlet {i}")
        state = 0 if val == "on" else 2
        if state == 0:
            msg = f'"PDU Outlet {i}" - OK - [{name}], ({val.upper()})'
        elif state == 2:
            msg = f'"PDU Outlet {i}" - CRIT - [{name}], ({val.upper()})'
        services.append((state, msg))

    return services

# === Main ===
if __name__ == "__main__":
    xml_root = fetch_status_xml()
    outlet_names = fetch_outlet_names()
    results = parse_services(xml_root, outlet_names)
    print('<<<local>>>')

    for state, msg in results:
        print(f'{state} {msg}')
status.xml file fetched from PDU, to work with
root@Srv01-SANDBOX:~# cat status.xml
<response>
<cur0>0.0</cur0>
<stat0>normal</stat0>
<curBan>0.0</curBan>
<tempBan>31</tempBan>
<humBan>19</humBan>
<statBan>normal</statBan>
<outletStat0>on</outletStat0>
<outletStat1>on</outletStat1>
<outletStat2>on</outletStat2>
<outletStat3>on</outletStat3>
<outletStat4>on</outletStat4>
<outletStat5>on</outletStat5>
<outletStat6>on</outletStat6>
<outletStat7>on</outletStat7>

<userVerifyRes>0</userVerifyRes>

</response>

Result:

OMD[mischief]:~/local/bin$ cmk -d MischiefPDU01
<<<local>>>
0 "PDU Load" - Current Load: 0.0 A
0 "PDU Temperature" - Temperature: 31 °C
0 "PDU Humidity" - Humidity: 19%
0 "PDU Outlet 0" - OK - [MschfAS01], (ON)
0 "PDU Outlet 1" - OK - [MschfFS01], (ON)
0 "PDU Outlet 2" - OK - [MschfFan0], (ON)
0 "PDU Outlet 3" - OK - [MschfSrv0], (ON)
0 "PDU Outlet 4" - OK - [BestaFans], (ON)
0 "PDU Outlet 5" - OK - [outlet6], (ON)
0 "PDU Outlet 6" - OK - [outlet7], (ON)
0 "PDU Outlet 7" - OK - [PS5_TV], (ON)

checkmkpdu

Thanks again for the quick help, I learned a lot!

2 Likes

Awesome result :muscle:

You could refine the script to make it (a bit) more dynamic by using the host/url as an argument from the agent-call rule and accepting it in the script.
This should remove the/your need for hard-coded PDU_HOST definition.
(i’m alway a bit allergic to hardcoded stuff, as mostly when things change you need to make more effort to get it working again)

Taking it a step further from there you could also add another argument to add the user you are using.

Unfortunately there is no option to also include the (if authentication is needed) password in a secure way, so i would not take it that far due to security -issues.

It would be a nice extension if one were able to use a stored password in calling the program/executable.

  • Glowsome
1 Like

You’re right, I too am a little bit allergic to hardcoded credentials and addresses.
The credentials are used solely for fetching the outlet names as well so it’s not that important anyway, but a big convenience all the same.

I might make it more dynamic in the future, but for now, this works perfectly well for me and for my purposes. :slight_smile:

2 Likes