Software Engineering Wiki

Networking

Network automation

Collecting device state, parsing CLI output into structures, making configuration changes safely, and the Python libraries that do each job.

Cheatsheet #

TaskTool or snippet
SSH to one device, run commandsnetmiko.ConnectHandler(**dev).send_command("show ip int brief")
Fast async SSHscrapli.Scrapli(**dev)
Vendor-neutral facts and confignapalm.get_network_driver("ios")
Many devices, inventory-drivennornir with the netmiko or scrapli plugin
Parse CLI text into dictsntc_templates.parse.parse_output (TextFSM)
Structured output from the deviceshow ... | json (NX-OS), NETCONF, RESTCONF, gNMI
Config diff before applynapalm compare_config()
Roll backnapalm discard_config() / rollback()
Validate an IP or prefixipaddress.ip_network("10.0.0.0/24")
Bulk reachabilityfping -a -g 10.0.0.0/24
Interactive promptssend_command_timing or an expect string

Choosing the interface #

InterfaceWhen
SSH + CLI scrapingAlways available; the fallback when nothing structured exists
NETCONF / RESTCONFStructured config with candidate datastores and commit/rollback
gNMIStreaming telemetry and config on modern platforms
Vendor REST APIController-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 dicts
from 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} -> Record

On 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 reload

A 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 themselves
fping -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 available

Verify 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 #

SymptomCause
Authentication failed on some devicesTACACS policy differs by group; check the privilege level too
Netmiko hangsUnexpected prompt or a --More-- pager — terminal length 0
Output truncatedPager 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 scriptEnable mode not entered, or the command needs config mode
Random failures at scaleToo many concurrent sessions: devices and AAA servers both cap them
Config applied but lost after reloadNever saved — write memory or copy run start
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("netmiko").setLevel(logging.DEBUG)   # full session transcript

Oneliners #

# 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'

Last updated 15 September 2026 · Edit this page