109 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
Daniele Sluijters
c6c4bc1679 README: Add newline after code-block. 2013-10-14 13:05:22 +02:00
Daniele Sluijters
9be5aaebd9 0.0.2: Release.
This is the second 'release' for Puppetboard introducing some new
features and compatibility with PuppetDB 1.5 / v3 API.
2013-10-14 12:53:25 +02:00
Daniele Sluijters
a75b08f882 nodes: Check PUPPETDB_API, not _EXPERIMENTAL. 2013-10-14 12:53:25 +02:00
Daniele Sluijters
f85f0fa864 requirements: Upgrade to pypuppetdb 0.0.4 2013-10-14 12:53:25 +02:00
Daniele Sluijters
095967e445 Merge pull request #20 from nibalizer/graph_fix
_macros.html: Sorting and bucketing facts graphs.
2013-10-13 12:32:48 -07:00
Spencer Krum
8a56e83b69 puppetboard/templates/_macros.html
This adds sorting and bucketing to the two graphs in the facts
view.
2013-10-13 12:25:56 -07:00
Daniele Sluijters
934f90c12a _macros.html: Graphs a bit smaller, FlatUI colors.
Currently the graphs are shown at the top of the page and because of
that really take up a lot of screen real estate. This commit makes the
graphs a bit smaller but we really need to rethink the layout here.

Additionally we switch the colors to the FlatUI swatches to match the
rest of the theme better.
2013-10-13 15:31:33 +02:00
Spencer Krum
0422b2ccd0 _macros.html: mod ensure different adjecent colors 2013-10-13 15:01:04 +02:00
Spencer Krum
e1603608bc _macros.html: Add a second Pie chart to facts/<fact>
The second chart is in order of the value of the fact. So in the
uptime fact we see 1 day before 2 days before 50 days.
2013-10-13 15:00:03 +02:00
Spencer Krum
23af033cbb facts: Add graph for facts endpoint. 2013-10-13 14:59:22 +02:00
Spencer Krum
e71f30ab50 Add flag to enable or disable the query page
Puppetboard is an excelent radiator of information, but sometimes
we want to expose information to users we don't trust giving full
access to the PuppetDB query language.

I would reccomend that Puppetboard be run twice. One, with query
enabled, run on a port behind apache login. Another, with query
disabled, run unproxied for the unwashed masses.

Closes #10.
2013-10-13 14:32:24 +02:00
Daniele Sluijters
cc87e54cea Support PuppetDB API v3.
Recently changes were made to pypuppetdb to support, at a basic level,
the v3 API introduced with PuppetDB 1.5.

This commit changes some internals of Puppetboard to handle the new
situation:
 * The experimental endpoints are gone, so is the ExperimentalDisabled
   Error;
 * v2 API can no longer access Reports/Events so we now check that we're
   talking at least v3 API;
 * Introduce a configuration setting PUPPETDB_API which takes an integer
   repersenting the API version we want to talk.
