Dynamic Inventory Sync: Populating NetBox & Nautobot via ezsnmp

Note

  • Published: November 10, 2026

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

  • Estimated Reading Time: 5 minutes

  • Tags: Python • Snmp • Netbox • Nautobot • Netdevops • Ezsnmp • Inventory

The Challenge of Operational Drift in Source of Truth

Modern NetDevOps workflows center on a digital Source of Truth (SoT) such as NetBox or Nautobot. These platforms define the intended state of device hardware, interface configurations, IP allocations, and serial numbers.

However, operational reality frequently drifts from documented intent:

  • Hardware RMAs: Failed switches or linecards replaced during outages without updated serial numbers in the inventory database.

  • Transceiver Moves: SFP+ and QSFP modules swapped between ports, altering interface speeds and optical capabilities.

  • Firmware Maintenance: Operating systems patched during maintenance windows without synchronizing version records.

Auditing this operational state via SSH screen-scraping (Netmiko/Scrapli) or heavy vendor REST APIs across thousands of switches requires privileged credentials, strains switch control-plane CPUs, and takes hours to complete.

ezsnmp provides an ultra-fast, read-only discovery engine. By querying standard MIBs over SNMP, automation pipelines can extract serial numbers, interface MACs, and firmware versions across an entire data center in minutes, reconciling SoT drift with zero CLI overhead.

The Hardware Discovery MIB Architecture

Two standard IETF MIBs provide complete hardware and operational inventory:

1. ENTITY-MIB (RFC 6933)

The entPhysicalTable (OID .1.3.6.1.2.1.47.1.1.1) catalogs physical components within a device chassis:

  • ``entPhysicalClass``: Component category: chassis(3), module(9), port(10), powerSupply(6), or fan(7).

  • ``entPhysicalName``: Component label (e.g. Chassis or Slot 1).

  • ``entPhysicalModelName``: Exact vendor hardware part number.

  • ``entPhysicalSerialNum``: Factory hardware serial number.

  • ``entPhysicalSoftwareRev``: Installed operating system or firmware.

2. IF-MIB (RFC 2863)

The ifXTable tracks physical interface parameters:

  • ``ifName``: Canonical port name (e.g. HundredGigE1/0/1).

  • ``ifPhysAddress``: Hardware MAC address (binary octet string).

  • ``ifHighSpeed``: Interface speed in Megabits per second (Mbps).

  • ``ifMtu``: Configured Maximum Transmission Unit size.

Practical Implementation: NetBox Reconciliation Script

Below is a complete script demonstrating how to collect hardware identity via ezsnmp, query NetBox via pynetbox, and detect configuration drift:

import pynetbox
from ezsnmp import Session
from ezsnmp.exceptions import GenericError

NETBOX_URL = "https://netbox.internal.net"
NETBOX_TOKEN = "0123456789abcdef0123456789abcdef01234567"

def parse_mac_address(raw_val: str | bytes) -> str:
    """Normalize raw octets or formatted hex strings to standard colon-separated MAC."""
    if isinstance(raw_val, bytes):
        return ":".join(f"{b:02x}" for b in raw_val)
    cleaned = raw_val.replace(":", "").replace("-", "").replace(" ", "")
    try:
        raw_bytes = bytes.fromhex(cleaned)
        if len(raw_bytes) == 6:
            return ":".join(f"{b:02x}" for b in raw_bytes)
    except ValueError:
        pass
    if len(raw_val) == 6:
        return ":".join(f"{ord(c):02x}" for c in raw_val)
    return ""

def discover_device_inventory(
    hostname: str, community: str = "public"
) -> dict:
    """Extract chassis serial and interfaces via ezsnmp."""
    session = Session(
        hostname=hostname,
        community=community,
        version=2,
        timeout=2,
        retries=1,
        print_enums_numerically=True,
        set_max_repeaters_to_num=25
    )

    try:
        # 1. Query system metadata (unpack Result tuple from session.get)
        sys_name = session.get("sysName.0")[0].value
        sys_descr = session.get("sysDescr.0")[0].value

        # 2. Extract Chassis Serial from ENTITY-MIB
        class_vars = session.bulk_walk("entPhysicalClass")
        serial_vars = session.bulk_walk("entPhysicalSerialNum")
        name_vars = session.bulk_walk("entPhysicalName")

        serials = {v.index: v.value for v in serial_vars}
        names = {v.index: v.value for v in name_vars}

        chassis_serial = "UNKNOWN"
        for var in class_vars:
            # Class 3 = chassis (numeric or symbolic)
            if var.value in ("3", "chassis"):
                candidate = serials.get(var.index, "").strip()
                if candidate:
                    chassis_serial = candidate
                    break

        # 3. Collect interface MAC addresses from IF-MIB
        if_names = session.bulk_walk("ifName")
        if_macs = session.bulk_walk("ifPhysAddress")
        mac_map = {
            v.index: parse_mac_address(v.value)
            for v in if_macs
            if parse_mac_address(v.value)
        }

        interfaces = {
            v.value: mac_map.get(v.index, "")
            for v in if_names if v.index in mac_map
        }

        return {
            "hostname": sys_name,
            "serial": chassis_serial,
            "description": sys_descr,
            "interfaces": interfaces
        }

    except GenericError as exc:
        print(f"[{hostname}] Discovery failed: {exc}")
        return {}

def reconcile_with_netbox(discovered: dict):
    """Compare operational reality against NetBox Source of Truth."""
    nb = pynetbox.api(NETBOX_URL, token=NETBOX_TOKEN)
    device = nb.dcim.devices.get(name=discovered["hostname"])

    if not device:
        print(f"Device {discovered['hostname']} not found in NetBox!")
        return

    print(f"\n--- Audit Report: {device.name} ---")

    # Serial number drift detection
    disc_serial = discovered.get("serial", "UNKNOWN")
    if disc_serial in ("", "UNKNOWN"):
        print(
            f"  [INCONCLUSIVE] Chassis serial could not be discovered via SNMP;\n"
            f"    preserving NetBox serial {device.serial} without overwriting."
        )
    elif device.serial != disc_serial:
        print(
            f"  [DRIFT DETECTED] Serial mismatch!\n"
            f"    NetBox Intent : {device.serial}\n"
            f"    Actual Switch : {disc_serial}"
        )
        # Reconcile serial if authorized
        # device.serial = disc_serial
        # device.save()
    else:
        print(f"  [OK] Serial number matches: {device.serial}")

if __name__ == "__main__":
    inv = discover_device_inventory("edge-leaf01.internal")
    if inv:
        reconcile_with_netbox(inv)

Production Best Practices

  • Dry-Run Audit Before Writing: Never blindly overwrite Source of Truth records during discovery. Always generate a structured diff and route changes through an approval workflow or review gate.

  • Filter Chassis from Components: Switches report dozens of physical serials (power supplies, fan trays, transceivers). Always check that entPhysicalClass == 3 to identify the root chassis serial.

  • Binary Safe MAC Parsing: Physical interface MAC addresses in ifPhysAddress are returned as raw 6-byte octet strings. Encode with latin1 and format with f"{b:02x}" to construct standardized colon- separated MAC strings.

Migration Notes: Easy SNMP vs ezsnmp

  • Concurrency Architecture in ezsnmp: While ezsnmp’s C++17 session architecture ensures deterministic connection cleanup, Net-SNMP itself retains process-global C state protected by internal mutexes. For large multi-device crawls, isolate workers using concurrent.futures.ProcessPoolExecutor rather than large thread pools to prevent global lock contention.

  • Binary Octet String Preservation: Correctly handles raw binary strings in ifPhysAddress without truncation or encoding faults.