Building a VPN Dashboard using Django and JunOS pyEZ (Part 4 – Polling the SRX)

This is a multi-part series – If you just hit this page, please check out the prior posts first!


Alright – We’re finally at the last step to this project. We already have Django running with a somewhat decent-looking web dashboard. Now the only thing that remains is polling our SRX firewalls for VPN connections, then populating this information into the database.

For this to happen, I built a custom management command for Django – the cool thing here is that we can easily use this within a cron job to automatically schedule updates at regular intervals. So within our application (the vpn folder), we’re going to create a management folder then a commands folder within that. In here you can name your script whatever you want, so I went with cronpoller.py.

The script that I wrote is extremely simplistic and could probably use quite a number of improvements – which I will slowly make over time. But for now, it is what it is – and it does exactly what I need it to.

First thing we need to do is import a few things we will need:

from django.core.management.base import BaseCommand, CommandError
from vpn.models import Firewall, Datacenter
from jnpr.junos import Device
from lxml import etree

We have some Django modules for treating this as a management command – and we’re also importing our existing models into this script. This part is really cool to me, because it means that this cron script can just reference the existing objects that we created to get their attributes. Next, we import the JunOS stuff from their pyEZ packages (don’t forget to install pyEZ!). The last one is going to be used to parse the responses from our SRX into something we can use.

Alright – so in order to build a management command, we have to create a class called Command, then define our functions within that. Within the Command class, Django will look for a function called handle to execute. In my final script, I have that function plus one called getVPNStatus. Let’s start with putting together getVPNStatus:

# This function will poll the device for status 
def getVPNStatus(self, fw, dc):
    connectedlist = []
    # So we generate a JunOS pyEZ device connection using the information about the 
    # firewall that we gather from the database object
    device = Device(fw.firewall_manageip,
                    user=fw.firewall_user,
                    password=fw.firewall_pass)
    # Try to open a connection out to the target device
    try:
        dev.open()
    except:
        # If for some reason this doesn't work, just return UNREACHABLE - which
        # we'll assume means the device is down
        return "UNREACHABLE"
 
    # Here is where we poll the SRX for a list of all IPSec Security Associations.
    # The equivalent of the 'show security ipsec sa' command
    response = etree.tostring(dev.rpc.get_security_associations_information())
    # The SRX returns a response in XML, which we'll need to dig through
    # Credit to the guys over at Packet Pushers for a great post explaining how to
    # parse these responses
    with open(response) as a:
        xmldoc = etree.parse(a)
        docroot = xmldoc.getroot()
        rootchildren = docroot.iter()
        for child in rootchildren:
            # For each IPSec SA returned, we need to find the remote gateway IP, which 
            # we use to tie the connection back to the connected datacenter
            if child.tag == "sa-remote-gateway":
                connectedlist.append(child.text)
   
    # Once we've built our list, send it back!
    return connectedlist

That already is major part of our script. It will connect out to each device, grab a list of every connected IPSec tunnel, then return back a list of connected gateways. Now we just need to write our handle function, which will do all of the remaining work.

def handle(self, *args, **options):
    # Let's quickly create two lists - based on our firewall and datacenter models
    # This actually polls the database for anything that's been created
    dclist = Datacenter.objects.order_by('datacenter_code')
    fwlist = Firewall.objects.order_by('firewall_name')
 
    # This will loop through each firewall, then call the getVPNStatus function
    for fw in Firewall.objects.order_by('firewall_name'):
        statuslist = self.getVPNStatus(fw, dclist)


    # Once we have our response, we're going to check the returned list of connected
    # gateways against our list of datacenters from the database
    for dc in dclist:
        for remoteFW in fwlist:
            # If we find another datacenter in the connected gateway list, add 
            # that datacenter to the vpnstatus list as "VPNUP", otherwise assume it's down
            if remoteFW.firewall_vpnip in statuslist:
                vpnstatus[dc.datacenter_code] = "VPNUP"
                break
            else:
                vpnstatus[dc.datacenter_code] = "VPNDOWN"
 
    # After all that is done - we just save the new vpnstatus list to the firewall_vpnstatus
    # field in the database
    fw.firewall_vpnstatus = vpnstatus
    fw.save(update_fields=["firewall_vpnstatus"])

I’ve said this before, but I think it’s worth noting again – This probably isn’t the best or most efficient/reliable way of doing this. I think that over time I would like to revisit this and refine it a bit – but this is what I’ve put together so far. In fact, I think it’s worth stating that this will probably break in a fantastically horrific manner. This isn’t a finished product, but pretty much just version 0.1 – the base functionality works, but it’s in desperate need of refinement.

Alright – so now we just have to add a cron job in our system to call this script and run it. Depending on how many firewalls you have and how the latency is, you might need a different interval than I am using – but I went ahead and set mine to run once every 10 minutes. I would like to get this down to 5 minutes or less, but I might need to figure out a way to potentially multi-thread the cronpoller.py script. So here is what I did in /etc/cron.d/vpnstatuspoller:

*/10  * * * * * python /root/junos-dashboard/manage.py cronpoller

After letting the script poll my firewalls – I ended up with a dashboard that looks like this:

dashboard-complete

Future improvements

Of course, I still have a few things I would like to add or improve on. I’ve been keeping a list of some ideas that I’ve had, which I’ll share here:

  • Add a timestamp to each firewall that contains the last poll time (in case something gets missed or hasn’t updated yet)
  • Possibly add ability to click a ‘down’ VPN cell to force clear SA
  • Set up email alerts from the cronpoller to automatically notify me of a failure
  • Ability to click on any VPN cell and get additional info – like VPN uptime, subnets routed across it, or maybe the IKE/IPSec negotiation parameters
  • Ability to click on any firewall name and get additional info – like uptime, JunOS version, CPU/Memory

I finally got around to getting myself a GitHub account, so I might put the final code up there once I’m done. I also have a number of other JunOS scripts that are probably worth posting up there as well.

Well, I hope you enjoyed reading about this project – because I certainly enjoyed working on it. This was one of my first real projects using the JunOS pyEZ libraries, and I got to pick up and learn how to use Django as well. The experience of building this has given me ideas for other JunOS automation projects – so look out for some of those in the future!