Self plugin snmp cache

CMK version: 2.4.0p20 CRE
OS version:

I created a plugin myself to monitor my Teltonika router. I don’t have any errors because no errors occur in that sense. The plugin works and services are also displayed. However, these are not updated when changes are made.
I want to monitor an Active WAN service, which includes the current WAN interface from the failover. However, this only changes when I search for new services. They do not change automatically because. I suspect it’s due to the SNMP Walk Max Age, which apparently doesn’t update the values.

Read from cache: NoCache(path_template=/dev/null, max_age=MaxAge(checking=0.0, discovery=0.0, inventory=0.0), simulation=False, use_only_cache=False, file_cache_mode=1)

Does anyone have any idea how I can change this so that the SNMP queries run again and are not pulled from the cache?

Did you change the default service check interval? If yes it could happen that you did not wait enough time for data to be collected.

Where should I adjust that? I waited a whole day :slight_smile:

Try this

CLI

# shows exactly which OID is used or if cache data used
cmk --debug -vvn OUR_HOSTNAME

Take care in the output for:
[SNMP] Success, found ... entries vs. [SNMP] Use cached data


the index trap (most common cause)

With Teltonika (and many other routers), the indexes in the SNMP table often change during a failover.

The problem:
If your plugin detects during discovery that WAN1 is on index .1, Checkmk stores this index for the service. If the failover now switches to WAN2 (e.g., index .2), the normal check continues to stubbornly query index .1. Since this may still “exist” but does not provide any new data, the value remains “static.”

The solution:
Your plugin should not map to a fixed index, but rather read the entire table (or at least the column with the interface names) during each check in order to dynamically find the currently “active” index.

Greetz Bernd

perhaps you can provide the code of the plugin

There is a ruleset called “Normal check interval for service checks” that handles that. If the interval was changed the situation that you described might happen. Can you also share more details about your statement when changes are made? Do you mean like adding or removing services into the host? If that is the case your might be looking for ”Periodic Service Discovery” which can add and remove services automatically as they appear or are removed.

here the plugincode.

#!/usr/bin/env python3

from cmk.agent_based.v2 import (
    CheckPlugin,
    DiscoveryResult,
    Result,
    Service,
    SimpleSNMPSection,
    SNMPTree,
    State,
    StringTable,
    exists,
    OIDEnd,
)

# ------------------------------------------------------------
# Helpers
# ------------------------------------------------------------
def _as_int(s, default: int = 0) -> int:
    try:
        return int(str(s).strip())
    except Exception:
        return default

def _get_row0(st: StringTable):
    # Scalar SNMPTree often returns one row with many columns.
    # But keep it robust.
    if not st:
        return []
    if len(st) == 1:
        return list(st[0])
    # fallback: treat as first column per row
    return [r[0] if r else "" for r in st]

def _col(st: StringTable, idx: int, default: str = "") -> str:
    try:
        row0 = _get_row0(st)
        return str(row0[idx]).strip()
    except Exception:
        return default


# ============================================================
# 1) DEVICE (Router basics) 48690.1.*  (fetched in one go)
# 1.1 ID, 1.2 Name, 1.3 Model, 1.4 HWRev, 1.5 (?), 1.6 FW
# ============================================================
def parse_teltonika_device(st: StringTable) -> dict:
    # unterstützt sowohl 1-Zeilen- als auch Multi-Zeilen-SNMP
    flat = []
    for row in st:
        flat.extend(row)

    def g(i):
        try:
            return str(flat[i])
        except Exception:
            return ""

    return {
        "id":    g(0),
        "name":  g(1),
        "model": g(2),
        "hw":    g(3),
        "fw":    g(5),
    }

snmp_section_teltonika_device = SimpleSNMPSection(
    name="teltonika_device",
    parse_function=parse_teltonika_device,
    detect=exists(".1.3.6.1.4.1.48690.1.1.0"),
    fetch=SNMPTree(
        base=".1.3.6.1.4.1.48690.1",
        # IMPORTANT: without ".0" to reduce separate scalar calls
        oids=["1", "2", "3", "4", "5", "6"],
    ),
)

