Netmiko Python Automation for Network Engineers: Getting Started - 夜莺博客

Netmiko Python Automation for Network Engineers: Getting Started

Netmiko is the Python library that makes network automation approachable: it wraps SSH sessions to network devices and gives you simple methods like send_command() and send_config_set(), handling all the vendor quirks (prompts, pagination, privilege levels) under the hood. Created by Kirk Byers, it supports 60+ platforms including Cisco IOS/IOS-XR/NX-OS, Juniper Junos, Arista EOS, Huawei VRP and Aruba. This article shows you how to install it, connect to a device, run commands, push configuration, and write a practical config-backup script you can run every night.

The reason Netmiko exists is worth one paragraph. If you have ever scripted a device with raw Paramiko or paramiko.exec_command(), you know the pattern: open an interactive shell, wait for the prompt, send text, read output, figure out where the output ends, detect whether the device asked a follow-up question, and strip the prompt from the result. Netmiko does all of that and abstracts it behind a device_type string. It is deliberately a low-level library — it speaks SSH and CLI, not intent — which is exactly what makes it the fastest route from "I know the CLI" to "I have a working script".

Installation

pip install netmiko

Netmiko needs Python 3.7+ and uses Paramiko for SSH. Optional extras: pip install netmiko[textfsm] enables structured output parsing.

A virtual environment is strongly recommended, because network automation scripts accumulate dependencies quickly (TextFSM, NTC templates, YAML, a secrets library) and you do not want them fighting the system Python:


python3 -m venv netauto
source netauto/bin/activate
pip install netmiko
pip show netmiko | grep -i version

Netmiko 4.x changed a few defaults from the 3.x line — most notably that send_command() no longer performs a pre-read disable by default and that failed authentication raises NetmikoAuthenticationException rather than a generic exception — so pin the version in production and read the release notes before upgrading a fleet of scripts.

First Connection and Command Execution

from netmiko import ConnectHandler

device = {
    "device_type": "cisco_ios",
    "host": "192.168.1.10",
    "username": "admin",
    "password": "secret",
    "port": 22,
}

with ConnectHandler(**device) as conn:
    output = conn.send_command("show ip interface brief")
    print(output)

device_type is the most important field - pick from values like cisco_ios, cisco_xr, juniper_junos, arista_eos, huawei, dell_os10, linux. Using with closes the connection automatically.

Three parameters you will want almost immediately. read_timeout (default 10 seconds) is how long Netmiko waits for output to stop arriving — long commands such as show tech-support or a large show running-config on a busy chassis need 60 seconds or more. fast_cli (default True) shortens delays for interactive commands but can break some devices, in which case set it to False. session_log writes a complete transcript of the session to a file, which is the single most valuable debugging tool when a script behaves differently in production than it did in the lab:


device = {
    "device_type": "cisco_ios",
    "host": "192.168.1.10",
    "username": "admin",
    "password": "secret",
    "read_timeout": 60,
    "fast_cli": False,
    "session_log": "logs/192.168.1.10.log",
}
with ConnectHandler(**device) as conn:
    print(conn.find_prompt())
    print(conn.send_command("show version"))

find_prompt() returns the current prompt, which is the quickest sanity check that you are logged in as the privilege level you think you are. If the device needs enable mode, either pass secret="..." in the dictionary or call conn.enable() explicitly — Netmiko will not elevate silently.

Choosing the Right device_type

device_type is not cosmetic: it selects the prompt pattern, the paging-disable command, the config-mode entry and exit strings, and the behaviour of send_command() when the device asks a follow-up question. A wrong value produces the classic symptom of a script that connects successfully and then hangs or returns empty output. The values worth memorising:


