Python for Network Engineers · Configuring and collecting at scale

Python + SSH: collect, configure & verify many devices

Let a script do the typing: prepare routers for SSH, log in with a netmiko-style library, collect show output, push configuration to many routers from a dictionary, and verify the result instead of hoping.

48 min read9 chapters2 labs15 quiz7 scenarios15 interview Q&A
New to a topic used here? Learn it first
  • OSPF basics: network statements, neighbours, FULL state
    Lab 2 pushes OSPF area 0 to three routers and checks that the neighbours reach FULL.

This first module is free: read the lesson and take the quiz. Create a free account to run up to 3 hands-on labs.

Log inStart free
Jump to chapter (9)
01

Scripts that log in for you: what you will learn and the big picture

What you will learn in this module. You will learn to make a small program log in to routers over SSH, read their output, change their configuration and prove that the change worked. You will use Python and a netmiko-style library called nkmiko. By the end you will back up three routers, push OSPF to them from one script, verify the result and read an error message without panic. These are the device-automation skills behind the CCNP Automation exam (350-901 AUTOCOR), and the same skills are reused in the Python for Network Engineers course.

Prerequisites. You need to know what a router, an IP address and the CLI are. You do not need to know how to code. Every line of code in this module is explained, and each chapter teaches one small idea.

Analogy: a very fast, very careful junior engineer

Imagine you give a junior engineer a checklist: "log in to each router, run show ip interface brief, paste the answer into a file." The junior follows the list exactly, never gets bored and never mistypes. A script is that checklist, written in a language the computer understands. The computer is the junior: fast and obedient, but it does exactly what you wrote, not what you meant.

That is both the power and the danger. A script can fix 300 routers in a minute. A wrong script can also break 300 routers in a minute. So we build scripts in small safe steps, and we always check the result.

AUTO10.99.0.100SW1switchR110.99.0.1R210.99.0.2R310.99.0.3SSH

The lab: one automation host (AUTO) reaches three routers over the management network 10.99.0.0/24.

The lab you will use

The AUTO host is a small Linux-like computer with Python. Three routers R1, R2 and R3 sit on the management network 10.99.0.0/24. A management network is a network used only to administer devices. AUTO logs in to the routers using the username netops and the password NK@2026. These are lab values; in a real network a password never lives inside a script, and Chapter 9 shows what to do instead.

The big idea: collect, change, verify

1. Collectread show output2. Changesend config lines3. Verifyread again and check

Every network script is collect, change, verify. Verify is the step beginners skip.

  1. Collect: log in and read the current state, for example show ip interface brief.
  2. Change: send configuration lines, only when needed.
  3. Verify: read the state again and test that the change really worked: the neighbour is up, the interface has the address, the route exists.

A script that prints "done" has only proved that it sent text. It has not proved that the network works.

Your first five minutes on the AUTO host

A script is a text file with the ending .py. You create and run it from the AUTO terminal using these small tools:

  • ls lists the files, cat file.py shows one, and nl file.py shows it with line numbers.
  • edit file.py 3 TEXT replaces line 3 with TEXT. Spaces after the number are kept, which matters in Python.
  • write file.py lets you type a whole file; finish by typing EOF on a line by itself.
  • python3 file.py runs the script.

Try the smallest script possible. It has two lines: print() is a function (a named action) that shows text on the screen.

print("Hello, network!")
print("2 + 3 =", 2 + 3)
AUTO> python3 hello.py
Hello, network!
2 + 3 = 5

Python ran the file from top to bottom, one line at a time. The second line shows that Python can also calculate: 2 + 3 became 5.

Worked example. Without a script: log in to R1, R2, R3, type show ip interface brief on each, copy three screens to a text file. About ten minutes of clicking and one chance in twenty of a slip. With a script: the same job is eight lines of code and takes two seconds, and it can be run again tomorrow, the same way. You will write exactly this script in Chapter 5.

Common beginner mistake. Thinking a script is magic or "smart". It is not. It sends exactly the text you wrote. If you wrote the wrong IP address, it will happily configure the wrong router.

Exam trap. The exam may ask which step makes automation safe. The answer is verification (and testing before production), not speed. Also remember that SSH with a library such as netmiko automates the CLI text, while REST APIs (the next two modules) automate structured data.

The nightly backup that nobody did

A retail company had 40 branch routers. The rule said "back up the configs every night", but it was a manual job and was skipped whenever people were busy. After a power failure two routers came back with empty configs and there was no recent copy. The team wrote a 12-line script that logged in to every router and saved the running config to a dated file. It ran from a scheduler at 02:00. The next failure was a 20-minute recovery.

Lesson: automation is first of all about boring, repeatable jobs done the same way every time.

"Why would you automate with SSH when APIs exist?"

A strong answer: many devices in a real network only have a CLI, so SSH-based automation reaches everything, including old gear. APIs return structured data and are cleaner when they exist. A good engineer knows both and picks per device. Mention collect, change, verify, and that you never push a change you cannot verify.

Key takeaways

  • A script is a checklist a computer follows exactly, fast and without typos.
  • Every network script follows collect, change, verify.
  • "Done" printed by a script proves nothing; reading the state again does.
  • In the lab, AUTO (10.99.0.100) reaches R1, R2 and R3 on 10.99.0.0/24 with user netops.
  • You run a Python file with python3 file.py and read the output from top to bottom.
02

Preparing a router for SSH: the eight lines

Before any script can log in to a router, the router must accept an SSH connection. Think of a hotel: the building can exist, but a guest cannot enter until the door is unlocked, the reception desk is open and a room key has been printed in the guest's name. Router SSH needs the same three things.

What SSH is, in one paragraph

SSH (Secure Shell) is an encrypted way to type commands on a remote device. It replaced Telnet, which sent everything, including passwords, as readable text. SSH works over TCP port 22. Version 2 is the modern one and the only one you should allow. When your script connects, the router proves who it is with an RSA key pair (a public key it shows to everyone and a private key it never shares), and then the session is encrypted.

AUTOthe scriptTCP 22door open?vty linelogin localnetopsprivilege 15RSA key + ssh version 2transport input sshusername ... secret

Three things must be ready before a script can log in: an open port 22, a vty line that allows SSH, and a valid user.

The eight lines, explained

You type these once per router in configuration mode. The lab router R3 is a new router that does not have them yet, which is why its backup fails in Lab 1.

