Python Netmiko — Connect and Run Show Commands
Every show command you have ever run manually on a
Cisco router is a candidate for automation. Netmiko
is a Python library built on top of Paramiko (the SSH library)
that handles the tedious parts of SSH sessions to network devices:
detecting the device type, handling prompts, dealing with
pagination (--More--), and waiting for the correct
output before returning. Where raw Paramiko requires you to
write custom prompt-handling code for every device vendor,
Netmiko ships with built-in support for over 80 device types
— including every major Cisco platform (IOS, IOS-XE,
IOS-XR, NX-OS, ASA), Juniper, Arista, HP, and more. The
result: connecting to a Cisco router and running a command
takes fewer than ten lines of Python.
This lab walks through the complete workflow: installing
Netmiko, establishing an SSH connection to
NetsTuts-R1, running
show ip interface brief, and then parsing that
raw text output into structured Python data (a list of
dictionaries) so it can be filtered, compared, reported on,
or fed into other automation tools. For background on Python
in networking see Python for Networking
and Network Automation Overview.
For the SSH configuration that Netmiko connects to, see
SSH Configuration.
For the AAA authentication that controls login credentials,
see AAA TACACS+
Configuration. For running configuration changes
(not just show commands) via Netmiko, the same
ConnectHandler pattern extends naturally using
send_config_set().
1. How Netmiko Works
Netmiko abstracts the SSH session lifecycle into a clean Python object. Understanding the layers beneath it explains why certain parameters are required and what Netmiko is doing on your behalf:
Your Python Script
│
▼
netmiko.ConnectHandler(device_type='cisco_ios', host=..., ...)
│ • Selects the correct driver for the device type
│ • Calls Paramiko to open TCP/22 SSH connection
│ • Authenticates (password or key-based)
│ • Detects the CLI prompt (e.g. "NetsTuts-R1#")
│ • Disables paging: sends "terminal length 0"
│ so --More-- prompts never interrupt output
│
▼
connection.send_command("show ip interface brief")
│ • Sends the command string to the SSH channel
│ • Waits until the device prompt reappears
│ • Returns everything between the command and
│ the returning prompt as a Python string
│
▼
Raw string output (same text you see in the terminal)
│
▼
Parsing (manual regex OR TextFSM via use_textfsm=True)
│ • Converts unstructured text into a Python list
│ of dictionaries — one dict per interface
│
▼
Structured data → filter, report, compare, export to CSV/JSON
| Netmiko Concept | What It Does | Why It Matters |
|---|---|---|
| device_type | Selects the correct Netmiko driver class (e.g. cisco_ios, cisco_nxos, juniper_junos) |
Each vendor's CLI has different prompts, paging commands, and enable sequences. The wrong device_type causes connection or parsing failures |
| ConnectHandler | Factory function that instantiates the correct driver and establishes the SSH session | Single entry point — you do not need to import individual driver classes. Raises NetmikoTimeoutException or NetmikoAuthenticationException on failure |
| send_command() | Sends one command and returns the full output as a string, waiting for the prompt to reappear | The most common method for show commands such as show ip interface brief. Handles --More-- automatically because terminal length 0 was sent during connection setup |
| send_config_set() | Enters config mode, sends a list of configuration commands, then exits config mode | Used for configuration changes — not needed for this lab but the natural extension of the same pattern |
| disconnect() | Cleanly closes the SSH session and channel | Always call this or use the with context manager — leaving sessions open consumes VTY lines on the device |
| use_textfsm=True | Passes the raw output through a TextFSM template to return structured data instead of a raw string | Built-in parsing for common show commands — eliminates manual regex for standard Cisco outputs |
Common Netmiko device_type Values
| device_type String | Platform | Notes |
|---|---|---|
cisco_ios |
Cisco IOS and IOS-XE (ISR, Catalyst, ASR) | Most common — covers all classic IOS and modern IOS-XE routers and switches |
cisco_nxos |
Cisco NX-OS (Nexus data centre switches) | NX-OS has different paging and prompt behaviour from IOS |
cisco_xr |
Cisco IOS-XR (ASR 9000, NCS) | IOS-XR uses a different commit model for configuration changes |
cisco_asa |
Cisco ASA Firewall | ASA CLI has different enable and context-switching behaviour |
juniper_junos |
Juniper routers and switches | JunOS uses a structured CLI with pipe-to-display formatting |
arista_eos |
Arista switches | Arista also supports a JSON API (eAPI) but Netmiko works for CLI-based automation |
linux |
Linux hosts (via SSH) | Useful for automating Linux servers alongside network devices in the same script |
2. Lab Environment & Prerequisites
Automation Workstation NetsTuts-R1
(Windows / Linux / macOS) (Cisco ISR 4321)
Python 3.8+ IOS-XE 16.09
Netmiko installed SSH enabled (crypto key rsa 2048)
IP: 192.168.10.5 Mgmt IP: 192.168.10.1
│ │
└─────────── SSH TCP/22 ─────────────────┘
192.168.10.0/24
Management LAN
R1 Prerequisites (must be configured before running the script):
┌────────────────────────────────────────────────────────────────┐
│ hostname NetsTuts-R1 │
│ ip domain-name netstuts.com │
│ crypto key generate rsa modulus 2048 │
│ ip ssh version 2 │
│ username netauto privilege 15 secret AutoPass2026! │
│ line vty 0 4 │
│ transport input ssh │
│ login local │
└────────────────────────────────────────────────────────────────┘
| Parameter | Value Used in This Lab |
|---|---|
| Router hostname | NetsTuts-R1 |
| Router management IP | 192.168.10.1 |
| SSH username | netauto |
| SSH password | AutoPass2026! |
| Enable secret (privilege 15 user — no enable needed) | N/A (privilege 15 skips enable) |
| Python version | 3.8 or later (3.10+ recommended) |
| Netmiko version | 4.x (install via pip) |
privilege 15
on the router, the session starts directly at the privileged
exec prompt (#) and no enable password is needed.
If the user has a lower privilege level, Netmiko's
ConnectHandler accepts a secret
parameter for the enable password and automatically sends
enable during connection setup. For automation,
using privilege 15 is simpler and avoids storing
two separate credentials.
3. Step 1 — Install Python and Netmiko
Verify Python Installation
# ── On Linux / macOS ───────────────────────────────────────────── $ python3 --version Python 3.11.4 $ pip3 --version pip 23.2.1 from /usr/local/lib/python3.11/site-packages/pip (python 3.11) # ── On Windows (Command Prompt or PowerShell) ───────────────────── C:\> python --version Python 3.11.4 C:\> pip --version pip 23.2.1
Install Netmiko
# ── Install Netmiko and its dependencies ───────────────────────── # Netmiko automatically installs: paramiko, textfsm, ntc-templates, # scp, pyserial, and other dependencies $ pip3 install netmiko Collecting netmiko Downloading netmiko-4.3.0-py3-none-any.whl (239 kB) Collecting paramiko>=2.9.5 Collecting textfsm!=1.1.0,>=1.1.2 Collecting ntc-templates>=2.0.0 Installing collected packages: paramiko, textfsm, ntc-templates, netmiko Successfully installed netmiko-4.3.0 paramiko-3.3.1 textfsm-1.1.3 ntc-templates-4.1.0 # ── Verify install ──────────────────────────────────────────────── $ python3 -c "import netmiko; print(netmiko.__version__)" 4.3.0 # ── Optional: use a virtual environment (recommended for projects) ─ $ python3 -m venv netmiko-lab $ source netmiko-lab/bin/activate # Linux/macOS $ netmiko-lab\Scripts\activate # Windows (netmiko-lab) $ pip install netmiko
venv) isolates Netmiko and its dependencies from
your system Python packages, preventing version conflicts
between projects. This is best practice for any Python project
beyond a quick one-off script. The virtual environment creates
a self-contained directory with its own Python interpreter and
pip — activate it before running your scripts and
deactivate it when done.
4. Step 2 — Basic Connection and First Command
The minimal working script: connect to the router, run one command, print the output, disconnect. This is the foundation every more complex script builds on:
# basic_connect.py # Minimal Netmiko script: connect, run one show command, disconnect from netmiko import ConnectHandler # ── Device definition dictionary ───────────────────────────────── # All connection parameters are passed as a single dictionary device = { 'device_type': 'cisco_ios', # Netmiko driver for Cisco IOS/IOS-XE 'host': '192.168.10.1', # Router management IP 'username': 'netauto', # SSH username on the router 'password': 'AutoPass2026!', # SSH password 'port': 22, # SSH port (default 22) 'timeout': 10, # TCP connection timeout in seconds } # ── Establish the SSH connection ────────────────────────────────── # ConnectHandler returns a connection object representing the session print("Connecting to NetsTuts-R1...") connection = ConnectHandler(**device) print(f"Connected! Prompt: {connection.find_prompt()}") # ── Run the show command ────────────────────────────────────────── # send_command() waits for the device prompt to return # Returns the command output as a plain Python string output = connection.send_command("show ip interface brief") # ── Print the raw output ────────────────────────────────────────── print("\n--- Raw Output ---") print(output) # ── Cleanly close the SSH session ──────────────────────────────── connection.disconnect() print("\nDisconnected.")
Expected Output
Connecting to NetsTuts-R1... Connected! Prompt: NetsTuts-R1# --- Raw Output --- Interface IP-Address OK? Method Status Protocol GigabitEthernet0/0 192.168.10.1 YES NVRAM up up GigabitEthernet0/1 192.168.20.1 YES NVRAM up up GigabitEthernet0/2 unassigned YES unset administratively down down Loopback0 10.255.255.1 YES NVRAM up up Serial0/1/0 203.0.113.1 YES NVRAM up up Disconnected.
**device syntax (double-asterisk) unpacks the
dictionary into keyword arguments:
ConnectHandler(**device) is equivalent to writing
ConnectHandler(device_type='cisco_ios',
host='192.168.10.1', ...) with every key-value pair
written out. This dictionary pattern makes it easy to store
device parameters in configuration files or databases and
pass them to ConnectHandler without modifying
the connection code. find_prompt() returns the
current CLI prompt as a string — useful to confirm which
device you are connected to and at what privilege level
(# = privileged exec, > = user
exec).
5. Step 3 — Using the Context Manager (Best Practice)
The with statement (context manager) automatically
calls disconnect() when the block exits —
even if an exception is raised inside the block. This is the
recommended pattern for all production Netmiko scripts:
# context_manager.py # Recommended pattern: with statement ensures disconnect() always runs from netmiko import ConnectHandler device = { 'device_type': 'cisco_ios', 'host': '192.168.10.1', 'username': 'netauto', 'password': 'AutoPass2026!', } # ── Context manager: disconnect() is called automatically ───────── with ConnectHandler(**device) as conn: output = conn.send_command("show ip interface brief") print(output) # disconnect() called automatically here — even if an exception occurred
with block
(network error, device timeout, unexpected output), Python's
context manager guarantees the __exit__ method
is called, which triggers disconnect(). Without
the context manager, an unhandled exception mid-script would
leave the SSH session open on the router, consuming a VTY line.
On a router with line vty 0 4
(five VTY lines),
five crashed scripts would exhaust all VTY lines and lock out
further SSH access until the sessions time out.
6. Step 4 — Exception Handling
Network automation scripts run against live infrastructure. Devices can be unreachable, credentials can be wrong, or a device can crash mid-session. Proper exception handling prevents a single failed device from aborting a script that runs against dozens of devices:
# exception_handling.py # Handle connection failures gracefully from netmiko import ConnectHandler from netmiko.exceptions import ( NetmikoTimeoutException, # TCP connection timed out (device unreachable) NetmikoAuthenticationException, # SSH auth failed (wrong username/password) ) device = { 'device_type': 'cisco_ios', 'host': '192.168.10.1', 'username': 'netauto', 'password': 'AutoPass2026!', } try: with ConnectHandler(**device) as conn: output = conn.send_command("show ip interface brief") print(output) except NetmikoTimeoutException: # Device did not respond — unreachable IP, SSH port blocked, device down print("ERROR: Connection timed out. Is 192.168.10.1 reachable on TCP/22?") except NetmikoAuthenticationException: # SSH connected but username/password was rejected by the router print("ERROR: Authentication failed. Check username and password.") except Exception as e: # Catch-all for unexpected errors (SSH key mismatch, IOS bug, etc.) print(f"ERROR: Unexpected error — {type(e).__name__}: {e}")
| Exception | Cause | Common Fix |
|---|---|---|
NetmikoTimeoutException |
TCP connection to SSH port (22) timed out — device unreachable, wrong IP, SSH service not running, ACL blocking port 22 | Verify IP reachability with ping. Check SSH is enabled on device: show ip ssh. Verify no ACL on vty or interface blocks TCP/22 |
NetmikoAuthenticationException |
TCP connection succeeded but SSH authentication failed — wrong username, wrong password, account locked, or SSH key mismatch | Verify credentials manually with an SSH client. Check show login failures on the router. Ensure login local is configured on the VTY lines |
ReadTimeout |
Command was sent but the device took longer than read_timeout seconds to return the prompt — very slow commands or very large outputs |
Increase timeout: send_command("...", read_timeout=60). Use terminal length 0 (Netmiko does this automatically) to prevent pagination delays |
SSHException (from Paramiko) |
SSH negotiation failed — incompatible SSH key exchange algorithms, outdated IOS SSH version, or SSH host key changed | Check IOS SSH version: show ip ssh must show SSH version 2. Clear known hosts file or set 'ssh_config_file': '' in the device dict |
7. Step 5 — Parsing Output Manually with Python
The raw output of show ip interface brief is a
fixed-width text table. Python string methods and a regular
expression can convert each line into a structured dictionary
with named fields — making the data queryable and
reusable:
# manual_parse.py # Connect, retrieve output, parse it manually into a list of dicts import re from netmiko import ConnectHandler from netmiko.exceptions import NetmikoTimeoutException, NetmikoAuthenticationException device = { 'device_type': 'cisco_ios', 'host': '192.168.10.1', 'username': 'netauto', 'password': 'AutoPass2026!', } def parse_ip_interface_brief(raw_output): """ Parse 'show ip interface brief' text output into a list of dicts. Each dict has keys: interface, ip_address, ok, method, status, protocol Raw line example: GigabitEthernet0/0 192.168.10.1 YES NVRAM up up """ interfaces = [] # Regex: match each data line of show ip interface brief # Groups: (interface)(ip_address)(ok)(method)(status)(protocol) pattern = re.compile( r'^(\S+)\s+' # Interface name (non-whitespace) r'(\S+)\s+' # IP address or 'unassigned' r'(YES|NO)\s+' # OK? field r'(\S+)\s+' # Method (NVRAM, DHCP, manual, unset) r'([\w\s]+?)\s{2,}' # Status (may contain spaces: 'administratively down') r'(\S+)$', # Protocol (up or down) re.MULTILINE ) for match in pattern.finditer(raw_output): interfaces.append({ 'interface': match.group(1), 'ip_address': match.group(2), 'ok': match.group(3), 'method': match.group(4), 'status': match.group(5).strip(), 'protocol': match.group(6), }) return interfaces try: with ConnectHandler(**device) as conn: raw = conn.send_command("show ip interface brief") # Parse the raw string into structured data interfaces = parse_ip_interface_brief(raw) # ── Print structured output ─────────────────────────────────────── print(f"{'Interface':<30} {'IP Address':<18} {'Status':<22} {'Protocol'}") print("-" * 78) for iface in interfaces: print( f"{iface['interface']:<30} " f"{iface['ip_address']:<18} " f"{iface['status']:<22} " f"{iface['protocol']}" ) # ── Filter: show only interfaces that are DOWN ──────────────────── print("\n--- Interfaces with issues (status or protocol down) ---") down_interfaces = [ i for i in interfaces if i['status'] != 'up' or i['protocol'] != 'up' ] if down_interfaces: for i in down_interfaces: print(f" {i['interface']}: {i['status']} / {i['protocol']}") else: print(" All interfaces up.") except NetmikoTimeoutException: print("ERROR: Connection timed out.") except NetmikoAuthenticationException: print("ERROR: Authentication failed.")
Script Output
Interface IP Address Status Protocol ------------------------------------------------------------------------------ GigabitEthernet0/0 192.168.10.1 up up GigabitEthernet0/1 192.168.20.1 up up GigabitEthernet0/2 unassigned administratively down down Loopback0 10.255.255.1 up up Serial0/1/0 203.0.113.1 up up --- Interfaces with issues (status or protocol down) --- GigabitEthernet0/2: administratively down / down
re module. The status group uses
[\w\s]+? (non-greedy) to handle the
"administratively down" case — a
two-word status that would trip up a simple
\S+ match. The re.MULTILINE flag
makes ^ match the start of each line in the
multi-line string rather than only the start of the whole
string. The list comprehension
[i for i in interfaces if ...] filters the
parsed data in one readable line — this is the power
of converting text output to structured data: any filtering,
sorting, or reporting logic becomes trivial Python.
8. Step 6 — Automatic Parsing with TextFSM (Recommended)
Writing a custom regex for every show command is time-consuming
and fragile. TextFSM is a template-based
parsing engine. NTC-Templates is a community
library of pre-written TextFSM templates for hundreds of
standard Cisco (and other vendor) show commands —
installed automatically with Netmiko. Setting
use_textfsm=True in send_command()
activates this automatic parsing:
# textfsm_parse.py # Use use_textfsm=True to get structured data automatically from netmiko import ConnectHandler from netmiko.exceptions import NetmikoTimeoutException, NetmikoAuthenticationException device = { 'device_type': 'cisco_ios', 'host': '192.168.10.1', 'username': 'netauto', 'password': 'AutoPass2026!', } try: with ConnectHandler(**device) as conn: # use_textfsm=True: Netmiko selects the correct NTC-Templates # template for 'cisco_ios' + 'show ip interface brief' # and returns a list of dicts instead of a raw string interfaces = conn.send_command( "show ip interface brief", use_textfsm=True ) # interfaces is now a list of dicts — no manual parsing needed print(f"Parsed {len(interfaces)} interfaces:\n") # ── Print all interfaces ────────────────────────────────────────── for iface in interfaces: print(f" {iface['intf']:<28} {iface['ipaddr']:<18}" f" {iface['status']:<22} {iface['proto']}") # ── Filter: only UP/UP interfaces with a real IP ────────────────── print("\n--- Active interfaces with assigned IPs ---") active = [ i for i in interfaces if i['status'] == 'up' and i['proto'] == 'up' and i['ipaddr'] != '' ] for i in active: print(f" {i['intf']}: {i['ipaddr']}") except NetmikoTimeoutException: print("ERROR: Connection timed out.") except NetmikoAuthenticationException: print("ERROR: Authentication failed.")
TextFSM Output — Key Names for show ip interface brief
| TextFSM Key | Maps to Field | Example Value |
|---|---|---|
intf |
Interface name | GigabitEthernet0/0 |
ipaddr |
IP address (empty string if unassigned) | 192.168.10.1 |
status |
Interface line status | up, administratively down |
proto |
Layer 2 protocol status | up, down |
use_textfsm=True uses
NTC-Templates (the larger, more mature community library)
and use_ttp=True uses TTP (Template Text Parser,
a newer alternative with a different template syntax). For
show ip interface brief and most standard Cisco
IOS commands, use_textfsm=True with NTC-Templates
is the recommended choice — it has been battle-tested
across thousands of IOS versions and device types. If
send_command() returns a raw string instead of
a list when use_textfsm=True is set, it means
no NTC-Template was found for that command/device_type
combination — fall back to manual parsing.
9. Step 7 — Run Against Multiple Devices
The real automation payoff comes when the same script runs against a list of devices. A function encapsulates the per-device logic; a loop drives it across the device inventory:
# multi_device.py # Poll show ip interface brief from multiple routers from netmiko import ConnectHandler from netmiko.exceptions import NetmikoTimeoutException, NetmikoAuthenticationException # ── Device inventory ────────────────────────────────────────────── # In production: load this from a YAML/JSON file or a CMDB DEVICES = [ {'device_type': 'cisco_ios', 'host': '192.168.10.1', 'username': 'netauto', 'password': 'AutoPass2026!'}, {'device_type': 'cisco_ios', 'host': '192.168.10.2', 'username': 'netauto', 'password': 'AutoPass2026!'}, {'device_type': 'cisco_ios', 'host': '192.168.10.3', 'username': 'netauto', 'password': 'AutoPass2026!'}, ] def get_interfaces(device_dict): """Connect to one device, return parsed interface list or error string.""" try: with ConnectHandler(**device_dict) as conn: return conn.send_command("show ip interface brief", use_textfsm=True) except NetmikoTimeoutException: return f"TIMEOUT: {device_dict['host']} unreachable" except NetmikoAuthenticationException: return f"AUTH FAIL: {device_dict['host']}" except Exception as e: return f"ERROR: {device_dict['host']} — {e}" # ── Poll all devices sequentially ──────────────────────────────── for dev in DEVICES: print(f"\n{'='*60}") print(f"Device: {dev['host']}") print(f"{'='*60}") result = get_interfaces(dev) if isinstance(result, str): # Error string returned — print and move on print(result) else: # List of dicts returned — display interface table for iface in result: status_flag = "✓" if iface['status'] == 'up' else "✗" print(f" {status_flag} {iface['intf']:<26} {iface['ipaddr']:<18} {iface['status']}/{iface['proto']}")
Multi-Device Output
============================================================ Device: 192.168.10.1 ============================================================ ✓ GigabitEthernet0/0 192.168.10.1 up/up ✓ GigabitEthernet0/1 192.168.20.1 up/up ✗ GigabitEthernet0/2 unassigned administratively down/down ✓ Loopback0 10.255.255.1 up/up ============================================================ Device: 192.168.10.2 ============================================================ ✓ GigabitEthernet0/0 192.168.10.2 up/up ✓ GigabitEthernet0/1 192.168.30.1 up/up ✓ Loopback0 10.255.255.2 up/up ============================================================ Device: 192.168.10.3 ============================================================ TIMEOUT: 192.168.10.3 unreachable
get_interfaces() with its own
try/except block — a timeout or auth failure on one
device does not affect the others. The main loop simply
calls the function and handles whatever comes back: a list
of dicts (success) or an error string (failure). For large
device inventories (50+ devices), consider using Python's
concurrent.futures.ThreadPoolExecutor to poll
devices in parallel rather than sequentially — each
SSH connection takes 2–5 seconds, so sequential polling of
100 devices would take 3–8 minutes while parallel polling
could complete in under 30 seconds.
10. Step 8 — Save Output to File and Export to CSV
Structured data can be written to CSV for reporting in spreadsheet tools or to a JSON file for integration with other systems:
# save_output.py # Export parsed interface data to CSV and JSON import csv import json from datetime import datetime from netmiko import ConnectHandler device = { 'device_type': 'cisco_ios', 'host': '192.168.10.1', 'username': 'netauto', 'password': 'AutoPass2026!', } with ConnectHandler(**device) as conn: hostname = conn.find_prompt().strip('#') interfaces = conn.send_command("show ip interface brief", use_textfsm=True) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # ── Add device hostname and timestamp to each record ───────────── for iface in interfaces: iface['device'] = hostname iface['timestamp'] = timestamp # ── Write CSV ───────────────────────────────────────────────────── csv_file = f"interfaces_{hostname}_{timestamp}.csv" with open(csv_file, 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=['device','intf','ipaddr','status','proto','timestamp']) writer.writeheader() writer.writerows(interfaces) print(f"CSV written: {csv_file}") # ── Write JSON ──────────────────────────────────────────────────── json_file = f"interfaces_{hostname}_{timestamp}.json" with open(json_file, 'w') as f: json.dump(interfaces, f, indent=2) print(f"JSON written: {json_file}")
JSON Output Sample
[
{
"intf": "GigabitEthernet0/0",
"ipaddr": "192.168.10.1",
"status": "up",
"proto": "up",
"device": "NetsTuts-R1",
"timestamp": "20260307_143022"
},
{
"intf": "GigabitEthernet0/2",
"ipaddr": "",
"status": "administratively down",
"proto": "down",
"device": "NetsTuts-R1",
"timestamp": "20260307_143022"
}
]
11. Troubleshooting Netmiko Connection Issues
| Problem | Error / Symptom | Cause | Fix |
|---|---|---|---|
| Connection timed out immediately | NetmikoTimeoutException: Connection to device timed-out after a few seconds |
Device IP unreachable, wrong IP in device dict, SSH not enabled on router, TCP/22 blocked by ACL or firewall | Test reachability first: ping 192.168.10.1 from the workstation. Test SSH manually: ssh [email protected] — if this works but Netmiko fails, check Python version or paramiko version. On the router, verify show ip ssh shows SSH version 2 enabled and transport input ssh is on the VTY lines |
| Authentication fails | NetmikoAuthenticationException: Authentication failure |
Wrong username or password in the device dict, account not configured on the router (username command missing), VTY using login (no authentication) instead of login local, or AAA rejecting the credential |
Test with ssh [email protected] interactively and enter the password manually. On the router, check: show running-config | section username and show running-config | section line vty. Verify login local is set. If AAA is active, check show aaa servers |
send_command() returns raw string instead of list when use_textfsm=True |
The function returns a plain string (the raw CLI output) instead of a list of dicts, even with use_textfsm=True |
No NTC-Templates template exists for this command + device_type combination, or the ntc-templates package version does not include the template, or the command output format does not match the template (IOS version differences) | Verify ntc-templates is installed: pip show ntc-templates. Check available templates: python3 -c "import ntc_templates; print(ntc_templates.__file__)" then browse the templates directory. If no template exists, use manual regex parsing. Use isinstance(result, list) to detect whether parsing succeeded before processing the result |
| SSH key algorithm mismatch | SSHException: Error reading SSH protocol banner or No suitable authentication method found |
Older Cisco IOS SSH implementations use RSA key exchange algorithms (diffie-hellman-group1-sha1) that newer OpenSSH and Paramiko versions disabled for security reasons | Add SSH config options to the device dict: 'ssh_config_file': '' and 'disabled_algorithms': {'kex': []} to permit weaker algorithms. Or upgrade the IOS SSH key: crypto key zeroize rsa → crypto key generate rsa modulus 2048 on the router to use a stronger key that modern Paramiko accepts |
Script hangs indefinitely on send_command() |
Script appears to freeze after sending the command. No output returned, no timeout raised | The command produced output that includes a --More-- prompt that Netmiko did not suppress (happens if terminal length 0 was not accepted by the device — unusual), or a very long-running command, or the device crashed mid-output |
Add an explicit read_timeout: conn.send_command("show ...", read_timeout=30). Manually verify terminal length 0 works on the device via SSH. Check device_type is correct — a wrong device type may send the wrong paging-disable command |
| Netmiko enable password prompt | Script connects but output shows the user-exec prompt (>) and commands return % Invalid input detected for show commands requiring privilege exec |
SSH user has privilege lower than 15 and enable was not provided in the device dict. Netmiko requires a secret key to automatically send the enable password |
Add 'secret': 'enable_password_here' to the device dict. Netmiko will automatically send enable and the secret during connection setup. The connection object will be at the # prompt. Alternatively, configure privilege 15 on the SSH user on the router to bypass enable entirely |
Enabling Netmiko Debug Logging
# debug_logging.py # Enable Netmiko debug output to see exactly what is sent/received import logging # Set Netmiko logger to DEBUG — prints all SSH channel traffic logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger('netmiko') # Now run ConnectHandler — terminal shows every byte sent and received from netmiko import ConnectHandler conn = ConnectHandler(device_type='cisco_ios', host='192.168.10.1', username='netauto', password='AutoPass2026!')
logging.WARNING.
Key Points & Exam Tips
- Netmiko is a Python library built on Paramiko that abstracts SSH sessions to network devices. Its key value is vendor-specific driver support: it handles prompt detection, pagination (
terminal length 0is sent automatically), and enable mode for over 80 device types. Install withpip install netmiko. - The device dictionary is the core pattern: all connection parameters (
device_type,host,username,password) are passed as a dictionary toConnectHandler(**device). This pattern makes it trivial to loop over a list of device dicts to poll multiple devices. - The
device_typeparameter selects the correct driver.cisco_ioscovers IOS and IOS-XE. Usecisco_nxosfor Nexus,cisco_xrfor IOS-XR,cisco_asafor ASA. The wrong device_type causes prompt detection failures or wrong paging-disable commands. send_command()sends a single command and returns the full output as a string once the device prompt reappears. It is the primary method for show commands.send_config_set()accepts a list of configuration commands, enters config mode, sends them, and exits — used for making changes.- Always use the
withcontext manager:with ConnectHandler(**device) as conn:. This guaranteesdisconnect()is called even if an exception occurs, preventing VTY line exhaustion on the device. use_textfsm=Trueinsend_command()activates automatic parsing via NTC-Templates (installed with Netmiko). For standard show commands likeshow ip interface brief, it returns a list of dicts instead of a raw string — eliminating the need to write custom regex. If no template matches, it falls back to returning the raw string.- The two critical Netmiko exceptions to handle are
NetmikoTimeoutException(device unreachable) andNetmikoAuthenticationException(wrong credentials). Always wrap connection code in try/except to prevent one failed device from aborting a multi-device script. - For privilege levels: configure the automation user with
privilege 15on the router to start directly at the#prompt. If the user has a lower privilege, add'secret': 'enable_password'to the device dict and Netmiko will sendenableautomatically during connection setup. See Login Security & Brute-Force Protection to harden the VTY lines that Netmiko connects to. - For large device inventories, parallel execution with
concurrent.futures.ThreadPoolExecutordramatically reduces total runtime. SSH connections are I/O-bound (waiting for the network), making them ideal candidates for threading — 50 parallel connections complete in roughly the same time as one sequential connection. - For the CCNA automation objectives: understand the purpose of Netmiko (
ConnectHandlerfor SSH connection), the device dictionary pattern,send_command()for show commands,send_config_set()for configuration, and the importance ofdisconnect()/ context manager for clean session management. For additional Python networking scripts see Python Automation Scripts.
os.environ.get('NET_PASSWORD')) or a secrets
manager like HashiCorp Vault or Python's
keyring library. For running configuration
changes (not just show commands) over the same Netmiko
connection, see how send_config_set() extends
the pattern shown in this lab.