Networking
Network automation
Collecting device state, parsing CLI output into structures, making configuration changes safely, and the Python libraries that do each job.
Cheatsheet #
| Task | Tool or snippet |
|---|---|
| SSH to one device, run commands | netmiko.ConnectHandler(**dev).send_command("show ip int brief") |
| Fast async SSH | scrapli.Scrapli(**dev) |
| Vendor-neutral facts and config | napalm.get_network_driver("ios") |
| Many devices, inventory-driven | nornir with the netmiko or scrapli plugin |
| Parse CLI text into dicts | ntc_templates.parse.parse_output (TextFSM) |
| Structured output from the device | show ... | json (NX-OS), NETCONF, RESTCONF, gNMI |
| Config diff before apply | napalm compare_config() |
| Roll back | napalm discard_config() / rollback() |
| Validate an IP or prefix | ipaddress.ip_network("10.0.0.0/24") |
| Bulk reachability | fping -a -g 10.0.0.0/24 |
| Interactive prompts | send_command_timing or an expect string |
Choosing the interface #
| Interface | When |
|---|---|
| SSH + CLI scraping | Always available; the fallback when nothing structured exists |
| NETCONF / RESTCONF | Structured config with candidate datastores and commit/rollback |
| gNMI | Streaming telemetry and config on modern platforms |
| Vendor REST API | Controller-based fabrics (ACI, Meraki, DNA Center) |
Prefer structured interfaces when the platform has them: parsing CLI text is a maintenance liability that breaks on a software upgrade, and NETCONF gives you a candidate configuration you can validate before committing.
Connecting and collecting #
from netmiko import ConnectHandler
device = {
"device_type": "cisco_ios",
"host": "sw-01.example.internal",
"username": os.environ["NET_USER"],
"password": os.environ["NET_PASS"],
"conn_timeout": 10,
"fast_cli": True,
}
with ConnectHandler(**device) as conn:
conn.enable()
out = conn.send_command("show ip interface brief")
parsed = conn.send_command("show ip interface brief", use_textfsm=True) # list of dictsfrom nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command
from nornir_utils.plugins.functions import print_result
nr = InitNornir(config_file="config.yaml")
result = nr.filter(site="syd").run(
task=netmiko_send_command, command_string="show version", use_textfsm=True
)
print_result(result)Nornir gives an inventory, threading and per-host results without the overhead of a full automation platform; it is Python all the way down, so ordinary debugging works.
from napalm import get_network_driver
driver = get_network_driver("ios")
with driver("sw-01", user, password) as dev:
facts = dev.get_facts()
interfaces = dev.get_interfaces()
neighbours = dev.get_lldp_neighbors()
arp = dev.get_arp_table()Parsing output #
from ntc_templates.parse import parse_output
rows = parse_output(platform="cisco_ios", command="show ip interface brief", data=raw)
# [{'interface': 'GigabitEthernet0/1', 'ipaddr': '10.0.0.1', 'status': 'up', 'proto': 'up'}, ...]TextFSM templates handle the common commands; when one does not exist, write the template rather than a regex — it stays readable and the failure mode is an empty list, not a wrong match.
Value INTERFACE (\S+)
Value IPADDR (\S+)
Value STATUS (up|down|administratively down)
Start
^${INTERFACE}\s+${IPADDR}\s+\w+\s+\w+\s+${STATUS} -> RecordOn NX-OS and modern IOS-XE, ask the device instead:
import json
data = json.loads(conn.send_command("show ip route | json"))Configuration changes #
Never push a configuration you have not diffed, and never push one you cannot back out of.
with driver("sw-01", user, password) as dev:
dev.load_merge_candidate(filename="vlan.cfg")
diff = dev.compare_config()
if not diff:
dev.discard_config()
elif approved(diff):
dev.commit_config() # platforms with commit support roll back on loss of session
else:
dev.discard_config()with ConnectHandler(**device) as conn:
conn.enable()
out = conn.send_config_set([
"interface GigabitEthernet0/2",
"description uplink to core",
"switchport mode trunk",
])
conn.save_config() # write memory — otherwise it is gone on reloadA config push can remove your own access
Use reload in 5 before the change and reload cancel after verifying, or a platform’s confirmed-commit. On a device with neither, have console access arranged before you start.
Order matters for anything touching the path you are connected over: apply the new configuration first, verify, then remove the old. Applying an ACL before adding your management rule ends the session.
Idempotence and templates #
Generate the intended configuration from data, compare it with the running configuration, and push only the difference. That makes reruns safe and the repository the source of truth.
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("templates"), trim_blocks=True, lstrip_blocks=True)
cfg = env.get_template("switch.j2").render(hostname="sw-01", vlans=[10, 20, 30])hostname {{ hostname }}
{% for vlan in vlans %}
vlan {{ vlan }}
name VLAN{{ vlan }}
{% endfor %}Validate the intent before it reaches a device: pyATS/genie and batfish both check reachability and policy from configuration alone, which catches a broken ACL before it is deployed.
Bulk operations safely #
from concurrent.futures import ThreadPoolExecutor
def collect(host: str) -> tuple[str, str]:
with ConnectHandler(host=host, **base) as c:
return host, c.send_command("show version")
with ThreadPoolExecutor(max_workers=10) as pool: # 10, not 200: TACACS has limits too
for host, out in pool.map(collect, hosts):
Path(f"out/{host}.txt").write_text(out)Stage changes: one device, then one site, then the fleet, with a verification step between each. Record before and after state for every device so a rollback has something to compare against.
Discovery and verification #
neighbours = dev.get_lldp_neighbors() # build the topology from the devices themselvesfping -a -g 10.0.0.0/24 2>/dev/null # which addresses answer
nmap -sn 10.0.0.0/24 -oG - # host discovery with names
snmpwalk -v2c -c public sw-01 IF-MIB::ifDescr # interface inventory where SNMP is availableVerify after every change against the state you collected before it: interface counters, neighbour tables, route counts and a targeted reachability test. “The command applied without error” is not verification.
Troubleshooting #
| Symptom | Cause |
|---|---|
Authentication failed on some devices | TACACS policy differs by group; check the privilege level too |
| Netmiko hangs | Unexpected prompt or a --More-- pager — terminal length 0 |
| Output truncated | Pager again, or a read timeout too short for a large table |
use_textfsm returns [] | No template for that platform/command pair; parse manually or add one |
| Works interactively, fails in script | Enable mode not entered, or the command needs config mode |
| Random failures at scale | Too many concurrent sessions: devices and AAA servers both cap them |
| Config applied but lost after reload | Never saved — write memory or copy run start |
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("netmiko").setLevel(logging.DEBUG) # full session transcriptOneliners #
# Reachability sweep, fast
fping -a -g 10.0.0.0/24 2>/dev/null | tee reachable.txt
# Run one command everywhere and keep the output
while read -r h; do echo "== $h"; ssh -o ConnectTimeout=5 "$h" 'show version | include uptime'; done < hosts.txt
# Diff a device's running config against the repository copy
ssh sw-01 'show running-config' | diff -u configs/sw-01.cfg - | head -40
# Parse any command output into JSON on the shell
python3 -c 'import sys,json;from ntc_templates.parse import parse_output;print(json.dumps(parse_output(platform="cisco_ios",command=sys.argv[1],data=sys.stdin.read())))' "show ip int brief" < out.txt
# Which interfaces are down but not administratively
ssh sw-01 'show ip interface brief' | awk '$5=="down" && $6=="down"'
# Back up every device before a change window
while read -r h; do ssh "$h" 'show running-config' > "backups/$h-$(date +%F).cfg"; done < hosts.txt
# Find a MAC address across the fleet
while read -r h; do ssh "$h" "show mac address-table | include 0011.2233" | sed "s/^/$h /"; done < hosts.txt
# Confirm NTP and DNS on every device
while read -r h; do printf '%s ' "$h"; ssh "$h" 'show ntp status | include synchron'; done < hosts.txt
# Count routes before and after a change
ssh rtr-01 'show ip route summary | include Total'