hostname R3
ip domain name nk.lab
username netops privilege 15 secret NK@2026
crypto key generate rsa modulus 2048
ip ssh version 2
line vty 0 4
 login local
 transport input ssh
  • hostname R3: the router name. The key is named after the hostname plus the domain, so set it first.
  • ip domain name nk.lab: a domain name. IOS refuses to make an RSA key without one.
  • username netops privilege 15 secret NK@2026: a local user. secret stores the password hashed. privilege 15 is the highest level; the user lands directly in enable mode (prompt R3#), so a script can run show and configuration commands without an extra enable password.
  • crypto key generate rsa modulus 2048: makes the key pair. 2048 bits is the sensible minimum today.
  • ip ssh version 2: accept only SSH version 2.
  • line vty 0 4: the five virtual terminal lines, which are the doors for remote logins. They are called "virtual" because there is no physical port.
  • login local: check the username and password against the local user list.
  • transport input ssh: allow only SSH on these lines, so Telnet is refused.

Check the door before you write code

Always test SSH by hand first. If it does not work by hand, it will not work from a script. This is R3 before the setup:

R3# show ip ssh
SSH Disabled - version 1.99
%Please create RSA keys to enable SSH (and of atleast 768 bits for SSH v2).
...

Now the router has no key, so SSH is off. Notice that a ping to R3 still works, because ping is a different protocol and does not need SSH:

AUTO> ping 10.99.0.3
84 bytes from 10.99.0.3 icmp_seq=1 ttl=255 time=0.500 ms
...

After typing the eight lines (the key generation takes a few seconds and prints a message), the same command shows the difference:

R3# show ip ssh
SSH Enabled - version 2.0
...

Worked example. A new branch router arrives. You console in and type the eight lines with the real hostname. You run show ip ssh and read SSH Enabled - version 2.0. From the AUTO host you try one login by hand. Only now do you add the router's IP to the script's list. This order, router first, hand test second, script third, saves hours of guessing which part is broken.

Common beginner mistakes. (1) Forgetting ip domain name, so the key command fails. (2) Generating the key before setting the hostname, so the key carries a wrong name. (3) Using login instead of login local, so the vty line asks for a line password that does not exist. (4) Leaving transport input all, which keeps Telnet open.

Exam trap. Ping working does not prove SSH works. Also know that transport input ssh makes the router refuse the TCP connection for Telnet, and that SSH needs a hostname, a domain name and an RSA key.

Ping works, script times out

The night shift ran a backup script and it failed on R3 with a timeout. R3 answered ping, so the engineer blamed the network. Then she ran show ip ssh on the R3 console and read "SSH Disabled". The router had been replaced after a hardware fault and nobody had re-created the RSA key. She typed the eight lines, tested one manual login, and the script ran.

Lesson: a reachable router and a router that accepts SSH are two different facts. Test the second one.

"What do you need to configure on a Cisco router before a script can SSH to it?"

Hostname and domain name, an RSA key of at least 2048 bits, ip ssh version 2, a local user with the right privilege, and vty lines with login local and transport input ssh. Add that in production you would also restrict who may connect with an access class and use AAA instead of local users.

Key takeaways

  • SSH uses TCP 22 and replaces Telnet because it is encrypted; allow version 2 only.
  • The router needs a hostname, a domain name and an RSA key (2048 bits).
  • login local plus username ... privilege 15 secret ... gives the script a valid login that lands in enable mode.
  • transport input ssh keeps Telnet out.
  • Verify with show ip ssh and a manual login before you write any code.
03

Your first script: log in and read one command

This chapter teaches one idea: how a script logs in to one router and reads one command. First we need four words of Python, and nothing more.

Four words of Python

  • A variable is a name that holds a value, like a labelled box. router_ip = "10.99.0.1" puts the text 10.99.0.1 into a box called router_ip. The single = means "store", not "equals".
  • A string is text, always inside quotes: "R1". An integer is a whole number, without quotes: 22.
  • A function is a named action you start with brackets, like print("hi"). What you put inside the brackets are its arguments.
  • A method is a function that belongs to an object. You write object.method(). The dot means "ask this object to do this".

Run this small file and read it top to bottom. The last line asks Python what kind of value each variable holds.

router_name = "R1"
router_ip = "10.99.0.1"
port = 22
print(router_name)
print(router_name, "is at", router_ip)
print("port plus one is", port + 1)
print(type(router_name), type(port))
AUTO> python3 basics.py
R1
R1 is at 10.99.0.1
port plus one is 23
<class 'str'> <class 'int'>

Notice that print() with several values separates them with a space, and that port + 1 was calculated because port is a number. Had port been the string "22", adding 1 would fail, because Python does not guess.

Your first login script

Now the real script. Read it slowly; every line is explained below.

from nkmiko import ConnectHandler

conn = ConnectHandler(
    device_type="nk_ios",
    host="10.99.0.1",
    username="netops",
    password="NK@2026",
)
print("Logged in. Prompt is:", conn.find_prompt())
output = conn.send_command("show ip interface brief")
print(output)
conn.disconnect()
  • from nkmiko import ConnectHandler: a library is code someone else wrote. import brings one tool from it into your script. ConnectHandler is the tool that opens SSH sessions. The real netmiko library works the same way.
  • conn = ConnectHandler( ... ): calls the tool with four named arguments (written name=value) and stores the open session in the variable conn. The call spreads over several lines for readability; Python allows that inside brackets.
  • conn.find_prompt(): asks the session "what is your prompt?" The answer is the string R1#.
  • conn.send_command("show ip interface brief"): types the command on the router and returns everything it printed, as one long string. We store it in output.
  • print(output): shows that string.
  • conn.disconnect(): closes the session politely. Always do this, because each open session uses one of the router's five vty lines.
device_type="nk_ios"host="10.99.0.1"username="netops"password="NK@2026"which command style the router speakswhere to connect (the router IP)who is logging inproof of identity (lab value only)

Each named argument answers one question you would otherwise answer by typing it into an SSH client.

Run it

AUTO> python3 first.py
Logged in. Prompt is: R1#
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet0/0     10.99.0.1       YES manual up                    up
GigabitEthernet0/1     10.12.0.1       YES manual up                    up
GigabitEthernet0/2     unassigned      YES unset  administratively down down
GigabitEthernet0/3     unassigned      YES unset  administratively down down

The output is exactly what you would see after typing the command yourself. The script did not "understand" it; it only fetched the text. That is why the next chapters teach you to search and parse text.

What a failure looks like

Change the password to nk@2026 (wrong capital letters, because passwords are case-sensitive) and run again:

from nkmiko import ConnectHandler

conn = ConnectHandler(
    device_type="nk_ios",
    host="10.99.0.1",
    username="netops",
    password="nk@2026",
)
print("never reached")
AUTO> python3 wrongpw.py
Traceback (most recent call last):
  File "wrongpw.py", line 3, in <module>
    conn = ConnectHandler(
nkmiko.exceptions.NetmikoAuthenticationException: Authentication to device failed.

Common causes of this problem are:
1. Invalid username and password
2. Incorrect SSH-key file
...

Read a traceback (Python's error report) from the bottom. The last line names the error: NetmikoAuthenticationException. Above it, Python shows which file and line failed. "Authentication" means the connection opened (port 22 was fine) but the login was refused. So the fix is the username or password, not the network.

Worked example. You want to know the IOS version of R1. You copy first.py, change the command to "show version", run it, and read the answer. You did not learn anything new about Python; you reused the same four lines of login code with one different string. Most real scripts are this kind of small change to a known-good script.

Common beginner mistakes. (1) Forgetting the quotes around text, which makes Python look for a variable of that name. (2) Writing send_command without the brackets, which only names the action but does not run it. (3) Never calling disconnect(). (4) Using password="NK@2026" in a script that others can read; here it is a lab value only.

Exam trap. send_command() is for show (exec) commands and returns text. Configuration lines go through send_config_set(), which you meet in Chapter 6. Also remember that device_type tells the library how to talk to the platform; a wrong value fails before any login.

The script that logged in to the wrong router

An engineer copied a working script and changed only the command. The output looked odd: the hostname was R4, not R1. He had forgotten to change host=, so the script was talking to the router it had always talked to. Printing conn.find_prompt() at the start of every script would have shown the mistake at once.

Lesson: make the script prove which device it is on before it does anything else.

"What is the difference between an authentication error and a timeout in an SSH script?"

A timeout means the TCP connection to port 22 was never established: wrong IP, no route, SSH not enabled, or a filter in the way. An authentication error means the connection worked but the credentials were refused. So I check reachability and SSH setup for the first, and username, password and login method for the second.

Key takeaways

  • A variable stores a value, a string is text in quotes, a function does an action, a method is called with a dot.
  • ConnectHandler(...) opens an SSH session; keep it in a variable such as conn.
  • send_command() returns the router's output as one string.
  • Always disconnect(), and read tracebacks from the bottom line up.
  • Authentication error means login refused; timeout means the connection never opened.
04

Many routers, one loop: lists and for

One router is a demo. Real networks have dozens. The idea of this chapter is a single sentence: a loop repeats the same steps for every device in a list.

A list holds many values

A list is an ordered collection written inside square brackets, separated by commas. Each value is called an item.

ROUTERS = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]
print(len(ROUTERS))      # 3 : len() counts the items
print(ROUTERS[0])        # 10.99.0.1 : counting starts at 0, not 1

Python counts positions from zero, so the first item is ROUTERS[0]. Names written in CAPITAL LETTERS are a habit for values that do not change while the script runs.

The for loop

Typing show on 300 routers by hand is a loop in your head: "next router, log in, type, log out". Python writes the loop like this:

from nkmiko import ConnectHandler

ROUTERS = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]

for ip in ROUTERS:
    conn = ConnectHandler(device_type="nk_ios", host=ip, username="netops", password="NK@2026")
    print(ip, "->", conn.find_prompt())
    conn.disconnect()
print("finished")

Read for ip in ROUTERS: as "for each item in the list, call it ip and run the indented lines". The colon at the end is required. The four lines indented by four spaces are the loop body; they run once per router. The last line, print("finished"), is not indented, so it runs once after the loop is over.

ROUTERS[ip1, ip2, ip3]ip = 10.99.0.1round 1ip = 10.99.0.2round 2ip = 10.99.0.3round 3same indentedblock runs

A for loop takes the items of a list one at a time and runs the indented block once for each.

Indentation is the grammar

In most languages, curly brackets mark a block. In Python the indentation (the spaces at the start of the line) does that job. Use exactly four spaces per level and never mix tabs and spaces. If you forget to indent after the colon, Python refuses to start the script. This is the message for a loop whose body is not indented:

for ip in ["10.99.0.1"]:
print(ip)
AUTO> python3 indent.py
  File "indent.py", line 2
    print(ip)
    ^
IndentationError: expected an indented block after 'for' statement on line 1

The error is not a network problem. It means the script was never run. Errors that appear before any output are about the shape of the code.

Run it on three routers

When all three routers accept SSH (after the Chapter 2 setup on R3), the loop prints one line per router:

AUTO> python3 loop.py
10.99.0.1 -> R1#
10.99.0.2 -> R2#
10.99.0.3 -> R3#
finished

Now extend the loop to collect something useful. send_command("show version | include uptime") asks the router to show only the line that contains the word uptime, and .rstrip("#>") cuts the prompt character off the end of R1# so we get the clean name R1:

from nkmiko import ConnectHandler

ROUTERS = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]

