44 Commits

Author SHA1 Message Date
corey.hammerton
9334827826 Version bump to 0.1.2 2016-02-09 20:10:28 -05:00
Corey Hammerton
7c44854988 Merge pull request #162 from visibilityspots/master
Examples add to the readme
2016-02-09 19:40:59 -05:00
Corey Hammerton
3844d72bcb Merge pull request #219 from corey-hammerton/issue-218
puppetboard/app: Enabling environment filtering on overview metrics
2016-02-09 19:37:42 -05:00
Corey Hammerton
43ff09fbd4 Merge pull request #217 from corey-hammerton/issue-198
puppetboard/app: Disabling CSRF protection globally.

I still think this is a bad idea but I don't see any other way.
2016-02-09 19:37:18 -05:00
corey.hammerton
0111e52096 puppetboard/app: Enabling environment filtering on overview metrics
This fixes https://github.com/voxpupuli/puppetboard/issues/218

Adding environment filters to only report then number of managed nodes
and resources that are present in the current environment. Still using
the same metrics endpoint information for all environments.
2016-02-08 21:48:49 -05:00
Corey Hammerton
beef893b6a Merge pull request #163 from raphink/colors
Use more standard colors for Puppet run states
2016-02-06 19:02:13 -05:00
Corey Hammerton
7ed551dbe9 Merge pull request #216 from roidelapluie/fix-screenshots
Fix screenshots
2016-02-06 19:00:46 -05:00
corey.hammerton
492e1057e0 puppetboard/app: Disabling CSRF protection globally.
I really did not want to resort to this but I could not find any answers
from previously asked questions on the subject. The template is properly
creating the csrf_token hidden field which is being submitted to the server,
but I could not figure out why it was not using it it.

This fixes https://github.com/voxpupuli/puppetboard/issues/198

This will not be permanent, will have to look into upgrading the flask-wtf
and WTForms packages to the latest versions.
2016-02-06 18:55:45 -05:00
Julien Pivotto
e8bc0ccbb5 Fix screenshots 2016-02-05 16:06:10 +01:00
Corey Hammerton
ddf08b9b75 Merge pull request #215 from nibalizer/rename_vp
"Rename nibalizer to voxpupuli"
2016-02-04 20:05:33 -05:00
Spencer Krum
fbe646f196 Rename nibalizer to voxpupuli 2016-02-03 06:41:37 -08:00
Corey Hammerton
65c6251bac Merge pull request #214 from corey-hammerton/issue-213
templates/index: Directly referencing config.REFRESH_RATE
2016-02-01 20:44:24 -05:00
Corey Hammerton
5874a58965 Merge pull request #212 from corey-hammerton/issue-209
puppetboard/app.py: Don't filter on environments if all are selected
2016-02-01 20:44:16 -05:00
corey.hammerton
f9edda82b4 templates/index: Directly referencing config.REFRESH_RATE
Fixes https://github.com/voxpupuli/puppetboard/issues/213

Also changing the 'if' condition from '!=' to '>' since this application
can only work with unsigned integers
2016-02-01 19:59:24 -05:00
corey.hammerton
8fa0514585 puppetboard/app.py: If all environments are selected do not filter for event counts.
Fixes https://github.com/voxpupuli/puppetboard/issues/209

The event_count queries for the reports in the reports() and reports_node()
functions were always filtering on environments, even if all environments, *,
were selected. This update removes the environment clause from the query
string if 'All Environments' are selected
2016-02-01 19:43:46 -05:00
Corey Hammerton
fd29fe4261 Merge pull request #208 from corey-hammerton/readme
README.rst: Updating documentation with new configuration settings.
2016-02-01 19:25:33 -05:00
corey.hammerton
3ebde245ed requirements.txt: Updating the required version of pypuppetdb
The standard package requirements have a dependency on version 0.2.1
of pypuppetdb, updating the developer requirements list with the same version
2016-02-01 19:23:31 -05:00
Jan Collijs
21fee5b775 Feature: added examples to the readme 2016-01-22 10:11:02 +01:00
Raphaël Pinson
8e4af7c034 Use more standard colors for Puppet run states 2016-01-21 18:20:50 +01:00
Corey Hammerton
370c514745 README.rst: Updating documentation with new configuration settings.
Adding documentation for new configuration settings that have been
added since it was last updated.
2016-01-18 20:56:07 -05:00
Corey Hammerton
de22c61056 Merge pull request #204 from corey-hammerton/pypuppetdb-0.2.1
Using new Node and Catalog fields available in pypuppetdb 0.2.1

With the latest_report_hash field available on the Node object the report_latest can be partially deprecated. Will hopefully be removed in the next major/minor release.
2016-01-18 20:03:55 -05:00
Corey Hammerton
4226fdc368 Merge pull request #203 from corey-hammerton/issue-193
templates/catalog_compare: Switching from basic tables to fixed tables
2016-01-18 20:01:04 -05:00
Corey Hammerton
43e37fdf64 Using new Node and Catalog fields available in pypuppetdb 0.2.1
Showing the Code ID field in the catalogs page. This is currently
unused in PuppetDB as of 3.2.2 but may be useful when it will be used

If available, using the latest_report_hash field of the node object
in the index and nodes templates for the link to the latest report
available for the node.

Updating the report_latest function in app.py to query the nodes
endpoint and redirecting using the latest_report_hash field if available.
If not query the reports endpoint for the node's latest report.
2015-12-28 16:07:43 -05:00
Corey Hammerton
9b8c8332ef templates/catalog_compare: Switching from basic tables to fixed tables
Fixed https://github.com/puppet-community/puppetboard/issues/193

Fixed Semantic UI tables set a fixed position on the applied tables
that do not affect the alignment with narrow browser windows. Downside
of this approach is that users will have to scroll horizontally to view
the entire catalog comparison.
2015-12-28 15:25:59 -05:00
Corey Hammerton
40bd73415d Merge pull request #145 from nibalizer/graphing_facts
"Add clientversion to graphing facts"
2015-12-19 10:12:54 -05:00
Spencer Krum
596e850189 Add clientversion to graphing facts 2015-12-17 10:14:13 -08:00
Corey Hammerton
23b95dc1d2 Merge pull request #189 from corey-hammerton/reload_envs
puppetboard: Reloading the available environments on every page load.
2015-12-14 21:28:03 -05:00
Corey Hammerton
cc9b3de2ec puppetboard/app: Loading the environments in each error_processor function
In the previous commit the environment loading was moving to each request
and that functionality change was not reflected in the error processor
functions (for 4xx and 5xx series errors)
2015-12-14 21:26:09 -05:00
Corey Hammerton
1c7363afa0 Merge pull request #191 from octomike/fix_format_index
fix: wrong index with format()
2015-12-14 21:25:38 -05:00
Spencer Krum
f5ff5b378d Merge pull request #99 from tjayl/auto-refresh
Add auto-refresh on index page
2015-12-05 12:45:25 -08:00
TJayl
177f6c234a Add the auto-refresh to the index page 2015-12-04 13:45:45 +00:00
Michael Krause
8ddec01ca0 fix: wrong index with format() 2015-11-19 14:57:09 +01:00
Corey Hammerton
26d7d43d17 puppetboard: Reloading the available environments on every page load.
Moving the global environment list from a global context to a functional
context because octomike reported that the environment list in his environment
was not being refreshed.

Adding CHANGELOG entries for 0.1.1 and this new change.
2015-11-18 18:34:01 -05:00
Corey Hammerton
f63a0cefcb Merge pull request #185 from raphink/all_envs
Allow to list all nodes again (remove environment filter)

Adding a configurable default environment setting in case some environments to not have a 'production' environment.

