Python NAPALM — Multi-Vendor Network Automation
Every vendor speaks a slightly different CLI dialect. A script that
retrieves interface statistics from a Cisco IOS router with
show interfaces needs to be rewritten entirely for a
Juniper or Arista device. NAPALM (Network Automation and
Programmability Abstraction Layer with Multivendor support)
solves this by providing a single, unified Python API that works
identically across Cisco IOS, IOS-XE, IOS-XR, NX-OS, Junos, EOS,
and more. A call to get_interfaces() returns the same
Python dictionary structure regardless of the vendor underneath —
your automation logic never changes, only the driver name does.
Beyond read-only getters, NAPALM implements a candidate configuration workflow — load a change, preview the diff against the running config, commit only when satisfied, and roll back automatically on error. This is safer than issuing raw CLI commands because it gives you a preview before anything changes on the device.
This lab uses Cisco IOS as the target platform. For the lower-level SSH connection layer NAPALM uses internally, see Python Netmiko Show Commands. For the REST-based alternative to SSH-driven automation, see Cisco RESTCONF Basics. For Ansible playbooks that call NAPALM modules see Ansible IOS Configuration. For enabling SSH on the Cisco device that NAPALM connects to, see SSH Configuration.
1. NAPALM Architecture — Core Concepts
How NAPALM Works
NAPALM sits between your Python script and the network device. Your code calls a driver-agnostic method; NAPALM translates that into the appropriate CLI commands or API calls for the target platform, parses the raw output, and returns a normalised Python dictionary. You never see the CLI scraping — only clean structured data:
Your Python Script
│
│ driver.get_facts() ← same call for any vendor
▼
NAPALM Driver (ios / junos / eos / nxos / iosxr)
│
│ Translates to platform-specific commands:
│ ios: "show version", "show hostname", "show ip interface brief"
│ junos: "show version", "show interfaces terse"
│ eos: "show version", "show interfaces status"
▼
Network Device (SSH / NETCONF / eAPI)
│
▼
Raw CLI / API Output
│
▼
NAPALM Parser (TextFSM / regex / XML)
│
▼
Normalised Python Dict { "hostname": "R1", "vendor": "Cisco", ... }
│
▼
Your Python Script receives identical structure regardless of vendor
Supported Platforms and Driver Names
| Driver Name | Platform | Transport | Notes |
|---|---|---|---|
ios |
Cisco IOS / IOS-XE | SSH (Netmiko) | Most common lab target; used throughout this guide |
iosxr |
Cisco IOS-XR | SSH (Netmiko) | Service provider routers; some getters differ from ios |
nxos_ssh |
Cisco NX-OS | SSH | Use nxos for NX-API (HTTP); nxos_ssh for SSH |
junos |
Juniper Junos | NETCONF | Requires junos-eznc package |
eos |
Arista EOS | eAPI (HTTP) | Requires eAPI enabled on the device |
NAPALM vs Netmiko vs RESTCONF
| Tool | Abstraction Level | Returns | Best For |
|---|---|---|---|
| NAPALM | High — vendor-neutral getters and config workflows | Normalised Python dicts | Multi-vendor environments; config diff/push workflows; inventory audits |
| Netmiko | Low — SSH helper that sends raw CLI and returns raw text | Raw CLI string | Single-vendor; custom show commands; quick ad-hoc scripting |
| RESTCONF | Medium — HTTP calls to YANG model endpoints | JSON / XML structured data | Modern IOS-XE devices; CI/CD pipelines; stateless HTTP integration |
2. Lab Topology & Prerequisites
[Automation Host — Python 3.8+]
IP: 192.168.10.50
│
│ SSH TCP/22
▼
NetsTuts_R1 (Cisco IOS 15.x / IOS-XE)
IP: 192.168.10.1
Hostname: NetsTuts_R1
Username: netauto
Password: Aut0P@ss!
Enable: En@ble99!
NetsTuts_R2 (Cisco IOS — second device for multi-device examples)
IP: 192.168.10.2
Prerequisites on each Cisco device:
ip domain-name netstuts.com
crypto key generate rsa modulus 2048
ip ssh version 2
username netauto privilege 15 secret Aut0P@ss!
line vty 0 4
login local
transport input ssh
3. Step 1 — Install NAPALM
# ── Install NAPALM and the IOS driver dependencies ─────── pip install napalm # ── Verify installation ─────────────────────────────────── python3 -c "import napalm; print(napalm.__version__)" 4.1.0 # ── Install TextFSM for enhanced IOS parsing (recommended) pip install textfsm ntc-templates
pip install napalm installs all built-in drivers
including the ios, iosxr, nxos,
and eos drivers. The junos driver requires
the additional junos-eznc package. TextFSM and
ntc-templates enhance the IOS driver's parsing accuracy for show
commands that NAPALM processes internally — strongly recommended for
production use.
4. Step 2 — Connect to a Device
Every NAPALM script follows the same three-line connection pattern:
get the driver class, instantiate it with connection parameters,
and call open(). Always close the connection in a
finally block to ensure the SSH session is released
even if an exception occurs:
#!/usr/bin/env python3
"""napalm_connect.py — Basic NAPALM connection to Cisco IOS"""
import napalm
# ── Step 1: Get the driver class for the target platform ──
driver = napalm.get_network_driver("ios")
# ── Step 2: Instantiate with connection parameters ────────
device = driver(
hostname="192.168.10.1",
username="netauto",
password="Aut0P@ss!",
optional_args={
"secret": "En@ble99!", # enable password for IOS
"port": 22, # SSH port (default 22)
"conn_timeout": 10, # connection timeout in seconds
"global_delay_factor": 2, # slow device? increase this
}
)
# ── Step 3: Open the connection ───────────────────────────
try:
device.open()
print(f"Connected to {device.hostname}")
# ... getters and config operations go here ...
finally:
device.close()
print("Connection closed")
optional_args dictionary passes driver-specific
parameters through to the underlying transport. For the IOS driver,
secret is the enable password — without it NAPALM cannot
enter privileged EXEC mode and most getters will fail with a
permission error. The global_delay_factor multiplies all
internal timing delays — increase to 2 or 3 for slow lab devices or
high-latency WAN connections. The try/finally pattern
ensures device.close() always runs even if a getter
raises an exception mid-script. See SSH Configuration
for the prerequisite device configuration.
5. Step 3 — Retrieving Data with Getters
NAPALM getters are methods prefixed with get_. Each
returns a normalised Python dictionary or list of dictionaries.
The structure is identical across all supported platforms — the
same key names appear whether the device is Cisco, Juniper, or
Arista.
get_facts() — Device Summary
#!/usr/bin/env python3
"""napalm_facts.py — Retrieve device facts"""
import napalm
import json
driver = napalm.get_network_driver("ios")
device = driver(hostname="192.168.10.1", username="netauto",
password="Aut0P@ss!", optional_args={"secret": "En@ble99!"})
try:
device.open()
facts = device.get_facts()
print(json.dumps(facts, indent=2))
finally:
device.close()
# ── Output ────────────────────────────────────────────────
{
"hostname": "NetsTuts_R1",
"fqdn": "NetsTuts_R1.netstuts.com",
"vendor": "Cisco",
"model": "CSR1000V",
"os_version": "IOS-XE Software, Version 16.09.05",
"serial_number": "9TKUWGKFSTJ",
"uptime": 432000,
"interface_list": [
"GigabitEthernet1",
"GigabitEthernet2",
"Loopback0"
]
}
get_facts() runs show version and
show interfaces internally, parses the output, and
returns a normalised dictionary with eight standard keys. The same
eight keys appear for every supported vendor — hostname,
fqdn, vendor, model,
os_version, serial_number,
uptime (in seconds), and interface_list.
This is ideal for building a multi-vendor device inventory: loop
across a list of hosts, call get_facts() on each, and
write the results to a CSV or database.
get_interfaces() — Interface Status and Counters
interfaces = device.get_interfaces()
print(json.dumps(interfaces, indent=2))
# ── Output (truncated to two interfaces) ─────────────────
{
"GigabitEthernet1": {
"is_up": true,
"is_enabled": true,
"description": "WAN Uplink",
"last_flapped": 432000.0,
"speed": 1000,
"mtu": 1500,
"mac_address": "00:1A:2B:3C:4D:5E"
},
"GigabitEthernet2": {
"is_up": true,
"is_enabled": true,
"description": "LAN",
"last_flapped": 432000.0,
"speed": 1000,
"mtu": 1500,
"mac_address": "00:1A:2B:3C:4D:5F"
}
}
get_interfaces_ip() — IP Address Assignments
ip_info = device.get_interfaces_ip()
print(json.dumps(ip_info, indent=2))
# ── Output ────────────────────────────────────────────────
{
"GigabitEthernet1": {
"ipv4": {
"192.168.10.1": {
"prefix_length": 24
}
}
},
"Loopback0": {
"ipv4": {
"10.0.0.1": {
"prefix_length": 32
}
},
"ipv6": {
"2001:db8::1": {
"prefix_length": 128
}
}
}
}
get_interfaces_ip() uses show ip interface brief
internally. Note the ipv6 key on Loopback0 — NAPALM reports both
IPv4 and IPv6 addresses in the same structure.
get_route_to() — Routing Table Lookup
# ── Check best path to a specific destination ────────
routes = device.get_route_to(destination="0.0.0.0/0")
print(json.dumps(routes, indent=2))
# ── Output ────────────────────────────────────────────────
{
"0.0.0.0/0": [
{
"protocol": "static",
"inactive": false,
"age": "never",
"next_hop": "203.0.113.1",
"outgoing_interface": "GigabitEthernet1",
"preference": 1,
"metric": 0,
"current_active": true
}
]
}
get_route_to() wraps show ip route [dest],
accepts a destination in CIDR notation and returns all matching routes including recursive next-hops and
protocol source. To retrieve the entire routing table, call
get_route_to(destination="") with an empty string —
this returns all prefixes, which can be large on production devices.
For targeted checks (e.g. verifying a default route exists after a
config change), pass the specific prefix.
get_bgp_neighbors() — BGP Peer State
bgp = device.get_bgp_neighbors()
for vrf, vrf_data in bgp.items():
for peer_ip, peer_data in vrf_data["peers"].items():
print(f"VRF: {vrf} Peer: {peer_ip} "
f"State: {'Up' if peer_data['is_up'] else 'Down'} "
f"Prefixes: {peer_data['address_family']['ipv4']['received_prefixes']}")
# ── Output ──────────────────────────────────────────────── VRF: global Peer: 203.0.113.1 State: Up Prefixes: 847231
Full Getter Reference
| Getter Method | Returns | Underlying IOS Command(s) |
|---|---|---|
get_facts() |
Hostname, vendor, model, OS version, serial, uptime, interfaces | show version, show interfaces |
get_interfaces() |
Per-interface up/enabled status, speed, MTU, MAC, last flap | show interfaces |
get_interfaces_ip() |
IPv4 and IPv6 address + prefix length per interface | show ip interface brief, show ipv6 interface brief |
get_interfaces_counters() |
Tx/Rx packets, bytes, errors, drops per interface | show interfaces |
get_route_to(destination) |
Matching routes — protocol, next-hop, interface, metric, preference | show ip route [dest] |
get_bgp_neighbors() |
BGP peer state, AS number, prefix counts per AFI per VRF | show bgp summary, show bgp neighbors |
get_lldp_neighbors() |
LLDP neighbour hostname and port per local interface | show lldp neighbors |
get_lldp_neighbors_detail() |
Full LLDP detail — system description, capabilities, management IP | show lldp neighbors detail |
get_arp_table() |
ARP entries — IP, MAC, interface, age | show arp |
get_mac_address_table() |
MAC table — MAC, VLAN, interface, type (static/dynamic) | show mac address-table |
get_snmp_information() |
SNMP community strings, contact, location, chassis ID | show snmp |
get_ntp_peers() |
Configured NTP server addresses | show ntp associations. See NTP Configuration. |
get_config(retrieve) |
Running, startup, or candidate config as a raw string | show running-config, show startup-config |
get_environment() |
CPU load, memory usage, temperature, power supply, fan state | show processes cpu, show environment |
get_vlans() |
VLAN IDs, names, and member interfaces | show vlan brief |
6. Step 4 — Multi-Device Inventory Script
The real value of NAPALM's normalised output is looping across multiple devices with identical code regardless of vendor. This script collects facts and interface IP addresses from every device in a YAML inventory file and writes the results to a CSV:
inventory.yaml
# inventory.yaml ── device list for NAPALM scripts
devices:
- hostname: "192.168.10.1"
driver: "ios"
username: "netauto"
password: "Aut0P@ss!"
secret: "En@ble99!"
site: "HQ"
- hostname: "192.168.10.2"
driver: "ios"
username: "netauto"
password: "Aut0P@ss!"
secret: "En@ble99!"
site: "Branch1"
- hostname: "192.168.20.1"
driver: "eos"
username: "admin"
password: "Ar1sta!"
secret: ""
site: "DataCentre"
napalm_inventory.py
#!/usr/bin/env python3
"""napalm_inventory.py — Collect facts from all devices in inventory"""
import napalm
import yaml
import csv
import json
from datetime import datetime
def collect_device_facts(dev_cfg):
"""Connect to one device and return its facts dict, or None on error."""
driver = napalm.get_network_driver(dev_cfg["driver"])
device = driver(
hostname=dev_cfg["hostname"],
username=dev_cfg["username"],
password=dev_cfg["password"],
optional_args={"secret": dev_cfg.get("secret", "")}
)
try:
device.open()
facts = device.get_facts()
facts["site"] = dev_cfg["site"] # add our own field
facts["collected_at"] = datetime.now().isoformat()
return facts
except Exception as exc:
print(f" ERROR connecting to {dev_cfg['hostname']}: {exc}")
return None
finally:
device.close()
# ── Load inventory ────────────────────────────────────────
with open("inventory.yaml") as f:
inventory = yaml.safe_load(f)
# ── Collect facts from every device ──────────────────────
all_facts = []
for dev in inventory["devices"]:
print(f"Connecting to {dev['hostname']} ({dev['site']})...")
facts = collect_device_facts(dev)
if facts:
all_facts.append(facts)
print(f" OK — {facts['hostname']} {facts['vendor']} {facts['model']}")
# ── Write CSV report ──────────────────────────────────────
csv_fields = ["hostname", "vendor", "model", "os_version",
"serial_number", "uptime", "site", "collected_at"]
with open("device_inventory.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=csv_fields, extrasaction="ignore")
writer.writeheader()
writer.writerows(all_facts)
print(f"\nInventory written to device_inventory.csv ({len(all_facts)} devices)")
# ── Sample output ───────────────────────────────────────── Connecting to 192.168.10.1 (HQ)... OK — NetsTuts_R1 Cisco CSR1000V Connecting to 192.168.10.2 (Branch1)... OK — NetsTuts_R2 Cisco ISR4331 Connecting to 192.168.20.1 (DataCentre)... OK — DC-LEAF1 Arista DCS-7050CX3 Inventory written to device_inventory.csv (3 devices)
get_facts() call works for both Cisco IOS
and Arista EOS devices — the script does not need any vendor-specific
branching logic. The extrasaction="ignore" on the CSV
writer discards extra keys (like interface_list) that
are not in the csv_fields list. For large inventories,
replace the sequential loop with Python's
concurrent.futures.ThreadPoolExecutor to connect to
multiple devices in parallel — significantly faster for 50+ devices.
7. Step 5 — Compare Configuration States
NAPALM's get_config() retrieves the running, startup,
or candidate configuration as a string. Combined with Python's
difflib, you can produce a human-readable diff between
two config states — useful for change audits, drift detection, or
verifying that a deployment applied exactly the intended changes.
See Saving and Managing Cisco Configurations
for understanding the relationship between running-config and startup-config:
Compare Running vs Startup Config (Detect Unsaved Changes)
#!/usr/bin/env python3
"""napalm_config_diff.py — Detect unsaved changes (running vs startup)"""
import napalm
import difflib
driver = napalm.get_network_driver("ios")
device = driver(hostname="192.168.10.1", username="netauto",
password="Aut0P@ss!", optional_args={"secret": "En@ble99!"})
try:
device.open()
# ── Retrieve both configs ─────────────────────────────
configs = device.get_config(retrieve="all")
running = configs["running"].splitlines(keepends=True)
startup = configs["startup"].splitlines(keepends=True)
# ── Generate unified diff ─────────────────────────────
diff = list(difflib.unified_diff(
startup, running,
fromfile="startup-config",
tofile="running-config",
lineterm=""
))
if diff:
print("UNSAVED CHANGES DETECTED:\n")
print("".join(diff))
else:
print("Running and startup configs are identical — no unsaved changes.")
finally:
device.close()
# ── Sample output when unsaved changes exist ────────────── UNSAVED CHANGES DETECTED: --- startup-config +++ running-config @@ -42,6 +42,9 @@ interface GigabitEthernet2 ip address 192.168.10.1 255.255.255.0 no shutdown +! +ip route 0.0.0.0 0.0.0.0 203.0.113.1 +! line vty 0 4 login local
+ exist in the running config but
not in startup — these are changes that would be lost on a reload.
Lines prefixed with - exist in startup but not running —
these were removed since the last wr. This script
can run as a scheduled task across all devices to detect configuration
drift and alert when unsaved changes accumulate — a common compliance
requirement in production networks. Compare with
show running-config output
to manually verify any findings.
8. Step 6 — Push Configuration Changes
NAPALM's configuration push workflow is safer than raw CLI because it separates loading a change from committing it. You load the candidate configuration, inspect the diff, and only commit after confirming the diff is exactly what you intended. On platforms that support atomic commits (Junos, EOS, IOS-XR), the change is applied in a single transaction — partial application is impossible. On IOS, NAPALM merges the candidate line-by-line using Netmiko but still provides the diff preview before applying:
Workflow Overview
load_merge_candidate(config=...) or load_replace_candidate(config=...)
│
▼
compare_config() ← returns unified diff (running vs candidate)
│
├── diff is empty → no change needed, discard_config()
│
├── diff looks wrong → discard_config() and fix the candidate
│
└── diff is correct ──► commit_config()
│
▼
Changes applied to device
(rollback_config() available if needed)
load_merge_candidate — Add Lines to Existing Config
#!/usr/bin/env python3
"""napalm_push_merge.py — Merge new config lines into running config"""
import napalm
# ── Configuration snippet to push ────────────────────────
# load_merge_candidate ADDS these lines — it does not replace
# the entire config. Existing lines are preserved.
NEW_CONFIG = """
ntp server 216.239.35.0
ntp server 216.239.35.4
logging host 192.168.99.10
logging trap informational
ip access-list standard MGMT-ACCESS
permit 192.168.10.0 0.0.0.255
deny any log
"""
driver = napalm.get_network_driver("ios")
device = driver(hostname="192.168.10.1", username="netauto",
password="Aut0P@ss!", optional_args={"secret": "En@ble99!"})
try:
device.open()
# ── Stage the candidate config ────────────────────────
device.load_merge_candidate(config=NEW_CONFIG)
# ── Preview the diff before committing ───────────────
diff = device.compare_config()
if not diff:
print("No changes detected — config already applied.")
device.discard_config()
else:
print("Pending changes:\n")
print(diff)
confirm = input("\nApply these changes? [yes/no]: ")
if confirm.lower() == "yes":
device.commit_config()
print("Configuration committed successfully.")
else:
device.discard_config()
print("Changes discarded — no modifications made.")
except Exception as exc:
print(f"Error: {exc}")
device.discard_config() # always discard on error
finally:
device.close()
# ── Sample output ───────────────────────────────────────── Pending changes: +ntp server 216.239.35.0 +ntp server 216.239.35.4 +logging host 192.168.99.10 +logging trap informational +ip access-list standard MGMT-ACCESS + permit 192.168.10.0 0.0.0.255 + deny any log Apply these changes? [yes/no]: yes Configuration committed successfully.
load_merge_candidate() stages the config without applying
it. compare_config() returns a unified diff showing only
the lines that will be added (+) or removed (-)
— it does not return the entire running config. The config snippet above
adds NTP servers,
a syslog host,
and an access list.
The interactive confirmation step is optional but strongly recommended for production
scripts. For fully automated pipelines (CI/CD), replace the
input() call with a programmatic check: if the diff
matches the expected change template, commit automatically; otherwise
raise an alert.
load_replace_candidate — Replace the Entire Config
#!/usr/bin/env python3
"""napalm_push_replace.py — Replace entire config from a file"""
import napalm
driver = napalm.get_network_driver("ios")
device = driver(hostname="192.168.10.1", username="netauto",
password="Aut0P@ss!", optional_args={"secret": "En@ble99!"})
try:
device.open()
# ── Load candidate from a full config file ────────────
# load_replace_candidate REPLACES the entire running config.
# Lines in running but NOT in the candidate will be REMOVED.
# Use with extreme caution — always review the diff first.
device.load_replace_candidate(filename="r1_desired_state.cfg")
diff = device.compare_config()
print("Diff (- lines will be REMOVED, + lines will be ADDED):\n")
print(diff)
confirm = input("\nThis will REPLACE the running config. Proceed? [yes/no]: ")
if confirm.lower() == "yes":
device.commit_config()
print("Replace committed.")
else:
device.discard_config()
print("Discarded — no changes made.")
except Exception as exc:
print(f"Error: {exc}")
device.discard_config()
finally:
device.close()
load_replace_candidate() is the declarative approach —
the candidate file represents the complete intended state of the
device. Any line in the running config that is not in the candidate
file will be removed on commit. This is powerful but dangerous: a
missing ip route 0.0.0.0 0.0.0.0 line in the candidate
file will delete the default route on commit, cutting off management
access. Always review the full diff — especially - lines
— before committing a replace operation. For Cisco IOS, replace is
implemented via configure replace — confirm your IOS
version supports this feature before use. See
Saving and Managing Cisco Configurations
for how to generate a complete config baseline file.
rollback_config() — Undo the Last Commit
# ── Rollback immediately after a bad commit ───────────
device.open()
device.rollback_config()
print("Rolled back to pre-commit state.")
device.close()
rollback_config() restores the configuration that existed
immediately before the last commit_config() call. On
IOS, NAPALM saves the pre-commit running config to a rollback file
and applies it via configure replace. On Junos (using NETCONF) and EOS,
rollback uses the platform's native transactional commit history.
Note that rollback only covers the most recent commit — it is not
a full commit history. For production safety, save the pre-change
config with get_config(retrieve="running") before
committing so you always have a known-good baseline to restore.
Compare with show running-config
to verify the rollback was successful.
9. Step 7 — Automated Compliance Checking
A practical pattern is combining getters to verify network-wide standards. This script checks a list of compliance rules against every device in the inventory and reports violations — useful for security audits, change verification, or daily health checks:
#!/usr/bin/env python3
"""napalm_compliance.py — Check devices against compliance rules"""
import napalm
import yaml
COMPLIANCE_RULES = {
"ntp_servers": ["216.239.35.0", "216.239.35.4"], # required NTP peers
"min_uptime_hours": 1, # flag if just rebooted
"required_interfaces_up": ["GigabitEthernet1"], # must be up
}
def check_device(dev_cfg):
driver = napalm.get_network_driver(dev_cfg["driver"])
device = driver(
hostname=dev_cfg["hostname"],
username=dev_cfg["username"],
password=dev_cfg["password"],
optional_args={"secret": dev_cfg.get("secret", "")}
)
violations = []
try:
device.open()
facts = device.get_facts()
ntp_peers = device.get_ntp_peers()
interfaces = device.get_interfaces()
# ── Rule 1: Uptime check ──────────────────────────
uptime_hours = facts["uptime"] / 3600
if uptime_hours < COMPLIANCE_RULES["min_uptime_hours"]:
violations.append(
f"LOW UPTIME: {uptime_hours:.1f}h — possible unplanned reboot"
)
# ── Rule 2: NTP servers present ───────────────────
configured_ntp = list(ntp_peers.keys())
for required in COMPLIANCE_RULES["ntp_servers"]:
if required not in configured_ntp:
violations.append(f"MISSING NTP SERVER: {required}")
# ── Rule 3: Critical interfaces must be up ────────
for intf in COMPLIANCE_RULES["required_interfaces_up"]:
if intf in interfaces:
if not interfaces[intf]["is_up"]:
violations.append(f"INTERFACE DOWN: {intf}")
else:
violations.append(f"INTERFACE NOT FOUND: {intf}")
return facts["hostname"], violations
except Exception as exc:
return dev_cfg["hostname"], [f"CONNECTION ERROR: {exc}"]
finally:
device.close()
# ── Run against all devices ───────────────────────────────
with open("inventory.yaml") as f:
inventory = yaml.safe_load(f)
print("=" * 60)
print("COMPLIANCE REPORT")
print("=" * 60)
all_pass = True
for dev in inventory["devices"]:
hostname, violations = check_device(dev)
if violations:
all_pass = False
print(f"\n[FAIL] {hostname}")
for v in violations:
print(f" ✗ {v}")
else:
print(f"[PASS] {hostname}")
print("\n" + ("ALL DEVICES COMPLIANT" if all_pass else "VIOLATIONS FOUND"))
# ── Sample output ─────────────────────────────────────────
============================================================
COMPLIANCE REPORT
============================================================
[PASS] NetsTuts_R1
[PASS] DC-LEAF1
[FAIL] NetsTuts_R2
✗ MISSING NTP SERVER: 216.239.35.4
✗ INTERFACE DOWN: GigabitEthernet1
VIOLATIONS FOUND
get_ntp_peers() to verify
NTP server configuration and get_interfaces()
to check interface states. For interface failures flagged here, investigate with
show interfaces on the device directly.
For missing NTP servers, see NTP Configuration.
10. Troubleshooting NAPALM Connections
| Error | Cause | Fix |
|---|---|---|
ConnectionException: Unable to connect |
TCP/22 not reachable — firewall blocking SSH, device not listening, wrong IP | Ping the device from the automation host. Verify ip ssh version 2 and transport input ssh on VTY lines. Check SSH Configuration. |
AuthenticationException |
Wrong username or password in the driver instantiation | Verify credentials directly with an SSH client first. Check username and privilege 15 on the device. See SSH Configuration. |
| Getters return empty dicts or missing keys | Device did not enter privileged EXEC mode — enable password not provided or wrong | Pass "secret": "enable-password" in optional_args. Verify by SSHing manually and running enable with the same password. See show running-config to confirm privilege level. |
ReadTimeout on slow devices |
Device takes longer to respond than the default timeout allows | Increase "global_delay_factor": 3 or "conn_timeout": 30 in optional_args. Also try "fast_cli": False to disable fast-mode parsing. See Python Netmiko for underlying timing behaviour. |
compare_config() returns empty string |
Candidate config was not loaded (no load_merge_candidate called), or config was already discarded |
Ensure load_merge_candidate() or load_replace_candidate() is called before compare_config(). Check that discard_config() was not called earlier in the same session. |
MergeConfigException on IOS |
One or more config lines in the candidate were rejected by IOS syntax validation | NAPALM applies lines one at a time on IOS — check which line caused the error. Validate the candidate snippet manually in the CLI before pushing via NAPALM. Use show running-config to compare after manual testing. |
load_replace_candidate cuts connectivity |
Candidate config file missing management interface IP, SSH config, or default route | Always include all required lines in replace candidates: IP address on management interface, VTY SSH config, default route, and local username. Preview diff for - lines before committing. See Saving Cisco Configurations. |
Key Points & Exam Tips
- NAPALM provides a unified API across multiple vendors — the same Python method calls work for Cisco IOS, NX-OS, Junos, and Arista EOS. Only the driver name changes between platforms. See Controller-Based Networking for the broader context.
- The standard connection pattern is:
get_network_driver()→ instantiate driver →open()→ run getters/config operations →close()in afinallyblock. - For Cisco IOS, always pass
"secret": "enable-password"inoptional_args— without it the driver cannot enter privileged EXEC mode and most getters will return empty or incomplete data. get_facts()returns eight normalised keys:hostname,fqdn,vendor,model,os_version,serial_number,uptime, andinterface_list— identical structure for every supported platform. Underlying IOS commands:show versionandshow interfaces.- The config push workflow is:
load_merge_candidate()(orload_replace_candidate()) →compare_config()→ review diff →commit_config()ordiscard_config(). load_merge_candidate()adds lines to the existing config.load_replace_candidate()replaces the entire config — lines in running but not in the candidate file will be removed. Always review the diff for-lines before a replace commit. See Saving and Managing Cisco Configurations.- Always call
discard_config()in exception handlers — a staged candidate that is never committed or discarded can cause unexpected behaviour on subsequent config operations in the same session. - NAPALM is vendor-neutral at the Python API layer but the underlying IOS driver uses Netmiko for SSH. Connectivity issues should be diagnosed at the Netmiko/SSH layer first — if manual SSH fails, NAPALM will also fail.
- For the CCNA exam and automation track: know the difference between merge vs replace candidates, the role of
compare_config()as a safety gate before committing, and how NAPALM's abstraction layer differs from raw Netmiko scripting.