for ip in ROUTERS:
    conn = ConnectHandler(device_type="nk_ios", host=ip, username="netops", password="NK@2026")
    name = conn.find_prompt().rstrip("#>")
    line = conn.send_command("show version | include uptime")
    print(name, "|", line)
    conn.disconnect()
AUTO> python3 uptime.py
R1 | R1 uptime is 0 minutes
R2 | R2 uptime is 0 minutes
R3 | R3 uptime is 0 minutes

The three lab routers have just booted, so the uptime is "0 minutes". The point is the pattern: one block of code, three results, no extra typing. Adding a fourth router means adding one item to the list.

What if one router is not ready?

Before R3 was prepared for SSH, the same loop printed two good lines and then stopped with a traceback:

AUTO> python3 loop.py
10.99.0.1 -> R1#
10.99.0.2 -> R2#
Traceback (most recent call last):
  File "loop.py", line 6, in <module>
    conn = ConnectHandler(device_type="nk_ios", host=ip, username="netops", password="NK@2026")
... (rest of the message shortened)

The script stopped at the first failure, so a healthy router later in the list would never be visited. Chapter 8 teaches try and except, which let the loop report the problem and carry on.

Worked example. A company has branch routers 10.99.0.1 to 10.99.0.3. The list grows to 40 addresses. The loop does not change at all: the engineer only adds addresses to ROUTERS. The nightly job still needs about a second per router, so 40 routers take under a minute.

Common beginner mistakes. (1) Forgetting the colon after the for line. (2) Indenting the body with a different number of spaces on different lines. (3) Putting conn.disconnect() outside the loop, so only the last session gets closed. (4) Expecting ROUTERS[3] to be the third item; position 3 is the fourth item, and in a three-item list it does not exist.

Exam trap. Python lists are zero-indexed, and indentation (not brackets) defines the loop body. A line indented under for runs once per item; the same line without indentation runs once.

Half the switches were never audited

An engineer pasted 12 login blocks one after another, one per switch, to collect a version report. When two more switches arrived, she forgot to paste them, and the audit quietly missed them for a quarter. A rewrite with one loop over a list put every switch in one place: the list. A new device had to be added to that one list, and the list could be checked against the asset register.

Lesson: a loop plus a list turns "copy and paste" into "add one line", and makes the inventory visible.

"How do you run the same command on 50 devices from Python?"

Keep the device addresses in a list or dictionary, loop over it, and inside the loop connect, run the command, store or print the result and disconnect. Mention error handling for unreachable devices, and that for very large estates you would use threads or a framework such as Nornir, but the logic is still a loop.

Key takeaways

  • A list holds many items in square brackets; counting starts at 0 and len() counts them.
  • for item in list: runs the indented block once per item.
  • Indentation (four spaces) is part of Python's grammar; a missing indent is an IndentationError.
  • .rstrip("#>") cuts prompt characters, so R1# becomes R1.
  • An unprepared router stops the loop with an exception; Chapter 8 shows how to carry on.