Adding a '*' option to remove environment filters.
2015-11-18 18:14:22 -05:00
Corey Hammerton
05f14e3735 Merge pull request #183 from ghakfoort/patch-1
puppetboard/templates/layout: version number updated
2015-11-18 18:13:21 -05:00
Daniele Sluijters
fff45e607a CHANGELOG: dropping PuppetDB 2 support 2015-11-18 17:32:36 +01:00
Daniele Sluijters
2c3ead77d2 README: Add PuppetDB 3 requirement 2015-11-18 17:31:33 +01:00
Raphaël Pinson
b7fdfd8b0d Add All environments to dropdown 2015-11-18 09:25:28 +01:00
Raphaël Pinson
e93db585e1 Add '*' to request all environments 2015-11-18 09:25:25 +01:00
Raphaël Pinson
bfa4d1042e Allow configuration of default environment 2015-11-17 09:32:07 +01:00
ghakfoort
bba5d1dc15 version number updated
the version of puppetboard was updated to 0.1.1 but the version number in this layout file was still 0.1.0.
2015-11-13 11:18:19 +01:00
Corey Hammerton
8b0a797097 puppetboard/setup.py: Version bump 2015-11-12 19:50:28 -05:00
Corey Hammerton
13decf04d5 Merge pull request #182 from corey-hammerton/reports
puppetboard/templates/reports: Passing current_env to the reports_table macro

This fixes https://github.com/puppet-community/puppetboard/issues/181
2015-11-12 19:41:21 -05:00
Corey Hammerton
0fdad9287e puppetboard/templates/reports: Passing the current_env parameter to the reports_table macro
This fixes https://github.com/puppet-community/puppetboard/issues/181
2015-11-11 18:44:12 -05:00
15 changed files with 417 additions and 150 deletions

View File

@@ -4,10 +4,42 @@ Changelog
This is the changelog for Puppetboard. This is the changelog for Puppetboard.
0.1.2
====
* Add configuration option to set the default environment with new
configuration option DEFAULT_ENVIRONMENT, defaults to 'production'.
* Loading all available environments with every page load.
* Adding an "All Environments" item to the Environments dropdown to
remove all environment filters on PuppetDB data.
* Updating README.rst to update links and describe new configuration
options.
* Fixing Query form submission problem by disabling CSRF protection.
Needs to be re-implemented.
* Updating the pypuppetdb requirement to >= 0.2.1, using information
available in PuppetDB 3.2 and higher
** latest_report_hash and latest_report_status fields from the Nodes
endpoint, this effectively deprecates the report_latest() function
** code_id from the Catalogs endpoint (current unused)
* Adding a automatic refresh on the overview page to reload the page
every X number of seconds, defaults to 30. This is configurable
with the configuration option REFRESH_RATE
* Fixing the table alignment in the catalog_compare() page by switching
to fixed tables from basic tables.
* Using colors similar to Puppet Dashboard and Foreman for the status
counts sections
0.1.1
====
* Fix bug where the reports template was not generating the report links
with the right environment
0.1.0 0.1.0
==== ====
* Requires pypuppetdb >= 0.2.0 * Requires pypuppetdb >= 0.2.0
* Drop support for PuppetDB 2 and earlier
* Full support for PuppetDB 3.x * Full support for PuppetDB 3.x
* The first directory location is now a Puppet environment which is filtered * The first directory location is now a Puppet environment which is filtered
on all supported queries. Users can browse different environments with a on all supported queries. Users can browse different environments with a

View File