2013-10-11 11:20:34 +02:00
Daniele Sluijters
3b4db8f37e Merge pull request #19 from daniellawrence/fact_table_template_title_and_links
Add links for facts and nodes in fact table macro
2013-10-11 00:55:31 -07:00
Daniel Lawrence
3fd8a0aad4 Add links for facts and nodes in fact table macro
Added links to the node page from the facts_table if show_nodes is True.
Added links to the facts page from the facts_table if show_nodes is
False.
Added logic to switch TH table from facts to nodes if show_nodes is True.
2013-10-11 14:46:39 +11:00
Daniele Sluijters
9b662a661e README: Fix the issue link 2013-10-08 14:30:24 +02:00
Daniele Sluijters
cfb1383025 README: Add a Getting Help section
[noci]
2013-10-08 14:29:41 +02:00
Daniele Sluijters
8db60d14bd nodes: Only show report button if we have a report
Based on if the report_timestamp is truthy we'll show the reports
button, instead of always having it show up.
2013-08-22 13:17:48 -07:00
Daniele Sluijters
8cf181a9e8 Add screenshots of the new metrics tab. 2013-08-22 07:37:18 -07:00
Daniele Sluijters
543f706fd7 Merge pull request #7 from nicklewis/add-metric-name
Add the name of the metric to the metric page
2013-08-21 17:08:45 -07:00
Nick Lewis
644e169a7f Add the name of the metric to the metric page 2013-08-21 17:07:24 -07:00
Daniele Sluijters
b25d85bd32 dev: Import DEV_LISTEN_HOST 2013-08-21 16:57:12 -07:00
Daniele Sluijters
9918ec8f4b Merge pull request #6 from nicklewis/sort-metrics
Sort list of metrics and metrics data
2013-08-21 16:55:38 -07:00
Daniele Sluijters
da68bb259b Merge pull request #4 from nibalizer/dev_expose_config
dev.py: exposing some parameters
2013-08-21 16:53:01 -07:00
Nick Lewis
3c05071aef Sort list of metrics and metrics data
This makes the metrics a lot easier to scan, because it groups similar
metrics (like all the HTTP metrics) as well as the percentiles, etc.
2013-08-21 16:52:52 -07:00
Spencer Krum
cb64b73832 dev.py: exposing some parameters 2013-08-21 16:37:25 -07:00
Daniele Sluijters
c3821e777f Add a Metrics tab.
This gives you access to all metrics.
2013-08-21 16:21:02 -07:00
Daniele Sluijters
c04d45f602 README: add a TOC 2013-08-21 14:59:09 -07:00
Daniele Sluijters
d900ccf09a overview: Fix string formatting for py26. 2013-08-15 00:55:58 +02:00
Daniele Sluijters
8b3f3ea61e Merge pull request #3 from nibalizer/readme_forge
README.rst: adding forge command to readme
2013-08-14 15:53:42 -07:00
Spencer Krum
f273d24f80 README.rst: adding forge command to readme 2013-08-14 11:04:53 -07:00
Daniele Sluijters
d152d8e3a1 README: Got Spencer's first and last name mixed up 2013-08-12 20:12:56 +02:00
Daniele Sluijters
c64a2b79b2 README: Forgot the newlines for bullets. 2013-08-12 19:26:23 +02:00
Daniele Sluijters
083da989de README: Add a third party section. 2013-08-12 19:25:13 +02:00
Daniele Sluijters
c3a9b5e81c Merge pull request #2 from nibalizer/typo_fix
templates/query.html Fix typo in placeholder text
2013-08-12 01:32:07 -07:00
Spencer Krum
00d0f96914 templates/query.html Fix typo in placeholder text
The placeholder text on the query form wasn't correct syntax.
2013-08-11 17:10:47 -07:00
Daniele Sluijters
7b71eb39d2 requirements. Update to pypuppetdb 0.0.2.
There was a stupid packaging error in pypuppetdb 0.0.1 preventing
successful installation of it and its depedencies.
2013-08-09 16:54:45 +02:00
Daniele Sluijters
1a178ef2af requirements: Get rid of wsgiref.
http://stackoverflow.com/questions/6627035
2013-08-09 09:35:37 +02:00
Daniele Sluijters
4d80b6c128 template/query: Fix a typo in experimental.
Thanks @geekygirldawn!
2013-08-08 20:57:40 +02:00
Daniele Sluijters
462fcbf76c requirements: Add missing dependencies.
No idea what happend but requirements.txt should list all the
dependencies, not just the 'top' ones.
2013-08-08 18:17:48 +02:00
Daniele Sluijters
c8825d3d92 Merge pull request #1 from hunner/fix_reqs
Missing = sign
2013-08-08 09:06:59 -07:00
Hunter Haugen
1d705d04dd Missing = sign 2013-08-07 17:38:39 -07:00
Daniele Sluijters
88d1944b4b utils: Remove unused import. 2013-08-07 16:17:40 +02:00
Daniele Sluijters
bda3adc078 utils: Group the exceptions in yield_or_abort.
Additionally, the empty yield is rather unnecessary.
2013-08-07 16:15:34 +02:00
42 changed files with 2868 additions and 244 deletions

85
CHANGELOG.rst Normal file
View File