05

Saving results to files: the backup script

The most useful first automation in any network is a configuration backup. It uses only what you know (login, send a command) plus one new idea: writing text into a file.

Files in Python: open, mode, write

A file is a named piece of text on the AUTO host. To use it, Python needs two things: the file name and the mode, which says what you plan to do.

ModeMeaningIf the file does not exist
"r"read only (the default)error: FileNotFoundError
"w"write; empties the file firstcreates it
"a"append at the endcreates it

The safe pattern uses with. It opens the file, gives it a short name (f), and closes it automatically when the indented block ends, even if an error happens inside:

with open("R1.cfg", "w") as f:     # open for writing, call it f
    f.write(config)                # put the text inside
# leaving the indented block closes the file

The classic mistake: the wrong mode

If you open a file that does not exist with mode "r", Python tells you at once:

with open("R9.cfg", "r") as f:
    text = f.read()
AUTO> python3 readmode.py
Traceback (most recent call last):
  File "readmode.py", line 1, in <module>
    with open("R9.cfg", "r") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'R9.cfg'

This is exactly the bug in the Lab 1 starter script: it uses "r" where it needs "w". Reading a file that was never created is impossible, so the fix is the mode, not the file.

AUTObackup.pyR1show running-configR2show running-configR3show running-configR1.cfgR2.cfgR3.cfg

The backup script reads each router over SSH and writes the text into one file per router.

The complete backup script

from nkmiko import ConnectHandler

ROUTERS = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]

for ip in ROUTERS:
    conn = ConnectHandler(device_type="nk_ios", host=ip, username="netops", password="NK@2026")
    name = conn.find_prompt().rstrip("#>")
    config = conn.send_command("show running-config")
    conn.disconnect()
    with open(name + ".cfg", "w") as f:
        f.write(config)
    print("Backed up", name, "->", name + ".cfg")
  • conn.find_prompt().rstrip("#>") gives the router name: R1# becomes R1. We use it to name the file, so no name is typed by hand.
  • config = conn.send_command("show running-config"): the whole configuration as one string.
  • conn.disconnect() comes before writing the file. The session is no longer needed, so we release the vty line early.
  • name + ".cfg" joins two strings with +. "R1" + ".cfg" is "R1.cfg".
  • f.write(config) writes the text into the file.

Run it (all three routers accept SSH at this point):

AUTO> python3 backup.py
Backed up R1 -> R1.cfg
Backed up R2 -> R2.cfg
Backed up R3 -> R3.cfg

Never trust a script blindly: look at the file

The script printed "Backed up", but that only proves it reached the last line. Open a file yourself. Two quick ways: grep hostname R3.cfg on the host, or a few lines of Python that count lines and print the first twelve:

with open("R3.cfg") as f:
    text = f.read()
lines = text.splitlines()
print("lines in file:", len(lines))
for line in lines[:12]:
    print(line)
AUTO> python3 peek.py
lines in file: 52
Building configuration...

Current configuration : 852 bytes
!
version 2.0
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
!
hostname R3
!
boot-start-marker
AUTO> grep hostname R3.cfg
hostname R3

Each file starts with the lines Building configuration... and Current configuration, which belong to the command output, not the configuration itself. That is normal for a backup; just remember it if you later compare two backups with a tool.

A better file name: add the date

One file per router overwrites yesterday's backup. Adding the date keeps history. The datetime module (part of Python) gives the date, and strftime("%Y-%m-%d") formats it as year-month-day:

from datetime import datetime

stamp = datetime.now().strftime("%Y-%m-%d")
name = "R1"
filename = name + "_" + stamp + ".cfg"
print(filename)

with open("notes.txt", "a") as f:
    f.write("first line\n")
with open("notes.txt", "a") as f:
    f.write("second line\n")
print(open("notes.txt").read())
AUTO> python3 stamp.py
R1_2026-09-29.cfg
first line
second line

The date printed is the lab clock's date, so you will see today's lab date. The second half of the script shows mode "a": two separate with blocks both added a line, and neither erased the other.

Worked example. A company wants 30 days of history for 3 routers. Each night the script writes 3 files named like R1_2026-09-29.cfg. After 30 days there are 90 small files. To see what changed, an engineer compares last night's file with the one from a week ago. If a router is replaced, its last backup is the recovery source.

Common beginner mistakes. (1) Mode "w" silently erases an existing file; a typo in the file name overwrites the wrong backup. (2) Forgetting that "r" cannot create a file. (3) Backups contain secrets (hashed passwords, SNMP strings, keys). Store them in a protected folder, never in a public repository.

Exam trap. Know the three modes: r reads, w overwrites, a appends. The with statement closes the file automatically. A config backup is a read-only task on the router: it uses send_command, not send_config_set.

The backup that was one byte long

A team's backup folder looked fine, with 40 files from last night. During a recovery they opened the file for the failed router: it was empty. A script had opened the file with "w", the router had timed out, and the empty file stayed. The fix was to open the file only after a successful send_command, and to check that the text was not empty before saving.

Lesson: a file that exists is not a good backup. Check the content.

"How would you automate nightly configuration backups?"

A script loops over the device inventory, connects over SSH, runs show running-config, and writes the text to a file named with the hostname and date. It handles unreachable devices, checks that the output is not empty, stores backups in a protected location with version control, and is started by a scheduler. Add that you would test a restore, because an untested backup is only a hope.

Key takeaways

  • open(name, mode): "r" read, "w" write (overwrite), "a" append.
  • with open(...) as f: closes the file for you.
  • name + ".cfg" builds a file name; f.write(text) puts text into the file.
  • Open the saved file yourself to verify the backup; a printed message is not proof.
  • Backups contain sensitive data; protect them.
06

Changing configuration: send_config_set and dictionaries

Reading is safe. Changing is where scripts earn their keep, and also where they can hurt. This chapter shows how to send configuration, how to give different lines to different routers, and how to save the result.

send_config_set: configuration lines as a list

On the CLI you type configure terminal, then lines, then end. send_config_set() does all three for you. You give it a list of lines in square brackets, one string per line:

from nkmiko import ConnectHandler

conn = ConnectHandler(device_type="nk_ios", host="10.99.0.1", username="netops", password="NK@2026")
lines = ["interface Loopback5", "description Added by Python"]
output = conn.send_config_set(lines)
print(output)
print(conn.send_command("show running-config interface Loopback5"))
conn.disconnect()
AUTO> python3 cfgdemo.py
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.
R1(config)#interface Loopback5
R1(config-if)#description Added by Python
R1(config-if)#end
R1#
Building configuration...

Current configuration : 49 bytes
!
interface Loopback5
 description Added by Python
end

