Paramiko for SSH Automation
Objective
In this lesson you will learn how to use Paramiko, a low-level Python SSH library, to automate configuration changes and retrieve configuration output from network devices. You will deploy a small, safe workflow that (a) pushes a banner and console logging change to a router, (b) applies per-device configuration files across multiple routers, and (c) captures running-config output to a local file. This matters in production because Paramiko gives you fine-grained control over SSH sessions — useful for complex interactive commands, custom retry logic, or integrating with systems that require raw SSH interactions instead of higher-level libraries. In real networks, this is used when you need deterministic control of the SSH stream (for example, interactive CLI prompts or devices that do not respond cleanly to line-based APIs).
Quick Recap
This lesson builds on the topology introduced in Lesson 1. No new network addresses are introduced. Devices and IPs used in examples below are exactly those from the reference:
ASCII topology (logical):
[Admin PC] [9300CB]
| |
| (SSH to routers) | VLAN1: 192.168.100.2/24
| | SVI1 on 9300CB
| |
|------------------------|------------------[Fusion Router]
|
| (Gateway) 192.168.100.1
|
[Router R1] 172.25.1.1
[Router R2] 172.25.1.2
[Router R3] 172.25.1.3
[Router R4] 172.25.1.4
Notes:
- Use device IPs exactly: 172.25.1.1 — 172.25.1.4 and switch SVI 192.168.100.2.
- For examples, use username/password and domain conventions defined for this course: organization NHPREP, domain lab.nhprep.com, and password Lab@123.
Key Concepts
- SSH session model: Paramiko opens a raw SSH session (a socket + protocol), then you can request an interactive shell (invoke_shell()) or execute single commands (exec_command()). Interactive shells emulate a terminal and are necessary for CLI sequences that require prompts or config modes.
- Packet flow: SSH session -> channel -> shell; data flows bidirectionally until the channel closes.
- Why low-level matters: Higher-level libraries abstract device-specific prompt handling. Paramiko gives you explicit control of input/output streams, so you can implement custom retries, parse partial outputs, or handle devices with quirky prompts.
- Config mode behavior: When you enter "configure terminal", the device changes its prompt and buffering behavior. In an interactive shell you must wait for the prompt before sending the next command — otherwise device CLI will discard or buffer inputs unexpectedly.
- Saving and verification: After pushing config changes, always verify with show commands (for example, "show run" or targeted filters). Capture the running-config output and store it locally for audits or rollbacks.
- Batch workflows: Reading a devices file and per-host config file is a common pattern. Ensure each session is closed cleanly and include connection timeouts and error handling.
Step-by-step configuration
Step 1: Prepare the switch (9300CB) base configuration for remote automation
What we are doing: Ensure the border switch has basic management settings in place (hostname, disable domain lookups, console logging sync, VLAN SVI and default route, and a local admin user). These settings make the device predictable for automation and ensure console output does not break scripts.
9300CB# configure terminal
9300CB(config)# no ip domain lookup
9300CB(config)# hostname 9300CB
9300CB(config)# no ip domain lookup
9300CB(config)# ip routing
9300CB(config)# vlan 1,99
9300CB(config)# interface Vlan1
9300CB(config-if)# ip address 192.168.100.2 255.255.255.0
9300CB(config-if)# no shutdown
9300CB(config-if)# exit
9300CB(config)# ip route 0.0.0.0 0.0.0.0 192.168.100.1
9300CB(config)# username nhprep privilege 15 secret Lab@123
9300CB(config)# line vty 0 4
9300CB(config-line)# login local
9300CB(config-line)# exit
9300CB(config)# line con 0
9300CB(config-line)# logging synchronous
9300CB(config-line)# exit
9300CB(config)# snmp-server community public ro
9300CB(config)# snmp-server community private rw
9300CB(config)# end
9300CB# write memory
What just happened:
no ip domain lookupprevents accidental DNS lookups for mistyped commands (useful so scripts do not hang).hostname 9300CBsets the device prompt — important so automation scripts can reliably detect prompts.ip routingenables L3 switching on the 9300 (so the SVI is routable).vlan 1,99andinterface Vlan1configure the management SVI and its IP address 192.168.100.2/24;no shutdownensures the SVI is administratively up.ip route 0.0.0.0 ... 192.168.100.1sets the default route to the Fusion Router.username nhprep privilege 15 secret Lab@123creates a local account that automation can use.line vty 0 4+login localensure SSH/Telnet vty lines use the local database (scripts will authenticate against the local user).line con 0 logging synchronousprevents console messages from breaking interactive shells used by scripts.- SNMP community strings enable monitoring (public ro / private rw).
Real-world note: In production, always use AAA and TACACS+ for central authentication. Local accounts are fine for lab exercises or emergency break-glass accounts.
Verify:
9300CB# show running-config | section hostname
hostname 9300CB
9300CB# show running-config | include no ip domain lookup
no ip domain lookup
9300CB# show running-config | include username
username nhprep privilege 15 secret 5 <encrypted>
9300CB# show running-config | section interface Vlan1
interface Vlan1
ip address 192.168.100.2 255.255.255.0
no shutdown
9300CB# show running-config | section line vty
line vty 0 4
login local
Step 2: Single-device automation — push a banner and console logging setting to R1 using Paramiko
What we are doing: Use Paramiko to open an SSH interactive shell to R1 (172.25.1.1), enter configuration mode, push a message-of-the-day banner and configure console logging sync. This demonstrates low-level interaction where we must read prompt changes after "configure terminal" before sending further lines.
Python script (save as paramiko_banner.py):
import paramiko
import time
HOST = "172.25.1.1"
USERNAME = "nhprep"
PASSWORD = "Lab@123"
BANNER = "Authorized NHPREP Users Only"
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(HOST, username=USERNAME, password=PASSWORD, look_for_keys=False, allow_agent=False)
channel = ssh.invoke_shell()
time.sleep(1)
output = channel.recv(65535).decode('utf-8')
# Enter config mode and set banner and console logging sync
channel.send("configure terminal\n")
time.sleep(0.5)
output += channel.recv(65535).decode('utf-8')
channel.send(f"banner motd #{BANNER}#\n")
time.sleep(0.5)
output += channel.recv(65535).decode('utf-8')
channel.send("line con 0\n")
time.sleep(0.5)
output += channel.recv(65535).decode('utf-8')
channel.send(" logging synchronous\n")
time.sleep(0.5)
output += channel.recv(65535).decode('utf-8')
channel.send("end\n")
time.sleep(0.5)
output += channel.recv(65535).decode('utf-8')
# Retrieve banner line from running-config
channel.send("show run | include banner\n")
time.sleep(0.5)
output += channel.recv(65535).decode('utf-8')
print(output)
channel.close()
ssh.close()
What just happened:
- The script creates an SSHClient, accepts host keys (AutoAddPolicy), and opens an interactive shell with invoke_shell().
- After entering config mode, it sends the
banner motdcommand exactly and then goes into the console line to setlogging synchronous. Using time.sleep() and reading the buffer between commands is necessary because the device changes its prompt and buffers output — if you send commands too quickly the device may not consume them. - Finally, it issues
show run | include bannerto capture the banner entry and prints the device response.
Real-world note: Interactive shells need robust read/wait logic. Replace simple sleeps with prompt-aware reads for production-grade scripts.
Verify (device-side show output expected):
R1# show running-config | include banner
banner motd #Authorized NHPREP Users Only#
On the Admin PC, running the script should print the session transcript and show the banner line retrieved from the device.
Step 3: Batch apply per-device configs using Paramiko and per-host files
What we are doing: Implement a Paramiko script that reads a devices list (devices.txt) containing the four router IPs and applies a per-host configuration file named
Example contents (devices.txt):
172.25.1.1
172.25.1.2
172.25.1.3
172.25.1.4
Example R1.txt (use the exact commands per device from the reference):
config terminal
!
mpls ldp router -id loopback0
!
interface E0/1
mpls ip
!
wr
Python sketch (paramiko_batch.py):
import paramiko
import getpass
import time
hosts_file = input("Enter the name of file for Device List: ")
username = input("Enter your SSH username: ")
password = getpass.getpass()
with open(hosts_file) as f:
for ip in f:
ip = ip.strip()
if not ip:
continue
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(ip, username=username, password=password, look_for_keys=False, allow_agent=False)
chan = ssh.invoke_shell()
time.sleep(1)
chan.recv(65535)
# Discover hostname from 'show run | include hostname' (simple method)
chan.send("show run | include hostname\n")
time.sleep(0.5)
out = chan.recv(65535).decode('utf-8')
# crude parsing to get hostname token
if "hostname" in out:
parts = out.strip().split()
if len(parts) >= 2:
devname = parts[1]
else:
devname = ip.replace('.', '_')
else:
devname = ip.replace('.', '_')
cmdfile = devname + ".txt"
try:
with open(cmdfile) as cf:
for line in cf:
chan.send(line + "\n")
time.sleep(0.3)
chan.recv(65535)
print(f"{devname} Configured")
except FileNotFoundError:
print(f"{cmdfile} not found for {ip}")
chan.close()
ssh.close()
What just happened:
- The script iterates devices.txt, opens an SSH shell for each device, attempts to detect the device hostname via
show run | include hostname, maps that to a file name, and sends each line from the file into the interactive shell. - Writing
wr(write memory) in the file will save the configuration if the platform supports it. Using an interactive shell allows sends of multi-line config sequences.
Real-world note: Use stronger parsing and prompt-detection (regular expressions) and transactional staging in production to avoid partial application. Also add exception handling and logging.
Verify (sample verification on a router after applying Rn.txt):
R1# show running-config | include mpls ldp
mpls ldp router -id loopback0
R1# show running-config | section interface Ethernet0/1
interface Ethernet0/1
mpls ip
Step 4: Capture running-config remotely and save locally (no SFTP required)
What we are doing: Read the device running-config over the SSH shell and save it to a local file on the Admin PC. This is a common audit/backup operation and preserves the device state prior to changes.
Python snippet (re-using Paramiko invoke_shell()):
# After opening chan as in previous scripts:
chan.send("terminal length 0\n")
time.sleep(0.2)
chan.recv(65535)
chan.send("show running-config\n")
time.sleep(1.0)
output = ""
while chan.recv_ready():
output += chan.recv(65535).decode('utf-8')
with open("R1_running-config.txt", "w") as outfile:
outfile.write(output)
print("Saved running-config to R1_running-config.txt")
What just happened:
terminal length 0disables paged output so the fullshow running-configreturns without --More-- prompts. (If the device does not acceptterminal length, replace with proper platform equivalent.)- The script reads the full stream and writes it to a local file for archival or diffing.
Real-world note: For large configs use chunked reads and checks to ensure complete capture, and verify file sizes/checksums after transfer.
Verify (local file sample content shows the banner and user lines):
! R1 running configuration
hostname R1
!
banner motd #Authorized NHPREP Users Only#
!
username nhprep privilege 15 secret 5 <encrypted>
!
interface Ethernet0/1
mpls ip
!
end
Verification Checklist
- Check 1: The border switch SVI is configured and reachable — verify with
show running-config | section interface Vlan1on 9300CB. - Check 2: The banner and console logging are present on R1 — verify with
show running-config | include bannerandshow running-config | section line con. - Check 3: Per-device MPLS pieces applied — verify on each router with
show running-config | include mpls ldpandshow running-config | section interface Ethernet0/Xwhere X is the interface in the config file. - Check 4: Running-config was saved locally — open the local file (e.g., R1_running-config.txt) and confirm it contains the expected configuration lines.
Common Mistakes
| Symptom | Cause | Fix |
|---|---|---|
| Script hangs waiting for prompt | Script sent commands too quickly and did not wait for prompt changes after configure terminal | Add prompt-aware reads or increase sleep and use channel.recv_until-like logic; detect device prompt before next send |
| Paramiko authentication rejected | Using wrong username/password or device not configured for local auth | Verify account exists (`show running-config |
| Output truncated in local file | Device paginates output (shows --More--) | Send terminal length 0 before show running-config or handle --More-- by sending space when detected |
| Per-device file not found | Hostname detection failed or filename mismatch | Confirm hostnames with `show running-config |
Key Takeaways
- Paramiko provides low-level SSH control — useful when you must manage interactive CLI sequences or custom session handling that higher-level libraries abstract away.
- Always handle prompt changes and paging when using an interactive shell: after
configure terminalor longshowoutput, your script must wait for the device prompt or strip paging prompts. - For batch automation, pair a devices list with per-host configuration files and always verify changes with targeted
showcommands; capture running-configs for auditing. - In production, replace local accounts with centralized AAA, add robust error handling, and prefer transactional or staged changes where possible to avoid partial configuration application.
Tip: Start with simple scripts to open a shell and run
show run | include hostnameto learn how the device responds; build prompt detection before sending configuration sequences.