Building a VPN Dashboard using Django and JunOS pyEZ (Part 3 – Creating the Dashboard)

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


In the last post, we got Django working well enough to start serving up web requests. That’s a great start – So now it’s on to actually building the project. We’re going to cover this in two parts: the front-end dashboard, and the back-end scripts. In this post, I’m going to show you how I began putting everything together to assemble the front-end HTML dashboard.

As a quick refresher note, we created a project in the last post called junos-dashboard. Within that project, we created our app named vpn. This folder is primarily where we’ll be spending our time today. There are two files in our app folder that will need a few edits:

views.py – This is where we originally created our basic index page, which currently just spits out a message. So we’ll need to add code in here to render our dashboard instead.

models.py – In here we will define our objects and the characteristics about those objects that we want to store/retrieve in the sqlite database.

Let’s go ahead and open up our models.py file. For my dashboard, I have two objects I want to track – Firewalls (the SRX hosting the VPN) and Datacenter (as a location tracking). So in order to create those objects, we just have to add two classes into the models.py file:

from __future__ import unicode_literals
from django.db import models

class Datacenter(models.Model):
    def __str__(self):
        return self.datacenter_code

class Firewall(models.Model):
    def __str__(self):
        return self.firewall_name

Easy enough, right? So here is the really cool thing about Django (or probably any web framework really) – These model objects are now something that we can reference in our code to change or query attributes about them. Even cooler is that Django automatically builds an admin page to our site, where we can create new instances of each model right there (which I’ll cover later). So this means that all I have to do is define these two models and their attributes, then anyone on my team can log into the dashboard admin page and add new firewalls or datacenters.

Okay, so in order to actually make these models useful, we need to add our attributes. For the datacenter object, I only care about two things – What is the datacenter code (identifier), and is the datacenter an active location. So here is where we add those options under our datacenter class:

class Datacenter(models.Model):
    datacenter_code = models.CharField('Datacenter Code', max_length=10)
    datacenter_active = models.BooleanField('Datacenter Active?', default=True)

The firewall object gets a little more complicated because I want to be able to define quite a few things. The obvious attributes are: the firewall name, which datacenter it belongs to, and whether or not it is an active firewall. I’ll also need to collect this firewall’s VPN peer IP, so that I can compare it against other devices to check the connection. But wait – how am I actually checking that VPN status? Oh, I guess I’ll also need to collect a username/password for the SRX API, as well as a valid management IP to reach the API.

Let’s take a moment though – because I stopped here and realized that I’ll need to collect user/password data in a form, then it’s going to be stored in a sqlite database. Oh, and of course it will be unencrypted. I didn’t really like that idea – so I did some research on how to encrypt one of the object attributes. Turns out, it’s actually pretty easy since someone has already created a module to do exactly that.

So let’s grab that module real quick:

[[email protected] vpn]# pip install django-encrypted-fields

Once we have the module, it requires us to generate some keys which will be used for encrypting our data. The following is based on the steps listed on the GitHub page for django-encrypted-fields:

[[email protected] junos-dashboard]# mkdir fieldkeys
[[email protected] junos-dashboard]# keyczart create --location=fieldkeys --purpose=crypt
[[email protected] junos-dashboard]# keyczart addkey --location=fieldkeys --status=primary --size=256

Next, we just need to add this keyfile into our settings.py:

ENCRYPTED_FIELDS_KEYDIR = '/root/junos-dashboard/fieldkeys'

Alright – back to actually creating the attributes we need for our firewalls… Since we’re using this new module, we’ll need to add an additional import statement – otherwise, we are just adding the fields I covered earlier:

from encrypted_fields import EncryptedCharField

class Firewall(models.Model):
    firewall_name = models.CharField('Firewall Name', max_length=50)
    firewall_location = models.ForeignKey('Datacenter', on_delete=models.CASCADE)
    firewall_active = models.BooleanField('Firewall Active?', default=True)
    firewall_manageip = models.GenericIPAddressField('Management IP')
    firewall_vpnip = models.GenericIPAddressField('VPN Interface IP')
    firewall_user = models.CharField('API User', max_length=50, blank=True)
    firewall_pass = EncryptedCharField('API Pass', max_length=50, blank=True)
    firewall_vpnstatus = models.TextField(editable=False, blank=True)