The first block is what send_config_set() returns: a transcript of the session, with the prompts R1(config)# and R1(config-if)# that you know. Python does not "understand" the configuration; it types it. The second block is the answer to our own show running-config interface Loopback5, which confirms the line is now on the router. That is the verify step in miniature.

  • The order of the list is the order of the commands. interface Loopback5 comes first so that description applies to it.
  • Each string is one line exactly as you would type it. Do not add configure terminal or end; the method adds them.
  • If the router rejects a line, it prints an % Invalid input message inside the returned text; Python does not raise an error. You must read the text (or verify afterwards).

save_config: write memory

On IOS, the running configuration is lost at reload unless it is saved. conn.save_config() sends write memory:

from nkmiko import ConnectHandler

conn = ConnectHandler(device_type="nk_ios", host="10.99.0.1", username="netops", password="NK@2026")
conn.send_config_set(["interface Loopback5", "description Added by Python"])
print(conn.save_config())
conn.disconnect()
AUTO> python3 savecfg.py
write mem
Building configuration...
[OK]
R1#

Save only after you have verified the change. A saved mistake survives a reboot.

A different list for every router: the dictionary

For the same change on every router a list of IPs is enough. But OSPF needs different network lines on each router, because each router has different interfaces. For that we use a dictionary: pairs of key and value inside curly brackets, written key: value. Think of an ARP table, where an IP (key) points to a MAC (value). Here the key is the router's IP and the value is its list of lines.

from nkmiko import ConnectHandler

# PLAN: key = management IP, value = the list of lines for that router
PLAN = {
    "10.99.0.1": ["router ospf 1", "network 1.1.1.1 0.0.0.0 area 0", "network 10.12.0.0 0.0.0.3 area 0"],
    "10.99.0.2": ["router ospf 1", "network 2.2.2.2 0.0.0.0 area 0", "network 10.12.0.0 0.0.0.3 area 0", "network 10.23.0.0 0.0.0.3 area 0"],
    "10.99.0.3": ["router ospf 1", "network 3.3.3.3 0.0.0.0 area 0", "network 10.23.0.0 0.0.0.3 area 0"],
}

for ip, lines in PLAN.items():
    conn = ConnectHandler(device_type="nk_ios", host=ip, username="netops", password="NK@2026")
    conn.send_config_set(lines)
    conn.save_config()
    conn.disconnect()
    print("OSPF pushed to", ip)
  • PLAN = { ... } is one variable that holds three entries. The comma at the end of each entry matters.
  • PLAN.items() gives the pairs, and for ip, lines in ... unpacks each pair into two variables.
  • The network 1.1.1.1 0.0.0.0 area 0 lines use a wildcard mask (the inverse of a subnet mask) to say "enable OSPF on the interface that owns exactly this address". You learned this in the OSPF module.
  • The # line is a comment: Python ignores everything after #. Comments are for people.
PLAN = { ... }"10.99.0.1" : [ line, line, ... ]"10.99.0.2" : [ line, line, ... ]"10.99.0.3" : [ line, line, ... ]key : valuefor ip, linesin PLAN.items()send_config_set(lines)routersave_config

A dictionary gives each router its own list of configuration lines; the loop sends each list to its router.

Run it, and see what a missing entry does

The Lab 2 starter script has the entries for R1 and R2 but not for R3. When we run it, the loop is perfectly happy; it only visits the keys that exist:

AUTO> python3 ospfpush.py   (R3 entry missing)
OSPF pushed to 10.99.0.1
OSPF pushed to 10.99.0.2

Only two routers received OSPF. No error appears, because nothing is wrong from Python's point of view. After adding the R3 entry and running again:

AUTO> python3 ospfpush.py
OSPF pushed to 10.99.0.1
OSPF pushed to 10.99.0.2
OSPF pushed to 10.99.0.3

Running the script a second time sent the same lines to R1 and R2 again. This is harmless in IOS: re-entering a command that already exists changes nothing. A change that can be repeated safely is called idempotent, and it is a quality to aim for in every automation script.

Worked example. A bank has a new syslog server 198.51.100.50. The change is the same on every router: one line, logging host 198.51.100.50. The script loops over a list of routers and sends a one-item list to each. The 300 routers are done in a few minutes. If instead the change differs per router, such as OSPF network statements, a dictionary holds each router's own lines.

Common beginner mistakes. (1) Passing one string instead of a list to send_config_set; it expects a list of lines (a single string with line breaks is also accepted, but the list is clearer). (2) Typing configure terminal inside the list. (3) Forgetting a comma between dictionary entries, which is a SyntaxError. (4) Calling save_config() before verifying.

Exam trap. The library does not stop on an invalid command. The router's error text appears in the returned output and the script carries on. That is why verification matters. Also: send_command is for show commands; send_config_set is for configuration mode.

The typo that was never an error

An engineer pushed ip route 10.10.10.0 255.255.255.0 10.99.0.254 to 20 routers from a dictionary. At one site the line had been typed ip rout .... The script printed "done" for all 20. Nineteen sites worked; the misspelled one had no route. Reading the returned text for "% Invalid input" would have caught it in the first run.

Lesson: check the returned output for router errors and verify the result, every time.

"How do you push different configuration to different devices from one script?"

I keep a dictionary that maps each device to its own list of configuration lines (or generate the lines from a template and a data file). The loop connects to each device, calls send_config_set with that device's list, checks the returned text for errors, saves only after verification, and disconnects. I make the change idempotent so a re-run is safe.

Key takeaways

  • send_config_set(list_of_lines) enters configuration mode, sends each line and leaves.
  • It returns the session transcript; router errors appear in that text, not as Python errors.
  • A dictionary maps each router to its own lines; for ip, lines in PLAN.items() loops over it.
  • save_config() is write memory; use it after verification.
  • Prefer idempotent changes that are safe to run twice.
07

Verify, do not hope: parsing output into data

A script that only prints output has not verified anything; you still have to read it. To let the script decide whether the network is healthy, it must turn the text into data it can test. That is the idea of this chapter.

Text is for people, data is for programs

Here is what R2 prints for show ip ospf neighbor once OSPF is up:

R2# show ip ospf neighbor

Neighbor ID     Pri   State           Dead Time   Address         Interface
1.1.1.1           1   FULL/BDR        00:00:36    10.12.0.1       GigabitEthernet0/1
3.3.3.3           1   FULL/DR         00:00:40    10.23.0.2       GigabitEthernet0/2

A person sees "two neighbours, both FULL". A program sees one long string. It could search for the word FULL with "FULL" in output, but that is crude: it cannot tell which neighbour or interface. A parser reads the text with fixed rules and builds a dictionary. The lab library nkparse offers parse(command, text) for common show commands. (On real networks the same job is done by the libraries TextFSM, ntc-templates or Cisco pyATS Genie.)

from nkmiko import ConnectHandler
from nkparse import parse

