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 vs enable secret in Netmiko. If the SSH user is configured with 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
  
Installing into a virtual environment (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.
  
The **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
  
If an exception occurs inside the 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
  
The regex pattern uses named capture groups in the Python 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 vs use_ttp. Netmiko supports two auto-parsing backends: 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
  
The function-per-device pattern is the correct architecture for multi-device scripts. Each device runs independently inside 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 rsacrypto 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!')
  
Debug logging shows every character sent to and received from the device's SSH channel — prompts, commands, output, timing. This is the first tool to use when Netmiko behaves unexpectedly. The debug output will show exactly which prompt pattern Netmiko is waiting for and whether it ever appears, diagnosing hangs or unexpected prompt issues immediately. Disable debug logging in production scripts by removing or changing the log level to 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 0 is sent automatically), and enable mode for over 80 device types. Install with pip install netmiko.
  • The device dictionary is the core pattern: all connection parameters (device_type, host, username, password) are passed as a dictionary to ConnectHandler(**device). This pattern makes it trivial to loop over a list of device dicts to poll multiple devices.
  • The device_type parameter selects the correct driver. cisco_ios covers IOS and IOS-XE. Use cisco_nxos for Nexus, cisco_xr for IOS-XR, cisco_asa for 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 with context manager: with ConnectHandler(**device) as conn:. This guarantees disconnect() is called even if an exception occurs, preventing VTY line exhaustion on the device.
  • use_textfsm=True in send_command() activates automatic parsing via NTC-Templates (installed with Netmiko). For standard show commands like show 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) and NetmikoAuthenticationException (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 15 on 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 send enable automatically 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.ThreadPoolExecutor dramatically 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 (ConnectHandler for SSH connection), the device dictionary pattern, send_command() for show commands, send_config_set() for configuration, and the importance of disconnect() / context manager for clean session management. For additional Python networking scripts see Python Automation Scripts.
Next Steps: With Netmiko established as the SSH connection layer, the natural progression is to add structured data management. Load device inventories from YAML files using PyYAML instead of hardcoding lists. Use Nornir (a Python automation framework built on Netmiko and NAPALM) to add inventory management, task runners, and parallel execution with a clean programmatic interface. For structured configuration management using XML and YANG models instead of screen scraping, see NETCONF with ncclient (Python). For fleet-wide automation without per-device Python scripts, see Ansible IOS Configuration. For the SSH configuration that Netmiko connects to, review SSH Configuration. For securing the automation credentials against exposure in plain-text scripts, consider environment variables (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

1. 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?

Correct answer is B. Cisco IOS VTY lines are a finite resource — 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.

2. 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?

Correct answer is D. This is the full sequence Netmiko performs inside 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().

3. 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?

Correct answer is A. When 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.

4. 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?

Correct answer is C. This is a fundamental Netmiko concept. 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.

5. 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?

Correct answer is D. This is the classic Netmiko pagination issue. By default, Cisco IOS paginates long outputs with --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.

6. 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?

Correct answer is B. Cisco IOS has two primary exec modes: user exec (prompt ends with >, 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.

7. 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?

Correct answer is C. Python's Global Interpreter Lock (GIL) is a well-known limitation that prevents true parallel CPU execution across threads. However, the GIL is released during I/O operations — when a thread is waiting for data from the network (which represents the vast majority of an SSH session's time), other threads can run freely. A 4-second SSH session to a Cisco router breaks down roughly as: ~1s TCP handshake + SSH negotiation, ~1s authentication, ~1s sending terminal length 0 and getting to prompt, ~0.5s sending the command and receiving output, ~0.5s disconnect. All of this time is network I/O, not CPU computation. Threading allows 20 (or more) of these I/O waits to happen concurrently. The practical speedup for 50 devices with 20 workers: 50 devices / 20 parallel workers × 4 seconds ≈ 10 seconds total, vs 50 × 4 = 200 seconds sequential. The genuine risk is router resource exhaustion: each simultaneous connection consumes one VTY line, one SSH session (limited by 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.

8. Why is it a security risk to hardcode credentials (username and password) directly in a Python script file, and what are two safer alternatives?

Correct answer is A. This is one of the most common security mistakes in network automation. Scripts with hardcoded credentials look like: '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. getpassimport 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.

9. What does the ** operator do in ConnectHandler(**device), and why is the device dictionary pattern preferred over listing parameters individually?

Correct answer is D. The double-asterisk ** 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.

10. 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?

Correct answer is C. This is a common real-world Netmiko/TextFSM debugging scenario. The raw CLI output shows "administratively down" but the NTC-Templates TextFSM template for 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.