This is the post where Python starts talking to actual routers. Everything so far — data structures, parsing, sockets — has been preparation. Today opens an SSH session to a device from Python, runs a command, and reads the result back. The library is Paramiko, the pure-Python SSH implementation that nearly every higher-level network tool (including Netmiko, tomorrow’s topic) is built on top of.
Why learn Paramiko when Netmiko is easier? Because understanding the layer underneath makes the layer above make sense. When Netmiko throws a timeout, knowing what actually happened on the wire is what makes it fixable.
Install and First Connection
Paramiko is third-party, so it goes in the virtual environment (the venv from Week 1):
pip install paramiko
The minimal connection looks like this:
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
hostname="192.168.1.1",
username="admin",
password="cisco123",
look_for_keys=False, # don't try SSH keys; we're using a password
allow_agent=False,
timeout=10,
)
print("Connected!")
client.close()
Two lines deserve explanation. set_missing_host_key_policy(AutoAddPolicy()) tells Paramiko to accept a device’s SSH host key automatically the first time it sees it. Production code would verify keys properly, but in the lab this gets things moving. look_for_keys=False and allow_agent=False stop Paramiko from trying key-based auth first — without them, password auth to network gear is often slow or fails outright.
exec_command vs. an Interactive Shell — the Big Gotcha
Paramiko offers two ways to run things, and choosing wrong is the number-one beginner mistake with network devices.
# exec_command: opens a NEW channel per command, runs it, closes.
# Works great on Linux. Often BREAKS on Cisco IOS because IOS
# expects an interactive shell, not a one-shot exec channel.
stdin, stdout, stderr = client.exec_command("show version")
print(stdout.read().decode())
On a Linux server, exec_command is perfect. On a Cisco router, many platforms do not support exec channels and an interactive shell is required instead — the same PTY a real SSH login provides:
import time
shell = client.invoke_shell()
time.sleep(1)
shell.recv(65535) # drain the login banner
shell.send("terminal length 0\n") # disable --More-- paging
time.sleep(1)
shell.send("show ip interface brief\n")
time.sleep(2) # give the device time to respond
output = shell.recv(65535).decode()
print(output)
Notice the time.sleep() calls and the manual recv(). This is the raw, fiddly reality of driving an interactive shell: send text, wait, read whatever has arrived. Get the timing wrong and the response comes back half-finished. This friction is exactly why Netmiko exists — it handles the waiting and prompt-detection automatically. But seeing the friction once makes what Netmiko does easy to appreciate.
terminal length 0: The First Command, Always
Interactive sessions on Cisco gear paginate long output with --More-- prompts. A script does not know to press space, so it hangs. Sending terminal length 0 before anything else disables paging for the session. Make it the first command sent, every time.
Cisco Context: A Reusable Connect-and-Run Helper
Wrapping the boilerplate in a function — using the try/finally discipline from Week 1 so the connection always closes — produces something genuinely reusable:
import time
import paramiko
def run_commands(host, user, pw, commands, read_wait=2):
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
client.connect(hostname=host, username=user, password=pw,
look_for_keys=False, allow_agent=False, timeout=10)
shell = client.invoke_shell()
time.sleep(1)
shell.recv(65535) # clear banner
shell.send("terminal length 0\n")
time.sleep(1)
shell.recv(65535)
output = {}
for cmd in commands:
shell.send(cmd + "\n")
time.sleep(read_wait)
output[cmd] = shell.recv(65535).decode()
return output
finally:
client.close()
results = run_commands("192.168.1.1", "admin", "cisco123",
["show version", "show ip int brief"])
for cmd, out in results.items():
print(f"=== {cmd} ===\n{out}\n")
Handling Auth and Connection Failures
Real networks have unreachable devices and wrong passwords. Catching the specific exceptions Paramiko raises keeps a single bad device from killing a whole run:
import paramiko, socket
try:
client.connect(host, username=user, password=pw, timeout=5,
look_for_keys=False, allow_agent=False)
except paramiko.AuthenticationException:
print(f"{host}: bad credentials")
except (socket.timeout, OSError):
print(f"{host}: unreachable")
except paramiko.SSHException as e:
print(f"{host}: SSH error - {e}")
AuthenticationException, a connection timeout, and the catch-all SSHException are the three that come up most. This is the Week 1 exception handling applied to a real-world failure surface.
Exercises
With no lab device on hand, a Linux box with SSH enabled works for the exec_command exercises, and a free always-on sandbox from Cisco DevNet covers the IOS ones.
This is Day 12 of the 21‑post Python for Network Engineers series.
- Warm-up. Connect to a device (or a Linux host) and confirm the connection by printing
"Connected", then close cleanly. Usetry/finally. - One command. On a Linux host, use
exec_commandto rununame -aand print the decoded stdout. - Shell mode. On a Cisco device, use
invoke_shellto disable paging and runshow version, returning the output as a string. - Robust connect. Wrap a connection in handlers for bad credentials and unreachable hosts so the script prints a clear reason instead of a traceback. Test it by pointing at an unreachable IP.
- Challenge. Write
backup_config(host, user, pw)that connects, runsshow running-config, and writes the output to a file named{host}_running.txt. Strip the echoed command and prompt lines so the file starts at the real config.
Answers
Show answers
1. Warm-up
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
client.connect("192.168.1.1", username="admin", password="cisco123",
look_for_keys=False, allow_agent=False, timeout=10)
print("Connected")
finally:
client.close()
2. One command (Linux)
stdin, stdout, stderr = client.exec_command("uname -a")
print(stdout.read().decode().strip())
3. Shell mode (Cisco)
import time
def show_version(client):
shell = client.invoke_shell()
time.sleep(1); shell.recv(65535)
shell.send("terminal length 0\n"); time.sleep(1); shell.recv(65535)
shell.send("show version\n"); time.sleep(2)
return shell.recv(65535).decode()
4. Robust connect
import paramiko, socket
def safe_connect(host, user, pw):
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
c.connect(host, username=user, password=pw, timeout=5,
look_for_keys=False, allow_agent=False)
return c
except paramiko.AuthenticationException:
print(f"{host}: bad credentials"); return None
except (socket.timeout, OSError):
print(f"{host}: unreachable"); return None
5. Challenge
import time, paramiko
def backup_config(host, user, pw):
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
c.connect(host, username=user, password=pw,
look_for_keys=False, allow_agent=False, timeout=10)
sh = c.invoke_shell()
time.sleep(1); sh.recv(65535)
sh.send("terminal length 0\n"); time.sleep(1); sh.recv(65535)
sh.send("show running-config\n"); time.sleep(3)
raw = sh.recv(655350).decode()
# keep lines from the first '!' or 'version' onward
lines = raw.splitlines()
start = next((i for i, l in enumerate(lines)
if l.strip().startswith(("version", "!"))), 0)
with open(f"{host}_running.txt", "w") as f:
f.write("\n".join(lines[start:]))
print(f"Saved {host}_running.txt")
finally:
c.close()
The next(... ) with a generator finds the index of the first real config line, dropping the echoed command and banner. Tomorrow Netmiko does all of this — paging, prompt handling, command echo — in two lines. Having seen the raw version makes clear exactly what it saves.
Previously: Socket Programming Basics. Coming tomorrow — Netmiko: the same job as today, minus all the time.sleep() pain.