cisco_ios        Cisco IOS and IOS-XE (Catalyst, ISR, ASR)
cisco_xe         Explicit IOS-XE driver (preferred for newer Catalyst)
cisco_xr         Cisco IOS-XR (ASR 9000, NCS)
cisco_nxos       Cisco NX-OS (Nexus)
cisco_asa        Cisco ASA firewalls
juniper_junos    Juniper Junos (EX, MX, SRX, QFX)
arista_eos       Arista EOS
huawei           Huawei VRP (S-series, CE)
hp_comware       HPE Comware (FlexFabric, H3C compatible)
aruba_aoscx      ArubaOS-CX (6000/6100/6300/6400)
dell_os10        Dell EMC SmartFabric OS10
dell_os6         Dell N-series (PowerConnect lineage)
fortinet         FortiGate FortiOS
mikrotik_routeros  MikroTik RouterOS
linux            Generic Linux host over SSH

If you are unsure, netmiko.ssh_dispatcher.class_mapper() lists every supported type at runtime for the installed version, and modern Netmiko releases can autodetect many platforms from the SSH banner — useful when an inventory export does not carry a vendor field. For platforms derived from another vendor's CLI, such as HPE Comware or Dell OS10, start with the closest generic type and enable session_log on the first connection so that an unexpected prompt is visible immediately.

Pushing Configuration

config_commands = [
    "interface GigabitEthernet0/1",
    "description Uplink-to-core",
    "switchport mode trunk",
    "switchport trunk allowed vlan 10,20",
]
with ConnectHandler(**device) as conn:
    conn.send_config_set(config_commands)
    conn.save_config()  # write memory / copy running-config startup-config

send_config_set() enters config mode, sends each line, and exits. save_config() knows the vendor-specific save command.

Two refinements make this much safer. First, send_config_set() returns the device output — log it, because a line that was rejected (a typo, a missing VLAN, an unsupported command on that platform) still produces output and would otherwise pass unnoticed. Second, prefer send_config_set(..., enter_config_mode=True, exit_config_mode=True) with error_pattern set explicitly, or wrap the push in a change window, so that a failure aborts rather than half-applying a template:


output = conn.send_config_set(config_commands)
if "% Invalid" in output or "Error" in output:
    raise RuntimeError("Config rejected:\n" + output)
conn.save_config()

For larger changes, keep the commands in a file and use send_config_from_file("changes.cfg"); the file is parsed once and applied identically across devices, which makes review and rollback far easier than embedding dot commands in Python.

Practical Script: Nightly Config Backup

import datetime
from netmiko import ConnectHandler

devices = [
    {"device_type": "cisco_ios", "host": "192.168.1.10", "username": "admin", "password": "secret"},
    {"device_type": "juniper_junos", "host": "192.168.1.11", "username": "admin", "password": "secret"},
    {"device_type": "arista_eos", "host": "192.168.1.12", "username": "admin", "password": "secret"},
]

for dev in devices:
    try:
        with ConnectHandler(**dev) as conn:
            config = conn.send_command("show running-config")
            date = datetime.date.today().isoformat()
            filename = f"backup/{dev['host']}_{date}.cfg"
            with open(filename, "w") as f:
                f.write(config)
            print(f"Backed up {dev['host']} -> {filename}")
    except Exception as e:
        print(f"FAILED {dev['host']}: {e}")

Add a cron job and you have automated config backup across a multi-vendor fleet in under 30 lines.

The version above is a teaching script, not a production one. Three upgrades turn it into something you can trust: a retry wrapper, a device_type-aware command (Junos answers show configuration | display set, not show running-config, though Netmiko maps the latter for many platforms), and a failure exit code so cron can alert:


import sys
from time import sleep
from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException

def backup(dev, attempts=3):
    for n in range(attempts):
        try:
            with ConnectHandler(**dev) as conn:
                conn.disable_paging()
                return conn.send_command("show running-config", read_timeout=120)
        except NetmikoAuthenticationException:
            raise
        except Exception as err:
            print(f"attempt {n+1} failed on {dev['host']}: {err}")
            sleep(5 * (n + 1))
    raise RuntimeError(f"{dev['host']} unreachable")

failures = 0
for dev in devices:
    try:
        cfg = backup(dev)
        path = f"backup/{dev['host']}_{datetime.date.today().isoformat()}.cfg"
        open(path, "w").write(cfg)
        print(f"OK {dev['host']} -> {path}")
    except Exception as e:
        failures += 1
        print(f"FAILED {dev['host']}: {e}")
