NAPALM: One API for Cisco, Arista, and Juniper

Netmiko sends commands and reads text. That is perfect when each platform’s CLI is familiar. But consider asking “give me the interfaces” and getting the same structured answer whether the device is a Cisco IOS-XE router, an Arista switch, or a Juniper firewall — without learning three command syntaxes. That is NAPALM: Network Automation and Programmability Abstraction Layer with Multivendor support. One Python API, many vendors, structured data out, and — the headline feature — safe configuration changes with diffs and rollback.

NAPALM sits one level of abstraction above Netmiko. It is the right tool for a multi-vendor network, or for config changes that need to be previewed and undone.

Install and the Driver Concept

pip install napalm

NAPALM uses drivers — one per platform. Request a driver by name, then open a connection:

from napalm import get_network_driver

driver = get_network_driver("ios")        # ios, eos, junos, nxos, iosxr
device = driver(
    hostname="192.168.1.1",
    username="admin",
    password="cisco123",
    optional_args={"secret": "enablepass"},
)

device.open()
print("Connected")
device.close()

Supported drivers cover the big four: ios, eos (Arista), junos (Juniper), nxos (Nexus), plus iosxr. The point is that everything below works identically across all of them.

Getters: Structured Facts, Same Shape Everywhere

NAPALM’s “getters” are read methods that return normalized dictionaries. The same call returns the same structure regardless of vendor:

device.open()

facts = device.get_facts()
print(facts["hostname"], facts["model"], facts["os_version"])
print(facts["uptime"], "seconds")

interfaces = device.get_interfaces()
for name, data in interfaces.items():
    state = "up" if data["is_up"] else "down"
    print(f"{name}: {state}, MAC {data['mac_address']}")

device.close()

Run the exact same code against an Arista box (driver eos) and the keys are identical — is_up, mac_address, description. That normalization is the whole value proposition. Other common getters: get_interfaces_ip(), get_bgp_neighbors(), get_lldp_neighbors(), get_arp_table(), get_environment().

The Killer Feature: Config Diff and Rollback

This is why people adopt NAPALM. Load a candidate configuration, ask NAPALM what would change, and only then commit — or discard. On platforms without native candidate configs, NAPALM emulates the workflow automatically.

device.open()

# Stage a config change (merge = add to running config)
device.load_merge_candidate(config="ntp server 10.0.0.1\nlogging host 10.0.0.50")

# Preview EXACTLY what will change - before touching anything
diff = device.compare_config()
print(diff)
# +ntp server 10.0.0.1
# +logging host 10.0.0.50

if diff:
    device.commit_config()      # apply it
    print("Committed")
else:
    device.discard_config()     # nothing to do
    print("No changes")

device.close()

That flow is worth reading twice, because it is a different mindset from Netmiko. Nothing gets pushed blindly. Stage the change, see the diff, decide. For change windows and peer review, compare_config() is gold — the exact diff drops straight into the change ticket.

Merge vs. Replace

Two ways to load config, and the difference matters enormously:

  • load_merge_candidate() — adds new lines to the existing config. Like typing commands at the CLI. Safe and incremental.
  • load_replace_candidate() — replaces the entire running config with the supplied file. Powerful for enforcing a golden config, dangerous if that file is incomplete. The diff preview is the seatbelt here.
# Enforce a full golden config from a file, but LOOK before leaping
device.load_replace_candidate(filename="golden_r1.cfg")
print(device.compare_config())     # review every add AND removal
# commit only if the diff is what you expect

Rollback

When a committed change turns out wrong, NAPALM can roll back to the previous config on platforms that support it:

device.rollback()     # revert the last commit

Cisco Context: A Multi-Vendor Health Snapshot

Because getters are normalized, one function can inventory a mixed fleet — IOS and EOS side by side — with zero per-vendor branching:

from napalm import get_network_driver

