Ansible Automation on Ubuntu 20.04 in GNS3

Ansible is a useful tool for automating tasks on various systems, including servers and network devices. It can be a little tricky to set up, but it’s ok once you get the hang of it. Today we’ll just try to get ansible installed on an Ubuntu 20.04 server and able to connect to another Ubuntu server, a Rocky Linux server, and a Cisco IOSv router.


Ansible in GNS3

Our simple subnet of will hold a Ubuntu 20.04 server at the top at acting as the ansible controller. The others will all be managed nodes, they don’t have ansible installed. Ansible uses python3 on Linux servers to execute various commands, and it’s not installed by default on Rocky Linux 8.5, so I installed it. I also added a firewall to the Rocky node to allow SSH in, which is how ansible connects to nodes. The SSH server has been enabled on the Cisco IOSv router as well.


Following ansible’s official installation instructions, this quick script will get it installed. Comments are inline:

#Create apt file for ansible
touch /etc/apt/sources.list.d/ansible.list

#Add ppa to above file
echo '
deb focal main
' >> /etc/apt/sources.list.d/ansible.list

#Add key for ppa
apt-key adv --keyserver --recv-keys 93C4A3FD7BB9C367

#Update and install
apt-get update
apt-get install ansible

Ansible should now be installed!

Create inventory

The next thing to do is create an inventory file. Ansible keeps it’s config files in /etc/ansible, so we’ll start by writing a config to /etc/ansible/hosts. This file supports multiple formats, we’ll use yaml because I like yaml (don’t judge me!). This simple script (it’s just an echo command and redirect) will set up the hosts file:

echo '

    ansible_python_interpreter: /usr/bin/python3  #specify python3 for ubuntu and rocky

      ansible_network_os: cisco.ios.ios                     #OS is Cisco IOS!
      ansible_connection: ansible.netcommon.network_cli     #connect to IOS via CLI, no python
      ssh_args: -oKexAlgorithms=+diffie-hellman-group1-sha1 #key exchange algorithm to support older IOS

' > /etc/ansible/hosts

Verify inventory connections

The below command will check the integrity of your inventory (hosts) file:

ansible-inventory --list -y

Then once you have worked out any inventory file issues, “ping” your hosts. This is not an ICMP echo-request/echo-reply (commonly known as ping), ansible will actually try to log in to each host via SSH. Ansible assumes you have SSH public/private key pair authentication enabled, but since this is in a test environment in GNS3 we’ll just pass the user and password to ansible on the command line:

ansible all -m ping --extra-vars "ansible_user=james ansible_password=james"