def discover_teltonika_device(section_teltonika_device, section_teltonika_modeminfo) -> DiscoveryResult:
    yield Service()

def check_teltonika_device(section_teltonika_device, section_teltonika_modeminfo):
    d = section_teltonika_device or {}
    name  = d.get("name", "?")
    model = d.get("model", "?")
    rid   = d.get("id", "?")
    hw    = d.get("hw", "?")
    fw    = d.get("fw", "?")

    yield Result(
        state=State.OK,
        summary=f"Name={name} Model={model} ID={rid} HWRev={hw} FW={fw}",
    )

check_plugin_teltonika_device = CheckPlugin(
    name="teltonika_device",
    service_name="Teltonika Device",
    sections=["teltonika_device", "teltonika_modeminfo"],
    discovery_function=discover_teltonika_device,
    check_function=check_teltonika_device,
)


# ============================================================
# 2) GPS 48690.3.* (lat, lon, speed, time, fix)
# 3.1 lat, 3.2 lon, 3.3 speed, 3.4 time, 3.5 fix
# ============================================================
def parse_teltonika_gps(st: StringTable) -> dict:
    flat = []
    for row in st:
        flat.extend(row)

    def g(i):
        try:
            return str(flat[i])
        except Exception:
            return ""

    return {
        "lat":   g(0),
        "lon":   g(1),
        "speed": g(2),
        "time":  g(3),
        "fix":   g(4),
    }

snmp_section_teltonika_gps = SimpleSNMPSection(
    name="teltonika_gps",
    parse_function=parse_teltonika_gps,
    detect=exists(".1.3.6.1.4.1.48690.3.1.0"),
    fetch=SNMPTree(
        base=".1.3.6.1.4.1.48690.3",
        oids=["1", "2", "3", "4", "5"],
    ),
)

def discover_teltonika_gps(section) -> DiscoveryResult:
    yield Service()

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

def check_teltonika_gps(section):
    lat = section.get("lat") or "?"
    lon = section.get("lon") or "?"
    fix = section.get("fix") or "?"
    spd = section.get("speed") or "?"
    tme = section.get("time") or ""

    tz = ZoneInfo("Europe/Berlin")
    time_txt = "?"

    if tme and tme != "?":
        try:
            dt = datetime.strptime(tme, "%Y-%m-%d %H:%M:%S")

            now_local = datetime.now(tz).replace(tzinfo=None)

            # Wenn der Timestamp "in der Zukunft" liegt, ist die Teltonika-Zeitbasis bei dir off
            # -> korrigieren wir automatisch um 1h oder 2h (was näher an jetzt liegt)
            if dt - now_local > timedelta(minutes=10):
                cand0 = dt
                cand1 = dt - timedelta(hours=1)
                cand2 = dt - timedelta(hours=2)

                d0 = abs((now_local - cand0).total_seconds())
                d1 = abs((now_local - cand1).total_seconds())
                d2 = abs((now_local - cand2).total_seconds())

                dt = min([(d0, cand0), (d1, cand1), (d2, cand2)], key=lambda x: x[0])[1]

            time_txt = dt.strftime("%Y-%m-%d %H:%M:%S")

        except Exception:
            time_txt = tme

    yield Result(
        state=State.OK,
        summary=f"GPS {lat}, {lon} (fix={fix}, speed={spd}, time={time_txt})",
    )

check_plugin_teltonika_gps = CheckPlugin(
    name="teltonika_gps",
    service_name="Teltonika GPS",
    discovery_function=discover_teltonika_gps,
    check_function=check_teltonika_gps,
)


# ============================================================
# 3) MODEM/IMEI/ICCID/IMSI etc. (from 48690.2.2.1.*)
# This is parsed separately so SIM checks can include IMEI.
#
# From your walk:
# 3 IMEI
# 4 modem model (e.g. RG520N-EB)
# 5 vendor (Quectel)
# 7 modem serial (MPY...)
# 8 IMSI
# 27 ICCID
# 9 sim inserted
# ============================================================
def parse_teltonika_modeminfo(st: StringTable) -> dict:
    res = {}
    for i, row in enumerate(st, start=1):
        cols = [str(x) for x in row]
        cols += [""] * (7 - len(cols))
        res[str(i)] = {
            "imei": cols[0],
            "modem_model": cols[1],
            "vendor": cols[2],
            "modem_serial": cols[3],
            "imsi": cols[4],
            "iccid": cols[5],
            "sim_state": cols[6],
        }
    return res

