Merge pull request #161 from corey-hammerton/catalog

puppetboard: Adding a more intuitive catalog view
This commit is contained in:
Spencer Krum
2015-11-04 17:18:01 -08:00
6 changed files with 221 additions and 3 deletions

View File

@@ -18,7 +18,7 @@ from flask_wtf.csrf import CsrfProtect
from pypuppetdb import connect from pypuppetdb import connect
from puppetboard.forms import QueryForm from puppetboard.forms import (CatalogForm, QueryForm)
from puppetboard.utils import ( from puppetboard.utils import (
get_or_abort, yield_or_stop, get_or_abort, yield_or_stop,
limit_reports, jsonprint limit_reports, jsonprint
@@ -407,6 +407,45 @@ def metric(metric):
name=name, name=name,
metric=sorted(metric.items())) metric=sorted(metric.items()))
@app.route('/catalogs')
def catalogs():
if app.config['ENABLE_CATALOG']:
nodenames = []
catalog_list = []
nodes = get_or_abort(puppetdb.nodes,
query='["null?", "catalog_timestamp", false]',
with_status=False,
order_by='[{"field": "certname", "order": "asc"}]')
nodes, temp = tee(nodes)
for node in temp:
nodenames.append(node.name)
for node in nodes:
table_row = {
'name': node.name,
'catalog_timestamp': node.catalog_timestamp
}
if len(nodenames) > 1:
form = CatalogForm()
form.compare.data = node.name
form.against.choices = [(x, x) for x in nodenames
if x != node.name]
table_row['form'] = form
else:
table_row['form'] = None
catalog_list.append(table_row)
return render_template(
'catalogs.html',
nodes=catalog_list)
else:
log.warn('Access to catalogs endpoint disabled by administrator')
abort(403)
@app.route('/catalog/<node_name>') @app.route('/catalog/<node_name>')
def catalog_node(node_name): def catalog_node(node_name):
"""Fetches from PuppetDB the compiled catalog of a given node.""" """Fetches from PuppetDB the compiled catalog of a given node."""
@@ -416,3 +455,43 @@ def catalog_node(node_name):
else: else:
log.warn('Access to catalog interface disabled by administrator') log.warn('Access to catalog interface disabled by administrator')
abort(403) abort(403)
@app.route('/catalog/submit', methods=['POST'])
def catalog_submit():
"""Receives the submitted form data from the catalogs page and directs
the users to the comparison page. Directs users back to the catalogs
page if no form submission data is found.
"""
if app.config['ENABLE_CATALOG']:
form = CatalogForm(request.form)
form.against.choices = [(form.against.data, form.against.data)]
if form.validate_on_submit():
compare = form.compare.data
against = form.against.data
return redirect(
url_for('catalog_compare',
compare=compare,
against=against))
return redirect(url_for('catalogs'))
else:
log.warn('Access to catalog interface disabled by administrator')
abort(403)
@app.route('/catalogs/compare/<compare>...<against>')
def catalog_compare(compare, against):
"""Compares the catalog of one node, parameter compare, with that of
with that of another node, parameter against.
"""
if app.config['ENABLE_CATALOG']:
compare_cat = get_or_abort(puppetdb.catalog,
node=compare)
against_cat = get_or_abort(puppetdb.catalog,
node=against)
return render_template('catalog_compare.html',
compare=compare_cat,
against=against_cat)
else:
log.warn('Access to catalog interface disabled by administrator')
abort(403)

View File

@@ -2,7 +2,10 @@ from __future__ import unicode_literals
from __future__ import absolute_import from __future__ import absolute_import
from flask.ext.wtf import Form from flask.ext.wtf import Form
from wtforms import RadioField, TextAreaField, validators from wtforms import (
HiddenField, RadioField, SelectField,
TextAreaField, validators
)
class QueryForm(Form): class QueryForm(Form):
@@ -18,3 +21,8 @@ class QueryForm(Form):
('reports', 'Reports'), ('reports', 'Reports'),
('events', 'Events'), ('events', 'Events'),
]) ])
class CatalogForm(Form):
"""The form used to compare the catalogs of different nodes."""
compare = HiddenField('compare')
against = SelectField('against')