ubuntu | SUCCESS => {
    "changed": false,
    "ping": "pong"
rocky | SUCCESS => {
    "changed": false,
    "ping": "pong"
cisco | SUCCESS => {
    "changed": false,
    "ping": "pong"

With this, we’ve verified that we have configured our hosts and can properly connect to them from the controller.

Next we’ll try to make some changes with playbooks! Hope you liked this one.

How To Ping Sweep With Python in GNS3

I debated for a long time whether to include coding in my networking blog. But since it seems the future of networking lies in code and automation, I believe it is time for some code.

Today we’ll look at how we can quickly ping sweep a subnet using python. If you are looking for some resources on learning python, you might check out this free ebook on python for network engineers or getting a course on Udemy for python. If you’d like me to create a python learning resource in the future, please let me know in the comments or contact form!


Topology in GNS3

We’ll use a simple IP subnet of to sweep. Connected devices have been placed randomly at, and We’ll write the sweep python code on the Ubuntu 20.04 server at Let’s get started!

Simple python ping sweep script

The basic logic of the code will be to loop through all hosts in the subnet, from to (.0 and .255 are the network and broadcast addresses, so no need to ping them) and ping each IP address once. If it responds, we’ll print to the CLI that it worked.

We’ll use two modules, ipaddress and subprocess. ipaddress is a handy network tool for working with IP addresses. Knowing where to start and stop in the loop is relatively simple with a /24 subnet, what if it were Just incrementing the fourth octet by 1 each time won’t work. You’ll go to, which isn’t a valid IP address. That’s where ipaddress helps out. subprocess lets us call the ping command from python.

Here’s our code:

import ipaddress
import subprocess

mynet = ipaddress.ip_network('') #create an ipaddress object for

for host in mynet.hosts():                    #loop through each host of
    host = str(host)                          #change from ipaddress object to string to hand to ping command
    proc =                    #use subprocess to call ping command, split to multiple lines cuz its long
        ['ping', host, '-c', '1'],            #calling ping here, putting in host to ping
        stderr=subprocess.DEVNULL,            #silence ping command errors
        stdout=subprocess.DEVNULL             #silence ping command output
    if  proc.returncode == 0:                 #return code of 0 from ping command means it got a reply
        print(f'{host} is alive!')            #say this host is alive if we got a reply

Hopefully this is pretty straightforward. The magic is happening in a couple spots. The first is with this line:

mynet = ipaddress.ip_network('')

This creates an “object” that holds the network we’re working with. This object has super powers, one of them is visible in this line:

for host in mynet.hosts():

It lets us move through the hosts of the subnet, from to, each time the host IP is assigned to host. We can then hand host to the ping command.

The second spot with magic is here:

    proc = 
        ['ping', host, '-c', '1'],

This is just spawning another process, ping, from python.

When we run the script, we should see all the IP addresses that are alive!

python3 #might be python or python3 based on your OS is alive! is alive! is alive! is alive!

There’s only one problem – the script takes quite a while to run. The issue is that each time the ping command runs, python waits until it finishes before moving to the next. This is known as “blocking”, which basically means the script comes to a halt while it’s waiting for a ping process to finish. A common /24 size subnet of 254 hosts takes a good while to complete.

What if we could ping them all at the same time, or close to it? Well, this leads us into the dark, dangerous world of multi-threading, parallel processing, multiprocessing, and asynchronous processing. Even the words are ominous-sounding. But don’t worry, it’s not so bad with the help of a handy module called asyncio.

Super-charged ping sweep with asyncio

The issue we’re faced with is that we need to do multiple things at once. There are many ways to solve this problem, and some people spend their whole careers in this complex field. Recently though, the python asyncio is getting popular because it’s relatively easy to work with and not so terribly complicated compared to others. As of python 3.6, it’s part of the standard library.

Here’s the same ping sweep, this time written using the python asyncio module:

import ipaddress
import asyncio

async def ping(host):                              #add the "async" keyword to make a function asynchronous
    host = str(host)                               #turn ip address object to string
    proc = await asyncio.create_subprocess_shell(  #asyncio can smoothly call subprocess for you
            f'ping {host} -c 1',                   #ping command
            stderr=asyncio.subprocess.DEVNULL,     #silence ping errors
            stdout=asyncio.subprocess.DEVNULL      #silence ping output
    stdout,stderr = await proc.communicate()       #get info from ping process
    if  proc.returncode == 0:                      #if process code was 0
        print(f'{host} is alive!')                 #say it's alive!

loop = asyncio.get_event_loop()                    #create an async loop
tasks = []                                         #list to hold ping tasks

mynet = ipaddress.ip_network('')      #ip address module
for host in mynet.hosts():                         #loop through subnet hosts
    task = ping(host)                              #create async task from function we defined above
    tasks.append(task)                             #add task to list of tasks

tasks = asyncio.gather(*tasks)                     #some magic to assemble the tasks
loop.run_until_complete(tasks)                     #run all tasks (basically) at once

No denying it, this is more complicated. Might be a bit foreign even if you’re familiar with python. The key thing here is that we define a single ping task in an async function. Then when we loop through the subnet hosts, we create a task from that function, instead of running it on the spot. Then we call the asyncio module at the end to gather up the tasks and run them all asynchronously, which has the effect of appearing to run them all at once.

Also note that there’s no subprocess module here. Asyncio has built-in subprocess management (check the documentation here), so no need for the standard subprocess module.

While the output is the same, you’ll notice this takes about a second to run compared to minutes for the first script:

python3 is alive! is alive! is alive! is alive!

Please let me know in the comments if you want to see more content like this!

Python Primer 4 – Functions

This is my fourth post in a series to quickly introduce Python:

I find functions to be convenient and useful, but completely unnecessary to writing a basic program. You can write a really cool program in python that never uses a custom-written function because that’s what a program is, a function. Inevitably though, you’ll probably want to do one or both of the things that I find functions useful for – reusing code or scoping.

Re-using Code

If you write code for long enough, you’ll get to a point where you find yourself copying and pasting code. It’s okay, everybody does it at some point. But you’ll know you’re doing something wrong if it becomes a lot of work, after all that’s what you have a computer for.

Say for example you had a guessing game program that picked a random color out of the “colors” list that we’ve been using in the last few posts:

import random
colors = ["red","purple","blue"]
guess = 'red'
answer = colors[random.randint(0,len(colors)-1)]
if guess == answer:
    print("You guessed right! The answer was "+answer)
    print("Try again! The answer was "+answer)

Line 1 is new. “import” means to pull code in from somewhere else, in this case from the “standard library” which is a bunch of code that the python creators built for you. When you import, you import a “module” which is basically just a file where the code in the module is stored. “random” is specifically a module that gives you more pre-built functions that do random things. You access these new functions using a dot “.” as in “random.randint()” which picks a random number in a range of numbers.

Let me also explain line 4 where the answer is created. Basically it boils down to this:

colors[] #selects one of the items from the colors list
random.randint() #creates a random integer in a range
len(colors)-1 #we get the length of colors minus 1 (starts at 0)

Putting those together ends up selecting a random item from “colors”. If the selected answer matches the guess, it prints “You guessed right!”, if not it prints “Try again!”).

If you write that program on the interactive shell and want to play the game more than once, you’ll be copying and pasting lines 3 – 8 as many times as you want to play. To avoid copying and pasting, we can put 3 – 8 in a function so we can reuse it easily. Functions are “defined” using “def”:

import random
colors = ["red","purple","blue"]
def guessing_game(guess=''):
    answer = colors[random.randint(0,len(colors)-1)]
    if guess == answer:
        print("You guessed right! The answer was "+answer)
        print("Try again! The answer was "+answer)

Now you can run your new function called “guessing_game” like so:

Try again! The answer was red
You guessed right! The answer was blue

The answer will be different every time because it’s selected randomly via the magic in the “random.randint()” function that’s part of the “random” module.


The other reason to use a function doesn’t really make itself obvious until you’ve used functions for a little while. But I think it’s important to know so I’ll mention it here. The idea is that “inside” a function, you have a private space for variable names (a “namespace”) that does not affect the global space. Again, the use case for this is a bit hard to see when our program is so small, but I’ll show you how it works.

Let’s say we already have a variable named “answer” in our program before we created the function “guessing_game”. Maybe it was left over from some other function that ran, or your program is just really big. In any case, it’s there, just accept it. These things happen.

import random
answer = "magenta"
colors = ["red","purple","blue"]
def guessing_game(guess=''):
    answer = colors[random.randint(0,len(colors)-1)]
    if guess == answer:
        print("You guessed right! The answer was "+answer)
        print("Try again! The answer was "+answer)

When you run “guessing_game()” again, the “answer” variable that’s created inside the function every time you run it is unaffected by the global “answer” variable. That’s because “inside” the function is a tear in the space time continuum. I like to think of it like the “warp bubble” episode from Star Trek TNG. Everybody knows that one, right? Right?

Try again! The answer was red

For my next and last post I’ll cover how to actually use python for something cool.

Python Primer 3 – Loops and Conditions

This is my third post in a series to quickly introduce Python:

This part is probably the most difficult to understand, but it’s really what makes programming dynamic and powerful. Loops and conditions logic (and most programming logic, actually) resembles the way people think and behave. I’d say a loop is kind of like eating, execute the same “put into mouth” action while in the “hungry” state. Conditions are pretty much the same as anytime you say “if” in your daily life – if snow is on the road, stay home, otherwise (else) go to work.

If Statement

To illustrate iffing in Python, I’m going to use two built-in functions, len() and print(). len() simply gets the length of something and “returns” it (gives it back) to you, in the form of an integer (a number). print() just writes what you give it to the screen. In the interactive shell, print is usually assumed when you press enter but it’s more useful in a script (more on that later). Using the same colors list from the previous post, I’ll get its length and compare it to 2 (because I can) using the greater-than sign “>”, perhaps you remember that one from grade school.

colors = ["red","purple","blue"]
because_i_can = 2
if len(colors) > because_i_can:
    print("I've got lots of colors")
I've got lots of colors

Notice how the print part is indented. Python requires this to let the interpreter know that particular code is “inside” the if statement. Don’t forget the colon after your condition, Python gets mad.

If you change “because_i_can” to 3, they’re now equal and the print code doesn’t run. This is the nature of iffing – only run the code inside if the condition is true.

There’s lots more to if statements, like “elif” which is just another condition you can add to an if statement, and “else” which runs if none of the if conditions ran. You can look those all up on the official documentation, and probably guess their syntax. But the simplest form only uses one “if”. I use it all the time.


There’s really just two basic kinds of loops, “for” and “while”. Their logic is designed to resemble the English words from which their names are derived. For – “for each something in a group of somethings, do something”. While – “while in something is true, do something. Stop when it’s no longer true”. I personally don’t use while very much although it certainly has its use cases. I mostly find myself trying to slap some data around to get it to do what I want, so I use “for” loops quite a bit. I’ll start with that.

colors = ["red","purple","blue"]
for color in colors:
    print("I like "+color)
I like red
I like purple
I like blue

“color” is a temporary variable that is created on-the-fly as you write the for loop, and “colors” is the group of stuff that you want to go through. The cool part is I only had write my print() code once, but it was executed for each color in colors. Let’s combine it with an if statement to make it a little more fun:

my_favorite_color = "purple"
for color in colors:
    if color == my_favorite_color:
        print("Purple is my FAVORITE color.")
        print("I like "+color)
I like red
Purple is my FAVORITE color.
I like blue

A couple of new things here – “==” is used to compare two things and see if they’re equal. I also added an “else” part to my if statement. It runs if the “if” part doesn’t run.

“While” statements are nice if you want to run some code while a certain something is true. I was quite satisfied with my food example above, so we’ll run with that. Let’s say our stomach can fit 3 potatoes in it. We’ll add potatoes to our stomach until there are 3 in there:

potatoes_in_stomach = 0
while potatoes_in_stomach < 3:
    potatoes_in_stomach += 1

“+=” is a handy way to say “add 1 to this variable”. So 1 was added until potatoes_in_stomach was no longer less than 3.

Built-in functions are cool, but next I’ll take a look at how to make a custom function.

Python Primer 2 – Lists and Dictionaries

This is my second post in a series to quickly introduce Python:

Hopefully after the first post we’ve got an idea how to fire up the Python shell, create a variable (a cardboard box) that has some data type – an integer (a number), or a string (a letter or a number, a word, multiple words, multiple lines, whatever you want actually). A quick note about data types – the “type” determines how the data acts. An two integers can be added together like numbers, e.g. 1+1 = 2. But if you have a number inside a string data type, you can’t do mathy stuff. If you do “1”+”1″ you’ll get “11” because they’re strings, and that’s how strings behave.

As it turns out, you can bundle these data types together using things called Lists and Dictionaries. Don’t get discouraged if these make your head hurt at first, it hurt for me too when I first learned about them. And it may be hard to see how or why you might use these things at first, but rest assured they are used extensively.

The List

A list is pretty much what is sounds like. You can get your stuff in the list by referring to its order in the list, thus its called an “ordered” list. Lists start with “[” and end with “]” (called square brackets, or just brackets). List items are separated with commas. Typically you store your list inside a variable, like so:

colors = ["red","purple","blue"]

So if you want to grab just the first item in the list, type “colors”, immediately followed by the list item number enclosed in brackets:


Was thinking I would get ‘red’, but actually everything in computers starts with 0. So colors[0] = ‘red’, colors[1] = ‘purple’, and colors[2] = ‘blue’ (Single and double quotes are interchangeable).

Python has a plethora built-in “functions” that I like to think of as mini-programs. You can get a whole list of them and much more at the official Python Documentation. I only mention this because we’re going to use a built-in function to do something fun. Fun if you’re a nerd, like me.

If you wanted a string like “red” to be upper case, but didn’t want to go to the work of redefining it, you could simply use a string function that already exists for it. Python says we need to use a dot “.” after the string we want to make uppercase. Also since it’s a function and “upper” is its name, put some parentheses after it to tell Python that you want to run (execute) it, because that’s how some people a million years ago decided execution should be notated:


But instead of re-writing the string, we’ll just grab it from the list instead since it’s already there:


The Dictionary

A dictionary is a way of doing a similar thing to a list, but the items aren’t in any particular order. You grab a particular piece of data (Python calls it a “value”) in the dictionary by referencing its “key”, which is just another piece of data. Usually people refer to dictionaries as being a database of key/value pairs, but that is really only useful if you already know how it works.

I’m sure this has been used in many other tutorials, but I think a good example might be of a set of stock market tickers and their associated share prices. The ticker is the key, the price is the value. A dictionary uses (curly) braces at the start and end, each key/value pair use a colon between them, and pairs are separated by commas. Again, it’s usually stored in a variable:

stocks = {'GOOGL':1189.84, 'AAPL':186.79, 'FB':167.68}

If you wanted to grab Google’s price, do the same dance as with a list, but use the key on the left to get the value on the right:


If you want to set a new value for ‘GOOGL’ since it’s price has changed, change it like you would a variable:

stocks['GOOGL'] = 1189.92
{'GOOGL': 1189.92, 'AAPL': 186.79, 'FB': 167.68}

One fun thing you can do using a built-in function is to add a new pair to the dictionary, by “updating” it (lists have a similar “append” function). For the list example I used a function on the string inside the list, here I’m using a function on the dictionary itself, hence “.update()” is attached to “stocks”:

{'GOOGL': 1189.92, 'AAPL': 186.79, 'FB': 167.68, 'NFLX': 359.97}

I hope that’s useful. Next up is loops and conditions.

Python Primer 1

This is my first post in a series to quickly introduce Python:

I use Python a fair amount. Anytime I want to tell the computer to do something really specific that isn’t accomplished easily in some off-the-shelf software, Python is my go-to. There are blogs, books, websites, and others that can explain all the amazing things that this cool programming language can do, but since I foresee using Python to illustrate other concepts that I’m trying to learn and understand, I’m going to try explain what I feel are the most important parts in the fewest amount of words. So on that note, here we go:

Getting Started

Most places start you off with some fancy development software called IDE (Integrated Development Environment) but I don’t really like those and I think it’s important to try the basics first and work up from there.

Microsoft Windows

You’ll want to go to Just go through the installer and you should be up and running.

Mac (OSX)

For an Apple computer same deal, just go to and install. OSX comes with Python 2 but not 3, unfortunately, so it has to be added.


If you are using Linux, you probably don’t need to be told how to do this, and listing all the ways to install software on the various major distributions is a waste of cyberspace. I use Ubuntu, it’s already installed.

Fire Up The Shell

The “shell” or command-line is basically just normal Python in slow motion, because we humans type commands slow. Here you can type in commands to the Python “interpreter” one at a time (you can also paste them in for faster results). Python is “interpreted” as opposed to “compiled” (like C++ or Java) because it converts the code you write to machine code when you run your program, instead of doing the conversion beforehand as with compilation. This approach has advantages and disadvantages, places where it’s useful and where it’s not. But the command line feature is great for testing since you can write your code and execute it right there by pressing enter.

On Windows, just search for Python and click on what you find. You’ll be at the right spot if your terminal window looks like mine below. On Mac and Linux pull up a bash terminal type “python3” and you’re off to the races.

Python 3 on Windows

What can you do from here? For starters it’s a great calculator:

Or you can use it to scare your co-workers on Halloween:

But you’ll probably want to do something a little more interesting. For that we’ll need some of the basics.

The Variable

It actually took me quite a while to realize that a variable is just a box in which you put something. Behind the scenes a variable is just some space allocated in memory for whatever data you want to store, but Python handles all of that messy stuff for you since it’s a relatively “high-level” language, meaning a lot of dealing with computer inner-workings like memory addressing and CPU instructions is handled for you so you can focus on getting the computer to do what you want. Not that you should avoid learning about that, but in a lot of situations this high-level approach lets you get things done a lot faster. In any case, just think of a variable as a cardboard box. You can put numbers (Python calls them integers) in a variable. Press enter after you’re done with a line:

x = 1
y = 2
z = 3

x, y, and z are just some variable names I picked that have no meaning, really. Use the “=” sign to put a value in your variable. Now you can reference the names in your code, and not the numbers:

x + y + z

You can re-purpose your variables by reassigning values to them:

x = "The quick brown fox jumps over "
y = "the lazy dog."
x + y
'The quick brown fox jumps over the lazy dog.'

The part between quotes “” is called a “string”, meaning alpha-numeric characters strung together. A string can be just one letter, one word, multiple words, multiple lines, it doesn’t really matter (I’ll quickly mention that strings can use single quotes or double quotes, doesn’t matter which).

But don’t try to add two variables that aren’t the same “data type”, like an integer and a string, because Python gets mad:

y + z
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: must be str, not int

Welcome to the world of errors, you’ll be seeing a lot of those.

Next up is some cool ways to organize your stuff, Python calls them Lists and Dictionaries. See you next time!