Python Netmiko Network Automation — Deep Dive

Netmiko’s architecture

Netmiko inherits from BaseConnection, which wraps a Paramiko SSH channel. Each vendor class (e.g., CiscoIosSSH, JuniperSSH) overrides methods to handle vendor-specific behavior:

  • session_preparation() — runs after SSH connection: sets terminal width, disables pagination
  • config_mode() / exit_config_mode() — enters and exits configuration mode
  • commit() — for platforms like Junos that require explicit commits
  • save_config() — persists running configuration
  • check_config_mode() — verifies the current prompt indicates config mode

This class hierarchy means you can subclass any vendor driver to handle custom prompts or modified firmware behavior.

Connection parameters in depth

device = {
    "device_type": "cisco_ios",
    "host": "10.0.0.1",
    "username": "admin",
    "password": "secret",
    "secret": "enable_password",      # Enable/privilege mode password
    "port": 22,
    "timeout": 30,                     # TCP connection timeout
    "auth_timeout": 30,                # SSH auth timeout
    "banner_timeout": 30,              # Pre-auth banner timeout
    "session_timeout": 60,             # Idle session timeout
    "global_delay_factor": 2,          # Multiply all delays (slow devices)
    "fast_cli": True,                  # Disable delays for fast devices
    "session_log": "session.log",      # Log all I/O to file
    "session_log_record_writes": True, # Include sent commands in log
    "conn_timeout": 10,                # Paramiko connect timeout
    "ssh_config_file": "~/.ssh/config",
}

Delay factors

Network devices vary wildly in response time. A Cisco Catalyst 2960 responds in milliseconds; an old Cisco ASA might take seconds. The global_delay_factor multiplies all internal sleep timers:

  • 1 (default) — standard timing
  • 2 — double all waits (for slow WAN links or overloaded devices)
  • 4 — very conservative (for devices responding over satellite links)

Alternatively, fast_cli=True sets delays to near-zero for modern, fast devices. Do not use both simultaneously.

Concurrent device access

Threading approach

from concurrent.futures import ThreadPoolExecutor, as_completed
from netmiko import ConnectHandler

def get_version(device):
    try:
        with ConnectHandler(**device) as conn:
            output = conn.send_command("show version", use_textfsm=True)
            return {"host": device["host"], "status": "success", "data": output}
    except Exception as e:
        return {"host": device["host"], "status": "error", "error": str(e)}

devices = [...]  # List of device dicts
results = []

with ThreadPoolExecutor(max_workers=20) as executor:
    futures = {executor.submit(get_version, d): d for d in devices}
    for future in as_completed(futures):
        results.append(future.result())

# Report
for r in results:
    if r["status"] == "error":
        print(f"FAILED: {r['host']}{r['error']}")
    else:
        print(f"OK: {r['host']}")

20 concurrent workers can audit 200 devices in under a minute instead of the 30+ minutes sequential access would take. Keep max_workers reasonable — each connection holds an SSH socket, and your machine has file descriptor limits.

Integration with Nornir

For fleet-scale automation, Nornir provides inventory management and task execution on top of Netmiko:

from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command

nr = InitNornir(config_file="nornir_config.yaml")

# Run a command on all devices in inventory
result = nr.run(task=netmiko_send_command, command_string="show ip route")

for host, host_result in result.items():
    print(f"{host}: {host_result.result[:100]}")

Nornir handles threading, inventory (from YAML, NetBox, or custom sources), and result aggregation. Netmiko handles the actual device communication.

SCP file transfers

Netmiko includes SCP support for devices that allow it:

from netmiko import ConnectHandler, file_transfer

device = {
    "device_type": "cisco_ios",
    "host": "10.0.0.1",
    "username": "admin",
    "password": "secret",
}

with ConnectHandler(**device) as conn:
    transfer = file_transfer(
        conn,
        source_file="firmware.bin",
        dest_file="firmware.bin",
        file_system="flash:",
        direction="put",
        overwrite_file=True,
    )
    print(f"Transfer: {transfer}")

The file_transfer function checks if the file already exists (by MD5 hash), verifies available space, and handles the SCP transfer. This is essential for firmware upgrades across a fleet.

Session logging and debugging

Full session logging

conn = ConnectHandler(
    **device,
    session_log="debug_session.log",
    session_log_record_writes=True,
    session_log_file_mode="append",
)