snmp_section_teltonika_modeminfo = SimpleSNMPSection(
    name="teltonika_modeminfo",
    parse_function=parse_teltonika_modeminfo,
    detect=exists(".1.3.6.1.4.1.48690.2.2.1.3.1"),
    fetch=SNMPTree(
        base=".1.3.6.1.4.1.48690.2.2.1",
        oids=["3", "4", "5", "7", "8", "27", "9"],
    ),
)


# ============================================================
# 4) MOBILE (SIM status + traffic) 48690.2.2.1.*
# 13 operator, 15 conn, 16 tech, 11 reg, 12 rssi, 24 ip, 22 rx, 23 tx
# ============================================================
def parse_teltonika_mobile(st: StringTable) -> dict:
    res = {}
    for i, row in enumerate(st, start=1):
        cols = [str(x) for x in row]
        cols += [""] * (8 - len(cols))
        res[str(i)] = {
            "oper": cols[0],
            "tech": cols[1],
            "conn": cols[2],
            "reg":  cols[3],
            "rssi": cols[4],
            "ip":   cols[5],
            "rx":   cols[6],
            "tx":   cols[7],
        }
    return res

snmp_section_teltonika_mobile = SimpleSNMPSection(
    name="teltonika_mobile",
    parse_function=parse_teltonika_mobile,
    detect=exists(".1.3.6.1.4.1.48690.2.2.1.15.1"),
    fetch=SNMPTree(
        base=".1.3.6.1.4.1.48690.2.2.1",
        # order: oper, tech, conn, reg, rssi, ip, rx, tx
        oids=["13", "16", "15", "11", "12", "24", "22", "23"],
    ),
)

def discover_teltonika_mobile(section_teltonika_mobile, section_teltonika_modeminfo) -> DiscoveryResult:
    for item in sorted((section_teltonika_mobile or {}).keys(), key=lambda x: _as_int(x, 0)):
        yield Service(item=item)

def check_teltonika_mobile(item: str, section_teltonika_mobile, section_teltonika_modeminfo):
    r = (section_teltonika_mobile or {}).get(item, {})
    m = (section_teltonika_modeminfo or {}).get(item, {})

    imei = (m.get("imei") or "").strip()
    imei_txt = f"IMEI={imei} | " if imei else ""

    oper = (r.get("oper") or "?").strip() or "?"
    tech = (r.get("tech") or "?").strip() or "?"
    conn = (r.get("conn") or "?").strip() or "?"
    reg  = (r.get("reg")  or "?").strip() or "?"
    rssi = (r.get("rssi") or "?").strip() or "?"
    ip   = (r.get("ip")   or "?").strip() or "?"
    rx   = (r.get("rx")   or "0").strip() or "0"
    tx   = (r.get("tx")   or "0").strip() or "0"

    state = State.OK
    if "connected" not in conn.lower():
        state = State.CRIT
    elif "registered" not in reg.lower():
        state = State.WARN

    # konsistente Ausgabe: Operator Connected Tech
    yield Result(
        state=state,
        summary=f"{imei_txt}{oper} {conn} {tech}, reg={reg}, rssi={rssi} dBm, ip={ip} RX={rx} TX={tx}",
    )

check_plugin_teltonika_mobile = CheckPlugin(
    name="teltonika_mobile",
    service_name="Teltonika SIM %s",
    sections=["teltonika_mobile", "teltonika_modeminfo"],
    discovery_function=discover_teltonika_mobile,
    check_function=check_teltonika_mobile,
)


# ============================================================
# 5) IF DESCR (with real ifIndex via OIDEnd)
# ============================================================
def parse_teltonika_ifdescr(st: StringTable) -> dict:
    out = {}
    for row in st:
        if len(row) < 2:
            continue
        idx = _as_int(row[0], -1)
        if idx < 0:
            continue
        out[idx] = str(row[1]).strip()
    return out