A few things to note here – The firewall_location attribute will actually query the table of datacenter objects – so we’re not creating any data twice. You might also notice that I added a firewall_vpnstatus text field, which is going to be hidden in the administrative console. Because I’m a terrible programmer, I’ll be storing all of the VPN status info in this text field in the database. Yes, there is probably a much more elegant way of handling this – but again, I’m a network admin not a professional coder. For what I need, this gets the job done. If you have a better way of accomplishing this – let me know! I would be very interested in another method.

I also found out later that just because I encrypted the field doesn’t mean that Django knows the field is a password. I wanted the field to be treated like a password input, so the data wasn’t visible to anyone who just logged in. I found out that you can create a forms.py file within the vpn directory, and pretty much override the field type to account for this.

So here is what I threw into the forms.py file to handle password input:

from django.forms import ModelForm, PasswordInput
from .models import Firewall

class FirewallForm(ModelForm):
    firewall_pass = CharField(widget=forms.PasswordInput)
    class Meta:
        model = Firewall
        fields = ['firewall_pass']

That was actually way easier than I had anticipated….

Awesome, now our work on the models is completely done – why don’t we log into the administrative  web interface and take a look at how it all turned out?

Here is a view of the datacenter page – where we can add a new location:

And now for the firewall objects – with all the fields we need to get the job done:

Looks pretty good, huh? And we didn’t even need to do any work to get the free admin functionality! This is honestly my favorite part of Django. Without posting a ton of additional screenshots, the admin page also provides audit logs – so we can see who changed what objects. Very helpful.


Alright – now let’s start work on our views.py file, so we can actually start putting all this together in our dashboard!

Within our views.py file, we already had our index function, which just returned a string of text. We’ll be adding to that in a moment – but first I created a separate function called buildrows, which does exactly what you think. This function will pull each firewall and it’s status, and build our little HTML table. I’m not going to go into great detail on this – but check out the comments for more information on what’s going on here:

# This is our function to build the HTML table - We're going to be expecting this function
# to be fed a list of firewalls and datacenters
def buildrows(firewall_list, datacenter_list):
    firewallstatus = []
    # We will iterate through each firewall and compile each row for our table
    for fw in firewall_list:
        # The first cell in our row is going to be the firewall name and it's own VPN peer IP
        onerow = "<td><b>%s</b><br><i>%s</i></td>" % (fw.firewall_name, fw.firewall_vpnip)
        # Now we iterate through every datacenter, in order to see which ones we have a
        # connection to from this firewall
        for dc in datacenter_list:
            # First thing is first - If this firewall contains the name of the datacenter
            # then we'll print N/A, since we wouldn't expect a VPN to itself 
            if str(dc.datacenter_code) in str(fw.firewall_name):
                onerow += ("<td>N/A</td>")
                continue
            # Also, if there is no VPN status in the database, then just print 'No Status'
            if not fw.firewall_vpnstatus:
                onerow += ("<td>No Status</td>")
                continue
     
            # This portion is pretty self-explanatory - We parse the vpnstatus field in the
            # database. If the state of the connection is 'VPNUP', then we print a cell for
            # that datacenter with the text 'UP'. If the state is 'VPNDOWN', then we print
            # 'DOWN'. And if nothing matches, print 'No Status'
            statuslist = ast.literal_eval(fw.firewall_vpnstatus)
            try:
                status = statuslist[dc.datacenter_code]
                if status == "VPNUP":
                    onerow += ("<td class=\"vpnup\">")
                    onerow += ("UP")
                elif status == "VPNDOWN":
                    onerow += ("<td class=\"vpndown\">")
                    onerow += ("DOWN")
                else:
                    onerow += ("<td>")
                    onerow += ("No Status")
                    onerow += ("</td>")
            except KeyError:
                onerow += ("<td>")
                onerow += ("No Status")
                onerow += ("</td>")
        # Once complete, we append this row to the status list
        firewallstatus.append(onerow)
    
    # All done! Return our list of statuses!
    return firewallstatus

