66 Commits

Author SHA1 Message Date
Daniele Sluijters
aabd82a08e setup: Bump to 0.0.4. 2014-01-21 15:13:02 +01:00
Daniele Sluijters
c52da03f60 setup: Fix your license so bdist_rpm doesn't trip. 2014-01-21 15:12:40 +01:00
Daniele Sluijters
386fea9e1e templates: Sort fact tables.
We actually had a function that would sort the facts tables by default
based on the first column but weren't using this. Testing with the
upcoming PuppetDB 1.6 made this bug surface because PuppetDB stopped
sorting facts by itself.
2014-01-21 15:10:28 +01:00
Daniele Sluijters
bdd1a39b10 README: Add type to every codeblock for PyPi.
PyPi apparently has a slightly patched/weird version of docutils which
breaks when code-block::'s don't have a type.
2014-01-16 14:39:16 +01:00
Daniele Sluijters
d90e22397b wsgi: Get rid of the wsgi files.
Though useful they need to be customised per environment anyway and the
necessary examples are now included in the docs.
2014-01-16 11:39:15 +01:00
Daniele Sluijters
c6b194ca83 Add setup.py|cfg and MANIFEST.in for packaging. 2014-01-16 11:31:49 +01:00
Daniele Sluijters
59ae9657ff docs: Update README and CHANGELOG
Added CHANGELOG entries for the changes since Puppetboard 0.0.2.
Done some serious work on the README which now includes much improved
and tested installation instructions.
2014-01-16 10:44:06 +01:00
Daniele Sluijters
d92a068057 Switch to using pypuppetdb 0.1.0. 2014-01-13 13:12:21 +01:00
Daniele Sluijters
cbb3b8640f Merge pull request #41 from jasperla/avg_resources
Report rounded avg. resources per node.
2014-01-13 02:41:48 -08:00
Daniele Sluijters
9b7c33dcff Merge pull request #43 from nibalizer/fix_ssl
fix arguments to pypuppetdb connect()
2013-12-30 01:53:39 -08:00
Spencer Krum
b01a749bab fix arguments to pypuppetdb connect() 2013-12-30 00:28:03 -08:00
Jasper Lievisse Adriaanse
5fa260e748 Report rounded avg. resources per node. 2013-12-23 11:08:12 +01:00
Daniele Sluijters
eadcf8708c Merge pull request #39 from sijis/node_link
Adding link to node details page when viewing a specific report
2013-12-11 00:01:39 -08:00
Sijis Aviles
e57437e705 adding link to node details page when viewing a specific report 2013-12-10 14:17:26 -06:00
Daniele Sluijters
bf86b1780d Merge pull request #32 from lsjostro/remove-empty-message-check
Remove check for empty message in events.
2013-11-25 00:42:25 -08:00
Daniele Sluijters
5bed3d503f Merge pull request #36 from digitalmediacenter/fix-noreport
Fix for latest report if there is no report available
2013-11-25 00:40:33 -08:00
Daniele Sluijters
52b689d174 Merge pull request #37 from fnaard/readme_typo_fix
Fix typos in README.rst, Development section.
2013-11-22 00:46:02 -08:00
Gabriel M Schuyler
c6bba09beb Fix typos in README.rst, Development section.
Correcting two typos in the first line of the Development section.
2013-11-21 18:19:51 -08:00
Julius Härtl
73e26e8c1c error fix, format string and css button width fix 2013-11-20 13:34:13 +01:00
Julius Härtl
08bad89041 fix for latest report in overview
* report/latest/<node_name> uses limit parameter in _query
  to get just one report
* disable "Latest Report" button if there is no report
* HTTP Status 500 if there is no report on report/latest/<node_name>
2013-11-20 11:30:08 +01:00
Daniele Sluijters
dcf8abefe9 Wrong Facts screenshot. 2013-11-11 11:09:31 +01:00
Daniele Sluijters
dd0e8d8eb0 New screenshots, updated in README too. 2013-11-11 11:07:04 +01:00
Daniele Sluijters
5e9f4b5526 Merge pull request #33 from nibalizer/pep8again
puppetboard/app.py: Pep8 fixes
2013-11-07 13:28:23 -08:00
Spencer Krum
ac06c65d73 puppetboard/app.py: Pep8 fixes 2013-11-07 12:52:44 -08:00
Lars Sjöström
e55e43ed6a remove check for empty message 2013-11-06 14:20:56 +01:00
Daniele Sluijters
71c3f809ca Merge pull request #31 from fretterick/use-UNRESPONSIVE_HOURS-node-overview
UNRESPONSIVE_HOURS not used for node overview
2013-11-06 04:05:22 -08:00
Frederik Happel
b728896fea use unreported=app.config['UNRESPONSIVE_HOURS'] for node overview as
well
2013-11-06 13:00:10 +01:00
Daniele Sluijters
398156b0ae fact: Add the counter back to facts.
Since we're now already consuming the generator and creating a list we
can call lenght on it just fine.

