Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d90e22397b | ||
|
|
c6b194ca83 | ||
|
|
59ae9657ff | ||
|
|
d92a068057 | ||
|
|
cbb3b8640f | ||
|
|
9b7c33dcff | ||
|
|
b01a749bab | ||
|
|
5fa260e748 | ||
|
|
eadcf8708c | ||
|
|
e57437e705 | ||
|
|
bf86b1780d | ||
|
|
5bed3d503f | ||
|
|
52b689d174 | ||
|
|
c6bba09beb | ||
|
|
73e26e8c1c | ||
|
|
08bad89041 | ||
|
|
dcf8abefe9 | ||
|
|
dd0e8d8eb0 | ||
|
|
5e9f4b5526 | ||
|
|
ac06c65d73 | ||
|
|
e55e43ed6a | ||
|
|
71c3f809ca | ||
|
|
b728896fea | ||
|
|
398156b0ae | ||
|
|
eb1bf7c3ab | ||
|
|
0992763d9d | ||
|
|
fb763e637f | ||
|
|
d067fe3ed3 | ||
|
|
efe488aafc | ||
|
|
79ac5b3cb0 | ||
|
|
754784f4af | ||
|
|
0563224c87 | ||
|
|
3efdb58ce3 | ||
|
|
de6a77951c | ||
|
|
e753fc444a | ||
|
|
a1f00a7b66 | ||
|
|
b3d08233f3 | ||
|
|
ffdbfcda24 | ||
|
|
f187638b6e | ||
|
|
a84da91f06 | ||
|
|
09b249b0ca | ||
|
|
269aeeec57 | ||
|
|
3cbb21ac60 | ||
|
|
49afb9ed34 | ||
|
|
7265dc2fd0 | ||
|
|
f2e7ecc67e | ||
|
|
61548c819c | ||
|
|
ee2775512d | ||
|
|
75da9b9209 | ||
|
|
029b50405b | ||
|
|
795d243e9d | ||
|
|
c0cef0a3c0 | ||
|
|
58625b5ee0 | ||
|
|
a4dc1f694e | ||
|
|
bbb65939c9 | ||
|
|
5ca758dd39 | ||
|
|
c7bae2efa3 | ||
|
|
7c027dd97d | ||
|
|
efae19dc6d | ||
|
|
bb124e1ba5 | ||
|
|
89117ce844 | ||
|
|
b5fde343ed |
@@ -2,6 +2,42 @@
|
||||
Changelog
|
||||
#########
|
||||
|
||||
This is the changelog for Puppetboard.
|
||||
|
||||
0.0.3
|
||||
=====
|
||||
This release introduces a few big changes. The most obvious one is the
|
||||
revamped Overview page which has received significant love. Most of the work
|
||||
was done by Julius Härtl. The Nodes tab has been given a slight face-lift
|
||||
too.
|
||||
|
||||
Other changes:
|
||||
|
||||
* This release depends on the new pypuppetdb 0.1.0. Because of this the SSL
|
||||
configuration options have been changed:
|
||||
|
||||
* ``PUPPETDB_SSL`` is gone and replaced by ``PUPPETDB_SSL_VERIFY`` which
|
||||
now defaults to ``True``. This only affects connections to PuppetDB that
|
||||
happen over SSL.
|
||||
* SSL is automatically enabled if both ``PUPPETDB_CERT`` and
|
||||
``PUPPETDB_KEY`` are provided.
|
||||
|
||||
* Display of deeply nested metrics and query results have been fixed.
|
||||
* Average resources per node metric is now displayed as a natural number.
|
||||
* A link back to the node has been added to the reports.
|
||||
* A few issues with reports have been fixed.
|
||||
* A new setting called ``UNRESPONSIVE_HOURS`` has been added which denotes
|
||||
the amount of hours after which Puppetboard will display the node as
|
||||
unreported if it hasn't checked in. We default to ``2`` hours.
|
||||
* The event message can now be viewed by clicking on the event.
|
||||
|
||||
Puppetboard is now neatly packaged up and available on PyPi. This should
|
||||
significantly help reduce the convoluted installation instructions people had
|
||||
to follow.
|
||||
|
||||
Updated installation instructions have been added on how to install from PyPi
|
||||
and how to configure your HTTPD.
|
||||
|
||||
0.0.2
|
||||
=====
|
||||
In this release we've introduced a few new things. First of all we now require
|
||||
|
||||
5
MANIFEST.in
Normal file
@@ -0,0 +1,5 @@
|
||||
include README.rst
|
||||
include CHANGELOG.rst
|
||||
include LICENSE
|
||||
recursive-include puppetboard/static *.css *.js
|
||||
recursive-include puppetboard/templates *.html
|
||||
418
README.rst
@@ -17,7 +17,7 @@ Because this project is powered by Flask we are restricted to:
|
||||
* Python 2.6
|
||||
* Python 2.7
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/node-v3.png
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/overview.png
|
||||
:alt: View of a node
|
||||
:width: 1024
|
||||
:height: 700
|
||||
@@ -40,88 +40,357 @@ this might throw at you.
|
||||
Installation
|
||||
============
|
||||
|
||||
Currently you can only run from source:
|
||||
Puppetboard is now packaged and available on PyPi.
|
||||
|
||||
Production
|
||||
----------
|
||||
|
||||
To install it simply issue the following command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git clone https://github.com/nedap/puppetboard
|
||||
$ pip install -r requirements.txt
|
||||
$ pip install puppetboard
|
||||
|
||||
This will install all the requirements for Puppetboard.
|
||||
This will install Puppetboard and take care of the dependencies. If you
|
||||
do this Puppetboard will be installed in the so called site-packages or
|
||||
dist-packages of your Python distribution.
|
||||
|
||||
Run it
|
||||
======
|
||||
The complete path on Debian systems would be:
|
||||
``/usr/local/lib/python2.X/lib/dist-packages/puppetboard``.
|
||||
|
||||
You will need this path in order to configure your HTTPD and WSGI-capable
|
||||
application server.
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
||||
You can run in it in development mode by simple executing:
|
||||
If you wish to hack on Puppetboard you should fork/clone the Github repository
|
||||
and then install the requirements through:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install -r requirements.txt
|
||||
|
||||
You're advised to do this inside a virtualenv specifically created to work on
|
||||
Puppetboard as to not pollute your global Python installation.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
The following instructions will help you configure Puppetboard and your HTTPD.
|
||||
|
||||
Settings
|
||||
--------
|
||||
Puppetboard will look for a file pointed at by the ``PUPPETBOARD_SETTINGS``
|
||||
environment variable. The file has to be identical to ``default_settings.py``
|
||||
but should only override the settings you need changed.
|
||||
|
||||
You can grab a copy of ``default_settings.py`` from the path where pip
|
||||
installed Puppetboard to or by looking in the source checkout.
|
||||
|
||||
If you run PuppetDB and Puppetboard on the same machine the default settings
|
||||
provided will be enough to get you started and you won't need a custom
|
||||
settings file.
|
||||
|
||||
Assuming your webserver and PuppetDB machine are not identical you will at
|
||||
least have to change the following settings:
|
||||
|
||||
* ``PUPPETDB_HOST``
|
||||
* ``PUPPETDB_PORT``
|
||||
|
||||
By default PuppetDB requires SSL to be used when a non-local client wants to
|
||||
connect. Therefor you'll also have to supply the following settings:
|
||||
|
||||
* ``PUPPETDB_KEY = /path/to/private/keyfile.pem``
|
||||
* ``PUPPETDB_CERT = /path/to/public/keyfile.crt``
|
||||
|
||||
For information about how to generate the correct keys please refer to the
|
||||
`pypuppetdb documentation`_.
|
||||
|
||||
Other settings that might be interesting:
|
||||
|
||||
* ``PUPPETDB_TIMEOUT``: Defaults to 20 seconds but you might need to increase
|
||||
this value. It depends on how big the results are when querying PuppetDB.
|
||||
This behaviour will change in a future release when pagination will be
|
||||
introduced.
|
||||
* ``UNRESPONSIVE_HOURS``: The amount of hours since the last check-in after
|
||||
which a node is considered unresponsive.
|
||||
* ``LOGLEVEL``: A string representing the loglevel. It defaults to ``'info'``
|
||||
but can be changed to ``'warning'`` or ``'critical'`` for less verbose
|
||||
logging or ``'debug'`` for more information.
|
||||
* ``ENABLE_QUERY``: Defaults to ``True`` causing a Query tab to show up in the
|
||||
web interface allowing users to write and execute arbitrary queries against
|
||||
a set of endpoints in PuppetDB. Change this to ``False`` to disable this.
|
||||
|
||||
.. _pypuppetdb documentation: http://pypuppetdb.readthedocs.org/en/v0.1.0/quickstart.html#ssl
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
||||
You can run it in development mode by simply executing:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ python dev.py
|
||||
|
||||
Use ``PUPPETBOARD_SETTINGS`` to change the different settings or patch
|
||||
``default_settings.py`` directly. Take care not to include your local
|
||||
changes on that file when submitting patches for Puppetboard.
|
||||
|
||||
Production
|
||||
----------
|
||||
For WSGI capable webservers a ``wsgi.py`` is provided which ``mod_wsgi``
|
||||
and ``uwsgi`` can deal with.
|
||||
To run Puppetboard in production we provide instructions for the following
|
||||
scenarios:
|
||||
|
||||
* Apache mod_wsgi configuration: http://flask.pocoo.org/docs/deploying/mod_wsgi/
|
||||
* uwsgi configuration: ``uwsgi --http :9090 --wsgi-file /path/to/puppetboard/wsgi.py``
|
||||
* Passenger
|
||||
* Apache + mod_wsgi
|
||||
* Apache + mod_passenger
|
||||
* uwsgi + nginx
|
||||
|
||||
In the case of uwsgi you'll of course need something like nginx in front of it to
|
||||
proxy the requests to it.
|
||||
If you deploy Puppetboard through a different setup we'd welcome a pull
|
||||
request that adds the instructions to this section.
|
||||
|
||||
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
|
||||
Javascript and CSS this application has so no one has to bother with that).
|
||||
Apache + mod_wsgi
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Passenger
|
||||
^^^^^^^^^
|
||||
From within the Puppetboard checkout:
|
||||
First we need to create the necessary directories:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
mkdir public
|
||||
mkdir tmp
|
||||
ln -s wsgi.py passenger_wsgi.py
|
||||
$ mkdir -p /var/www/puppetboard
|
||||
$ chown www-data:www-data /var/www/puppetboard
|
||||
|
||||
The apache vhost configuration:
|
||||
Copy Puppetboard's ``default_settings.py`` to the newly created puppetboard
|
||||
directory and name the file ``settings.py``. This file will be available
|
||||
at the path Puppetboard was installed, for example:
|
||||
``/usr/local/lib/python2.X/lib/dist-packages/puppetboard/default_settings.py``.
|
||||
|
||||
Change the settings that need changing to match your environment and delete
|
||||
or comment with a ``#`` the rest of the entries.
|
||||
|
||||
If you don't need to change any settings you can skip the creation of the
|
||||
``settings.py`` file entirely.
|
||||
|
||||
Now create a ``wsgi.py`` with the following content in the newly created
|
||||
puppetboard directory:
|
||||
|
||||
.. code-block::
|
||||
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
|
||||
# Needed if a settings.py file exists
|
||||
os.environ['PUPPETBOARD_SETTINGS'] = '/var/www/puppetboard/settings.py'
|
||||
from puppetboard.app import app as application
|
||||
|
||||
Make sure this file is owned by the user and group the webserver runs as.
|
||||
|
||||
The last thing we need to do is configure Apache:
|
||||
|
||||
.. code-block::
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName puppetboard.example.tld
|
||||
WSGIDaemonProcess puppetboard user=www-data group=www-data threads=5
|
||||
WSGIScriptAlias / /var/www/puppetboard/wsgi.py
|
||||
ErrorLog /var/log/apache2/puppetboard.error.log
|
||||
CustomLog /var/log/apache2/puppetboard.access.log combined
|
||||
|
||||
Alias /static /usr/local/lib/python2.X/dist-packages/puppetboard/static
|
||||
|
||||
<Directory /usr/local/lib/python2.X/dist-packages/puppetboard>
|
||||
WSGIProcessGroup puppetboard
|
||||
WSGIApplicationGroup %{GLOBAL}
|
||||
Order deny,allow
|
||||
Allow from all
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
|
||||
Note the directory path, it's the path to where pip installed Puppetboard. We
|
||||
also alias the ``/static`` path so that Apache will serve the static files
|
||||
like the included CSS and Javascript.
|
||||
|
||||
Apache + mod_passenger
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
It is possible to run Python applications through Passenger. Passenger has
|
||||
supported this since version 3 but it's considered experimental. Since the
|
||||
release of Passenger 4 it's a 'core' feature of the product.
|
||||
|
||||
Performance wise it also leaves something to be desired compared to the
|
||||
mod_wsgi powered solution. Application start up is noticeably slower and
|
||||
loading pages takes a fraction longer.
|
||||
|
||||
First we need to create the necessary directories:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ mkdir -p /var/www/puppetboard/{tmp,public}
|
||||
$ chown -R www-data:www-data /var/www/puppetboard
|
||||
|
||||
Copy Puppetboard's ``default_settings.py`` to the newly created puppetboard
|
||||
directory and name the file ``settings.py``. This file will be available
|
||||
at the path Puppetboard was installed, for example:
|
||||
``/usr/local/lib/python2.X/lib/dist-packages/puppetboard/default_settings.py``.
|
||||
|
||||
Change the settings that need changing to match your environment and delete
|
||||
or comment with a ``#`` the rest of the entries.
|
||||
|
||||
If you don't need to change any settings you can skip the creation of the
|
||||
``settings.py`` file entirely.
|
||||
|
||||
Now create a ``passenger_wsgi.py`` with the following content in the newly
|
||||
created puppetboard directory:
|
||||
|
||||
.. code-block::
|
||||
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
import logging
|
||||
|
||||
logging.basicConfig(filename=/path/to/file/for/logging, level=logging.INFO)
|
||||
|
||||
# Needed if a settings.py file exists
|
||||
os.environ['PUPPETBOARD_SETTINGS'] = '/var/www/puppetboard/settings.py'
|
||||
|
||||
try:
|
||||
from puppetboard.app import app as application
|
||||
except Exception, inst:
|
||||
logging.exception("Error: %s", str(type(inst)))
|
||||
|
||||
Unfortunately due to the way Passenger works we also need to configure logging
|
||||
inside ``passenger_wsgi.py`` else application start up issues won't be logged.
|
||||
|
||||
This means that even though ``LOGLEVEL`` might be set in your ``settings.py``
|
||||
this setting will take precedence over it.
|
||||
|
||||
Now the only thing left to do is configure Apache:
|
||||
|
||||
.. code-block::
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName puppetboard.example.tld
|
||||
DocumentRoot /path/to/puppetboard/public
|
||||
DocumentRoot /var/www/puppetboard/public
|
||||
ErrorLog /var/log/apache2/puppetboard.error.log
|
||||
CustomLog /var/log/apache2/puppetboard.access.log combined
|
||||
|
||||
RackAutoDetect On
|
||||
Alias /static /path/to/puppetboard/static
|
||||
<Directory /path/to/puppetboard/>
|
||||
Options None
|
||||
Order allow,deny
|
||||
allow from all
|
||||
</Directory>
|
||||
Alias /static /usr/local/lib/python2.X/dist-packages/puppetboard/static
|
||||
</VirtualHost>
|
||||
|
||||
Configuration
|
||||
=============
|
||||
Note the ``/static`` alias path, it's the path to where pip installed
|
||||
Puppetboard. This is needed so that Apache will serve the static files like
|
||||
the included CSS and Javascript.
|
||||
|
||||
Puppetboard has some configuration settings, their defaults can
|
||||
be viewed in ``puppetboard/default_settings.py``.
|
||||
nginx + uwsgi
|
||||
^^^^^^^^^^^^^
|
||||
A common Python deployment scenario is to use the uwsgi application server
|
||||
(which can also serve rails/rack, PHP, Perl and other applications) and proxy
|
||||
to it through something like nginx or perhaps even HAProxy.
|
||||
|
||||
Additionally Puppetboard will look for an environment variable
|
||||
called ``PUPPETBOARD_SETTINGS`` pointing to a file with identical
|
||||
markup as ``default_settings.py``. Any setting defined in
|
||||
``PUPPETBOARD_SETTINGS`` will override the defaults.
|
||||
uwsgi has a feature that every instance can run as its own user. In this
|
||||
example we'll use the ``www-data`` user but you can create a separate user
|
||||
solely for running Puppetboard and use that instead.
|
||||
|
||||
Experimental
|
||||
------------
|
||||
Pypuppetdb and Puppetboard can query and display information from
|
||||
PuppetDB's experimental API endpoints.
|
||||
First we need to create the necessary directories:
|
||||
|
||||
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.
|
||||
.. code-block:: bash
|
||||
|
||||
$ mkdir -p /var/www/puppetboard
|
||||
$ chown www-data:www-data /var/www/puppetboard
|
||||
|
||||
Copy Puppetboard's ``default_settings.py`` to the newly created puppetboard
|
||||
directory and name the file ``settings.py``. This file will be available
|
||||
at the path Puppetboard was installed, for example:
|
||||
``/usr/local/lib/python2.X/lib/dist-packages/puppetboard/default_settings.py``.
|
||||
|
||||
Change the settings that need changing to match your environment and delete
|
||||
or comment with a ``#`` the rest of the entries.
|
||||
|
||||
If you don't need to change any settings you can skip the creation of the
|
||||
``settings.py`` file entirely.
|
||||
|
||||
Now create a ``wsgi.py`` with the following content in the newly created
|
||||
puppetboard directory:
|
||||
|
||||
.. code-block::
|
||||
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
|
||||
# Needed if a settings.py file exists
|
||||
os.environ['PUPPETBOARD_SETTINGS'] = '/var/www/puppetboard/settings.py'
|
||||
from puppetboard.app import app as application
|
||||
|
||||
Make sure this file is owned by the user and group the uwsgi instance will run
|
||||
as.
|
||||
|
||||
Now we need to start uwsgi:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ uwsgi --http :9090 --wsgi-file /var/www/puppetboard/wsgi.py
|
||||
|
||||
Feel free to change the port to something other than ``9090``.
|
||||
|
||||
The last thing we need to do is configure nginx to proxy the requests:
|
||||
|
||||
.. code-block::
|
||||
|
||||
upstream puppetboard {
|
||||
server 127.0.0.1:9090;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name puppetboard.example.tld;
|
||||
charset utf-8;
|
||||
|
||||
location /static {
|
||||
alias /usr/local/lib/python2.X/dist-packages/puppetboard/static;
|
||||
}
|
||||
|
||||
location / {
|
||||
uwsgi_pass puppetboard;
|
||||
include /path/to/uwsgi_params/probably/etc/nginx/uwsgi_params;
|
||||
}
|
||||
}
|
||||
|
||||
If all went well you should now be able to access to Puppetboard. Note the
|
||||
``/static`` location block to make nginx serve static files like the included
|
||||
CSS and Javascript.
|
||||
|
||||
Because nginx natively supports the uwsgi protocol we use ``uwsgi_pass``
|
||||
instead of the traditional ``proxy_pass``.
|
||||
|
||||
Security
|
||||
--------
|
||||
|
||||
If you wish to make users authenticate before getting access to Puppetboard
|
||||
you can use one of the following configuration snippets.
|
||||
|
||||
Apache
|
||||
^^^^^^
|
||||
|
||||
Inside the ``VirtualHost``:
|
||||
|
||||
.. code-block::
|
||||
|
||||
<Location "/">
|
||||
AuthType Basic
|
||||
AuthName "Puppetboard"
|
||||
Require valid-user
|
||||
AuthBasicProvider file
|
||||
AuthUserFile /path/to/a/file.htpasswd
|
||||
</Location>
|
||||
|
||||
nginx
|
||||
^^^^^
|
||||
|
||||
Inside the ``location / {}`` block that has the ``uwsgi_pass`` directive:
|
||||
|
||||
.. code-block::
|
||||
|
||||
auth_basic "Puppetboard";
|
||||
auth_basic_user_file /path/to/a/file.htpasswd;
|
||||
|
||||
Getting Help
|
||||
============
|
||||
@@ -194,8 +463,32 @@ messages have a look at this post by `Tim Pope`_.
|
||||
Screenshots
|
||||
===========
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/overview.png
|
||||
:alt: Overview / Index / Homepage
|
||||
:width: 1024
|
||||
:height: 700
|
||||
:align: center
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/nodes.png
|
||||
:alt: Nodes view, all active nodes
|
||||
:width: 1024
|
||||
:height: 700
|
||||
:align: center
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/node.png
|
||||
:alt: Node without experimental endpoints endabled
|
||||
:alt: Single node page / overview
|
||||
:width: 1024
|
||||
:height: 700
|
||||
:align: center
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/report.png
|
||||
:alt: Report view
|
||||
:width: 1024
|
||||
:height: 700
|
||||
:align: center
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/report_message.png
|
||||
:alt: Report view with message
|
||||
:width: 1024
|
||||
:height: 700
|
||||
:align: center
|
||||
@@ -206,26 +499,26 @@ Screenshots
|
||||
:height: 700
|
||||
:align: center
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/nodes.png
|
||||
:alt: Nodes table without experimental endpoints enabled
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/fact.png
|
||||
:alt: Single fact, with graphs
|
||||
:width: 1024
|
||||
:height: 700
|
||||
:align: center
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/overview.png
|
||||
:alt: Overview / Index / Homepage
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/fact_value.png
|
||||
:alt: All nodes that have this fact with that value
|
||||
:width: 1024
|
||||
:height: 700
|
||||
:align: center
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/metrics.png
|
||||
:alt: Query view
|
||||
:alt: Metrics view
|
||||
:width: 1024
|
||||
:height: 700
|
||||
:align: center
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/metric.png
|
||||
:alt: Query view
|
||||
:alt: Single metric
|
||||
:width: 1024
|
||||
:height: 700
|
||||
:align: center
|
||||
@@ -236,23 +529,8 @@ Screenshots
|
||||
:height: 700
|
||||
:align: center
|
||||
|
||||
API v3
|
||||
------
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/nodes-v3.png
|
||||
:alt: Nodes table with experimental endpoints enabled
|
||||
:width: 1024
|
||||
:height: 700
|
||||
:align: center
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/node-v3.png
|
||||
:alt: Node view with experimental endpoints enabled
|
||||
:width: 1024
|
||||
:height: 700
|
||||
:align: center
|
||||
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/report.png
|
||||
:alt: Nodes table with experimental endpoints enabled
|
||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/broken.png
|
||||
:alt: Error page
|
||||
:width: 1024
|
||||
:height: 700
|
||||
:align: center
|
||||
|
||||
2
dev.py
@@ -5,5 +5,5 @@ from puppetboard.app import app
|
||||
from puppetboard.default_settings import DEV_LISTEN_HOST, DEV_LISTEN_PORT
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.debug=True
|
||||
app.debug = True
|
||||
app.run(DEV_LISTEN_HOST, DEV_LISTEN_PORT)
|
||||
|
||||
@@ -5,10 +5,12 @@ import os
|
||||
import logging
|
||||
import collections
|
||||
import urllib
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from flask import (
|
||||
Flask, render_template, abort, url_for,
|
||||
Response, stream_with_context,
|
||||
Response, stream_with_context, redirect,
|
||||
request
|
||||
)
|
||||
|
||||
from pypuppetdb import connect
|
||||
@@ -16,7 +18,7 @@ from pypuppetdb import connect
|
||||
from puppetboard.forms import QueryForm
|
||||
from puppetboard.utils import (
|
||||
get_or_abort, yield_or_stop,
|
||||
ten_reports,
|
||||
ten_reports, jsonprint
|
||||
)
|
||||
|
||||
|
||||
@@ -25,14 +27,16 @@ app.config.from_object('puppetboard.default_settings')
|
||||
app.config.from_envvar('PUPPETBOARD_SETTINGS', silent=True)
|
||||
app.secret_key = os.urandom(24)
|
||||
|
||||
app.jinja_env.filters['jsonprint'] = jsonprint
|
||||
|
||||
puppetdb = connect(
|
||||
api_version=app.config['PUPPETDB_API'],
|
||||
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'],)
|
||||
api_version=3,
|
||||
host=app.config['PUPPETDB_HOST'],
|
||||
port=app.config['PUPPETDB_PORT'],
|
||||
ssl_verify=app.config['PUPPETDB_SSL_VERIFY'],
|
||||
ssl_key=app.config['PUPPETDB_KEY'],
|
||||
ssl_cert=app.config['PUPPETDB_CERT'],
|
||||
timeout=app.config['PUPPETDB_TIMEOUT'],)
|
||||
|
||||
numeric_level = getattr(logging, app.config['LOGLEVEL'].upper(), None)
|
||||
if not isinstance(numeric_level, int):
|
||||
@@ -40,6 +44,7 @@ if not isinstance(numeric_level, int):
|
||||
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)
|
||||
@@ -47,66 +52,115 @@ def stream_template(template_name, **context):
|
||||
rv.enable_buffering(5)
|
||||
return rv
|
||||
|
||||
|
||||
@app.errorhandler(400)
|
||||
def bad_request(e):
|
||||
return render_template('400.html'), 400
|
||||
|
||||
|
||||
@app.errorhandler(403)
|
||||
def bad_request(e):
|
||||
return render_template('403.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 missing features
|
||||
depending on the API version."""
|
||||
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."""
|
||||
"""This view generates the index page and displays a set of metrics and
|
||||
latest reports on nodes 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')
|
||||
prefix = 'com.puppetlabs.puppetdb.query.population'
|
||||
num_nodes = get_or_abort(
|
||||
puppetdb.metric,
|
||||
"{0}{1}".format(prefix, ':type=default,name=num-nodes'))
|
||||
num_resources = get_or_abort(
|
||||
puppetdb.metric,
|
||||
"{0}{1}".format(prefix, ':type=default,name=num-resources'))
|
||||
avg_resources_node = get_or_abort(
|
||||
puppetdb.metric,
|
||||
"{0}{1}".format(prefix, ':type=default,name=avg-resources-per-node'))
|
||||
metrics = {
|
||||
'num_nodes': num_nodes['Value'],
|
||||
'num_resources': num_resources['Value'],
|
||||
'avg_resources_node': "{0:10.6f}".format(avg_resources_node['Value']),
|
||||
'mean_failed_commands': mean_failed_commands['MeanRate'],
|
||||
'mean_command_time': "{0:10.6f}".format(mean_command_time['MeanRate']),
|
||||
}
|
||||
return render_template('index.html', metrics=metrics)
|
||||
'num_nodes': num_nodes['Value'],
|
||||
'num_resources': num_resources['Value'],
|
||||
'avg_resources_node': "{0:10.0f}".format(avg_resources_node['Value']),
|
||||
}
|
||||
|
||||
nodes = puppetdb.nodes(
|
||||
unreported=app.config['UNRESPONSIVE_HOURS'],
|
||||
with_status=True)
|
||||
|
||||
nodes_overview = []
|
||||
stats = {
|
||||
'changed': 0,
|
||||
'unchanged': 0,
|
||||
'failed': 0,
|
||||
'unreported': 0,
|
||||
}
|
||||
|
||||
for node in nodes:
|
||||
if node.status == 'unreported':
|
||||
stats['unreported'] += 1
|
||||
elif node.status == 'changed':
|
||||
stats['changed'] += 1
|
||||
elif node.status == 'failed':
|
||||
stats['failed'] += 1
|
||||
else:
|
||||
stats['unchanged'] += 1
|
||||
|
||||
if node.status != 'unchanged':
|
||||
nodes_overview.append(node)
|
||||
|
||||
return render_template(
|
||||
'index.html',
|
||||
metrics=metrics,
|
||||
nodes=nodes_overview,
|
||||
stats=stats
|
||||
)
|
||||
|
||||
|
||||
@app.route('/nodes')
|
||||
def nodes():
|
||||
"""Fetch all (active) nodes from PuppetDB and stream a table displaying
|
||||
"""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()))))
|
||||
status_arg = request.args.get('status', '')
|
||||
nodelist = puppetdb.nodes(
|
||||
unreported=app.config['UNRESPONSIVE_HOURS'],
|
||||
with_status=True)
|
||||
nodes = []
|
||||
for node in yield_or_stop(nodelist):
|
||||
if status_arg:
|
||||
if node.status == status_arg:
|
||||
nodes.append(node)
|
||||
else:
|
||||
nodes.append(node)
|
||||
return Response(stream_with_context(
|
||||
stream_template('nodes.html', nodes=nodes)))
|
||||
|
||||
|
||||
@app.route('/node/<node_name>')
|
||||
def node(node_name):
|
||||
@@ -115,56 +169,70 @@ def node(node_name):
|
||||
heavy to do within a single request.
|
||||
"""
|
||||
node = get_or_abort(puppetdb.node, node_name)
|
||||
facts = node.facts()
|
||||
if app.config['PUPPETDB_API'] > 2:
|
||||
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))
|
||||
facts = node.facts()
|
||||
reports = ten_reports(node.reports())
|
||||
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_API'] > 2:
|
||||
return render_template('reports.html')
|
||||
else:
|
||||
log.warn('PuppetDB API prior to v3 cannot access reports.')
|
||||
abort(412)
|
||||
return render_template('reports.html')
|
||||
|
||||
|
||||
@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_API'] > 2:
|
||||
reports = ten_reports(yield_or_stop(
|
||||
puppetdb.reports('["=", "certname", "{0}"]'.format(node))))
|
||||
reports = ten_reports(yield_or_stop(
|
||||
puppetdb.reports('["=", "certname", "{0}"]'.format(node))))
|
||||
return render_template(
|
||||
'reports_node.html',
|
||||
reports=reports,
|
||||
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
|
||||
"""
|
||||
node = get_or_abort(puppetdb.node, node_name)
|
||||
reports = get_or_abort(puppetdb._query, 'reports',
|
||||
query='["=","certname","{0}"]'.format(node_name),
|
||||
limit=1)
|
||||
if len(reports) > 0:
|
||||
report = reports[0]['hash']
|
||||
return redirect(url_for('report', node=node_name, report_id=report))
|
||||
else:
|
||||
log.warn('PuppetDB API prior to v3 cannot access reports.')
|
||||
abort(412)
|
||||
return render_template('reports_node.html', reports=reports,
|
||||
nodename=node)
|
||||
abort(404)
|
||||
|
||||
|
||||
@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_API'] > 2:
|
||||
reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node))
|
||||
else:
|
||||
log.warn('PuppetDB API prior to v3 cannot access reports.')
|
||||
abort(412)
|
||||
report and their status.
|
||||
"""
|
||||
reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node))
|
||||
|
||||
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))
|
||||
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
|
||||
@@ -180,17 +248,32 @@ def facts():
|
||||
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."""
|
||||
# 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',
|
||||
localfacts = [f for f in yield_or_stop(puppetdb.facts(name=fact))]
|
||||
return Response(stream_with_context(stream_template(
|
||||
'fact.html',
|
||||
name=fact,
|
||||
facts=localfacts)))
|
||||
|
||||
|
||||
@app.route('/fact/<fact>/<value>')
|
||||
def fact_value(fact, value):
|
||||
"""On asking for fact/value get all nodes with that fact."""
|
||||
facts = get_or_abort(puppetdb.facts, fact, value)
|
||||
localfacts = [f for f in yield_or_stop(facts)]
|
||||
return render_template(
|
||||
'fact.html',
|
||||
name=fact,
|
||||
value=value,
|
||||
facts=localfacts)
|
||||
|
||||
|
||||
@app.route('/query', methods=('GET', 'POST'))
|
||||
def query():
|
||||
"""Allows to execute raw, user created querries against PuppetDB. This is
|
||||
@@ -199,25 +282,32 @@ def query():
|
||||
the JSON of the response or a message telling you what whent wrong /
|
||||
why nothing was returned."""
|
||||
if app.config['ENABLE_QUERY']:
|
||||
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)
|
||||
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)
|
||||
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()))
|
||||
metrics = get_or_abort(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()))
|
||||
name = urllib.unquote(metric)
|
||||
metric = puppetdb.metric(metric)
|
||||
return render_template(
|
||||
'metric.html',
|
||||
name=name,
|
||||
metric=sorted(metric.items()))
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
PUPPETDB_HOST='localhost'
|
||||
PUPPETDB_PORT=8080
|
||||
PUPPETDB_SSL=False
|
||||
PUPPETDB_KEY=None
|
||||
PUPPETDB_CERT=None
|
||||
PUPPETDB_TIMEOUT=20
|
||||
PUPPETDB_API=3
|
||||
DEV_LISTEN_HOST='127.0.0.1'
|
||||
DEV_LISTEN_PORT=5000
|
||||
ENABLE_QUERY=True
|
||||
LOGLEVEL='info'
|
||||
PUPPETDB_HOST = 'localhost'
|
||||
PUPPETDB_PORT = 8080
|
||||
PUPPETDB_SSL_VERIFY = True
|
||||
PUPPETDB_KEY = None
|
||||
PUPPETDB_CERT = None
|
||||
PUPPETDB_TIMEOUT = 20
|
||||
DEV_LISTEN_HOST = '127.0.0.1'
|
||||
DEV_LISTEN_PORT = 5000
|
||||
UNRESPONSIVE_HOURS = 2
|
||||
ENABLE_QUERY = True
|
||||
LOGLEVEL = 'info'
|
||||
|
||||
@@ -4,17 +4,17 @@ 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.')])
|
||||
message='A query is required.')])
|
||||
endpoints = RadioField('API endpoint', choices = [
|
||||
('nodes', 'Nodes'),
|
||||
('resources', 'Resources'),
|
||||
('facts', 'Facts'),
|
||||
('fact-names', 'Fact Names'),
|
||||
('reports', 'Reports'),
|
||||
('events', 'Events'),
|
||||
])
|
||||
|
||||
('nodes', 'Nodes'),
|
||||
('resources', 'Resources'),
|
||||
('facts', 'Facts'),
|
||||
('fact-names', 'Fact Names'),
|
||||
('reports', 'Reports'),
|
||||
('events', 'Events'),
|
||||
])
|
||||
|
||||
@@ -2,15 +2,22 @@ $ = jQuery
|
||||
$ ->
|
||||
$('.nodes').tablesorter(
|
||||
headers:
|
||||
3:
|
||||
4:
|
||||
sorter: false
|
||||
sortList: [[0,0]]
|
||||
sortList: [[1,0]]
|
||||
)
|
||||
|
||||
$('.facts').tablesorter(
|
||||
sortList: [[0,0]]
|
||||
)
|
||||
|
||||
$('.dashboard').tablesorter(
|
||||
headers:
|
||||
2:
|
||||
sorter: false
|
||||
sortList: [[0, 1]]
|
||||
)
|
||||
|
||||
$('input.filter-table').parent('div').removeClass('hide')
|
||||
$("input.filter-table").on "keyup", (e) ->
|
||||
rex = new RegExp($(this).val(), "i")
|
||||
|
||||
@@ -49,3 +49,56 @@ th.headerSortDown:after {
|
||||
.navbar .brand:hover {
|
||||
color: #fff;
|
||||
}
|
||||
.table tbody tr.error>td {
|
||||
background-color: #f2dede;
|
||||
}
|
||||
h1.error {
|
||||
color: rgb(223, 46, 27);
|
||||
}
|
||||
h1.success {
|
||||
color: #18BC9C;
|
||||
}
|
||||
h1.noop {
|
||||
color:#aaa;
|
||||
}
|
||||
tr.event {
|
||||
cursor: pointer;
|
||||
}
|
||||
td.message {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background-color: #FFFFE9;
|
||||
}
|
||||
div[id^='message-event'] {
|
||||
display: none;
|
||||
padding: 4px 15px 4px 15px;
|
||||
}
|
||||
.label-count {
|
||||
width:25px;
|
||||
text-align:center;
|
||||
}
|
||||
.label-time {
|
||||
width:73px;
|
||||
text-align:center;
|
||||
}
|
||||
.label-status {
|
||||
width:100px;
|
||||
text-align:center;
|
||||
}
|
||||
.label-nothing {
|
||||
background-color:#ddd;
|
||||
color:#ddd;
|
||||
}
|
||||
.label.label-failed {
|
||||
background-color: rgb(231, 76, 60);
|
||||
}
|
||||
.label.label-changed {
|
||||
background-color: rgb(24, 188, 156);
|
||||
}
|
||||
.label.label-unreported {
|
||||
background-color: rgb(231, 76, 60);
|
||||
background-color: rgb(129, 145, 146);
|
||||
}
|
||||
.btn-lastreport {
|
||||
width:100px;
|
||||
}
|
||||
|
||||
@@ -8,17 +8,24 @@
|
||||
|
||||
$('.nodes').tablesorter({
|
||||
headers: {
|
||||
3: {
|
||||
4: {
|
||||
sorter: false
|
||||
}
|
||||
},
|
||||
sortList: [[0, 0]]
|
||||
sortList: [[1, 0]]
|
||||
});
|
||||
|
||||
$('.facts').tablesorter({
|
||||
sortList: [[0, 0]]
|
||||
});
|
||||
|
||||
$('.dashboard').tablesorter({
|
||||
headers: {
|
||||
2: { sorter: false }
|
||||
},
|
||||
sortList: [[0, 1]]
|
||||
});
|
||||
|
||||
$('input.filter-table').parent('div').removeClass('hide');
|
||||
|
||||
$("input.filter-table").on("keyup", function(e) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% macro facts_table(facts, autofocus=False, condensed=False, show_node=False, margin_top=20, margin_bottom=20) -%}
|
||||
{% macro facts_table(facts, autofocus=False, condensed=False, show_node=False, show_value=True, link_facts=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>
|
||||
@@ -10,7 +10,9 @@
|
||||
{% else %}
|
||||
<th>Fact</th>
|
||||
{% endif %}
|
||||
{% if show_value %}
|
||||
<th>Value</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
@@ -21,7 +23,15 @@
|
||||
{% else %}
|
||||
<td><a href="{{url_for('fact', fact=fact.name)}}">{{fact.name}}</a></td>
|
||||
{% endif %}
|
||||
<td style="word-wrap:break-word">{{fact.value}}</td>
|
||||
{% if show_value %}
|
||||
<td style="word-wrap:break-word">
|
||||
{% if link_facts %}
|
||||
<a href="{{url_for('fact_value', fact=fact.name, value=fact.value)}}">{{fact.value}}</a>
|
||||
{% else %}
|
||||
{{fact.value}}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% import '_macros.html' as macros %}
|
||||
{% block content %}
|
||||
<h1>{{name}}</h1>
|
||||
<h1>{{name}}{% if value %}/{{value}}{% endif %} ({{facts|length}})</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)}}
|
||||
{% if value %}
|
||||
{{macros.facts_table(facts, autofocus=True, show_node=True, show_value=False, margin_bottom=10)}}
|
||||
{% else %}
|
||||
{{macros.facts_table(facts, autofocus=True, show_node=True, link_facts=True, margin_bottom=10)}}
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,11 +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">
|
||||
<a href="nodes?status=failed">
|
||||
<h1 class="error">{{stats['failed']}}
|
||||
<small>{% if stats['failed']== 1 %} node {% else %} nodes {% endif %}</small>
|
||||
</h1>
|
||||
</a>
|
||||
<span>with status failed</span>
|
||||
</div>
|
||||
<div class="span4 stat">
|
||||
<a href="nodes?status=changed">
|
||||
<h1 class="success">{{stats['changed']}}
|
||||
<small>{% if stats['changed']== 1 %} node {% else %} nodes {% endif %}</small>
|
||||
</h1>
|
||||
</a>
|
||||
<span>with status changed</span>
|
||||
</div>
|
||||
<div class="span4 stat">
|
||||
<a href="nodes?status=unreported">
|
||||
<h1 class="noop">{{ stats['unreported'] }}
|
||||
<small>{% if stats['unreported']== 1 %} node {% else %} nodes {% endif %}</small>
|
||||
</h1>
|
||||
</a>
|
||||
<span>
|
||||
unreported in the last {{ config.UNRESPONSIVE_HOURS }} hours
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
<div class="span4 stat">
|
||||
@@ -22,16 +49,53 @@
|
||||
</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>
|
||||
{% if nodes %}
|
||||
<h2>Nodes status detail ({{nodes|length}})</h2>
|
||||
<table class='dashboard table table-striped table-condensed'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:220px;">Status</th>
|
||||
<th style="width:600px;">Hostname</th>
|
||||
<th style="width:120px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for node in nodes %}
|
||||
{% if node.status != 'unchanged' %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{url_for('report_latest', node_name=node.name)}}">
|
||||
<span class="label label-status label-{{node.status}}">{{node.status}}</span>
|
||||
</a>
|
||||
{% if node.status=='unreported'%}
|
||||
<span class="label label-time label-unreported"> {{ node.unreported_time }} </label>
|
||||
{% else %}
|
||||
{% if node.events['failures'] %}<span class="label label-important label-count">{{node.events['failures']}}</span>{% else %}<span class="label label-count">0</span>{% endif%}
|
||||
{% if node.events['successes'] %}<span class="label label-success label-count">{{node.events['successes']}}</span>{% else %}<span class="label label-count">0</span>{% endif%}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><a href="{{url_for('node', node_name=node.name)}}">{{ node.name }}</a></td>
|
||||
<td>
|
||||
{% if node.unreported_time != None or node.status != 'unreported' %}
|
||||
<a class="btn btn-small btn-primary btn-lastreport" href="{{url_for('report_latest', node_name=node.name)}}">Latest Report</a>
|
||||
{% else %}
|
||||
<a class="btn btn-small btn-lastreport"> No Report </a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<h2>Nodes status detail</h2>
|
||||
<div class="alert alert-info">
|
||||
Nothing seems to be changing.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
|
||||
<script src="//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>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Metric
|
||||
{% set name = "%s…"|format(name[:75])|safe if name|length > 75%}
|
||||
<small>{{name}}</small>
|
||||
</h1>
|
||||
</div>
|
||||
@@ -10,7 +11,11 @@
|
||||
{% for key,value in metric %}
|
||||
<tr>
|
||||
<td>{{key}}</td>
|
||||
<td>{{value}}</td>
|
||||
{% if value is mapping %}
|
||||
<td><pre>{{value|jsonprint}}</pre></td>
|
||||
{% else %}
|
||||
<td>{{value}}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -2,45 +2,36 @@
|
||||
{% import '_macros.html' as macros %}
|
||||
{% block content %}
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<div class="span12">
|
||||
<h1>Details</h1>
|
||||
<table class="table table-striped table-condensed" style="table-layout:fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>Facts uploaded at</th>
|
||||
<th>Catalog compiled at</th>
|
||||
<th>Report uploaded at</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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.catalog_timestamp}}</td>
|
||||
<td rel="utctimestamp">{{node.report_timestamp}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if config.PUPPETDB_API > 2 %}
|
||||
<div class="span4">
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
<h1>Facts</h1>
|
||||
{{macros.facts_table(facts, condensed=True, margin_top=10)}}
|
||||
{{macros.facts_table(facts, link_facts=True, condensed=True, margin_top=10)}}
|
||||
</div>
|
||||
<div class="span4">
|
||||
<div class="span6">
|
||||
<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 %}
|
||||
|
||||
@@ -18,20 +18,28 @@
|
||||
<table class='nodes table table-striped table-condensed'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Hostname</th>
|
||||
<th>Catalog compiled at</th>
|
||||
{% if config.PUPPETDB_API > 2 %}
|
||||
<th>Last report</th>
|
||||
<th> </th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for node in nodes %}
|
||||
<tr>
|
||||
<td><a href="{{url_for('report_latest', node_name=node.name)}}">
|
||||
<span class="label label-status label-{{ node.status }}">{{ node.status }}</span>
|
||||
</a>
|
||||
{% if node.status=='unreported'%}
|
||||
<span class="label label-time label-unreported"> {{ node.unreported_time }} </label>
|
||||
{% else %}
|
||||
{% if node.events['failures'] %}<span class="label label-count label-important">{{node.events['failures']}}</span>{% else %}<span class="label label-count">0</span>{% endif%}
|
||||
{% if node.events['successes'] %}<span class="label label-count label-success">{{node.events['successes']}}</span>{% else %}<span class="label label-count">0</span>{% endif%}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><a href="{{url_for('node', node_name=node.name)}}">{{node.name}}</a></td>
|
||||
<td rel="utctimestamp">{{node.catalog_timestamp}}</td>
|
||||
{% if config.PUPPETDB_API > 2 %}
|
||||
<td>
|
||||
{% if node.report_timestamp %}
|
||||
<span rel="utctimestamp">{{ node.report_timestamp }}</span>
|
||||
@@ -41,10 +49,10 @@
|
||||
</td>
|
||||
<td>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
<h2>Result</h2>
|
||||
<pre><code>{{ result|tojson|replace(", ", ",\n") }}</code></pre>
|
||||
<pre><code>{{result|jsonprint}}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{report.node}}</td>
|
||||
<td><a href="{{url_for('node', node_name=report.node)}}">{{ report.node }}</a></td>
|
||||
<td>
|
||||
{{report.version}}
|
||||
</td>
|
||||
@@ -39,15 +39,22 @@
|
||||
<tbody>
|
||||
{% for event in events %}
|
||||
{% if not event.failed and event.item['old'] != event.item['new'] %}
|
||||
<tr class='success'>
|
||||
<tr id='event-{{loop.index}}' class='success event'>
|
||||
{% elif event.failed %}
|
||||
<tr class='error'>
|
||||
<tr id='event-{{loop.index}}' class='error event'>
|
||||
{% endif %}
|
||||
<td>{{event.item['type']}}[{{event.item['title']}}]</td>
|
||||
<td>{{event.status}}</td>
|
||||
<td>{{event.item['old']}}</td>
|
||||
<td>{{event.item['new']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class='message' colspan='4'>
|
||||
<div id='message-event-{{loop.index}}'>
|
||||
{{event.item['message']}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -57,6 +64,10 @@
|
||||
<script type='text/javascript'>
|
||||
jQuery(function ($) {
|
||||
$("[rel=tooltip]").tooltip();
|
||||
$(".event").click(function() {
|
||||
$("#message-" + this.id).slideToggle(200);
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock script %}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
from requests.exceptions import HTTPError, ConnectionError
|
||||
from pypuppetdb.errors import EmptyResponseError
|
||||
|
||||
from flask import abort
|
||||
|
||||
def jsonprint(value):
|
||||
return json.dumps(value, indent=2, separators=(',', ': ') )
|
||||
|
||||
|
||||
def get_or_abort(func, *args, **kwargs):
|
||||
"""Execute the function with its arguments and handle the possible
|
||||
errors that might occur.
|
||||
|
||||
@@ -5,5 +5,5 @@ MarkupSafe==0.18
|
||||
WTForms==1.0.4
|
||||
Werkzeug==0.9.3
|
||||
itsdangerous==0.22
|
||||
pypuppetdb==0.0.4
|
||||
pypuppetdb==0.1.0
|
||||
requests==1.2.3
|
||||
|
||||
BIN
screenshots/fact.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
screenshots/fact_value.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 280 KiB |
|
Before Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 474 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 159 KiB |
BIN
screenshots/report_message.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
52
setup.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import sys
|
||||
import os
|
||||
import codecs
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
|
||||
if sys.argv[-1] == 'publish':
|
||||
os.system('python setup.py sdist upload')
|
||||
sys.exit()
|
||||
|
||||
VERSION = "0.0.3"
|
||||
|
||||
with codecs.open('README.rst', encoding='utf-8') as f:
|
||||
README = f.read()
|
||||
|
||||
with codecs.open('CHANGELOG.rst', encoding='utf-8') as f:
|
||||
CHANGELOG = f.read()
|
||||
|
||||
with codecs.open('LICENSE', encoding='utf-8') as f:
|
||||
LICENSE = f.read()
|
||||
|
||||
setup(
|
||||
name='puppetboard',
|
||||
version=VERSION,
|
||||
author='Daniele Sluijters',
|
||||
author_email='daniele.sluijters+pypi@gmail.com',
|
||||
packages=find_packages(),
|
||||
url='https://github.com/nedap/puppetboard',
|
||||
license=LICENSE,
|
||||
description='Web frontend for PuppetDB',
|
||||
include_package_data=True,
|
||||
long_description='\n'.join((README, CHANGELOG)),
|
||||
install_requires=[
|
||||
"Flask >= 0.10.1",
|
||||
"Flask-WTF >= 0.9.4",
|
||||
"pypuppetdb >= 0.1.0",
|
||||
],
|
||||
keywords="puppet puppetdb puppetboard",
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Flask',
|
||||
'Intended Audience :: System Administrators',
|
||||
'Natural Language :: English',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: POSIX',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
],
|
||||
)
|
||||