Compare commits
105 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 | ||
|
|
c6c4bc1679 | ||
|
|
9be5aaebd9 | ||
|
|
a75b08f882 | ||
|
|
f85f0fa864 | ||
|
|
095967e445 | ||
|
|
8a56e83b69 | ||
|
|
934f90c12a | ||
|
|
0422b2ccd0 | ||
|
|
e1603608bc | ||
|
|
23af033cbb | ||
|
|
e71f30ab50 | ||
|
|
cc87e54cea | ||
|
|
3b4db8f37e | ||
|
|
3fd8a0aad4 | ||
|
|
9b662a661e | ||
|
|
cfb1383025 | ||
|
|
8db60d14bd | ||
|
|
8cf181a9e8 | ||
|
|
543f706fd7 | ||
|
|
644e169a7f | ||
|
|
b25d85bd32 | ||
|
|
9918ec8f4b | ||
|
|
da68bb259b | ||
|
|
3c05071aef | ||
|
|
cb64b73832 | ||
|
|
c3821e777f | ||
|
|
c04d45f602 | ||
|
|
d900ccf09a | ||
|
|
8b3f3ea61e | ||
|
|
f273d24f80 | ||
|
|
d152d8e3a1 | ||
|
|
c64a2b79b2 | ||
|
|
083da989de | ||
|
|
c3a9b5e81c | ||
|
|
00d0f96914 | ||
|
|
7b71eb39d2 | ||
|
|
1a178ef2af | ||
|
|
4d80b6c128 | ||
|
|
462fcbf76c | ||
|
|
c8825d3d92 | ||
|
|
1d705d04dd | ||
|
|
88d1944b4b | ||
|
|
bda3adc078 |
77
CHANGELOG.rst
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#########
|
||||||
|
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
|
||||||
|
pypuppetdb version 0.0.4 or later which includes support for the v3 API
|
||||||
|
introduced with PuppetDB 1.5.
|
||||||
|
|
||||||
|
Because of changes in PuppetDB 1.5 and therefor in pypuppetdb users of the v2
|
||||||
|
API, regardless of the PuppetDB version, will no longer be able to view reports
|
||||||
|
or events.
|
||||||
|
|
||||||
|
In light of this the following settings have been removed:
|
||||||
|
|
||||||
|
* ``PUPPETDB_EXPERIMENTAL``
|
||||||
|
|
||||||
|
Two new settings have been added:
|
||||||
|
|
||||||
|
* ``PUPPETDB_API``: an integer, defaulting to ``3``, representing the API
|
||||||
|
version we want to use.
|
||||||
|
* ``ENABLE_QUERY``: a boolean, defaulting to ``True``, on wether or not to
|
||||||
|
be able to use the Query tab.
|
||||||
|
|
||||||
|
We've also added a few new features:
|
||||||
|
|
||||||
|
* Thanks to some work done during PuppetConf together with Nick Lewis (from
|
||||||
|
Puppet Labs) we now expose all of PuppetDB's metrics in the Metrics tab. The
|
||||||
|
formatting isn't exactly pretty but it's a start.
|
||||||
|
* Spencer Krum added the graphing capabilities to the Facts tab.
|
||||||
|
* Daniel Lawrence added a feature so that facts on the node view are clickable
|
||||||
|
and take you to the complete overview of that fact for your infrastructure
|
||||||
|
and made the nodes in the complete facts list clickable so you can jump to a
|
||||||
|
node.
|
||||||
|
* Klavs Klavsen contributed some documentation on how to run Puppetboard with
|
||||||
|
Passenger.
|
||||||
|
|
||||||
|
0.0.1
|
||||||
|
=====
|
||||||
|
Initial release.
|
||||||
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
|
||||||
475
README.rst
@@ -17,12 +17,14 @@ Because this project is powered by Flask we are restricted to:
|
|||||||
* Python 2.6
|
* Python 2.6
|
||||||
* Python 2.7
|
* Python 2.7
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/node-experimental.png
|
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/overview.png
|
||||||
:alt: View of a node
|
:alt: View of a node
|
||||||
:width: 1024
|
:width: 1024
|
||||||
:height: 700
|
:height: 700
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
Word of caution
|
Word of caution
|
||||||
===============
|
===============
|
||||||
|
|
||||||
@@ -38,60 +40,393 @@ this might throw at you.
|
|||||||
Installation
|
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
|
.. code-block:: bash
|
||||||
|
|
||||||
$ git clone https://github.com/nedap/puppetboard
|
$ pip install puppetboard
|
||||||
$ pip install -r requirements.txt
|
|
||||||
|
|
||||||
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
|
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
|
.. code-block:: bash
|
||||||
|
|
||||||
$ python dev.py
|
$ 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
|
Production
|
||||||
----------
|
----------
|
||||||
For WSGI capable webservers a ``wsgi.py`` is provided which ``mod_wsgi``
|
To run Puppetboard in production we provide instructions for the following
|
||||||
and ``uwsgi`` can deal with.
|
scenarios:
|
||||||
|
|
||||||
* Apache mod_wsgi configuration: http://flask.pocoo.org/docs/deploying/mod_wsgi/
|
* Apache + mod_wsgi
|
||||||
* uwsgi configuration: ``uwsgi --http :9090 --wsgi-file /path/to/puppetboard/wsgi.py``
|
* Apache + mod_passenger
|
||||||
|
* uwsgi + nginx
|
||||||
|
|
||||||
In the case of uwsgi you'll of course need something like nginx in front of it to
|
If you deploy Puppetboard through a different setup we'd welcome a pull
|
||||||
proxy the requests to it.
|
request that adds the instructions to this section.
|
||||||
|
|
||||||
Don't forget that you also need to serve the ``static/`` folder on the
|
Apache + mod_wsgi
|
||||||
``/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).
|
|
||||||
|
|
||||||
Configuration
|
First we need to create the necessary directories:
|
||||||
=============
|
|
||||||
|
|
||||||
Puppetboard has some configuration settings, their defaults can
|
.. code-block:: bash
|
||||||
be viewed in ``puppetboard/default_settings.py``.
|
|
||||||
|
|
||||||
Additionally Puppetboard will look for an environment variable
|
$ mkdir -p /var/www/puppetboard
|
||||||
called ``PUPPETBOARD_SETTINGS`` pointing to a file with identical
|
$ chown www-data:www-data /var/www/puppetboard
|
||||||
markup as ``default_settings.py``. Any setting defined in
|
|
||||||
``PUPPETBOARD_SETTINGS`` will override the defaults.
|
|
||||||
|
|
||||||
Experimental
|
Copy Puppetboard's ``default_settings.py`` to the newly created puppetboard
|
||||||
------------
|
directory and name the file ``settings.py``. This file will be available
|
||||||
Pypuppetdb and Puppetboard can query and display information from
|
at the path Puppetboard was installed, for example:
|
||||||
PuppetDB's experimental API endpoints.
|
``/usr/local/lib/python2.X/lib/dist-packages/puppetboard/default_settings.py``.
|
||||||
|
|
||||||
However, if you haven't enabled them for Puppet it isn't particularily
|
Change the settings that need changing to match your environment and delete
|
||||||
useful to enable them here as there will be no data to retrieve.
|
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 /var/www/puppetboard/public
|
||||||
|
ErrorLog /var/log/apache2/puppetboard.error.log
|
||||||
|
CustomLog /var/log/apache2/puppetboard.access.log combined
|
||||||
|
|
||||||
|
RackAutoDetect On
|
||||||
|
Alias /static /usr/local/lib/python2.X/dist-packages/puppetboard/static
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
First we need to create the necessary directories:
|
||||||
|
|
||||||
|
.. 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
|
||||||
|
============
|
||||||
|
This project is still very new so it's not inconceivable you'll run into
|
||||||
|
issues.
|
||||||
|
|
||||||
|
For bug reports you can file an `issue`_. If you need help with something
|
||||||
|
feel free to hit up `@daenney`_ by e-mail or find him on IRC. He can usually
|
||||||
|
be found on `IRCnet`_ and `Freenode`_ and idles in #puppet.
|
||||||
|
|
||||||
|
There's now also the #puppetboard channel on `Freenode`_ where we hang out
|
||||||
|
and answer questions related to pypuppetdb and Puppetboard.
|
||||||
|
|
||||||
|
.. _issue: https://github.com/nedap/puppetboard/issues
|
||||||
|
.. _@daenney: https://github.com/daenney
|
||||||
|
.. _IRCnet: http://www.ircnet.org
|
||||||
|
.. _Freenode: http://freenode.net
|
||||||
|
|
||||||
|
Third party
|
||||||
|
===========
|
||||||
|
Some people have already started building things with and around Puppetboard.
|
||||||
|
|
||||||
|
`Hunter Haugen`_ has provided a Vagrant setup:
|
||||||
|
|
||||||
|
* https://github.com/hunner/puppetboard-vagrant
|
||||||
|
|
||||||
|
`Spencer Krum`_ created a Puppet module to install Puppetboard with:
|
||||||
|
|
||||||
|
* https://github.com/nibalizer/puppet-module-puppetboard
|
||||||
|
|
||||||
|
You can install it with:
|
||||||
|
|
||||||
|
puppet module install nibalizer-puppetboard
|
||||||
|
|
||||||
|
.. _Hunter Haugen: https://github.com/hunner
|
||||||
|
.. _Spencer Krum: https://github.com/nibalizer
|
||||||
|
|
||||||
Contributing
|
Contributing
|
||||||
============
|
============
|
||||||
@@ -128,8 +463,32 @@ messages have a look at this post by `Tim Pope`_.
|
|||||||
Screenshots
|
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
|
.. 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
|
:width: 1024
|
||||||
:height: 700
|
:height: 700
|
||||||
:align: center
|
:align: center
|
||||||
@@ -140,14 +499,26 @@ Screenshots
|
|||||||
:height: 700
|
:height: 700
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/nodes.png
|
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/fact.png
|
||||||
:alt: Nodes table without experimental endpoints enabled
|
:alt: Single fact, with graphs
|
||||||
:width: 1024
|
:width: 1024
|
||||||
:height: 700
|
:height: 700
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/overview.png
|
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/fact_value.png
|
||||||
:alt: Overview / Index / Homepage
|
: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: Metrics view
|
||||||
|
:width: 1024
|
||||||
|
:height: 700
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/metric.png
|
||||||
|
:alt: Single metric
|
||||||
:width: 1024
|
:width: 1024
|
||||||
:height: 700
|
:height: 700
|
||||||
:align: center
|
:align: center
|
||||||
@@ -158,38 +529,8 @@ Screenshots
|
|||||||
:height: 700
|
:height: 700
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
With experimental endpoints
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/nodes-experimental.png
|
|
||||||
:alt: Nodes table with experimental endpoints enabled
|
|
||||||
:width: 1024
|
|
||||||
:height: 700
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/node-experimental.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
|
|
||||||
:width: 1024
|
|
||||||
:height: 700
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
Error page
|
|
||||||
----------
|
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/no-experimental.png
|
|
||||||
:alt: Accessing disabled experimental feature
|
|
||||||
:width: 1024
|
|
||||||
:height: 700
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/broken.png
|
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/broken.png
|
||||||
:alt: Error message
|
:alt: Error page
|
||||||
:width: 1024
|
:width: 1024
|
||||||
:height: 700
|
:height: 700
|
||||||
:align: center
|
:align: center
|
||||||
|
|||||||
5
dev.py
@@ -2,7 +2,8 @@ from __future__ import unicode_literals
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
from puppetboard.app import app
|
from puppetboard.app import app
|
||||||
|
from puppetboard.default_settings import DEV_LISTEN_HOST, DEV_LISTEN_PORT
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.debug=True
|
app.debug = True
|
||||||
app.run('127.0.0.1')
|
app.run(DEV_LISTEN_HOST, DEV_LISTEN_PORT)
|
||||||
|
|||||||
@@ -4,19 +4,21 @@ from __future__ import absolute_import
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import collections
|
import collections
|
||||||
|
import urllib
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask, render_template, abort, url_for,
|
Flask, render_template, abort, url_for,
|
||||||
Response, stream_with_context,
|
Response, stream_with_context, redirect,
|
||||||
|
request
|
||||||
)
|
)
|
||||||
|
|
||||||
from pypuppetdb import connect
|
from pypuppetdb import connect
|
||||||
from pypuppetdb.errors import ExperimentalDisabledError
|
|
||||||
|
|
||||||
from puppetboard.forms import QueryForm
|
from puppetboard.forms import QueryForm
|
||||||
from puppetboard.utils import (
|
from puppetboard.utils import (
|
||||||
get_or_abort, yield_or_stop,
|
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.config.from_envvar('PUPPETBOARD_SETTINGS', silent=True)
|
||||||
app.secret_key = os.urandom(24)
|
app.secret_key = os.urandom(24)
|
||||||
|
|
||||||
|
app.jinja_env.filters['jsonprint'] = jsonprint
|
||||||
|
|
||||||
puppetdb = connect(
|
puppetdb = connect(
|
||||||
host=app.config['PUPPETDB_HOST'],
|
api_version=3,
|
||||||
port=app.config['PUPPETDB_PORT'],
|
host=app.config['PUPPETDB_HOST'],
|
||||||
ssl=app.config['PUPPETDB_SSL'],
|
port=app.config['PUPPETDB_PORT'],
|
||||||
ssl_key=app.config['PUPPETDB_KEY'],
|
ssl_verify=app.config['PUPPETDB_SSL_VERIFY'],
|
||||||
ssl_cert=app.config['PUPPETDB_CERT'],
|
ssl_key=app.config['PUPPETDB_KEY'],
|
||||||
timeout=app.config['PUPPETDB_TIMEOUT'],
|
ssl_cert=app.config['PUPPETDB_CERT'],
|
||||||
experimental=app.config['PUPPETDB_EXPERIMENTAL'])
|
timeout=app.config['PUPPETDB_TIMEOUT'],)
|
||||||
|
|
||||||
numeric_level = getattr(logging, app.config['LOGLEVEL'].upper(), None)
|
numeric_level = getattr(logging, app.config['LOGLEVEL'].upper(), None)
|
||||||
if not isinstance(numeric_level, int):
|
if not isinstance(numeric_level, int):
|
||||||
@@ -40,6 +44,7 @@ if not isinstance(numeric_level, int):
|
|||||||
logging.basicConfig(level=numeric_level)
|
logging.basicConfig(level=numeric_level)
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def stream_template(template_name, **context):
|
def stream_template(template_name, **context):
|
||||||
app.update_template_context(context)
|
app.update_template_context(context)
|
||||||
t = app.jinja_env.get_template(template_name)
|
t = app.jinja_env.get_template(template_name)
|
||||||
@@ -47,61 +52,115 @@ def stream_template(template_name, **context):
|
|||||||
rv.enable_buffering(5)
|
rv.enable_buffering(5)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(400)
|
@app.errorhandler(400)
|
||||||
def bad_request(e):
|
def bad_request(e):
|
||||||
return render_template('400.html'), 400
|
return render_template('400.html'), 400
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(403)
|
||||||
|
def bad_request(e):
|
||||||
|
return render_template('403.html'), 400
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(e):
|
def not_found(e):
|
||||||
return render_template('404.html'), 404
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(412)
|
@app.errorhandler(412)
|
||||||
def precond_failed(e):
|
def precond_failed(e):
|
||||||
"""We're slightly abusing 412 to handle ExperimentalDisabled errors."""
|
"""We're slightly abusing 412 to handle missing features
|
||||||
|
depending on the API version."""
|
||||||
return render_template('412.html'), 412
|
return render_template('412.html'), 412
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(500)
|
@app.errorhandler(500)
|
||||||
def server_error(e):
|
def server_error(e):
|
||||||
return render_template('500.html'), 500
|
return render_template('500.html'), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""This view generates the index page and displays a set of metrics fetched
|
"""This view generates the index page and displays a set of metrics and
|
||||||
from PuppetDB."""
|
latest reports on nodes fetched from PuppetDB.
|
||||||
|
"""
|
||||||
# TODO: Would be great if we could parallelize this somehow, doing these
|
# TODO: Would be great if we could parallelize this somehow, doing these
|
||||||
# requests in sequence is rather pointless.
|
# requests in sequence is rather pointless.
|
||||||
num_nodes = get_or_abort(puppetdb.metric,
|
prefix = 'com.puppetlabs.puppetdb.query.population'
|
||||||
'com.puppetlabs.puppetdb.query.population:type=default,name=num-nodes')
|
num_nodes = get_or_abort(
|
||||||
num_resources = get_or_abort(puppetdb.metric,
|
puppetdb.metric,
|
||||||
'com.puppetlabs.puppetdb.query.population:type=default,name=num-resources')
|
"{0}{1}".format(prefix, ':type=default,name=num-nodes'))
|
||||||
avg_resources_node = get_or_abort(puppetdb.metric,
|
num_resources = get_or_abort(
|
||||||
'com.puppetlabs.puppetdb.query.population:type=default,name=avg-resources-per-node')
|
puppetdb.metric,
|
||||||
mean_failed_commands = get_or_abort(puppetdb.metric,
|
"{0}{1}".format(prefix, ':type=default,name=num-resources'))
|
||||||
'com.puppetlabs.puppetdb.command:type=global,name=fatal')
|
avg_resources_node = get_or_abort(
|
||||||
mean_command_time = get_or_abort(puppetdb.metric,
|
puppetdb.metric,
|
||||||
'com.puppetlabs.puppetdb.command:type=global,name=processing-time')
|
"{0}{1}".format(prefix, ':type=default,name=avg-resources-per-node'))
|
||||||
metrics = {
|
metrics = {
|
||||||
'num_nodes': num_nodes['Value'],
|
'num_nodes': num_nodes['Value'],
|
||||||
'num_resources': num_resources['Value'],
|
'num_resources': num_resources['Value'],
|
||||||
'avg_resources_node': "{:10.6f}".format(avg_resources_node['Value']),
|
'avg_resources_node': "{0:10.0f}".format(avg_resources_node['Value']),
|
||||||
'mean_failed_commands': mean_failed_commands['MeanRate'],
|
}
|
||||||
'mean_command_time': "{:10.6f}".format(mean_command_time['MeanRate']),
|
|
||||||
}
|
nodes = puppetdb.nodes(
|
||||||
return render_template('index.html', metrics=metrics)
|
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')
|
@app.route('/nodes')
|
||||||
def 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.
|
those nodes.
|
||||||
|
|
||||||
Downside of the streaming aproach is that since we've already sent our
|
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
|
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
|
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
|
works. Once pagination is in place we can change this but we'll need to
|
||||||
provide a search feature instead.
|
provide a search feature instead.
|
||||||
"""
|
"""
|
||||||
return Response(stream_with_context(stream_template('nodes.html',
|
status_arg = request.args.get('status', '')
|
||||||
nodes=yield_or_stop(puppetdb.nodes()))))
|
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>')
|
@app.route('/node/<node_name>')
|
||||||
def node(node_name):
|
def node(node_name):
|
||||||
@@ -110,56 +169,70 @@ def node(node_name):
|
|||||||
heavy to do within a single request.
|
heavy to do within a single request.
|
||||||
"""
|
"""
|
||||||
node = get_or_abort(puppetdb.node, node_name)
|
node = get_or_abort(puppetdb.node, node_name)
|
||||||
facts = node.facts()
|
facts = node.facts()
|
||||||
if app.config['PUPPETDB_EXPERIMENTAL']:
|
reports = ten_reports(node.reports())
|
||||||
reports = ten_reports(node.reports())
|
return render_template(
|
||||||
else:
|
'node.html',
|
||||||
reports = iter([])
|
node=node,
|
||||||
return render_template('node.html', node=node, facts=yield_or_stop(facts),
|
facts=yield_or_stop(facts),
|
||||||
reports=yield_or_stop(reports))
|
reports=yield_or_stop(reports))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/reports')
|
@app.route('/reports')
|
||||||
def reports():
|
def reports():
|
||||||
"""Doesn't do much yet but is meant to show something like the reports of
|
"""Doesn't do much yet but is meant to show something like the reports of
|
||||||
the last half our, something like that."""
|
the last half our, something like that."""
|
||||||
if app.config['PUPPETDB_EXPERIMENTAL']:
|
return render_template('reports.html')
|
||||||
return render_template('reports.html')
|
|
||||||
else:
|
|
||||||
log.warn('Access to experimental endpoint not allowed.')
|
|
||||||
abort(412)
|
|
||||||
|
|
||||||
@app.route('/reports/<node>')
|
@app.route('/reports/<node>')
|
||||||
def reports_node(node):
|
def reports_node(node):
|
||||||
"""Fetches all reports for a node and processes them eventually rendering
|
"""Fetches all reports for a node and processes them eventually rendering
|
||||||
a table displaying those reports."""
|
a table displaying those reports."""
|
||||||
if app.config['PUPPETDB_EXPERIMENTAL']:
|
reports = ten_reports(yield_or_stop(
|
||||||
reports = ten_reports(yield_or_stop(
|
puppetdb.reports('["=", "certname", "{0}"]'.format(node))))
|
||||||
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:
|
else:
|
||||||
log.warn('Access to experimental endpoint not allowed.')
|
abort(404)
|
||||||
abort(412)
|
|
||||||
return render_template('reports_node.html', reports=reports,
|
|
||||||
nodename=node)
|
|
||||||
|
|
||||||
@app.route('/report/<node>/<report_id>')
|
@app.route('/report/<node>/<report_id>')
|
||||||
def report(node, report_id):
|
def report(node, report_id):
|
||||||
"""Displays a single report including all the events associated with that
|
"""Displays a single report including all the events associated with that
|
||||||
report and their status."""
|
report and their status.
|
||||||
if app.config['PUPPETDB_EXPERIMENTAL']:
|
"""
|
||||||
reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node))
|
reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node))
|
||||||
else:
|
|
||||||
log.warn('Access to experimental endpoint not allowed.')
|
|
||||||
abort(412)
|
|
||||||
|
|
||||||
for report in reports:
|
for report in reports:
|
||||||
if report.hash_ == report_id:
|
if report.hash_ == report_id:
|
||||||
events = puppetdb.events('["=", "report", "{0}"]'.format(
|
events = puppetdb.events('["=", "report", "{0}"]'.format(
|
||||||
report.hash_))
|
report.hash_))
|
||||||
return render_template('report.html', report=report,
|
return render_template(
|
||||||
events=yield_or_stop(events))
|
'report.html',
|
||||||
|
report=report,
|
||||||
|
events=yield_or_stop(events))
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/facts')
|
@app.route('/facts')
|
||||||
def facts():
|
def facts():
|
||||||
"""Displays an alphabetical list of all facts currently known to
|
"""Displays an alphabetical list of all facts currently known to
|
||||||
@@ -175,13 +248,31 @@ def facts():
|
|||||||
sorted_facts_dict = sorted(facts_dict.items())
|
sorted_facts_dict = sorted(facts_dict.items())
|
||||||
return render_template('facts.html', facts_dict=sorted_facts_dict)
|
return render_template('facts.html', facts_dict=sorted_facts_dict)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/fact/<fact>')
|
@app.route('/fact/<fact>')
|
||||||
def fact(fact):
|
def fact(fact):
|
||||||
"""Fetches the specific fact from PuppetDB and displays its value per
|
"""Fetches the specific fact from PuppetDB and displays its value per
|
||||||
node for which this fact is known."""
|
node for which this fact is known."""
|
||||||
return Response(stream_with_context(stream_template('fact.html',
|
# 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',
|
||||||
name=fact,
|
name=fact,
|
||||||
facts=yield_or_stop(puppetdb.facts(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'))
|
@app.route('/query', methods=('GET', 'POST'))
|
||||||
def query():
|
def query():
|
||||||
@@ -190,9 +281,33 @@ def query():
|
|||||||
of the possible exceptions are being handled just yet. This will return
|
of the possible exceptions are being handled just yet. This will return
|
||||||
the JSON of the response or a message telling you what whent wrong /
|
the JSON of the response or a message telling you what whent wrong /
|
||||||
why nothing was returned."""
|
why nothing was returned."""
|
||||||
form = QueryForm()
|
if app.config['ENABLE_QUERY']:
|
||||||
if form.validate_on_submit():
|
form = QueryForm()
|
||||||
result = get_or_abort(puppetdb._query, form.endpoints.data,
|
if form.validate_on_submit():
|
||||||
|
result = get_or_abort(
|
||||||
|
puppetdb._query,
|
||||||
|
form.endpoints.data,
|
||||||
query='[{0}]'.format(form.query.data))
|
query='[{0}]'.format(form.query.data))
|
||||||
return render_template('query.html', form=form, result=result)
|
return render_template('query.html', form=form, result=result)
|
||||||
return render_template('query.html', form=form)
|
return render_template('query.html', form=form)
|
||||||
|
else:
|
||||||
|
log.warn('Access to query interface disabled by administrator..')
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/metrics')
|
||||||
|
def metrics():
|
||||||
|
metrics = 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()))
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
PUPPETDB_HOST='localhost'
|
PUPPETDB_HOST = 'localhost'
|
||||||
PUPPETDB_PORT=8080
|
PUPPETDB_PORT = 8080
|
||||||
PUPPETDB_SSL=False
|
PUPPETDB_SSL_VERIFY = True
|
||||||
PUPPETDB_KEY=None
|
PUPPETDB_KEY = None
|
||||||
PUPPETDB_CERT=None
|
PUPPETDB_CERT = None
|
||||||
PUPPETDB_TIMEOUT=20
|
PUPPETDB_TIMEOUT = 20
|
||||||
PUPPETDB_EXPERIMENTAL=False
|
DEV_LISTEN_HOST = '127.0.0.1'
|
||||||
LOGLEVEL='info'
|
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 flask.ext.wtf import Form
|
||||||
from wtforms import RadioField, TextAreaField, validators
|
from wtforms import RadioField, TextAreaField, validators
|
||||||
|
|
||||||
|
|
||||||
class QueryForm(Form):
|
class QueryForm(Form):
|
||||||
"""The form used to allow freeform queries to be executed against
|
"""The form used to allow freeform queries to be executed against
|
||||||
PuppetDB."""
|
PuppetDB."""
|
||||||
query = TextAreaField('Query', [validators.Required(
|
query = TextAreaField('Query', [validators.Required(
|
||||||
message='A query is required.')])
|
message='A query is required.')])
|
||||||
endpoints = RadioField('API endpoint', choices = [
|
endpoints = RadioField('API endpoint', choices = [
|
||||||
('nodes', 'Nodes'),
|
('nodes', 'Nodes'),
|
||||||
('resources', 'Resources'),
|
('resources', 'Resources'),
|
||||||
('facts', 'Facts'),
|
('facts', 'Facts'),
|
||||||
('fact-names', 'Fact Names'),
|
('fact-names', 'Fact Names'),
|
||||||
('reports', 'Reports'),
|
('reports', 'Reports'),
|
||||||
('events', 'Events'),
|
('events', 'Events'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,22 @@ $ = jQuery
|
|||||||
$ ->
|
$ ->
|
||||||
$('.nodes').tablesorter(
|
$('.nodes').tablesorter(
|
||||||
headers:
|
headers:
|
||||||
3:
|
4:
|
||||||
sorter: false
|
sorter: false
|
||||||
sortList: [[0,0]]
|
sortList: [[1,0]]
|
||||||
)
|
)
|
||||||
|
|
||||||
$('.facts').tablesorter(
|
$('.facts').tablesorter(
|
||||||
sortList: [[0,0]]
|
sortList: [[0,0]]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
$('.dashboard').tablesorter(
|
||||||
|
headers:
|
||||||
|
2:
|
||||||
|
sorter: false
|
||||||
|
sortList: [[0, 1]]
|
||||||
|
)
|
||||||
|
|
||||||
$('input.filter-table').parent('div').removeClass('hide')
|
$('input.filter-table').parent('div').removeClass('hide')
|
||||||
$("input.filter-table").on "keyup", (e) ->
|
$("input.filter-table").on "keyup", (e) ->
|
||||||
rex = new RegExp($(this).val(), "i")
|
rex = new RegExp($(this).val(), "i")
|
||||||
|
|||||||
@@ -49,3 +49,56 @@ th.headerSortDown:after {
|
|||||||
.navbar .brand:hover {
|
.navbar .brand:hover {
|
||||||
color: #fff;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
1770
puppetboard/static/js/Chart.js
vendored
Normal file
@@ -8,17 +8,24 @@
|
|||||||
|
|
||||||
$('.nodes').tablesorter({
|
$('.nodes').tablesorter({
|
||||||
headers: {
|
headers: {
|
||||||
3: {
|
4: {
|
||||||
sorter: false
|
sorter: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sortList: [[0, 0]]
|
sortList: [[1, 0]]
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.facts').tablesorter({
|
$('.facts').tablesorter({
|
||||||
sortList: [[0, 0]]
|
sortList: [[0, 0]]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('.dashboard').tablesorter({
|
||||||
|
headers: {
|
||||||
|
2: { sorter: false }
|
||||||
|
},
|
||||||
|
sortList: [[0, 1]]
|
||||||
|
});
|
||||||
|
|
||||||
$('input.filter-table').parent('div').removeClass('hide');
|
$('input.filter-table').parent('div').removeClass('hide');
|
||||||
|
|
||||||
$("input.filter-table").on("keyup", function(e) {
|
$("input.filter-table").on("keyup", function(e) {
|
||||||
|
|||||||
11
puppetboard/templates/403.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% block row_fluid %}
|
||||||
|
<div class="container" style="margin-bottom:55px;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="span12">
|
||||||
|
<h2>Permission Denied</h2>
|
||||||
|
<p>What you were looking for has been disabled by the administrator.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
<div class="container" style="margin-bottom:55px;">
|
<div class="container" style="margin-bottom:55px;">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="span12">
|
<div class="span12">
|
||||||
<h2>Experimental Disabled</h2>
|
<h2>Feature unavailable</h2>
|
||||||
<p>You're trying to access a feature restricted to PuppetDB's Experimental API but haven't configured Puppetboard to allow this.</p>
|
<p>You've configured Puppetboard with an API version that does not support this feature.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +1,99 @@
|
|||||||
{% 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;">
|
<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">
|
<input {% if autofocus %} autofocus="autofocus" {% endif %} style="width:100%" type="text" class="filter-table input-medium search-query" placeholder="Type here to filter">
|
||||||
</div>
|
</div>
|
||||||
<table class="filter-table table table-striped {% if condensed %}table-condensed{% endif%}" style="table-layout:fixed">
|
<table class="filter-table table table-striped {% if condensed %}table-condensed{% endif%}" style="table-layout:fixed">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
{% if show_node %}
|
||||||
|
<th>Node</th>
|
||||||
|
{% else %}
|
||||||
<th>Fact</th>
|
<th>Fact</th>
|
||||||
|
{% endif %}
|
||||||
|
{% if show_value %}
|
||||||
<th>Value</th>
|
<th>Value</th>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="searchable">
|
<tbody class="searchable">
|
||||||
{% for fact in facts %}
|
{% for fact in facts %}
|
||||||
<tr>
|
<tr>
|
||||||
{% if show_node %}
|
{% if show_node %}
|
||||||
<td>{{fact.node}}</td>
|
<td><a href="{{url_for('node', node_name=fact.node)}}">{{fact.node}}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>{{fact.name}}</td>
|
<td><a href="{{url_for('fact', fact=fact.name)}}">{{fact.name}}</a></td>
|
||||||
|
{% endif %}
|
||||||
|
{% 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 %}
|
{% endif %}
|
||||||
<td style="word-wrap:break-word">{{fact.value}}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
{% macro facts_graph(facts, autofocus=False, condensed=False, show_node=False, margin_top=20, margin_bottom=20) -%}
|
||||||
|
<script src="{{url_for('static', filename='js/Chart.js')}}"></script>
|
||||||
|
<canvas id="factChart" width="300" height="300"></canvas>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var colors = ["#9B59B6", "#3498DB", "#2ECC71", "#1ABC9C", "#F1C40F", "#E67E22", "#E74C3C"];
|
||||||
|
var len_colors = colors.length;
|
||||||
|
var data = [
|
||||||
|
{% for fact in facts|groupby('value') %}
|
||||||
|
{
|
||||||
|
label: "{{ fact.grouper }}",
|
||||||
|
value: {{ fact.list|length }}
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
data[i].color = colors[i % len_colors];
|
||||||
|
}
|
||||||
|
var sorted_data = data.sort(function(a,b) { return parseFloat(b.value) - parseFloat(a.value)});
|
||||||
|
var top7 = sorted_data.slice(0,7);
|
||||||
|
var bottom = data.slice(7, -1);
|
||||||
|
var bottom_sum = 0;
|
||||||
|
for (var i = 0; i < bottom.length; i++) {
|
||||||
|
bottom_sum += bottom[i].value;
|
||||||
|
}
|
||||||
|
top7.push({ label: "Other", value: bottom_sum, color: "#B30202" });
|
||||||
|
var ctx = document.getElementById("factChart").getContext("2d");
|
||||||
|
new Chart(ctx).Pie(top7);
|
||||||
|
</script>
|
||||||
|
{%- endmacro %}
|
||||||
|
{% macro facts_graph_value(facts, autofocus=False, condensed=False, show_node=False, margin_top=20, margin_bottom=20) -%}
|
||||||
|
<script src="{{url_for('static', filename='js/Chart.js')}}"></script>
|
||||||
|
<canvas id="factChart_value" width="300" height="300"></canvas>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var colors = ["#9B59B6", "#3498DB", "#2ECC71", "#1ABC9C", "#F1C40F", "#E67E22", "#E74C3C"];
|
||||||
|
var len_colors = colors.length;
|
||||||
|
var data = [
|
||||||
|
{% for fact in facts|groupby('value') %}
|
||||||
|
{
|
||||||
|
label: "{{ fact.grouper }}",
|
||||||
|
value: {{ fact.list|length }}
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
data[i].color = colors[i % len_colors];
|
||||||
|
}
|
||||||
|
var ctx = document.getElementById("factChart_value").getContext("2d");
|
||||||
|
new Chart(ctx).Pie(data.sort(function(a,b) { return parseInt(a.label) - parseInt(b.label)}));
|
||||||
|
</script>
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro reports_table(reports, nodename, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True) -%}
|
{% macro reports_table(reports, nodename, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True) -%}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% import '_macros.html' as macros %}
|
{% import '_macros.html' as macros %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{{name}}</h1>
|
<h1>{{name}}{% if value %}/{{value}}{% endif %} ({{facts|length}})</h1>
|
||||||
{{macros.facts_table(facts, autofocus=True, show_node=True, margin_bottom=10)}}
|
{{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)}}
|
||||||
|
{% 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 %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -1,11 +1,38 @@
|
|||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% block row_fluid %}
|
{% block row_fluid %}
|
||||||
<div class="span12">
|
|
||||||
<div class='alert alert-info'>
|
|
||||||
We need something fancy here.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container" style="margin-bottom:55px;">
|
<div class="container" style="margin-bottom:55px;">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<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="row">
|
||||||
<div class="span12">
|
<div class="span12">
|
||||||
<div class="span4 stat">
|
<div class="span4 stat">
|
||||||
@@ -22,16 +49,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="span12">
|
<div class="span12">
|
||||||
<div class="span4 stat">
|
{% if nodes %}
|
||||||
<h1>{{metrics['mean_failed_commands']}}</h1>
|
<h2>Nodes status detail ({{nodes|length}})</h2>
|
||||||
<span>Mean command failures</span>
|
<table class='dashboard table table-striped table-condensed'>
|
||||||
</div>
|
<thead>
|
||||||
<div class="span4 stat offset4">
|
<tr>
|
||||||
<h1>{{metrics['mean_command_time']}}s</h1>
|
<th style="width:220px;">Status</th>
|
||||||
<span>Mean command execution time</span>
|
<th style="width:600px;">Hostname</th>
|
||||||
</div>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
('nodes', 'Nodes'),
|
('nodes', 'Nodes'),
|
||||||
('facts', 'Facts'),
|
('facts', 'Facts'),
|
||||||
('reports', 'Reports'),
|
('reports', 'Reports'),
|
||||||
|
('metrics', 'Metrics'),
|
||||||
('query', 'Query'),
|
('query', 'Query'),
|
||||||
] %}
|
] %}
|
||||||
<li{% if endpoint == request.endpoint %} class=active{% endif
|
<li{% if endpoint == request.endpoint %} class=active{% endif
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="//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="//cdn.jsdelivr.net/tablesorter/2.0.3/jquery.tablesorter.min.js"></script>
|
||||||
<script src="{{ url_for('static', filename='js/moment.js')}}"></script>
|
<script src="{{ url_for('static', filename='js/moment.js')}}"></script>
|
||||||
|
|||||||
23
puppetboard/templates/metric.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Metric
|
||||||
|
{% set name = "%s…"|format(name[:75])|safe if name|length > 75%}
|
||||||
|
<small>{{name}}</small>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tbody>
|
||||||
|
{% for key,value in metric %}
|
||||||
|
<tr>
|
||||||
|
<td>{{key}}</td>
|
||||||
|
{% if value is mapping %}
|
||||||
|
<td><pre>{{value|jsonprint}}</pre></td>
|
||||||
|
{% else %}
|
||||||
|
<td>{{value}}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock content %}
|
||||||
9
puppetboard/templates/metrics.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Metrics</h1>
|
||||||
|
<ul>
|
||||||
|
{% for key,value in metrics %}
|
||||||
|
<li><a href="{{url_for('metric', metric=value)}}">{{key}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock content %}
|
||||||
@@ -2,45 +2,36 @@
|
|||||||
{% import '_macros.html' as macros %}
|
{% import '_macros.html' as macros %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span4">
|
<div class="span12">
|
||||||
<h1>Details</h1>
|
<h1>Details</h1>
|
||||||
<table class="table table-striped table-condensed" style="table-layout:fixed">
|
<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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width:140px">Hostname</td>
|
|
||||||
<td style="word-wrap:break-word"><b>{{node.name}}</b></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>
|
<td rel="utctimestamp">{{node.facts_timestamp}}</td>
|
||||||
</tr>
|
<td rel="utctimestamp">{{node.catalog_timestamp}}</td>
|
||||||
{% if config.PUPPETDB_EXPERIMENTAL %}
|
|
||||||
<tr>
|
|
||||||
<td>Report uploaded at</td>
|
|
||||||
<td rel="utctimestamp">{{node.report_timestamp}}</td>
|
<td rel="utctimestamp">{{node.report_timestamp}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if config.PUPPETDB_EXPERIMENTAL %}
|
</div>
|
||||||
<div class="span4">
|
<div class="row-fluid">
|
||||||
|
<div class="span6">
|
||||||
<h1>Facts</h1>
|
<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>
|
||||||
<div class="span4">
|
<div class="span6">
|
||||||
<h1>Reports</h1>
|
<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)}}
|
{{ macros.reports_table(reports, node.name, condensed=True, hash_truncate=True, show_conf_col=False, show_agent_col=False, show_host_col=False)}}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
<div class="span8">
|
|
||||||
<h1>Facts</h1>
|
|
||||||
{{macros.facts_table(facts, condensed=True, margin_top=10)}}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -18,20 +18,28 @@
|
|||||||
<table class='nodes table table-striped table-condensed'>
|
<table class='nodes table table-striped table-condensed'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
<th>Hostname</th>
|
<th>Hostname</th>
|
||||||
<th>Catalog compiled at</th>
|
<th>Catalog compiled at</th>
|
||||||
{% if config.PUPPETDB_EXPERIMENTAL %}
|
|
||||||
<th>Last report</th>
|
<th>Last report</th>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
{% endif %}
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="searchable">
|
<tbody class="searchable">
|
||||||
{% for node in nodes %}
|
{% for node in nodes %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td><a href="{{url_for('report_latest', node_name=node.name)}}">
|
||||||
|
<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><a href="{{url_for('node', node_name=node.name)}}">{{node.name}}</a></td>
|
||||||
<td rel="utctimestamp">{{node.catalog_timestamp}}</td>
|
<td rel="utctimestamp">{{node.catalog_timestamp}}</td>
|
||||||
{% if config.PUPPETDB_EXPERIMENTAL %}
|
|
||||||
<td>
|
<td>
|
||||||
{% if node.report_timestamp %}
|
{% if node.report_timestamp %}
|
||||||
<span rel="utctimestamp">{{ node.report_timestamp }}</span>
|
<span rel="utctimestamp">{{ node.report_timestamp }}</span>
|
||||||
@@ -40,9 +48,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<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>
|
<a class="btn btn-small btn-primary" href="{{url_for('reports_node', node=node.name)}}">Reports</a>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% block row_fluid %}
|
{% block row_fluid %}
|
||||||
<div class="span12">
|
<div class="span12">
|
||||||
<div class="alert">
|
<div class="alert">
|
||||||
This is highly exeprimental and will likely set your server on fire.
|
This is highly experimental and will likely set your server on fire.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" style="margin-bottom:55px;">
|
<div class="container" style="margin-bottom:55px;">
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="control-group {% if form.query.errors %} error {% endif %}">
|
<div class="control-group {% if form.query.errors %} error {% endif %}">
|
||||||
{{form.query.label(class_="control-label")}}
|
{{form.query.label(class_="control-label")}}
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
{{form.query(class_="input-block-level", autofocus="autofocus", rows=5, placeholder="\"=\", \"name\" \"hostname\"")}}
|
{{form.query(class_="input-block-level", autofocus="autofocus", rows=5, placeholder="\"=\", \"name\", \"hostname\"")}}
|
||||||
{% if form.query.errors %}
|
{% if form.query.errors %}
|
||||||
<span class="help-inline">{% for error in form.query.errors %}{{error}}{% endfor %}</span>
|
<span class="help-inline">{% for error in form.query.errors %}{{error}}{% endfor %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="span12">
|
<div class="span12">
|
||||||
<h2>Result</h2>
|
<h2>Result</h2>
|
||||||
<pre><code>{{ result|tojson|replace(", ", ",\n") }}</code></pre>
|
<pre><code>{{result|jsonprint}}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{report.node}}</td>
|
<td><a href="{{url_for('node', node_name=report.node)}}">{{ report.node }}</a></td>
|
||||||
<td>
|
<td>
|
||||||
{{report.version}}
|
{{report.version}}
|
||||||
</td>
|
</td>
|
||||||
@@ -39,15 +39,22 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for event in events %}
|
{% for event in events %}
|
||||||
{% if not event.failed and event.item['old'] != event.item['new'] %}
|
{% 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 %}
|
{% elif event.failed %}
|
||||||
<tr class='error'>
|
<tr id='event-{{loop.index}}' class='error event'>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{event.item['type']}}[{{event.item['title']}}]</td>
|
<td>{{event.item['type']}}[{{event.item['title']}}]</td>
|
||||||
<td>{{event.status}}</td>
|
<td>{{event.status}}</td>
|
||||||
<td>{{event.item['old']}}</td>
|
<td>{{event.item['old']}}</td>
|
||||||
<td>{{event.item['new']}}</td>
|
<td>{{event.item['new']}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class='message' colspan='4'>
|
||||||
|
<div id='message-event-{{loop.index}}'>
|
||||||
|
{{event.item['message']}}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -57,6 +64,10 @@
|
|||||||
<script type='text/javascript'>
|
<script type='text/javascript'>
|
||||||
jQuery(function ($) {
|
jQuery(function ($) {
|
||||||
$("[rel=tooltip]").tooltip();
|
$("[rel=tooltip]").tooltip();
|
||||||
|
$(".event").click(function() {
|
||||||
|
$("#message-" + this.id).slideToggle(200);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from requests.exceptions import HTTPError, ConnectionError
|
import json
|
||||||
from pypuppetdb.errors import EmptyResponseError, ExperimentalDisabledError
|
|
||||||
|
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=(',', ': ') )
|
||||||
|
|
||||||
from flask import abort, flash
|
|
||||||
|
|
||||||
def get_or_abort(func, *args, **kwargs):
|
def get_or_abort(func, *args, **kwargs):
|
||||||
"""Execute the function with its arguments and handle the possible
|
"""Execute the function with its arguments and handle the possible
|
||||||
@@ -18,8 +24,6 @@ def get_or_abort(func, *args, **kwargs):
|
|||||||
abort(e.response.status_code)
|
abort(e.response.status_code)
|
||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
abort(500)
|
abort(500)
|
||||||
except ExperimentalDisabledError:
|
|
||||||
abort(412)
|
|
||||||
except EmptyResponseError:
|
except EmptyResponseError:
|
||||||
abort(204)
|
abort(204)
|
||||||
|
|
||||||
@@ -40,22 +44,12 @@ def yield_or_stop(generator):
|
|||||||
generators and handle certain errors.
|
generators and handle certain errors.
|
||||||
|
|
||||||
Since this is also used in streaming responses where we can't just abort
|
Since this is also used in streaming responses where we can't just abort
|
||||||
a request we always yield empty and then raise StopIteration.
|
a request we raise StopIteration.
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
yield next(generator)
|
yield next(generator)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
raise
|
raise
|
||||||
except ExperimentalDisabledError:
|
except (EmptyResponseError, ConnectionError, HTTPError):
|
||||||
yield
|
|
||||||
raise StopIteration
|
|
||||||
except EmptyResponseError:
|
|
||||||
yield
|
|
||||||
raise StopIteration
|
|
||||||
except ConnectionError:
|
|
||||||
yield
|
|
||||||
raise StopIteration
|
|
||||||
except HTTPError:
|
|
||||||
yield
|
|
||||||
raise StopIteration
|
raise StopIteration
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
Flask==0.10.1
|
Flask==0.10.1
|
||||||
Flask-WTF==0.8.4
|
Flask-WTF==0.8.4
|
||||||
pypuppetdb=0.0.1
|
Jinja2==2.7
|
||||||
|
MarkupSafe==0.18
|
||||||
|
WTForms==1.0.4
|
||||||
|
Werkzeug==0.9.3
|
||||||
|
itsdangerous==0.22
|
||||||
|
pypuppetdb==0.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: 287 KiB After Width: | Height: | Size: 389 KiB |
BIN
screenshots/metric.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
screenshots/metrics.png
Normal file
|
After Width: | Height: | Size: 367 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 241 KiB |
|
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',
|
||||||
|
],
|
||||||
|
)
|
||||||