Closes #18
2013-11-06 08:40:46 +01:00
Daniele Sluijters
eb1bf7c3ab Merge pull request #29 from fretterick/use-UNRESPONSIVE_HOURS
Use UNRESPONSIVE_HOURS setting.
2013-11-05 10:14:41 -08:00
Daniele Sluijters
0992763d9d Merge pull request #27 from lsjostro/display-event-message
Toggle display of event message in reports
2013-11-05 10:13:03 -08:00
Lars Sjöström
fb763e637f Feature: Toggle event message in event reports
Toggle event message in reports

cursor pointer and indent of message

rebase from master
2013-11-05 18:51:45 +01:00
Frederik Happel
d067fe3ed3 use configuration variable UNRESPONSIVE_HOURS to determine if a node's
status is unreported
2013-11-05 18:31:54 +01:00
Daniele Sluijters
efe488aafc Add a new jsonprint filter, used in metrics, query 2013-11-05 16:41:47 +01:00
Daniele Sluijters
79ac5b3cb0 node: Give the node overview some breathing room.
The interface was too packed causing the facts and reports tables to be
jammed into place.

Currently working on a complete new node overview page but this should
make things a bit more workable in the meantime.
2013-11-05 16:10:46 +01:00
Daniele Sluijters
754784f4af Make fact value clickable.
In the Facts view you can now click on the value of a fact and get a
listing of all the nodes with that value for that fact.

Closes #13
2013-11-05 15:44:34 +01:00
Daniele Sluijters
0563224c87 metric: 100 is a bit much, 75 looks better. 2013-11-05 14:20:03 +01:00
Daniele Sluijters
3efdb58ce3 metric: Truncate the name. 2013-11-05 14:12:47 +01:00
Daniele Sluijters
de6a77951c app: Abort if we can't fetch metrics. 2013-11-05 12:20:48 +01:00
Daniele Sluijters
e753fc444a overview: Add a count, info if nothing is changing
The Nodes status detail now shows for how many nodes we have events.
Additionally when there are no events we simply show an alert that
nothing is going on.
2013-11-05 12:12:49 +01:00
Daniele Sluijters
a1f00a7b66 Merge pull request #28 from digitalmediacenter/improve-nodestatus
Enhance node status feature in overview and nodes
2013-11-05 03:01:02 -08:00
Julius Härtl
b3d08233f3 change sorting of the overview node status in javascript 2013-11-05 11:54:28 +01:00
Julius Härtl
ffdbfcda24 nodes view filter now works with new status attribute 2013-11-05 10:49:59 +01:00
Julius Härtl
f187638b6e Enhance node status feature in overview and nodes
This commit uses the new parameter with_status from nedap/pypuppetdb#18

Node status is now shown as text with the additional information of
failed/succeded events, unreported time

The statistics on Overview now show the *number of nodes*
that have status failed/changed/unreported
2013-11-05 10:07:53 +01:00
Daniele Sluijters
a84da91f06 Merge pull request #26 from lsjostro/list-nodes-with-failure
make failed/success event counts clickable
2013-11-04 03:25:06 -08:00
Lars Sjöström
09b249b0ca make failed/success event counts clickable 2013-11-04 11:40:32 +01:00
Daniele Sluijters
269aeeec57 Merge pull request #23 from nibalizer/pep8
Assorted PEP8 fixes.
2013-11-04 02:33:12 -08:00
Daniele Sluijters
3cbb21ac60 templates: use jQuery protocol relative URL. 2013-10-30 09:57:25 +01:00
Spencer Krum
49afb9ed34 Pep8 check most files
I left some errors on puppetboard/forms.py because I'm not
sure how to fix them.
2013-10-29 11:51:51 -07:00
Spencer Krum
7265dc2fd0 puppetboard/app.py: pep8 2013-10-29 11:41:53 -07:00
Daniele Sluijters
f2e7ecc67e Merge pull request #25 from lsjostro/fix-nodes-without-timestamp
Add nodes with missing timestamp to unreported nodes
2013-10-29 06:33:06 -07:00
Lars Sjöström
61548c819c Add nodes with missing timestamp to unreported nodes
Skip nodes with NoneType timestamps

