Merge pull request #22 from digitalmediacenter/feature-status
Add basic support for node status by using the most recent report Closes #5 #15
This commit is contained in:
@@ -5,10 +5,11 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
import collections
|
import collections
|
||||||
import urllib
|
import urllib
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask, render_template, abort, url_for,
|
Flask, render_template, abort, url_for,
|
||||||
Response, stream_with_context,
|
Response, stream_with_context, redirect,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pypuppetdb import connect
|
from pypuppetdb import connect
|
||||||
@@ -71,8 +72,9 @@ def server_error(e):
|
|||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""This view generates the index page and displays a set of metrics fetched
|
"""This view generates the index page and displays a set of metrics and latest reports on
|
||||||
from PuppetDB."""
|
nodes fetched from PuppetDB.
|
||||||
|
"""
|
||||||
# TODO: Would be great if we could parallelize this somehow, doing these
|
# TODO: Would be great if we could parallelize this somehow, doing these
|
||||||
# requests in sequence is rather pointless.
|
# requests in sequence is rather pointless.
|
||||||
num_nodes = get_or_abort(puppetdb.metric,
|
num_nodes = get_or_abort(puppetdb.metric,
|
||||||
@@ -92,7 +94,21 @@ def index():
|
|||||||
'mean_failed_commands': mean_failed_commands['MeanRate'],
|
'mean_failed_commands': mean_failed_commands['MeanRate'],
|
||||||
'mean_command_time': "{0:10.6f}".format(mean_command_time['MeanRate']),
|
'mean_command_time': "{0:10.6f}".format(mean_command_time['MeanRate']),
|
||||||
}
|
}
|
||||||
return render_template('index.html', metrics=metrics)
|
|
||||||
|
latest_event_count = puppetdb._query('aggregate-event-counts', query='["=", "latest-report?", true]', summarize_by='certname')
|
||||||
|
latest_event_count['noopskip'] = latest_event_count['noops']+latest_event_count['skips']
|
||||||
|
|
||||||
|
latest_events = puppetdb._query('event-counts', query='["=", "latest-report?", true]', summarize_by='certname')
|
||||||
|
|
||||||
|
unreported = []
|
||||||
|
for node in puppetdb.nodes():
|
||||||
|
node_last_seen = node.report_timestamp.replace(tzinfo=None)
|
||||||
|
if node_last_seen < (datetime.utcnow()-timedelta(hours=app.config['UNRESPONSIVE_HOURS'])):
|
||||||
|
delta = (datetime.utcnow()-node_last_seen)
|
||||||
|
node.noresponse = str(delta.days) + "d " + str(int(delta.seconds/3600)) +"h " + str(int((delta.seconds%3600)/60))+ "m"
|
||||||
|
unreported.append(node)
|
||||||
|
|
||||||
|
return render_template('index.html', metrics=metrics, latest_event_count=latest_event_count, latest_events=latest_events, unreported=unreported, unreported_time=app.config['UNRESPONSIVE_HOURS'])
|
||||||
|
|
||||||
@app.route('/nodes')
|
@app.route('/nodes')
|
||||||
def nodes():
|
def nodes():
|
||||||
@@ -105,8 +121,19 @@ def nodes():
|
|||||||
works. Once pagination is in place we can change this but we'll need to
|
works. Once pagination is in place we can change this but we'll need to
|
||||||
provide a search feature instead.
|
provide a search feature instead.
|
||||||
"""
|
"""
|
||||||
|
latest_events = puppetdb._query('event-counts', query='["=", "latest-report?", true]', summarize_by='certname')
|
||||||
|
nodes = []
|
||||||
|
for node in yield_or_stop(puppetdb.nodes()):
|
||||||
|
# check if node name is contained in any of the event-counts (grouped by certname)
|
||||||
|
status = [s for s in latest_events if s['subject']['title'] == node.name]
|
||||||
|
if status:
|
||||||
|
node.status = status[0]
|
||||||
|
else:
|
||||||
|
node.status = None
|
||||||
|
nodes.append(node)
|
||||||
|
|
||||||
return Response(stream_with_context(stream_template('nodes.html',
|
return Response(stream_with_context(stream_template('nodes.html',
|
||||||
nodes=yield_or_stop(puppetdb.nodes()))))
|
nodes=nodes)))
|
||||||
|
|
||||||
@app.route('/node/<node_name>')
|
@app.route('/node/<node_name>')
|
||||||
def node(node_name):
|
def node(node_name):
|
||||||
@@ -146,10 +173,26 @@ def reports_node(node):
|
|||||||
return render_template('reports_node.html', reports=reports,
|
return render_template('reports_node.html', reports=reports,
|
||||||
nodename=node)
|
nodename=node)
|
||||||
|
|
||||||
|
@app.route('/report/latest/<node_name>')
|
||||||
|
def report_latest(node_name):
|
||||||
|
"""Redirect to the latest report of a given node. This is a workaround
|
||||||
|
as long as PuppetDB can't filter reports for latest-report? field. This
|
||||||
|
feature has been requested: http://projects.puppetlabs.com/issues/21554
|
||||||
|
"""
|
||||||
|
# TODO: use limit parameter in _query to get just one report
|
||||||
|
node = get_or_abort(puppetdb.node, node_name)
|
||||||
|
if app.config['PUPPETDB_API'] > 2:
|
||||||
|
reports = ten_reports(node.reports())
|
||||||
|
else:
|
||||||
|
reports = iter([])
|
||||||
|
report = list(yield_or_stop(reports))[0]
|
||||||
|
return redirect(url_for('report', node=node_name, report_id=report))
|
||||||
|
|
||||||
@app.route('/report/<node>/<report_id>')
|
@app.route('/report/<node>/<report_id>')
|
||||||
def report(node, report_id):
|
def report(node, report_id):
|
||||||
"""Displays a single report including all the events associated with that
|
"""Displays a single report including all the events associated with that
|
||||||
report and their status."""
|
report and their status.
|
||||||
|
"""
|
||||||
if app.config['PUPPETDB_API'] > 2:
|
if app.config['PUPPETDB_API'] > 2:
|
||||||
reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node))
|
reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ PUPPETDB_TIMEOUT=20
|
|||||||
PUPPETDB_API=3
|
PUPPETDB_API=3
|
||||||
DEV_LISTEN_HOST='127.0.0.1'
|
DEV_LISTEN_HOST='127.0.0.1'
|
||||||
DEV_LISTEN_PORT=5000
|
DEV_LISTEN_PORT=5000
|
||||||
|
UNRESPONSIVE_HOURS=2
|
||||||
ENABLE_QUERY=True
|
ENABLE_QUERY=True
|
||||||
LOGLEVEL='info'
|
LOGLEVEL='info'
|
||||||
|
|||||||
@@ -52,3 +52,21 @@ th.headerSortDown:after {
|
|||||||
.table tbody tr.error>td {
|
.table tbody tr.error>td {
|
||||||
background-color: #f2dede;
|
background-color: #f2dede;
|
||||||
}
|
}
|
||||||
|
h1.error {
|
||||||
|
color: rgb(223, 46, 27);
|
||||||
|
}
|
||||||
|
h1.success {
|
||||||
|
color: #18BC9C;
|
||||||
|
}
|
||||||
|
h1.noop {
|
||||||
|
color:#aaa;
|
||||||
|
}
|
||||||
|
.numtag {
|
||||||
|
width:20px;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
.label-nothing {
|
||||||
|
background-color:#ddd;
|
||||||
|
width:20px;
|
||||||
|
color:#ddd;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
|
|
||||||
$('.nodes').tablesorter({
|
$('.nodes').tablesorter({
|
||||||
headers: {
|
headers: {
|
||||||
3: {
|
4: {
|
||||||
sorter: false
|
sorter: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sortList: [[0, 0]]
|
sortList: [[1, 0]]
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.facts').tablesorter({
|
$('.facts').tablesorter({
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% block row_fluid %}
|
{% 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="container" style="margin-bottom:55px;">
|
||||||
|
<div class="row">
|
||||||
|
<h2>Statistics</h2>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="span12">
|
||||||
|
<div class="span4 stat">
|
||||||
|
<h1 class="error">{{latest_event_count['failures']}} <small>Failed</small></h1>
|
||||||
|
<span>nodes in the latest reports</span>
|
||||||
|
</div>
|
||||||
|
<div class="span4 stat">
|
||||||
|
<h1 class="success">{{latest_event_count['successes']}} <small>Succeeded</small></h1>
|
||||||
|
<span>nodes in the latest reports</span>
|
||||||
|
</div>
|
||||||
|
<div class="span4 stat">
|
||||||
|
<h1 class="noop">{{ unreported|length }} <small>Unreported</small></h1>
|
||||||
|
<span>nodes in the last {{ unreported_time }} hours</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="span12">
|
<div class="span12">
|
||||||
<div class="span4 stat">
|
<div class="span4 stat">
|
||||||
@@ -34,5 +50,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="span12">
|
||||||
|
<h2>Nodes without Report since {{ unreported_time }} hours</h2>
|
||||||
|
<table class='nodes table table-striped table-condensed'>
|
||||||
|
<tbody class="searchable">
|
||||||
|
{% for node in unreported %}
|
||||||
|
<tr>
|
||||||
|
<td style="width:110px;"><span class="label label-important">No Report</span></td>
|
||||||
|
<td>{{ node.noresponse }}</td>
|
||||||
|
<td><a href="{{url_for('node', node_name=node.name)}}">{{ node.name }}</a></td>
|
||||||
|
<td style="width:120px;"><a class="btn btn-small btn-primary" href="{{url_for('report_latest', node_name=node.name)}}">Latest Report</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2>Latest Reports with events</h2>
|
||||||
|
<table class='nodes table table-striped table-condensed'>
|
||||||
|
<tbody class="searchable">
|
||||||
|
{% for event in latest_events %}
|
||||||
|
<tr>
|
||||||
|
<td style="width:110px;">
|
||||||
|
{% if event['failures'] %}<span class="label label-important numtag">{{event['failures']}}</span>{% else %}<span class="label numtag">0</span>{% endif%}
|
||||||
|
{% if event['successes'] %}<span class="label label-success numtag">{{event['successes']}}</span>{% else %}<span class="label numtag">0</span>{% endif%}
|
||||||
|
</td>
|
||||||
|
<td style="width:700px;"><a href="{{url_for('node', node_name=event['subject']['title'])}}">{{event['subject']['title']}}</a></td>
|
||||||
|
<td style="width:120px;"><a class="btn btn-small btn-primary" href="{{url_for('report_latest', node_name=event['subject']['title'])}}">Latest Report</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock row_fluid %}
|
{% endblock row_fluid %}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<table class='nodes table table-striped table-condensed'>
|
<table class='nodes table table-striped table-condensed'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
<th>Hostname</th>
|
<th>Hostname</th>
|
||||||
<th>Catalog compiled at</th>
|
<th>Catalog compiled at</th>
|
||||||
{% if config.PUPPETDB_API > 2 %}
|
{% if config.PUPPETDB_API > 2 %}
|
||||||
@@ -29,6 +30,12 @@
|
|||||||
<tbody class="searchable">
|
<tbody class="searchable">
|
||||||
{% for node in nodes %}
|
{% for node in nodes %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td><a href="{{url_for('report_latest', node_name=node.name)}}">
|
||||||
|
{% if node.status['failures'] %}<span class="label numtag label-important">{{node.status['failures']}}</span>{% else %}<span class="label label-nothing">0</span>{% endif %}
|
||||||
|
{% if node.status['successes'] %}<span class="label numtag label-success">{{node.status['successes']}}</span>{% else %}<span class="label label-nothing">0</span>{% endif %}
|
||||||
|
{% if node.status['noops'] or node.status['skips'] %}<span class="label numtag">{{node.status['skips']+node.status['noops']}}</span>{% else %}<span class="label label-nothing">0</span>{% endif %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td><a href="{{url_for('node', node_name=node.name)}}">{{node.name}}</a></td>
|
<td><a href="{{url_for('node', node_name=node.name)}}">{{node.name}}</a></td>
|
||||||
<td rel="utctimestamp">{{node.catalog_timestamp}}</td>
|
<td rel="utctimestamp">{{node.catalog_timestamp}}</td>
|
||||||
{% if config.PUPPETDB_API > 2 %}
|
{% if config.PUPPETDB_API > 2 %}
|
||||||
@@ -41,6 +48,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if node.report_timestamp %}
|
{% if node.report_timestamp %}
|
||||||
|
<a class="btn btn-small btn-primary" href="{{url_for('report_latest', node_name=node.name)}}">Latest Report</a>
|
||||||
<a class="btn btn-small btn-primary" href="{{url_for('reports_node', node=node.name)}}">Reports</a>
|
<a class="btn btn-small btn-primary" href="{{url_for('reports_node', node=node.name)}}">Reports</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ MarkupSafe==0.18
|
|||||||
WTForms==1.0.4
|
WTForms==1.0.4
|
||||||
Werkzeug==0.9.3
|
Werkzeug==0.9.3
|
||||||
itsdangerous==0.22
|
itsdangerous==0.22
|
||||||
pypuppetdb==0.0.4
|
git+https://github.com/nedap/pypuppetdb#egg=pypuppetdb
|
||||||
requests==1.2.3
|
requests==1.2.3
|
||||||
|
|||||||
Reference in New Issue
Block a user