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:
| 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
| 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.
TEST WHAT YOU LEARNED
A Python script uses Netmiko to connect to a Cisco router and run show ip interface brief. The device dictionary is correct, but after running the script three times it crashes partway through, and now no one can SSH into the router. What caused this and how is it prevented?
line vty 0 4 provides exactly five concurrent SSH sessions. Every ConnectHandler call opens one SSH session that occupies one VTY line. A normal script calls disconnect() at the end to release the line. A script that crashes mid-execution (exception, KeyboardInterrupt, system error) without a proper disconnect leaves the SSH session open on the router. The session persists until the VTY exec-timeout (default 10 minutes) expires. If three crashes happen in quick succession before the first sessions timeout, and two human operators are also SSHed in, all five VTY lines are occupied and any new SSH attempt is rejected with "All VTY lines are in use." The fix is the context manager: with ConnectHandler(**device) as conn:. Python guarantees the __exit__ method (which calls disconnect()) runs when the with block exits — whether normally, by exception, or by interrupt. This is the single most important Netmiko best practice for production scripts. As a recovery measure: on the router console, run clear line vty [0-4] to forcibly terminate the stale sessions.What does ConnectHandler(**device) actually do step by step when called with a Cisco IOS device dict, before the first send_command() is ever called?
ConnectHandler.__init__(): (1) The device_type value selects the correct driver class — for cisco_ios, this is the CiscoIosSSH class. (2) Paramiko opens a TCP connection to port 22 (or the specified port). (3) SSH key exchange and authentication are performed — this is where NetmikoAuthenticationException is raised if credentials are wrong. (4) The driver waits for the initial CLI prompt to appear and captures it as a regex pattern. (5) If the prompt ends with > (user exec) and a secret was provided, Netmiko sends enable + the secret to reach the # prompt. (6) The driver sends terminal length 0 (or the equivalent for the device type — NX-OS uses terminal length 0, JunOS uses set cli screen-length 0) to disable pagination. Only after all of these steps is the connection object returned and available for send_command(). This is why connection failures raise exceptions at the ConnectHandler line, not at the first send_command().When send_command("show ip interface brief", use_textfsm=True) returns a plain string instead of a list of dictionaries, what does this indicate and how should the script handle it?
use_textfsm=True is set, Netmiko attempts to look up a TextFSM template from the ntc-templates library using the device_type and command string as keys. The template lookup maps: cisco_ios + show ip interface brief → template file → parse raw output → return list of dicts. If no template is found (unknown command, niche platform variation, very new command not yet in ntc-templates), Netmiko returns the raw string with no error raised — it silently falls back. This silent fallback is a common source of bugs: code that assumes structured data (e.g., for iface in result: print(iface['intf'])) will throw a TypeError when iterating over a string instead of a list. The defensive pattern is always: if isinstance(result, list): [process structured data] else: [handle raw string or log failure]. TextFSM and ntc-templates are both installed automatically as dependencies when you run pip install netmiko, so missing packages are not typically the cause.What is the difference between send_command() and send_config_set() in Netmiko, and why can you not use send_command() to send configuration commands like interface GigabitEthernet0/1?
send_command() works by: sending the command → waiting for the device prompt pattern (e.g., NetsTuts-R1#) to reappear in the output → returning everything between the command and the prompt. Show commands work perfectly with this pattern because the prompt reappears immediately after the output. Configuration commands break this pattern: sending configure terminal changes the prompt to NetsTuts-R1(config)#. send_command() is waiting for NetsTuts-R1# which never appears — the function hangs until read_timeout is exceeded. send_config_set() is purpose-built for this: it knows to send configure terminal first (changing to config mode), then send each item in the list as a separate command (accepting intermediate config sub-prompts), then send end to return to the privileged exec prompt. It handles the prompt transitions gracefully. You can technically use send_command() to enter config mode by passing expect_string=r'\(config\)#' to override the prompt it waits for — but send_config_set() is cleaner, correct, and the intended method.A script connects to the router successfully but show ip route returns a truncated output — the last several routes are missing. The same command in a manual SSH session returns the full routing table. What is the most likely cause?
--More-- prompts after every screen (terminal length lines). Netmiko disables this by sending terminal length 0 during the connection setup phase. If this command fails or is not sent (wrong device_type causes the wrong pagination-disable command to be sent, or an older IOS version uses a different command), the --More-- prompt appears in the output. send_command() detects the device prompt (#) pattern — not --More--. If it sees --More--, it may: (a) return the output up to that point if the wait pattern times out, or (b) in some Netmiko versions, get confused. The output truncation is caused by the --More-- interrupting the data stream. Diagnostic: print the raw output — if it ends with --More-- or lacks the final lines, pagination is the cause. Resolution: confirm the device_type is correct, or call conn.send_command("terminal length 0") manually right after connecting to force pagination off.Why must the automation user be configured with privilege 15 on the Cisco router, or alternatively have a secret key in the Netmiko device dictionary?
>, entered by default after SSH login with a non-privilege-15 account) and privileged exec (prompt ends with #, entered with enable command). User exec has a very limited command set — basic ping, traceroute, and a few show commands. Most diagnostic show commands including show ip interface brief, show running-config, show ip route, and virtually all commands useful for automation require privilege exec. When Netmiko connects and the session starts at >, commands that require privilege exec return % Invalid input detected at '^' marker or appear absent from the CLI. Netmiko handles this with the secret parameter: if secret is provided in the device dict and the post-login prompt is >, Netmiko automatically sends enable followed by the secret value to transition to #. The cleanest solution for automation is configuring the user with username netauto privilege 15 secret AutoPass2026! on the router — the session starts directly at # and no enable process is needed, reducing connection setup complexity.A script runs show ip interface brief against 50 routers sequentially. Each connection takes approximately 4 seconds. Total runtime is about 200 seconds (3.3 minutes). A colleague suggests using concurrent.futures.ThreadPoolExecutor instead. Why would this reduce the runtime dramatically, and is there a risk?
ip ssh maxstartups), and a small amount of router CPU for SSH encryption. For production environments, limit the thread pool size to a number that respects device VTY line counts — typically max_workers=10 is safe for most deployments.Why is it a security risk to hardcode credentials (username and password) directly in a Python script file, and what are two safer alternatives?
'password': 'AutoPass2026!' — the password is plain text in the source file. Risks: (1) Git commits: developers frequently commit scripts to repositories (internal or accidentally public GitHub repos) without realising credentials are embedded. Once committed, credentials appear in git history permanently even after removal. (2) File sharing: scripts shared via email, Slack, or USB drives include the credentials. (3) Backups: backup systems store the file, potentially in less-secured locations. (4) File system access: any user on the workstation with read access to the file can see the credentials. The two standard mitigations are: Environment variables — set the credential as an OS environment variable (export NET_PASSWORD='AutoPass2026!' on Linux/macOS, setx NET_PASSWORD "AutoPass2026!" on Windows) and read it in Python with import os; password = os.environ.get('NET_PASSWORD'). The script file contains no credential. getpass — import getpass; password = getpass.getpass("Router password: ") prompts the user at runtime with no terminal echo. For fully automated scripts (cron jobs, CI/CD pipelines), use a secrets manager like HashiCorp Vault, AWS Secrets Manager, or Python's keyring library.What does the ** operator do in ConnectHandler(**device), and why is the device dictionary pattern preferred over listing parameters individually?
** in a function call is Python's keyword argument unpacking operator. Given device = {'device_type': 'cisco_ios', 'host': '192.168.10.1', 'username': 'netauto', 'password': 'AutoPass2026!'}, writing ConnectHandler(**device) is syntactically identical to ConnectHandler(device_type='cisco_ios', host='192.168.10.1', username='netauto', password='AutoPass2026!'). Every key becomes a keyword argument name and every value becomes the argument value. The dictionary pattern is architecturally superior for automation: (1) Inventory as data: store device parameters in a YAML file (devices.yaml), load it with PyYAML (yaml.safe_load()), and iterate: for dev in devices: ConnectHandler(**dev). (2) Reusable code: the connection function never needs to change; only the data changes. (3) Conditional parameters: easily add 'secret': enable_pass or 'port': 2222 to specific device dicts without modifying the function. (4) Separation of concerns: device inventory is data, connection logic is code — the correct separation for maintainable automation.A network engineer writes a Netmiko script to audit all routers and report which interfaces are administratively down. The script uses use_textfsm=True and filters for status == 'administratively down'. On one router, the command returns successfully but the filter finds nothing even though show ip interface brief clearly shows two administratively down interfaces. What is the most likely cause?
cisco_ios_show_ip_interface_brief may store this as 'admin down' (truncated to match the abbreviated form seen in some IOS versions) or in a slightly different format. The exact string stored in the TextFSM-parsed dict depends on the template definition, not the raw output. The debugging approach: after calling send_command(..., use_textfsm=True), print the full list of dicts and inspect the exact values: for iface in interfaces: print(iface). You will see the exact string the template produces (e.g., 'status': 'admin down' vs 'status': 'administratively down'). Update the filter accordingly. The robust pattern is: if 'down' in iface['status'].lower() which catches all variants of down status regardless of the exact wording. Always print the raw parsed data during development to understand what TextFSM is actually returning before writing filters against it.Related Topics & Step-by-Step Tutorials
Related concepts and next steps:
- Reading and Interpreting Python Scripts — Netmiko library — SSH-based CLI automation
- Python for Network Engineers — Python networking overview
- Python NAPALM — Multi-Vendor Network Automation
- Ansible Playbook — Automate Cisco IOS Configuration
- Cisco IOS RESTCONF Basics — Query and Configure with…
- NETCONF with ncclient (Python)
- Jinja2 Templates for Config Generation