@@ -0,0 +1,85 @@
#########
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
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
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,12 +17,14 @@ 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-experimental.png
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/overview.png
:alt: View of a node
:width: 1024
:height: 700
:align: center
.. contents::
Word of caution
===============
@@ -38,60 +40,393 @@ 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``
* 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
^^^^^^^^^^^^^^^^^
Configuration
=============
First we need to create the necessary directories:
Puppetboard has some configuration settings, their defaults can
be viewed in ``puppetboard/default_settings.py``.
.. code-block:: bash
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.
$ mkdir -p /var/www/puppetboard
$ chown www-data:www-data /var/www/puppetboard
Experimental
------------
Pypuppetdb and Puppetboard can query and display information from
PuppetDB's experimental API endpoints.
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``.
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.
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 /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:: 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
============
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
============
@@ -128,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
@@ -140,14 +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: Metrics view
:width: 1024
:height: 700
:align: center
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/metric.png
:alt: Single metric
:width: 1024
:height: 700
:align: center
@@ -158,38 +529,8 @@ Screenshots
:height: 700
: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
:alt: Error message
:alt: Error page
:width: 1024
:height: 700
:align: center

5
dev.py
View File

@@ -2,7 +2,8 @@ from __future__ import unicode_literals
from __future__ import absolute_import
from puppetboard.app import app
from puppetboard.default_settings import DEV_LISTEN_HOST, DEV_LISTEN_PORT
if __name__ == '__main__':
app.debug=True
app.run('127.0.0.1')
app.debug = True
app.run(DEV_LISTEN_HOST, DEV_LISTEN_PORT)

View File

@@ -4,19 +4,21 @@ from __future__ import absolute_import
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
from pypuppetdb.errors import ExperimentalDisabledError
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=3,
host=app.config['PUPPETDB_HOST'],
port=app.config['PUPPETDB_PORT'],
ssl=app.config['PUPPETDB_SSL'],
ssl_verify=app.config['PUPPETDB_SSL_VERIFY'],
ssl_key=app.config['PUPPETDB_KEY'],
ssl_cert=app.config['PUPPETDB_CERT'],
timeout=app.config['PUPPETDB_TIMEOUT'],
experimental=app.config['PUPPETDB_EXPERIMENTAL'])
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,47 +52,89 @@ 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 ExperimentalDisabled errors."""
"""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': "{:10.6f}".format(avg_resources_node['Value']),
'mean_failed_commands': mean_failed_commands['MeanRate'],
'mean_command_time': "{:10.6f}".format(mean_command_time['MeanRate']),
'avg_resources_node': "{0:10.0f}".format(avg_resources_node['Value']),
}
return render_template('index.html', metrics=metrics)
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():
@@ -100,8 +147,20 @@ def nodes():
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):
@@ -111,55 +170,69 @@ def node(node_name):
"""
node = get_or_abort(puppetdb.node, node_name)
facts = node.facts()
if app.config['PUPPETDB_EXPERIMENTAL']:
reports = ten_reports(node.reports())
else:
reports = iter([])
return render_template('node.html', node=node, facts=yield_or_stop(facts),
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_EXPERIMENTAL']:
return render_template('reports.html')
else:
log.warn('Access to experimental endpoint not allowed.')
abort(412)
@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_EXPERIMENTAL']:
reports = ten_reports(yield_or_stop(
puppetdb.reports('["=", "certname", "{0}"]'.format(node))))
else:
log.warn('Access to experimental endpoint not allowed.')
abort(412)
return render_template('reports_node.html', reports=reports,
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:
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_EXPERIMENTAL']:
report and their status.
"""
reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node))
else:
log.warn('Access to experimental endpoint not allowed.')
abort(412)
for report in reports:
if report.hash_ == report_id:
events = puppetdb.events('["=", "report", "{0}"]'.format(
report.hash_))
return render_template('report.html', report=report,
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
@@ -175,13 +248,31 @@ 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."""
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,
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'))
def query():
@@ -190,9 +281,33 @@ def query():
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 /
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,
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 = 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()))

View File

@@ -1,8 +1,11 @@
PUPPETDB_HOST='localhost'
PUPPETDB_PORT=8080
PUPPETDB_SSL=False
PUPPETDB_KEY=None
PUPPETDB_CERT=None
PUPPETDB_TIMEOUT=20
PUPPETDB_EXPERIMENTAL=False
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,6 +4,7 @@ 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."""
@@ -17,4 +18,3 @@ class QueryForm(Form):
('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;
}

1770
puppetboard/static/js/Chart.js vendored Normal file

File diff suppressed because it is too large Load Diff

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

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

View File

@@ -3,8 +3,8 @@
<div class="container" style="margin-bottom:55px;">
<div class="row">
<div class="span12">
<h2>Experimental Disabled</h2>
<p>You're trying to access a feature restricted to PuppetDB's Experimental API but haven't configured Puppetboard to allow this.</p>
<h2>Feature unavailable</h2>
<p>You've configured Puppetboard with an API version that does not support this feature.</p>
</div>
</div>
</div>

View File

@@ -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;">
<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 %}
<th>Node</th>
{% else %}
<th>Fact</th>
{% endif %}
{% if show_value %}
<th>Value</th>
{% endif %}
</tr>
</thead>
<tbody class="searchable">
{% for fact in facts %}
<tr>
{% if show_node %}
<td>{{fact.node}}</td>
<td><a href="{{url_for('node', node_name=fact.node)}}">{{fact.node}}</a></td>
{% 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 %}
<td style="word-wrap:break-word">{{fact.value}}</td>
</tr>
{% endfor %}
</tbody>
</table>
{%- 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) -%}
<div class="alert alert-info">