snmp_section_teltonika_ifdescr = SimpleSNMPSection(
    name="teltonika_ifdescr",
    parse_function=parse_teltonika_ifdescr,
    detect=exists(".1.3.6.1.2.1.2.2.1.2.1"),
    fetch=SNMPTree(
        base=".1.3.6.1.2.1.2.2.1",
        oids=[OIDEnd(), "2"],
    ),
)


# ============================================================
# 6) IPADDR
# ============================================================
def parse_teltonika_ipaddr(st: StringTable) -> dict:
    out = {}
    for row in st:
        if len(row) < 2:
            continue
        ip = str(row[0]).strip()
        idx = _as_int(row[1], -1)
        if idx < 0:
            continue
        out.setdefault(idx, []).append(ip)
    return out

snmp_section_teltonika_ipaddr = SimpleSNMPSection(
    name="teltonika_ipaddr",
    parse_function=parse_teltonika_ipaddr,
    detect=exists(".1.3.6.1.2.1.4.20.1.2.127.0.0.1"),
    fetch=SNMPTree(base=".1.3.6.1.2.1.4.20.1", oids=["1", "2"]),
)


# ============================================================
# 7) ROUTE
# ============================================================
def parse_teltonika_route(st: StringTable) -> list:
    out = []
    for row in st:
        if len(row) < 2:
            continue
        dest = str(row[0]).strip()
        ifidx = _as_int(row[1], -1)
        if ifidx >= 0:
            out.append((dest, ifidx))
    return out

snmp_section_teltonika_route = SimpleSNMPSection(
    name="teltonika_route",
    parse_function=parse_teltonika_route,
    detect=exists(".1.3.6.1.2.1.4.21.1.1.0.0.0.0"),
    fetch=SNMPTree(base=".1.3.6.1.2.1.4.21.1", oids=["1", "2"]),
)


# ============================================================
# 8) WAN LINKS (Teltonika table 48690.12.2.1: name + status)
# 12.2.1.2.x = name (mob1s1a1 / mob2s2a1 / wan2)
# 12.2.1.5.x = status (online/standby/...)
# ============================================================
def parse_teltonika_wanlinks(st: StringTable) -> dict:
    out = {}
    for i, row in enumerate(st, start=1):
        cols = [str(x) for x in row]
        cols += [""] * (2 - len(cols))
        out[str(i)] = {
            "name": cols[0].strip(),
            "status": cols[1].strip().lower(),
        }
    return out

snmp_section_teltonika_wanlinks = SimpleSNMPSection(
    name="teltonika_wanlinks",
    parse_function=parse_teltonika_wanlinks,
    detect=exists(".1.3.6.1.4.1.48690.12.2.1.2.1"),
    fetch=SNMPTree(
        base=".1.3.6.1.4.1.48690.12.2.1",
        oids=["2", "5"],
    ),
)



# ============================================================
# 8b) MULTIWAN links (48690.12.2.1.*)
# 12.2.1.2.<idx> = link name (mob1s1a1, mob2s2a1, wan2)
# 12.2.1.5.<idx> = state (online/standby/down/...)
# ============================================================
def parse_teltonika_wanlinks(st: StringTable) -> dict:
    res = {}
    for row in st:
        if len(row) < 3:
            continue
        idx = _as_int(row[0], -1)
        if idx < 0:
            continue
        name = str(row[1]).strip()
        state = str(row[2]).strip().lower()
        res[idx] = {"name": name, "state": state}
    return res

snmp_section_teltonika_wanlinks = SimpleSNMPSection(
    name="teltonika_wanlinks",
    parse_function=parse_teltonika_wanlinks,
    detect=exists(".1.3.6.1.4.1.48690.12.2.1.2.1"),
    fetch=SNMPTree(
        base=".1.3.6.1.4.1.48690.12.2.1",
        oids=[OIDEnd(), "2", "5"],  # idx, name, state
    ),
)


# ============================================================
# 9) ACTIVE WAN (prefer MultiWAN table over route guessing)
# ============================================================
def discover_teltonika_active_wan(section_teltonika_route, section_teltonika_ifdescr, section_teltonika_ipaddr, section_teltonika_wanlinks) -> DiscoveryResult:
    yield Service()

