Agile
I am a big fan of Agile. This is basically how I live my whole life. When I was younger, my motto was just wing it. As I gained experience, I learned to trust established methods but the ability to improvise remains. Because of this, you can expect to see rapid iterations from pretty much any of my projects. Agile really is just a natural way of doing things, although I will concede that it doesn’t lend itself to pass/fail projects like building skyscrapers.
Security
I’ve played with various ways of securing my dashboard in the past. One method was to offload RBAC to vSphere. The assumption was this: If you’ve got certain privileges in vSPhere, which talks to Active Directory, you’ve probably got at least read-only privileges for things like VMs and blades. This is probably the better way, but for a small, fast tool, I also like standalone ability. You should be able to authenticate locally or via LDAP. So let’s look at how I do this:
import uuid
sessions = list()
valid_tokens = ['<uuid v4 token>', '<another token>']
def check_session(cookie, ip):
for session in sessions:
if session['uuid'] == cookie and session['ip'] == ip:
return True
return False
@app.route('/login', methods=['get', 'post'])
def login():
valid_session = check_session(flask.request.cookies.get('uuid'), flask.request.remote_addr)
if valid_session:
return flask.redirect('%s/home' % base_url, code=302)
if flask.request.method == 'GET':
html = base_html
html += '<h1>Infrastructure Dashboard</h1><form action="%s/login" method="post"><input type="text" placeholder="Token" name="token"><button type="submit">Login</button></form>' % base_url
return html
elif flask.request.method == 'POST':
token = flask.request.form['token']
if token in valid_tokens:
cookie = str(uuid.uuid4())
session = {'uuid': cookie, 'ip': str(flask.request.remote_addr)}
sessions.append(session)
response = flask.make_response(flask.redirect(base_url))
response.set_cookie('uuid', cookie)
return response
else:
html = base_html
html += '<h1>Infrastructure Dashboard</h1><p>Token not accepted</p><form action="%s/login" method="post"><input type="text" placeholder="Token" name="token"><button type="submit">Login</button></form>' % base_url
return html
@app.route('/<endpoint>', methods=['get'])
def generic_endpoint(endpoint):
valid_session = check_session(flask.request.cookies.get('uuid'), flask.request.remote_addr)
if not valid_session:
return flask.redirect('%s/login' % base_url, code=302)
# otherwise, keep going
Here, I maintain a list of accepted tokens as well as validated sessions. The valid sessions list contains a list of dictionaries containing a UUID and IP address of a host. Within the realm of security, authentication, and identity management there is this concept of three factor authentication:
- Something you know
- Something you have
- Something you are
The most common expression of this today is when you get a security code texted or emailed to you after already providing a username and password. I attempt to do some of the same things here, albeit with far less sophistication. In order to authenticate, you simply need to give me a UUID token. By virtue of length and complexity, a UUID password is pretty darn strong. It’s 128 bits and is nothing but pure entropy. So if you know the secret UUID to gain access, that’s a pretty strong indicator that you belong.
Flask also gives us the ability to set cookies and detect the client address. So for subsequent authentications, I simply check if you have a cookie called uuid
that contains a uniquely generated UUID just for you.
Your unique UUID is paired with your IP address. My server retains the list of valid sessions internally.
- You know the secret UUID
- You have your personal UUID token
- You are using a computer with a specific IP address
The IP address would be possible to spoof, but the chances of guessing the UUID paired to that IP address are vanishingly small. I’m sure there are ways to defeat this, but it’s a helluva lot stronger than admin
and password
!
Dynamic Content
Okay don’t get too excited. This is not interactive content, but rather just cleaner code. The rule of thumb is that if you write something more than once, create a function for it. Do not duplicate code!
import flask
import json
import os
from datetime import datetime
import uuid
base_html = """
<html>
<head>
<title>Dashboard</title>
<style>
* {font-family: Calibri;}
a {color: #3366CC;}
table {border-width: 1px; border-style: solid; border-color: lightgray; border-collapse: collapse; white-space: nowrap;}
th {border-width: 1px; padding: 4px; border-style: solid; border-color: lightgray; background-color: lightskyblue; white-space: nowrap;}
td {border-width: 1px; padding: 4px; border-style: solid; border-color: lightgray; white-space: nowrap;}
.flex-container {display: flex; flex-wrap: wrap; background-color: LightGray;}
.flex-container > div {margin: 10px; background-color: White; padding: 10px}
tr:nth-child(even) {background-color: #E7F5FE;}
</style>
</head>
<body>
"""
# Yes, I know, I should load the HTML from a file, I'll get around to that... eventually!
base_url = 'https://<yourserver>'
root_dir = '<your root dir>'
endpoints = {
'home': 'Home',
'vmware': 'VMware',
'ucs': 'UCS',
}
def generate_nav_bar(nav): # nav is the same as endpoints here
html = ''
for key in nav.keys():
html += '<a href="%s/%s">%s</a> — ' % (base_url, key, nav[key])
return html
def file_timestamp_str(filepath):
modified = os.path.getmtime(filepath)
time = str(datetime.fromtimestamp(modified))
return time
@app.route('/<endpoint>', methods=['get'])
def generic_endpoint(endpoint):
# ...authentication bits... (see above)
if endpoint == 'favicon.ico':
return flask.send_from_directory(root_dir, 'favicon.ico', mimetype='image/vnd.microsoft.icon')
if endpoint not in endpoints.keys():
return 'Endpoint %s not available' % endpoint, 404
html = base_html
html += generate_nav_bar(endpoints)
try:
html += '\n<h1>%s</h1>\n<p>Last Updated: %s</p>\n<div class="flex-container">' % (endpoints[endpoint], file_timestamp_str('%s/%s.json' % (root_dir, endpoint)))
html += load_json_content('c:/fast_links/%s.json' % endpoint)
except Exception as oops:
return 'Error loading JSON data: ' + str(oops), 500
html += '\n</div>\n</body>\n</html>'
html = html.replace('<title>Dashboard</title>','<title>Dashboard - %s</title>' % endpoints[endpoint])
return html
Here, I dynamically generate the navbar. This allows me to consolidate on a single function for most content. Now I have a /
which just redirects,
a /login
which does exactly what it says, and a /<endpoint>
which automatically populates whatever the content you ask for. I thought of another way to automate this further,
which was to just enumerate all JSON files in a data_dir
, but I haven’t gotten around to that yet. In that case, I would probably set the filename as the page header/title (minus the .json),
and then lower-case and remove whitespace for the URL slug.
Another thing I do here is inject the file modified timestamp. I realized that looking at page after page of HTML tables with no other context was a bit difficult. I have separate emails notifying me of when my data is updated, but the web page has nothing.
Ideas for endpoints
Here are some types of content I’m exploring for my personal instance:
- Hosts and blades
- Chassis and interconnects
- Serial numbers
- Alarms, faults, and errors
- Storage and network throughput
- Snapshots
- Cluster capacity
- Storage paths
The Whole Thing
import flask
import json
import os
from datetime import datetime
import uuid
base_html = """
<html>
<head>
<title>Dashboard</title>
<style>
* {font-family: Calibri;}
a {color: #3366CC;}
table {border-width: 1px; border-style: solid; border-color: lightgray; border-collapse: collapse; white-space: nowrap;}
th {border-width: 1px; padding: 4px; border-style: solid; border-color: lightgray; background-color: lightskyblue; white-space: nowrap;}
td {border-width: 1px; padding: 4px; border-style: solid; border-color: lightgray; white-space: nowrap;}
.flex-container {display: flex; flex-wrap: wrap; background-color: LightGray;}
.flex-container > div {margin: 10px; background-color: White; padding: 10px}
tr:nth-child(even) {background-color: #E7F5FE;}
</style>
</head>
<body>
"""
endpoints = {
'home': 'Home',
'slug': 'Header',
'slug': 'Header',
'slug': 'Header',
}
base_url = '<your server>'
root_dir = '<your root dir>'
sessions = list()
valid_tokens = ['<your UUID tokens>']
def json_to_html_table(data):
# data must be list of dicts, where all dicts have same keys
result = '<table><tr>'
keys = data[0].keys()
for k in keys:
result += '<th>%s</th>' % k
for row in data:
result += '<tr>'
for k in keys:
try:
if 'http' in row[k]:
short_name = row[k].replace('https://','').replace('http://', '').split('.')[0]
result += '<td><a href="%s" target="_blank">%s</a></td>' % (row[k], short_name)
else:
result += '<td>%s</td>' % row[k]
except:
result += '<td>%s</td>' % row[k]
result += '</tr>'
result += '</table>'
return result
def file_timestamp_str(filepath):
modified = os.path.getmtime(filepath)
time = str(datetime.fromtimestamp(modified))
return time
def load_json_content(filepath):
html = ''
with open(filepath, 'r') as infile:
content = json.load(infile)
for section in content:
html += '\n<div>\n<h2>%s</h2>\n' % section['Header']
html += json_to_html_table(section['Data'])
html += '\n</div>'
return html
def generate_nav_bar(nav):
html = ''
for key in nav.keys():
html += '<a href="%s/%s">%s</a> — ' % (base_url, key, nav[key])
return html
def check_session(cookie, ip):
for session in sessions:
if session['uuid'] == cookie and session['ip'] == ip:
return True
return False
app = flask.Flask('InfrastructureDashboard')
@app.route('/', methods=['get'])
def home():
return flask.redirect('%s/home' % base_url, code=302)
@app.route('/login', methods=['get', 'post'])
def login():
valid_session = check_session(flask.request.cookies.get('uuid'), flask.request.remote_addr)
if valid_session:
return flask.redirect('%s/home' % base_url, code=302)
if flask.request.method == 'GET':
html = base_html
html += '<h1>Infrastructure Dashboard</h1><form action="%s/login" method="post"><input type="text" placeholder="Token" name="token"><button type="submit">Login</button></form>' % base_url
return html
elif flask.request.method == 'POST':
token = flask.request.form['token']
if token in valid_tokens:
cookie = str(uuid.uuid4())
session = {'uuid': cookie, 'ip': str(flask.request.remote_addr)}
sessions.append(session)
response = flask.make_response(flask.redirect(base_url))
response.set_cookie('uuid', cookie)
return response
else:
html = base_html
html += '<h1>Infrastructure Dashboard</h1><p>Token not accepted</p><form action="%s/login" method="post"><input type="text" placeholder="Token" name="token"><button type="submit">Login</button></form>' % base_url
return html
@app.route('/<endpoint>', methods=['get'])
def generic_endpoint(endpoint):
valid_session = check_session(flask.request.cookies.get('uuid'), flask.request.remote_addr)
if not valid_session:
return flask.redirect('%s/login' % base_url, code=302)
if endpoint == 'favicon.ico':
return flask.send_from_directory('%s/' % root_dir, 'favicon.ico', mimetype='image/vnd.microsoft.icon')
if endpoint not in endpoints.keys():
return 'Endpoint %s not available' % endpoint, 404
html = base_html
html += generate_nav_bar(endpoints)
try:
html += '\n<h1>%s</h1>\n<p>Last Updated: %s</p>\n<div class="flex-container">' % (endpoints[endpoint], file_timestamp_str('%s/%s.json' % (root_dir, endpoint)))
html += load_json_content('%s/%s.json' % (root_dir, endpoint))
except Exception as oops:
return 'Error loading JSON data: ' + str(oops), 500
html += '\n</div>\n</body>\n</html>'
html = html.replace('<title>Dashboard</title>','<title>Dashboard - %s</title>' % endpoints[endpoint])
return html
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8443, ssl_context='adhoc')
JSON Content
Here’s an example of the JSON content. The primary thing is that it’s a list of dicts, where the dicts are each section with Header
and Data
. Header
is just a string, the title of the section.
Data
is another list of dicts with the actual table data.
[
{"Data": [
{},
{},
{},
{}
],
"Header": "Section 1 Title"},
{"Data": [
{},
{},
{},
{}
],
"Header": "Section 2 Title"}
]