add nodes with missing timestamp to unreported nodes
2013-10-29 14:14:48 +01:00
Daniele Sluijters
ee2775512d requirements: Change the URL to use git+git.
Some versions of Pip, depending on the platform and Python version have
issues with git+https URL's.
2013-10-29 09:03:55 +01:00
Daniele Sluijters
75da9b9209 Get rid of old settings.
I broke things with 795d243e9d because I
forgot to remove it everywhere from functions and templates. Also
removed the old PUPPETDB_EXPERIMENTAL switching.
2013-10-28 21:46:34 +01:00
Daniele Sluijters
029b50405b screenshots: New overview and nodes pages. 2013-10-28 17:15:43 +01:00
Daniele Sluijters
795d243e9d We now require PuppetDB 1.5 / API v3.
PUPPETDB_API is no longer configurable since we're now using features
that are v3 only. Limiting ourselves to v2 compatibility is far too
troublesome and people tend to update to newer versions of PuppetDB
fairly quickly.
2013-10-28 17:08:40 +01:00
Daniele Sluijters
c0cef0a3c0 overview: Cosmetic changes.
* Don't pass unresponsive to the view, access config[] object instead
* Remove the statistics header, it only takes up space
* Lowercase a few things
* Change the descriptions for 'radiator' to make the math work: Because
  of how PuppetDB's aggregate-events-count works nodes with both
  successful and failed events count for both causing success + failure
  + unreported to not equal population, which is weird. Now we're simply
  stating that they have failed events instead of saying that the node
  is succesful/failed.
2013-10-28 16:50:19 +01:00
Daniele Sluijters
58625b5ee0 overview: Remove command statistics. 2013-10-28 16:49:39 +01:00
Daniele Sluijters
a4dc1f694e Merge pull request #22 from digitalmediacenter/feature-status
Add basic support for node status by using the most recent report

Closes #5 #15
2013-10-28 07:43:26 -07:00
Julius Härtl
bbb65939c9 remove duplicate mean_command_time 2013-10-28 15:38:45 +01:00
Julius Härtl
5ca758dd39 show list of nodes without report for x hours in overview
- the amount of hours is defined as `UNRESPONSIVE_HOURS` in default_settings.py
- small status layout improvement in nodes list
- latest report button in nodes list
- nedap/pypuppetdb repo as requirement ( new api was merged nedap/pypuppetdb#17 )
2013-10-28 11:36:37 +01:00
Julius Härtl
c7bae2efa3 fix for required branch of pypuppetdb with new api support 2013-10-25 16:07:00 +02:00
Julius Härtl
7c027dd97d Add basic support for node status by using the most recent report
The following frontend features are implemented
- Number of failures, successes, noops/skips in overview
- Show latest reports with 1 or more events in overview
- Direct links to latest Report
- Number and types of events in nodes list
2013-10-25 15:43:14 +02:00
Daniele Sluijters
efae19dc6d css: Color table row/cells with class 'error'. 2013-10-18 15:47:59 +02:00
Daniele Sluijters
bb124e1ba5 Merge pull request #21 from KlavsKlavsen/master
added wsgi file for passenger
2013-10-15 01:02:34 -07:00
Klavs Klavsen
89117ce844 change default loglevel to be INFO instead of DEBUG. 2013-10-15 09:11:26 +02:00
Klavs Klavsen
b5fde343ed added wsgi file for passenger - with error handling and logging to tmp file - until I figure out how to make it log to apache error log instead 2013-10-14 15:48:31 +02:00
36 changed files with 857 additions and 234 deletions

View File

@@ -2,6 +2,50 @@
Changelog
#########
This is the changelog for Puppetboard.
0.0.4
=====
* Fix the sorting of the different tables containing facts.
* Fix the license in our ``setup.py``. The license shouldn't be longer than
200 characters. We were including the full license tripping up tools like
bdist_rpm.
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
View File

@@ -0,0 +1,5 @@
include README.rst
include CHANGELOG.rst
include LICENSE
recursive-include puppetboard/static *.css *.js
recursive-include puppetboard/templates *.html

View File

@@ -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``.
.. code-block::
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:: python
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:: apache
<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:: python
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:: apache
<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:: python
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:: nginx
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:: apache
<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:: nginx
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
View File

@@ -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)

View File

@@ -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()))

View File

@@ -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'

View File

@@ -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'),
])

View File

@@ -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")

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -1,8 +1,8 @@
{% 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>
<table class="filter-table table table-striped {% if condensed %}table-condensed{% endif%}" style="table-layout:fixed">
<table class="filter-table facts table table-striped {% if condensed %}table-condensed{% endif%}" style="table-layout:fixed">
<thead>
<tr>
{% if show_node %}
@@ -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>

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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>

View File

@@ -2,6 +2,7 @@
{% block content %}
<div class="page-header">
<h1>Metric
{% set name = "%s&hellip;"|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>

View File

@@ -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 %}

View File

@@ -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>&nbsp;</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>

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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.

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
screenshots/fact_value.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

2
setup.cfg Normal file
View File

@@ -0,0 +1,2 @@
[wheel]
universal = 1

49
setup.py Normal file
View File

@@ -0,0 +1,49 @@
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.4"
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()
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='Apache License 2.0',
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',
],
)

11
wsgi.py
View File

@@ -1,11 +0,0 @@
from __future__ import absolute_import
import os
import sys
me = os.path.dirname(os.path.abspath(__file__))
# Add us to the PYTHONPATH/sys.path if we're not on it
if not me in sys.path:
sys.path.insert(0, me)
from puppetboard.app import app as application