Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6c4bc1679 | ||
|
|
9be5aaebd9 | ||
|
|
a75b08f882 | ||
|
|
f85f0fa864 | ||
|
|
095967e445 | ||
|
|
8a56e83b69 | ||
|
|
934f90c12a | ||
|
|
0422b2ccd0 | ||
|
|
e1603608bc | ||
|
|
23af033cbb | ||
|
|
e71f30ab50 | ||
|
|
cc87e54cea | ||
|
|
3b4db8f37e | ||
|
|
3fd8a0aad4 | ||
|
|
9b662a661e | ||
|
|
cfb1383025 | ||
|
|
8db60d14bd | ||
|
|
8cf181a9e8 | ||
|
|
543f706fd7 | ||
|
|
644e169a7f | ||
|
|
b25d85bd32 | ||
|
|
9918ec8f4b | ||
|
|
da68bb259b | ||
|
|
3c05071aef | ||
|
|
cb64b73832 | ||
|
|
c3821e777f | ||
|
|
c04d45f602 | ||
|
|
d900ccf09a | ||
|
|
8b3f3ea61e | ||
|
|
f273d24f80 | ||
|
|
d152d8e3a1 | ||
|
|
c64a2b79b2 | ||
|
|
083da989de | ||
|
|
c3a9b5e81c | ||
|
|
00d0f96914 | ||
|
|
7b71eb39d2 | ||
|
|
1a178ef2af | ||
|
|
4d80b6c128 | ||
|
|
462fcbf76c | ||
|
|
c8825d3d92 | ||
|
|
1d705d04dd | ||
|
|
88d1944b4b | ||
|
|
bda3adc078 |
41
CHANGELOG.rst
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#########
|
||||||
|
Changelog
|
||||||
|
#########
|
||||||
|
|
||||||
|
0.0.2
|
||||||
|
=====
|
||||||
|
In this release we've introduced a few new things. First of all we now require
|
||||||
|
pypuppetdb version 0.0.4 or later which includes support for the v3 API
|
||||||
|
introduced with PuppetDB 1.5.
|
||||||
|
|
||||||
|
Because of changes in PuppetDB 1.5 and therefor in pypuppetdb users of the v2
|
||||||
|
API, regardless of the PuppetDB version, will no longer be able to view reports
|
||||||
|
or events.
|
||||||
|
|
||||||
|
In light of this the following settings have been removed:
|
||||||
|
|
||||||
|
* ``PUPPETDB_EXPERIMENTAL``
|
||||||
|
|
||||||
|
Two new settings have been added:
|
||||||
|
|
||||||
|
* ``PUPPETDB_API``: an integer, defaulting to ``3``, representing the API
|
||||||
|
version we want to use.
|
||||||
|
* ``ENABLE_QUERY``: a boolean, defaulting to ``True``, on wether or not to
|
||||||
|
be able to use the Query tab.
|
||||||
|
|
||||||
|
We've also added a few new features:
|
||||||
|
|
||||||
|
* Thanks to some work done during PuppetConf together with Nick Lewis (from
|
||||||
|
Puppet Labs) we now expose all of PuppetDB's metrics in the Metrics tab. The
|
||||||
|
formatting isn't exactly pretty but it's a start.
|
||||||
|
* Spencer Krum added the graphing capabilities to the Facts tab.
|
||||||
|
* Daniel Lawrence added a feature so that facts on the node view are clickable
|
||||||
|
and take you to the complete overview of that fact for your infrastructure
|
||||||
|
and made the nodes in the complete facts list clickable so you can jump to a
|
||||||
|
node.
|
||||||
|
* Klavs Klavsen contributed some documentation on how to run Puppetboard with
|
||||||
|
Passenger.
|
||||||
|
|
||||||
|
0.0.1
|
||||||
|
=====
|
||||||
|
Initial release.
|
||||||
103
README.rst
@@ -17,12 +17,14 @@ Because this project is powered by Flask we are restricted to:
|
|||||||
* Python 2.6
|
* Python 2.6
|
||||||
* Python 2.7
|
* Python 2.7
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/node-experimental.png
|
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/node-v3.png
|
||||||
:alt: View of a node
|
:alt: View of a node
|
||||||
:width: 1024
|
:width: 1024
|
||||||
:height: 700
|
:height: 700
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
Word of caution
|
Word of caution
|
||||||
===============
|
===============
|
||||||
|
|
||||||
@@ -66,6 +68,7 @@ and ``uwsgi`` can deal with.
|
|||||||
|
|
||||||
* Apache mod_wsgi configuration: http://flask.pocoo.org/docs/deploying/mod_wsgi/
|
* Apache mod_wsgi configuration: http://flask.pocoo.org/docs/deploying/mod_wsgi/
|
||||||
* uwsgi configuration: ``uwsgi --http :9090 --wsgi-file /path/to/puppetboard/wsgi.py``
|
* uwsgi configuration: ``uwsgi --http :9090 --wsgi-file /path/to/puppetboard/wsgi.py``
|
||||||
|
* Passenger
|
||||||
|
|
||||||
In the case of uwsgi you'll of course need something like nginx in front of it to
|
In the case of uwsgi you'll of course need something like nginx in front of it to
|
||||||
proxy the requests to it.
|
proxy the requests to it.
|
||||||
@@ -74,6 +77,33 @@ Don't forget that you also need to serve the ``static/`` folder on the
|
|||||||
``/static`` URL of your vhost. (I'm considering embedding the little additional
|
``/static`` URL of your vhost. (I'm considering embedding the little additional
|
||||||
Javascript and CSS this application has so no one has to bother with that).
|
Javascript and CSS this application has so no one has to bother with that).
|
||||||
|
|
||||||
|
Passenger
|
||||||
|
^^^^^^^^^
|
||||||
|
From within the Puppetboard checkout:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
mkdir public
|
||||||
|
mkdir tmp
|
||||||
|
ln -s wsgi.py passenger_wsgi.py
|
||||||
|
|
||||||
|
The apache vhost configuration:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName puppetboard.example.tld
|
||||||
|
DocumentRoot /path/to/puppetboard/public
|
||||||
|
|
||||||
|
RackAutoDetect On
|
||||||
|
Alias /static /path/to/puppetboard/static
|
||||||
|
<Directory /path/to/puppetboard/>
|
||||||
|
Options None
|
||||||
|
Order allow,deny
|
||||||
|
allow from all
|
||||||
|
</Directory>
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
=============
|
=============
|
||||||
|
|
||||||
@@ -93,6 +123,42 @@ PuppetDB's experimental API endpoints.
|
|||||||
However, if you haven't enabled them for Puppet it isn't particularily
|
However, if you haven't enabled them for Puppet it isn't particularily
|
||||||
useful to enable them here as there will be no data to retrieve.
|
useful to enable them here as there will be no data to retrieve.
|
||||||
|
|
||||||
|
Getting Help
|
||||||
|
============
|
||||||
|
This project is still very new so it's not inconceivable you'll run into
|
||||||
|
issues.
|
||||||
|
|
||||||
|
For bug reports you can file an `issue`_. If you need help with something
|
||||||
|
feel free to hit up `@daenney`_ by e-mail or find him on IRC. He can usually
|
||||||
|
be found on `IRCnet`_ and `Freenode`_ and idles in #puppet.
|
||||||
|
|
||||||
|
There's now also the #puppetboard channel on `Freenode`_ where we hang out
|
||||||
|
and answer questions related to pypuppetdb and Puppetboard.
|
||||||
|
|
||||||
|
.. _issue: https://github.com/nedap/puppetboard/issues
|
||||||
|
.. _@daenney: https://github.com/daenney
|
||||||
|
.. _IRCnet: http://www.ircnet.org
|
||||||
|
.. _Freenode: http://freenode.net
|
||||||
|
|
||||||
|
Third party
|
||||||
|
===========
|
||||||
|
Some people have already started building things with and around Puppetboard.
|
||||||
|
|
||||||
|
`Hunter Haugen`_ has provided a Vagrant setup:
|
||||||
|
|
||||||
|
* https://github.com/hunner/puppetboard-vagrant
|
||||||
|
|
||||||
|
`Spencer Krum`_ created a Puppet module to install Puppetboard with:
|
||||||
|
|
||||||
|
* https://github.com/nibalizer/puppet-module-puppetboard
|
||||||
|
|
||||||
|
You can install it with:
|
||||||
|
|
||||||
|
puppet module install nibalizer-puppetboard
|
||||||
|
|
||||||
|
.. _Hunter Haugen: https://github.com/hunner
|
||||||
|
.. _Spencer Krum: https://github.com/nibalizer
|
||||||
|
|
||||||
Contributing
|
Contributing
|
||||||
============
|
============
|
||||||
We welcome contributions to this project. However, there are a few ground
|
We welcome contributions to this project. However, there are a few ground
|
||||||
@@ -152,22 +218,34 @@ Screenshots
|
|||||||
:height: 700
|
:height: 700
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
|
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/metrics.png
|
||||||
|
:alt: Query view
|
||||||
|
:width: 1024
|
||||||
|
:height: 700
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/metric.png
|
||||||
|
:alt: Query view
|
||||||
|
:width: 1024
|
||||||
|
:height: 700
|
||||||
|
:align: center
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/query.png
|
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/query.png
|
||||||
:alt: Query view
|
:alt: Query view
|
||||||
:width: 1024
|
:width: 1024
|
||||||
:height: 700
|
:height: 700
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
With experimental endpoints
|
API v3
|
||||||
---------------------------
|
------
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/nodes-experimental.png
|
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/nodes-v3.png
|
||||||
:alt: Nodes table with experimental endpoints enabled
|
:alt: Nodes table with experimental endpoints enabled
|
||||||
:width: 1024
|
:width: 1024
|
||||||
:height: 700
|
:height: 700
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/node-experimental.png
|
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/node-v3.png
|
||||||
:alt: Node view with experimental endpoints enabled
|
:alt: Node view with experimental endpoints enabled
|
||||||
:width: 1024
|
:width: 1024
|
||||||
:height: 700
|
:height: 700
|
||||||
@@ -178,18 +256,3 @@ With experimental endpoints
|
|||||||
:width: 1024
|
:width: 1024
|
||||||
:height: 700
|
:height: 700
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
Error page
|
|
||||||
----------
|
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/no-experimental.png
|
|
||||||
:alt: Accessing disabled experimental feature
|
|
||||||
:width: 1024
|
|
||||||
:height: 700
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/broken.png
|
|
||||||
:alt: Error message
|
|
||||||
:width: 1024
|
|
||||||
:height: 700
|
|
||||||
:align: center
|
|
||||||
|
|||||||
3
dev.py
@@ -2,7 +2,8 @@ from __future__ import unicode_literals
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
from puppetboard.app import app
|
from puppetboard.app import app
|
||||||
|
from puppetboard.default_settings import DEV_LISTEN_HOST, DEV_LISTEN_PORT
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.debug=True
|
app.debug=True
|
||||||
app.run('127.0.0.1')
|
app.run(DEV_LISTEN_HOST, DEV_LISTEN_PORT)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import absolute_import
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import collections
|
import collections
|
||||||
|
import urllib
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask, render_template, abort, url_for,
|
Flask, render_template, abort, url_for,
|
||||||
@@ -11,7 +12,6 @@ from flask import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from pypuppetdb import connect
|
from pypuppetdb import connect
|
||||||
from pypuppetdb.errors import ExperimentalDisabledError
|
|
||||||
|
|
||||||
from puppetboard.forms import QueryForm
|
from puppetboard.forms import QueryForm
|
||||||
from puppetboard.utils import (
|
from puppetboard.utils import (
|
||||||
@@ -26,13 +26,13 @@ app.config.from_envvar('PUPPETBOARD_SETTINGS', silent=True)
|
|||||||
app.secret_key = os.urandom(24)
|
app.secret_key = os.urandom(24)
|
||||||
|
|
||||||
puppetdb = connect(
|
puppetdb = connect(
|
||||||
|
api_version=app.config['PUPPETDB_API'],
|
||||||
host=app.config['PUPPETDB_HOST'],
|
host=app.config['PUPPETDB_HOST'],
|
||||||
port=app.config['PUPPETDB_PORT'],
|
port=app.config['PUPPETDB_PORT'],
|
||||||
ssl=app.config['PUPPETDB_SSL'],
|
ssl=app.config['PUPPETDB_SSL'],
|
||||||
ssl_key=app.config['PUPPETDB_KEY'],
|
ssl_key=app.config['PUPPETDB_KEY'],
|
||||||
ssl_cert=app.config['PUPPETDB_CERT'],
|
ssl_cert=app.config['PUPPETDB_CERT'],
|
||||||
timeout=app.config['PUPPETDB_TIMEOUT'],
|
timeout=app.config['PUPPETDB_TIMEOUT'],)
|
||||||
experimental=app.config['PUPPETDB_EXPERIMENTAL'])
|
|
||||||
|
|
||||||
numeric_level = getattr(logging, app.config['LOGLEVEL'].upper(), None)
|
numeric_level = getattr(logging, app.config['LOGLEVEL'].upper(), None)
|
||||||
if not isinstance(numeric_level, int):
|
if not isinstance(numeric_level, int):
|
||||||
@@ -51,13 +51,18 @@ def stream_template(template_name, **context):
|
|||||||
def bad_request(e):
|
def bad_request(e):
|
||||||
return render_template('400.html'), 400
|
return render_template('400.html'), 400
|
||||||
|
|
||||||
|
@app.errorhandler(403)
|
||||||
|
def bad_request(e):
|
||||||
|
return render_template('403.html'), 400
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(e):
|
def not_found(e):
|
||||||
return render_template('404.html'), 404
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
@app.errorhandler(412)
|
@app.errorhandler(412)
|
||||||
def precond_failed(e):
|
def precond_failed(e):
|
||||||
"""We're slightly abusing 412 to handle ExperimentalDisabled errors."""
|
"""We're slightly abusing 412 to handle missing features
|
||||||
|
depending on the API version."""
|
||||||
return render_template('412.html'), 412
|
return render_template('412.html'), 412
|
||||||
|
|
||||||
@app.errorhandler(500)
|
@app.errorhandler(500)
|
||||||
@@ -83,9 +88,9 @@ def index():
|
|||||||
metrics = {
|
metrics = {
|
||||||
'num_nodes': num_nodes['Value'],
|
'num_nodes': num_nodes['Value'],
|
||||||
'num_resources': num_resources['Value'],
|
'num_resources': num_resources['Value'],
|
||||||
'avg_resources_node': "{:10.6f}".format(avg_resources_node['Value']),
|
'avg_resources_node': "{0:10.6f}".format(avg_resources_node['Value']),
|
||||||
'mean_failed_commands': mean_failed_commands['MeanRate'],
|
'mean_failed_commands': mean_failed_commands['MeanRate'],
|
||||||
'mean_command_time': "{: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)
|
return render_template('index.html', metrics=metrics)
|
||||||
|
|
||||||
@@ -111,7 +116,7 @@ def node(node_name):
|
|||||||
"""
|
"""
|
||||||
node = get_or_abort(puppetdb.node, node_name)
|
node = get_or_abort(puppetdb.node, node_name)
|
||||||
facts = node.facts()
|
facts = node.facts()
|
||||||
if app.config['PUPPETDB_EXPERIMENTAL']:
|
if app.config['PUPPETDB_API'] > 2:
|
||||||
reports = ten_reports(node.reports())
|
reports = ten_reports(node.reports())
|
||||||
else:
|
else:
|
||||||
reports = iter([])
|
reports = iter([])
|
||||||
@@ -122,21 +127,21 @@ def node(node_name):
|
|||||||
def reports():
|
def reports():
|
||||||
"""Doesn't do much yet but is meant to show something like the reports of
|
"""Doesn't do much yet but is meant to show something like the reports of
|
||||||
the last half our, something like that."""
|
the last half our, something like that."""
|
||||||
if app.config['PUPPETDB_EXPERIMENTAL']:
|
if app.config['PUPPETDB_API'] > 2:
|
||||||
return render_template('reports.html')
|
return render_template('reports.html')
|
||||||
else:
|
else:
|
||||||
log.warn('Access to experimental endpoint not allowed.')
|
log.warn('PuppetDB API prior to v3 cannot access reports.')
|
||||||
abort(412)
|
abort(412)
|
||||||
|
|
||||||
@app.route('/reports/<node>')
|
@app.route('/reports/<node>')
|
||||||
def reports_node(node):
|
def reports_node(node):
|
||||||
"""Fetches all reports for a node and processes them eventually rendering
|
"""Fetches all reports for a node and processes them eventually rendering
|
||||||
a table displaying those reports."""
|
a table displaying those reports."""
|
||||||
if app.config['PUPPETDB_EXPERIMENTAL']:
|
if app.config['PUPPETDB_API'] > 2:
|
||||||
reports = ten_reports(yield_or_stop(
|
reports = ten_reports(yield_or_stop(
|
||||||
puppetdb.reports('["=", "certname", "{0}"]'.format(node))))
|
puppetdb.reports('["=", "certname", "{0}"]'.format(node))))
|
||||||
else:
|
else:
|
||||||
log.warn('Access to experimental endpoint not allowed.')
|
log.warn('PuppetDB API prior to v3 cannot access reports.')
|
||||||
abort(412)
|
abort(412)
|
||||||
return render_template('reports_node.html', reports=reports,
|
return render_template('reports_node.html', reports=reports,
|
||||||
nodename=node)
|
nodename=node)
|
||||||
@@ -145,10 +150,10 @@ def reports_node(node):
|
|||||||
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_EXPERIMENTAL']:
|
if app.config['PUPPETDB_API'] > 2:
|
||||||
reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node))
|
reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node))
|
||||||
else:
|
else:
|
||||||
log.warn('Access to experimental endpoint not allowed.')
|
log.warn('PuppetDB API prior to v3 cannot access reports.')
|
||||||
abort(412)
|
abort(412)
|
||||||
|
|
||||||
for report in reports:
|
for report in reports:
|
||||||
@@ -179,9 +184,12 @@ def facts():
|
|||||||
def fact(fact):
|
def fact(fact):
|
||||||
"""Fetches the specific fact from PuppetDB and displays its value per
|
"""Fetches the specific fact from PuppetDB and displays its value per
|
||||||
node for which this fact is known."""
|
node for which this fact is known."""
|
||||||
|
# we can only consume the generator once, lists can be doubly consumed
|
||||||
|
# om nom nom
|
||||||
|
localfacts = [ f for f in yield_or_stop(puppetdb.facts(name=fact)) ]
|
||||||
return Response(stream_with_context(stream_template('fact.html',
|
return Response(stream_with_context(stream_template('fact.html',
|
||||||
name=fact,
|
name=fact,
|
||||||
facts=yield_or_stop(puppetdb.facts(name=fact)))))
|
facts=localfacts)))
|
||||||
|
|
||||||
@app.route('/query', methods=('GET', 'POST'))
|
@app.route('/query', methods=('GET', 'POST'))
|
||||||
def query():
|
def query():
|
||||||
@@ -190,9 +198,26 @@ def query():
|
|||||||
of the possible exceptions are being handled just yet. This will return
|
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 /
|
the JSON of the response or a message telling you what whent wrong /
|
||||||
why nothing was returned."""
|
why nothing was returned."""
|
||||||
|
if app.config['ENABLE_QUERY']:
|
||||||
form = QueryForm()
|
form = QueryForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
result = get_or_abort(puppetdb._query, form.endpoints.data,
|
result = get_or_abort(puppetdb._query, form.endpoints.data,
|
||||||
query='[{0}]'.format(form.query.data))
|
query='[{0}]'.format(form.query.data))
|
||||||
return render_template('query.html', form=form, result=result)
|
return render_template('query.html', form=form, result=result)
|
||||||
return render_template('query.html', form=form)
|
return render_template('query.html', form=form)
|
||||||
|
else:
|
||||||
|
log.warn('Access to query interface disabled by administrator..')
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
@app.route('/metrics')
|
||||||
|
def metrics():
|
||||||
|
metrics = puppetdb._query('metrics', path='mbeans')
|
||||||
|
for key,value in metrics.iteritems():
|
||||||
|
metrics[key]=value.split('/')[3]
|
||||||
|
return render_template('metrics.html', metrics=sorted(metrics.items()))
|
||||||
|
|
||||||
|
@app.route('/metric/<metric>')
|
||||||
|
def metric(metric):
|
||||||
|
name = urllib.unquote(metric)
|
||||||
|
metric = puppetdb.metric(metric)
|
||||||
|
return render_template('metric.html', name=name, metric=sorted(metric.items()))
|
||||||
|
|||||||
@@ -4,5 +4,8 @@ PUPPETDB_SSL=False
|
|||||||
PUPPETDB_KEY=None
|
PUPPETDB_KEY=None
|
||||||
PUPPETDB_CERT=None
|
PUPPETDB_CERT=None
|
||||||
PUPPETDB_TIMEOUT=20
|
PUPPETDB_TIMEOUT=20
|
||||||
PUPPETDB_EXPERIMENTAL=False
|
PUPPETDB_API=3
|
||||||
|
DEV_LISTEN_HOST='127.0.0.1'
|
||||||
|
DEV_LISTEN_PORT=5000
|
||||||
|
ENABLE_QUERY=True
|
||||||
LOGLEVEL='info'
|
LOGLEVEL='info'
|
||||||
|
|||||||
1770
puppetboard/static/js/Chart.js
vendored
Normal file
11
puppetboard/templates/403.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>Permission Denied</h2>
|
||||||
|
<p>What you were looking for has been disabled by the administrator.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
<div class="container" style="margin-bottom:55px;">
|
<div class="container" style="margin-bottom:55px;">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="span12">
|
<div class="span12">
|
||||||
<h2>Experimental Disabled</h2>
|
<h2>Feature unavailable</h2>
|
||||||
<p>You're trying to access a feature restricted to PuppetDB's Experimental API but haven't configured Puppetboard to allow this.</p>
|
<p>You've configured Puppetboard with an API version that does not support this feature.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
<table class="filter-table table table-striped {% if condensed %}table-condensed{% endif%}" style="table-layout:fixed">
|
<table class="filter-table table table-striped {% if condensed %}table-condensed{% endif%}" style="table-layout:fixed">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
{% if show_node %}
|
||||||
|
<th>Node</th>
|
||||||
|
{% else %}
|
||||||
<th>Fact</th>
|
<th>Fact</th>
|
||||||
|
{% endif %}
|
||||||
<th>Value</th>
|
<th>Value</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -13,9 +17,9 @@
|
|||||||
{% for fact in facts %}
|
{% for fact in facts %}
|
||||||
<tr>
|
<tr>
|
||||||
{% if show_node %}
|
{% if show_node %}
|
||||||
<td>{{fact.node}}</td>
|
<td><a href="{{url_for('node', node_name=fact.node)}}">{{fact.node}}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>{{fact.name}}</td>
|
<td><a href="{{url_for('fact', fact=fact.name)}}">{{fact.name}}</a></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td style="word-wrap:break-word">{{fact.value}}</td>
|
<td style="word-wrap:break-word">{{fact.value}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -23,6 +27,63 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
{% macro facts_graph(facts, autofocus=False, condensed=False, show_node=False, margin_top=20, margin_bottom=20) -%}
|
||||||
|
<script src="{{url_for('static', filename='js/Chart.js')}}"></script>
|
||||||
|
<canvas id="factChart" width="300" height="300"></canvas>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var colors = ["#9B59B6", "#3498DB", "#2ECC71", "#1ABC9C", "#F1C40F", "#E67E22", "#E74C3C"];
|
||||||
|
var len_colors = colors.length;
|
||||||
|
var data = [
|
||||||
|
{% for fact in facts|groupby('value') %}
|
||||||
|
{
|
||||||
|
label: "{{ fact.grouper }}",
|
||||||
|
value: {{ fact.list|length }}
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
data[i].color = colors[i % len_colors];
|
||||||
|
}
|
||||||
|
var sorted_data = data.sort(function(a,b) { return parseFloat(b.value) - parseFloat(a.value)});
|
||||||
|
var top7 = sorted_data.slice(0,7);
|
||||||
|
var bottom = data.slice(7, -1);
|
||||||
|
var bottom_sum = 0;
|
||||||
|
for (var i = 0; i < bottom.length; i++) {
|
||||||
|
bottom_sum += bottom[i].value;
|
||||||
|
}
|
||||||
|
top7.push({ label: "Other", value: bottom_sum, color: "#B30202" });
|
||||||
|
var ctx = document.getElementById("factChart").getContext("2d");
|
||||||
|
new Chart(ctx).Pie(top7);
|
||||||
|
</script>
|
||||||
|
{%- endmacro %}
|
||||||
|
{% macro facts_graph_value(facts, autofocus=False, condensed=False, show_node=False, margin_top=20, margin_bottom=20) -%}
|
||||||
|
<script src="{{url_for('static', filename='js/Chart.js')}}"></script>
|
||||||
|
<canvas id="factChart_value" width="300" height="300"></canvas>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var colors = ["#9B59B6", "#3498DB", "#2ECC71", "#1ABC9C", "#F1C40F", "#E67E22", "#E74C3C"];
|
||||||
|
var len_colors = colors.length;
|
||||||
|
var data = [
|
||||||
|
{% for fact in facts|groupby('value') %}
|
||||||
|
{
|
||||||
|
label: "{{ fact.grouper }}",
|
||||||
|
value: {{ fact.list|length }}
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
data[i].color = colors[i % len_colors];
|
||||||
|
}
|
||||||
|
var ctx = document.getElementById("factChart_value").getContext("2d");
|
||||||
|
new Chart(ctx).Pie(data.sort(function(a,b) { return parseInt(a.label) - parseInt(b.label)}));
|
||||||
|
</script>
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro reports_table(reports, nodename, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True) -%}
|
{% 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">
|
<div class="alert alert-info">
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
{% import '_macros.html' as macros %}
|
{% import '_macros.html' as macros %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{{name}}</h1>
|
<h1>{{name}}</h1>
|
||||||
|
{{macros.facts_graph(facts, autofocus=True, show_node=True, margin_bottom=10)}}
|
||||||
|
{{macros.facts_graph_value(facts, autofocus=True, show_node=True, margin_bottom=10)}}
|
||||||
{{macros.facts_table(facts, autofocus=True, show_node=True, margin_bottom=10)}}
|
{{macros.facts_table(facts, autofocus=True, show_node=True, margin_bottom=10)}}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
('nodes', 'Nodes'),
|
('nodes', 'Nodes'),
|
||||||
('facts', 'Facts'),
|
('facts', 'Facts'),
|
||||||
('reports', 'Reports'),
|
('reports', 'Reports'),
|
||||||
|
('metrics', 'Metrics'),
|
||||||
('query', 'Query'),
|
('query', 'Query'),
|
||||||
] %}
|
] %}
|
||||||
<li{% if endpoint == request.endpoint %} class=active{% endif
|
<li{% if endpoint == request.endpoint %} class=active{% endif
|
||||||
|
|||||||
18
puppetboard/templates/metric.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Metric
|
||||||
|
<small>{{name}}</small>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tbody>
|
||||||
|
{% for key,value in metric %}
|
||||||
|
<tr>
|
||||||
|
<td>{{key}}</td>
|
||||||
|
<td>{{value}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock content %}
|
||||||
9
puppetboard/templates/metrics.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Metrics</h1>
|
||||||
|
<ul>
|
||||||
|
{% for key,value in metrics %}
|
||||||
|
<li><a href="{{url_for('metric', metric=value)}}">{{key}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock content %}
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if config.PUPPETDB_EXPERIMENTAL %}
|
{% if config.PUPPETDB_API > 2 %}
|
||||||
<div class="span4">
|
<div class="span4">
|
||||||
<h1>Facts</h1>
|
<h1>Facts</h1>
|
||||||
{{macros.facts_table(facts, condensed=True, margin_top=10)}}
|
{{macros.facts_table(facts, condensed=True, margin_top=10)}}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Hostname</th>
|
<th>Hostname</th>
|
||||||
<th>Catalog compiled at</th>
|
<th>Catalog compiled at</th>
|
||||||
{% if config.PUPPETDB_EXPERIMENTAL %}
|
{% if config.PUPPETDB_API > 2 %}
|
||||||
<th>Last report</th>
|
<th>Last report</th>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<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_EXPERIMENTAL %}
|
{% if config.PUPPETDB_API > 2 %}
|
||||||
<td>
|
<td>
|
||||||
{% if node.report_timestamp %}
|
{% if node.report_timestamp %}
|
||||||
<span rel="utctimestamp">{{ node.report_timestamp }}</span>
|
<span rel="utctimestamp">{{ node.report_timestamp }}</span>
|
||||||
@@ -40,7 +40,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
{% if node.report_timestamp %}
|
||||||
<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 %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% block row_fluid %}
|
{% block row_fluid %}
|
||||||
<div class="span12">
|
<div class="span12">
|
||||||
<div class="alert">
|
<div class="alert">
|
||||||
This is highly exeprimental and will likely set your server on fire.
|
This is highly experimental and will likely set your server on fire.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" style="margin-bottom:55px;">
|
<div class="container" style="margin-bottom:55px;">
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="control-group {% if form.query.errors %} error {% endif %}">
|
<div class="control-group {% if form.query.errors %} error {% endif %}">
|
||||||
{{form.query.label(class_="control-label")}}
|
{{form.query.label(class_="control-label")}}
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
{{form.query(class_="input-block-level", autofocus="autofocus", rows=5, placeholder="\"=\", \"name\" \"hostname\"")}}
|
{{form.query(class_="input-block-level", autofocus="autofocus", rows=5, placeholder="\"=\", \"name\", \"hostname\"")}}
|
||||||
{% if form.query.errors %}
|
{% if form.query.errors %}
|
||||||
<span class="help-inline">{% for error in form.query.errors %}{{error}}{% endfor %}</span>
|
<span class="help-inline">{% for error in form.query.errors %}{{error}}{% endfor %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ from __future__ import absolute_import
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from requests.exceptions import HTTPError, ConnectionError
|
from requests.exceptions import HTTPError, ConnectionError
|
||||||
from pypuppetdb.errors import EmptyResponseError, ExperimentalDisabledError
|
from pypuppetdb.errors import EmptyResponseError
|
||||||
|
|
||||||
from flask import abort, flash
|
from flask import abort
|
||||||
|
|
||||||
def get_or_abort(func, *args, **kwargs):
|
def get_or_abort(func, *args, **kwargs):
|
||||||
"""Execute the function with its arguments and handle the possible
|
"""Execute the function with its arguments and handle the possible
|
||||||
@@ -18,8 +18,6 @@ def get_or_abort(func, *args, **kwargs):
|
|||||||
abort(e.response.status_code)
|
abort(e.response.status_code)
|
||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
abort(500)
|
abort(500)
|
||||||
except ExperimentalDisabledError:
|
|
||||||
abort(412)
|
|
||||||
except EmptyResponseError:
|
except EmptyResponseError:
|
||||||
abort(204)
|
abort(204)
|
||||||
|
|
||||||
@@ -40,22 +38,12 @@ def yield_or_stop(generator):
|
|||||||
generators and handle certain errors.
|
generators and handle certain errors.
|
||||||
|
|
||||||
Since this is also used in streaming responses where we can't just abort
|
Since this is also used in streaming responses where we can't just abort
|
||||||
a request we always yield empty and then raise StopIteration.
|
a request we raise StopIteration.
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
yield next(generator)
|
yield next(generator)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
raise
|
raise
|
||||||
except ExperimentalDisabledError:
|
except (EmptyResponseError, ConnectionError, HTTPError):
|
||||||
yield
|
|
||||||
raise StopIteration
|
|
||||||
except EmptyResponseError:
|
|
||||||
yield
|
|
||||||
raise StopIteration
|
|
||||||
except ConnectionError:
|
|
||||||
yield
|
|
||||||
raise StopIteration
|
|
||||||
except HTTPError:
|
|
||||||
yield
|
|
||||||
raise StopIteration
|
raise StopIteration
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
Flask==0.10.1
|
Flask==0.10.1
|
||||||
Flask-WTF==0.8.4
|
Flask-WTF==0.8.4
|
||||||
pypuppetdb=0.0.1
|
Jinja2==2.7
|
||||||
|
MarkupSafe==0.18
|
||||||
|
WTForms==1.0.4
|
||||||
|
Werkzeug==0.9.3
|
||||||
|
itsdangerous==0.22
|
||||||
|
pypuppetdb==0.0.4
|
||||||
|
requests==1.2.3
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 170 KiB |
BIN
screenshots/metric.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
screenshots/metrics.png
Normal file
|
After Width: | Height: | Size: 367 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 276 KiB |