| name | netmiko-ssh-automation |
| description | Multi-vendor network device SSH automation using Python Netmiko — connecting, sending commands, parsing output, handling enable mode, error handling, and batch operations across device lists. |
| origin | ECC |
Netmiko SSH Automation
Patterns for automating network device interaction via SSH using Netmiko. Covers single-device connections, batch operations, enable mode, error handling, and output parsing.
When to Activate
- Writing Python scripts to automate Cisco, Juniper, Arista, or other network devices via SSH
- Collecting show command output from multiple devices programmatically
- Applying configuration changes across a fleet of devices
- Parsing structured or semi-structured CLI output in Python
- Building network automation tools or scripts that talk to real devices
Basic Connection
from netmiko import ConnectHandler
device = {
"device_type": "cisco_ios",
"host": "192.168.1.1",
"username": "admin",
"password": "secret",
"secret": "enable_secret",
"port": 22,
"timeout": 30,
"session_log": "session.log",
}
with ConnectHandler(**device) as conn:
output = conn.send_command("show version")
print(output)
Device Types Reference
device = {
"device_type": "cisco_ios",
"host": "192.168.1.1",
"username": "admin",
"use_keys": True,
"key_file": "/home/user/.ssh/id_rsa",
}
Enable Mode
device = {
"device_type": "cisco_ios",
"host": "192.168.1.1",
"username": "admin",
"password": "secret",
"secret": "enable_secret",
}
with ConnectHandler(**device) as conn:
conn.enable()
output = conn.send_command("show running-config")
with ConnectHandler(**device) as conn:
if conn.check_enable_mode():
print("Already in enable mode")
else:
conn.enable()
Sending Configuration
from netmiko import ConnectHandler
commands = [
"interface GigabitEthernet0/1",
"description AUTOMATION-TEST",
"no shutdown",
]
with ConnectHandler(**device) as conn:
conn.enable()
output = conn.send_config_set(commands)
print(output)
conn.save_config()
Batch Operations Across Multiple Devices
from netmiko import ConnectHandler
from netmiko.exceptions import NetmikoAuthenticationException, NetmikoTimeoutException
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any
DEVICES = [
{"host": "10.0.0.1", "device_type": "cisco_ios"},
{"host": "10.0.0.2", "device_type": "cisco_ios"},
{"host": "10.0.0.3", "device_type": "arista_eos"},
]
BASE_CREDS = {"username": "admin", "password": "secret"}
def run_command_on_device(device_params: dict, command: str) -> dict[str, Any]:
params = {**device_params, **BASE_CREDS}
host = params["host"]
try:
with ConnectHandler(**params) as conn:
output = conn.send_command(command)
return {"host": host, "output": output, "error": None}
except NetmikoAuthenticationException:
return {"host": host, "output": None, "error": "Authentication failed"}
except NetmikoTimeoutException:
return {"host": host, "output": None, "error": "Connection timed out"}
except Exception as e:
return {"host": host, "output": None, "error": str(e)}
results = []
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {
executor.submit(run_command_on_device, d, "show version"): d["host"]
for d in DEVICES
}
for future in as_completed(futures):
results.append(future.result())
for r in results:
if r["error"]:
print(f"{r['host']}: ERROR — {r['error']}")
else:
print(f"{r['host']}: OK")
Parsing Output
from netmiko import ConnectHandler
import re
with ConnectHandler(**device) as conn:
parsed = conn.send_command("show ip interface brief", use_textfsm=True)
for intf in parsed:
print(f"{intf['intf']}: {intf['ipaddr']} — {intf['status']}/{intf['proto']}")
BGP_NEIGHBOR_RE = re.compile(
r"^(?P<ip>\d{1,3}(?:\.\d{1,3}){3})\s+\d+\s+(?P<as>\d+)"
r"\s+\d+\s+\d+\s+\d+\s+\d+\s+(?P<uptime>\S+)\s+(?P<state>\S+)",
re.MULTILINE,
)
with ConnectHandler(**device) as conn:
raw = conn.send_command("show bgp summary")
for m in BGP_NEIGHBOR_RE.finditer(raw):
print(f"Neighbor {m.group('ip')} (AS {m.group('as')}): {m.group('state')}")
Error Handling Patterns
from netmiko import ConnectHandler
from netmiko.exceptions import (
NetmikoAuthenticationException,
NetmikoTimeoutException,
NetMikoTimeoutException,
)
import socket
def safe_connect(device_params: dict) -> dict:
host = device_params.get("host", "unknown")
try:
conn = ConnectHandler(**device_params)
return {"conn": conn, "error": None}
except NetmikoAuthenticationException:
return {"conn": None, "error": f"{host}: authentication failed"}
except (NetmikoTimeoutException, NetMikoTimeoutException):
return {"conn": None, "error": f"{host}: SSH timeout"}
except socket.gaierror:
return {"conn": None, "error": f"{host}: DNS resolution failed"}
except ConnectionRefusedError:
return {"conn": None, "error": f"{host}: SSH port not open"}
except Exception as e:
return {"conn": None, "error": f"{host}: {type(e).__name__}: {e}"}
Anti-Patterns
conn = ConnectHandler(**device)
output = conn.send_command("show version")
conn.disconnect()
with ConnectHandler(**device) as conn:
output = conn.send_command("show version")
device = {
"host": "10.0.0.1",
"username": "admin",
"password": "MyPassword123",
}
import os
device = {
"host": "10.0.0.1",
"username": os.environ["NET_USERNAME"],
"password": os.environ["NET_PASSWORD"],
}
for d in devices:
result = run_command_on_device(d, "show version")
conn.send_config_set(["interface Gi0/1", "description CHANGED"])
conn.send_config_set(["interface Gi0/1", "description CHANGED"])
conn.save_config()
Best Practices
- Always use context managers (
with ConnectHandler(...) as conn) for automatic cleanup
- Store credentials in environment variables or a vault — never in source code
- Set explicit
timeout values — default can be too long for large-scale automation
- Use
session_log during development to capture full CLI I/O for debugging
- Use
use_textfsm=True on supported commands for structured output — much easier to process
- Wrap all connections in try/except to handle unreachable devices gracefully in batch jobs
- Limit parallel threads to 10–20 — more can exhaust SSH sessions on older devices
Related Skills
- cisco-ios-patterns
- network-bgp-diagnostics
- network-config-validation