Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9334827826 | ||
|
|
7c44854988 | ||
|
|
3844d72bcb | ||
|
|
43ff09fbd4 | ||
|
|
0111e52096 | ||
|
|
beef893b6a | ||
|
|
7ed551dbe9 | ||
|
|
492e1057e0 | ||
|
|
e8bc0ccbb5 | ||
|
|
ddf08b9b75 | ||
|
|
fbe646f196 | ||
|
|
65c6251bac | ||
|
|
5874a58965 | ||
|
|
f9edda82b4 | ||
|
|
8fa0514585 | ||
|
|
fd29fe4261 | ||
|
|
3ebde245ed | ||
|
|
21fee5b775 | ||
|
|
8e4af7c034 | ||
|
|
370c514745 | ||
|
|
de22c61056 | ||
|
|
4226fdc368 | ||
|
|
43e37fdf64 | ||
|
|
9b8c8332ef | ||
|
|
40bd73415d | ||
|
|
596e850189 | ||
|
|
23b95dc1d2 | ||
|
|
cc9b3de2ec | ||
|
|
1c7363afa0 | ||
|
|
f5ff5b378d | ||
|
|
177f6c234a | ||
|
|
8ddec01ca0 | ||
|
|
26d7d43d17 | ||
|
|
f63a0cefcb | ||
|
|
05f14e3735 | ||
|
|
fff45e607a | ||
|
|
2c3ead77d2 | ||
|
|
b7fdfd8b0d | ||
|
|
e93db585e1 | ||
|
|
bfa4d1042e | ||
|
|
bba5d1dc15 | ||
|
|
8b0a797097 | ||
|
|
13decf04d5 | ||
|
|
0fdad9287e | ||
|
|
709480a83f | ||
|
|
ba32cdc8a1 | ||
|
|
cb83144443 | ||
|
|
1c72a754d2 | ||
|
|
46439055f8 | ||
|
|
61fc5994fb | ||
|
|
b628032c39 | ||
|
|
f2393eabe4 | ||
|
|
6f6bd0585a | ||
|
|
b04f941e67 | ||
|
|
9486adbd14 | ||
|
|
e0866a12ea | ||
|
|
7f520af661 | ||
|
|
af05f67428 | ||
|
|
4362f80db6 | ||
|
|
2b5903375e | ||
|
|
b539fc9475 | ||
|
|
6af356a2fd | ||
|
|
7e3cf0189b | ||
|
|
5142f96b0b | ||
|
|
1aad26a0c8 | ||
|
|
0c5914ff44 | ||
|
|
15a9aaaa9f | ||
|
|
f2da1b295a | ||
|
|
4c13898490 | ||
|
|
a3473abf61 |
@@ -4,6 +4,69 @@ 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
|
||||||
|
====
|
||||||
|
|
||||||
|
* Requires pypuppetdb >= 0.2.0
|
||||||
|
* Drop support for PuppetDB 2 and earlier
|
||||||
|
* Full support for PuppetDB 3.x
|
||||||
|
* The first directory location is now a Puppet environment which is filtered
|
||||||
|
on all supported queries. Users can browse different environments with a
|
||||||
|
select field in the top NavBar
|
||||||
|
* Using limit, order_by and offset parameters adding pagaination on the Reports
|
||||||
|
page (available in the NavBar). Functionality is available to pages that
|
||||||
|
accept a page attribute.
|
||||||
|
* The report page now directly queries pypuppetdb to match the report_id
|
||||||
|
value with the report hash or configuration_version fields.
|
||||||
|
* Catching and aborting with a 404 if the report and report_latest function
|
||||||
|
queries do not return a generator object.
|
||||||
|
* Adding a Catalogs page (similar to the Nodes page) with a form to compare
|
||||||
|
one node's catalog information with that of another node.
|
||||||
|
* Updating the Query Endpoints for the Query page.
|
||||||
|
* Adding to ``templates/_macros.html`` status_counts that shows node/report
|
||||||
|
status information, like what is avaiable on the index and nodes pages,
|
||||||
|
available to the reports pages and tables also.
|
||||||
|
* Showing report logs and metrics in the report page.
|
||||||
|
* Removing ``limit_reports`` from ``utils.py`` because this helper function
|
||||||
|
has been replaced by the limit PuppetDB paging function.
|
||||||
|
|
||||||
|
**Known Issues**
|
||||||
|
|
||||||
|
* fact_value pages rendered from JSON valued facts return no results. A more
|
||||||
|
sophisticated API is required to make use of JSON valued facts (through the
|
||||||
|
factsets, fact-paths and/or fact-contents endpoints for example)
|
||||||
|
|
||||||
0.0.5
|
0.0.5
|
||||||
=====
|
=====
|
||||||
|
|
||||||
|
|||||||
94
README.rst
94
README.rst
@@ -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.
|
||||||
|
|
||||||
@@ -99,13 +101,13 @@ Native packages for your operating system will be provided in the near future.
|
|||||||
+-------------------+-----------+--------------------------------------------+
|
+-------------------+-----------+--------------------------------------------+
|
||||||
| `SuSE LE 11 SP3`_ | available | Maintained on `OpenSuSE Build Service`_ |
|
| `SuSE LE 11 SP3`_ | available | Maintained on `OpenSuSE Build Service`_ |
|
||||||
+-------------------+-----------+--------------------------------------------+
|
+-------------------+-----------+--------------------------------------------+
|
||||||
| `ArchLinux`_ | available | Maintained by `Niels Abspoel`_ |
|
| `ArchLinux`_ | available | Maintained by `Tim Meusel`_ |
|
||||||
+-------------------+-----------+--------------------------------------------+
|
+-------------------+-----------+--------------------------------------------+
|
||||||
| `OpenBSD`_ | available | Maintained by `Jasper Lievisse Adriaanse`_ |
|
| `OpenBSD`_ | available | Maintained by `Jasper Lievisse Adriaanse`_ |
|
||||||
+-------------------+-----------+--------------------------------------------+
|
+-------------------+-----------+--------------------------------------------+
|
||||||
|
|
||||||
.. _ArchLinux: https://aur.archlinux.org/packages/python2-puppetboard/
|
.. _ArchLinux: https://aur.archlinux.org/packages/python2-puppetboard/
|
||||||
.. _Niels Abspoel: https://github.com/aboe76
|
.. _Tim Meusel: https://github.com/bastelfreak
|
||||||
.. _Jasper Lievisse Adriaanse: https://github.com/jasperla
|
.. _Jasper Lievisse Adriaanse: https://github.com/jasperla
|
||||||
.. _OpenBSD: http://www.openbsd.org/cgi-bin/cvsweb/ports/www/puppetboard/
|
.. _OpenBSD: http://www.openbsd.org/cgi-bin/cvsweb/ports/www/puppetboard/
|
||||||
.. _OpenSuSE Build Service: https://build.opensuse.org/package/show/systemsmanagement:puppet/python-puppetboard
|
.. _OpenSuSE Build Service: https://build.opensuse.org/package/show/systemsmanagement:puppet/python-puppetboard
|
||||||
@@ -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
|
||||||
@@ -587,16 +609,19 @@ This project is still very new so it's not inconceivable you'll run into
|
|||||||
issues.
|
issues.
|
||||||
|
|
||||||
For bug reports you can file an `issue`_. If you need help with something
|
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
|
feel free to hit up the maintainers by e-mail or on IRC. They can usually
|
||||||
be found on `IRCnet`_ and `Freenode`_ and idles in #puppet.
|
be found on `IRCnet`_ and `Freenode`_ and idles in #puppetboard.
|
||||||
|
|
||||||
There's now also the #puppetboard channel on `Freenode`_ where we hang out
|
There's now also the #puppetboard channel on `Freenode`_ where we hang out
|
||||||
and answer questions related to pypuppetdb and Puppetboard.
|
and answer questions related to pypuppetdb and Puppetboard.
|
||||||
|
|
||||||
.. _issue: https://github.com/nedap/puppetboard/issues
|
There is also a `GoogleGroup`_ to exchange questions and discussions. Please
|
||||||
.. _@daenney: https://github.com/daenney
|
note that this group contains discussions of other Puppet Community projects.
|
||||||
|
|
||||||
|
.. _issue: https://github.com/puppet-community/puppetboard/issues
|
||||||
.. _IRCnet: http://www.ircnet.org
|
.. _IRCnet: http://www.ircnet.org
|
||||||
.. _Freenode: http://freenode.net
|
.. _Freenode: http://freenode.net
|
||||||
|
.. _GoogleGroup: https://groups.google.com/forum/?hl=en#!forum/puppet-community
|
||||||
|
|
||||||
Third party
|
Third party
|
||||||
===========
|
===========
|
||||||
@@ -649,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
|
||||||
|
|||||||
@@ -8,25 +8,24 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from itertools import tee
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask, render_template, abort, url_for,
|
Flask, render_template, abort, url_for,
|
||||||
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
|
||||||
|
|
||||||
from puppetboard.forms import QueryForm
|
from puppetboard.forms import (CatalogForm, QueryForm)
|
||||||
from puppetboard.utils import (
|
from puppetboard.utils import (
|
||||||
get_or_abort, yield_or_stop,
|
get_or_abort, yield_or_stop,
|
||||||
limit_reports, jsonprint
|
jsonprint, Pagination
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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']
|
||||||
@@ -37,7 +36,6 @@ app.secret_key = app.config['SECRET_KEY']
|
|||||||
app.jinja_env.filters['jsonprint'] = jsonprint
|
app.jinja_env.filters['jsonprint'] = jsonprint
|
||||||
|
|
||||||
puppetdb = connect(
|
puppetdb = connect(
|
||||||
api_version=3,
|
|
||||||
host=app.config['PUPPETDB_HOST'],
|
host=app.config['PUPPETDB_HOST'],
|
||||||
port=app.config['PUPPETDB_PORT'],
|
port=app.config['PUPPETDB_PORT'],
|
||||||
ssl_verify=app.config['PUPPETDB_SSL_VERIFY'],
|
ssl_verify=app.config['PUPPETDB_SSL_VERIFY'],
|
||||||
@@ -59,6 +57,31 @@ def stream_template(template_name, **context):
|
|||||||
rv.enable_buffering(5)
|
rv.enable_buffering(5)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
def url_for_pagination(page):
|
||||||
|
args = request.view_args.copy()
|
||||||
|
args['page'] = page
|
||||||
|
return url_for(request.endpoint, **args)
|
||||||
|
|
||||||
|
def url_for_environments(env):
|
||||||
|
args = request.view_args.copy()
|
||||||
|
args['env'] = env
|
||||||
|
return url_for(request.endpoint, **args)
|
||||||
|
|
||||||
|
def environments():
|
||||||
|
envs = get_or_abort(puppetdb.environments)
|
||||||
|
x = []
|
||||||
|
|
||||||
|
for env in envs:
|
||||||
|
x.append(env['name'])
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
def check_env(env, envs):
|
||||||
|
if env != '*' and env not in envs:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
app.jinja_env.globals['url_for_pagination'] = url_for_pagination
|
||||||
|
app.jinja_env.globals['url_for_environments'] = url_for_environments
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def utility_processor():
|
def utility_processor():
|
||||||
@@ -70,55 +93,92 @@ def utility_processor():
|
|||||||
|
|
||||||
@app.errorhandler(400)
|
@app.errorhandler(400)
|
||||||
def bad_request(e):
|
def bad_request(e):
|
||||||
return render_template('400.html'), 400
|
envs = environments()
|
||||||
|
return render_template('400.html', envs=envs), 400
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(403)
|
@app.errorhandler(403)
|
||||||
def forbidden(e):
|
def forbidden(e):
|
||||||
return render_template('403.html'), 400
|
envs = environments()
|
||||||
|
return render_template('403.html', envs=envs), 400
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(e):
|
def not_found(e):
|
||||||
return render_template('404.html'), 404
|
envs = environments()
|
||||||
|
return render_template('404.html', envs=envs), 404
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(412)
|
@app.errorhandler(412)
|
||||||
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."""
|
||||||
return render_template('412.html'), 412
|
envs = environments()
|
||||||
|
return render_template('412.html', envs=envs), 412
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(500)
|
@app.errorhandler(500)
|
||||||
def server_error(e):
|
def server_error(e):
|
||||||
return render_template('500.html'), 500
|
envs = environments()
|
||||||
|
return render_template('500.html', envs=envs), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
def index():
|
@app.route('/<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
|
||||||
latest reports on nodes fetched from PuppetDB.
|
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.
|
|
||||||
prefix = 'com.puppetlabs.puppetdb.query.population'
|
|
||||||
num_nodes = get_or_abort(
|
|
||||||
puppetdb.metric,
|
|
||||||
"{0}{1}".format(prefix, ':type=default,name=num-nodes'))
|
|
||||||
num_resources = get_or_abort(
|
|
||||||
puppetdb.metric,
|
|
||||||
"{0}{1}".format(prefix, ':type=default,name=num-resources'))
|
|
||||||
avg_resources_node = get_or_abort(
|
|
||||||
puppetdb.metric,
|
|
||||||
"{0}{1}".format(prefix, ':type=default,name=avg-resources-per-node'))
|
|
||||||
metrics = {
|
|
||||||
'num_nodes': num_nodes['Value'],
|
|
||||||
'num_resources': num_resources['Value'],
|
|
||||||
'avg_resources_node': "{0:10.0f}".format(avg_resources_node['Value']),
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes = puppetdb.nodes(
|
:param env: Search for nodes in this (Catalog and Fact) environment
|
||||||
|
:type env: :obj:`string`
|
||||||
|
"""
|
||||||
|
envs = environments()
|
||||||
|
metrics = {
|
||||||
|
'num_nodes': 0,
|
||||||
|
'num_resources': 0,
|
||||||
|
'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,
|
||||||
|
query=query,
|
||||||
unreported=app.config['UNRESPONSIVE_HOURS'],
|
unreported=app.config['UNRESPONSIVE_HOURS'],
|
||||||
with_status=True)
|
with_status=True)
|
||||||
|
|
||||||
@@ -150,12 +210,15 @@ def index():
|
|||||||
'index.html',
|
'index.html',
|
||||||
metrics=metrics,
|
metrics=metrics,
|
||||||
nodes=nodes_overview,
|
nodes=nodes_overview,
|
||||||
stats=stats
|
stats=stats,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/nodes')
|
@app.route('/nodes', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
def nodes():
|
@app.route('/<env>/nodes')
|
||||||
|
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
|
||||||
those nodes.
|
those nodes.
|
||||||
|
|
||||||
@@ -164,9 +227,23 @@ def nodes():
|
|||||||
we'll end up with an empty table instead because of how yield_or_stop
|
we'll end up with an empty table instead because of how yield_or_stop
|
||||||
works. Once pagination is in place we can change this but we'll need to
|
works. Once pagination is in place we can change this but we'll need to
|
||||||
provide a search feature instead.
|
provide a search feature instead.
|
||||||
|
|
||||||
|
:param env: Search for nodes in this (Catalog and Fact) environment
|
||||||
|
:type env: :obj:`string`
|
||||||
"""
|
"""
|
||||||
|
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=query,
|
||||||
unreported=app.config['UNRESPONSIVE_HOURS'],
|
unreported=app.config['UNRESPONSIVE_HOURS'],
|
||||||
with_status=True)
|
with_status=True)
|
||||||
nodes = []
|
nodes = []
|
||||||
@@ -177,11 +254,15 @@ def nodes():
|
|||||||
else:
|
else:
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
return Response(stream_with_context(
|
return Response(stream_with_context(
|
||||||
stream_template('nodes.html', nodes=nodes)))
|
stream_template('nodes.html',
|
||||||
|
nodes=nodes,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/inventory')
|
@app.route('/inventory', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
def inventory():
|
@app.route('/<env>/inventory')
|
||||||
|
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
|
||||||
those nodes along with a set of facts about them.
|
those nodes along with a set of facts about them.
|
||||||
|
|
||||||
@@ -190,7 +271,12 @@ def inventory():
|
|||||||
we'll end up with an empty table instead because of how yield_or_stop
|
we'll end up with an empty table instead because of how yield_or_stop
|
||||||
works. Once pagination is in place we can change this but we'll need to
|
works. Once pagination is in place we can change this but we'll need to
|
||||||
provide a search feature instead.
|
provide a search feature instead.
|
||||||
|
|
||||||
|
:param env: Search for facts in this environment
|
||||||
|
:type env: :obj:`string`
|
||||||
"""
|
"""
|
||||||
|
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
|
||||||
@@ -217,9 +303,15 @@ def inventory():
|
|||||||
fact_desc.append(description)
|
fact_desc.append(description)
|
||||||
fact_names.append(name)
|
fact_names.append(name)
|
||||||
|
|
||||||
query = '["or", {0}]'.format(
|
if env == '*':
|
||||||
', '.join('["=", "name", "{0}"]'.format(name)
|
query = '["or", {0}]]'.format(
|
||||||
for name in fact_names))
|
', '.join('["=", "name", "{0}"]'.format(name)
|
||||||
|
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)
|
||||||
@@ -239,92 +331,322 @@ def inventory():
|
|||||||
nodedata[node].append("undef")
|
nodedata[node].append("undef")
|
||||||
|
|
||||||
return Response(stream_with_context(
|
return Response(stream_with_context(
|
||||||
stream_template('inventory.html', nodedata=nodedata, fact_desc=fact_desc)))
|
stream_template('inventory.html',
|
||||||
|
nodedata=nodedata,
|
||||||
|
fact_desc=fact_desc,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/node/<node_name>')
|
@app.route('/node/<node_name>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
def node(node_name):
|
@app.route('/<env>/node/<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
|
||||||
node. This includes facts and reports but not Resources as that is too
|
node. This includes facts and reports but not Resources as that is too
|
||||||
heavy to do within a single request.
|
heavy to do within a single request.
|
||||||
|
|
||||||
|
:param env: Ensure that the node, facts and reports are in this environment
|
||||||
|
:type env: :obj:`string`
|
||||||
"""
|
"""
|
||||||
|
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 = limit_reports(node.reports(), app.config['REPORTS_COUNT'])
|
reports = get_or_abort(puppetdb.reports,
|
||||||
|
query=query,
|
||||||
|
limit=app.config['REPORTS_COUNT'],
|
||||||
|
order_by='[{"field": "start_time", "order": "desc"}]')
|
||||||
|
reports, reports_events = tee(reports)
|
||||||
|
report_event_counts = {}
|
||||||
|
|
||||||
|
for report in reports_events:
|
||||||
|
counts = get_or_abort(puppetdb.event_counts,
|
||||||
|
query='["and", ["=", "environment", "{0}"],' \
|
||||||
|
'["=", "certname", "{1}"], ["=", "report", "{2}"]]'.format(
|
||||||
|
env,
|
||||||
|
node_name,
|
||||||
|
report.hash_),
|
||||||
|
summarize_by="certname")
|
||||||
|
try:
|
||||||
|
report_event_counts[report.hash_] = counts[0]
|
||||||
|
except IndexError:
|
||||||
|
report_event_counts[report.hash_] = {}
|
||||||
return render_template(
|
return render_template(
|
||||||
'node.html',
|
'node.html',
|
||||||
node=node,
|
node=node,
|
||||||
facts=yield_or_stop(facts),
|
facts=yield_or_stop(facts),
|
||||||
reports=yield_or_stop(reports),
|
reports=yield_or_stop(reports),
|
||||||
reports_count=app.config['REPORTS_COUNT'])
|
reports_count=app.config['REPORTS_COUNT'],
|
||||||
|
report_event_counts=report_event_counts,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/reports')
|
@app.route('/reports/', defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'page': 1})
|
||||||
def reports():
|
@app.route('/<env>/reports/', defaults={'page': 1})
|
||||||
"""Doesn't do much yet but is meant to show something like the reports of
|
@app.route('/<env>/reports/page/<int:page>')
|
||||||
the last half our, something like that."""
|
def reports(env, page):
|
||||||
return render_template('reports.html')
|
"""Displays a list of reports and status from all nodes, retreived using the
|
||||||
|
reports endpoint, sorted by start_time.
|
||||||
|
|
||||||
|
:param env: Search for all reports in this environment
|
||||||
@app.route('/reports/<node_name>')
|
:type env: :obj:`string`
|
||||||
def reports_node(node_name):
|
:param page: Calculates the offset of the query based on the report count
|
||||||
"""Fetches all reports for a node and processes them eventually rendering
|
and this value
|
||||||
a table displaying those reports."""
|
:type page: :obj:`int`
|
||||||
reports = limit_reports(
|
|
||||||
yield_or_stop(
|
|
||||||
puppetdb.reports('["=", "certname", "{0}"]'.format(node_name))),
|
|
||||||
app.config['REPORTS_COUNT'])
|
|
||||||
return render_template(
|
|
||||||
'reports_node.html',
|
|
||||||
reports=reports,
|
|
||||||
nodename=node_name,
|
|
||||||
reports_count=app.config['REPORTS_COUNT'])
|
|
||||||
|
|
||||||
|
|
||||||
@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: https://tickets.puppetlabs.com/browse/PDB-203
|
|
||||||
"""
|
"""
|
||||||
reports = get_or_abort(puppetdb._query, 'reports',
|
envs = environments()
|
||||||
query='["=","certname","{0}"]'.format(node_name),
|
check_env(env, envs)
|
||||||
limit=1)
|
|
||||||
if len(reports) > 0:
|
if env == '*':
|
||||||
report = reports[0]['hash']
|
reports_query = None
|
||||||
return redirect(
|
total_query = '["extract", [["function", "count"]], ["~", "certname", ""]]'
|
||||||
url_for('report', node_name=node_name, report_id=report))
|
|
||||||
else:
|
else:
|
||||||
|
reports_query = '["=", "environment", "{0}"]'.format(env)
|
||||||
|
total_query = '["extract", [["function", "count"]],'\
|
||||||
|
'["and", ["=", "environment", "{0}"]]]'.format(env)
|
||||||
|
|
||||||
|
reports = get_or_abort(puppetdb.reports,
|
||||||
|
query=reports_query,
|
||||||
|
limit=app.config['REPORTS_COUNT'],
|
||||||
|
offset=(page-1) * app.config['REPORTS_COUNT'],
|
||||||
|
order_by='[{"field": "start_time", "order": "desc"}]')
|
||||||
|
total = get_or_abort(puppetdb._query,
|
||||||
|
'reports',
|
||||||
|
query=total_query)
|
||||||
|
total = total[0]['count']
|
||||||
|
reports, reports_events = tee(reports)
|
||||||
|
report_event_counts = {}
|
||||||
|
|
||||||
|
if total == 0 and page != 1:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
for report in reports_events:
|
||||||
|
if env == '*':
|
||||||
|
event_count_query = '["and",' \
|
||||||
|
'["=", "certname", "{0}"],' \
|
||||||
|
'["=", "report", "{1}"]]'.format(
|
||||||
|
report.node,
|
||||||
|
report.hash_)
|
||||||
|
else:
|
||||||
|
event_count_query = '["and",' \
|
||||||
|
'["=", "environment", "{0}"],' \
|
||||||
|
'["=", "certname", "{1}"],' \
|
||||||
|
'["=", "report", "{2}"]]'.format(
|
||||||
|
env,
|
||||||
|
report.node,
|
||||||
|
report.hash_)
|
||||||
|
counts = get_or_abort(puppetdb.event_counts,
|
||||||
|
query=event_count_query,
|
||||||
|
summarize_by="certname")
|
||||||
|
try:
|
||||||
|
report_event_counts[report.hash_] = counts[0]
|
||||||
|
except IndexError:
|
||||||
|
report_event_counts[report.hash_] = {}
|
||||||
|
return Response(stream_with_context(stream_template(
|
||||||
|
'reports.html',
|
||||||
|
reports=yield_or_stop(reports),
|
||||||
|
reports_count=app.config['REPORTS_COUNT'],
|
||||||
|
report_event_counts=report_event_counts,
|
||||||
|
pagination=Pagination(page, app.config['REPORTS_COUNT'], total),
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)))
|
||||||
|
|
||||||
@app.route('/report/<node_name>/<report_id>')
|
|
||||||
def report(node_name, report_id):
|
@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>/page/<int:page>')
|
||||||
|
def reports_node(env, node_name, page):
|
||||||
|
"""Fetches all reports for a node and processes them eventually rendering
|
||||||
|
a table displaying those reports.
|
||||||
|
|
||||||
|
:param env: Search for reports in this environment
|
||||||
|
:type env: :obj:`string`
|
||||||
|
:param node_name: Find the reports whose certname match this value
|
||||||
|
:type node_name: :obj:`string`
|
||||||
|
:param page: Calculates the offset of the query based on the report count
|
||||||
|
and this value
|
||||||
|
:type page: :obj:`int`
|
||||||
|
"""
|
||||||
|
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),
|
||||||
|
|
||||||
|
reports = get_or_abort(puppetdb.reports,
|
||||||
|
query=query,
|
||||||
|
limit=app.config['REPORTS_COUNT'],
|
||||||
|
offset=(page-1) * app.config['REPORTS_COUNT'],
|
||||||
|
order_by='[{"field": "start_time", "order": "desc"}]')
|
||||||
|
total = get_or_abort(puppetdb._query,
|
||||||
|
'reports',
|
||||||
|
query='["extract", [["function", "count"]],' \
|
||||||
|
'["and", ["=", "environment", "{0}"], ["=", "certname", "{1}"]]]'.format(
|
||||||
|
env,
|
||||||
|
node_name))
|
||||||
|
total = total[0]['count']
|
||||||
|
reports, reports_events = tee(reports)
|
||||||
|
report_event_counts = {}
|
||||||
|
|
||||||
|
if total == 0 and page != 1:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
for report in reports_events:
|
||||||
|
if env == '*':
|
||||||
|
event_count_query = '["and",' \
|
||||||
|
'["=", "certname", "{0}"],' \
|
||||||
|
'["=", "report", "{1}"]]'.format(
|
||||||
|
report.node,
|
||||||
|
report.hash_)
|
||||||
|
else:
|
||||||
|
event_count_query = '["and",' \
|
||||||
|
'["=", "environment", "{0}"],' \
|
||||||
|
'["=", "certname", "{1}"],' \
|
||||||
|
'["=", "report", "{2}"]]'.format(
|
||||||
|
env,
|
||||||
|
report.node,
|
||||||
|
report.hash_)
|
||||||
|
counts = get_or_abort(puppetdb.event_counts,
|
||||||
|
query=event_count_query,
|
||||||
|
summarize_by="certname")
|
||||||
|
try:
|
||||||
|
report_event_counts[report.hash_] = counts[0]
|
||||||
|
except IndexError:
|
||||||
|
report_event_counts[report.hash_] = {}
|
||||||
|
return render_template(
|
||||||
|
'reports.html',
|
||||||
|
reports=reports,
|
||||||
|
reports_count=app.config['REPORTS_COUNT'],
|
||||||
|
report_event_counts=report_event_counts,
|
||||||
|
pagination=Pagination(page, app.config['REPORTS_COUNT'], total),
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/report/latest/<node_name>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
|
@app.route('/<env>/report/latest/<node_name>')
|
||||||
|
def report_latest(env, node_name):
|
||||||
|
"""Redirect to the latest report of a given node.
|
||||||
|
|
||||||
|
:param env: Search for reports in this environment
|
||||||
|
:type env: :obj:`string`
|
||||||
|
:param node_name: Find the reports whose certname match this value
|
||||||
|
:type node_name: :obj:`string`
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
try:
|
||||||
|
node = next(get_or_abort(puppetdb.nodes,
|
||||||
|
query=node_query,
|
||||||
|
with_status=True))
|
||||||
|
except StopIteration:
|
||||||
|
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(
|
||||||
|
url_for('report', env=env, node_name=node_name, report_id=hash_))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/report/<node_name>/<report_id>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
|
@app.route('/<env>/report/<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
|
||||||
report and their status.
|
report and their status.
|
||||||
|
|
||||||
The report_id may be the puppetdb's report hash or the
|
The report_id may be the puppetdb's report hash or the
|
||||||
configuration_version. This allows for better integration
|
configuration_version. This allows for better integration
|
||||||
into puppet-hipchat.
|
into puppet-hipchat.
|
||||||
"""
|
|
||||||
reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node_name))
|
|
||||||
|
|
||||||
for report in reports:
|
:param env: Search for reports in this environment
|
||||||
if report.hash_ == report_id or report.version == report_id:
|
:type env: :obj:`string`
|
||||||
events = puppetdb.events('["=", "report", "{0}"]'.format(
|
:param node_name: Find the reports whose certname match this value
|
||||||
report.hash_))
|
:type node_name: :obj:`string`
|
||||||
return render_template(
|
:param report_id: The hash or the configuration_version of the desired
|
||||||
'report.html',
|
report
|
||||||
report=report,
|
:type report_id: :obj:`string`
|
||||||
events=yield_or_stop(events))
|
"""
|
||||||
|
envs = environments()
|
||||||
|
check_env(env, envs)
|
||||||
|
|
||||||
|
if env == '*':
|
||||||
|
query = '["and", ["=", "certname", "{0}"],' \
|
||||||
|
'["or", ["=", "hash", "{1}"], ["=", "configuration_version", "{1}"]]]'.format(
|
||||||
|
node_name, report_id)
|
||||||
else:
|
else:
|
||||||
|
query = '["and", ["=", "environment", "{0}"], ["=", "certname", "{1}"],' \
|
||||||
|
'["or", ["=", "hash", "{2}"], ["=", "configuration_version", "{2}"]]]'.format(
|
||||||
|
env, node_name, report_id)
|
||||||
|
reports = puppetdb.reports(query=query)
|
||||||
|
|
||||||
|
try:
|
||||||
|
report = next(reports)
|
||||||
|
except StopIteration:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'report.html',
|
||||||
|
report=report,
|
||||||
|
events=yield_or_stop(report.events()),
|
||||||
|
logs=report.logs,
|
||||||
|
metrics=report.metrics,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)
|
||||||
|
|
||||||
@app.route('/facts')
|
|
||||||
def facts():
|
@app.route('/facts', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
|
@app.route('/<env>/facts')
|
||||||
|
def facts(env):
|
||||||
"""Displays an alphabetical list of all facts currently known to
|
"""Displays an alphabetical list of all facts currently known to
|
||||||
PuppetDB."""
|
PuppetDB.
|
||||||
|
|
||||||
|
:param env: Serves no purpose for this function, only for consistency's
|
||||||
|
sake
|
||||||
|
:type env: :obj:`string`
|
||||||
|
"""
|
||||||
|
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)
|
||||||
for fact in facts:
|
for fact in facts:
|
||||||
@@ -334,46 +656,96 @@ def facts():
|
|||||||
facts_dict[letter] = letter_list
|
facts_dict[letter] = letter_list
|
||||||
|
|
||||||
sorted_facts_dict = sorted(facts_dict.items())
|
sorted_facts_dict = sorted(facts_dict.items())
|
||||||
return render_template('facts.html', facts_dict=sorted_facts_dict)
|
return render_template('facts.html',
|
||||||
|
facts_dict=sorted_facts_dict,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/fact/<fact>')
|
@app.route('/fact/<fact>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
def fact(fact):
|
@app.route('/<env>/fact/<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
|
||||||
node for which this fact is known."""
|
node for which this fact is known.
|
||||||
|
|
||||||
|
:param env: Searches for facts in this environment
|
||||||
|
:type env: :obj:`string`
|
||||||
|
:param fact: Find all facts with this name
|
||||||
|
:type fact: :obj:`string`
|
||||||
|
"""
|
||||||
|
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
|
||||||
localfacts = [f for f in yield_or_stop(puppetdb.facts(name=fact))]
|
if env == '*':
|
||||||
|
query = None
|
||||||
|
else:
|
||||||
|
query = '["=", "environment", "{0}"]'.format(env)
|
||||||
|
localfacts = [f for f in yield_or_stop(puppetdb.facts(
|
||||||
|
name=fact, query=query))]
|
||||||
return Response(stream_with_context(stream_template(
|
return Response(stream_with_context(stream_template(
|
||||||
'fact.html',
|
'fact.html',
|
||||||
name=fact,
|
name=fact,
|
||||||
render_graph=render_graph,
|
render_graph=render_graph,
|
||||||
facts=localfacts)))
|
facts=localfacts,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/fact/<fact>/<value>')
|
@app.route('/fact/<fact>/<value>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
def fact_value(fact, value):
|
@app.route('/<env>/fact/<fact>/<value>')
|
||||||
"""On asking for fact/value get all nodes with that fact."""
|
def fact_value(env, fact, value):
|
||||||
facts = get_or_abort(puppetdb.facts, fact, value)
|
"""On asking for fact/value get all nodes with that fact.
|
||||||
|
|
||||||
|
:param env: Searches for facts in this environment
|
||||||
|
:type env: :obj:`string`
|
||||||
|
:param fact: Find all facts with this name
|
||||||
|
:type fact: :obj:`string`
|
||||||
|
:param value: Filter facts whose value is equal to this
|
||||||
|
:type value: :obj:`string`
|
||||||
|
"""
|
||||||
|
envs = environments()
|
||||||
|
check_env(env, envs)
|
||||||
|
|
||||||
|
if env == '*':
|
||||||
|
query = None
|
||||||
|
else:
|
||||||
|
query = '["=", "environment", "{0}"]'.format(env)
|
||||||
|
facts = get_or_abort(puppetdb.facts,
|
||||||
|
name=fact,
|
||||||
|
value=value,
|
||||||
|
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',
|
||||||
name=fact,
|
name=fact,
|
||||||
value=value,
|
value=value,
|
||||||
facts=localfacts)
|
facts=localfacts,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/query', methods=('GET', 'POST'))
|
@app.route('/query', methods=('GET', 'POST'), defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
def query():
|
@app.route('/<env>/query', methods=('GET', 'POST'))
|
||||||
|
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
|
||||||
currently highly experimental and explodes in interesting ways since none
|
currently highly experimental and explodes in interesting ways since none
|
||||||
of the possible exceptions are being handled just yet. This will return
|
of the possible exceptions are being handled just yet. This will return
|
||||||
the JSON of the response or a message telling you what whent wrong /
|
the JSON of the response or a message telling you what whent wrong /
|
||||||
why nothing was returned."""
|
why nothing was returned.
|
||||||
|
|
||||||
|
:param env: Serves no purpose for the query data but is required for the
|
||||||
|
select field in the environment block
|
||||||
|
:type env: :obj:`string`
|
||||||
|
"""
|
||||||
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] == '[':
|
||||||
@@ -384,36 +756,195 @@ def query():
|
|||||||
puppetdb._query,
|
puppetdb._query,
|
||||||
form.endpoints.data,
|
form.endpoints.data,
|
||||||
query=query)
|
query=query)
|
||||||
return render_template('query.html', form=form, result=result)
|
return render_template('query.html',
|
||||||
return render_template('query.html', form=form)
|
form=form,
|
||||||
|
result=result,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)
|
||||||
|
return render_template('query.html',
|
||||||
|
form=form,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)
|
||||||
else:
|
else:
|
||||||
log.warn('Access to query interface disabled by administrator..')
|
log.warn('Access to query interface disabled by administrator..')
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/metrics')
|
@app.route('/metrics', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
def metrics():
|
@app.route('/<env>/metrics')
|
||||||
metrics = get_or_abort(puppetdb._query, 'metrics', path='mbeans')
|
def metrics(env):
|
||||||
|
"""Lists all available metrics that PuppetDB is aware of.
|
||||||
|
|
||||||
|
:param env: While this parameter serves no function purpose it is required
|
||||||
|
for the environments template block
|
||||||
|
:type env: :obj:`string`
|
||||||
|
"""
|
||||||
|
envs = environments()
|
||||||
|
check_env(env, envs)
|
||||||
|
|
||||||
|
metrics = get_or_abort(puppetdb._query, 'mbean')
|
||||||
for key, value in metrics.items():
|
for key, value in metrics.items():
|
||||||
metrics[key] = value.split('/')[3]
|
metrics[key] = value.split('/')[2]
|
||||||
return render_template('metrics.html', metrics=sorted(metrics.items()))
|
return render_template('metrics.html',
|
||||||
|
metrics=sorted(metrics.items()),
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/metric/<metric>')
|
@app.route('/metric/<metric>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
def metric(metric):
|
@app.route('/<env>/metric/<metric>')
|
||||||
|
def metric(env, metric):
|
||||||
|
"""Lists all information about the metric of the given name.
|
||||||
|
|
||||||
|
:param env: While this parameter serves no function purpose it is required
|
||||||
|
for the environments template block
|
||||||
|
:type env: :obj:`string`
|
||||||
|
"""
|
||||||
|
envs = environments()
|
||||||
|
check_env(env, envs)
|
||||||
|
|
||||||
name = unquote(metric)
|
name = unquote(metric)
|
||||||
metric = puppetdb.metric(metric)
|
metric = puppetdb.metric(metric)
|
||||||
return render_template(
|
return render_template(
|
||||||
'metric.html',
|
'metric.html',
|
||||||
name=name,
|
name=name,
|
||||||
metric=sorted(metric.items()))
|
metric=sorted(metric.items()),
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)
|
||||||
|
|
||||||
|
@app.route('/catalogs', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
|
@app.route('/<env>/catalogs')
|
||||||
|
def catalogs(env):
|
||||||
|
"""Lists all nodes with a compiled catalog.
|
||||||
|
|
||||||
|
:param env: Find the nodes with this catalog_environment value
|
||||||
|
:type env: :obj:`string`
|
||||||
|
"""
|
||||||
|
envs = environments()
|
||||||
|
check_env(env, envs)
|
||||||
|
|
||||||
@app.route('/catalog/<node_name>')
|
|
||||||
def catalog_node(node_name):
|
|
||||||
"""Fetches from PuppetDB the compiled catalog of a given node."""
|
|
||||||
if app.config['ENABLE_CATALOG']:
|
if app.config['ENABLE_CATALOG']:
|
||||||
catalog = puppetdb.catalog(node=node_name)
|
nodenames = []
|
||||||
return render_template('catalog.html', catalog=catalog)
|
catalog_list = []
|
||||||
|
if env == '*':
|
||||||
|
query = '["null?", "catalog_timestamp", false]]'
|
||||||
|
else:
|
||||||
|
query = '["and",' \
|
||||||
|
'["=", "catalog_environment", "{0}"],' \
|
||||||
|
'["null?", "catalog_timestamp", false]]'.format(env),
|
||||||
|
nodes = get_or_abort(puppetdb.nodes,
|
||||||
|
query=query,
|
||||||
|
with_status=False,
|
||||||
|
order_by='[{"field": "certname", "order": "asc"}]')
|
||||||
|
nodes, temp = tee(nodes)
|
||||||
|
|
||||||
|
for node in temp:
|
||||||
|
nodenames.append(node.name)
|
||||||
|
|
||||||
|
for node in nodes:
|
||||||
|
table_row = {
|
||||||
|
'name': node.name,
|
||||||
|
'catalog_timestamp': node.catalog_timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(nodenames) > 1:
|
||||||
|
form = CatalogForm()
|
||||||
|
|
||||||
|
form.compare.data = node.name
|
||||||
|
form.against.choices = [(x, x) for x in nodenames
|
||||||
|
if x != node.name]
|
||||||
|
table_row['form'] = form
|
||||||
|
else:
|
||||||
|
table_row['form'] = None
|
||||||
|
|
||||||
|
catalog_list.append(table_row)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'catalogs.html',
|
||||||
|
nodes=catalog_list,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)
|
||||||
|
else:
|
||||||
|
log.warn('Access to catalog interface disabled by administrator')
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
@app.route('/catalog/<node_name>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
|
@app.route('/<env>/catalog/<node_name>')
|
||||||
|
def catalog_node(env, node_name):
|
||||||
|
"""Fetches from PuppetDB the compiled catalog of a given node.
|
||||||
|
|
||||||
|
:param env: Find the catalog with this environment value
|
||||||
|
:type env: :obj:`string`
|
||||||
|
"""
|
||||||
|
envs = environments()
|
||||||
|
check_env(env, envs)
|
||||||
|
|
||||||
|
if app.config['ENABLE_CATALOG']:
|
||||||
|
catalog = get_or_abort(puppetdb.catalog,
|
||||||
|
node=node_name)
|
||||||
|
return render_template('catalog.html',
|
||||||
|
catalog=catalog,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)
|
||||||
|
else:
|
||||||
|
log.warn('Access to catalog interface disabled by administrator')
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
@app.route('/catalog/submit', methods=['POST'], defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
|
@app.route('/<env>/catalog/submit', methods=['POST'])
|
||||||
|
def catalog_submit(env):
|
||||||
|
"""Receives the submitted form data from the catalogs page and directs
|
||||||
|
the users to the comparison page. Directs users back to the catalogs
|
||||||
|
page if no form submission data is found.
|
||||||
|
|
||||||
|
:param env: This parameter only directs the response page to the right
|
||||||
|
environment. If this environment does not exist return the use to the
|
||||||
|
catalogs page with the right environment.
|
||||||
|
:type env: :obj:`string`
|
||||||
|
"""
|
||||||
|
envs = environments()
|
||||||
|
check_env(env, envs)
|
||||||
|
|
||||||
|
if app.config['ENABLE_CATALOG']:
|
||||||
|
form = CatalogForm(request.form)
|
||||||
|
|
||||||
|
form.against.choices = [(form.against.data, form.against.data)]
|
||||||
|
if form.validate_on_submit():
|
||||||
|
compare = form.compare.data
|
||||||
|
against = form.against.data
|
||||||
|
return redirect(
|
||||||
|
url_for('catalog_compare',
|
||||||
|
env=env,
|
||||||
|
compare=compare,
|
||||||
|
against=against))
|
||||||
|
return redirect(url_for('catalogs', env=env))
|
||||||
|
else:
|
||||||
|
log.warn('Access to catalog interface disabled by administrator')
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
@app.route('/catalogs/compare/<compare>...<against>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
|
@app.route('/<env>/catalogs/compare/<compare>...<against>')
|
||||||
|
def catalog_compare(env, compare, against):
|
||||||
|
"""Compares the catalog of one node, parameter compare, with that of
|
||||||
|
with that of another node, parameter against.
|
||||||
|
|
||||||
|
:param env: Ensure that the 2 catalogs are in the same environment
|
||||||
|
:type env: :obj:`string`
|
||||||
|
"""
|
||||||
|
envs = environments()
|
||||||
|
check_env(env, envs)
|
||||||
|
|
||||||
|
if app.config['ENABLE_CATALOG']:
|
||||||
|
compare_cat = get_or_abort(puppetdb.catalog,
|
||||||
|
node=compare)
|
||||||
|
against_cat = get_or_abort(puppetdb.catalog,
|
||||||
|
node=against)
|
||||||
|
|
||||||
|
return render_template('catalog_compare.html',
|
||||||
|
compare=compare_cat,
|
||||||
|
against=against_cat,
|
||||||
|
envs=envs,
|
||||||
|
current_env=env)
|
||||||
else:
|
else:
|
||||||
log.warn('Access to catalog interface disabled by administrator')
|
log.warn('Access to catalog interface disabled by administrator')
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ from __future__ import unicode_literals
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
from flask.ext.wtf import Form
|
from flask.ext.wtf import Form
|
||||||
from wtforms import RadioField, TextAreaField, validators
|
from wtforms import (
|
||||||
|
HiddenField, RadioField, SelectField,
|
||||||
|
TextAreaField, validators
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class QueryForm(Form):
|
class QueryForm(Form):
|
||||||
@@ -14,7 +17,17 @@ class QueryForm(Form):
|
|||||||
('nodes', 'Nodes'),
|
('nodes', 'Nodes'),
|
||||||
('resources', 'Resources'),
|
('resources', 'Resources'),
|
||||||
('facts', 'Facts'),
|
('facts', 'Facts'),
|
||||||
('fact-names', 'Fact Names'),
|
('factsets', 'Fact Sets'),
|
||||||
|
('fact-paths', 'Fact Paths'),
|
||||||
|
('fact-contents', 'Fact Contents'),
|
||||||
('reports', 'Reports'),
|
('reports', 'Reports'),
|
||||||
('events', 'Events'),
|
('events', 'Events'),
|
||||||
|
('catalogs', 'Catalogs'),
|
||||||
|
('edges', 'Edges'),
|
||||||
|
('environments', 'Environments'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
class CatalogForm(Form):
|
||||||
|
"""The form used to compare the catalogs of different nodes."""
|
||||||
|
compare = HiddenField('compare')
|
||||||
|
against = SelectField('against')
|
||||||
|
|||||||
@@ -31,11 +31,60 @@ th.tablesorter-headerDesc::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
width: 47.5%;
|
width: 45%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
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;
|
||||||
@@ -75,6 +124,23 @@ th.tablesorter-headerDesc::after {
|
|||||||
color: #FFF;
|
color: #FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.menu.yellow {
|
||||||
|
background-color: #F0E965;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.yellow.header, i.yellow {
|
||||||
|
color: #F0E965;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.labels .yellow.label::before, .ui.yellow.labels .label::before, .ui.yellow.label::before {
|
||||||
|
background-color: #F0E965;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.yellow.labels .label, .ui.yellow.label {
|
||||||
|
background-color: #F0E965;
|
||||||
|
border-color: #F0E965;
|
||||||
|
}
|
||||||
|
|
||||||
#scroll-btn-top {
|
#scroll-btn-top {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -57,6 +57,10 @@
|
|||||||
sortList: [[0, 0]]
|
sortList: [[0, 0]]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$('.reports').tablesorter({
|
||||||
|
sortList: [[0, 0]]
|
||||||
|
})
|
||||||
|
|
||||||
$('input.filter-table').parent('div').removeClass('hide');
|
$('input.filter-table').parent('div').removeClass('hide');
|
||||||
|
|
||||||
$("input.filter-table").on("keyup", function(e) {
|
$("input.filter-table").on("keyup", function(e) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% macro facts_table(facts, autofocus=False, condensed=False, show_node=False, show_value=True, link_facts=False, margin_top=20, margin_bottom=20) -%}
|
{% macro facts_table(facts, current_env, autofocus=False, condensed=False, show_node=False, show_value=True, link_facts=False, margin_top=20, margin_bottom=20) -%}
|
||||||
<div class="ui fluid icon input hide" style="margin-bottom:20px">
|
<div class="ui fluid icon input hide" style="margin-bottom:20px">
|
||||||
<input {% if autofocus %} autofocus="autofocus" {% endif %} class="filter-table" placeholder="Type here to filter...">
|
<input {% if autofocus %} autofocus="autofocus" {% endif %} class="filter-table" placeholder="Type here to filter...">
|
||||||
</div>
|
</div>
|
||||||
@@ -19,16 +19,24 @@
|
|||||||
{% for fact in facts %}
|
{% for fact in facts %}
|
||||||
<tr>
|
<tr>
|
||||||
{% if show_node %}
|
{% if show_node %}
|
||||||
<td><a href="{{url_for('node', node_name=fact.node)}}">{{fact.node}}</a></td>
|
<td><a href="{{url_for('node', env=current_env, node_name=fact.node)}}">{{fact.node}}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td><a href="{{url_for('fact', fact=fact.name)}}">{{fact.name}}</a></td>
|
<td><a href="{{url_for('fact', env=current_env, fact=fact.name)}}">{{fact.name}}</a></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if show_value %}
|
{% if show_value %}
|
||||||
<td style="word-wrap:break-word">
|
<td style="word-wrap:break-word">
|
||||||
{% if link_facts %}
|
{% if link_facts %}
|
||||||
<a href="{{url_for('fact_value', fact=fact.name, value=fact.value)}}">{{fact.value}}</a>
|
{% if fact.value is mapping %}
|
||||||
|
<a href="{{url_for('fact_value', env=current_env, fact=fact.name, value=fact.value)}}"><pre>{{fact.value|jsonprint}}</pre></a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{url_for('fact_value', env=current_env, fact=fact.name, value=fact.value)}}">{{fact.value}}</a>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{fact.value}}
|
{% if fact.value is mapping %}
|
||||||
|
<pre>{{fact.value|jsonprint}}</pre>
|
||||||
|
{% else %}
|
||||||
|
{{fact.value}}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -70,30 +78,40 @@
|
|||||||
</script>
|
</script>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro reports_table(reports, nodename, reports_count, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True) -%}
|
{% macro reports_table(reports, reports_count, report_event_counts, current_env, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True, show_run_col=False, show_full_col=False, show_search_bar=False, searchable=False) -%}
|
||||||
|
{% if show_search_bar %}
|
||||||
|
<div class="ui fluid icon input hide" style="margin-bottom:20px">
|
||||||
|
<input autofocus="autofocus" class="filter-table" placeholder="Type here to filter...">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="ui info message">
|
<div class="ui info message">
|
||||||
|
|
||||||
Only showing the last {{reports_count}} reports.
|
Only showing {{reports_count}} reports sorted by Start Time.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<table class='ui table basic {% if condensed %}compact{% endif %}'>
|
<table class='ui table basic {% if condensed %}compact{% endif %} report'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Start time</th>
|
<th>Start time</th>
|
||||||
|
<th>Status</th>
|
||||||
|
{% if show_host_col %}
|
||||||
|
<th>Hostname</th>
|
||||||
|
{% endif %}
|
||||||
|
{% if show_run_col %}
|
||||||
<th>Run time</th>
|
<th>Run time</th>
|
||||||
|
{% endif %}
|
||||||
|
{% if show_full_col %}
|
||||||
<th>Full report</th>
|
<th>Full report</th>
|
||||||
|
{% endif %}
|
||||||
{% if show_conf_col %}
|
{% if show_conf_col %}
|
||||||
<th>Configuration version</th>
|
<th>Configuration version</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if show_agent_col %}
|
{% if show_agent_col %}
|
||||||
<th>Agent version</th>
|
<th>Agent version</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if show_host_col %}
|
|
||||||
<th>Hostname</th>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody {% if searchable %}class="searchable" {% endif %}>
|
||||||
{% for report in reports %}
|
{% for report in reports %}
|
||||||
{% if hash_truncate %}
|
{% if hash_truncate %}
|
||||||
{% set rep_hash = "%s…"|format(report.hash_[0:10])|safe %}
|
{% set rep_hash = "%s…"|format(report.hash_[0:10])|safe %}
|
||||||
@@ -105,21 +123,80 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td rel="utctimestamp">{{report.start}}</td>
|
<td rel="utctimestamp">{{report.start}}</td>
|
||||||
|
<td>
|
||||||
|
{% call status_counts(status=report.status, node_name=report.node, events=report_event_counts[report.hash_], report_hash=report.hash_, current_env=current_env) %}{% endcall %}
|
||||||
|
</td>
|
||||||
|
{% if show_host_col %}
|
||||||
|
<td><a href="{{url_for('node', env=current_env, node_name=report.node)}}">{{ report.node }}</a></td>
|
||||||
|
{% endif %}
|
||||||
|
{% if show_run_col %}
|
||||||
<td>{{report.run_time}}</td>
|
<td>{{report.run_time}}</td>
|
||||||
|
{% endif %}
|
||||||
<td><a href="{{url_for('report', node_name=nodename, report_id=report.hash_)}}">{{rep_hash}}</a></td>
|
{% if show_full_col %}
|
||||||
|
<td><a href="{{url_for('report', env=current_env, node_name=report.node, report_id=report.hash_)}}">{{rep_hash}}</a></td>
|
||||||
|
{% endif %}
|
||||||
{% if show_conf_col %}
|
{% if show_conf_col %}
|
||||||
<td>{{report.version}}</td>
|
<td>{{report.version}}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if show_agent_col %}
|
{% if show_agent_col %}
|
||||||
<td>{{report.agent_version}}</td>
|
<td>{{report.agent_version}}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if show_host_col %}
|
|
||||||
<td><a href="{{url_for('node', node_name=report.node)}}">{{ report.node }}</a></td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
{% macro status_counts(caller, status, node_name, events, current_env, unreported_time=False, report_hash=False) -%}
|
||||||
|
<a class="ui small status label
|
||||||
|
{% if status == 'failed' -%}
|
||||||
|
failed
|
||||||
|
{% elif status == 'changed' -%}
|
||||||
|
changed
|
||||||
|
{% elif status == 'unreported' -%}
|
||||||
|
unreported
|
||||||
|
{% elif status == 'noop' -%}
|
||||||
|
noop
|
||||||
|
{% elif status == 'unchanged' -%}
|
||||||
|
unchanged
|
||||||
|
{% endif -%}
|
||||||
|
" href="
|
||||||
|
{% if report_hash -%}
|
||||||
|
{{url_for('report', env=current_env, node_name=node_name, report_id=report_hash)}}
|
||||||
|
{% else -%}
|
||||||
|
{{url_for('report_latest', env=current_env, node_name=node_name)}}
|
||||||
|
{% endif -%}
|
||||||
|
">
|
||||||
|
{{status}}
|
||||||
|
</a>
|
||||||
|
{% if status == 'unreported' %}
|
||||||
|
<span class="ui small label status"> {{ unreported_time }} </span>
|
||||||
|
{% else %}
|
||||||
|
{% 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 changed">{{events['successes']}}</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 %}
|
||||||
|
{%- endmacro %}
|
||||||
|
{% macro render_pagination(pagination) -%}
|
||||||
|
<div class="pagination">
|
||||||
|
{% if pagination.has_prev %}
|
||||||
|
<a href="{{url_for_pagination(1)}}">« First</a>
|
||||||
|
<a href="{{url_for_pagination(pagination.page - 1)}}">Prev</a>
|
||||||
|
{% endif %}
|
||||||
|
{% for page in pagination.iter_pages() %}
|
||||||
|
{% if page %}
|
||||||
|
{% if page != pagination.page %}
|
||||||
|
<a href="{{url_for_pagination(page)}}">{{page}}</a>
|
||||||
|
{% else %}
|
||||||
|
<span style="font-weight:bold;">{{page}}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="ellipsis">...</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if pagination.has_next %}
|
||||||
|
<a href="{{url_for_pagination(pagination.page + 1)}}">Next</a>
|
||||||
|
<a href="{{url_for_pagination(pagination.pages)}}">Last »</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|||||||
@@ -10,13 +10,15 @@
|
|||||||
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{url_for('node', 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>
|
||||||
@@ -48,7 +50,7 @@
|
|||||||
<th>Target</th>
|
<th>Target</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class='searchable'>
|
||||||
{% for edge in catalog.get_edges() %}
|
{% for edge in catalog.get_edges() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{edge.source}}</td>
|
<td>{{edge.source}}</td>
|
||||||
|
|||||||
90
puppetboard/templates/catalog_compare.html
Normal file
90
puppetboard/templates/catalog_compare.html
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="ui fluid icon input hide" style="margin-bottom:20px">
|
||||||
|
<input autofocus="autofocus" class="filter-table" placeholder="Type here to filter...">
|
||||||
|
</div>
|
||||||
|
<table class="ui basic table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th><h1>Comparing</h1></th>
|
||||||
|
<th><h1>Against</h1></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{compare.node}}</td>
|
||||||
|
<td>{{against.node}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="ui fixed table compact catalog">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Resources</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class='searchable'>
|
||||||
|
{% for resource in compare.get_resources() %}
|
||||||
|
<tr>
|
||||||
|
<td>{{resource.type_}}[{{resource.name}}]</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<table class="ui fixed table compact catalog">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Resources</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class='searchable'>
|
||||||
|
{% for resource in against.get_resources() %}
|
||||||
|
<tr>
|
||||||
|
<td>{{resource.type_}}[{{resource.name}}]</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="ui fixed table compact catalog">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Edges</th>
|
||||||
|
<th>-></th>
|
||||||
|
<th>Target</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class='searchable'>
|
||||||
|
{% for edge in compare.get_edges() %}
|
||||||
|
<tr>
|
||||||
|
<td>{{edge.source}}</td>
|
||||||
|
<td>{{edge.relationship}}</td>
|
||||||
|
<td>{{edge.target}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<table class="ui fixed table compact catalog">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Edge</th>
|
||||||
|
<th>-></th>
|
||||||
|
<th>Target</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class='searchable'>
|
||||||
|
{% for edge in against.get_edges() %}
|
||||||
|
<tr>
|
||||||
|
<td>{{edge.source}}</td>
|
||||||
|
<td>{{edge.relationship}}</td>
|
||||||
|
<td>{{edge.target}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock content %}
|
||||||
40
puppetboard/templates/catalogs.html
Normal file
40
puppetboard/templates/catalogs.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% import '_macros.html' as macros %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="ui fluid icon input hide" style="margin-bottom:20px">
|
||||||
|
<input autofocus="autofocus" class="filter-table" placeholder="Type here to filter...">
|
||||||
|
</div>
|
||||||
|
<table class='ui compact basic table nodes'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Hostname</th>
|
||||||
|
<th>Compile Time</th>
|
||||||
|
<th>Compare With</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="searchable">
|
||||||
|
{% for node in nodes %}
|
||||||
|
<tr>
|
||||||
|
<td></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>
|
||||||
|
{% if node.form %}
|
||||||
|
<div class="ui form">
|
||||||
|
<form method="POST" action="{{url_for('catalog_submit', env=current_env)}}">
|
||||||
|
{{node.form.csrf_token}}
|
||||||
|
<div class="field inline">
|
||||||
|
{{node.form.compare}}
|
||||||
|
{{node.form.against}}
|
||||||
|
<input type="submit" class="ui submit button" style="height:auto;" value="Compare"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock content %}
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
{{macros.facts_graph(facts, autofocus=True, show_node=True, margin_bottom=10)}}
|
{{macros.facts_graph(facts, autofocus=True, show_node=True, margin_bottom=10)}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if value %}
|
{% if value %}
|
||||||
{{macros.facts_table(facts, autofocus=True, show_node=True, show_value=False, margin_bottom=10)}}
|
{{macros.facts_table(facts, current_env=current_env, autofocus=True, show_node=True, show_value=False, margin_bottom=10)}}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{macros.facts_table(facts, autofocus=True, show_node=True, link_facts=True, margin_bottom=10)}}
|
{{macros.facts_table(facts, current_env=current_env, autofocus=True, show_node=True, link_facts=True, margin_bottom=10)}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<span class='ui label darkblue'>{{key}}</span>
|
<span class='ui label darkblue'>{{key}}</span>
|
||||||
<ul class="searchable">
|
<ul class="searchable">
|
||||||
{%- for fact in facts_list %}
|
{%- for fact in facts_list %}
|
||||||
<li><a href="{{url_for('fact', fact=fact)}}">{{fact}}</a></li>
|
<li><a href="{{url_for('fact', env=current_env, fact=fact)}}">{{fact}}</a></li>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
|
{% 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="nodes?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>
|
||||||
@@ -12,8 +16,8 @@
|
|||||||
<span>with status failed</span>
|
<span>with status failed</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<a href="nodes?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>
|
||||||
@@ -21,8 +25,8 @@
|
|||||||
<span>with status pending</span>
|
<span>with status pending</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<a href="nodes?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>
|
||||||
@@ -30,8 +34,8 @@
|
|||||||
<span>with status changed</span>
|
<span>with status changed</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<a href="nodes?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>
|
||||||
@@ -75,40 +79,29 @@
|
|||||||
{% if node.status != 'unchanged' %}
|
{% if node.status != 'unchanged' %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a class="ui small status label
|
{% if node.latest_report_hash %}
|
||||||
{% if node.status == 'failed' %}
|
{{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)}}
|
||||||
red
|
{% else %}
|
||||||
{% elif node.status == 'changed' %}
|
{{macros.status_counts(status=node.status, node_name=node.name, events=node.events, unreported_time=node.unreported_time, current_env=current_env)}}
|
||||||
green
|
{% endif %}
|
||||||
{% elif node.status == 'unreported' %}
|
|
||||||
black
|
|
||||||
{% elif node.status == 'noop' %}
|
|
||||||
blue
|
|
||||||
{% endif %}
|
|
||||||
" href="{{url_for('report_latest', node_name=node.name)}}">
|
|
||||||
{{node.status}}
|
|
||||||
</a>
|
|
||||||
{% if node.status=='unreported'%}
|
|
||||||
<span class="ui small label status"> {{ node.unreported_time }} </span>
|
|
||||||
{% else %}
|
|
||||||
{% if node.events['failures'] %}<span class="ui small count label red">{{node.events['failures']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
|
||||||
{% if node.events['successes'] %}<span class="ui small count label green">{{node.events['successes']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
|
||||||
{% if node.events['skips'] %}<span class="ui small count label orange">{{node.events['skips']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{url_for('node', 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', 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 %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if node.report_timestamp %}
|
{% if node.report_timestamp %}
|
||||||
<a title='Reports' href="{{url_for('reports_node', node_name=node.name)}}"><i class='large darkblue book icon'></i></a>
|
<a title='Reports' href="{{url_for('reports_node', env=current_env, node_name=node.name)}}"><i class='large darkblue book icon'></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<nav class="ui fixed darkblue inverted menu">
|
<div class="ui fixed darkblue inverted menu">
|
||||||
<div class="title item">
|
<div class="title item">
|
||||||
Puppetboard
|
Puppetboard
|
||||||
</div>
|
</div>
|
||||||
@@ -34,13 +34,24 @@
|
|||||||
('reports', 'Reports'),
|
('reports', 'Reports'),
|
||||||
('metrics', 'Metrics'),
|
('metrics', 'Metrics'),
|
||||||
('inventory', 'Inventory'),
|
('inventory', 'Inventory'),
|
||||||
|
('catalogs', 'Catalogs'),
|
||||||
('query', 'Query'),
|
('query', 'Query'),
|
||||||
] %}
|
] %}
|
||||||
<a {% if endpoint == request.endpoint %} class="active item" {% else %} class="item" {% endif %}
|
<a {% if endpoint == request.endpoint %} class="active item" {% else %} class="item" {% endif %}
|
||||||
href="{{ url_for(endpoint) }}">{{ caption }}</a>
|
href="{{ url_for(endpoint, env=current_env) }}">{{ caption }}</a>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
<div class="item" style="float:right"><a href="https://github.com/puppet-community/puppetboard" target="_blank">v0.0.5</a></div>
|
<div class="ui item dropdown">
|
||||||
</nav>
|
Environments
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
<div class="menu">
|
||||||
|
<a class="item {% if '*' == current_env %}active{% endif %}" href="{{url_for_environments('*')}}">All environments</a>
|
||||||
|
{% for env in envs %}
|
||||||
|
<a class="item {% if env == current_env %}active{% endif %}" href="{{url_for_environments(env)}}">{{env}}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item right"><a href="https://github.com/puppet-community/puppetboard" target="_blank">v0.1.2</a></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>
|
||||||
<div class="fourteen wide column">
|
<div class="fourteen wide column">
|
||||||
@@ -55,7 +66,7 @@
|
|||||||
|
|
||||||
<footer class="ui absolute fixed bottom">
|
<footer class="ui absolute fixed bottom">
|
||||||
<div>
|
<div>
|
||||||
Copyright © 2013-{{ now('%Y') }} <a href="https://github.com/daenney" target="_blank">Daniele Sluijters</a>. <span style="float:right">Live from PuppetDB.</span>
|
Copyright © 2013-{{ now('%Y') }} <a href="https://github.com/puppet-community" target="_blank">Puppet Community</a>. <span style="float:right">Live from PuppetDB.</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@@ -80,6 +91,9 @@
|
|||||||
<script src="{{ url_for('static', filename='js/lists.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/lists.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/tables.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/tables.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/scroll.top.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/scroll.top.js') }}"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(".ui.dropdown").dropdown();
|
||||||
|
</script>
|
||||||
|
|
||||||
{% block script %} {% endblock script %}
|
{% block script %} {% endblock script %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<h1>Metrics</h1>
|
<h1>Metrics</h1>
|
||||||
<ul>
|
<ul>
|
||||||
{% for key,value in metrics %}
|
{% for key,value in metrics %}
|
||||||
<li><a href="{{url_for('metric', metric=value)}}">{{key}}</li>
|
<li><a href="{{url_for('metric', env=current_env, metric=value)}}">{{key}}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -28,12 +28,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<h1>Reports</h1>
|
<h1>Reports</h1>
|
||||||
{{ macros.reports_table(reports, node.name, reports_count, condensed=True, hash_truncate=True, show_conf_col=False, show_agent_col=False, show_host_col=False)}}
|
{{ macros.reports_table(reports, reports_count, report_event_counts, condensed=True, hash_truncate=True, show_conf_col=False, show_agent_col=False, show_host_col=False, current_env=current_env)}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='column'>
|
<div class='column'>
|
||||||
<h1>Facts</h1>
|
<h1>Facts</h1>
|
||||||
{{macros.facts_table(facts, link_facts=True, condensed=True)}}
|
{{macros.facts_table(facts, link_facts=True, condensed=True, current_env=current_env)}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
|
{% import '_macros.html' as macros %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui fluid icon input hide" style="margin-bottom:20px">
|
<div class="ui fluid icon input hide" style="margin-bottom:20px">
|
||||||
<input autofocus="autofocus" class="filter-table" placeholder="Type here to filter...">
|
<input autofocus="autofocus" class="filter-table" placeholder="Type here to filter...">
|
||||||
@@ -17,40 +18,28 @@
|
|||||||
{% for node in nodes %}
|
{% for node in nodes %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a class="ui small status label
|
{% if node.latest_report_hash %}
|
||||||
{% if node.status == 'failed' %}
|
{{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)}}
|
||||||
red
|
{% else %}
|
||||||
{% elif node.status == 'changed' %}
|
{{macros.status_counts(status=node.status, node_name=node.name, events=node.events, unreported_time=node.unreported_time, current_env=current_env)}}
|
||||||
green
|
{% endif %}
|
||||||
{% elif node.status == 'unreported' %}
|
|
||||||
black
|
|
||||||
{% elif node.status == 'noop' %}
|
|
||||||
blue
|
|
||||||
{% endif %}
|
|
||||||
" href="{{url_for('report_latest', node_name=node.name)}}">
|
|
||||||
{{node.status}}
|
|
||||||
</a>
|
|
||||||
{% if node.status=='unreported'%}
|
|
||||||
<span class="ui small label status"> {{ node.unreported_time }} </label>
|
|
||||||
{% else %}
|
|
||||||
<span>{% if node.events['failures'] %}<span class="ui small count label red">{{node.events['failures']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
|
||||||
{% if node.events['successes'] %}<span class="ui small count label green">{{node.events['successes']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}</span>
|
|
||||||
{% if node.events['skips'] %}<span class="ui small count label yellow">{{node.events['skips']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
<td><a href="{{url_for('node', 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', 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', 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 %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if node.report_timestamp %}
|
{% if node.report_timestamp %}
|
||||||
<a title='Reports' href="{{url_for('reports_node', node_name=node.name)}}"><i class='large darkblue book icon'></i></a>
|
<a title='Reports' href="{{url_for('reports_node', env=current_env, node_name=node.name, page=1)}}"><i class='large darkblue book icon'></i></a>
|
||||||
<i class='large darkblue trash icon'></i>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -11,10 +11,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<form method="POST" action="{{ url_for('query') }}">
|
<form method="POST" action="{{ url_for('query', env=current_env) }}">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<div class="field {% if form.query.errors %} error {% endif %}">
|
<div class="field {% if form.query.errors %} error {% endif %}">
|
||||||
{{ form.query(autofocus="autofocus", rows=5, placeholder="Enter your query: [\"=\", \"name\", \"hostname\"]. You may omit the opening and closing bracket.") }}
|
{{ form.query(autofocus="autofocus", rows=5, placeholder="Enter your query: [\"=\", \"certname\", \"hostname\"]. You may omit the opening and closing bracket.") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="inline fields">
|
<div class="inline fields">
|
||||||
{% for subfield in form.endpoints %}
|
{% for subfield in form.endpoints %}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{url_for('node', node_name=report.node)}}">{{ report.node }}</a></td>
|
<td><a href="{{url_for('node', env=current_env, node_name=report.node)}}">{{ report.node }}</a></td>
|
||||||
<td>
|
<td>
|
||||||
{{report.version}}
|
{{report.version}}
|
||||||
</td>
|
</td>
|
||||||
@@ -39,35 +39,73 @@
|
|||||||
<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>
|
||||||
<td>{{event.item['old']}}</td>
|
<td>{{event.item['old']}}</td>
|
||||||
<td>{{event.item['new']}}</td>
|
<td>{{event.item['new']}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{# <tr>
|
{% endfor %}
|
||||||
<td class='message' colspan='4'>
|
</tbody>
|
||||||
<div id='message-event-{{loop.index}}'>
|
</table>
|
||||||
{{event.item['message']}}
|
|
||||||
</div>
|
<h1>Logs</h1>
|
||||||
</td>
|
<table class="ui basic table compact">
|
||||||
</tr>#}
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Tags</th>
|
||||||
|
<th>Message</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for log in logs %}
|
||||||
|
{% if log.level == 'info' or log.level == 'notice' %}
|
||||||
|
<tr class='positive'>
|
||||||
|
{% elif log.level == 'warning' %}
|
||||||
|
<tr class='warning'>
|
||||||
|
{% else %}
|
||||||
|
<tr class='error'>
|
||||||
|
{% endif %}
|
||||||
|
<td rel="utctimestamp">{{log.time}}</td>
|
||||||
|
<td>{{log.source}}</td>
|
||||||
|
<td>{{log.tags|join(', ')}}</td>
|
||||||
|
<td>{{log.message}}</td>
|
||||||
|
{% if log.file != None and log.line != None %}
|
||||||
|
<td>{{log.file}}:{{log.line}}</td>
|
||||||
|
{% else %}
|
||||||
|
<td></td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h1>Metrics</h1>
|
||||||
|
<table class="ui basic table compact">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for metric in metrics %}
|
||||||
|
<tr>
|
||||||
|
<td>{{metric.category}}</td>
|
||||||
|
<td>{{metric.name}}</td>
|
||||||
|
<td>{{metric.value|round(2)}}</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
{% block script %}
|
|
||||||
<script type='text/javascript'>
|
|
||||||
jQuery(function ($) {
|
|
||||||
$("[rel=tooltip]").tooltip();
|
|
||||||
$(".event").click(function() {
|
|
||||||
$("#message-" + this.id).slideToggle(200);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock script %}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
|
{% import '_macros.html' as macros %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui warning message">
|
{{ 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)}}
|
||||||
Pending <a href="https://tickets.puppetlabs.com/browse/PDB-201">#PDB-201</a>. You can access reports for a node or individual reports through the <a href="{{url_for('nodes')}}">Nodes</a> tab.
|
{{ macros.render_pagination(pagination)}}
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{% extends 'layout.html' %}
|
|
||||||
{% import '_macros.html' as macros %}
|
|
||||||
{% block content %}
|
|
||||||
{{ macros.reports_table(reports, nodename, reports_count, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True)}}
|
|
||||||
{% endblock content %}
|
|
||||||
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from math import ceil
|
||||||
from requests.exceptions import HTTPError, ConnectionError
|
from requests.exceptions import HTTPError, ConnectionError
|
||||||
from pypuppetdb.errors import EmptyResponseError
|
from pypuppetdb.errors import EmptyResponseError
|
||||||
|
|
||||||
@@ -29,17 +30,6 @@ def get_or_abort(func, *args, **kwargs):
|
|||||||
abort(204)
|
abort(204)
|
||||||
|
|
||||||
|
|
||||||
def limit_reports(reports, limit):
|
|
||||||
"""Helper to yield a number of from the reports generator.
|
|
||||||
|
|
||||||
This is an ugly solution at best...
|
|
||||||
"""
|
|
||||||
for count, report in enumerate(reports):
|
|
||||||
if count == limit:
|
|
||||||
raise StopIteration
|
|
||||||
yield report
|
|
||||||
|
|
||||||
|
|
||||||
def yield_or_stop(generator):
|
def yield_or_stop(generator):
|
||||||
"""Similar in intent to get_or_abort this helper will iterate over our
|
"""Similar in intent to get_or_abort this helper will iterate over our
|
||||||
generators and handle certain errors.
|
generators and handle certain errors.
|
||||||
@@ -54,3 +44,35 @@ def yield_or_stop(generator):
|
|||||||
raise
|
raise
|
||||||
except (EmptyResponseError, ConnectionError, HTTPError):
|
except (EmptyResponseError, ConnectionError, HTTPError):
|
||||||
raise StopIteration
|
raise StopIteration
|
||||||
|
|
||||||
|
class Pagination(object):
|
||||||
|
|
||||||
|
def __init__(self, page, per_page, total_count):
|
||||||
|
self.page = page
|
||||||
|
self.per_page = per_page
|
||||||
|
self.total_count = total_count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pages(self):
|
||||||
|
return int(ceil(self.total_count / float(self.per_page)))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_prev(self):
|
||||||
|
return self.page > 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_next(self):
|
||||||
|
return self.page < self.pages
|
||||||
|
|
||||||
|
def iter_pages(self, left_edge=2, left_current=2,
|
||||||
|
right_current=5, right_edge=2):
|
||||||
|
last = 0
|
||||||
|
for num in xrange(1, self.pages + 1):
|
||||||
|
if num <= left_edge or \
|
||||||
|
(num > self.page - left_current - 1 and \
|
||||||
|
num < self.page + right_current) or \
|
||||||
|
num > self.pages - right_edge:
|
||||||
|
if last + 1 != num:
|
||||||
|
yield None
|
||||||
|
yield num
|
||||||
|
last = num
|
||||||
|
|||||||
@@ -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.1.1
|
pypuppetdb==0.2.1
|
||||||
requests==2.2.1
|
requests==2.2.1
|
||||||
|
|||||||
6
setup.py
6
setup.py
@@ -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.0.5"
|
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()
|
||||||
@@ -23,7 +23,7 @@ setup(
|
|||||||
author='Daniele Sluijters',
|
author='Daniele Sluijters',
|
||||||
author_email='daniele.sluijters+pypi@gmail.com',
|
author_email='daniele.sluijters+pypi@gmail.com',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
url='https://github.com/nedap/puppetboard',
|
url='https://github.com/puppet-community/puppetboard',
|
||||||
license='Apache License 2.0',
|
license='Apache License 2.0',
|
||||||
description='Web frontend for PuppetDB',
|
description='Web frontend for PuppetDB',
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
@@ -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.1.0, < 0.2.0",
|
"pypuppetdb >= 0.2.1, < 0.3.0",
|
||||||
],
|
],
|
||||||
keywords="puppet puppetdb puppetboard",
|
keywords="puppet puppetdb puppetboard",
|
||||||
classifiers=[
|
classifiers=[
|
||||||
|
|||||||
Reference in New Issue
Block a user