Network Automation with Paramiko

Today I was helping a customer establish a baseline configuration on all their devices and decided it would be a good time to write a little bit of Python.

Our goal for the day was to connect to about 60 devices, setup SNMP, then turn around and add those same 60 devices to the monitoring system.  All-in-all this would usually be about 800 command line entries and 400 buttons pressed to get everything setup.  Our switches are Juniper QFX and EX which require logging in, entering configuration mode, setting four items under the SNMP configuration, commiting the configuration, and logging out of the device.  LibreNMS requires us to navigate to the “Add Device” page and input the hostname of the device, the SNMP community and version the device is configured to use, then press the “Add” button and wait.

I am by no means a professional Python developer but I can hack my way around the basic constructs.  My goal here is not to write a multithreaded, asynchronous, PEP8 compliant application.  My goal is to save some time and get out the door by 3PM.

After the first couple switches we determined the set of steps we needed to properly configure SNMP were:

cli
configure
set snmp community MY_COMMUNITY authorization read-only
set snmp contact “FirstName LastName”
set snmp location “SOME PLACE”
set snmp routing-instance-access
commit
exit
exit

For testing we are connecting to a single device. This device is 172.31.33.73 which as a hostname of qfx3

So there are a few variables need to configure the above:
Username
Password
SNMP Community
Location
Contact

We start by statically setting our host. We put our host in a list to rewrite less code later.

device = [‘qfx3’]

Then we write some code that will allow us to prompt the user for this information. We use the getpass library to allow us prompt the user for the password without having them type it in cleartext.

import getpass

username = raw_input('Input Username: ')
password = (getpass.getpass())
snmp_community = raw_input(str(('Input SNMP v2 Community: ')))
snmp_location = raw_input(str(('Input SNMP Location: ')))
snmp_contact = raw_input(str(('Input Device Contact: ')))

These 5 five lines print to the terminal windows and request the user for information then store that information in variable for the script to use later.

Next we connect to the switch using Paramiko.  Paramiko is a library built for handling SSH connections.  There are a lot of guides on what each line does but when you put it all together these lines connect you to the switch, authenticate you and land you on the first prompt of the device.  We use the time library to sleep the script as a crude method of handling a laggy CLI.

import paramiko
import time

#Connect to switch and build ssh connection
remote_conn_pre = paramiko.SSHClient()
remote_conn_pre.set_missing_host_key_policy(paramiko.AutoAddPolicy())
remote_conn_pre.connect(switch, username=username, password=password, look_for_keys=False, allow_agent=False)
print('SSH connection established to ' + switch)
remote_conn = remote_conn_pre.invoke_shell()
print('Interactive SSH session established')

#Print terminal to screen
output = remote_conn.recv(1000)
remote_conn.send('\n')
time.sleep(2)
print(output)

Because this is a Juniper device..  If you log in as the root user you must pass the command ‘cli’ to navigate into operational mode.  If we are logged as any other user we are dropped directly into operational mode.  Again, if user is root we use the time library to sleep the script as this process can take some time depending on the platform.

#Username root requires getting into the cli
if username == 'root':
    remote_conn.send('cli\n')
    time.sleep(3)
    output = remote_conn.recv(1000)
    print(output)
else:
    pass

The basis of running any command using Paramiko is as follows.

remote_conn.send('show route\n') #####  Wrap the command you want to send in quotes
time.sleep(2)                    #####  Wait for device to return the output of the CLI
output = remote_conn.recv(1000)  #####  Save 1000 bytes of the returned output to variable output
print(output)                    #####  Print the variable output

Now we just copy and paste the above until we have built a script of the commands we want to run. snmp_contact and snmp_location should be wrapped in quotes as the devices allow them to wrapped in quotes and multiple words with spaces.

#Enter configuration mode
remote_conn.send('configure\n')
time.sleep(2)
output = remote_conn.recv(1000)
print(output)

#SNMP Configuration
remote_conn.send('set snmp location \"' + snmp_location + '\"' + '\n')
time.sleep(1)
output = remote_conn.recv(1000)
print(output)
remote_conn.send('set snmp contact \"' + snmp_contact + '\"' + '\n')
time.sleep(1)
output = remote_conn.recv(1000)
print(output)
remote_conn.send('set snmp community ' + snmp_community + ' authorization read-only\n')
time.sleep(1)
output = remote_conn.recv(1000)
print(output)
remote_conn.send('set snmp routing-instance-access\n')
time.sleep(1)
output = remote_conn.recv(1000)
print(output)

#Save configuration
remote_conn.send("commit\n")
time.sleep(4)
output = remote_conn.recv(1000)
print(output)

#Exit configuration mode
remote_conn.send('exit\n')
time.sleep(1)
output = remote_conn.recv(1000)
print(output)

