Overview
Manual CLI changes across hundreds of devices are slow, error-prone, and unauditable. Python-based network automation eliminates the repetitive work, enforces consistency, and gives you a defensible record of every change. This guide covers the two most practical automation libraries โ Netmiko for SSH-based CLI interaction and NAPALM for structured multi-vendor config management โ along with real-world patterns for inventory management, config auditing, and compliance reporting.
Part 1 โ Netmiko: SSH CLI Automation
Netmiko abstracts SSH connections across 80+ vendor platforms, handling prompt detection, paging, and error handling automatically.
Installation
pip install netmiko
pip install netmiko[textfsm] # adds structured output parsing
Basic Connection and Commands
from netmiko import ConnectHandler
device = {
'device_type': 'cisco_ios',
'host': '192.168.1.1',
'username': 'admin',
'password': 'SecurePass!',
'secret': 'EnablePass!',
}
with ConnectHandler(**device) as net_connect:
net_connect.enable()
# Show command
output = net_connect.send_command('show ip interface brief')
print(output)
# Push config
config_commands = [
'interface Loopback0',
'description AUTOMATION-MANAGED',
'ip address 10.255.255.1 255.255.255.255',
]
net_connect.send_config_set(config_commands)
net_connect.save_config()
Structured Output with TextFSM
with ConnectHandler(**device) as net_connect:
# Returns list of dicts instead of raw text
bgp_summary = net_connect.send_command(
'show bgp summary',
use_textfsm=True
)
for neighbor in bgp_summary:
print(f"Neighbor: {neighbor['bgp_neigh']} State: {neighbor['state_pfxrcd']}")
Parallel Execution Across a Fleet
from netmiko import ConnectHandler
from concurrent.futures import ThreadPoolExecutor, as_completed
import yaml
def get_version(device):
try:
with ConnectHandler(**device) as conn:
output = conn.send_command('show version', use_textfsm=True)
return {'host': device['host'], 'version': output[0]['version']}
except Exception as e:
return {'host': device['host'], 'error': str(e)}
with open('inventory.yml') as f:
devices = yaml.safe_load(f)['devices']
results = []
with ThreadPoolExecutor(max_workers=20) as executor:
futures = {executor.submit(get_version, d): d for d in devices}
for future in as_completed(futures):
results.append(future.result())
for r in sorted(results, key=lambda x: x['host']):
print(f"{r['host']:20} {r.get('version', r.get('error'))}")
Part 2 โ NAPALM: Multi-Vendor Structured Config
NAPALM provides a unified API across IOS, IOS-XR, NX-OS, JunOS, and EOS โ returning structured Python objects instead of raw CLI text.
from napalm import get_network_driver
driver = get_network_driver('ios')
device = driver(hostname='192.168.1.1', username='admin', password='SecurePass!')
device.open()
# Structured facts โ identical API across all vendors
facts = device.get_facts()
print(f"Hostname: {facts['hostname']} Model: {facts['model']} OS: {facts['os_version']}")
# BGP neighbors with structured data
bgp = device.get_bgp_neighbors()
for vrf, data in bgp.items():
for peer, info in data['peers'].items():
prefixes = info['address_family']['ipv4']['received_prefixes']
print(f"VRF {vrf} | Peer {peer} | Up: {info['is_up']} | Prefixes: {prefixes}")
# Detect interfaces that are admin-up but link-down
interfaces = device.get_interfaces()
for name, iface in interfaces.items():
if iface['is_enabled'] and not iface['is_up']:
print(f"ALERT: {name} is admin-up but link-down")
device.close()
Config Diff and Atomic Replace
device.open()
device.load_replace_candidate(filename='desired_config.txt')
diff = device.compare_config()
print(diff)
if diff:
confirm = input("Commit? [y/N]: ")
if confirm.lower() == 'y':
device.commit_config()
else:
device.discard_config()
device.close()
Part 3 โ Config Compliance Auditing
from netmiko import ConnectHandler
import yaml, json
from datetime import datetime
REQUIRED_CONFIG = [
'service password-encryption',
'no ip http server',
'logging buffered 65536',
'ip ssh version 2',
'spanning-tree portfast bpduguard default',
]
BANNED_CONFIG = [
'ip http server',
'snmp-server community public',
'no service password-encryption',
]
def audit_device(device):
result = {'host': device['host'], 'missing': [], 'violations': [], 'error': None}
try:
with ConnectHandler(**device) as conn:
conn.enable()
running = conn.send_command('show running-config')
for r in REQUIRED_CONFIG:
if r not in running:
result['missing'].append(r)
for b in BANNED_CONFIG:
if b in running:
result['violations'].append(b)
except Exception as e:
result['error'] = str(e)
return result
with open('inventory.yml') as f:
devices = yaml.safe_load(f)['devices']
results = [audit_device(d) for d in devices]
report = {'timestamp': datetime.now().isoformat(), 'results': results}
with open('compliance_report.json', 'w') as f:
json.dump(report, f, indent=2)
for r in results:
status = "PASS" if not r['missing'] and not r['violations'] else "FAIL"
print(f"{r['host']:20} [{status}] missing={len(r['missing'])} violations={len(r['violations'])}")
Quick Reference โ Netmiko Device Types
Automation Best Practices
- Always test scripts against a lab or staging device before running on production
- Cap
max_workersat 20โ30 โ too many parallel SSH sessions can overwhelm device control planes - Wrap every device connection in try/except โ one unreachable host should not abort the entire job
- Log all config changes with timestamp, operator name, and device hostname
- Store configs in Git and run daily backups โ diff against previous commits to detect configuration drift
- Never store credentials in scripts or YAML files โ use environment variables or a secrets manager such as HashiCorp Vault
- Use
send_command_timing()instead ofsend_command()for interactive prompts that lack a fixed ending pattern