This is the post where Python starts talking to actual routers. Everything so far — data structures, parsing, sockets — has been preparation. Today you open an SSH session to a device from Python, run a command, and read the result back. The library is Paramiko, the pure-Python SSH implementation that nearly every higher-level network tool (including Netmiko, which we cover tomorrow) is built on top of.
Why learn Paramiko if Netmiko is easier? Because understanding the layer underneath makes the layer above make sense. When Netmiko throws a timeout, you will know what actually happened on the wire.
Install and First Connection
Paramiko is third-party, so it goes in your virtual environment (the venv we set up in 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. In production you would verify keys properly, but in the lab this gets you 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 gives you 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 you need an interactive shell instead — the same PTY you would get from a real SSH login:
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: you send text, you wait, you read whatever has arrived. Get the timing wrong and you read a half-finished response. This friction is exactly why Netmiko exists — it handles the waiting and prompt-detection for you. But seeing the friction once makes you appreciate what Netmiko does.
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 thing you send, 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 — gives you something you will actually reuse:
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. Catch the specific exceptions Paramiko raises so a single bad device does not kill 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 you will meet most. This is your Week 1 exception handling applied to a real-world failure surface.
Exercises
If you do not have a lab device, a Linux box with SSH enabled works for the exec_command exercises, and you can rent a free always-on sandbox from Cisco DevNet for the IOS ones.
- 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 so we drop the echoed command and banner. Tomorrow Netmiko will do all this — paging, prompt handling, command echo — in two lines. Now you will know what it saved you.
Previously: Socket Programming Basics. Coming tomorrow — Netmiko: the same job as today, minus all the time.sleep() pain.