Multi-Vendor SSH with Netmiko: send_command and send_config_set

Yesterday you drove a router with Paramiko and felt every bump: manual time.sleep() calls, draining banners, guessing when output was done. Today that all disappears. Netmiko wraps Paramiko specifically for network devices — it knows about prompts, paging, configuration mode, and dozens of vendor platforms. For day-to-day SSH automation, Netmiko is the tool most network engineers actually reach for first.

If you only learn one library from this whole week, make it this one.

Install and Connect

pip install netmiko

A connection is a single dictionary describing the device, passed to ConnectHandler:

from netmiko import ConnectHandler

device = {
    "device_type": "cisco_ios",      # this is the magic field
    "host": "192.168.1.1",
    "username": "admin",
    "password": "cisco123",
    "secret": "enablepass",          # enable password, if needed
}

conn = ConnectHandler(**device)
print(conn.find_prompt())            # e.g. 'R1>'
conn.disconnect()

The device_type is what makes Netmiko multi-vendor. Change it to arista_eos, juniper_junos, cisco_nxos, cisco_xr, hp_procurve, and Netmiko adjusts its prompt patterns and paging commands automatically. Same code, different platform.

send_command: Read Operations Done Right

This single method replaces all of yesterday’s sleep-and-recv dance. Netmiko sends the command, waits for the prompt to return, strips the echoed command, and hands you clean output:

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

No terminal length 0 — Netmiko disables paging for you. No timing guesswork — it watches for the prompt. This is the payoff for understanding the raw layer yesterday.

The Best Part: send_command with use_textfsm

Add one argument and Netmiko parses the output into structured data using the ntc-templates library (we devote a whole post to it later this week). Instead of a wall of text, you get a list of dictionaries:

data = conn.send_command("show ip interface brief", use_textfsm=True)
# data is now a list of dicts:
# [{'interface': 'GigabitEthernet0/0', 'ipaddr': '192.168.1.1',
#   'status': 'up', 'proto': 'up'}, ...]

for row in data:
    if row["status"] != "up":
        print(f"{row['interface']} is {row['status']}")

This is enormous. You go straight from a CLI command to data you can filter, count, and compare — no regex required. (Install the templates with pip install ntc-templates; Netmiko uses them automatically when use_textfsm=True.)

Making Configuration Changes: send_config_set

Reading is half the job. To push configuration, hand Netmiko a list of commands and it enters config mode, sends them in order, and exits — all automatically:

commands = [
    "interface Loopback100",
    "description Created by Netmiko",
    "ip address 10.100.100.1 255.255.255.255",
]
output = conn.send_config_set(commands)
print(output)

# Don't forget to save (IOS):
print(conn.save_config())            # issues 'write memory'

Netmiko handles the configure terminal and end wrapping for you. You just describe what to configure. And save_config() knows the right save command per platform — write memory on IOS, commit on Junos, and so on.

enable() and Privileged Mode

If you land in user EXEC mode (R1>), call enable() to get to privileged mode (R1#) using the secret from your device dict:

conn = ConnectHandler(**device)
if not conn.check_enable_mode():
    conn.enable()
conn.send_config_set(["ntp server 10.0.0.1"])

Cisco Context: Push One Change to a Fleet

The real win is doing the same thing to many devices. Loop a list, use with so each session closes cleanly, and keep going even if one device fails:

from netmiko import ConnectHandler
from netmiko.exceptions import NetmikoTimeoutException, NetmikoAuthenticationException

inventory = [
    {"device_type": "cisco_ios", "host": "192.168.1.1",
     "username": "admin", "password": "cisco123"},
    {"device_type": "cisco_ios", "host": "192.168.1.2",
     "username": "admin", "password": "cisco123"},
]

config = ["ntp server 10.0.0.1", "logging host 10.0.0.50"]

for dev in inventory:
    host = dev["host"]
    try:
        with ConnectHandler(**dev) as conn:
            conn.send_config_set(config)
            conn.save_config()
            print(f"{host}: applied and saved")
    except NetmikoAuthenticationException:
        print(f"{host}: AUTH FAILED")
    except NetmikoTimeoutException:
        print(f"{host}: UNREACHABLE")

That is a production-shaped script: idempotent-ish config, per-device error isolation, automatic cleanup. Swap the inventory for a YAML file (Monday’s topic) and you have a tool.

send_command Timing Knobs

Occasionally a command runs long (a big show tech) or a device is slow. Two arguments handle it: read_timeout raises the ceiling on how long to wait for the prompt, and expect_string lets you wait for a custom prompt (useful right after a hostname change). You rarely need them, but knowing they exist saves an afternoon.

Exercises

  1. Warm-up. Connect to a device and print its prompt with find_prompt(), then disconnect cleanly using a with block.
  2. Read. Run show version and print just the line containing the software version (combine with your regex/string skills).
  3. Structured read. Use send_command(..., use_textfsm=True) on show ip interface brief and print only interfaces whose protocol is down.
  4. Config push. Add a loopback interface with a description and a /32 address using send_config_set, then save the config.
  5. Challenge. Write audit_ntp(inventory, expected_server) that connects to each device, runs show running-config | include ntp server, and reports for each device whether expected_server is configured — isolating failures so one dead device does not stop the audit.

Answers

Show answers

1. Warm-up

from netmiko import ConnectHandler
device = {"device_type": "cisco_ios", "host": "192.168.1.1",
          "username": "admin", "password": "cisco123"}
with ConnectHandler(**device) as conn:
    print(conn.find_prompt())

2. Read the version line

with ConnectHandler(**device) as conn:
    out = conn.send_command("show version")
for line in out.splitlines():
    if "Version" in line:
        print(line.strip())
        break

3. Structured read

with ConnectHandler(**device) as conn:
    rows = conn.send_command("show ip interface brief", use_textfsm=True)
for r in rows:
    if r["proto"] == "down":
        print(f"{r['interface']} protocol down (status {r['status']})")

4. Config push

cmds = ["interface Loopback123",
        "description lab loopback",
        "ip address 10.123.123.1 255.255.255.255"]
with ConnectHandler(**device) as conn:
    print(conn.send_config_set(cmds))
    print(conn.save_config())

5. Challenge

from netmiko import ConnectHandler
from netmiko.exceptions import NetmikoTimeoutException, NetmikoAuthenticationException

def audit_ntp(inventory, expected_server):
    for dev in inventory:
        host = dev["host"]
        try:
            with ConnectHandler(**dev) as conn:
                out = conn.send_command(
                    f"show running-config | include ntp server")
                ok = expected_server in out
                print(f"{host}: {'OK' if ok else 'MISSING'} "
                      f"({expected_server})")
        except NetmikoAuthenticationException:
            print(f"{host}: AUTH FAILED")
        except NetmikoTimeoutException:
            print(f"{host}: UNREACHABLE")

Same error-isolation pattern as the fleet push: wrap each device, catch the two common Netmiko exceptions, and keep going. This is the backbone of every multi-device script you will write.


Previously: SSH Automation with Paramiko. Coming tomorrow — NAPALM: one consistent API across Cisco, Arista, and Juniper, with config diffs and rollback built in.

Leave a Reply