fleet = [
    {"driver": "ios", "host": "192.168.1.1"},
    {"driver": "eos", "host": "192.168.1.10"},
]
USER, PW = "admin", "cisco123"

for entry in fleet:
    drv = get_network_driver(entry["driver"])
    dev = drv(hostname=entry["host"], username=USER, password=PW)
    try:
        dev.open()
        f = dev.get_facts()
        down = [n for n, d in dev.get_interfaces().items() if not d["is_up"]]
        print(f"{f['hostname']:12} {f['model']:14} up {f['uptime']//86400}d, "
              f"{len(down)} interfaces down")
    except Exception as e:
        print(f"{entry['host']}: ERROR {e}")
    finally:
        dev.close()

That is the goal: one report across vendors, written once. No if device_type == ... anywhere.

Netmiko or NAPALM? A Quick Rule

Reach for Netmiko when the job needs arbitrary CLI commands, vendor-specific features, or fine control over exactly what is sent. Reach for NAPALM for normalized facts across vendors, or safe config changes with diff and rollback. Many shops use both — NAPALM for getters and config management, Netmiko for the one weird command NAPALM does not expose. They are friends, not rivals.

Exercises

  1. Warm-up. Open a connection with the ios driver, print the hostname and model from get_facts(), and close cleanly with try/finally.
  2. Interface report. Use get_interfaces() to print a table of interface name, up/down, and description.
  3. Safe change. Stage an NTP server line with load_merge_candidate, print the diff, and commit only if the diff is non-empty (otherwise discard).
  4. BGP check. Use get_bgp_neighbors() to print each neighbor IP and whether the session is up.
  5. Challenge. Write enforce_golden(host, driver, golden_file) that loads a replace candidate from a file, prints the diff, and — only if the diff contains no removal lines (no lines starting with -) — commits; otherwise it discards and warns that the change would remove config. This is a guardrail against an incomplete golden file wiping a device.

Answers

Show answers

1. Warm-up

from napalm import get_network_driver
dev = get_network_driver("ios")(
    hostname="192.168.1.1", username="admin", password="cisco123")
try:
    dev.open()
    f = dev.get_facts()
    print(f["hostname"], f["model"])
finally:
    dev.close()

2. Interface report

dev.open()
for name, d in dev.get_interfaces().items():
    print(f"{name:22} {'up' if d['is_up'] else 'down':5} {d['description']}")
dev.close()

3. Safe change

dev.open()
dev.load_merge_candidate(config="ntp server 10.0.0.1")
diff = dev.compare_config()
print(diff)
if diff:
    dev.commit_config()
else:
    dev.discard_config()
dev.close()

4. BGP check

dev.open()
bgp = dev.get_bgp_neighbors()
for vrf, data in bgp.items():
    for peer, info in data["peers"].items():
        print(f"{peer}: {'up' if info['is_up'] else 'down'}")
dev.close()

5. Challenge

from napalm import get_network_driver

def enforce_golden(host, driver, golden_file):
    dev = get_network_driver(driver)(
        hostname=host, username="admin", password="cisco123")
    try:
        dev.open()
        dev.load_replace_candidate(filename=golden_file)
        diff = dev.compare_config()
        removals = [l for l in diff.splitlines() if l.startswith("-")]
        if removals:
            print(f"{host}: REFUSING — change would remove "
                  f"{len(removals)} lines:")
            print("\n".join(removals))
            dev.discard_config()
        else:
            print(f"{host}: safe additive change, committing")
            dev.commit_config()
    finally:
        dev.close()

Inspecting the diff text for lines starting with - turns NAPALM’s preview into an automated guardrail. This is the kind of safety net that makes a change window restful — and a fitting end to a week of talking to real devices.

This is Day 14 of the 21‑post Python for Network Engineers series.


Previously: Multi-Vendor SSH with Netmiko. Coming next week, the series moves up the stack — YAML and JSON for inventories and configs, then Jinja2 templating, REST APIs, NETCONF, TextFSM, and two end-to-end mini-projects.