@@ -8,6 +8,8 @@ functionality of `Puppet Dashboard`_.
Puppetboard relies on the `pypuppetdb`_ library to fetch data from PuppetDB Puppetboard relies on the `pypuppetdb`_ library to fetch data from PuppetDB
and is built with the help of the `Flask`_ microframework. and is built with the help of the `Flask`_ microframework.
As of version 0.1.0 and higher, Puppetboard **requires** PuppetDB 3.
.. _pypuppetdb: https://pypi.python.org/pypi/pypuppetdb .. _pypuppetdb: https://pypi.python.org/pypi/pypuppetdb
.. _PuppetDB: http://docs.puppetlabs.com/puppetdb/latest/index.html .. _PuppetDB: http://docs.puppetlabs.com/puppetdb/latest/index.html
.. _Puppet Dashboard: http://docs.puppetlabs.com/dashboard/ .. _Puppet Dashboard: http://docs.puppetlabs.com/dashboard/
@@ -17,7 +19,7 @@ At the current time of writing, Puppetboard supports the following Python versio
* Python 2.6 * Python 2.6
* Python 2.7 * Python 2.7
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/overview.png .. image:: screenshots/overview.png
:alt: View of a node :alt: View of a node
:width: 1024 :width: 1024
:height: 700 :height: 700
@@ -51,10 +53,10 @@ There is a `Puppet module`_ by `Spencer Krum`_ that takes care of installing Pup
You can install it with: You can install it with:
puppet module install nibalizer-puppetboard puppet module install puppet-puppetboard
.. _Spencer Krum: https://github.com/nibalizer .. _Spencer Krum: https://github.com/nibalizer
.. _Puppet module: https://forge.puppetlabs.com/nibalizer/puppetboard .. _Puppet module: https://forge.puppetlabs.com/puppet/puppetboard
Manual Manual
^^^^^^ ^^^^^^
@@ -69,7 +71,7 @@ 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 do this Puppetboard will be installed in the so called site-packages or
dist-packages of your Python distribution. dist-packages of your Python distribution.
The complete path on Debian and Ubuntu systems would be ``/usr/local/lib/pythonX.Y/lib/dist-packages/puppetboard`` and on Fedora would be ``/usr/lib/pythonX.Y/lib/site-packages/puppetboard`` The complete path on Debian and Ubuntu systems would be ``/usr/local/lib/pythonX.Y/lib/dist-packages/puppetboard`` and on Fedora would be ``/usr/lib/pythonX.Y/site-packages/puppetboard``
where X and Y are replaced by your major and minor python versions. where X and Y are replaced by your major and minor python versions.
@@ -172,10 +174,11 @@ connect. Therefor you'll also have to supply the following settings:
For information about how to generate the correct keys please refer to the For information about how to generate the correct keys please refer to the
`pypuppetdb documentation`_. `pypuppetdb documentation`_.
Other settings that might be interesting: Other settings that might be interesting in no particular order:
* ``SECRET_KEY``: Refer to `Flask documentation`_, section sessions: How to * ``SECRET_KEY``: Refer to `Flask documentation`_, section sessions: How to
generate good secret keys, to set the value. generate good secret keys, to set the value. Defaults to a random 24-char
string generated by os.random(24)
* ``PUPPETDB_TIMEOUT``: Defaults to 20 seconds but you might need to increase * ``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 value. It depends on how big the results are when querying PuppetDB.
This behaviour will change in a future release when pagination will be This behaviour will change in a future release when pagination will be
@@ -188,6 +191,25 @@ Other settings that might be interesting:
* ``ENABLE_QUERY``: Defaults to ``True`` causing a Query tab to show up in the * ``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 web interface allowing users to write and execute arbitrary queries against
a set of endpoints in PuppetDB. Change this to ``False`` to disable this. a set of endpoints in PuppetDB. Change this to ``False`` to disable this.
* ``GRAPH_FACTS``: A list of fact names to tell PuppetBoard to generate a
pie-chart on the fact page. With some fact values being unique per node,
like ipaddress, uuid, and serial number, as well as structured facts it was
no longer feasible to generate a graph for everything.
* ``INVENTORY_FACTS``: A list of tuples that serve as the column header and
the fact name to search for to create the inventory page. If a fact is not
found for a node then ``undef`` is printed.
* ``ENABLE_CATALOG``: If set to ``True`` allows the user to view a node's
latest catalog. This includes all managed resources, their file-system
locations and their relationships, if available. Defaults to ``False``.
* ``REFRESH_RATE``: Defaults to ``30`` the number of seconds to wait until
the index page is automatically refreshed.
* ``DEFAULT_ENVIRONMENT``: Defaults to ``'production'``, as the name
suggests, load all information filtered by this environment value.
* ``REPORTS_COUNT``: Defaults to ``10`` the limit of the number of reports
to load on the node or any reports page.
* ``OFFLINE_MODE``: If set to ``True`` load static assets (jquery,
semantic-ui, tablesorter, etc) from the local web server instead of a CDN.
Defaults to ``False``.
.. _pypuppetdb documentation: http://pypuppetdb.readthedocs.org/en/v0.1.0/quickstart.html#ssl .. _pypuppetdb documentation: http://pypuppetdb.readthedocs.org/en/v0.1.0/quickstart.html#ssl
.. _Flask documentation: http://flask.pocoo.org/docs/0.10/quickstart/#sessions .. _Flask documentation: http://flask.pocoo.org/docs/0.10/quickstart/#sessions
@@ -238,7 +260,7 @@ First we need to create the necessary directories:
.. code-block:: bash .. code-block:: bash
$ mkdir -p /var/www/puppetboard $ mkdir -p /var/www/html/puppetboard
Copy Puppetboard's ``default_settings.py`` to the newly created puppetboard Copy Puppetboard's ``default_settings.py`` to the newly created puppetboard
directory and name the file ``settings.py``. This file will be available directory and name the file ``settings.py``. This file will be available
@@ -260,7 +282,7 @@ puppetboard directory:
import os import os
# Needed if a settings.py file exists # Needed if a settings.py file exists
os.environ['PUPPETBOARD_SETTINGS'] = '/var/www/puppetboard/settings.py' os.environ['PUPPETBOARD_SETTINGS'] = '/var/www/html/puppetboard/settings.py'
from puppetboard.app import app as application from puppetboard.app import app as application
Make sure this file is readable by the user the webserver runs as. Make sure this file is readable by the user the webserver runs as.
@@ -274,7 +296,7 @@ Here is a sample configuration for Debian and Ubuntu:
<VirtualHost *:80> <VirtualHost *:80>
ServerName puppetboard.example.tld ServerName puppetboard.example.tld
WSGIDaemonProcess puppetboard user=www-data group=www-data threads=5 WSGIDaemonProcess puppetboard user=www-data group=www-data threads=5
WSGIScriptAlias / /var/www/puppetboard/wsgi.py WSGIScriptAlias / /var/www/html/puppetboard/wsgi.py
ErrorLog /var/log/apache2/puppetboard.error.log ErrorLog /var/log/apache2/puppetboard.error.log
CustomLog /var/log/apache2/puppetboard.access.log combined CustomLog /var/log/apache2/puppetboard.access.log combined
@@ -299,11 +321,11 @@ Here is a sample configuration for Fedora:
<VirtualHost *:80> <VirtualHost *:80>
ServerName puppetboard.example.tld ServerName puppetboard.example.tld
WSGIDaemonProcess puppetboard user=apache group=apache threads=5 WSGIDaemonProcess puppetboard user=apache group=apache threads=5
WSGIScriptAlias / /var/www/puppetboard/wsgi.py WSGIScriptAlias / /var/www/html/puppetboard/wsgi.py
ErrorLog /var/log/httpd/puppetboard.error.log ErrorLog logs/puppetboard-error_log
CustomLog /var/log/httpd/puppetboard.access.log combined CustomLog logs/puppetboard-access_log combined
Alias /static /usr/local/lib/pythonX.Y/site-packages/puppetboard/static Alias /static /usr/lib/pythonX.Y/site-packages/puppetboard/static
<Directory /usr/lib/python2.X/site-packages/puppetboard/static> <Directory /usr/lib/python2.X/site-packages/puppetboard/static>
Satisfy Any Satisfy Any
Allow from all Allow from all
@@ -652,70 +674,79 @@ messages have a look at this post by `Tim Pope`_.
.. _Tim Pope: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html .. _Tim Pope: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
Examples
========
`vagrant-puppetboard`_
.. _vagrant-puppetboard: https://github.com/visibilityspots/vagrant-puppet/tree/puppetboard
A vagrant project to show off the puppetboard functionallity using the puppetboard puppet module on a puppetserver with puppetdb.
Screenshots Screenshots
=========== ===========
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/overview.png .. image:: screenshots/overview.png
:alt: Overview / Index / Homepage :alt: Overview / Index / Homepage
:width: 1024 :width: 1024
:height: 700 :height: 700
:align: center :align: center
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/nodes.png .. image:: screenshots/nodes.png
:alt: Nodes view, all active nodes :alt: Nodes view, all active nodes
:width: 1024 :width: 1024
:height: 700 :height: 700
:align: center :align: center
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/node.png .. image:: screenshots/node.png
:alt: Single node page / overview :alt: Single node page / overview
:width: 1024 :width: 1024
:height: 700 :height: 700
:align: center :align: center
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/report.png .. image:: screenshots/report.png
:alt: Report view :alt: Report view
:width: 1024 :width: 1024
:height: 700 :height: 700
:align: center :align: center
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/facts.png .. image:: screenshots/facts.png
:alt: Facts view :alt: Facts view
:width: 1024 :width: 1024
:height: 700 :height: 700
:align: center :align: center
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/fact.png .. image:: screenshots/fact.png
:alt: Single fact, with graphs :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/fact_value.png .. image:: screenshots/fact_value.png
:alt: All nodes that have this fact with that value :alt: All nodes that have this fact with that value
:width: 1024 :width: 1024
:height: 700 :height: 700
:align: center :align: center
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/metrics.png .. image:: screenshots/metrics.png
:alt: Metrics view :alt: Metrics view
:width: 1024 :width: 1024
:height: 700 :height: 700
:align: center :align: center
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/metric.png .. image:: screenshots/metric.png
:alt: Single metric :alt: Single metric
:width: 1024 :width: 1024
:height: 700 :height: 700
:align: center :align: center
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/query.png .. image:: screenshots/query.png
:alt: Query view :alt: Query view
:width: 1024 :width: 1024
:height: 700 :height: 700
:align: center :align: center
.. image:: https://raw.github.com/nedap/puppetboard/master/screenshots/broken.png .. image:: screenshots/broken.png
:alt: Error page :alt: Error page
:width: 1024 :width: 1024
:height: 700 :height: 700

View File