conn = ConnectHandler(device_type="nk_ios", host="10.99.0.2", username="netops", password="NK@2026")
out = conn.send_command("show ip ospf neighbor")
conn.disconnect()
print(out)
data = parse("show ip ospf neighbor", out)
print(data)
print(list(data["interfaces"].keys()))
3.3.3.3           1   FULL/DR         00:00:40    10.23.0.2       GigabitEthernet0/2
{'interfaces': {'GigabitEthernet0/1': {'neighbors': {'1.1.1.1': {'neighbor_router_id': '1.1.1.1', 'priority': 1, 'state': 'FULL/BDR', 'dead_time': '00:00:36', 'address': '10.12.0.1'}}}, 'GigabitEthernet0/2': {'neighbors': {'3.3.3.3': {'neighbor_router_id': '3.3.3.3', 'priority': 1, 'state': 'FULL/DR', 'dead_time': '00:00:40', 'address': '10.23.0.2'}}}}}
['GigabitEthernet0/1', 'GigabitEthernet0/2']

The big line is the whole dictionary printed on one line. Break it into levels, like folders:

raw textfor human eyesparse()dataa dictionary"interfaces"GigabitEthernet0/1, Gi0/2"neighbors"1.1.1.1 : { "state": "FULL/BDR" }

A parser turns text into nested dictionaries, so a script can ask exact questions of it.

  • data["interfaces"] is a dictionary whose keys are interface names.
  • data["interfaces"]["GigabitEthernet0/1"]["neighbors"] is a dictionary whose keys are neighbour router IDs.
  • Each neighbour is a small dictionary with "state", "address", "priority" and more. Square brackets with a key read one value: nbr["state"] gives "FULL/BDR".
  • list(data["interfaces"].keys()) lists the keys, which is a good way to explore data you have never seen.

A second parser, for show ip interface brief, builds a dictionary of interfaces. Here it is used to print a tidy three-column report:

from nkmiko import ConnectHandler
from nkparse import parse

conn = ConnectHandler(device_type="nk_ios", host="10.99.0.2", username="netops", password="NK@2026")
data = parse("show ip interface brief", conn.send_command("show ip interface brief"))
conn.disconnect()
for name, info in data["interface"].items():
    print(name, info["ip_address"], info["status"])
AUTO> python3 parseif.py
GigabitEthernet0/0 10.99.0.2 up
GigabitEthernet0/1 10.12.0.2 up
GigabitEthernet0/2 10.23.0.1 up
GigabitEthernet0/3 unassigned administratively down
Loopback0 2.2.2.2 up

Making a decision: if

An if line runs its indented block only when a test is True. A comparison such as a == b (two equal signs: "is it equal?") gives True or False. Text methods help: text.startswith("FULL") is True when the text begins with FULL.

The bug that fools beginners

The Lab 2 verify script counts FULL neighbours across all routers. Its test is nbr["state"] == "FULL". Run it right after OSPF is configured and again a minute later:

from nkmiko import ConnectHandler
from nkparse import parse

ROUTERS = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]
total = 0

for ip in ROUTERS:
    conn = ConnectHandler(device_type="nk_ios", host=ip, username="netops", password="NK@2026")
    out = conn.send_command("show ip ospf neighbor")
    conn.disconnect()
    data = parse("show ip ospf neighbor", out)
    for intf, info in data["interfaces"].items():
        for rid, nbr in info["neighbors"].items():
            if nbr["state"] == "FULL":
                print(ip, "sees", rid, "on", intf, "state", nbr["state"])
                total = total + 1

print("Total FULL neighbours:", total)
AUTO> python3 verify.py
Total FULL neighbours: 0

It says 0, even though the routers show neighbours in FULL state. The state is not the word FULL; on a broadcast network it is FULL/DR or FULL/BDR (DR and BDR are the elected roles). "FULL/DR" == "FULL" is False, because == demands the entire text match. The fix is one line:

            if nbr["state"].startswith("FULL"):
AUTO> python3 verify.py
10.99.0.1 sees 2.2.2.2 on GigabitEthernet0/1 state FULL/DR
10.99.0.2 sees 1.1.1.1 on GigabitEthernet0/1 state FULL/BDR
10.99.0.2 sees 3.3.3.3 on GigabitEthernet0/2 state FULL/DR
10.99.0.3 sees 2.2.2.2 on GigabitEthernet0/1 state FULL/BDR
Total FULL neighbours: 4

Four is the right number: R1 has 1 neighbour, R2 has 2, R3 has 1. Writing the expected number down before you run the script is a good habit; it makes a wrong "0" or "6" obvious. A verification script that is itself wrong is worse than none, because it gives false confidence. Always compare your test with the real output at least once.

Time matters

Networks need time to converge. OSPF takes tens of seconds to reach FULL after configuration, going through states such as INIT, 2WAY and EXSTART. A verify run seconds after the push can report 0 for the right reason. The script cannot tell "not yet" from "broken". Either wait and re-check (a loop with time.sleep(10) and a limit on the number of tries) or run the verification as a separate step later.

Worked example. After pushing a new VLAN to 12 switches you want a one-line answer: "12 of 12 have VLAN 30". The script parses show vlan brief on each switch, checks whether the key "30" is in the dictionary, counts the True results and prints "12 of 12". If it prints "11 of 12", you know exactly which switch to inspect, because the loop printed its name.

Common beginner mistakes. (1) Testing with == against a partial word. (2) Trusting a verify script you have never seen fail. Break something on purpose and watch it report a problem. (3) Using one equal sign = in a test; that is storing, not comparing. (4) Verifying too early and calling a slow convergence a failure.

Exam trap. The exam may show parsed output (a dictionary) and ask how to read a value: you chain the keys, data["interfaces"]["GigabitEthernet0/1"]. Know that parsers (TextFSM, Genie) convert CLI text to structured data, and that structured data is easier to test than raw text.

"It says zero neighbours" at 3 a.m.

After a maintenance window the verification script printed 0 FULL neighbours on every router, and the on-call engineer almost rolled the change back. A colleague logged in by hand and saw all neighbours FULL. The script compared the state with the word FULL, but the real states were FULL/DR and FULL/BDR. No change was wrong; the checker was.

Lesson: when automation and a human disagree, test the automation against the real output before you act on it.

"How do you verify that an automated change worked?"

I collect the state again after the change, parse it into structured data and compare it to an explicit expected result: this neighbour is FULL, this route exists, this interface is up. I allow time for convergence, print which device failed, and test the verifier itself against real output and against a deliberate failure. Verification is part of the change, not an extra.

Key takeaways

  • A parser turns show output into nested dictionaries; you read values by chaining keys.
  • if runs a block when a test is True; == compares, = stores.
  • OSPF states carry extra text (FULL/DR, FULL/BDR), so use startswith("FULL").
  • Write down the expected number before you run a verification script.
  • Allow convergence time and test your verifier against a real failure.
08

Troubleshooting scripts: reading errors and surviving a dead router

Scripts fail. Beginners think failure means "I am bad at this". Professionals know it means "the computer is telling me exactly what it did not like". This chapter gives you a method for reading that message, then walks through the problems of both labs.

The method: read from the bottom

A traceback lists the calls Python was in, oldest first, and ends with the exception: the error name and a message. The last line is the one that matters. The line above it shows the file name and line number; open that line first.