View File

@@ -48,7 +48,7 @@
<th>Target</th> <th>Target</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class='searchable'>
{% for edge in catalog.get_edges() %} {% for edge in catalog.get_edges() %}
<tr> <tr>
<td>{{edge.source}}</td> <td>{{edge.source}}</td>

View File

@@ -0,0 +1,90 @@
{% extends 'layout.html' %}
{% block content %}
<div class="ui fluid icon input hide" style="margin-bottom:20px">
<input autofocus="autofocus" class="filter-table" placeholder="Type here to filter...">
</div>
<table class="ui basic table">
<tbody>
<tr>
<th><h1>Comparing</h1></th>
<th><h1>Against</h1></th>
</tr>
<tr>
<td>{{compare.node}}</td>
<td>{{against.node}}</td>
</tr>
<tr>
<td>
<table class="ui basic table compact catalog">
<thead>
<tr><th>Resources</th></tr>
</thead>
<tbody class='searchable'>
{% for resource in compare.get_resources() %}
<tr>
<td>{{resource.type_}}[{{resource.name}}]</td>
</tr>
{% endfor %}
</tbody>
</table>
</td>
<td>
<table class="ui basic table compact catalog">
<thead>
<tr><th>Resources</th></tr>
</thead>
<tbody class='searchable'>
{% for resource in against.get_resources() %}
<tr>
<td>{{resource.type_}}[{{resource.name}}]</td>
</tr>
{% endfor %}
</tbody>
</table>
</td>
</tr>
<tr>
<td>
<table class="ui basic table compact catalog">
<thead>
<tr>
<th>Edges</th>
<th>-&gt;</th>
<th>Target</th>
</tr>
</thead>
<tbody class='searchable'>
{% for edge in compare.get_edges() %}
<tr>
<td>{{edge.source}}</td>
<td>{{edge.relationship}}</td>
<td>{{edge.target}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</td>
<td>
<table class="ui basic table compact catalog">
<thead>
<tr>
<th>Edge</th>
<th>-&gt;</th>
<th>Target</th>
</tr>
</thead>
<tbody class='searchable'>
{% for edge in against.get_edges() %}
<tr>
<td>{{edge.source}}</td>
<td>{{edge.relationship}}</td>
<td>{{edge.target}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
{% endblock content %}

View File

@@ -0,0 +1,40 @@
{% extends 'layout.html' %}
{% import '_macros.html' as macros %}
{% block content %}
<div class="ui fluid icon input hide" style="margin-bottom:20px">
<input autofocus="autofocus" class="filter-table" placeholder="Type here to filter...">
</div>
<table class='ui compact basic table nodes'>
<thead>
<tr>
<th></th>
<th>Hostname</th>
<th>Compile Time</th>
<th>Compare With</th>
</tr>
</thead>
<tbody class="searchable">
{% for node in nodes %}
<tr>
<td></td>
<td><a href="{{url_for('node', node_name=node.name)}}">{{node.name}}</a></td>
<td><a rel="utctimestamp" href="{{url_for('catalog_node', node_name=node.name)}}">{{node.catalog_timestamp}}</a></td>
<td>
{% if node.form %}
<div class="ui form">
<form method="POST" action="{{url_for('catalog_submit')}}">
{{node.form.csrf_token}}
<div class="field inline">
{{node.form.compare}}
{{node.form.against}}
<input type="submit" class="ui submit button" style="height:auto;" value="Compare"/>
</div>
</form>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %}

View File

@@ -34,6 +34,7 @@
('reports', 'Reports'), ('reports', 'Reports'),
('metrics', 'Metrics'), ('metrics', 'Metrics'),
('inventory', 'Inventory'), ('inventory', 'Inventory'),
('catalogs', 'Catalogs'),
('query', 'Query'), ('query', 'Query'),
] %} ] %}
<a {% if endpoint == request.endpoint %} class="active item" {% else %} class="item" {% endif %} <a {% if endpoint == request.endpoint %} class="active item" {% else %} class="item" {% endif %}