That block might not make a ton of sense at the moment – but it will shortly. Essentially I have a script that is connecting to each firewall via the SRX API and polling the VPN status for each peer. The script is then writing all of this information to a field in the database called vpnstatus in one long string, which kinda looks like this:

{u'US-1': 'VPNUP', u'US-2': 'VPNUP', u'EU-1': 'VPNUP', u'EU-2': 'VPNUP', u'CN-1': 'VPNDOWN'}

The buildrows function is then just grabbing each firewall and then parsing this data to figure out what it has connections to – then generates a row in our table.

With that done, our index function within views.py needs a little work to start displaying our table. You’ll also notice we’re importing the models we created, as well as a template loader – which I’ll get to in a moment. So here is what that function will look like when we’re done:

from .models import Firewall, Datacenter
from django.template import loader
import sys, ast

def index(request):
    firewall_list = []
    datacenter_list = []
    firewall_status = []
    # This will grab each datacenter object (if they're marked active) from the 
    # database and add them to a list
    for each in Datacenter.objects.order_by('datacenter_code'):
        if each.datacenter_active == True: datacenter_list.append(each)
    # Same thing here - grab our firewalls and append to a list
    for each in Firewall.objects.order_by('firewall_name'):
        if each.firewall_active == True: firewall_list.append(each)
    # Now we pass those lists to the buildrows function to assemble our HTML table
    firewall_status = buildrows(firewall_list, datacenter_list)
    # We're also going to apply a template to make the page look fancy
    template = loader.get_template('web/index.html')
    # This is taking all of our lists, and passing them into our HTML template
    # so that it can build the page semi-automatically
    context = {
        'datacenter_list':datacenter_list,
        'firewall_list': firewall_list,
        'firewall_status':firewall_status,
    }
    return HttpResponse(template.render(context,request))

Alright – our views.py file is now complete. I wanted to keep the index function kind of simple, so we just have it pull data from the database, pass it to another function for processing, then return it to the browser for display.

So in the code above, you might have noticed that I was using an HTML template to make the page look fancier. I did this by going out to Google and finding a free CSS template for HTML tables. I won’t post the CSS here, but there are plenty of free templates out there – just find whatever suits your taste. Once you have that CSS file, create a directory (within our app folder) called static and place the CSS file in there.

Then, it’s one simple template HTML file to load our CSS and render our table. Within the app directory, I created a templates folder, then a folder called web within that. I added a new file called index.html – which looks like this:

<title>SRX VPN Dashboard</title>
<div align="center">
    {% load static %}
    <!-- Load the CSS template here -->
    <link rel="stylesheet" type="text/css" href="{% static 'style.css' %}" />
    <!-- Add a header to the table -->
    <div class="table-title">
        <h3>SRX VPN Dashboard</h3>
    </div>

    <!-- Create our first row, which will be a list of each data center location -->
    {% if firewall_list %}
        <table class="container">
        <tr>
        <th></th>
        {% for datacenter in datacenter_list %}
            <th>{{ datacenter.datacenter_code }}</th>
        {% endfor %}
        </tr>

        <!-- Then we add a row for each firewall, followed by its status 
             for each datacenter -->
        {% for firewall in firewall_status %}
            <tr>
            {{firewall|safe}}
            </tr>
        {%endfor%}

        </table>
        <!-- If something goes wrong, we just print an error -->
    {% else %}
        <p>No firewalls were found.</p>
    {% endif %}
</div>

Guess what? The dashboard is complete! Now we can run our Django server using ‘./manage.py runserver’ and take a look at what we have created.

Pretty simplistic – but we’re getting very close to what I wanted to accomplish. The dashboard loads and generates the table, but it doesn’t have any data yet.

 

In the next post – We’ll take a look at building the backend script to poll all of our SRX firewalls for VPN connections. I hope you enjoyed reading this – let me know what you think in the comments below!


This is a multi-part series – Check out the other posts: