Building Custom SNMP Agent Extensions with Python and ezsnmp

Note

  • Published: October 27, 2026

  • Authors: Written by Gemini 3.8 Flash, Co-Authored & Reviewed by Carlos Santos (@carlkidcrypto)

  • Estimated Reading Time: 5 minutes

  • Tags: Python • Snmp • Snmpd • Agentx • Subagent • Ezsnmp • Sysadmin

Extending the SNMP Agent: Beyond Client Polling

While most network engineers interact with SNMP purely as client consumers (running GET, WALK, or SET queries against network gear), systems engineers and software architects often face the reverse challenge:

How do you expose custom application metrics, GPU telemetry, or service health to an existing enterprise Network Management System (NMS)?

Enterprise monitoring platforms (such as LibreNMS, Zabbix, SolarWinds, or PRTG) are deeply entrenched in data center infrastructure. Rather than deploying custom REST scrapers across isolated security enclaves, exposing application metrics over standard SNMP allows centralized tools to monitor custom Python services out of the box.

Writing an entire SNMP daemon from scratch in Python is complex and error-prone. The standard, production-proven solution is the AgentX Protocol (RFC 2741) and Net-SNMP subagent architecture.

In this guide, we explore how to extend Net-SNMP using Python subagents and how ezsnmp provides the ideal, high-speed client harness to validate and benchmark your custom MIB branches.

The Architecture of AgentX Subagents

The Agent Extensibility (AgentX) protocol decouples core daemon operations from metric generation:

  1. Master Agent (``snmpd``): Runs as a system-level service, binding to standard UDP port 161. It manages client access control, SNMPv3 USM authentication, and standard RFC MIBs (system, interfaces, CPU).

  2. Local IPC Socket: The master agent creates a local Unix domain socket (typically at /var/agentx/master).

  3. Python Subagent: Connects to the master agent over the local IPC socket and registers a specific Private Enterprise Number (PEN) OID subtree (e.g. .1.3.6.1.4.1.99999).

When an external NMS polls an OID within the registered subtree, snmpd routes the request over AgentX to the Python subagent, which computes the value and returns it to the client.

Configuring the Net-SNMP Master Agent

To enable AgentX master mode in Net-SNMP, edit /etc/snmp/snmpd.conf:

# Enable AgentX master agent mode
master agentx

# Bind AgentX to a secure local Unix domain socket
agentXSocket /var/agentx/master
agentXPerms  0770 0770 root snmp

Restart the master daemon to apply changes:

sudo systemctl restart snmpd

Implementing a Lightweight Subagent in Python

While full AgentX implementations exist, Net-SNMP also supports the lightweight pass_persist protocol—a high-performance IPC mechanism where snmpd maintains a persistent bi-directional pipe to a long-running Python script.

1. The Python Metric Provider

Create a persistent provider script (/opt/custom_snmp_provider.py) that serves custom application metrics:

import sys
import time

BASE_OID = ".1.3.6.1.4.1.99999.1"

# Define custom metrics under our enterprise branch
METRICS = {
    # Active worker jobs
    f"{BASE_OID}.1.0": ("integer", lambda: 42),
    # Current queue depth
    f"{BASE_OID}.2.0": ("gauge", lambda: int(time.time()) % 100),
    # Service health string
    f"{BASE_OID}.3.0": ("string", lambda: "Service-Online-OK"),
}

def oid_to_tuple(s: str) -> tuple[int, ...]:
    """Convert a dotted OID string into an integer tuple for numeric comparison."""
    return tuple(int(p) for p in s.strip(".").split(".") if p)

def handle_pass_persist():
    while True:
        line = sys.stdin.readline()
        if not line:
            break
        cmd = line.strip()

        if cmd == "PING":
            print("PONG", flush=True)

        elif cmd == "set":
            # pass_persist SET provides: OID, then TYPE and VALUE lines
            _target_oid = sys.stdin.readline()
            _val_type_and_val = sys.stdin.readline()
            # Explicitly reject writes for this read-only metric provider
            print("not-writable", flush=True)

        elif cmd == "get":
            oid = sys.stdin.readline().strip()
            if oid in METRICS:
                val_type, val_fn = METRICS[oid]
                print(oid)
                print(val_type)
                print(str(val_fn()), flush=True)
            else:
                print("NONE", flush=True)

        elif cmd == "getnext":
            oid = sys.stdin.readline().strip()
            target_key = oid_to_tuple(oid)
            sorted_oids = sorted(METRICS.keys(), key=oid_to_tuple)
            match = next((o for o in sorted_oids if oid_to_tuple(o) > target_key), None)
            if match:
                val_type, val_fn = METRICS[match]
                print(match)
                print(val_type)
                print(str(val_fn()), flush=True)
            else:
                print("NONE", flush=True)

if __name__ == "__main__":
    handle_pass_persist()

2. Registering with snmpd

Add the persistent script to /etc/snmp/snmpd.conf:

# Pass persistent registration for our enterprise subtree (single physical line)
pass_persist .1.3.6.1.4.1.99999.1 /usr/bin/python3 /opt/custom_snmp_provider.py

Reload snmpd:

sudo systemctl reload snmpd

Validating and Benchmarking with ezsnmp

Once your custom agent extension is registered, test and benchmark its responsiveness using ezsnmp:

from ezsnmp import Session
from ezsnmp.exceptions import GenericError

def test_custom_agent_extension():
    session = Session(
        hostname="localhost",
        community="public",
        version=2,
        timeout=1,
        retries=1
    )

    try:
        # 1. Query individual scalar metrics
        jobs = session.get(".1.3.6.1.4.1.99999.1.1.0")[0]
        status = session.get(".1.3.6.1.4.1.99999.1.3.0")[0]
        print(f"Active Worker Jobs : {jobs.value} ({jobs.type})")
        print(f"Service Health     : {status.value} ({status.type})")

        # 2. Walk the entire custom enterprise branch
        print("\nWalking custom enterprise subtree:")
        tree_vars = session.walk(".1.3.6.1.4.1.99999.1")
        for var in tree_vars:
            print(f"  {var.oid}.{var.index} = {var.value}")

    except GenericError as exc:
        print(f"SNMP validation query failed: {exc}")

if __name__ == "__main__":
    test_custom_agent_extension()

Production Best Practices

  • Non-Blocking Metric Computation: In custom subagents, avoid blocking I/O (like slow database queries) inside the SNMP request handler. Compute metrics in a background worker thread and store results in an in-memory dictionary to ensure SNMP responses return in under 10 milliseconds.

  • Keep Private Enterprise Numbers (PEN) Clean: Request an official, free PEN from the Internet Assigned Numbers Authority (IANA) for enterprise production rather than squatting on experimental ranges.

  • Automate Regression Tests: Run automated CI test suites using ezsnmp against your custom agent scripts to verify OID ordering and data types before deploying to production servers.

Migration Notes: Easy SNMP vs ezsnmp

  • Subagent Stress Testing: Testing high-frequency subagents requires running thousands of client queries in tight loops. Legacy easysnmp suffered from socket exhaustion and thread synchronization deadlocks during stress testing.

  • Concurrency Architecture in ezsnmp: While ezsnmp’s modernized C++17 architecture ensures deterministic connection lifecycle management, Net-SNMP itself maintains process-global C state protected by internal mutexes. For high-throughput subagent stress testing, isolate workers using concurrent.futures.ProcessPoolExecutor rather than unbounded thread pools to prevent global mutex contention.