sys.exit(1 if failures else 0)

Authentication failures are separated from transport failures deliberately: a wrong password will fail identically on all three retries and should alert immediately, while an unreachable device often recovers after a moment.

Structured Output with TextFSM

output = conn.send_command("show ip interface brief", use_textfsm=True)
for interface in output:
    print(interface["interface"], interface["ip_address"], interface["status"])

With use_textfsm=True you get a list of dicts instead of raw text - perfect for feeding dashboards or comparing state.

TextFSM works by matching the CLI output against a template from the NTC-templates library, which Netmiko installs with the [textfsm] extra. It is important to understand the trade-off: templates are regular-expression based, so a vendor cosmetic change to a show command — a new column, a changed header — can silently produce empty dictionaries rather than an error. Always check the shape of the parsed result before trusting it, and keep a known-good example of each command's output in your test suite. For more complex or nested output, the Genie parser that ships with Cisco pyATS is more robust and is worth learning alongside TextFSM. Both approaches beat the alternative, which is a pile of brittle string slicing against column positions.

Error Handling and Best Practices

  • Wrap connections in try/except and log failures - one dead device should not stop the whole batch.
  • Store credentials in environment variables or a secrets manager, never in the script.
  • Use conn.enable() explicitly when the device needs privileged mode (or set secret in the dict).
  • For device_type discovery use netmiko.ssh_dispatcher.class_mapper() or the device_type autodetect in newer versions.

Beyond the list, four habits separate working automation from fragile automation. Set conn_timeout explicitly so a device that accepts the TCP connection but never presents an SSH banner does not hang your job indefinitely. Call conn.disable_paging() at the start of a session if you are unsure whether the device has terminal length 0 configured — a paged output that stops on --More-- looks exactly like a timeout and will waste an hour of debugging. Normalise whitespace and line endings before comparing configurations, because the same device will produce subtly different output depending on TACACS logging banners and terminal width. Finally, always record which user account ran the change: AAA accounting on the device plus a logging line in your script makes the correlation possible when someone asks who reconfigured an uplink at 02:00.

Scaling Up: Concurrency and Inventory

A serial loop over 300 devices at two seconds each is ten minutes; the same job with a thread pool is under a minute. Netmiko is blocking I/O, so threads — not asyncio — are the natural fit:


from concurrent.futures import ThreadPoolExecutor
from netmiko import ConnectHandler

def pull(dev):
    with ConnectHandler(**dev) as conn:
        return dev["host"], conn.send_command("show version")

with ThreadPoolExecutor(max_workers=20) as pool:
    for host, out in pool.map(pull, devices):
        print(host, "->", len(out), "bytes")

Twenty parallel sessions is a reasonable starting point; go higher only after confirming that your AAA servers, syslog collectors and the devices' own SSH limits can take it. For anything larger than a few hundred devices, stop hand-rolling inventory and move to Nornir, which gives you structured inventory, per-task parallelism and proper result objects while still using Netmiko underneath. Pair it with an IPAM source of truth such as NetBox so the device list is never a hard-coded Python list.

Testing Automation Safely

Every script should be exercised against a lab before it touches production. Containerlab builds multi-vendor topologies in Docker in seconds and is the cheapest way to get real devices to test against; the same script that works on a containerlab cEOS or vJunos node will behave on the real thing. Keep a --dry-run flag that connects, gathers state and prints the diff without applying anything, and run the read-only half of your automation against production first — a surprising number of "config push bugs" turn out to be parsing bugs in the read path.

Going Further

Netmiko pairs naturally with Ansible (network modules), Nornir (parallel execution) and TextFSM/ntc-templates. For inventory and parallelism, Nornir + Netmiko is the power combo used by many network teams.

Related: Ansible network automation and multi-vendor automation with Ansible and Jinja2. If you prefer a model-driven library over CLI scraping, see NAPALM getters and config diff and, for validating state after a change, pyATS and Genie state comparison. Running a single vendor? Junos PyEZ gives you a native NETCONF path instead of CLI parsing.

原文链接:https://github.com/ktbyers/netmiko