Automating the CLI (Part 2): Building a Web Dashboard with Flask & Bootstrap

In the last post, I covered a simple project that I'm working on while studying for the Cisco DevNet certifications. This is part two of a series, which will continue on while I work through this project.

As a brief summary - I started using a combination of Scrapli & Cisco Genie for a short network automation script. This script connects to a list of network switches & outputs a spreadsheet of interface statistics. This provides a quick snapshot view into the current port capacity within your network.

In this post, I'll be showing off a bit of how to build a simple web frontend - which I'll put together using Flask and Bootstrap.

Getting Started with Flask

Okay so I have a bad habit of making a lot of Python scripts which are essentially just command-line utilities.

In theory, this isn't necessarily problem. But what if I want to share some of the tools I build? Maybe not everyone wants to compile config files & figure out what command-line arguments are needed. For some, they may prefer to have an easy-to-use web interface instead.

So for this project I opted to venture a little outside my typical comfort zone & build a web dashboard. I'll admit I've done this once or twice before, but certainly not something I would claim proficiency in!

The good thing is - building a web interface doesn't have to be complex. My intent with this post is to try & break it down a bit - and hopefully encourage you to try it yourself!

Installing Flask & Hello World

Installing Flask is much like any other python module - we'll use pip:

pip install flask

Once installed, we can import the module into our python script:

from flask import Flask

Simple enough!

Now let's look at how easy it is to set up the web server & return a simple web page:

app = Flask(__name__)

@app.route('/')
def main():
    return "Hello there!"

if __name__ == '__main__':
    app.run()

First we create a new instance of Flask, using the syntax app = Flask(name).

Next, we just create a quick python function to return the text Hello there! You'll notice that there is a decorator above the function - this is used by Flask to route incoming HTTP requests.

In this example, we use @app.route('/'). This tells Flask to register this python function for any HTTP calls to http://<url>/. If we wanted a function for any HTTP requests to http://<url>/networking, we would modify the decorator to @app.route('/networking').

Lastly, we need to start the Flask web server when the script is run. This is accomplished using app.run()

Let's go ahead and try to run the script - which should start the Flask server:

flask-runoutput

In the above screenshot, you'll see that by default Flask will start on http://127.0.0.1:5000. You may also notice that Flask will print out real-time logging of incoming HTTP requests.

If we visit our page, we'll see pretty much what we might expect:

Hello-world

Before we move onto the next section, I did want to show one more example.

Let's say that we have a python variable (or dictionary, etc). Could we pass that as part of our web page function?

Let's look at this modified example:

web_page_text = {"Hello": "There!"}

@app.route('/')
def main():
    return web_page_text

If we reload our local web page, we'll now see the following:

hello-world-2

Keep this in mind - We'll use this later!

Making life easier with HTML templates

Now then - you could build all of your HTML in python, then return the entire HTML page as a response to the function call. That being said, just because you can doesn't mean you should 🙃

Let's take a quick look at how we can use HTML templates to help build our web page.

First we'll create a new directory called templates, and create an HTML file. In this case, I named my template main.html:

dir

To start with, we'll just add some simple text into the HTML file:

<body>
    Hello There - From the template!
</body>

On the Flask side, we'll have to modify our imports to include the render_template module. We'll also update what we're returning when an HTTP request is received:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def main():
    return render_template('main.html')

if __name__ == '__main__':
    app.run()

So now instead of returning plain text, we'll return render_template('main.html').

Flask uses Jinja2 for templating, which we'll get into shortly. The above function call tells our app to use main.html as a base template for the content that is returned to the end user.

Let's check out the page:

hello-world-3

Awesome - we receive our HTML file as a response this time.

Extending functionality with Jinja2

Now we can start getting into the fun stuff.

Manually writing out a bunch of HTML is fun and all - but what if we could auto-generate everything programmatically?

Jinja2 allows for exactly that. We'll be able to insert variables, if/then statements, and looping logic directly into our HTML template.

As an example, let's say we had the following python dictionary - which we wanted to display on a web page:

switchList = [
        {
        "name": "C9200",
        "serial": "ABCD00010",
        "ip": "192.168.1.1",
        "check": True,
        "total": 28,
        "up": 10,
        "down": 5,
        "disabled": 13,
        "capacity": 35       
    },
    {
        "name": "C9300",
        "serial": "ABCD00011",
        "ip": "172.168.44.2",
        "check": False,
        "total": 48,
        "up": 32,
        "down": 6,
        "disabled": 10,  
        "capacity": 67       
    },
        {
        "name": "C9400",
        "serial": "ABCD00012",
        "ip": "172.33.44.2",
        "check": True,
        "total": 48,
        "up": 40,
        "down": 6,
        "disabled": 2,  
        "capacity": 82       
    }
]

This dictionary is an example from the scrapli script I've been working on (more detail in my last post).

One way for us to present this using an HTML template would be the following:

<body>
    {% block content %}
    
    {% for switch in switches %}
    Switch Name: {{ switch.name }} <br>
      - Serial: {{ switch.serial }} <br>
      - Reachable at: {{ switch.ip }} <br>
      <br>
    {% endfor %}
    
    {% endblock %}
</body>

As you can see - that's a bit of a change from what we were using in our template before.

The Jinja2 syntax above uses a combination of curly braces & percentage signs to indicate which parts of our template are logic that should be processed before returning content to the user. Variables are represented with double curly braces.

Once our template receives the switchList dictionary, we can iterate through it similar to how we might in python. Using the {% for switch in switches %} syntax, we can iterate through each item - and pull out relevant data.

Within our for loop, we can reference the individual values of each item in the dictionary. Using the double braces, we can insert a placeholder for where a piece of content should be inserted. For example, in the above template we have a spot where we want to insert the name of the switch. We use {{ switch.name }} as a placeholder, where the value from the dictionary will be inserted once our template is processed.