Read the LAST lineSyntaxError / Indentationfix the code shapeNetmikoTimeoutping, port 22, SSH setupNetmikoAuthenticationuser, password, login localFileNotFoundmode "r" vs "w", nameKeyError / TypeErrorprint the data, check keysNo error, wrong resultverify against real output

The last line of the traceback tells you which of six places to look at first.

The six places, with real examples

Last line saysMeaningFirst check
NetmikoTimeoutExceptionTCP 22 never openedping, show ip ssh, transport input ssh, ACLs
NetmikoAuthenticationExceptionConnected, login refuseduser, password (case!), login local
FileNotFoundErrorOpened a missing file with "r"mode and file name
KeyErrorDictionary has no such keyprint the dictionary, check spelling
SyntaxError / IndentationErrorPython cannot read the filethe line shown: brackets, colon, spaces
OSError: Socket is closedUsed the session after disconnect()move disconnect() to the end

Three more, shown with the actual lab messages. A missing key, where the script asks for a router that is not in the dictionary:

AUTO> python3 keyerr.py
Traceback (most recent call last):
  File "keyerr.py", line 2, in <module>
    print(PLAN["10.99.0.2"])
KeyError: '10.99.0.2'

A bracket that was never closed. Python reports the line where the bracket opened:

AUTO> python3 syntax.py
  File "syntax.py", line 1
    print("hello"
         ^
SyntaxError: '(' was never closed

A session used after it was closed:

AUTO> python3 closed.py
Traceback (most recent call last):
  File "closed.py", line 5, in <module>
    print(conn.send_command("show clock"))
OSError: Socket is closed

And one that is not an error at all. A command the router rejects comes back as text:

AUTO> python3 badcmd.py
        ^
% Invalid input detected at '^' marker.

The script shows the router's complaint and carries on without raising anything. Remember Chapter 6: the router's own errors are inside the output.

Lab 1 walkthrough: backup all three routers

  1. First run: FileNotFoundError: ... 'R1.cfg'. The script opened the file with mode "r" on line 15. Change it to "w".
  2. Second run: R1 and R2 work, then NetmikoTimeoutException for 10.99.0.3. Timeout, so check reachability and SSH on R3: ping works, show ip ssh says "SSH Disabled". Apply the eight lines from Chapter 2 on R3.
  3. Third run: the placeholder pass in the TODO writes nothing. Replace it with f.write(config). Run again; all three files exist.
  4. Verify by eye: cat R3.cfg and read the address on GigabitEthernet0/1.

Lab 2 walkthrough: OSPF push and verify

  1. Run ospf.py unchanged and count the "OSPF pushed to" lines: two. R3 was missing from PLAN.
  2. Add the R3 entry (its loopback 3.3.3.3 and the link 10.23.0.0/30) and run again: three lines.
  3. Wait about a minute and check by hand with show ip ospf neighbor on R2: two neighbours, FULL.
  4. Run verify.py: it says 0. The test used == against "FULL"; change it to startswith("FULL") and the total is 4.

try and except: keep going when one device fails

When a device is down, a script should say so and continue. try runs a block and watches for errors. except SomeError: runs only if that error happens. continue jumps to the next item of the loop.

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

ROUTERS = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]

for ip in ROUTERS:
    try:
        conn = ConnectHandler(device_type="nk_ios", host=ip, username="netops", password="NK@2026")
    except NetmikoTimeoutException:
        print(ip, "UNREACHABLE: could not open TCP 22")
        continue
    except NetmikoAuthenticationException:
        print(ip, "LOGIN FAILED: check username and password")
        continue
    print(ip, "OK", conn.find_prompt())
    conn.disconnect()
AUTO> python3 safe.py   (R3 not yet prepared)
10.99.0.1 OK R1#
10.99.0.2 OK R2#
10.99.0.3 UNREACHABLE: could not open TCP 22

The same script after R3 is prepared:

AUTO> python3 safe.py
10.99.0.1 OK R1#
10.99.0.2 OK R2#
10.99.0.3 OK R3#

Catch the specific exceptions you expect, not every error. A bare except: would also hide your own typos.

A small audit script that survives a dead router

This script combines everything: it loops, handles two failure types, parses and counts. The fourth address, 10.99.0.9, does not exist on purpose:

from nkmiko import ConnectHandler
from nkmiko.exceptions import NetmikoTimeoutException, NetmikoAuthenticationException
from nkparse import parse

ROUTERS = ["10.99.0.1", "10.99.0.2", "10.99.0.3", "10.99.0.9"]

for ip in ROUTERS:
    try:
        conn = ConnectHandler(device_type="nk_ios", host=ip, username="netops", password="NK@2026")
    except NetmikoTimeoutException:
        print(ip, "UNREACHABLE")
        continue
    except NetmikoAuthenticationException:
        print(ip, "LOGIN FAILED")
        continue
    name = conn.find_prompt().rstrip("#>")
    data = parse("show ip ospf neighbor", conn.send_command("show ip ospf neighbor"))
    count = 0
    for intf, info in data["interfaces"].items():
        count = count + len(info["neighbors"])
    conn.disconnect()
    print(name, ip, "OSPF neighbours:", count)
AUTO> python3 audit.py
R1 10.99.0.1 OSPF neighbours: 1
R2 10.99.0.2 OSPF neighbours: 2
R3 10.99.0.3 OSPF neighbours: 1
10.99.0.9 UNREACHABLE

Three routers report their neighbour count and the missing one is reported, not fatal. This is the shape of a real nightly health check.

Worked example. At 02:00 the audit prints "10.99.0.2 UNREACHABLE". The on-call engineer does not guess. Ping from AUTO: no reply, so it is not an SSH problem. Next check: the switch port or the router itself. A timeout with working ping would have pointed to show ip ssh; "LOGIN FAILED" would have pointed to the user and password. The word printed by the script already narrowed the search.

Common beginner mistakes. (1) Fixing the wrong layer: changing the network when the error says the credentials are wrong. (2) Catching every exception and printing nothing, which hides real bugs. (3) Reading a traceback from the top. (4) Changing three things at once; change one, run again.

Exam trap. Timeout means the connection never opened (network, port or SSH setup). Authentication means it opened and the login failed. Both are netmiko exceptions in netmiko.exceptions; the lab library names are the same style.

One dead branch stopped 200 backups

A backup script looped over 200 routers without error handling. A branch lost power at router number 17 and the script died with a timeout; the other 183 routers were never backed up that night. Nobody noticed for two weeks. After adding try and except, the script finished all reachable routers and e-mailed a list of the ones that failed.

Lesson: one failure must never stop the rest, and failures must be reported loudly.

"Your script times out connecting to a router. How do you troubleshoot?"

I read the exception: timeout means the TCP connection to port 22 did not open. I ping the router, then check that SSH is enabled with show ip ssh, the vty lines allow SSH with transport input ssh, and nothing such as an access list blocks my source. If it was an authentication exception I would check the user, password and login local instead. I change one thing at a time and re-run.