@@ -15,7 +15,6 @@ from flask import (
Response, stream_with_context, redirect, Response, stream_with_context, redirect,
request request
) )
from flask_wtf.csrf import CsrfProtect
from pypuppetdb import connect from pypuppetdb import connect
@@ -27,7 +26,6 @@ from puppetboard.utils import (
app = Flask(__name__) app = Flask(__name__)
CsrfProtect(app)
app.config.from_object('puppetboard.default_settings') app.config.from_object('puppetboard.default_settings')
graph_facts = app.config['GRAPH_FACTS'] graph_facts = app.config['GRAPH_FACTS']
@@ -78,15 +76,13 @@ def environments():
return x return x
def check_env(env): def check_env(env, envs):
if env not in envs: if env != '*' and env not in envs:
abort(404) abort(404)
app.jinja_env.globals['url_for_pagination'] = url_for_pagination app.jinja_env.globals['url_for_pagination'] = url_for_pagination
app.jinja_env.globals['url_for_environments'] = url_for_environments app.jinja_env.globals['url_for_environments'] = url_for_environments
envs = environments()
@app.context_processor @app.context_processor
def utility_processor(): def utility_processor():
def now(format='%m/%d/%Y %H:%M:%S'): def now(format='%m/%d/%Y %H:%M:%S'):
@@ -97,16 +93,19 @@ def utility_processor():
@app.errorhandler(400) @app.errorhandler(400)
def bad_request(e): def bad_request(e):
envs = environments()
return render_template('400.html', envs=envs), 400 return render_template('400.html', envs=envs), 400
@app.errorhandler(403) @app.errorhandler(403)
def forbidden(e): def forbidden(e):
envs = environments()
return render_template('403.html', envs=envs), 400 return render_template('403.html', envs=envs), 400
@app.errorhandler(404) @app.errorhandler(404)
def not_found(e): def not_found(e):
envs = environments()
return render_template('404.html', envs=envs), 404 return render_template('404.html', envs=envs), 404
@@ -114,15 +113,17 @@ def not_found(e):
def precond_failed(e): def precond_failed(e):
"""We're slightly abusing 412 to handle missing features """We're slightly abusing 412 to handle missing features
depending on the API version.""" depending on the API version."""
envs = environments()
return render_template('412.html', envs=envs), 412 return render_template('412.html', envs=envs), 412
@app.errorhandler(500) @app.errorhandler(500)
def server_error(e): def server_error(e):
envs = environments()
return render_template('500.html', envs=envs), 500 return render_template('500.html', envs=envs), 500
@app.route('/', defaults={'env': 'production'}) @app.route('/', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/') @app.route('/<env>/')
def index(env): def index(env):
"""This view generates the index page and displays a set of metrics and """This view generates the index page and displays a set of metrics and
@@ -131,30 +132,53 @@ def index(env):
:param env: Search for nodes in this (Catalog and Fact) environment :param env: Search for nodes in this (Catalog and Fact) environment
:type env: :obj:`string` :type env: :obj:`string`
""" """
check_env(env) envs = environments()
# TODO: Would be great if we could parallelize this somehow, doing these
# requests in sequence is rather pointless.
prefix = '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 = { metrics = {
'num_nodes': num_nodes['Value'], 'num_nodes': 0,
'num_resources': num_resources['Value'], 'num_resources': 0,
'avg_resources_node': "{0:10.0f}".format(avg_resources_node['Value']), 'avg_resources_node': 0}
} check_env(env, envs)
if env == '*':
query = None
prefix = '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']
metrics['num_resources'] = num_resources['Value']
metrics['avg_resources_node'] = "{0:10.0f}".format(
avg_resources_node['Value'])
else:
query = '["and", {0}]'.format(
", ".join('["=", "{0}", "{1}"]'.format(field, env)
for field in ['catalog_environment', 'facts_environment']))
num_nodes = get_or_abort(
puppetdb._query,
'nodes',
query='["extract", [["function", "count"]],["and", {0}]]'.format(
",".join('["=", "{0}", "{1}"]'.format(field, env)
for field in ['catalog_environment', 'facts_environment'])))
num_resources = get_or_abort(
puppetdb._query,
'resources',
query='["extract", [["function", "count"]],' \
'["=", "environment", "{0}"]]'.format(
env))
metrics['num_nodes'] = num_nodes[0]['count']
metrics['num_resources'] = num_resources[0]['count']
metrics['avg_resources_node'] = "{0:10.0f}".format(
(num_resources[0]['count'] / num_nodes[0]['count']))
nodes = get_or_abort(puppetdb.nodes, nodes = get_or_abort(puppetdb.nodes,
query='["and", {0}]'.format( query=query,
", ".join('["=", "{0}", "{1}"]'.format(field, env)
for field in ['catalog_environment', 'facts_environment'])),
unreported=app.config['UNRESPONSIVE_HOURS'], unreported=app.config['UNRESPONSIVE_HOURS'],
with_status=True) with_status=True)
@@ -192,7 +216,7 @@ def index(env):
) )
@app.route('/nodes', defaults={'env': 'production'}) @app.route('/nodes', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/nodes') @app.route('/<env>/nodes')
def nodes(env): def nodes(env):
"""Fetch all (active) nodes from PuppetDB and stream a table displaying """Fetch all (active) nodes from PuppetDB and stream a table displaying
@@ -207,13 +231,19 @@ def nodes(env):
:param env: Search for nodes in this (Catalog and Fact) environment :param env: Search for nodes in this (Catalog and Fact) environment
:type env: :obj:`string` :type env: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
if env == '*':
query = None
else:
query = '["and", {0}]'.format(
", ".join('["=", "{0}", "{1}"]'.format(field, env)
for field in ['catalog_environment', 'facts_environment'])),
status_arg = request.args.get('status', '') status_arg = request.args.get('status', '')
nodelist = puppetdb.nodes( nodelist = puppetdb.nodes(
query='["and", {0}]'.format( query=query,
", ".join('["=", "{0}", "{1}"]'.format(field, env)
for field in ['catalog_environment', 'facts_environment'])),
unreported=app.config['UNRESPONSIVE_HOURS'], unreported=app.config['UNRESPONSIVE_HOURS'],
with_status=True) with_status=True)
nodes = [] nodes = []
@@ -230,7 +260,7 @@ def nodes(env):
current_env=env))) current_env=env)))
@app.route('/inventory', defaults={'env': 'production'}) @app.route('/inventory', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/inventory') @app.route('/<env>/inventory')
def inventory(env): def inventory(env):
"""Fetch all (active) nodes from PuppetDB and stream a table displaying """Fetch all (active) nodes from PuppetDB and stream a table displaying
@@ -245,7 +275,8 @@ def inventory(env):
:param env: Search for facts in this environment :param env: Search for facts in this environment
:type env: :obj:`string` :type env: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
fact_desc = [] # a list of fact descriptions to go fact_desc = [] # a list of fact descriptions to go
# in the table header # in the table header
@@ -272,10 +303,15 @@ def inventory(env):
fact_desc.append(description) fact_desc.append(description)
fact_names.append(name) fact_names.append(name)
query = '["and", ["=", "environment", "{0}"], ["or", {1}]]'.format( if env == '*':
env, query = '["or", {0}]]'.format(
', '.join('["=", "name", "{0}"]'.format(name) ', '.join('["=", "name", "{0}"]'.format(name)
for name in fact_names)) for name in fact_names))
else:
query = '["and", ["=", "environment", "{0}"], ["or", {1}]]'.format(
env,
', '.join('["=", "name", "{0}"]'.format(name)
for name in fact_names))
# get all the facts from PuppetDB # get all the facts from PuppetDB
facts = puppetdb.facts(query=query) facts = puppetdb.facts(query=query)
@@ -302,7 +338,7 @@ def inventory(env):
current_env=env))) current_env=env)))
@app.route('/node/<node_name>', defaults={'env': 'production'}) @app.route('/node/<node_name>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/node/<node_name>') @app.route('/<env>/node/<node_name>')
def node(env, node_name): def node(env, node_name):
"""Display a dashboard for a node showing as much data as we have on that """Display a dashboard for a node showing as much data as we have on that
@@ -312,13 +348,19 @@ def node(env, node_name):
:param env: Ensure that the node, facts and reports are in this environment :param env: Ensure that the node, facts and reports are in this environment
:type env: :obj:`string` :type env: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
if env == '*':
query = '["=", "certname", "{0}"]]'.format(node_name)
else:
query='["and", ["=", "environment", "{0}"],' \
'["=", "certname", "{1}"]]'.format(env, node_name),
node = get_or_abort(puppetdb.node, node_name) node = get_or_abort(puppetdb.node, node_name)
facts = node.facts() facts = node.facts()
reports = get_or_abort(puppetdb.reports, reports = get_or_abort(puppetdb.reports,
query='["and", ["=", "environment", "{0}"],' \ query=query,
'["=", "certname", "{1}"]]'.format(env, node_name),
limit=app.config['REPORTS_COUNT'], limit=app.config['REPORTS_COUNT'],
order_by='[{"field": "start_time", "order": "desc"}]') order_by='[{"field": "start_time", "order": "desc"}]')
reports, reports_events = tee(reports) reports, reports_events = tee(reports)
@@ -347,7 +389,7 @@ def node(env, node_name):
current_env=env) current_env=env)
@app.route('/reports/', defaults={'env': 'production', 'page': 1}) @app.route('/reports/', defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'page': 1})
@app.route('/<env>/reports/', defaults={'page': 1}) @app.route('/<env>/reports/', defaults={'page': 1})
@app.route('/<env>/reports/page/<int:page>') @app.route('/<env>/reports/page/<int:page>')
def reports(env, page): def reports(env, page):
@@ -360,18 +402,25 @@ def reports(env, page):
and this value and this value
:type page: :obj:`int` :type page: :obj:`int`
""" """
check_env(env) envs = environments()
check_env(env, envs)
if env == '*':
reports_query = None
total_query = '["extract", [["function", "count"]], ["~", "certname", ""]]'
else:
reports_query = '["=", "environment", "{0}"]'.format(env)
total_query = '["extract", [["function", "count"]],'\
'["and", ["=", "environment", "{0}"]]]'.format(env)
reports = get_or_abort(puppetdb.reports, reports = get_or_abort(puppetdb.reports,
query='["=", "environment", "{0}"]'.format(env), query=reports_query,
limit=app.config['REPORTS_COUNT'], limit=app.config['REPORTS_COUNT'],
offset=(page-1) * app.config['REPORTS_COUNT'], offset=(page-1) * app.config['REPORTS_COUNT'],
order_by='[{"field": "start_time", "order": "desc"}]') order_by='[{"field": "start_time", "order": "desc"}]')
total = get_or_abort(puppetdb._query, total = get_or_abort(puppetdb._query,
'reports', 'reports',
query='["extract", [["function", "count"]],'\ query=total_query)
'["and", ["=", "environment", "{0}"]]]'.format(
env))
total = total[0]['count'] total = total[0]['count']
reports, reports_events = tee(reports) reports, reports_events = tee(reports)
report_event_counts = {} report_event_counts = {}
@@ -380,14 +429,22 @@ def reports(env, page):
abort(404) abort(404)
for report in reports_events: for report in reports_events:
counts = get_or_abort(puppetdb.event_counts, if env == '*':
query='["and",' \ event_count_query = '["and",' \
'["=", "certname", "{0}"],' \
'["=", "report", "{1}"]]'.format(
report.node,
report.hash_)
else:
event_count_query = '["and",' \
'["=", "environment", "{0}"],' \ '["=", "environment", "{0}"],' \
'["=", "certname", "{1}"],' \ '["=", "certname", "{1}"],' \
'["=", "report", "{2}"]]'.format( '["=", "report", "{2}"]]'.format(
env, env,
report.node, report.node,
report.hash_), report.hash_)
counts = get_or_abort(puppetdb.event_counts,
query=event_count_query,
summarize_by="certname") summarize_by="certname")
try: try:
report_event_counts[report.hash_] = counts[0] report_event_counts[report.hash_] = counts[0]
@@ -403,7 +460,7 @@ def reports(env, page):
current_env=env))) current_env=env)))
@app.route('/reports/<node_name>/', defaults={'env': 'production', 'page': 1}) @app.route('/reports/<node_name>/', defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'page': 1})
@app.route('/<env>/reports/<node_name>', defaults={'page': 1}) @app.route('/<env>/reports/<node_name>', defaults={'page': 1})
@app.route('/<env>/reports/<node_name>/page/<int:page>') @app.route('/<env>/reports/<node_name>/page/<int:page>')
def reports_node(env, node_name, page): def reports_node(env, node_name, page):
@@ -418,12 +475,18 @@ def reports_node(env, node_name, page):
and this value and this value
:type page: :obj:`int` :type page: :obj:`int`
""" """
check_env(env) envs = environments()
check_env(env, envs)
reports = get_or_abort(puppetdb.reports, if env == '*':
query = '["=", "certname", "{0}"]]'.format(node_name)
else:
query='["and",' \ query='["and",' \
'["=", "environment", "{0}"],' \ '["=", "environment", "{0}"],' \
'["=", "certname", "{1}"]]'.format(env, node_name), '["=", "certname", "{1}"]]'.format(env, node_name),
reports = get_or_abort(puppetdb.reports,
query=query,
limit=app.config['REPORTS_COUNT'], limit=app.config['REPORTS_COUNT'],
offset=(page-1) * app.config['REPORTS_COUNT'], offset=(page-1) * app.config['REPORTS_COUNT'],
order_by='[{"field": "start_time", "order": "desc"}]') order_by='[{"field": "start_time", "order": "desc"}]')
@@ -441,11 +504,22 @@ def reports_node(env, node_name, page):
abort(404) abort(404)
for report in reports_events: for report in reports_events:
counts = get_or_abort(puppetdb.event_counts, if env == '*':
query='["and",' \ event_count_query = '["and",' \
'["=", "certname", "{0}"],' \
'["=", "report", "{1}"]]'.format(
report.node,
report.hash_)
else:
event_count_query = '["and",' \
'["=", "environment", "{0}"],' \ '["=", "environment", "{0}"],' \
'["=", "certname", "{1}"],' \ '["=", "certname", "{1}"],' \
'["=", "report", "{2}"]]'.format(env, report.node, report.hash_), '["=", "report", "{2}"]]'.format(
env,
report.node,
report.hash_)
counts = get_or_abort(puppetdb.event_counts,
query=event_count_query,
summarize_by="certname") summarize_by="certname")
try: try:
report_event_counts[report.hash_] = counts[0] report_event_counts[report.hash_] = counts[0]
@@ -461,7 +535,7 @@ def reports_node(env, node_name, page):
current_env=env) current_env=env)
@app.route('/report/latest/<node_name>', defaults={'env': 'production'}) @app.route('/report/latest/<node_name>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/report/latest/<node_name>') @app.route('/<env>/report/latest/<node_name>')
def report_latest(env, node_name): def report_latest(env, node_name):
"""Redirect to the latest report of a given node. """Redirect to the latest report of a given node.
@@ -471,25 +545,50 @@ def report_latest(env, node_name):
:param node_name: Find the reports whose certname match this value :param node_name: Find the reports whose certname match this value
:type node_name: :obj:`string` :type node_name: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
if env == '*':
node_query = '["=", "certname", "{0}"]'.format(node_name)
else:
node_query = '["and",' \
'["=", "report_environment", "{0}"],' \
'["=", "certname", "{1}"]]'.format(env, node_name)
reports = get_or_abort(puppetdb.reports,
query='["and",' \
'["=", "environment", "{0}"],' \
'["=", "certname", "{1}"],' \
'["=", "latest_report?", true]]'.format(
env,
node_name))
try: try:
report = next(reports) node = next(get_or_abort(puppetdb.nodes,
query=node_query,
with_status=True))
except StopIteration: except StopIteration:
abort(404) abort(404)
if node.latest_report_hash is not None:
hash_ = node.latest_report_hash
else:
if env == '*':
query='["and",' \
'["=", "certname", "{0}"],' \
'["=", "latest_report?", true]]'.format(node.name)
else:
query='["and",' \
'["=", "environment", "{0}"],' \
'["=", "certname", "{1}"],' \
'["=", "latest_report?", true]]'.format(
env,
node.name)
reports = get_or_abort(puppetdb.reports, query=query)
try:
report = next(reports)
hash_ = report.hash_
except StopIteration:
abort(404)
return redirect( return redirect(
url_for('report', env=env, node_name=node_name, report_id=report.hash_)) url_for('report', env=env, node_name=node_name, report_id=hash_))
@app.route('/report/<node_name>/<report_id>', defaults={'env': 'production'}) @app.route('/report/<node_name>/<report_id>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/report/<node_name>/<report_id>') @app.route('/<env>/report/<node_name>/<report_id>')
def report(env, node_name, report_id): def report(env, node_name, report_id):
"""Displays a single report including all the events associated with that """Displays a single report including all the events associated with that
@@ -507,11 +606,17 @@ def report(env, node_name, report_id):
report report
:type report_id: :obj:`string` :type report_id: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
query = '["and", ["=", "environment", "{0}"], ["=", "certname", "{1}"],' \ if env == '*':
'["or", ["=", "hash", "{2}"], ["=", "configuration_version", "{2}"]]]'.format( query = '["and", ["=", "certname", "{0}"],' \
env, node_name, report_id) '["or", ["=", "hash", "{1}"], ["=", "configuration_version", "{1}"]]]'.format(
node_name, report_id)
else:
query = '["and", ["=", "environment", "{0}"], ["=", "certname", "{1}"],' \
'["or", ["=", "hash", "{2}"], ["=", "configuration_version", "{2}"]]]'.format(
env, node_name, report_id)
reports = puppetdb.reports(query=query) reports = puppetdb.reports(query=query)
try: try:
@@ -529,7 +634,7 @@ def report(env, node_name, report_id):
current_env=env) current_env=env)
@app.route('/facts', defaults={'env': 'production'}) @app.route('/facts', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/facts') @app.route('/<env>/facts')
def facts(env): def facts(env):
"""Displays an alphabetical list of all facts currently known to """Displays an alphabetical list of all facts currently known to
@@ -539,7 +644,8 @@ def facts(env):
sake sake
:type env: :obj:`string` :type env: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
facts_dict = collections.defaultdict(list) facts_dict = collections.defaultdict(list)
facts = get_or_abort(puppetdb.fact_names) facts = get_or_abort(puppetdb.fact_names)
@@ -556,7 +662,7 @@ def facts(env):
current_env=env) current_env=env)
@app.route('/fact/<fact>', defaults={'env': 'production'}) @app.route('/fact/<fact>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/fact/<fact>') @app.route('/<env>/fact/<fact>')
def fact(env, fact): def fact(env, fact):
"""Fetches the specific fact from PuppetDB and displays its value per """Fetches the specific fact from PuppetDB and displays its value per
@@ -567,16 +673,20 @@ def fact(env, fact):
:param fact: Find all facts with this name :param fact: Find all facts with this name
:type fact: :obj:`string` :type fact: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
# we can only consume the generator once, lists can be doubly consumed # we can only consume the generator once, lists can be doubly consumed
# om nom nom # om nom nom
render_graph = False render_graph = False
if fact in graph_facts: if fact in graph_facts:
render_graph = True render_graph = True
if env == '*':
query = None
else:
query = '["=", "environment", "{0}"]'.format(env)
localfacts = [f for f in yield_or_stop(puppetdb.facts( localfacts = [f for f in yield_or_stop(puppetdb.facts(
name=fact, name=fact, query=query))]
query='["=", "environment", "{0}"]'.format(env)))]
return Response(stream_with_context(stream_template( return Response(stream_with_context(stream_template(
'fact.html', 'fact.html',
name=fact, name=fact,
@@ -586,7 +696,7 @@ def fact(env, fact):
current_env=env))) current_env=env)))
@app.route('/fact/<fact>/<value>', defaults={'env': 'production'}) @app.route('/fact/<fact>/<value>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/fact/<fact>/<value>') @app.route('/<env>/fact/<fact>/<value>')
def fact_value(env, fact, value): def fact_value(env, fact, value):
"""On asking for fact/value get all nodes with that fact. """On asking for fact/value get all nodes with that fact.
@@ -598,12 +708,17 @@ def fact_value(env, fact, value):
:param value: Filter facts whose value is equal to this :param value: Filter facts whose value is equal to this
:type value: :obj:`string` :type value: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
if env == '*':
query = None
else:
query = '["=", "environment", "{0}"]'.format(env)
facts = get_or_abort(puppetdb.facts, facts = get_or_abort(puppetdb.facts,
name=fact, name=fact,
value=value, value=value,
query='["=", "environment", "{0}"]'.format(env)) query=query)
localfacts = [f for f in yield_or_stop(facts)] localfacts = [f for f in yield_or_stop(facts)]
return render_template( return render_template(
'fact.html', 'fact.html',
@@ -614,7 +729,7 @@ def fact_value(env, fact, value):
current_env=env) current_env=env)
@app.route('/query', methods=('GET', 'POST'), defaults={'env': 'production'}) @app.route('/query', methods=('GET', 'POST'), defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/query', methods=('GET', 'POST')) @app.route('/<env>/query', methods=('GET', 'POST'))
def query(env): def query(env):
"""Allows to execute raw, user created querries against PuppetDB. This is """Allows to execute raw, user created querries against PuppetDB. This is
@@ -627,9 +742,10 @@ def query(env):
select field in the environment block select field in the environment block
:type env: :obj:`string` :type env: :obj:`string`
""" """
check_env(env)
if app.config['ENABLE_QUERY']: if app.config['ENABLE_QUERY']:
envs = environments()
check_env(env, envs)
form = QueryForm() form = QueryForm()
if form.validate_on_submit(): if form.validate_on_submit():
if form.query.data[0] == '[': if form.query.data[0] == '[':
@@ -654,7 +770,7 @@ def query(env):
abort(403) abort(403)
@app.route('/metrics', defaults={'env': 'production'}) @app.route('/metrics', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/metrics') @app.route('/<env>/metrics')
def metrics(env): def metrics(env):
"""Lists all available metrics that PuppetDB is aware of. """Lists all available metrics that PuppetDB is aware of.
@@ -663,7 +779,8 @@ def metrics(env):
for the environments template block for the environments template block
:type env: :obj:`string` :type env: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
metrics = get_or_abort(puppetdb._query, 'mbean') metrics = get_or_abort(puppetdb._query, 'mbean')
for key, value in metrics.items(): for key, value in metrics.items():
@@ -674,7 +791,7 @@ def metrics(env):
current_env=env) current_env=env)
@app.route('/metric/<metric>', defaults={'env': 'production'}) @app.route('/metric/<metric>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/metric/<metric>') @app.route('/<env>/metric/<metric>')
def metric(env, metric): def metric(env, metric):
"""Lists all information about the metric of the given name. """Lists all information about the metric of the given name.
@@ -683,7 +800,8 @@ def metric(env, metric):
for the environments template block for the environments template block
:type env: :obj:`string` :type env: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
name = unquote(metric) name = unquote(metric)
metric = puppetdb.metric(metric) metric = puppetdb.metric(metric)
@@ -694,7 +812,7 @@ def metric(env, metric):
envs=envs, envs=envs,
current_env=env) current_env=env)
@app.route('/catalogs', defaults={'env': 'production'}) @app.route('/catalogs', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/catalogs') @app.route('/<env>/catalogs')
def catalogs(env): def catalogs(env):
"""Lists all nodes with a compiled catalog. """Lists all nodes with a compiled catalog.
@@ -702,15 +820,20 @@ def catalogs(env):
:param env: Find the nodes with this catalog_environment value :param env: Find the nodes with this catalog_environment value
:type env: :obj:`string` :type env: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
if app.config['ENABLE_CATALOG']: if app.config['ENABLE_CATALOG']:
nodenames = [] nodenames = []
catalog_list = [] catalog_list = []
nodes = get_or_abort(puppetdb.nodes, if env == '*':
query='["and",' \ query = '["null?", "catalog_timestamp", false]]'
else:
query = '["and",' \
'["=", "catalog_environment", "{0}"],' \ '["=", "catalog_environment", "{0}"],' \
'["null?", "catalog_timestamp", false]]'.format(env), '["null?", "catalog_timestamp", false]]'.format(env),
nodes = get_or_abort(puppetdb.nodes,
query=query,
with_status=False, with_status=False,
order_by='[{"field": "certname", "order": "asc"}]') order_by='[{"field": "certname", "order": "asc"}]')
nodes, temp = tee(nodes) nodes, temp = tee(nodes)
@@ -745,7 +868,7 @@ def catalogs(env):
log.warn('Access to catalog interface disabled by administrator') log.warn('Access to catalog interface disabled by administrator')
abort(403) abort(403)
@app.route('/catalog/<node_name>', defaults={'env': 'production'}) @app.route('/catalog/<node_name>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/catalog/<node_name>') @app.route('/<env>/catalog/<node_name>')
def catalog_node(env, node_name): def catalog_node(env, node_name):
"""Fetches from PuppetDB the compiled catalog of a given node. """Fetches from PuppetDB the compiled catalog of a given node.
@@ -753,7 +876,8 @@ def catalog_node(env, node_name):
:param env: Find the catalog with this environment value :param env: Find the catalog with this environment value
:type env: :obj:`string` :type env: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
if app.config['ENABLE_CATALOG']: if app.config['ENABLE_CATALOG']:
catalog = get_or_abort(puppetdb.catalog, catalog = get_or_abort(puppetdb.catalog,
@@ -766,7 +890,7 @@ def catalog_node(env, node_name):
log.warn('Access to catalog interface disabled by administrator') log.warn('Access to catalog interface disabled by administrator')
abort(403) abort(403)
@app.route('/catalog/submit', methods=['POST'], defaults={'env': 'production'}) @app.route('/catalog/submit', methods=['POST'], defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/catalog/submit', methods=['POST']) @app.route('/<env>/catalog/submit', methods=['POST'])
def catalog_submit(env): def catalog_submit(env):
"""Receives the submitted form data from the catalogs page and directs """Receives the submitted form data from the catalogs page and directs
@@ -778,7 +902,8 @@ def catalog_submit(env):
catalogs page with the right environment. catalogs page with the right environment.
:type env: :obj:`string` :type env: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
if app.config['ENABLE_CATALOG']: if app.config['ENABLE_CATALOG']:
form = CatalogForm(request.form) form = CatalogForm(request.form)
@@ -797,7 +922,7 @@ def catalog_submit(env):
log.warn('Access to catalog interface disabled by administrator') log.warn('Access to catalog interface disabled by administrator')
abort(403) abort(403)
@app.route('/catalogs/compare/<compare>...<against>', defaults={'env': 'production'}) @app.route('/catalogs/compare/<compare>...<against>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/catalogs/compare/<compare>...<against>') @app.route('/<env>/catalogs/compare/<compare>...<against>')
def catalog_compare(env, compare, against): def catalog_compare(env, compare, against):
"""Compares the catalog of one node, parameter compare, with that of """Compares the catalog of one node, parameter compare, with that of
@@ -806,7 +931,8 @@ def catalog_compare(env, compare, against):
:param env: Ensure that the 2 catalogs are in the same environment :param env: Ensure that the 2 catalogs are in the same environment
:type env: :obj:`string` :type env: :obj:`string`
""" """
check_env(env) envs = environments()
check_env(env, envs)
if app.config['ENABLE_CATALOG']: if app.config['ENABLE_CATALOG']:
compare_cat = get_or_abort(puppetdb.catalog, compare_cat = get_or_abort(puppetdb.catalog,

View File

@@ -6,6 +6,7 @@ PUPPETDB_SSL_VERIFY = True
PUPPETDB_KEY = None PUPPETDB_KEY = None
PUPPETDB_CERT = None PUPPETDB_CERT = None
PUPPETDB_TIMEOUT = 20 PUPPETDB_TIMEOUT = 20
DEFAULT_ENVIRONMENT = 'production'
SECRET_KEY = os.urandom(24) SECRET_KEY = os.urandom(24)
DEV_LISTEN_HOST = '127.0.0.1' DEV_LISTEN_HOST = '127.0.0.1'
DEV_LISTEN_PORT = 5000 DEV_LISTEN_PORT = 5000
@@ -18,6 +19,7 @@ REPORTS_COUNT = 10
OFFLINE_MODE = False OFFLINE_MODE = False
ENABLE_CATALOG = False ENABLE_CATALOG = False
GRAPH_FACTS = ['architecture', GRAPH_FACTS = ['architecture',
'clientversion',
'domain', 'domain',
'lsbcodename', 'lsbcodename',
'lsbdistcodename', 'lsbdistcodename',
@@ -34,3 +36,4 @@ INVENTORY_FACTS = [ ('Hostname', 'fqdn' ),
('Architecture', 'hardwaremodel' ), ('Architecture', 'hardwaremodel' ),
('Kernel Version', 'kernelrelease' ), ('Kernel Version', 'kernelrelease' ),
('Puppet Version', 'puppetversion' ), ] ('Puppet Version', 'puppetversion' ), ]
REFRESH_RATE = 30

View File

@@ -36,6 +36,55 @@ th.tablesorter-headerDesc::after {
display: block; display: block;
} }
.ui.label.status {
color: white;
text-shadow: 0 0 1px;
}
.ui.header.failed, .ui.line.failed {
color: #AA4643;
}
.ui.label.failed {
background-color: #AA4643;
}
.ui.header.changed, .ui.line.changed {
color: #4572A7;
}
.ui.label.changed {
background-color: #4572A7;
}
.ui.header.unreported {
color: #3D96AE;
}
.ui.label.unreported {
background-color: #3D96AE;
}
.ui.header.noop {
color: #DB843D;
}
.ui.label.noop {
background-color: #DB843D;
}
.ui.label.unchanged {
background-color: #89A54E;
}
.ui.line.skipped {
color: orange;
}
.ui.label.skipped {
background-color: orange;
}
.count { .count {
width: 14%; width: 14%;
text-align: center; text-align: center;

View File

@@ -150,13 +150,15 @@
{% macro status_counts(caller, status, node_name, events, current_env, unreported_time=False, report_hash=False) -%} {% macro status_counts(caller, status, node_name, events, current_env, unreported_time=False, report_hash=False) -%}
<a class="ui small status label <a class="ui small status label
{% if status == 'failed' -%} {% if status == 'failed' -%}
red failed
{% elif status == 'changed' -%} {% elif status == 'changed' -%}
green changed
{% elif status == 'unreported' -%} {% elif status == 'unreported' -%}
black unreported
{% elif status == 'noop' -%} {% elif status == 'noop' -%}
blue noop
{% elif status == 'unchanged' -%}
unchanged
{% endif -%} {% endif -%}
" href=" " href="
{% if report_hash -%} {% if report_hash -%}
@@ -170,9 +172,9 @@
{% if status == 'unreported' %} {% if status == 'unreported' %}
<span class="ui small label status"> {{ unreported_time }} </span> <span class="ui small label status"> {{ unreported_time }} </span>
{% else %} {% else %}
{% if events['failures'] %}<span class="ui small count label red">{{events['failures']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%} {% if events['failures'] %}<span class="ui small count label failed">{{events['failures']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
{% if events['successes'] %}<span class="ui small count label green">{{events['successes']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%} {% if events['successes'] %}<span class="ui small count label changed">{{events['successes']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
{% if events['skips'] %}<span class="ui small count label orange">{{events['skips']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%} {% if events['skips'] %}<span class="ui small count label skipped">{{events['skips']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
{% endif %} {% endif %}
{%- endmacro %} {%- endmacro %}
{% macro render_pagination(pagination) -%} {% macro render_pagination(pagination) -%}

View File

@@ -10,6 +10,7 @@
<th>Hostname</th> <th>Hostname</th>
<th>Version</th> <th>Version</th>
<th>Transaction UUID</th> <th>Transaction UUID</th>
<th>Code ID</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -17,6 +18,7 @@
<td><a href="{{url_for('node', env=current_env, node_name=catalog.node)}}">{{catalog.node}}</a></td> <td><a href="{{url_for('node', env=current_env, node_name=catalog.node)}}">{{catalog.node}}</a></td>
<td>{{catalog.version}}</td> <td>{{catalog.version}}</td>
<td>{{catalog.transaction_uuid}}</td> <td>{{catalog.transaction_uuid}}</td>
<td>{{catalog.code_id}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -15,7 +15,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<table class="ui basic table compact catalog"> <table class="ui fixed table compact catalog">
<thead> <thead>
<tr><th>Resources</th></tr> <tr><th>Resources</th></tr>
</thead> </thead>
@@ -29,7 +29,7 @@
</table> </table>
</td> </td>
<td> <td>
<table class="ui basic table compact catalog"> <table class="ui fixed table compact catalog">
<thead> <thead>
<tr><th>Resources</th></tr> <tr><th>Resources</th></tr>
</thead> </thead>
@@ -45,7 +45,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<table class="ui basic table compact catalog"> <table class="ui fixed table compact catalog">
<thead> <thead>
<tr> <tr>
<th>Edges</th> <th>Edges</th>
@@ -65,7 +65,7 @@
</table> </table>
</td> </td>
<td> <td>
<table class="ui basic table compact catalog"> <table class="ui fixed table compact catalog">
<thead> <thead>
<tr> <tr>
<th>Edge</th> <th>Edge</th>

View File

@@ -1,11 +1,14 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% import '_macros.html' as macros %} {% import '_macros.html' as macros %}
{% block content %} {% block content %}
{% if config.REFRESH_RATE > 0 %}
<meta http-equiv="refresh" content="{{config.REFRESH_RATE}}">
{% endif %}
<div class="ui vertical grid"> <div class="ui vertical grid">
<div class="four column row"> <div class="four column row">
<div class="column"> <div class="column">
<a href="{{url_for('nodes', env=current_env, status='failed')}}"> <a href="{{url_for('nodes', env=current_env, status='failed')}}">
<h1 class="ui red header no-margin-bottom"> <h1 class="ui failed header no-margin-bottom">
{{stats['failed']}} {{stats['failed']}}
<small>{% if stats['failed']== 1 %} node {% else %} nodes {% endif %}</small> <small>{% if stats['failed']== 1 %} node {% else %} nodes {% endif %}</small>
</h1> </h1>
@@ -14,7 +17,7 @@
</div> </div>
<div class="column"> <div class="column">
<a href="{{url_for('nodes', env=current_env, status='noop')}}"> <a href="{{url_for('nodes', env=current_env, status='noop')}}">
<h1 class="ui header purple no-margin-bottom"> <h1 class="ui header noop no-margin-bottom">
{{stats['noop']}} {{stats['noop']}}
<small>{% if stats['noop']== 1 %} node {% else %} nodes {% endif %}</small> <small>{% if stats['noop']== 1 %} node {% else %} nodes {% endif %}</small>
</h1> </h1>
@@ -23,7 +26,7 @@
</div> </div>
<div class="column"> <div class="column">
<a href="{{url_for('nodes', env=current_env, status='changed')}}"> <a href="{{url_for('nodes', env=current_env, status='changed')}}">
<h1 class="ui header green no-margin-bottom"> <h1 class="ui header changed no-margin-bottom">
{{stats['changed']}} {{stats['changed']}}
<small>{% if stats['changed']== 1 %} node {% else %} nodes {% endif %}</small> <small>{% if stats['changed']== 1 %} node {% else %} nodes {% endif %}</small>
</h1> </h1>
@@ -32,7 +35,7 @@
</div> </div>
<div class="column"> <div class="column">
<a href="{{url_for('nodes', env=current_env, status='unreported')}}"> <a href="{{url_for('nodes', env=current_env, status='unreported')}}">
<h1 class="ui header black no-margin-bottom"> <h1 class="ui header unreported no-margin-bottom">
{{ stats['unreported'] }} {{ stats['unreported'] }}
<small>{% if stats['unreported']== 1 %} node {% else %} nodes {% endif %}</small> <small>{% if stats['unreported']== 1 %} node {% else %} nodes {% endif %}</small>
</h1> </h1>
@@ -76,14 +79,22 @@
{% if node.status != 'unchanged' %} {% if node.status != 'unchanged' %}
<tr> <tr>
<td> <td>
{{macros.status_counts(status=node.status, node_name=node.name, events=node.events, unreported_time=node.unreported_time, current_env=current_env)}} {% if node.latest_report_hash %}
{{macros.status_counts(status=node.status, node_name=node.name, events=node.events, unreported_time=node.unreported_time, current_env=current_env, report_hash=node.latest_report_hash)}}
{% else %}
{{macros.status_counts(status=node.status, node_name=node.name, events=node.events, unreported_time=node.unreported_time, current_env=current_env)}}
{% endif %}
</td> </td>
<td> <td>
<a href="{{url_for('node', env=current_env, node_name=node.name)}}">{{ node.name }}</a> <a href="{{url_for('node', env=current_env, node_name=node.name)}}">{{ node.name }}</a>
</td> </td>
<td> <td>
{% if node.report_timestamp %} {% if node.report_timestamp %}
<a href="{{url_for('report_latest', env=current_env, node_name=node.name)}}" rel='utctimestamp'>{{ node.report_timestamp }}</a> {% if node.latest_report_hash %}
<a href="{{url_for('report', env=current_env, node_name=node.name, report_id=node.latest_report_hash)}}" rel='utctimestamp'>{{ node.report_timestamp }}</a>
{% else %}
<a href="{{url_for('report_latest', env=current_env, node_name=node.name)}}" rel='utctimestamp'>{{ node.report_timestamp }}</a>
{% endif %}
{% else %} {% else %}
<i class="large ban circle icon"></i> <i class="large ban circle icon"></i>
{% endif %} {% endif %}

View File

@@ -44,12 +44,13 @@
Environments Environments
<i class="dropdown icon"></i> <i class="dropdown icon"></i>
<div class="menu"> <div class="menu">
<a class="item {% if '*' == current_env %}active{% endif %}" href="{{url_for_environments('*')}}">All environments</a>
{% for env in envs %} {% for env in envs %}
<a class="item {% if env == current_env %}active{% endif %}" href="{{url_for_environments(env)}}">{{env}}</a> <a class="item {% if env == current_env %}active{% endif %}" href="{{url_for_environments(env)}}">{{env}}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="item right"><a href="https://github.com/puppet-community/puppetboard" target="_blank">v0.1.0</a></div> <div class="item right"><a href="https://github.com/puppet-community/puppetboard" target="_blank">v0.1.2</a></div>
</div> </div>
<div class="ui grid padding-bottom"> <div class="ui grid padding-bottom">
<div class="one wide column"></div> <div class="one wide column"></div>

View File

@@ -18,13 +18,21 @@
{% for node in nodes %} {% for node in nodes %}
<tr> <tr>
<td> <td>
{{macros.status_counts(status=node.status, node_name=node.name, events=node.events, unreported_time=node.unreported_time, current_env=current_env)}} {% if node.latest_report_hash %}
{{macros.status_counts(status=node.status, node_name=node.name, events=node.events, unreported_time=node.unreported_time, current_env=current_env, report_hash=node.latest_report_hash)}}
{% else %}
{{macros.status_counts(status=node.status, node_name=node.name, events=node.events, unreported_time=node.unreported_time, current_env=current_env)}}
{% endif %}
</td> </td>
<td><a href="{{url_for('node', env=current_env, node_name=node.name)}}">{{node.name}}</a></td> <td><a href="{{url_for('node', env=current_env, node_name=node.name)}}">{{node.name}}</a></td>
<td><a rel="utctimestamp" href="{{url_for('catalog_node', env=current_env, node_name=node.name)}}">{{node.catalog_timestamp}}</a></td> <td><a rel="utctimestamp" href="{{url_for('catalog_node', env=current_env, node_name=node.name)}}">{{node.catalog_timestamp}}</a></td>
<td> <td>
{% if node.report_timestamp %} {% if node.report_timestamp %}
<a href="{{url_for('report_latest', env=current_env, node_name=node.name)}}" rel='utctimestamp'>{{ node.report_timestamp }}</a> {% if node.latest_report_hash %}
<a href="{{url_for('report', env=current_env, node_name=node.name, report_id=node.latest_report_hash)}}" rel='utctimestamp'>{{ node.report_timestamp }}</a>
{% else %}
<a href="{{url_for('report_latest', env=current_env, node_name=node.name)}}" rel='utctimestamp'>{{ node.report_timestamp }}</a>
{% endif %}
{% else %} {% else %}
<i class="large ban circle icon"></i> <i class="large ban circle icon"></i>
{% endif %} {% endif %}

View File

@@ -39,9 +39,11 @@
<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 id='event-{{loop.index}}' class='positive'> <tr id='event-{{loop.index}}' class='ui line changed'>
{% elif event.failed %} {% elif event.failed %}
<tr id='event-{{loop.index}}' class='error'> <tr id='event-{{loop.index}}' class='ui line failed'>
{% else %}
<tr id='event-{{loop.index}}' class='ui line {{event.status}}'>
{% 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>

View File

@@ -1,6 +1,6 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% import '_macros.html' as macros %} {% import '_macros.html' as macros %}
{% block content %} {% block content %}
{{ macros.reports_table(reports, reports_count, report_event_counts, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True, show_search_bar=True, searchable=True)}} {{ macros.reports_table(reports, reports_count, report_event_counts, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True, show_search_bar=True, searchable=True, current_env=current_env)}}
{{ macros.render_pagination(pagination)}} {{ macros.render_pagination(pagination)}}
{% endblock content %} {% endblock content %}

View File

@@ -5,5 +5,5 @@ MarkupSafe==0.19
WTForms==1.0.5 WTForms==1.0.5
Werkzeug==0.9.4 Werkzeug==0.9.4
itsdangerous==0.23 itsdangerous==0.23
pypuppetdb==0.2.0 pypuppetdb==0.2.1
requests==2.2.1 requests==2.2.1

View File

@@ -9,7 +9,7 @@ if sys.argv[-1] == 'publish':
os.system('python setup.py sdist upload') os.system('python setup.py sdist upload')
sys.exit() sys.exit()
VERSION = "0.1.0" VERSION = "0.1.2"
with codecs.open('README.rst', encoding='utf-8') as f: with codecs.open('README.rst', encoding='utf-8') as f:
README = f.read() README = f.read()
@@ -32,7 +32,7 @@ setup(
"Flask >= 0.10.1", "Flask >= 0.10.1",
"Flask-WTF >= 0.9.4, <= 0.9.5", "Flask-WTF >= 0.9.4, <= 0.9.5",
"WTForms < 2.0", "WTForms < 2.0",
"pypuppetdb >= 0.2.0, < 0.3.0", "pypuppetdb >= 0.2.1, < 0.3.0",
], ],
keywords="puppet puppetdb puppetboard", keywords="puppet puppetdb puppetboard",
classifiers=[ classifiers=[