The session log captures every byte sent and received over the SSH channel. This is invaluable for debugging prompt detection issues or understanding why a command failed on a specific device.

Debug-level logging

import logging
logging.basicConfig(filename="netmiko_debug.log", level=logging.DEBUG)
logger = logging.getLogger("netmiko")

This logs Netmiko’s internal decision-making: which prompt patterns it tried, how long it waited, and when it detected config mode transitions.

Custom device types

For unsupported or modified devices, create a custom driver:

from netmiko.cisco_base_connection import CiscoBaseConnection

class CustomSwitch(CiscoBaseConnection):
    def session_preparation(self):
        """Custom prep for this device."""
        self._test_channel_read(pattern=r"CUSTOM>")
        self.set_base_prompt()
        self.disable_paging(command="terminal length 0")

    def config_mode(self, config_command="configure", pattern=r"\(config\)#"):
        return super().config_mode(config_command=config_command, pattern=pattern)

    def save_config(self, cmd="save running", confirm=True, confirm_response="y"):
        return super().save_config(
            cmd=cmd, confirm=confirm, confirm_response=confirm_response
        )

# Register and use
CLASS_MAPPER["custom_switch"] = CustomSwitch

device = {"device_type": "custom_switch", "host": "10.0.0.1", ...}

Error handling patterns

Timeout recovery

from netmiko.exceptions import NetmikoTimeoutException, NetmikoAuthenticationException

def safe_command(device, command, retries=2):
    for attempt in range(retries + 1):
        try:
            with ConnectHandler(**device) as conn:
                return conn.send_command(command, read_timeout=30)
        except NetmikoTimeoutException:
            if attempt < retries:
                log.warning(f"Timeout on {device['host']}, retry {attempt + 1}")
                time.sleep(5)
            else:
                log.error(f"Timeout on {device['host']} after {retries + 1} attempts")
                raise
        except NetmikoAuthenticationException:
            log.error(f"Auth failed on {device['host']}")
            raise  # No point retrying auth failures

Configuration validation

def safe_config_push(conn, commands, validation_command, expected_output):
    """Push config and validate it took effect."""
    conn.send_config_set(commands)

    # Verify
    output = conn.send_command(validation_command)
    if expected_output not in output:
        log.error(f"Validation failed on {conn.host}. Rolling back...")
        conn.send_config_set(["no " + cmd for cmd in reversed(commands)])
        raise RuntimeError(f"Config validation failed on {conn.host}")

    conn.save_config()
    log.info(f"Config applied and validated on {conn.host}")

Security considerations

  • Credential storage — never hardcode passwords. Use environment variables, HashiCorp Vault, or CyberArk
  • SSH keys — prefer key-based auth over passwords: "use_keys": True, "key_file": "/path/to/key"
  • Jump hosts — Netmiko supports SSH proxy commands via ssh_config_file for bastion host access
  • Audit trails — session logs provide a complete record of what was sent to each device
  • Least privilege — use read-only accounts for show commands and separate accounts for configuration changes

Performance benchmarks

OperationSingle device100 devices (20 threads)
Connect + show version~2s~12s
Connect + 10 show commands~8s~45s
Connect + config push (5 lines)~5s~30s
Connect + firmware transfer (50MB)~120s~300s (4 threads)

The bottleneck is almost always the device, not Netmiko. Slow devices, WAN latency, and SSH key exchange dominate execution time.

The one thing to remember: Netmiko handles the messy reality of multi-vendor network CLIs — but production automation demands concurrent execution, robust error handling, session logging, and proper credential management on top of the basics.

pythonautomationnetworkingsshdevops

See Also

  • Python Fabric Remote Execution Run commands on faraway computers from your desk using Python Fabric — like a universal remote for servers.
  • Python Invoke Task Runner Automate boring computer chores with Python Invoke — like teaching your computer a recipe book of tasks.
  • Python Schedule Task Scheduling Make Python run tasks on a timer — like setting an alarm clock for your code.
  • Python Watchdog File Monitoring Let your Python program notice when files change — like a guard dog that barks whenever someone touches your stuff.
  • Ci Cd Why big apps can ship updates every day without turning your phone into a glitchy mess — CI/CD is the behind-the-scenes quality gate and delivery truck.