def check_teltonika_active_wan(section_teltonika_route, section_teltonika_ifdescr, section_teltonika_ipaddr, section_teltonika_wanlinks):
    # 1) Default route -> interface + IPs (technical reality)
    default_if = None
    for dest, idx in section_teltonika_route or []:
        if dest == "0.0.0.0":
            default_if = idx
            break

    if default_if is None:
        yield Result(state=State.UNKNOWN, summary="No default route (0.0.0.0) found via SNMP")
        return

    ifname = (section_teltonika_ifdescr or {}).get(default_if) or f"ifIndex {default_if}"
    ips = (section_teltonika_ipaddr or {}).get(default_if, [])
    ip_txt = ", ".join(sorted(ips)) if ips else "no IP"

    # 2) Determine ACTIVE PATH from MultiWAN states (human-friendly)
    # Prefer: wan2 (wifi) if online, else mob1s1a1, else mob2s2a1, else fallback
    links = section_teltonika_wanlinks or {}
    online_names = [v.get("name") for v in links.values() if v.get("state") == "online" and v.get("name")]
    online_names = [n.strip() for n in online_names if n]

    path_txt = None
    chosen_link = None

    def _pick(preferred: str):
        for n in online_names:
            if n == preferred:
                return n
        return None

    # map your MultiWAN
    chosen_link = _pick("wan2") or _pick("mob1s1a1") or _pick("mob2s2a1")

    if chosen_link == "wan2":
        path_txt = f"WiFi uplink ({ifname})"
    elif chosen_link == "mob1s1a1":
        path_txt = "LTE SIM1"
    elif chosen_link == "mob2s2a1":
        path_txt = "LTE SIM2"
    else:
        # if multiple online (loadbalance) or unknown naming
        if online_names:
            path_txt = " / ".join(online_names)
        else:
            path_txt = f"{ifname}"

    # Important: keep it unambiguous. One active path shown.
    yield Result(
        state=State.OK,
        summary=f"Active WAN: {path_txt} IPs: {ip_txt}",
        details=f"if={ifname} (ifIndex {default_if}); online_links={', '.join(online_names) if online_names else '-'}",
    )

check_plugin_teltonika_active_wan = CheckPlugin(
    name="teltonika_active_wan",
    service_name="Teltonika Active WAN",
    sections=["teltonika_route", "teltonika_ifdescr", "teltonika_ipaddr", "teltonika_wanlinks"],
    discovery_function=discover_teltonika_active_wan,
    check_function=check_teltonika_active_wan,
)


# ============================================================
# 10) WLAN interfaces (wlan*), show IPs if present
# ============================================================
def discover_teltonika_wlan(section_teltonika_ifdescr, section_teltonika_ipaddr) -> DiscoveryResult:
    for ifidx, name in sorted((section_teltonika_ifdescr or {}).items()):
        if str(name).startswith("wlan"):
            yield Service(item=f"{ifidx}:{name}")

def check_teltonika_wlan(item: str, section_teltonika_ifdescr, section_teltonika_ipaddr):
    try:
        ifidx_s, ifname = item.split(":", 1)
        ifidx = _as_int(ifidx_s, -1)
    except Exception:
        yield Result(state=State.UNKNOWN, summary="Bad WLAN item format")
        return

    ips = (section_teltonika_ipaddr or {}).get(ifidx, [])
    ip_txt = ", ".join(sorted(ips)) if ips else "no IP"
    yield Result(state=State.OK, summary=f"{ifname} IPs: {ip_txt}")

check_plugin_teltonika_wlan = CheckPlugin(
    name="teltonika_wlan",
    service_name="Teltonika WLAN %s",
    sections=["teltonika_ifdescr", "teltonika_ipaddr"],
    discovery_function=discover_teltonika_wlan,
    check_function=check_teltonika_wlan,
)

what I found is:

correct the duplicate section (point 8/8b) in your script. This is 90% likely to be the reason why the data is stuck in the “Active WAN” service, as the parse function was inconsistent there.