View File

@@ -1,6 +1,12 @@
{% extends 'layout.html' %}
{% import '_macros.html' as macros %}
{% block content %}
<h1>{{name}}</h1>
{{macros.facts_table(facts, autofocus=True, show_node=True, margin_bottom=10)}}
<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)}}
{% 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>
{% 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

@@ -25,6 +25,7 @@
('nodes', 'Nodes'),
('facts', 'Facts'),
('reports', 'Reports'),
('metrics', 'Metrics'),
('query', 'Query'),
] %}
<li{% if endpoint == request.endpoint %} class=active{% endif
@@ -54,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

@@ -0,0 +1,23 @@
{% extends 'layout.html' %}
{% 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>
<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 %}

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

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_EXPERIMENTAL %}
<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_EXPERIMENTAL %}
<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_EXPERIMENTAL %}
<td>
{% if node.report_timestamp %}
<span rel="utctimestamp">{{ node.report_timestamp }}</span>
@@ -40,9 +48,11 @@
{% endif %}
</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>
</td>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -2,7 +2,7 @@
{% block row_fluid %}
<div class="span12">
<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 class="container" style="margin-bottom:55px;">
@@ -23,7 +23,7 @@
<div class="control-group {% if form.query.errors %} error {% endif %}">
{{form.query.label(class_="control-label")}}
<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 %}
<span class="help-inline">{% for error in form.query.errors %}{{error}}{% endfor %}</span>
{% endif %}
@@ -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,10 +1,16 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from requests.exceptions import HTTPError, ConnectionError
from pypuppetdb.errors import EmptyResponseError, ExperimentalDisabledError
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=(',', ': ') )
from flask import abort, flash
def get_or_abort(func, *args, **kwargs):
"""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)
except ConnectionError:
abort(500)
except ExperimentalDisabledError:
abort(412)
except EmptyResponseError:
abort(204)
@@ -40,22 +44,12 @@ def yield_or_stop(generator):
generators and handle certain errors.
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:
try:
yield next(generator)
except StopIteration:
raise
except ExperimentalDisabledError:
yield
raise StopIteration
except EmptyResponseError:
yield
raise StopIteration
except ConnectionError:
yield
raise StopIteration
except HTTPError:
yield
except (EmptyResponseError, ConnectionError, HTTPError):
raise StopIteration

View File

@@ -1,3 +1,9 @@
Flask==0.10.1
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

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: 287 KiB

After

Width:  |  Height:  |  Size: 389 KiB

BIN
screenshots/metric.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

BIN
screenshots/metrics.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 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