NAPALM Network Automation: Getters, Config Diff and Load - 夜莺博客

NAPALM Network Automation: Getters, Config Diff and Load

Writing one script per vendor - SSH to Cisco with one parser, NETCONF to Juniper with another - does not scale. NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor support) gives you one Python API across IOS, IOS-XE, NX-OS, EOS, Junos and more: getters return structured dictionaries instead of raw CLI text, and configuration changes run through a safe load-diff-commit cycle with rollback. This guide covers the NAPALM essentials with working code samples.

Why NAPALM Exists: The Abstraction Problem

Every network operating system speaks a different language. Cisco IOS wants show ip interface brief parsed out of a terminal buffer; Junos wants NETCONF with XML payloads; Arista EOS offers eAPI with JSON. If you write a script per vendor, you own every parser, every pagination quirk and every prompt-detection edge case forever. Multiply that by the number of models in your fleet and the maintenance cost grows faster than the automation saves.

NAPALM inverts that: you write against one Python interface, and a per-vendor driver translates your calls into the right southbound protocol. The same get_facts() call works whether the target is a Catalyst running IOS-XE, an Arista running EOS or a Juniper running Junos. You learn one library, and the vendor-specific knowledge lives in the driver the community maintains, not in your scripts.

Three ideas make NAPALM worth adopting over hand-rolled Netmiko scripts:

  • Structured getters. Instead of scraping text, you get Python dictionaries and lists with stable keys, so downstream code does not break when the CLI output cosmetics change.
  • Transaction semantics for configuration. A candidate configuration is loaded, diffed, committed or rolled back - never applied blindly.
  • Multi-vendor support matrix. The same script can loop over mixed-vendor inventory, since the driver is chosen at runtime from the device's os type.

Installing NAPALM and Opening a Device

pip install napalm

Drivers for the most common platforms ship with the core package. For less common OSs you install the matching extra, for example pip install napalm[optional], or a community driver such as napalm-fortios. Always pin versions in a virtual environment, because NAPALM drivers occasionally change the exact keys they return.

Opening a session is a one-liner once you pick a driver:

import napalm

driver = napalm.get_network_driver("ios")  # or eos, junos, nxos, iosxr
with driver(hostname="192.168.1.1", username="admin",
            password="cisco123", optional_args={"port": 22}) as device:
    facts = device.get_facts()
    print(facts["hostname"], facts["model"], facts["os_version"])

The context manager closes the connection automatically. Behind the scenes NAPALM uses Netmiko over SSH for IOS-style platforms and NETCONF for Junos - you do not need to care which, the API is identical.

The optional_args dictionary is where driver-specific behaviour lives. A few that matter in practice:

  • port - non-standard SSH or NETCONF port.
  • secret - the enable password for platforms that need privilege escalation.
  • conn_timeout and timeout - raise these on slow devices or WAN links.
  • global_delay_factor - multiply every wait when a switch is heavily loaded.
  • netconf_read_timeout - useful on Junos when large configuration trees are read.

You can also lean on the context manager for the whole lifecycle, or call device.open() and device.close() manually if you want to reuse a session across many operations.

Reading State with Getters

Getters are the read side of the API. Each returns a native Python structure - dictionaries keyed by name, lists of records - not screen-scraped text:

with driver(hostname="10.0.0.11", username="net", password="secret") as d:
    d.open()
    bgp = d.get_bgp_neighbors()
    interfaces = d.get_interfaces()
    vlans = d.get_vlans()
    config = d.get_config()
    print(config["running"])

Useful getters include get_facts(), get_interfaces(), get_lldp_neighbors(), get_arp_table(), get_mac_address_table(), get_bgp_neighbors() and get_environment() for power/fan/temperature. Getters that a driver does not implement raise NotImplementedError - check the support matrix and handle it.

Wrapping a bulk read in a defensive loop is the difference between a script that survives a mixed fleet and one that dies on switch number three:

def safe_getter(device, name, *args, **kwargs):
    """Return the getter result or None if the driver lacks it."""
    try:
        return getattr(device, name)(*args, **kwargs)
    except NotImplementedError:
        print(f"{device.hostname}: {name} not supported by {device.driver_name}")
        return None

with driver(hostname="10.0.0.11", username="net", password="secret") as d:
    interfaces = safe_getter(d, "get_interfaces")
    environment = safe_getter(d, "get_environment")
    arp = safe_getter(d, "get_arp_table")

The shape of the returned data is stable enough to index directly. get_interfaces() returns a mapping of interface name to a dictionary with is_up, is_enabled, speed, mtu, mac_address and more; get_bgp_neighbors() nests by address family and peer. That predictability is what lets you build reports, feed monitoring or compare against a source of truth without writing a parser.

Persisting and Diffing Configurations

The get_config() getter returns a dictionary with up to three keys - running, candidate and startup - depending on what the platform exposes. By default it asks for the running configuration in text form, which is exactly what you want for backups:

with driver(hostname="10.0.0.11", username="net", password="secret") as d:
    running = d.get_config(retrieve="running")["running"]
    startup = d.get_config(retrieve="startup")["startup"]

Backing up is trivial once you can read the configuration, and diffing yesterday against today with Python's standard library gives you a change log almost for free:

import difflib

def config_diff(old, new):
    return "\n".join(difflib.unified_diff(
        old.splitlines(), new.splitlines(),
        fromfile="yesterday", tofile="today", lineterm=""))

# old_running was saved to disk on the previous run
print(config_diff(old_running, running))

This pattern is the backbone of configuration drift detection. Store a golden copy per device, grab the live running config on a schedule, and any difference is either authorised change or unauthorised drift. Because NAPALM normalises the retrieval, the same script works against every vendor in the inventory - only the saved text differs, not your code.