Okay - so before we test this, we have one minor modification to our python function:

@app.route('/')
def main():
    return render_template('main.html', switches=switchList)

We've changed our function render_template, but also include the python object that we'll be passing to our template.

Let's test this out:

switchdict

Now there's some magic! Using a combination of python & our Jinja2 templates - we can drastically reduce the amount of HTML code that we need to write.

Okay, but that's not pretty...

Yes I know. But we needed to get the basics done first!

Here's where Bootstrap comes in.

I went out to Google and searched for good CSS templates. I'm not a web developer, nor am I good at design - so I'll leave that work to someone else.

In my searching, I found quite a bunch of free templates... but I fell in love with one called Lux by Bootswatch.

They provide a great sample page for the template, which shows off different variations. But the best part is that it also includes code samples!

After we pick & download our CSS template, we'll place it in a new directory named static:

dir2

To get started, we'll need to install one additional python module:

pip install flask_bootstrap

Then we'll also need a quick modification to our base Flask app:

from flask_bootstrap import Bootstrap

... code omitted ...

if __name__ == '__main__':
    Bootstrap(app)
    app.run()

I removed some code to focus on just the two parts that change.

First, we need to import our new module. Second, we need to inject our bootstrap plugin into the Flask app. We do this with Bootstrap(app) prior to app.run().

Let's move back over to our HTML template, since this is where the most of our changes will be.

First, we'll need to include a few things to make sure our CSS file is loaded:

{%- extends "bootstrap/base.html" %}

<head>
    {% block styles %}
    {{super()}}
    <link rel="stylesheet"
            href="{{url_for('static', filename='bootstrap.css')}}">
    {% endblock %}

</head>

I won't dive in too deep on this piece, as it's fairly straightforward. First, we'll need to include the base/default bootstrap template. Next, we'll include a reference to where our CSS file is located & it's name - so that it gets loaded when our page is rendered.

Okay, now for the body of the page:

<body>
    {% block content %}
    <table class="table table-hover">
        <thead>
          <tr>
            <th scope="col">Name</th>
            <th scope="col">Current Capacity</th>
          </tr>
        </thead>
        <tbody>
          {% for switch in switches %}
            {% if switch.capacity > 75 %}
                <tr class="table-danger">
            {% else %}
                <tr class="table-success">
            {% endif %}
            <th scope="row">{{ switch.name }}</th>
            <td>{{ switch.capacity }}%</td>
          </tr>
          {% endfor %}
        </tbody>
      </table> 
    {% endblock %}
</body>

Now instead of just returning a plain-text list of all three switches, we'll use our CSS template to generate a colorful table. For the purpose of this example, I kept the data to just showing the switch name & it's current capacity.

Most of the template is fairly simple. First we build our table header using the <thead> tags. Next we'll build the rows of our table using <tbody> and <tr>.

For the Jinja2 logic - We'll iterate through our list of switches, and add a new table row (<tr>) for each device. I also added logic to check the current capacity of the switch. If the switch capacity is greater than 75%, we create the table row using <tr class="table-danger"> - which is a reference in our CSS stylesheet that will color the row red. If capacity is less than 75%, we use table-success for a green color.

Let's go ahead and check out what this looks like:

table

And as we expect - we get our nicely formatted table & the correct colors on each row.

Bringing it all together

Now that we've gotten some background on how to use Flask & Bootstrap, let me show you a bit of what I've been working on.

I won't cover much code in this section, but you can check out the GitHub repo if you're interested in seeing the details.

The scrapli script that I wrote previously has been extended a little. The biggest change is that instead of writing its output to a spreadsheet, it will insert the data into a sqlite database.

The web frontend has been assembled using the same basic ideas covered in this blog (plus a lot of banging my head against the desk, trying to figure out why things don't format or align properly 🙂). Whenever a request is made to the web dashboard, it will query the sqlite database for the requested information.

So, without further delay - here it is!

dashboard-01

I currently have one physical device running, plus a handful of virtual nexus 9k & CSRv devices running in Cisco Modeling Labs. So all of the data shown in this dashboard is being pulled from real(ish) equipment - no mock data.

This was one of those projects where I started off thinking my dashboard was going to be really simple... but then I kept having new ideas and getting carried away with the design & functionality.

The table above shows each switch, it's serial number, management IP, current software version, and the port inventory. Similar to the example earlier in this post, I have a small progress bar that shows switch capacity - and will change color depending on percentage of in-use ports.

If you click on any of the switch names, you'll be taken to a page with additional detail on that particular device:

dashboard-02

On this first tab, you'll see some quick info - mostly the same that is on the main page of the dashboard. However, I added two additional tabs here - one for a detailed port breakdown & one for a dump of the raw CLI output.

If we click on the port info:

dashboard-03

We have a breakdown of how many ports are currently in use vs down, as well as our breakdown of port media & operational speed. Since I account for switches that are anywhere from 10M up to 100G, the data for operational speeds will only show the speeds that are actually in-use on the switch.

Just in case it's needed, I added a tab to show the raw CLI output from the switch:

dashboard-04

This could be a quick way to check port counters & errors, or any other information that isn't already presented via the dashboard.

Lastly, I built a separate page to provide statistics on ALL devices in the network:

dashboard-05

This page will show port count & availability network-wide. However, I also added some spaces to show the top 5 hardware models & software versions that are in-use across the network.


I went into this project a bit worried because frontend development isn't my strength. That being said, I really enjoyed working on this project. It was a different challenge than I'm normally used to - and it gave me a creative way to try and format & display the data I am collecting.

I hope this post was useful to you. Please leave a comment below - and check out my Github repo for the full code.