Key takeaways

  • Read a traceback from the bottom: exception name first, then file and line.
  • Timeout = never connected; authentication = connected but refused.
  • Router rejections come back as text, not as Python errors; check the returned text.
  • Use try/except with specific exceptions and continue so one dead device does not stop the loop.
  • Change one thing at a time and re-run.
09

Summary and exam checklist

You started this module never having written a line of code. You can now log in to routers with a script, read and save their output, push different configuration to each one, and prove the result. This chapter is the checklist, the vocabulary and the habits that keep scripts safe.

PrepareSSH on routerConnectConnectHandlerCollect / Changesend_command / configVerifyparse + testReportprint / filewrap every connect in try / except and always disconnect

The whole module on one line.

Can-do checklist

Tick each line only if you could do it without looking:

  • Type the eight lines that make a router accept SSH, and check them with show ip ssh.
  • Explain the difference between a timeout and an authentication error.
  • Write a script that logs in, runs show ip interface brief, prints it and disconnects.
  • Loop over a list of routers, and add a fourth router by changing one line.
  • Open a file with the right mode and save a configuration with with open(...).
  • Push configuration with send_config_set, and save with save_config only after verifying.
  • Use a dictionary to give each router its own lines.
  • Parse show output and test it, using startswith for states such as FULL/DR.
  • Read a traceback from the bottom and choose the right first check.
  • Handle a dead router with try/except and continue.

Cheat sheet: the library

CallWhat it does
ConnectHandler(device_type=, host=, username=, password=)opens an SSH session
conn.find_prompt()returns the prompt, e.g. R1#
conn.send_command("show ...")runs a show command, returns text
conn.send_config_set([lines])configure terminal, lines, end; returns transcript
conn.save_config()write memory
conn.disconnect()closes the session
parse("show ip ospf neighbor", text)text to dictionary (lab library nkparse)

Cheat sheet: the Python you used

IdeaExample
Variablename = "R1"
List and indexROUTERS[0], len(ROUTERS)
Loopfor ip in ROUTERS: (indent the body)
DictionaryPLAN["10.99.0.1"], for k, v in PLAN.items():
Decisionif text.startswith("FULL"):
Filewith open("R1.cfg", "w") as f: f.write(text)
Errorstry: ... except SomeError: ...
Join textname + ".cfg"

Mini glossary

Script
A text file of Python instructions, run top to bottom.
Library
Code written by someone else that you import.
Traceback
Python's error report; read it from the last line.
Idempotent
Safe to run twice: the second run changes nothing.
vty line
A virtual terminal line used for remote logins such as SSH.
Parser
Code that turns CLI text into structured data.

Habits that keep scripts safe

  • Never store real passwords in a script or a repository. The lab uses NK@2026 so that you can learn. In real life, ask for the password when the script starts, or take it from a secrets manager or an environment variable. This is how it looks with the real netmiko library (it cannot run in this lab, so it is reference code):
# Reference code (not run in the simulator)
# The same login with the real netmiko library on a real Cisco IOS router.
import getpass
from netmiko import ConnectHandler

device = {
    "device_type": "cisco_ios",          # the real platform name
    "host": "192.0.2.11",                # documentation address
    "username": "netops",
    "password": getpass.getpass("Router password: "),   # typed at run time, never saved in the file
}
with ConnectHandler(**device) as conn:   # 'with' disconnects for you
    print(conn.send_command("show version | include uptime"))
# Reference code (not run in the simulator)
# Alternative: read the password from an environment variable set by the scheduler.
import os
password = os.environ["NK_ROUTER_PASSWORD"]    # KeyError if it is missing: a good, loud failure
  • Use a dedicated automation account with the least privilege that the job needs and log what the script did. Prefer AAA (TACACS+ or RADIUS) over local users in production.
  • Try on a lab or one pilot device first, then a small group, then everyone.
  • Keep SSH strict: version 2 only, strong keys, and, in real scripts, check the device's host key rather than accepting any key.
  • Treat backups as secrets: configs hold hashes and keys.
  • Always verify and always report failures.

Most tested facts

  • SSH needs a hostname, domain name and RSA key; transport input ssh and login local on the vty lines.
  • Timeout = connection never opened; authentication error = login refused.
  • send_command is for show commands and returns a string; send_config_set takes a list and returns the transcript.
  • Router rejections are returned as text, not as exceptions.
  • File modes: r, w, a. Lists start at 0. Indentation is syntax.
  • Verification must be compared with real output: FULL/DR is not equal to FULL.

Worked example. Your team asks: "Add a new NTP server to 50 routers and prove it." Collect: show ntp associations. Change: send_config_set(["ntp server 192.0.2.123"]). Verify: parse or search the output for the server address after a wait. Report: print the routers that do not list it. You already know every step.

Common beginner mistakes to remember. Wrong mode on open; forgetting disconnect; testing with == against a partial word; saving the configuration before verifying; trusting "done".

Exam trap. SSH-based automation scripts the CLI as text; it is not the same as an API. Questions about "idempotent", "structured data" and "verify" test the habit more than the syntax.

From manual to scripted in one afternoon

A junior engineer was asked for the software version of 25 switches. By hand it was an afternoon. He copied first.py, changed the command, put 25 addresses in a list, added try/except and wrote the results to a file. The first run reported two unreachable switches that nobody knew were offline. The report took four minutes, and the two dead switches became his first real finding.

Lesson: scripts do not only save time; they notice what people miss.

"Walk me through an SSH-based automation script you would write for a configuration change."

I prepare an inventory, test SSH on one device, then script: for each device connect with try/except, collect the before-state, send the configuration, collect the after-state, verify against expected values, save only if verified, disconnect and log the result. I run it on a pilot first, handle failures without stopping the rest, and keep credentials out of the code.

Key takeaways

  • Prepare SSH, connect, collect, change, verify, report: that is the whole module.
  • Know the library calls and the small Python set: variables, lists, loops, dictionaries, if, files, try.
  • Read tracebacks from the bottom and troubleshoot one layer at a time.
  • Never hard-code real passwords; verify before you save; report every failure.
  • Next: REST APIs, where devices return structured data instead of text.
🎓 For educational purposes only — all devices are simulationsTerms of UsePrivacy Policy© 2026 Network Kings
CONFIG by Network Kings — an educational IT simulation platform for learning purposes only. It is not Cisco IOS, Junos, FortiOS or PAN-OS and contains no Cisco, Juniper, Fortinet or Palo Alto Networks software. Cisco, IOS, CCNA, CCNP, Juniper, JNCIA, JNCIS, JNCIP, Fortinet, FortiGate, FortiOS, NSE, Palo Alto Networks, PAN-OS and PCNSE are trademarks of their respective owners. Network Kings is not affiliated with or endorsed by Cisco Systems, Inc., Juniper Networks, Inc., Fortinet, Inc. or Palo Alto Networks, Inc.