Netmiko sends commands and reads text. That is perfect when you know each platform’s CLI. But what if you want to ask “give me the interfaces” and get 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 when you operate a multi-vendor network or when you want config changes you can preview and undo.
Install and the Driver Concept
pip install napalm
NAPALM uses drivers — one per platform. You ask for 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 you get the same keys — 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. You 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 for you.
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()
Read that flow again, because it is a different mindset from Netmiko. You never blindly push. You stage, you see the diff, you decide. For change windows and peer review, compare_config() is gold — you can paste the exact diff into the change ticket.
Merge vs. Replace
Two ways to load config, and the difference matters enormously:
load_merge_candidate()— adds your lines to the existing config. Like typing commands at the CLI. Safe and incremental.load_replace_candidate()— replaces the entire running config with the file you provide. Powerful for enforcing a golden config, dangerous if your file is incomplete. The diff preview is your seatbelt here.
# Enforce a full golden config from a file, but LOOK before you leap
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
If 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 dream: one report across vendors, written once. No if device_type == ... anywhere.
Netmiko or NAPALM? A Quick Rule
Use Netmiko when you need arbitrary CLI commands, vendor-specific features, or fine control over exactly what is sent. Use NAPALM when you want 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
- Warm-up. Open a connection with the
iosdriver, print the hostname and model fromget_facts(), and close cleanly withtry/finally. - Interface report. Use
get_interfaces()to print a table of interface name, up/down, and description. - 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). - BGP check. Use
get_bgp_neighbors()to print each neighbor IP and whether the session is up. - 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 lets you sleep during a change window — and a fitting end to a week of talking to real devices.
Previously: Multi-Vendor SSH with Netmiko. Coming next week we move up the stack — YAML and JSON for inventories and configs, then Jinja2 templating, REST APIs, NETCONF, TextFSM, and two end-to-end mini-projects.