puppetboard: Commit the current code.
This is all the code after a week and a bit hacking on this project. It's in a rather experimental state but should work with a little effort.
This commit is contained in:
8
dev.py
Normal file
8
dev.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
from puppetboard.app import app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.debug=True
|
||||
app.run('127.0.0.1')
|
||||
0
puppetboard/__init__.py
Normal file
0
puppetboard/__init__.py
Normal file
198
puppetboard/app.py
Normal file
198
puppetboard/app.py
Normal file
@@ -0,0 +1,198 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import logging
|
||||
import collections
|
||||
|
||||
from flask import (
|
||||
Flask, render_template, abort, url_for,
|
||||
Response, stream_with_context,
|
||||
)
|
||||
|
||||
from pypuppetdb import connect
|
||||
from pypuppetdb.errors import ExperimentalDisabledError
|
||||
|
||||
from puppetboard.forms import QueryForm
|
||||
from puppetboard.utils import (
|
||||
get_or_abort, yield_or_stop,
|
||||
ten_reports,
|
||||
)
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object('puppetboard.default_settings')
|
||||
app.config.from_envvar('PUPPETBOARD_SETTINGS', silent=True)
|
||||
app.secret_key = os.urandom(24)
|
||||
|
||||
puppetdb = connect(
|
||||
host=app.config['PUPPETDB_HOST'],
|
||||
port=app.config['PUPPETDB_PORT'],
|
||||
ssl=app.config['PUPPETDB_SSL'],
|
||||
ssl_key=app.config['PUPPETDB_KEY'],
|
||||
ssl_cert=app.config['PUPPETDB_CERT'],
|
||||
timeout=app.config['PUPPETDB_TIMEOUT'],
|
||||
experimental=app.config['PUPPETDB_EXPERIMENTAL'])
|
||||
|
||||
numeric_level = getattr(logging, app.config['LOGLEVEL'].upper(), None)
|
||||
if not isinstance(numeric_level, int):
|
||||
raise ValueError('Invalid log level: %s' % loglevel)
|
||||
logging.basicConfig(level=numeric_level)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def stream_template(template_name, **context):
|
||||
app.update_template_context(context)
|
||||
t = app.jinja_env.get_template(template_name)
|
||||
rv = t.stream(context)
|
||||
rv.enable_buffering(5)
|
||||
return rv
|
||||
|
||||
@app.errorhandler(400)
|
||||
def bad_request(e):
|
||||
return render_template('400.html'), 400
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
@app.errorhandler(412)
|
||||
def precond_failed(e):
|
||||
"""We're slightly abusing 412 to handle ExperimentalDisabled errors."""
|
||||
return render_template('412.html'), 412
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(e):
|
||||
return render_template('500.html'), 500
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""This view generates the index page and displays a set of metrics fetched
|
||||
from PuppetDB."""
|
||||
# TODO: Would be great if we could parallelize this somehow, doing these
|
||||
# requests in sequence is rather pointless.
|
||||
num_nodes = get_or_abort(puppetdb.metric,
|
||||
'com.puppetlabs.puppetdb.query.population:type=default,name=num-nodes')
|
||||
num_resources = get_or_abort(puppetdb.metric,
|
||||
'com.puppetlabs.puppetdb.query.population:type=default,name=num-resources')
|
||||
avg_resources_node = get_or_abort(puppetdb.metric,
|
||||
'com.puppetlabs.puppetdb.query.population:type=default,name=avg-resources-per-node')
|
||||
mean_failed_commands = get_or_abort(puppetdb.metric,
|
||||
'com.puppetlabs.puppetdb.command:type=global,name=fatal')
|
||||
mean_command_time = get_or_abort(puppetdb.metric,
|
||||
'com.puppetlabs.puppetdb.command:type=global,name=processing-time')
|
||||
metrics = {
|
||||
'num_nodes': num_nodes['Value'],
|
||||
'num_resources': num_resources['Value'],
|
||||
'avg_resources_node': "{:10.6f}".format(avg_resources_node['Value']),
|
||||
'mean_failed_commands': mean_failed_commands['MeanRate'],
|
||||
'mean_command_time': "{:10.6f}".format(mean_command_time['MeanRate']),
|
||||
}
|
||||
return render_template('index.html', metrics=metrics)
|
||||
|
||||
@app.route('/nodes')
|
||||
def nodes():
|
||||
"""Fetch all (active) nodes from PuppetDB and stream a table displaying
|
||||
those nodes.
|
||||
|
||||
Downside of the streaming aproach is that since we've already sent our
|
||||
headers we can't abort the request if we detect an error. Because of this
|
||||
we'll end up with an empty table instead because of how yield_or_stop
|
||||
works. Once pagination is in place we can change this but we'll need to
|
||||
provide a search feature instead.
|
||||
"""
|
||||
return Response(stream_with_context(stream_template('nodes.html',
|
||||
nodes=yield_or_stop(puppetdb.nodes()))))
|
||||
|
||||
@app.route('/node/<node_name>')
|
||||
def node(node_name):
|
||||
"""Display a dashboard for a node showing as much data as we have on that
|
||||
node. This includes facts and reports but not Resources as that is too
|
||||
heavy to do within a single request.
|
||||
"""
|
||||
node = get_or_abort(puppetdb.node, node_name)
|
||||
facts = node.facts()
|
||||
if app.config['PUPPETDB_EXPERIMENTAL']:
|
||||
reports = ten_reports(node.reports())
|
||||
else:
|
||||
reports = iter([])
|
||||
return render_template('node.html', node=node, facts=yield_or_stop(facts),
|
||||
reports=yield_or_stop(reports))
|
||||
|
||||
@app.route('/reports')
|
||||
def reports():
|
||||
"""Doesn't do much yet but is meant to show something like the reports of
|
||||
the last half our, something like that."""
|
||||
if app.config['PUPPETDB_EXPERIMENTAL']:
|
||||
return render_template('reports.html')
|
||||
else:
|
||||
log.warn('Access to experimental endpoint not allowed.')
|
||||
abort(412)
|
||||
|
||||
@app.route('/reports/<node>')
|
||||
def reports_node(node):
|
||||
"""Fetches all reports for a node and processes them eventually rendering
|
||||
a table displaying those reports."""
|
||||
if app.config['PUPPETDB_EXPERIMENTAL']:
|
||||
reports = ten_reports(yield_or_stop(
|
||||
puppetdb.reports('["=", "certname", "{0}"]'.format(node))))
|
||||
else:
|
||||
log.warn('Access to experimental endpoint not allowed.')
|
||||
abort(412)
|
||||
return render_template('reports_node.html', reports=reports,
|
||||
nodename=node)
|
||||
|
||||
@app.route('/report/<node>/<report_id>')
|
||||
def report(node, report_id):
|
||||
"""Displays a single report including all the events associated with that
|
||||
report and their status."""
|
||||
if app.config['PUPPETDB_EXPERIMENTAL']:
|
||||
reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node))
|
||||
else:
|
||||
log.warn('Access to experimental endpoint not allowed.')
|
||||
abort(412)
|
||||
|
||||
for report in reports:
|
||||
if report.hash_ == report_id:
|
||||
events = puppetdb.events('["=", "report", "{0}"]'.format(
|
||||
report.hash_))
|
||||
return render_template('report.html', report=report,
|
||||
events=yield_or_stop(events))
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
@app.route('/facts')
|
||||
def facts():
|
||||
"""Displays an alphabetical list of all facts currently known to
|
||||
PuppetDB."""
|
||||
facts_dict = collections.defaultdict(list)
|
||||
facts = get_or_abort(puppetdb.fact_names)
|
||||
for fact in facts:
|
||||
letter = fact[0].upper()
|
||||
letter_list = facts_dict[letter]
|
||||
letter_list.append(fact)
|
||||
facts_dict[letter] = letter_list
|
||||
|
||||
sorted_facts_dict = sorted(facts_dict.items())
|
||||
return render_template('facts.html', facts_dict=sorted_facts_dict)
|
||||
|
||||
@app.route('/fact/<fact>')
|
||||
def fact(fact):
|
||||
"""Fetches the specific fact from PuppetDB and displays its value per
|
||||
node for which this fact is known."""
|
||||
return Response(stream_with_context(stream_template('fact.html',
|
||||
name=fact,
|
||||
facts=yield_or_stop(puppetdb.facts(name=fact)))))
|
||||
|
||||
@app.route('/query', methods=('GET', 'POST'))
|
||||
def query():
|
||||
"""Allows to execute raw, user created querries against PuppetDB. This is
|
||||
currently highly experimental and explodes in interesting ways since none
|
||||
of the possible exceptions are being handled just yet. This will return
|
||||
the JSON of the response or a message telling you what whent wrong /
|
||||
why nothing was returned."""
|
||||
form = QueryForm()
|
||||
if form.validate_on_submit():
|
||||
result = get_or_abort(puppetdb._query, form.endpoints.data,
|
||||
query='[{0}]'.format(form.query.data))
|
||||
return render_template('query.html', form=form, result=result)
|
||||
return render_template('query.html', form=form)
|
||||
8
puppetboard/default_settings.py
Normal file
8
puppetboard/default_settings.py
Normal file
@@ -0,0 +1,8 @@
|
||||
PUPPETDB_HOST='localhost'
|
||||
PUPPETDB_PORT=8080
|
||||
PUPPETDB_SSL=False
|
||||
PUPPETDB_KEY=None
|
||||
PUPPETDB_CERT=None
|
||||
PUPPETDB_TIMEOUT=20
|
||||
PUPPETDB_EXPERIMENTAL=False
|
||||
LOGLEVEL='info'
|
||||
20
puppetboard/forms.py
Normal file
20
puppetboard/forms.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
from flask.ext.wtf import Form
|
||||
from wtforms import RadioField, TextAreaField, validators
|
||||
|
||||
class QueryForm(Form):
|
||||
"""The form used to allow freeform queries to be executed against
|
||||
PuppetDB."""
|
||||
query = TextAreaField('Query', [validators.Required(
|
||||
message='A query is required.')])
|
||||
endpoints = RadioField('API endpoint', choices = [
|
||||
('nodes', 'Nodes'),
|
||||
('resources', 'Resources'),
|
||||
('facts', 'Facts'),
|
||||
('fact-names', 'Fact Names'),
|
||||
('reports', 'Reports'),
|
||||
('events', 'Events'),
|
||||
])
|
||||
|
||||
17
puppetboard/static/coffeescript/lists.coffee
Normal file
17
puppetboard/static/coffeescript/lists.coffee
Normal file
@@ -0,0 +1,17 @@
|
||||
$ = jQuery
|
||||
$ ->
|
||||
$('input.filter-list').parent('div').removeClass('hide')
|
||||
$("input.filter-list").on "keyup", (e) ->
|
||||
rex = new RegExp($(this).val(), "i")
|
||||
|
||||
$(".searchable li").hide()
|
||||
$(".searchable li").filter( ->
|
||||
rex.test $(this).text()
|
||||
).show()
|
||||
|
||||
if e.keyCode is 27
|
||||
$(e.currentTarget).val ""
|
||||
ev = $.Event("keyup")
|
||||
ev.keyCode = 13
|
||||
$(e.currentTarget).trigger(ev)
|
||||
e.currentTarget.blur()
|
||||
28
puppetboard/static/coffeescript/tables.coffee
Normal file
28
puppetboard/static/coffeescript/tables.coffee
Normal file
@@ -0,0 +1,28 @@
|
||||
$ = jQuery
|
||||
$ ->
|
||||
$('.nodes').tablesorter(
|
||||
headers:
|
||||
3:
|
||||
sorter: false
|
||||
sortList: [[0,0]]
|
||||
)
|
||||
|
||||
$('.facts').tablesorter(
|
||||
sortList: [[0,0]]
|
||||
)
|
||||
|
||||
$('input.filter-table').parent('div').removeClass('hide')
|
||||
$("input.filter-table").on "keyup", (e) ->
|
||||
rex = new RegExp($(this).val(), "i")
|
||||
|
||||
$(".searchable tr").hide()
|
||||
$(".searchable tr").filter( ->
|
||||
rex.test $(this).text()
|
||||
).show()
|
||||
|
||||
if e.keyCode is 27
|
||||
$(e.currentTarget).val ""
|
||||
ev = $.Event("keyup")
|
||||
ev.keyCode = 13
|
||||
$(e.currentTarget).trigger(ev)
|
||||
e.currentTarget.blur()
|
||||
51
puppetboard/static/css/puppetboard.css
Normal file
51
puppetboard/static/css/puppetboard.css
Normal file
@@ -0,0 +1,51 @@
|
||||
body {
|
||||
padding-top: 60px;
|
||||
}
|
||||
th.headerSortUp {
|
||||
position: relative
|
||||
}
|
||||
th.headerSortDown {
|
||||
position: relative
|
||||
}
|
||||
th.header {
|
||||
position: relative
|
||||
}
|
||||
th.header:after {
|
||||
content: "\f0dc";
|
||||
font-family: FontAwesome;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-decoration: inherit;
|
||||
color: #000;
|
||||
font-size: 18px;
|
||||
padding-right: 0.5em;
|
||||
float:right;
|
||||
}
|
||||
th.headerSortUp:after {
|
||||
content: "\f0de";
|
||||
font-family: FontAwesome;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-decoration: inherit;
|
||||
color: #000;
|
||||
font-size: 18px;
|
||||
padding-right: 0.5em;
|
||||
float:right;
|
||||
}
|
||||
th.headerSortDown:after {
|
||||
content: "\f0dd";
|
||||
font-family: FontAwesome;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-decoration: inherit;
|
||||
color: #000;
|
||||
font-size: 18px;
|
||||
padding-right: 0.5em;
|
||||
float:right;
|
||||
}
|
||||
.stat {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.navbar .brand:hover {
|
||||
color: #fff;
|
||||
}
|
||||
27
puppetboard/static/js/lists.js
Normal file
27
puppetboard/static/js/lists.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// Generated by CoffeeScript 1.6.3
|
||||
(function() {
|
||||
var $;
|
||||
|
||||
$ = jQuery;
|
||||
|
||||
$(function() {});
|
||||
|
||||
$('input.filter-list').parent('div').removeClass('hide');
|
||||
|
||||
$("input.filter-list").on("keyup", function(e) {
|
||||
var ev, rex;
|
||||
rex = new RegExp($(this).val(), "i");
|
||||
$(".searchable li").hide();
|
||||
$(".searchable li").filter(function() {
|
||||
return rex.test($(this).text());
|
||||
}).show();
|
||||
if (e.keyCode === 27) {
|
||||
$(e.currentTarget).val("");
|
||||
ev = $.Event("keyup");
|
||||
ev.keyCode = 13;
|
||||
$(e.currentTarget).trigger(ev);
|
||||
return e.currentTarget.blur();
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
1400
puppetboard/static/js/moment.js
Normal file
1400
puppetboard/static/js/moment.js
Normal file
File diff suppressed because it is too large
Load Diff
40
puppetboard/static/js/tables.js
Normal file
40
puppetboard/static/js/tables.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// Generated by CoffeeScript 1.6.3
|
||||
(function() {
|
||||
var $;
|
||||
|
||||
$ = jQuery;
|
||||
|
||||
$(function() {});
|
||||
|
||||
$('.nodes').tablesorter({
|
||||
headers: {
|
||||
3: {
|
||||
sorter: false
|
||||
}
|
||||
},
|
||||
sortList: [[0, 0]]
|
||||
});
|
||||
|
||||
$('.facts').tablesorter({
|
||||
sortList: [[0, 0]]
|
||||
});
|
||||
|
||||
$('input.filter-table').parent('div').removeClass('hide');
|
||||
|
||||
$("input.filter-table").on("keyup", function(e) {
|
||||
var ev, rex;
|
||||
rex = new RegExp($(this).val(), "i");
|
||||
$(".searchable tr").hide();
|
||||
$(".searchable tr").filter(function() {
|
||||
return rex.test($(this).text());
|
||||
}).show();
|
||||
if (e.keyCode === 27) {
|
||||
$(e.currentTarget).val("");
|
||||
ev = $.Event("keyup");
|
||||
ev.keyCode = 13;
|
||||
$(e.currentTarget).trigger(ev);
|
||||
return e.currentTarget.blur();
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
23
puppetboard/static/js/timestamps.js
Normal file
23
puppetboard/static/js/timestamps.js
Normal file
@@ -0,0 +1,23 @@
|
||||
jQuery(function ($) {
|
||||
var localise_timestamp = function(timestamp){
|
||||
if (timestamp === "None"){
|
||||
return '';
|
||||
};
|
||||
d = moment.utc(timestamp);
|
||||
d.local();
|
||||
return d;
|
||||
};
|
||||
|
||||
$("[rel=utctimestamp]").each(
|
||||
function(index, timestamp){
|
||||
var tstamp = $(timestamp);
|
||||
var tstring = tstamp.text().trim();
|
||||
var result = localise_timestamp(tstring);
|
||||
if (result == '') {
|
||||
tstamp.text('Unknown');
|
||||
} else {
|
||||
tstamp.text(localise_timestamp(tstring).format('LLLL'));
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
11
puppetboard/templates/400.html
Normal file
11
puppetboard/templates/400.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block row_fluid %}
|
||||
<div class="container" style="margin-bottom:55px;">
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
<h2>Bad Request</h2>
|
||||
<p>The request sent to PuppetDB was invalid. This is usually caused by using an unsupported operator.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
puppetboard/templates/404.html
Normal file
11
puppetboard/templates/404.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block row_fluid %}
|
||||
<div class="container" style="margin-bottom:55px;">
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
<h2>Not Found</h2>
|
||||
<p>What you were looking for could not be found in PuppetDB.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
puppetboard/templates/412.html
Normal file
11
puppetboard/templates/412.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block row_fluid %}
|
||||
<div class="container" style="margin-bottom:55px;">
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
<h2>Experimental Disabled</h2>
|
||||
<p>You're trying to access a feature restricted to PuppetDB's Experimental API but haven't configured Puppetboard to allow this.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
16
puppetboard/templates/500.html
Normal file
16
puppetboard/templates/500.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block row_fluid %}
|
||||
<div class="container" style="margin-bottom:55px;">
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
<h2>Internal Server Error</h2>
|
||||
<p>This error usually occurs because:
|
||||
<ul>
|
||||
<li>We were unable to reach PuppetDB;</li>
|
||||
<Li>The query to be executed was malformed resulting in an incorrectly encoded request.</li>
|
||||
</ul></p>
|
||||
<p>Please have a look at the log output for further information.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
77
puppetboard/templates/_macros.html
Normal file
77
puppetboard/templates/_macros.html
Normal file
@@ -0,0 +1,77 @@
|
||||
{% macro facts_table(facts, autofocus=False, condensed=False, show_node=False, margin_top=20, margin_bottom=20) -%}
|
||||
<div class="filter" style="margin-bottom:{{margin_bottom}}px;margin-top:{{margin_top}}px;">
|
||||
<input {% if autofocus %} autofocus="autofocus" {% endif %} style="width:100%" type="text" class="filter-table input-medium search-query" placeholder="Type here to filter">
|
||||
</div>
|
||||
<table class="filter-table table table-striped {% if condensed %}table-condensed{% endif%}" style="table-layout:fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fact</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for fact in facts %}
|
||||
<tr>
|
||||
{% if show_node %}
|
||||
<td>{{fact.node}}</td>
|
||||
{% else %}
|
||||
<td>{{fact.name}}</td>
|
||||
{% endif %}
|
||||
<td style="word-wrap:break-word">{{fact.value}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro reports_table(reports, nodename, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True) -%}
|
||||
<div class="alert alert-info">
|
||||
Only showing the last ten reports.
|
||||
</div>
|
||||
<table class='table table-striped {% if condensed %}table-condensed{% endif %}'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Start time</th>
|
||||
<th>Run time</th>
|
||||
<th>Full report</th>
|
||||
{% if show_conf_col %}
|
||||
<th>Configuration version</th>
|
||||
{% endif %}
|
||||
{% if show_agent_col %}
|
||||
<th>Agent version</th>
|
||||
{% endif %}
|
||||
{% if show_host_col %}
|
||||
<th>Hostname</th>
|
||||
{% endif %}
|
||||
<tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for report in reports %}
|
||||
{% if hash_truncate %}
|
||||
{% set rep_hash = "%s…"|format(report.hash_[0:6])|safe %}
|
||||
{% else %}
|
||||
{% set rep_hash = report.hash_ %}
|
||||
{% endif %}
|
||||
{% if report.failed %}
|
||||
<tr class="error">
|
||||
{% else %}
|
||||
<tr>
|
||||
{% endif %}
|
||||
<td rel="utctimestamp">{{report.start}}</td>
|
||||
<td>{{report.run_time}}</td>
|
||||
|
||||
<td><a href="{{url_for('report', node=nodename, report_id=report.hash_)}}">{{rep_hash}}</a></td>
|
||||
{% if show_conf_col %}
|
||||
<td>{{report.version}}</td>
|
||||
{% endif %}
|
||||
{% if show_agent_col %}
|
||||
<td>{{report.agent_version}}</td>
|
||||
{% endif %}
|
||||
{% if show_host_col %}
|
||||
<td>{{nodename}}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endmacro %}
|
||||
6
puppetboard/templates/fact.html
Normal file
6
puppetboard/templates/fact.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% import '_macros.html' as macros %}
|
||||
{% block content %}
|
||||
<h1>{{name}}</h1>
|
||||
{{macros.facts_table(facts, autofocus=True, show_node=True, margin_bottom=10)}}
|
||||
{% endblock content %}
|
||||
16
puppetboard/templates/facts.html
Normal file
16
puppetboard/templates/facts.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block content %}
|
||||
<div class="hide" style="margin-bottom:20px">
|
||||
<input autofocus="autofocus" style="width:100%" type="text" class="filter-list input-medium search-query" placeholder="Type here to filter">
|
||||
</div>
|
||||
<div style="-moz-column-count:4; -webkit-column-count:4; column-count:4;">
|
||||
{%- for key,facts_list in facts_dict %}
|
||||
<span class='label label-success'>{{key}}</span>
|
||||
<ul class="searchable">
|
||||
{%- for fact in facts_list %}
|
||||
<li><a href="{{url_for('fact', fact=fact)}}">{{fact}}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
38
puppetboard/templates/index.html
Normal file
38
puppetboard/templates/index.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block row_fluid %}
|
||||
<div class="span12">
|
||||
<div class='alert alert-info'>
|
||||
We need something fancy here.
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" style="margin-bottom:55px;">
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
<div class="span4 stat">
|
||||
<h1>{{metrics['num_nodes']}}</h1>
|
||||
<span>Population</span>
|
||||
</div>
|
||||
<div class="span4 stat">
|
||||
<h1>{{metrics['num_resources']}}</h1>
|
||||
<span>Resources managed</span>
|
||||
</div>
|
||||
<div class="span4 stat">
|
||||
<h1>{{metrics['avg_resources_node']}}</h1>
|
||||
<span>Avg. resources/node</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
<div class="span4 stat">
|
||||
<h1>{{metrics['mean_failed_commands']}}</h1>
|
||||
<span>Mean command failures</span>
|
||||
</div>
|
||||
<div class="span4 stat offset4">
|
||||
<h1>{{metrics['mean_command_time']}}s</h1>
|
||||
<span>Mean command execution time</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock row_fluid %}
|
||||
66
puppetboard/templates/layout.html
Normal file
66
puppetboard/templates/layout.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Puppetᴃoard</title>
|
||||
<link href="//netdna.bootstrapcdn.com/bootswatch/2.3.2/flatly/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="{{url_for('static', filename='css/puppetboard.css')}}" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="font-variant:small-caps" class="navbar navbar-fixed-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container">
|
||||
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</a>
|
||||
<span style="margin-top:-2px;" class="brand">Puppetboard</span>
|
||||
<div class="nav-collapse collapse">
|
||||
<ul class="nav">
|
||||
{%- for endpoint, caption in [
|
||||
('index', 'Overview'),
|
||||
('nodes', 'Nodes'),
|
||||
('facts', 'Facts'),
|
||||
('reports', 'Reports'),
|
||||
('query', 'Query'),
|
||||
] %}
|
||||
<li{% if endpoint == request.endpoint %} class=active{% endif
|
||||
%}><a href="{{ url_for(endpoint) }}">{{ caption }}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% block container %}
|
||||
<div class="container-fluid" style="margin-bottom:55px;">
|
||||
<div class="row-fluid">
|
||||
{% block row_fluid %}
|
||||
<div class="span12">
|
||||
{% block content %} {% endblock content %}
|
||||
</div>
|
||||
{% endblock row_fluid %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock container %}
|
||||
<div class="navbar navbar-fixed-bottom" style="background-color:rgb(249, 249, 249);">
|
||||
<div class="navbar" style="height:55px;margin-bottom:0;">
|
||||
<div class="container-fluid" style="line-height:55px;">
|
||||
Copyright © 2013 <a href="https://github.com/daenney">Daniele Sluijters</a>. <span style="float:right">Live from PuppetDB.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
|
||||
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/tablesorter/2.0.3/jquery.tablesorter.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/moment.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='js/timestamps.js')}}"></script>
|
||||
<script src="{{url_for('static', filename='js/tables.js')}}"></script>
|
||||
<script src="{{url_for('static', filename='js/lists.js')}}"></script>
|
||||
{% block script %} {% endblock script %}
|
||||
</body>
|
||||
</html>
|
||||
46
puppetboard/templates/node.html
Normal file
46
puppetboard/templates/node.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% import '_macros.html' as macros %}
|
||||
{% block content %}
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<h1>Details</h1>
|
||||
<table class="table table-striped table-condensed" style="table-layout:fixed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:140px">Hostname</td>
|
||||
<td style="word-wrap:break-word"><b>{{node.name}}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Catalog compiled at</td>
|
||||
<td rel="utctimestamp">{{node.catalog_timestamp}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Facts retrieved at</td>
|
||||
<td rel="utctimestamp">{{node.facts_timestamp}}</td>
|
||||
</tr>
|
||||
{% if config.PUPPETDB_EXPERIMENTAL %}
|
||||
<tr>
|
||||
<td>Report uploaded at</td>
|
||||
<td rel="utctimestamp">{{node.report_timestamp}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if config.PUPPETDB_EXPERIMENTAL %}
|
||||
<div class="span4">
|
||||
<h1>Facts</h1>
|
||||
{{macros.facts_table(facts, condensed=True, margin_top=10)}}
|
||||
</div>
|
||||
<div class="span4">
|
||||
<h1>Reports</h1>
|
||||
{{ macros.reports_table(reports, node.name, condensed=True, hash_truncate=True, show_conf_col=False, show_agent_col=False, show_host_col=False)}}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="span8">
|
||||
<h1>Facts</h1>
|
||||
{{macros.facts_table(facts, condensed=True, margin_top=10)}}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
50
puppetboard/templates/nodes.html
Normal file
50
puppetboard/templates/nodes.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block content %}
|
||||
<div class="alert alert-info">
|
||||
PuppetDB currently only returns active nodes.
|
||||
</div>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{category}}">
|
||||
{{message}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="hide" style="margin-bottom:20px">
|
||||
<input autofocus="autofocus" style="width:100%" type="text" class="filter-table input-medium search-query" placeholder="Type here to filter">
|
||||
</div>
|
||||
<table class='nodes table table-striped table-condensed'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>Catalog compiled at</th>
|
||||
{% if config.PUPPETDB_EXPERIMENTAL %}
|
||||
<th>Last report</th>
|
||||
<th> </th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for node in nodes %}
|
||||
<tr>
|
||||
<td><a href="{{url_for('node', node_name=node.name)}}">{{node.name}}</a></td>
|
||||
<td rel="utctimestamp">{{node.catalog_timestamp}}</td>
|
||||
{% if config.PUPPETDB_EXPERIMENTAL %}
|
||||
<td>
|
||||
{% if node.report_timestamp %}
|
||||
<span rel="utctimestamp">{{ node.report_timestamp }}</span>
|
||||
{% else %}
|
||||
<i class="icon icon-ban-circle"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a class="btn btn-small btn-primary" href="{{url_for('reports_node', node=node.name)}}">Reports</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock content %}
|
||||
61
puppetboard/templates/query.html
Normal file
61
puppetboard/templates/query.html
Normal file
@@ -0,0 +1,61 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block row_fluid %}
|
||||
<div class="span12">
|
||||
<div class="alert">
|
||||
This is highly exeprimental and will likely set your server on fire.
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" style="margin-bottom:55px;">
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
<h2>Compose</h2>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{category}}">
|
||||
{{message}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form class="form-horizontal" method="POST" action="{{ url_for('query')}}">
|
||||
{{ form.csrf_token }}
|
||||
<div class="control-group {% if form.query.errors %} error {% endif %}">
|
||||
{{form.query.label(class_="control-label")}}
|
||||
<div class="controls">
|
||||
{{form.query(class_="input-block-level", autofocus="autofocus", rows=5, placeholder="\"=\", \"name\" \"hostname\"")}}
|
||||
{% if form.query.errors %}
|
||||
<span class="help-inline">{% for error in form.query.errors %}{{error}}{% endfor %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group {% if form.endpoints.errors %} error {% endif %}">
|
||||
{{form.endpoints.label(class_="control-label")}}
|
||||
<div class="controls">
|
||||
{% for subfield in form.endpoints %}
|
||||
{{subfield.label(class_="radio inline")}}
|
||||
{{subfield }}
|
||||
{% endfor %}
|
||||
{% if form.endpoints.errors %}
|
||||
<span class="help-inline">{% for error in form.endpoints.errors %}{{error}}{% endfor %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Yes I'm sure</button>
|
||||
<button type="button" class="btn">No thanks</button>
|
||||
</div>
|
||||
{{ form.hidden_tag() }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% if result %}
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
<h2>Result</h2>
|
||||
<pre><code>{{ result|tojson|replace(", ", ",\n") }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock row_fluid %}
|
||||
62
puppetboard/templates/report.html
Normal file
62
puppetboard/templates/report.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block content %}
|
||||
<h1>Summary</h1>
|
||||
<table class='table table-striped'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>Configuration version</th>
|
||||
<th>Start time</th>
|
||||
<th>End time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{report.node}}</td>
|
||||
<td>
|
||||
{{report.version}}
|
||||
</td>
|
||||
<td rel="utctimestamp">
|
||||
{{report.start}}
|
||||
</td>
|
||||
<td rel="utctimestamp">
|
||||
{{report.end}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h1>Events</h1>
|
||||
<table class='table table-striped table-condensed'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Resource</th>
|
||||
<th>Status</th>
|
||||
<th>Changed From</th>
|
||||
<th>Changed To</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in events %}
|
||||
{% if not event.failed and event.item['old'] != event.item['new'] %}
|
||||
<tr class='success'>
|
||||
{% elif event.failed %}
|
||||
<tr class='error'>
|
||||
{% endif %}
|
||||
<td>{{event.item['type']}}[{{event.item['title']}}]</td>
|
||||
<td>{{event.status}}</td>
|
||||
<td>{{event.item['old']}}</td>
|
||||
<td>{{event.item['new']}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock content %}
|
||||
{% block script %}
|
||||
<script type='text/javascript'>
|
||||
jQuery(function ($) {
|
||||
$("[rel=tooltip]").tooltip();
|
||||
});
|
||||
</script>
|
||||
{% endblock script %}
|
||||
6
puppetboard/templates/reports.html
Normal file
6
puppetboard/templates/reports.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block content %}
|
||||
<div class="alert">
|
||||
Pending <a href="http://projects.puppetlabs.com/issues/21600">#21600</a>. You can access reports for a node or individual reports through the <a href="{{url_for('nodes')}}">Nodes</a> tab.
|
||||
</div>
|
||||
{% endblock content %}
|
||||
5
puppetboard/templates/reports_node.html
Normal file
5
puppetboard/templates/reports_node.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% import '_macros.html' as macros %}
|
||||
{% block content %}
|
||||
{{ macros.reports_table(reports, nodename, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True)}}
|
||||
{% endblock content %}
|
||||
61
puppetboard/utils.py
Normal file
61
puppetboard/utils.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from requests.exceptions import HTTPError, ConnectionError
|
||||
from pypuppetdb.errors import EmptyResponseError, ExperimentalDisabledError
|
||||
|
||||
from flask import abort, flash
|
||||
|
||||
def get_or_abort(func, *args, **kwargs):
|
||||
"""Execute the function with its arguments and handle the possible
|
||||
errors that might occur.
|
||||
|
||||
In this case, if we get an exception we simply abort the request.
|
||||
"""
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except HTTPError, e:
|
||||
abort(e.response.status_code)
|
||||
except ConnectionError:
|
||||
abort(500)
|
||||
except ExperimentalDisabledError:
|
||||
abort(412)
|
||||
except EmptyResponseError:
|
||||
abort(204)
|
||||
|
||||
|
||||
def ten_reports(reports):
|
||||
"""Helper to yield the first then reports from the reports generator.
|
||||
|
||||
This is an ugly solution at best...
|
||||
"""
|
||||
for count, report in enumerate(reports):
|
||||
if count == 10:
|
||||
raise StopIteration
|
||||
yield report
|
||||
|
||||
|
||||
def yield_or_stop(generator):
|
||||
"""Similar in intent to get_or_abort this helper will iterate over our
|
||||
generators and handle certain errors.
|
||||
|
||||
Since this is also used in streaming responses where we can't just abort
|
||||
a request we always yield empty and then raise StopIteration.
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
yield next(generator)
|
||||
except StopIteration:
|
||||
raise
|
||||
except ExperimentalDisabledError:
|
||||
yield
|
||||
raise StopIteration
|
||||
except EmptyResponseError:
|
||||
yield
|
||||
raise StopIteration
|
||||
except ConnectionError:
|
||||
yield
|
||||
raise StopIteration
|
||||
except HTTPError:
|
||||
yield
|
||||
raise StopIteration
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Flask==0.10.1
|
||||
Flask-WTF==0.8.4
|
||||
pypuppetdb=0.0.1
|
||||
11
wsgi.py
Normal file
11
wsgi.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
me = os.path.dirname(os.path.abspath(__file__))
|
||||
# Add us to the PYTHONPATH/sys.path if we're not on it
|
||||
if not me in sys.path:
|
||||
sys.path.insert(0, me)
|
||||
|
||||
from puppetboard.app import app as application
|
||||
Reference in New Issue
Block a user