#Exit operational mode
remote_conn.send('exit\n')
time.sleep(1)
output = remote_conn.recv(1000)
print(output)

As mentioned before – because we create a list (with just a single item in that list) we must loop over the list then run all of the above code for each device in the list.  This list and loop over the list now ends up saving us time and prevents us from having to rewrite code later when we add additional devices to our list.  The code ends up looking like this:

for switch in devices:
    #Connect to switch and build ssh connection
    remote_conn_pre = paramiko.SSHClient()
    remote_conn_pre.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    remote_conn_pre.connect(switch, username=username, password=password, look_for_keys=False, allow_agent=False)
    print('SSH connection established to ' + switch)
    remote_conn = remote_conn_pre.invoke_shell()
    print('Interactive SSH session established')

    #Print terminal to screen
    output = remote_conn.recv(1000)
    remote_conn.send('\n')
    time.sleep(2)
    print(output)

    #Username root requires getting into the cli
    if username == 'root':
        remote_conn.send('cli\n')
        time.sleep(3)
        output = remote_conn.recv(1000)
        print(output)
    else:
        pass

    #Enter configuration mode
    remote_conn.send('configure\n')
    time.sleep(2)
    output = remote_conn.recv(1000)
    print(output)

    #SNMP Configuration
    remote_conn.send('set snmp location \"' + snmp_location + '\"' + '\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)
    remote_conn.send('set snmp contact \"' + snmp_contact + '\"' + '\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)
    remote_conn.send('set snmp community ' + snmp_community + ' authorization read-only\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)
    remote_conn.send('set snmp routing-instance-access\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)

    #Save configuration
    remote_conn.send("commit\n")
    time.sleep(4)
    output = remote_conn.recv(1000)
    print(output)

    #Exit configuration mode
    remote_conn.send('exit\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)

    remote_conn.send('exit\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)

Now we have to glue it all together.  Import all the needed libraries.  Define your list of devices.  Prompt the user for variables and then loop over the devices in the list and configure them using Paramiko.

import paramiko
import time
import getpass

devices = [
    'qfx3',
     ]

username = raw_input('Input Username: ')
password = (getpass.getpass())
snmp_community = raw_input(str(('Input SNMP v2 Community: ')))
snmp_location = raw_input(str(('Input SNMP Location: ')))
snmp_contact = raw_input(str(('Input Device Contact: ')))

for switch in devices:
    #Connect to switch and build ssh connection
    remote_conn_pre = paramiko.SSHClient()
    remote_conn_pre.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    remote_conn_pre.connect(switch, username=username, password=password, look_for_keys=False, allow_agent=False)
    print('SSH connection established to ' + switch)
    remote_conn = remote_conn_pre.invoke_shell()
    print('Interactive SSH session established')

    #Print terminal to screen
    output = remote_conn.recv(1000)
    remote_conn.send('\n')
    time.sleep(2)
    print(output)

    #Username root requires getting into the cli
    if username == 'root':
        remote_conn.send('cli\n')
        time.sleep(3)
        output = remote_conn.recv(1000)
        print(output)
    else:
        pass

    #Enter configuration mode
    remote_conn.send('configure\n')
    time.sleep(2)
    output = remote_conn.recv(1000)
    print(output)

    #SNMP Configuration
    remote_conn.send('set snmp location \"' + snmp_location + '\"' + '\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)
    remote_conn.send('set snmp contact \"' + snmp_contact + '\"' + '\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)
    remote_conn.send('set snmp community ' + snmp_community + ' authorization read-only\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)
    remote_conn.send('set snmp routing-instance-access\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)

    #Save configuration
    remote_conn.send("commit\n")
    time.sleep(4)
    output = remote_conn.recv(1000)
    print(output)

    #Exit configuration mode
    remote_conn.send('exit\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)

    #Exit operational mode
    remote_conn.send('exit\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)

 

Now save this as some juniper.py in your directory of choice then execute it using Python.  At first the user is prompted for information then our script connects to the device and executes our command.

python /home/justin/juniper.py

Input Username: root
Password:
Input SNMP v2 Community: A_COMMUNITY
Input SNMP Location: MY_HOUSE
Input Device Contact: JUSTIN OEDER
SSH connection established to qfx3
Interactive SSH session established
--- JUNOS 18.4R1.8 built 2018-12-17 03:30:15 UTC

[email protected]:RE:0%
[email protected]:RE:0% cli
{master:0}
root>
configure
Entering configuration mode
Users currently editing the configuration:
root terminal d0 (pid 1867) on since 2019-02-06 01:52:28 UTC
{master:0}[edit]

{master:0}[edit]
root#
set snmp location "MY_HOUSE"

{master:0}[edit]
root#
set snmp contact "JUSTIN OEDER"

{master:0}[edit]
root#
set snmp community A_COMMUNITY authorization read-only

{master:0}[edit]
root#
set snmp routing-instance-access

{master:0}[edit]
root#
commit
configuration check succeeds
commit complete

{master:0}[edit]
root#
exit
Exiting configuration mode

{master:0}
root>
exit

[email protected]:RE:0%

Now, to expand on this script and ACTUALLY save time with it our list of devices needs to get bigger! We can use hostnames or IP addresses.

devices = [
    'qfx1',
    '172.31.33.72',
    'qfx3',
    '172.31.33.74',
    ]

Now just keep adding devices to this list and run the script again!

Next came our real time sucker. Manually adding devices to the monitoring system. LibreNMS has a pretty well documented API

https://docs.librenms.org/API/Devices/#add_device

Here they tell you exactly how to add a device using a curl command.

curl -X POST -d '{"hostname":"localhost.localdomain","version":"v1","community":"public"}' -H 'X-Auth-Token: YOURAPITOKENHERE' https://librenms.org/api/v0/devices

We can test that this does what we expect by adding our varables to it and running it.

curl -X POST -d '{"hostname":"qfx1","version":"v2c","community":"MY_COMMUNITY"}' -H 'X-Auth-Token: LIBRENMS_API_TOKEN' https://librenms.chewonice.net/api/v0/devices

Now we can’t run a curl command from Python directly but the requests library can help us.

import requests

Then navigate to this little site https://curl.trillworks.com/

curl -X POST -d '{"hostname":"qfx1","version":"v2c","community":"MY_COMMUNITY"}' -H 'X-Auth-Token: LIBRENMS_API_TOKEN' https://librenms.chewonice.net/api/v0/devices

Gives us:

import requests

headers = {'X-Auth-Token': 'LIBRENMS_API_TOKEN',}
data = '{"hostname":"qfx1","version":"v2c","community":"MY_COMMUNITY"}'
response = requests.post('https://librenms.chewonice.net/api/v0/devices', headers=headers, data=data)

And once again we stitch it all together.

import paramiko
import time
import getpass
import requests

devices = [
    'qfx1',
    '172.31.33.72',
    'qfx3',
    '172.31.33.74',
    ]

username = raw_input('Input Username: ')
password = (getpass.getpass())
snmp_community = raw_input(str(('Input SNMP v2 Community: ')))
snmp_location = raw_input(str(('Input SNMP Location: ')))
snmp_contact = raw_input(str(('Input Device Contact: ')))

for switch in devices:
    #Connect to switch and build ssh connection
    remote_conn_pre = paramiko.SSHClient()
    remote_conn_pre.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    remote_conn_pre.connect(switch, username=username, password=password, look_for_keys=False, allow_agent=False)
    print('SSH connection established to ' + switch)
    remote_conn = remote_conn_pre.invoke_shell()
    print('Interactive SSH session established')

    #Print terminal to screen
    output = remote_conn.recv(1000)
    remote_conn.send('\n')
    time.sleep(2)
    print(output)

    #Username root requires getting into the cli
    if username == 'root':
        remote_conn.send('cli\n')
        time.sleep(3)
        output = remote_conn.recv(1000)
        print(output)
    else:
        pass

    #Enter configuration mode
    remote_conn.send('configure\n')
    time.sleep(2)
    output = remote_conn.recv(1000)
    print(output)

    #SNMP Configuration
    remote_conn.send('set snmp location \"' + snmp_location + '\"' + '\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)
    remote_conn.send('set snmp contact \"' + snmp_contact + '\"' + '\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)
    remote_conn.send('set snmp community ' + snmp_community + ' authorization read-only\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)
    remote_conn.send('set snmp routing-instance-access\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)

    #Save configuration
    remote_conn.send("commit\n")
    time.sleep(4)
    output = remote_conn.recv(1000)
    print(output)

    #Exit configuration mode
    remote_conn.send('exit\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)

    remote_conn.send('exit\n')
    time.sleep(1)
    output = remote_conn.recv(1000)
    print(output)

    #Add device to LibreNMS
    api_token = 'LIBRENMS_API_TOKEN'
    headers = {'X-Auth-Token': api_token,}
    data = '{"hostname":"' + switch + '","version":"v2c","community":"' + snmp_community + '"}'
    print(data)
    response = requests.post('http://172.31.33.66/api/v0/devices', headers=headers, data=data)
    print(response)

Now run the script, wait a minute for LibreNMS to discover the devices and enjoy the remainder of your day!  Out the door by 1:15.  Even better than expected.

This script can also be used to update the community on both the device and the monitoring software simultaneously!  Just rerun the script with another community.

If you look, there is a lot of repeated code.  I am hoping to clean this code up in later blog posts.  Juniper also has a native API which would provide us with the ability push configuration changes without having to “screen scrape” the SSH session.