Some drivers can also return the configuration in structured JSON with get_config(retrieve="running", full=True) or by passing a format flag, which lets you assert on individual configuration nodes rather than grepping text. When that is available it is far more robust than string matching, but not every driver supports it, so fall back to text comparison when it raises.

Configuration Changes: Load, Diff, Commit, Rollback

NAPALM treats config changes like a transaction - nothing is applied until you commit:

with driver(hostname="10.0.0.11", username="net", password="secret") as d:
    d.open()
    d.load_merge_candidate(filename="snmp.cfg")   # overlay changes
    # d.load_replace_candidate(filename="full.cfg")  # full replace
    print(d.compare_config())   # review the diff BEFORE committing
    d.commit_config()           # atomic apply
    # d.rollback()              # revert the last commit if needed

load_merge_candidate overlays only your snippet; load_replace_candidate makes the device end in exactly the supplied state and can wipe unrelated configuration if the candidate is incomplete - always run compare_config() first. On supported platforms rollback() reverts the last commit, and discard_config() throws away an uncommitted candidate.

You can feed the candidate from a string instead of a file with load_merge_candidate(config="..."), which is handy when you are generating configuration programmatically from a template or an inventory:

candidate = """
hostname R1
interface Loopback0
 ip address 10.10.10.1 255.255.255.255
"""
with driver(hostname="10.0.0.11", username="net", password="secret") as d:
    d.load_merge_candidate(config=candidate)
    diff = d.compare_config()
    if diff:
        print("Proposed change:\n" + diff)
        d.commit_config()
    else:
        print("Device already matches the desired state")
        d.discard_config()

The if diff guard is the idempotency check: if compare_config() returns an empty string the device already matches, so committing would be a no-op and you should discard the candidate instead. That single pattern turns an imperative push into a safe, repeatable, declarative operation.

Note that the merged snippet for IOS-style platforms is expressed as running-config lines, so a merge candidate for a VLAN looks like the CLI you would type, just without the prompt:

vlan 30
 name server_vlan
vlan 31
 name user_vlan

Junos is different - the merge candidate is the set form or the hierarchy form the driver expects - but because NAPALM abstracts the call, only the content of the string changes, not your control flow.

Validation and Compliance

NAPALM includes a lightweight validation helper that compares live device state against expected values, useful for post-change verification and periodic auditing:

report = d.compliance_report("compliance.yml")
print(report["complies"])          # True / False
print(report["skipped"])           # checks the driver could not run
print(report["compliance"])        # per-rule pass/fail detail

The rules file is simple YAML describing expected values, for example a required NTP server, a banned SNMP community or a mandatory BGP peer. Running this nightly across the fleet turns compliance from a manual audit into an automated gate. The same result can be captured with d.validate() when the driver exposes the underlying mechanism, but compliance_report() is the simpler, vendor-neutral entry point.

Parallel Execution: Nornir and Ansible

NAPALM itself is synchronous and per-device. For fleet-scale operations you pair it with an orchestrator rather than writing your own threading layer. Nornir is the natural fit: it builds inventory, runs NAPALM tasks in parallel and collects results, so a hundred-device configuration push completes in the time a handful used to take. The task body is the same load_merge_candidate / compare_config / commit_config sequence, only now executed by a worker pool.

from nornir import InitNornir
from nornir_napalm.plugins.tasks import napalm_get, napalm_configure

nr = InitNornir(config_file="config.yaml")
result = nr.run(task=napalm_get, getters=["facts", "interfaces"])
for host, task_result in result.items():
    print(host, task_result[0].result["facts"]["model"])

If your team already lives in Ansible, the napalm.napalm collection exposes the same getters, diff and validation as modules, so you get NAPALM's vendor abstraction inside inventory-driven playbooks without writing Python at all. Choose Ansible when the change is a one-off playbook, Nornir when you need programmatic control over concurrency and error handling.

Error Handling and Operational Notes

Network devices fail in ways servers do not: a configuration commit can drop the SSH session, a slow control plane can time out mid-read and an interface flap can invalidate your assumption halfway through a loop. A production NAPALM script therefore treats every call as failable:

from napalm.base.exceptions import (
    ConnectionException, MergeConfigException, CommitError)

try:
    with driver(hostname="10.0.0.11", username="net", password="secret") as d:
        d.load_merge_candidate(filename="snmp.cfg")
        if d.compare_config():
            d.commit_config()
except ConnectionException as e:
    print("Could not reach the device:", e)
except (MergeConfigException, CommitError) as e:
    print("Configuration rejected, device left unchanged:", e)

When a commit fails, NAPALM raises rather than silently leaving the device half-configured, and the uncommitted candidate can be discarded - the transaction model protects you. Log the compare_config() output before every commit so that your audit trail shows exactly what changed, and always retrieve a fresh running config immediately after a commit to confirm the intended state was reached.

Finally, keep credentials out of the script. Read them from environment variables or a secrets manager, and pass them into the driver constructor rather than hard-coding them, so the same code is safe to commit to version control.

Beyond Basics

NAPALM also offers get_network_driver("junos") NETCONF-based flows, ping()/traceroute() helpers, and a compliance_report() that validates device state against a source of truth. Pair it with Nornir for parallel execution across the fleet, or with Ansible via the napalm.napalm collection for inventory-driven playbooks.

Related Guides on This Site

Start with the foundations: Netmiko for SSH automation, Ansible multi-vendor command modules, and NETCONF vs RESTCONF vs gNMI to choose the right northbound API.

原文链接:https://napalm.readthedocs.io/en/latest/tutorials/samples.html