Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4636d2043b | ||
|
|
29344e0f17 | ||
|
|
22603e3e32 | ||
|
|
2c423d67c9 | ||
|
|
66d344b3c1 | ||
|
|
a169b25a99 | ||
|
|
b2880f13a5 | ||
|
|
28befcef9d | ||
|
|
6ca7067d8c | ||
|
|
e5c9b300ef | ||
|
|
548599bc59 | ||
|
|
1713f8ec95 | ||
|
|
b82493e201 | ||
|
|
c2129e3000 | ||
|
|
126b53aada | ||
|
|
9bd0784838 | ||
|
|
7bef20b116 | ||
|
|
c24e927c27 | ||
|
|
9488e8cb83 | ||
|
|
df915834a4 | ||
|
|
ecf94ef6fa | ||
|
|
f4d751060a | ||
|
|
38b2a2fc05 | ||
|
|
7cd8908adb | ||
|
|
26825aee4f | ||
|
|
8f5f7ac7c4 | ||
|
|
38f7958842 | ||
|
|
db2f8f8b59 | ||
|
|
64c26e19c2 | ||
|
|
671d538a8b | ||
|
|
42ed123fe3 | ||
|
|
16b197e0ce | ||
|
|
bb26c5bbea | ||
|
|
1758f972f1 | ||
|
|
2f347fd665 | ||
|
|
3b817e0a5e | ||
|
|
c229f51556 | ||
|
|
d2c47df31f | ||
|
|
c1fd33fb5c | ||
|
|
b17a2b0450 | ||
|
|
d4d7b6a56a | ||
|
|
4b96cfe196 | ||
|
|
484727b62c | ||
|
|
40511c007a | ||
|
|
a21bd0ac1d | ||
|
|
f13100664a | ||
|
|
e76fdb578c | ||
|
|
c59607564a | ||
|
|
cdedf94506 | ||
|
|
796e2ee7fd | ||
|
|
ac44aefea4 | ||
|
|
9e09db5f2d | ||
|
|
10147b993e | ||
|
|
507df87234 | ||
|
|
709d14e9a2 | ||
|
|
fd4051b619 | ||
|
|
21f9a8de8c | ||
|
|
fd88cad67b | ||
|
|
1e5f683b66 | ||
|
|
f908a1e86c | ||
|
|
4190e36278 | ||
|
|
b947bf05c2 |
28
.travis.yml
28
.travis.yml
@@ -5,29 +5,15 @@ python:
|
||||
- "2.7"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
env:
|
||||
- PINNED=TRUE
|
||||
- PINNED=FALSE
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: 2.6
|
||||
env: PINNED=FALSE
|
||||
- python: 2.7
|
||||
env: PINNED=FALSE
|
||||
- python: 3.5
|
||||
env: PINNED=FALSE
|
||||
- python: 3.6
|
||||
env: PINNED=FALSE
|
||||
|
||||
install:
|
||||
- if [ "${PINNED}" == "FALSE" ]; then python scripts/unpin.py; fi
|
||||
- pip install -r requirements.txt
|
||||
- pip install -U -r requirements-test.txt
|
||||
- pip install -q coverage coveralls --use-wheel
|
||||
- pip install -r requirements-test.txt
|
||||
- pip install -q coveralls --use-wheel
|
||||
script:
|
||||
- py.test --cov=puppetboard --pep8 -v
|
||||
- ./bandit.sh
|
||||
|
||||
- pytest --pep8
|
||||
- if [ "${TRAVIS_PYTHON_VERSION}" != "2.6" ]; then
|
||||
pip install bandit;
|
||||
bandit -r puppetboard;
|
||||
fi
|
||||
after_success:
|
||||
- coveralls
|
||||
|
||||
@@ -4,6 +4,22 @@ Changelog
|
||||
|
||||
This is the changelog for Puppetboard.
|
||||
|
||||
0.3.0
|
||||
=====
|
||||
|
||||
* Core UI Reowrk
|
||||
* Update to pypuppetdb 0.3.3
|
||||
* Fix sorty on data for index
|
||||
* Update debian documentation
|
||||
* Offline mode fix
|
||||
* Fix fact attrbitue error on paths
|
||||
* Enhanced testing
|
||||
* Radiator CSS uses same coloring
|
||||
* Markdown in config version
|
||||
* Update Flask
|
||||
* Cleanup requirements.txt
|
||||
* Update package maintainer for OpenBSD
|
||||
|
||||
0.2.1
|
||||
=====
|
||||
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@@ -1,12 +1,15 @@
|
||||
FROM python:2.7-alpine
|
||||
|
||||
ENV PUPPETBOARD_PORT 80
|
||||
ENV PUPPETBOARD_SETTINGS docker_settings.py
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
EXPOSE 80
|
||||
|
||||
ENV PUPPETBOARD_SETTINGS docker_settings.py
|
||||
RUN mkdir -p /usr/src/app/
|
||||
WORKDIR /usr/src/app/
|
||||
|
||||
COPY requirements*.txt /usr/src/app/
|
||||
RUN pip install -r requirements-docker.txt
|
||||
|
||||
COPY requirements-docker.txt /usr/src/app/
|
||||
RUN pip install --no-cache-dir -r requirements-docker.txt
|
||||
COPY . /usr/src/app
|
||||
|
||||
CMD gunicorn -b 0.0.0.0:${PUPPETBOARD_PORT} --access-logfile=/dev/stdout puppetboard.app:app
|
||||
|
||||
33
README.rst
33
README.rst
@@ -110,12 +110,12 @@ Native packages for your operating system will be provided in the near future.
|
||||
+-------------------+-----------+--------------------------------------------+
|
||||
| `ArchLinux`_ | available | Maintained by `Tim Meusel`_ |
|
||||
+-------------------+-----------+--------------------------------------------+
|
||||
| `OpenBSD`_ | available | Maintained by `Jasper Lievisse Adriaanse`_ |
|
||||
| `OpenBSD`_ | available | Maintained by `Sebastian Reitenbach`_ |
|
||||
+-------------------+-----------+--------------------------------------------+
|
||||
|
||||
.. _ArchLinux: https://aur.archlinux.org/packages/python2-puppetboard/
|
||||
.. _Tim Meusel: https://github.com/bastelfreak
|
||||
.. _Jasper Lievisse Adriaanse: https://github.com/jasperla
|
||||
.. _Sebastian Reitenbach: https://github.com/buzzdeee
|
||||
.. _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 12/13: https://build.opensuse.org/package/show/systemsmanagement:puppet/python-puppetboard
|
||||
@@ -151,7 +151,7 @@ and then install the requirements through:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install -r requirements.txt
|
||||
$ pip install -r requirements-test.txt
|
||||
|
||||
You're advised to do this inside a virtualenv specifically created to work on
|
||||
Puppetboard as to not pollute your global Python installation.
|
||||
@@ -241,6 +241,21 @@ Other settings that might be interesting in no particular order:
|
||||
* ``OFFLINE_MODE``: If set to ``True`` load static assets (jquery,
|
||||
semantic-ui, etc) from the local web server instead of a CDN.
|
||||
Defaults to ``False``.
|
||||
* ``DAILY_REPORTS_CHART_ENABLED``: Enable the use of daily chart graphs when
|
||||
looking at dashboard and node view.
|
||||
* ``DAILY_REPORTS_CHART_DAYS``: Number of days to show history for on the daily
|
||||
report graphs.
|
||||
* ``DISPLAYED_METRICS``: Metrics to show when displying node summary. Example:
|
||||
``'resources.total'``, ``'events.noop'``.
|
||||
* ``TABLE_COUNT_SELECTOR``: Configure the dropdown to limit number of hosts to
|
||||
show per page.
|
||||
* ``LITTLE_TABLE_COUNT``: Default number of reports to show when when looking at a node.
|
||||
* ``NORMAL_TABLE_COUNT``: Default number of nodes to show when displaying reports
|
||||
and catalog nodes.
|
||||
* ``LOCALISE_TIMESTAMP``: Normalize time based on localserver time.
|
||||
* ``DEV_LISTEN_HOST``: For use with `dev.py` for development. Default is localhost
|
||||
* ``DEV_LISTEN_PORT``: For use with `dev.py` for development. Default is 5000
|
||||
|
||||
|
||||
.. _pypuppetdb documentation: http://pypuppetdb.readthedocs.org/en/v0.1.0/quickstart.html#ssl
|
||||
.. _Flask documentation: http://flask.pocoo.org/docs/0.10/quickstart/#sessions
|
||||
@@ -285,6 +300,14 @@ scenarios:
|
||||
If you deploy Puppetboard through a different setup we'd welcome a pull
|
||||
request that adds the instructions to this section.
|
||||
|
||||
Installation On Linux Distros
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
`Debian Jessie Install`_.
|
||||
|
||||
.. _Debian Jessie Install: docs/Debian-Jessie.md
|
||||
|
||||
|
||||
Apache + mod_wsgi
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -683,11 +706,11 @@ Some people have already started building things with and around Puppetboard.
|
||||
|
||||
Packages
|
||||
--------
|
||||
* An OpenBSD port is being maintained by `Jasper Lievisse Adriaanse`_ and can be viewed `here <http://www.openbsd.org/cgi-bin/cvsweb/ports/www/puppetboard/>`_.
|
||||
* An OpenBSD port is being maintained by `Sebastian Reitenbach`_ and can be viewed `here <http://www.openbsd.org/cgi-bin/cvsweb/ports/www/puppetboard/>`_.
|
||||
|
||||
* A Docker image is being maintained by `Julien K.`_ and can be viewed `here <https://registry.hub.docker.com/u/kassis/puppetboard/>`_.
|
||||
|
||||
.. _Jasper Lievisse Adriaanse: https://github.com/jasperla
|
||||
.. _Sebastian Reitenbach: https://github.com/buzzdeee
|
||||
.. _Julien K.: https://github.com/juliengk
|
||||
|
||||
Contributing
|
||||
|
||||
12
bandit.sh
12
bandit.sh
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash -xe
|
||||
# Runs bandit tests
|
||||
|
||||
pyver="$(python -V 2>&1)"
|
||||
|
||||
if [[ $pyver =~ Python\ 2\.6 ]]
|
||||
then
|
||||
echo 'Bandit does not support python 2.6'
|
||||
else
|
||||
bandit -r puppetboard
|
||||
bandit -r tests
|
||||
fi
|
||||
62
docs/Debian-Jessie.md
Normal file
62
docs/Debian-Jessie.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Install Using debian jessie
|
||||
|
||||
```
|
||||
$ apt-get install python-pip git
|
||||
|
||||
$ mkdir /opt/voxpupuli-puppetboard/
|
||||
$ cd /opt/voxpupuli-puppetboard/
|
||||
$ git clone https://github.com/voxpupuli/puppetboard
|
||||
$ cd /opt/voxpupuli-puppetboard/puppetboard
|
||||
$ pip install puppetboard
|
||||
|
||||
```
|
||||
|
||||
* /etc/apache2/sites-available/voxpupuli-puppetboard.conf
|
||||
|
||||
```
|
||||
<VirtualHost *:80>
|
||||
ServerName puppetboard.my.domain
|
||||
WSGIDaemonProcess puppetboard user=www-data group=www-data threads=5 python-path=/usr/local/lib/python2.7/dist-packages/puppetboard:python-home=/opt/voxpupuli-puppetboard/puppetboard:/opt/voxpupuli-puppetboard/puppetboard/puppetboard:/usr/local/lib/python2.7/dist-packages/puppetboard/static
|
||||
WSGIScriptAlias / /opt/voxpupuli-puppetboard/puppetboard/wsgi.py
|
||||
ErrorLog /var/log/apache2/puppetboard.error.log
|
||||
CustomLog /var/log/apache2/puppetboard.access.log combined
|
||||
|
||||
<Directory /opt/voxpupuli-puppetboard/puppetboard>
|
||||
<Files wsgi.py>
|
||||
Order deny,allow
|
||||
Allow from all
|
||||
Require all granted
|
||||
</Files>
|
||||
</Directory>
|
||||
|
||||
Alias /static /usr/local/lib/python2.7/dist-packages/puppetboard/static
|
||||
<Directory /usr/local/lib/python2.7/dist-packages/puppetboard/static>
|
||||
Satisfy Any
|
||||
Allow from all
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<Directory /usr/local/lib/python2.7/dist-packages/puppetboard>
|
||||
WSGIProcessGroup puppetboard
|
||||
WSGIApplicationGroup %{GLOBAL}
|
||||
Order deny,allow
|
||||
Allow from all
|
||||
Require all granted
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
```
|
||||
$ a2ensite voxpupuli-puppetboard.conf
|
||||
```
|
||||
|
||||
* /opt/voxpupuli-puppetboard/puppetboard/wsgi.py
|
||||
```
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
|
||||
import sys
|
||||
sys.path.append('/opt/voxpupuli-puppetboard/puppetboard')
|
||||
|
||||
from puppetboard.app import app as application
|
||||
```
|
||||
11
hooks/pre_build
Executable file
11
hooks/pre_build
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
version=$(git describe HEAD --abbrev=4)
|
||||
|
||||
cat << EOF > puppetboard/version.py
|
||||
#
|
||||
# Puppetboard version module
|
||||
#
|
||||
__version__ = '${version}'
|
||||
EOF
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#
|
||||
# Pupppetboard
|
||||
#
|
||||
|
||||
from .version import __version__
|
||||
|
||||
@@ -4,9 +4,9 @@ from __future__ import absolute_import
|
||||
import logging
|
||||
import collections
|
||||
try:
|
||||
from urllib import unquote
|
||||
from urllib import unquote, unquote_plus, quote_plus
|
||||
except ImportError:
|
||||
from urllib.parse import unquote
|
||||
from urllib.parse import unquote, unquote_plus, quote_plus
|
||||
from datetime import datetime, timedelta
|
||||
from itertools import tee
|
||||
|
||||
@@ -15,19 +15,23 @@ from flask import (
|
||||
Response, stream_with_context, redirect,
|
||||
request, session, jsonify
|
||||
)
|
||||
from jinja2.utils import contextfunction
|
||||
|
||||
from pypuppetdb import connect
|
||||
from pypuppetdb.errors import EmptyResponseError
|
||||
from pypuppetdb.QueryBuilder import *
|
||||
|
||||
from puppetboard.forms import (CatalogForm, QueryForm)
|
||||
from puppetboard.utils import (
|
||||
get_or_abort, yield_or_stop, get_db_version,
|
||||
jsonprint, prettyprint
|
||||
)
|
||||
from puppetboard.forms import QueryForm
|
||||
from puppetboard.utils import (get_or_abort, yield_or_stop,
|
||||
get_db_version)
|
||||
from puppetboard.dailychart import get_daily_reports_chart
|
||||
|
||||
import werkzeug.exceptions as ex
|
||||
import CommonMark
|
||||
|
||||
from puppetboard.core import get_app, get_puppetdb, environments
|
||||
import puppetboard.errors
|
||||
|
||||
from . import __version__
|
||||
|
||||
|
||||
REPORTS_COLUMNS = [
|
||||
{'attr': 'end', 'filter': 'end_time',
|
||||
@@ -40,31 +44,26 @@ REPORTS_COLUMNS = [
|
||||
'name': 'Agent version'},
|
||||
]
|
||||
|
||||
app = Flask(__name__)
|
||||
CATALOGS_COLUMNS = [
|
||||
{'attr': 'certname', 'name': 'Certname', 'type': 'node'},
|
||||
{'attr': 'catalog_timestamp', 'name': 'Compile Time'},
|
||||
{'attr': 'form', 'name': 'Compare'},
|
||||
]
|
||||
|
||||
app.config.from_object('puppetboard.default_settings')
|
||||
app = get_app()
|
||||
graph_facts = app.config['GRAPH_FACTS']
|
||||
app.config.from_envvar('PUPPETBOARD_SETTINGS', silent=True)
|
||||
graph_facts += app.config['GRAPH_FACTS']
|
||||
app.secret_key = app.config['SECRET_KEY']
|
||||
|
||||
app.jinja_env.filters['jsonprint'] = jsonprint
|
||||
app.jinja_env.filters['prettyprint'] = prettyprint
|
||||
|
||||
puppetdb = connect(
|
||||
host=app.config['PUPPETDB_HOST'],
|
||||
port=app.config['PUPPETDB_PORT'],
|
||||
ssl_verify=app.config['PUPPETDB_SSL_VERIFY'],
|
||||
ssl_key=app.config['PUPPETDB_KEY'],
|
||||
ssl_cert=app.config['PUPPETDB_CERT'],
|
||||
timeout=app.config['PUPPETDB_TIMEOUT'],)
|
||||
|
||||
numeric_level = getattr(logging, app.config['LOGLEVEL'].upper(), None)
|
||||
if not isinstance(numeric_level, int):
|
||||
raise ValueError('Invalid log level: %s' % app.config['LOGLEVEL'])
|
||||
|
||||
logging.basicConfig(level=numeric_level)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
puppetdb = get_puppetdb()
|
||||
|
||||
|
||||
@app.template_global()
|
||||
def version():
|
||||
return __version__
|
||||
|
||||
|
||||
def stream_template(template_name, **context):
|
||||
app.update_template_context(context)
|
||||
@@ -74,29 +73,10 @@ def stream_template(template_name, **context):
|
||||
return rv
|
||||
|
||||
|
||||
def url_for_field(field, value):
|
||||
args = request.view_args.copy()
|
||||
args.update(request.args.copy())
|
||||
args[field] = value
|
||||
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_field'] = url_for_field
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def utility_processor():
|
||||
@@ -106,58 +86,6 @@ def utility_processor():
|
||||
return dict(now=now)
|
||||
|
||||
|
||||
#
|
||||
# 204 doesn't have a mapping in werkzeug, we need to define a custom
|
||||
# class and then set it to the mappings.
|
||||
#
|
||||
class NoContent(ex.HTTPException):
|
||||
code = 204
|
||||
description = '<p>No content</p'
|
||||
|
||||
abort.mapping[204] = NoContent
|
||||
|
||||
try:
|
||||
@app.errorhandler(204)
|
||||
def no_content(e):
|
||||
return '', 204
|
||||
except KeyError:
|
||||
@app.errorhandler(EmptyResponseError)
|
||||
def no_content(e):
|
||||
return '', 204
|
||||
|
||||
|
||||
@app.errorhandler(400)
|
||||
def bad_request(e):
|
||||
envs = environments()
|
||||
return render_template('400.html', envs=envs), 400
|
||||
|
||||
|
||||
@app.errorhandler(403)
|
||||
def forbidden(e):
|
||||
envs = environments()
|
||||
return render_template('403.html', envs=envs), 403
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
envs = environments()
|
||||
return render_template('404.html', envs=envs), 404
|
||||
|
||||
|
||||
@app.errorhandler(412)
|
||||
def precond_failed(e):
|
||||
"""We're slightly abusing 412 to handle missing features
|
||||
depending on the API version."""
|
||||
envs = environments()
|
||||
return render_template('412.html', envs=envs), 412
|
||||
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(e):
|
||||
envs = environments()
|
||||
return render_template('500.html', envs=envs), 500
|
||||
|
||||
|
||||
@app.route('/', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/')
|
||||
def index(env):
|
||||
@@ -330,29 +258,11 @@ def nodes(env):
|
||||
current_env=env)))
|
||||
|
||||
|
||||
@app.route('/inventory', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/inventory')
|
||||
def inventory(env):
|
||||
"""Fetch all (active) nodes from PuppetDB and stream a table displaying
|
||||
those nodes along with a set of facts about them.
|
||||
|
||||
Downside of the streaming aproach is that since we've already sent our
|
||||
headers we can't abort the request if we detect an error. Because of this
|
||||
we'll end up with an empty table instead because of how yield_or_stop
|
||||
works. Once pagination is in place we can change this but we'll need to
|
||||
provide a search feature instead.
|
||||
|
||||
:param env: Search for facts in this environment
|
||||
:type env: :obj:`string`
|
||||
"""
|
||||
envs = environments()
|
||||
check_env(env, envs)
|
||||
|
||||
headers = [] # a list of fact descriptions to go
|
||||
# in the table header
|
||||
fact_names = [] # a list of inventory fact names
|
||||
fact_data = {} # a multidimensional dict for node and
|
||||
# fact data
|
||||
def inventory_facts():
|
||||
# a list of facts descriptions to go in table header
|
||||
headers = []
|
||||
# a list of inventory fact names
|
||||
fact_names = []
|
||||
|
||||
# load the list of items/facts we want in our inventory
|
||||
try:
|
||||
@@ -370,38 +280,70 @@ def inventory(env):
|
||||
headers.append(desc)
|
||||
fact_names.append(name)
|
||||
|
||||
return headers, fact_names
|
||||
|
||||
|
||||
@app.route('/inventory', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/inventory')
|
||||
def inventory(env):
|
||||
"""Fetch all (active) nodes from PuppetDB and stream a table displaying
|
||||
those nodes along with a set of facts about them.
|
||||
|
||||
:param env: Search for facts in this environment
|
||||
:type env: :obj:`string`
|
||||
"""
|
||||
envs = environments()
|
||||
check_env(env, envs)
|
||||
headers, fact_names = inventory_facts()
|
||||
|
||||
return render_template(
|
||||
'inventory.html',
|
||||
envs=envs,
|
||||
current_env=env,
|
||||
fact_headers=headers)
|
||||
|
||||
|
||||
@app.route('/inventory/json',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/inventory/json')
|
||||
def inventory_ajax(env):
|
||||
"""Backend endpoint for inventory table"""
|
||||
draw = int(request.args.get('draw', 0))
|
||||
|
||||
envs = environments()
|
||||
check_env(env, envs)
|
||||
headers, fact_names = inventory_facts()
|
||||
|
||||
query = AndOperator()
|
||||
fact_query = OrOperator()
|
||||
fact_query.add([EqualsOperator("name", name) for name in fact_names])
|
||||
query.add(fact_query)
|
||||
|
||||
if env != '*':
|
||||
query.add(EqualsOperator("environment", env))
|
||||
|
||||
query.add(fact_query)
|
||||
|
||||
# get all the facts from PuppetDB
|
||||
facts = puppetdb.facts(query=query)
|
||||
|
||||
fact_data = {}
|
||||
for fact in facts:
|
||||
if fact.node not in fact_data:
|
||||
fact_data[fact.node] = {}
|
||||
|
||||
fact_data[fact.node][fact.name] = fact.value
|
||||
|
||||
return Response(stream_with_context(
|
||||
stream_template(
|
||||
'inventory.html',
|
||||
headers=headers,
|
||||
fact_names=fact_names,
|
||||
total = len(fact_data)
|
||||
|
||||
return render_template(
|
||||
'inventory.json.tpl',
|
||||
draw=draw,
|
||||
total=total,
|
||||
total_filtered=total,
|
||||
fact_data=fact_data,
|
||||
envs=envs,
|
||||
current_env=env
|
||||
)))
|
||||
columns=fact_names)
|
||||
|
||||
|
||||
@app.route('/node/<node_name>/',
|
||||
@app.route('/node/<node_name>',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/node/<node_name>/')
|
||||
@app.route('/<env>/node/<node_name>')
|
||||
def node(env, node_name):
|
||||
"""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
|
||||
@@ -420,21 +362,20 @@ def node(env, node_name):
|
||||
query.add(EqualsOperator("certname", node_name))
|
||||
|
||||
node = get_or_abort(puppetdb.node, node_name)
|
||||
facts = node.facts()
|
||||
|
||||
return render_template(
|
||||
'node.html',
|
||||
node=node,
|
||||
facts=yield_or_stop(facts),
|
||||
envs=envs,
|
||||
current_env=env,
|
||||
columns=REPORTS_COLUMNS[:2])
|
||||
|
||||
|
||||
@app.route('/reports/',
|
||||
@app.route('/reports',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT'],
|
||||
'node_name': None})
|
||||
@app.route('/<env>/reports/', defaults={'node_name': None})
|
||||
@app.route('/reports/<node_name>/',
|
||||
@app.route('/<env>/reports', defaults={'node_name': None})
|
||||
@app.route('/reports/<node_name>',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/reports/<node_name>')
|
||||
def reports(env, node_name):
|
||||
@@ -474,7 +415,7 @@ def reports_ajax(env, node_name):
|
||||
order_column = int(request.args.get('order[0][column]', 0))
|
||||
order_filter = REPORTS_COLUMNS[order_column].get(
|
||||
'filter', REPORTS_COLUMNS[order_column]['attr'])
|
||||
order_dir = request.args.get('order[0][dir]')
|
||||
order_dir = request.args.get('order[0][dir]', 'desc')
|
||||
order_args = '[{"field": "%s", "order": "%s"}]' % (order_filter, order_dir)
|
||||
status_args = request.args.get('columns[1][search][value]', '').split('|')
|
||||
max_col = len(REPORTS_COLUMNS)
|
||||
@@ -538,26 +479,17 @@ def reports_ajax(env, node_name):
|
||||
reports_events = []
|
||||
total = 0
|
||||
|
||||
report_event_counts = {}
|
||||
# Create a map from the metrics data to what the templates
|
||||
# use to express the data.
|
||||
report_map = {
|
||||
'success': 'successes',
|
||||
'failure': 'failures',
|
||||
'skipped': 'skips',
|
||||
'noops': 'noop'
|
||||
}
|
||||
# Convert metrics to relational dict
|
||||
metrics = {}
|
||||
for report in reports_events:
|
||||
if total is None:
|
||||
total = puppetdb.total
|
||||
|
||||
report_counts = {'successes': 0, 'failures': 0, 'skips': 0}
|
||||
for metrics in report.metrics:
|
||||
if 'name' in metrics and metrics['name'] in report_map:
|
||||
key_name = report_map[metrics['name']]
|
||||
report_counts[key_name] = metrics['value']
|
||||
|
||||
report_event_counts[report.hash_] = report_counts
|
||||
metrics[report.hash_] = {}
|
||||
for m in report.metrics:
|
||||
if m['category'] not in metrics[report.hash_]:
|
||||
metrics[report.hash_][m['category']] = {}
|
||||
metrics[report.hash_][m['category']][m['name']] = m['value']
|
||||
|
||||
if total is None:
|
||||
total = 0
|
||||
@@ -568,7 +500,7 @@ def reports_ajax(env, node_name):
|
||||
total=total,
|
||||
total_filtered=total,
|
||||
reports=reports,
|
||||
report_event_counts=report_event_counts,
|
||||
metrics=metrics,
|
||||
envs=envs,
|
||||
current_env=env,
|
||||
columns=REPORTS_COLUMNS[:max_col])
|
||||
@@ -614,6 +546,8 @@ def report(env, node_name, report_id):
|
||||
except StopIteration:
|
||||
abort(404)
|
||||
|
||||
report.version = CommonMark.commonmark(report.version)
|
||||
|
||||
return render_template(
|
||||
'report.html',
|
||||
report=report,
|
||||
@@ -638,108 +572,178 @@ def facts(env):
|
||||
check_env(env, envs)
|
||||
facts = []
|
||||
order_by = '[{"field": "name", "order": "asc"}]'
|
||||
|
||||
if env == '*':
|
||||
facts = get_or_abort(puppetdb.fact_names)
|
||||
else:
|
||||
query = ExtractOperator()
|
||||
query.add_field(str('name'))
|
||||
query.add_query(EqualsOperator("environment", env))
|
||||
query.add_group_by(str("name"))
|
||||
|
||||
for names in get_or_abort(puppetdb._query,
|
||||
'facts',
|
||||
query=query,
|
||||
order_by=order_by):
|
||||
facts.append(names['name'])
|
||||
|
||||
facts_dict = collections.defaultdict(list)
|
||||
facts_columns = [[]]
|
||||
letter = None
|
||||
letter_list = None
|
||||
break_size = (len(facts) / 4) + 1
|
||||
next_break = break_size
|
||||
count = 0
|
||||
for fact in facts:
|
||||
letter = fact[0].upper()
|
||||
letter_list = facts_dict[letter]
|
||||
letter_list.append(fact)
|
||||
facts_dict[letter] = letter_list
|
||||
count += 1
|
||||
|
||||
if letter != fact[0].upper() or not letter:
|
||||
if count > next_break:
|
||||
# Create a new column
|
||||
facts_columns.append([])
|
||||
next_break += break_size
|
||||
if letter_list:
|
||||
facts_columns[-1].append(letter_list)
|
||||
# Reset
|
||||
letter = fact[0].upper()
|
||||
letter_list = []
|
||||
|
||||
letter_list.append(fact)
|
||||
facts_columns[-1].append(letter_list)
|
||||
|
||||
sorted_facts_dict = sorted(facts_dict.items())
|
||||
return render_template('facts.html',
|
||||
facts_dict=sorted_facts_dict,
|
||||
facts_len=(sum(map(len, facts_dict.values())) +
|
||||
len(facts_dict) * 5),
|
||||
facts_columns=facts_columns,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
|
||||
|
||||
@app.route('/fact/<fact>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/fact/<fact>')
|
||||
def fact(env, fact):
|
||||
"""Fetches the specific fact from PuppetDB and displays its value per
|
||||
@app.route('/fact/<fact>',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'value': None})
|
||||
@app.route('/<env>/fact/<fact>', defaults={'value': None})
|
||||
@app.route('/fact/<fact>/<value>',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/fact/<fact>/<value>')
|
||||
def fact(env, fact, value):
|
||||
"""Fetches the specific fact(/value) from PuppetDB and displays per
|
||||
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
|
||||
# om nom nom
|
||||
render_graph = False
|
||||
if fact in graph_facts:
|
||||
render_graph = True
|
||||
|
||||
if env == '*':
|
||||
query = None
|
||||
else:
|
||||
query = EqualsOperator("environment", env)
|
||||
|
||||
localfacts = [f for f in yield_or_stop(puppetdb.facts(
|
||||
name=fact, query=query))]
|
||||
return Response(stream_with_context(stream_template(
|
||||
'fact.html',
|
||||
name=fact,
|
||||
render_graph=render_graph,
|
||||
facts=localfacts,
|
||||
envs=envs,
|
||||
current_env=env)))
|
||||
|
||||
|
||||
@app.route('/fact/<fact>/<value>',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/fact/<fact>/<value>')
|
||||
def fact_value(env, 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
|
||||
:param value: Find all facts with this value
|
||||
:type value: :obj:`string`
|
||||
"""
|
||||
envs = environments()
|
||||
check_env(env, envs)
|
||||
|
||||
if env == '*':
|
||||
query = None
|
||||
else:
|
||||
query = EqualsOperator("environment", env)
|
||||
render_graph = False
|
||||
if fact in graph_facts and not value:
|
||||
render_graph = True
|
||||
|
||||
value_safe = value
|
||||
if value is not None:
|
||||
value_safe = unquote_plus(value)
|
||||
|
||||
facts = get_or_abort(puppetdb.facts,
|
||||
name=fact,
|
||||
value=value,
|
||||
query=query)
|
||||
localfacts = [f for f in yield_or_stop(facts)]
|
||||
return render_template(
|
||||
'fact.html',
|
||||
name=fact,
|
||||
fact=fact,
|
||||
value=value,
|
||||
facts=localfacts,
|
||||
value_safe=value_safe,
|
||||
render_graph=render_graph,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
|
||||
|
||||
@app.route('/fact/<fact>/json',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT'],
|
||||
'node': None, 'value': None})
|
||||
@app.route('/<env>/fact/<fact>/json', defaults={'node': None, 'value': None})
|
||||
@app.route('/fact/<fact>/<value>/json',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'node': None})
|
||||
@app.route('/fact/<fact>/<path:value>/json',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'node': None})
|
||||
@app.route('/<env>/fact/<fact>/<value>/json', defaults={'node': None})
|
||||
@app.route('/node/<node>/facts/json',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT'],
|
||||
'fact': None, 'value': None})
|
||||
@app.route('/<env>/node/<node>/facts/json',
|
||||
defaults={'fact': None, 'value': None})
|
||||
def fact_ajax(env, node, fact, value):
|
||||
"""Fetches the specific facts matching (node/fact/value) from PuppetDB and
|
||||
return a JSON table
|
||||
|
||||
:param env: Searches for facts in this environment
|
||||
:type env: :obj:`string`
|
||||
:param node: Find all facts for this node
|
||||
:type node: :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`
|
||||
"""
|
||||
draw = int(request.args.get('draw', 0))
|
||||
|
||||
envs = environments()
|
||||
check_env(env, envs)
|
||||
|
||||
render_graph = False
|
||||
if fact in graph_facts and not value and not node:
|
||||
render_graph = True
|
||||
|
||||
query = AndOperator()
|
||||
if node:
|
||||
query.add(EqualsOperator("certname", node))
|
||||
|
||||
if env != '*':
|
||||
query.add(EqualsOperator("environment", env))
|
||||
|
||||
if len(query.operations) == 0:
|
||||
query = None
|
||||
|
||||
# Generator needs to be converted (graph / total)
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
if value is not None:
|
||||
query.add(EqualsOperator('value', unquote_plus(value)))
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
facts = [f for f in get_or_abort(
|
||||
puppetdb.facts,
|
||||
name=fact,
|
||||
query=query)]
|
||||
|
||||
total = len(facts)
|
||||
|
||||
counts = {}
|
||||
json = {
|
||||
'draw': draw,
|
||||
'recordsTotal': total,
|
||||
'recordsFiltered': total,
|
||||
'data': []}
|
||||
|
||||
for fact_h in facts:
|
||||
line = []
|
||||
if not fact:
|
||||
line.append(fact_h.name)
|
||||
if not node:
|
||||
line.append('<a href="{0}">{1}</a>'.format(
|
||||
url_for('node', env=env, node_name=fact_h.node),
|
||||
fact_h.node))
|
||||
if not value:
|
||||
fact_value = fact_h.value
|
||||
if isinstance(fact_value, unicode) or isinstance(fact_value, str):
|
||||
fact_value = quote_plus(fact_h.value)
|
||||
|
||||
line.append('<a href="{0}">{1}</a>'.format(
|
||||
url_for(
|
||||
'fact', env=env, fact=fact_h.name, value=fact_value),
|
||||
fact_h.value))
|
||||
|
||||
json['data'].append(line)
|
||||
|
||||
if render_graph:
|
||||
if fact_h.value not in counts:
|
||||
counts[fact_h.value] = 0
|
||||
counts[fact_h.value] += 1
|
||||
|
||||
if render_graph:
|
||||
json['chart'] = [
|
||||
{"label": "{0}".format(k).replace('\n', ' '),
|
||||
"value": counts[k]}
|
||||
for k in sorted(counts, key=lambda k: counts[k], reverse=True)]
|
||||
|
||||
return jsonify(json)
|
||||
|
||||
|
||||
@app.route('/query', methods=('GET', 'POST'),
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/query', methods=('GET', 'POST'))
|
||||
@@ -828,9 +832,14 @@ def metric(env, metric):
|
||||
current_env=env)
|
||||
|
||||
|
||||
@app.route('/catalogs', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/catalogs')
|
||||
def catalogs(env):
|
||||
@app.route('/catalogs',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT'],
|
||||
'compare': None})
|
||||
@app.route('/<env>/catalogs', defaults={'compare': None})
|
||||
@app.route('/catalogs/compare/<compare>',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/catalogs/compare/<compare>')
|
||||
def catalogs(env, compare):
|
||||
"""Lists all nodes with a compiled catalog.
|
||||
|
||||
:param env: Find the nodes with this catalog_environment value
|
||||
@@ -839,52 +848,79 @@ def catalogs(env):
|
||||
envs = environments()
|
||||
check_env(env, envs)
|
||||
|
||||
if app.config['ENABLE_CATALOG']:
|
||||
nodenames = []
|
||||
catalog_list = []
|
||||
query = AndOperator()
|
||||
|
||||
if env != '*':
|
||||
query.add(EqualsOperator("catalog_environment", env))
|
||||
|
||||
query.add(NullOperator("catalog_timestamp", False))
|
||||
|
||||
order_by_str = '[{"field": "certname", "order": "asc"}]'
|
||||
nodes = get_or_abort(puppetdb.nodes,
|
||||
query=query,
|
||||
with_status=False,
|
||||
order_by=order_by_str)
|
||||
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)
|
||||
if not app.config['ENABLE_CATALOG']:
|
||||
log.warn('Access to catalog interface disabled by administrator')
|
||||
abort(403)
|
||||
|
||||
return render_template(
|
||||
'catalogs.html',
|
||||
nodes=catalog_list,
|
||||
compare=compare,
|
||||
columns=CATALOGS_COLUMNS,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
|
||||
|
||||
@app.route('/catalogs/json',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT'],
|
||||
'compare': None})
|
||||
@app.route('/<env>/catalogs/json', defaults={'compare': None})
|
||||
@app.route('/catalogs/compare/<compare>/json',
|
||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||
@app.route('/<env>/catalogs/compare/<compare>/json')
|
||||
def catalogs_ajax(env, compare):
|
||||
"""Server data to catalogs as JSON to Jquery datatables
|
||||
"""
|
||||
draw = int(request.args.get('draw', 0))
|
||||
start = int(request.args.get('start', 0))
|
||||
length = int(request.args.get('length', app.config['NORMAL_TABLE_COUNT']))
|
||||
paging_args = {'limit': length, 'offset': start}
|
||||
search_arg = request.args.get('search[value]')
|
||||
order_column = int(request.args.get('order[0][column]', 0))
|
||||
order_filter = CATALOGS_COLUMNS[order_column].get(
|
||||
'filter', CATALOGS_COLUMNS[order_column]['attr'])
|
||||
order_dir = request.args.get('order[0][dir]', 'asc')
|
||||
order_args = '[{"field": "%s", "order": "%s"}]' % (order_filter, order_dir)
|
||||
|
||||
envs = environments()
|
||||
check_env(env, envs)
|
||||
|
||||
query = AndOperator()
|
||||
if env != '*':
|
||||
query.add(EqualsOperator("catalog_environment", env))
|
||||
if search_arg:
|
||||
query.add(RegexOperator("certname", r"%s" % search_arg))
|
||||
query.add(NullOperator("catalog_timestamp", False))
|
||||
|
||||
nodes = get_or_abort(puppetdb.nodes,
|
||||
query=query,
|
||||
include_total=True,
|
||||
order_by=order_args,
|
||||
**paging_args)
|
||||
|
||||
catalog_list = []
|
||||
total = None
|
||||
for node in nodes:
|
||||
if total is None:
|
||||
total = puppetdb.total
|
||||
|
||||
catalog_list.append({
|
||||
'certname': node.name,
|
||||
'catalog_timestamp': node.catalog_timestamp,
|
||||
'form': compare,
|
||||
})
|
||||
|
||||
if total is None:
|
||||
total = 0
|
||||
|
||||
return render_template(
|
||||
'catalogs.json.tpl',
|
||||
total=total,
|
||||
total_filtered=total,
|
||||
draw=draw,
|
||||
columns=CATALOGS_COLUMNS,
|
||||
catalogs=catalog_list,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
else:
|
||||
log.warn('Access to catalog interface disabled by administrator')
|
||||
abort(403)
|
||||
|
||||
|
||||
@app.route('/catalog/<node_name>',
|
||||
@@ -911,40 +947,6 @@ def catalog_node(env, node_name):
|
||||
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>')
|
||||
@@ -1090,3 +1092,15 @@ def daily_reports_chart(env):
|
||||
certname=certname,
|
||||
)
|
||||
return jsonify(result=result)
|
||||
|
||||
|
||||
@app.route('/offline/<path:filename>')
|
||||
def offline_static(filename):
|
||||
mimetype = 'text/html'
|
||||
if filename.endswith('.css'):
|
||||
mimetype = 'text/css'
|
||||
elif filename.endswith('.js'):
|
||||
mimetype = 'text/javascript'
|
||||
|
||||
return Response(response=render_template('static/%s' % filename),
|
||||
status=200, mimetype=mimetype)
|
||||
|
||||
64
puppetboard/core.py
Normal file
64
puppetboard/core.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from pypuppetdb import connect
|
||||
from puppetboard.utils import (jsonprint, prettyprint, url_for_field,
|
||||
url_static_offline, get_or_abort)
|
||||
|
||||
from . import __version__
|
||||
|
||||
APP = None
|
||||
PUPPETDB = None
|
||||
|
||||
|
||||
def get_app():
|
||||
global APP
|
||||
|
||||
if APP is None:
|
||||
app = Flask(__name__)
|
||||
app.config.from_object('puppetboard.default_settings')
|
||||
app.config.from_envvar('PUPPETBOARD_SETTINGS', silent=True)
|
||||
app.secret_key = app.config['SECRET_KEY']
|
||||
|
||||
numeric_level = getattr(logging, app.config['LOGLEVEL'].upper(), None)
|
||||
if not isinstance(numeric_level, int):
|
||||
raise ValueError('Invalid log level: %s' % app.config['LOGLEVEL'])
|
||||
|
||||
app.jinja_env.filters['jsonprint'] = jsonprint
|
||||
app.jinja_env.filters['prettyprint'] = prettyprint
|
||||
app.jinja_env.globals['url_for_field'] = url_for_field
|
||||
app.jinja_env.globals['url_static_offline'] = url_static_offline
|
||||
APP = app
|
||||
|
||||
return APP
|
||||
|
||||
|
||||
def get_puppetdb():
|
||||
global PUPPETDB
|
||||
|
||||
if PUPPETDB is None:
|
||||
app = get_app()
|
||||
puppetdb = connect(host=app.config['PUPPETDB_HOST'],
|
||||
port=app.config['PUPPETDB_PORT'],
|
||||
ssl_verify=app.config['PUPPETDB_SSL_VERIFY'],
|
||||
ssl_key=app.config['PUPPETDB_KEY'],
|
||||
ssl_cert=app.config['PUPPETDB_CERT'],
|
||||
timeout=app.config['PUPPETDB_TIMEOUT'],)
|
||||
PUPPETDB = puppetdb
|
||||
|
||||
return PUPPETDB
|
||||
|
||||
|
||||
def environments():
|
||||
puppetdb = get_puppetdb()
|
||||
envs = get_or_abort(puppetdb.environments)
|
||||
x = []
|
||||
|
||||
for env in envs:
|
||||
x.append(env['name'])
|
||||
|
||||
return x
|
||||
@@ -18,6 +18,11 @@ LOGLEVEL = 'info'
|
||||
NORMAL_TABLE_COUNT = 100
|
||||
LITTLE_TABLE_COUNT = 10
|
||||
TABLE_COUNT_SELECTOR = [10, 20, 50, 100, 500]
|
||||
DISPLAYED_METRICS = ['resources.total',
|
||||
'events.failure',
|
||||
'events.success',
|
||||
'resources.skipped',
|
||||
'events.noop']
|
||||
OFFLINE_MODE = False
|
||||
ENABLE_CATALOG = False
|
||||
OVERVIEW_FILTER = None
|
||||
|
||||
@@ -33,6 +33,13 @@ TABLE_COUNT_DEF = "10,20,50,100,500"
|
||||
TABLE_COUNT_SELECTOR = [int(x) for x in os.getenv('TABLE_COUNT_SELECTOR',
|
||||
TABLE_COUNT_DEF).split(',')]
|
||||
|
||||
DISP_METR_DEF = ','.join(['resources.total', 'events.failure',
|
||||
'events.success', 'resources.skipped',
|
||||
'events.noop'])
|
||||
|
||||
DISPLAYED_METRICS = [x.strip() for x in os.getenv('DISPLAYED_METRICS',
|
||||
DISP_METR_DEF).split(',')]
|
||||
|
||||
OFFLINE_MODE = bool(os.getenv('OFFLINE_MODE', 'False').upper() == 'TRUE')
|
||||
ENABLE_CATALOG = bool(os.getenv('ENABLE_CATALOG', 'False').upper() == 'TRUE')
|
||||
OVERVIEW_FILTER = os.getenv('OVERVIEW_FILTER', None)
|
||||
@@ -46,7 +53,6 @@ GRAPH_FACTS_DEFAULT = ','.join(['architecture', 'clientversion', 'domain',
|
||||
GRAPH_FACTS = [x.strip() for x in os.getenv('GRAPH_FACTS',
|
||||
GRAPH_FACTS_DEFAULT).split(',')]
|
||||
|
||||
|
||||
GRAPH_TYPE = os.getenv('GRAPH_TYPE', 'pie')
|
||||
|
||||
# Tuples are hard to express as an environment variable, so here
|
||||
|
||||
45
puppetboard/errors.py
Normal file
45
puppetboard/errors.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
from puppetboard.core import get_app, environments
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
from flask import render_template
|
||||
from . import __version__
|
||||
|
||||
app = get_app()
|
||||
|
||||
|
||||
@app.errorhandler(400)
|
||||
def bad_request(e):
|
||||
envs = environments()
|
||||
return render_template('400.html', envs=envs), 400
|
||||
|
||||
|
||||
@app.errorhandler(403)
|
||||
def forbidden(e):
|
||||
envs = environments()
|
||||
return render_template('403.html', envs=envs), 403
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
envs = environments()
|
||||
return render_template('404.html', envs=envs), 404
|
||||
|
||||
|
||||
@app.errorhandler(412)
|
||||
def precond_failed(e):
|
||||
"""We're slightly abusing 412 to handle missing features
|
||||
depending on the API version."""
|
||||
envs = environments()
|
||||
return render_template('412.html', envs=envs), 412
|
||||
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(e):
|
||||
envs = []
|
||||
try:
|
||||
envs = environments()
|
||||
except InternalServerError as e:
|
||||
pass
|
||||
return render_template('500.html', envs=envs), 500
|
||||
@@ -1,14 +1,14 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
from flask.ext.wtf import Form
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
HiddenField, RadioField, SelectField,
|
||||
TextAreaField, BooleanField, validators
|
||||
)
|
||||
|
||||
|
||||
class QueryForm(Form):
|
||||
class QueryForm(FlaskForm):
|
||||
"""The form used to allow freeform queries to be executed against
|
||||
PuppetDB."""
|
||||
query = TextAreaField('Query', [validators.Required(
|
||||
@@ -28,9 +28,3 @@ class QueryForm(Form):
|
||||
('pql', 'PQL'),
|
||||
])
|
||||
rawjson = BooleanField('Raw JSON')
|
||||
|
||||
|
||||
class CatalogForm(Form):
|
||||
"""The form used to compare the catalogs of different nodes."""
|
||||
compare = HiddenField('compare')
|
||||
against = SelectField('against')
|
||||
|
||||
@@ -44,7 +44,7 @@ h1.ui.header.no-margin-bottom {
|
||||
color: #AA4643;
|
||||
}
|
||||
|
||||
.ui.label.failed {
|
||||
.ui.label.failed, .ui.label.events.failure {
|
||||
background-color: #AA4643;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ h1.ui.header.no-margin-bottom {
|
||||
color: #4572A7;
|
||||
}
|
||||
|
||||
.ui.label.changed {
|
||||
.ui.label.changed, .ui.label.events.success {
|
||||
background-color: #4572A7;
|
||||
}
|
||||
|
||||
@@ -68,10 +68,14 @@ h1.ui.header.no-margin-bottom {
|
||||
color: #DB843D;
|
||||
}
|
||||
|
||||
.ui.label.noop {
|
||||
.ui.label.noop, .ui.label.events.noop {
|
||||
background-color: #DB843D;
|
||||
}
|
||||
|
||||
.ui.label.resources.total {
|
||||
background-color: #989898;
|
||||
}
|
||||
|
||||
.ui.label.unchanged {
|
||||
background-color: #89A54E;
|
||||
}
|
||||
@@ -80,7 +84,7 @@ h1.ui.header.no-margin-bottom {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.ui.label.skipped {
|
||||
.ui.label.skipped, .ui.label.resources.skipped {
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,20 +12,20 @@ body.radiator_controller table.node_summary {padding:20px;position:absolute;heig
|
||||
body.radiator_controller table.node_summary .count_column {min-width:2em;width:2em;}
|
||||
body.radiator_controller table.node_summary tr:last-child td {border-bottom:none;}
|
||||
body.radiator_controller table.node_summary tr:last-child td .label {border-left:0px #000 solid;}
|
||||
body.radiator_controller table.node_summary tr.unreported .percent {background-color:#ee7722;border-radius:0 3px 3px 0;}
|
||||
body.radiator_controller table.node_summary tr.unreported .label,body.radiator_controller table.node_summary tr.unreported .count {color:#ee7722;}
|
||||
body.radiator_controller table.node_summary tr.unreported .percent {background-color:#3D96AE;border-radius:0 3px 3px 0;}
|
||||
body.radiator_controller table.node_summary tr.unreported .label,body.radiator_controller table.node_summary tr.unreported .count {color:#3D96AE;}
|
||||
body.radiator_controller table.node_summary tr.unreported .label {border-left:1px #333 dashed;}
|
||||
body.radiator_controller table.node_summary tr.failed .percent {background-color:#cc2211;border-radius:0 3px 3px 0;}
|
||||
body.radiator_controller table.node_summary tr.failed .label,body.radiator_controller table.node_summary tr.failed .count {color:#cc2211;}
|
||||
body.radiator_controller table.node_summary tr.failed .label {border-left:1px #333 dashed;}
|
||||
body.radiator_controller table.node_summary tr.noop .percent {background-color:#eddc21;border-radius:0 3px 3px 0;}
|
||||
body.radiator_controller table.node_summary tr.noop .label,body.radiator_controller table.node_summary tr.noop .count {color:#eddc21;}
|
||||
body.radiator_controller table.node_summary tr.noop .percent {background-color:#DB843D;border-radius:0 3px 3px 0;}
|
||||
body.radiator_controller table.node_summary tr.noop .label,body.radiator_controller table.node_summary tr.noop .count {color:#DB843D;}
|
||||
body.radiator_controller table.node_summary tr.noop .label {border-left:1px #333 dashed;}
|
||||
body.radiator_controller table.node_summary tr.changed .percent {background-color:#009933;border-radius:0 3px 3px 0;}
|
||||
body.radiator_controller table.node_summary tr.changed .label,body.radiator_controller table.node_summary tr.changed .count {color:#009933;}
|
||||
body.radiator_controller table.node_summary tr.changed .percent {background-color:#4572A7;border-radius:0 3px 3px 0;}
|
||||
body.radiator_controller table.node_summary tr.changed .label,body.radiator_controller table.node_summary tr.changed .count {color:#4572A7;}
|
||||
body.radiator_controller table.node_summary tr.changed .label {border-left:1px #333 dashed;}
|
||||
body.radiator_controller table.node_summary tr.unchanged .percent {background-color:#2198ed;border-radius:0 3px 3px 0;}
|
||||
body.radiator_controller table.node_summary tr.unchanged .label,body.radiator_controller table.node_summary tr.unchanged .count {color:#2198ed;}
|
||||
body.radiator_controller table.node_summary tr.unchanged .percent {background-color:#89A54E;border-radius:0 3px 3px 0;}
|
||||
body.radiator_controller table.node_summary tr.unchanged .label,body.radiator_controller table.node_summary tr.unchanged .count {color:#89A54E;}
|
||||
body.radiator_controller table.node_summary tr.unchanged .label {border-left:1px #333 dashed;}
|
||||
body.radiator_controller table.node_summary tr.total {color:#fff;background-color:#181818;}
|
||||
body.radiator_controller table.node_summary tr.total .percent {background-color:white;border-radius:0 3px 3px 0;}
|
||||
|
||||
BIN
puppetboard/static/fonts/lato-italic-400.ttf
Normal file
BIN
puppetboard/static/fonts/lato-italic-400.ttf
Normal file
Binary file not shown.
BIN
puppetboard/static/fonts/lato-italic-700.ttf
Normal file
BIN
puppetboard/static/fonts/lato-italic-700.ttf
Normal file
Binary file not shown.
BIN
puppetboard/static/fonts/lato-normal-400.ttf
Normal file
BIN
puppetboard/static/fonts/lato-normal-400.ttf
Normal file
Binary file not shown.
BIN
puppetboard/static/fonts/lato-normal-700.ttf
Normal file
BIN
puppetboard/static/fonts/lato-normal-700.ttf
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
jQuery(function ($) {
|
||||
function generateChart(el) {
|
||||
var url = "/daily_reports_chart.json";
|
||||
var url = "daily_reports_chart.json";
|
||||
var certname = $(el).attr('data-certname');
|
||||
if (typeof certname !== typeof undefined && certname !== false) {
|
||||
url = url + "?certname=" + certname;
|
||||
|
||||
@@ -1,51 +1,3 @@
|
||||
{% 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">
|
||||
<input {% if autofocus %} autofocus="autofocus" {% endif %} class="filter-table" placeholder="Type here to filter...">
|
||||
</div>
|
||||
<table class="ui very basic {% if condensed %}very{% endif%} compact sortable table" style="table-layout: fixed;">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if show_node %}
|
||||
<th>Node</th>
|
||||
{% else %}
|
||||
<th class="default-sort">Fact</th>
|
||||
{% endif %}
|
||||
{% if show_value %}
|
||||
<th>Value</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for fact in facts %}
|
||||
<tr>
|
||||
{% if show_node %}
|
||||
<td><a href="{{url_for('node', env=current_env, node_name=fact.node)}}">{{fact.node}}</a></td>
|
||||
{% else %}
|
||||
<td><a href="{{url_for('fact', env=current_env, fact=fact.name)}}">{{fact.name}}</a></td>
|
||||
{% endif %}
|
||||
{% if show_value %}
|
||||
<td style="word-wrap:break-word">
|
||||
{% if link_facts %}
|
||||
{% 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 %}
|
||||
{% if fact.value is mapping %}
|
||||
<pre>{{fact.value|jsonprint}}</pre>
|
||||
{% else %}
|
||||
{{fact.value}}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro status_counts(caller, status, node_name, events, current_env, unreported_time=False, report_hash=False) -%}
|
||||
<a class="ui {{status}} label status" href="{{url_for('report', env=current_env, node_name=node_name, report_id=report_hash)}}">{{ status|upper }}</a>
|
||||
{% if status == 'unreported' %}
|
||||
@@ -57,9 +9,33 @@
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro report_status(caller, status, node_name, metrics, current_env, unreported_time=False, report_hash=False) -%}
|
||||
<a class="ui {{status}} label status" href="{{url_for('report', env=current_env, node_name=node_name, report_id=report_hash)}}">{{ status|upper }}</a>
|
||||
{% if status == 'unreported' %}
|
||||
<span class="ui label status"> {{ unreported_time|upper }} </span>
|
||||
{% else %}
|
||||
{% for metric in config.DISPLAYED_METRICS %}
|
||||
{% set path = metric.split('.') %}
|
||||
{% set title = ' '.join(path) %}
|
||||
{% if metrics[path[0]] and metrics[path[0]][path[1]] %}
|
||||
{% set value = metrics[path[0]][path[1]] %}
|
||||
{% if value != 0 and value|int != value %}
|
||||
{% set format_str = '%.2f' %}
|
||||
{% else %}
|
||||
{% set format_str = '%s' %}
|
||||
{% endif %}
|
||||
<span title="{{ title }}" class="ui small count label {{ title }}">{{ format_str|format(value) }}</span>
|
||||
{% else %}
|
||||
<span title="{{ title }}" class="ui small count label">0</span>
|
||||
{% endif%}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro datatable_init(table_html_id, ajax_url, default_length, length_selector, extra_options=None) -%}
|
||||
// Init datatable
|
||||
$.fn.dataTable.ext.errMode = 'throw';
|
||||
|
||||
var table = $('#{{ table_html_id }}').DataTable({
|
||||
// Permit flow auto-readjust (responsive)
|
||||
"autoWidth": false,
|
||||
@@ -76,12 +52,19 @@
|
||||
// Paging options
|
||||
"lengthMenu": {{ length_selector }},
|
||||
"pageLength": {{ default_length }},
|
||||
// Search as regex (does not apply if serverSide)
|
||||
"search": {"regex": true},
|
||||
// Default sort
|
||||
"order": [[ 0, "desc" ]],
|
||||
// Custom options
|
||||
{% if extra_options %}{% call extra_options() %}Callback to parent defined options{% endcall %}{% endif %}
|
||||
});
|
||||
|
||||
table.on('error', function ( e, settings, json ) {
|
||||
table.clear().draw();
|
||||
$('#facts_table_processing').hide(); })
|
||||
|
||||
|
||||
table.on('draw.dt', function(){
|
||||
$('#{{ table_html_id }} [rel=utctimestamp]').each(
|
||||
function(index, timestamp){
|
||||
|
||||
@@ -1,40 +1,21 @@
|
||||
{% 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 very basic very compact table nodes'>
|
||||
<table id="catalogs_table" class='ui very basic table stackable'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Certname</th>
|
||||
<th>Compile Time</th>
|
||||
<th>Compare With</th>
|
||||
{% for column in columns %}
|
||||
<th>{{ column.name }}</th>
|
||||
{% endfor %}
|
||||
</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 action input">
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock content %}
|
||||
{% block onload_script %}
|
||||
{% macro extra_options(caller) %}
|
||||
"order": [[ 0, "asc" ]],
|
||||
{% endmacro %}
|
||||
{{ macros.datatable_init(table_html_id="catalogs_table", ajax_url=url_for('catalogs_ajax', env=current_env, compare=compare), default_length=config.NORMAL_TABLE_COUNT, length_selector=config.TABLE_COUNT_SELECTOR, extra_options=extra_options) }}
|
||||
{% endblock onload_script %}
|
||||
|
||||
40
puppetboard/templates/catalogs.json.tpl
Normal file
40
puppetboard/templates/catalogs.json.tpl
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"draw": {{draw}},
|
||||
"recordsTotal": {{total}},
|
||||
"recordsFiltered": {{total_filtered}},
|
||||
"data": [
|
||||
{% for catalog in catalogs -%}
|
||||
{%- if not loop.first %},{%- endif -%}
|
||||
[
|
||||
{%- for column in columns -%}
|
||||
{%- if not loop.first %},{%- endif -%}
|
||||
{%- if column.attr == 'catalog_timestamp' -%}
|
||||
"<a rel=\"utctimestamp\" href=\"{{url_for('catalog_node', env=current_env, node_name=catalog.certname)}}\">{{ catalog.catalog_timestamp }}</a>"
|
||||
{%- elif column.type == 'node' -%}
|
||||
{% filter jsonprint %}<a href="{{url_for('node', env=current_env, node_name=catalog.certname)}}">{{ catalog.certname }}</a>{% endfilter %}
|
||||
{%- elif column.attr == 'form' -%}
|
||||
{% filter jsonprint -%}
|
||||
<div class="ui action input">
|
||||
{%- if catalog.form -%}
|
||||
<form method="GET" action="{{url_for('catalog_compare', env=current_env, compare=catalog.form, against=catalog.certname)}}">
|
||||
{%- else -%}
|
||||
<form method="GET" action="{{url_for('catalogs', env=current_env, compare=catalog.certname)}}">
|
||||
{%- endif -%}
|
||||
<div class="field inline">
|
||||
{%- if catalog.form -%}
|
||||
<input type="submit" class="ui submit button" style="height:auto;" value="Compare with {{ catalog.form }}"/>
|
||||
{%- else -%}
|
||||
<input type="submit" class="ui submit button" style="height:auto;" value="Compare with ..."/>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{%- endfilter -%}
|
||||
{%- else -%}
|
||||
""
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
]
|
||||
{% endfor %}
|
||||
]
|
||||
}
|
||||
@@ -1,50 +1,45 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% import '_macros.html' as macros %}
|
||||
|
||||
{% block javascript %}
|
||||
{% if render_graph %}
|
||||
var chart = null;
|
||||
var data = [
|
||||
{% for fact in facts|groupby('value') %}
|
||||
{
|
||||
label: '{{ fact.grouper.replace("\n", " ") }}',
|
||||
value: {{ fact.list|length }}
|
||||
},
|
||||
{% endfor %}
|
||||
{
|
||||
value: 0,
|
||||
}
|
||||
]
|
||||
var fact_values = data.map(function(item) { return [item.label, item.value]; }).filter(function(item){return item[0];}).sort(function(a,b){return b[1] - a[1];});
|
||||
var realdata = fact_values.slice(0, 15);
|
||||
var otherdata = fact_values.slice(15);
|
||||
if (otherdata.length > 0) {
|
||||
realdata.push(["other", otherdata.reduce(function(a,b){return a + b[1];},0)]);
|
||||
}
|
||||
{% endif %}
|
||||
{% endblock javascript %}
|
||||
|
||||
{% block onload_script %}
|
||||
$('table').tablesort();
|
||||
{% if render_graph %}
|
||||
chart = c3.generate({
|
||||
{% macro extra_options(caller) %}
|
||||
// No per page AJAX
|
||||
'serverSide': false,
|
||||
{% endmacro %}
|
||||
{{ macros.datatable_init(table_html_id="facts_table", ajax_url=url_for('fact_ajax', env=current_env, fact=fact, value=value), default_length=config.NORMAL_TABLE_COUNT, length_selector=config.TABLE_COUNT_SELECTOR, extra_options=extra_options) }}
|
||||
|
||||
{% if render_graph %}
|
||||
table.on('xhr', function(e, settings, json){
|
||||
var fact_values = json['chart'].map(function(item) { return [item.label, item.value]; }).filter(function(item){return item[0];}).sort(function(a,b){return b[1] - a[1];});
|
||||
var realdata = fact_values.slice(0, 15);
|
||||
var otherdata = fact_values.slice(15);
|
||||
if (otherdata.length > 0) {
|
||||
realdata.push(["other", otherdata.reduce(function(a,b){return a + b[1];},0)]);
|
||||
}
|
||||
c3.generate({
|
||||
bindto: '#factChart',
|
||||
data: {
|
||||
columns: realdata,
|
||||
type : '{{config.GRAPH_TYPE|default('pie')}}',
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
})
|
||||
{% endif %}
|
||||
{% endblock onload_script %}
|
||||
|
||||
{% block content %}
|
||||
{% if render_graph %}
|
||||
<div id="factChart" width="300" height="300"></div>
|
||||
<h1>{{name}}{% if value %}/{{value}}{% endif %} ({{facts|length}})</h1>
|
||||
|
||||
|
||||
{% if value %}
|
||||
{{macros.facts_table(facts, current_env=current_env, autofocus=True, show_node=True, show_value=False, margin_bottom=10)}}
|
||||
{% else %}
|
||||
{{macros.facts_table(facts, current_env=current_env, autofocus=True, show_node=True, link_facts=True, margin_bottom=10)}}
|
||||
{% endif %}
|
||||
<h1>{{ fact }}{% if value_safe %} : {{ value_safe }}{% endif %}</h1>
|
||||
<table id="facts_table" class='ui fixed very basic compact table stackable'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Node</th>
|
||||
{% if not value %}<th>Value</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -4,26 +4,19 @@
|
||||
<input autofocus="autofocus" class="filter-list" placeholder="Type here to filter...">
|
||||
</div>
|
||||
<div class="ui searchable stackable doubling four column grid factlist">
|
||||
{%- for column in facts_columns %}
|
||||
<div class="column">
|
||||
{%- set facts_count = 0 -%}
|
||||
{%- set break = facts_len//4 + 1 -%}
|
||||
{%- for key,facts_list in facts_dict %}
|
||||
{%- for letter in column %}
|
||||
<div class="ui list_hide_segment segment">
|
||||
<a class="ui darkblue ribbon label">{{key}}</a>
|
||||
<a class="ui darkblue ribbon label">{{ letter[0][0]|upper }}</a>
|
||||
<ul>
|
||||
{%- for fact in facts_list %}
|
||||
<li><a href="{{url_for('fact', env=current_env, fact=fact)}}">{{fact}}</a></li>
|
||||
{%- for fact in letter %}
|
||||
<li><a href="{{url_for('fact', env=current_env, fact=fact)}}">{{ fact }}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{%- set facts_count = facts_count + facts_list|length -%}
|
||||
{%- if facts_count >= break -%}
|
||||
</div>
|
||||
<div class="column">
|
||||
{%- set break = facts_len//4 + 1 + break -%}
|
||||
{%- endif -%}
|
||||
{%- set facts_count = facts_count + 5 -%}
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</div>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<tr>
|
||||
<th class="five wide">Status</th>
|
||||
<th class="five wide">Certname</th>
|
||||
<th class="five wide date default-sort">Report</th>
|
||||
<th class="five wide default-sort">Report</th>
|
||||
<th class="one wide"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
{% 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 very basic sortable table'>
|
||||
<table id="inventory_table" class='ui fixed compact very basic sortable table'>
|
||||
<thead>
|
||||
<tr>
|
||||
{% for head in headers %}
|
||||
<th{% if loop.index == 1 %} class="default-sort"{% endif %}>{{head}}</th>
|
||||
{% for head in fact_headers %}
|
||||
<th>{{head}}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for node, facts in fact_data.iteritems() %}
|
||||
<tr>
|
||||
{% for name in fact_names %}
|
||||
<td><a href="{{url_for('node', env=current_env, node_name=node)}}">{{facts.get(name, 'undef')}}</a></td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock content %}
|
||||
{% block onload_script %}
|
||||
{% macro extra_options(caller) %}
|
||||
'serverSide': false,
|
||||
{% endmacro %}
|
||||
{{ macros.datatable_init(table_html_id="inventory_table", ajax_url=url_for('inventory_ajax', env=current_env), default_length=config.NORMAL_TABLE_COUNT, length_selector=config.TABLE_COUNT_SELECTOR, extra_options=extra_options) }}
|
||||
{% endblock onload_script %}
|
||||
|
||||
23
puppetboard/templates/inventory.json.tpl
Normal file
23
puppetboard/templates/inventory.json.tpl
Normal file
@@ -0,0 +1,23 @@
|
||||
{%- import '_macros.html' as macros -%}
|
||||
{
|
||||
"draw": {{draw}},
|
||||
"recordsTotal": {{total}},
|
||||
"recordsFiltered": {{total_filtered}},
|
||||
"data": [
|
||||
{% for node in fact_data -%}
|
||||
{%- if not loop.first %},{%- endif -%}
|
||||
[
|
||||
{%- for column in columns -%}
|
||||
{%- if not loop.first %},{%- endif -%}
|
||||
{%- if column in ['fqdn', 'hostname'] -%}
|
||||
{% filter jsonprint %}<a href="{{ url_for('node', env=current_env, node_name=node) }}">{{ node }}</a>{% endfilter %}
|
||||
{%- elif fact_data[node][column] -%}
|
||||
{{ fact_data[node][column] | jsonprint }}
|
||||
{%- else -%}
|
||||
""
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
]
|
||||
{% endfor -%}
|
||||
]
|
||||
}
|
||||
@@ -13,11 +13,12 @@
|
||||
}
|
||||
</style>
|
||||
<link href='{{ url_for('static', filename='jquery-datatables-1.10.13/dataTables.semanticui.min.css') }}' rel='stylesheet' type='text/css'>
|
||||
<link href="{{ url_for('offline_static', filename='Semantic-UI-2.1.8/semantic.min.css') }}" rel="stylesheet" />
|
||||
{% else %}
|
||||
<link href='//fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css' />
|
||||
<link href="{{ url_for('static', filename='Semantic-UI-2.1.8/semantic.min.css') }}" rel="stylesheet" />
|
||||
<link href='//cdnjs.cloudflare.com/ajax/libs/datatables/1.10.13/css/dataTables.semanticui.min.css' rel='stylesheet' type='text/css'>
|
||||
{% endif %}
|
||||
<link href="{{ url_for('static', filename='Semantic-UI-2.1.8/semantic.min.css') }}" rel="stylesheet" />
|
||||
<link href="{{ url_for('static', filename='css/puppetboard.css') }}" rel="stylesheet" />
|
||||
|
||||
{% if config.OFFLINE_MODE %}
|
||||
@@ -87,7 +88,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="item right"><a href="https://github.com/voxpupuli/puppetboard" target="_blank">v0.2.1</a></div>
|
||||
<div class="item right"><a href="https://github.com/voxpupuli/puppetboard" target="_blank">{{version()}}</a></div>
|
||||
</div>
|
||||
<div class="ui grid padding-bottom">
|
||||
<div class="one wide column"></div>
|
||||
|
||||
@@ -17,7 +17,13 @@
|
||||
'pagingType': 'simple',
|
||||
"bFilter": false,
|
||||
{% endmacro %}
|
||||
{% macro facts_extra_options(caller) %}
|
||||
'paging': false,
|
||||
// No per page AJAX
|
||||
'serverSide': false,
|
||||
{% endmacro %}
|
||||
{{ macros.datatable_init(table_html_id="reports_table", ajax_url=url_for('reports_ajax', env=current_env, node_name=node.name), default_length=config.LITTLE_TABLE_COUNT, length_selector=config.TABLE_COUNT_SELECTOR, extra_options=extra_options) }}
|
||||
{{ macros.datatable_init(table_html_id="facts_table", ajax_url=url_for('fact_ajax', env=current_env, node=node.name), default_length=config.LITTLE_TABLE_COUNT, length_selector=config.TABLE_COUNT_SELECTOR, extra_options=facts_extra_options) }}
|
||||
{% endblock onload_script %}
|
||||
|
||||
{% block content %}
|
||||
@@ -69,7 +75,16 @@
|
||||
</div>
|
||||
<div class='column'>
|
||||
<h1>Facts</h1>
|
||||
{{macros.facts_table(facts, link_facts=True, condensed=True, current_env=current_env)}}
|
||||
<table id="facts_table" class='ui fixed very basic very compact table stackable'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th class="default">Certname</th>
|
||||
<th class="date default-sort">Catalog</th>
|
||||
<th class="date">Report</th>
|
||||
<th class="default-sort">Catalog</th>
|
||||
<th>Report</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -52,10 +52,10 @@
|
||||
<td>
|
||||
<div>
|
||||
<p class='label'>
|
||||
<span>Pending</span>
|
||||
<span>Noop</span>
|
||||
</p>
|
||||
<p class='percent' style='width:{{stats['noop_percent']}}%'>
|
||||
<span>Pending</span>
|
||||
<span>Noop</span>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<tr>
|
||||
<td><a href="{{url_for('node', env=current_env, node_name=report.node)}}">{{ report.node }}</a></td>
|
||||
<td>
|
||||
{{report.version}}
|
||||
{{report.version|safe}}
|
||||
</td>
|
||||
<td rel="utctimestamp">
|
||||
{{report.start}}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"<span rel=\"utctimestamp\">{{ report[column.attr] }}</span>"
|
||||
{%- elif column.type == 'status' -%}
|
||||
{% filter jsonprint -%}
|
||||
{{ macros.status_counts(status=report.status, node_name=report.node, events=report_event_counts[report.hash_], report_hash=report.hash_, current_env=current_env) }}
|
||||
{{ macros.report_status(status=report.status, node_name=report.node, metrics=metrics[report.hash_], report_hash=report.hash_, current_env=current_env) }}
|
||||
{%- endfilter %}
|
||||
{%- elif column.type == 'node' -%}
|
||||
{% filter jsonprint %}<a href="{{url_for('node', env=current_env, node_name=report.node)}}">{{ report.node }}</a>{% endfilter %}
|
||||
|
||||
11
puppetboard/templates/static/Semantic-UI-2.1.8/semantic.min.css
vendored
Normal file
11
puppetboard/templates/static/Semantic-UI-2.1.8/semantic.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
24
puppetboard/templates/static/css/google_fonts.css
Normal file
24
puppetboard/templates/static/css/google_fonts.css
Normal file
@@ -0,0 +1,24 @@
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Lato Regular'), local('Lato-Regular'), url({{ url_for('static', filename='fonts/lato-normal-400.ttf') }}) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Lato Bold'), local('Lato-Bold'), url({{ url_for('static', filename='fonts/lato-normal-700.ttf') }}) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Lato Italic'), local('Lato-Italic'), url({{ url_for('static', filename='fonts/lato-italic-400.ttf') }}) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Lato Bold Italic'), local('Lato-BoldItalic'), url({{ url_for('static', filename='fonts/lato-italic-700.ttf') }}) format('truetype');
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os.path
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -8,8 +9,8 @@ from math import ceil
|
||||
from requests.exceptions import HTTPError, ConnectionError
|
||||
from pypuppetdb.errors import EmptyResponseError
|
||||
|
||||
from flask import abort
|
||||
|
||||
from flask import abort, request, url_for
|
||||
from jinja2.utils import contextfunction
|
||||
|
||||
# Python 3 compatibility
|
||||
try:
|
||||
@@ -20,6 +21,21 @@ except NameError:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextfunction
|
||||
def url_static_offline(context, value):
|
||||
request_parts = os.path.split(os.path.dirname(context.name))
|
||||
static_path = '/'.join(request_parts[1:])
|
||||
|
||||
return url_for('static', filename="%s/%s" % (static_path, value))
|
||||
|
||||
|
||||
def url_for_field(field, value):
|
||||
args = request.view_args.copy()
|
||||
args.update(request.args.copy())
|
||||
args[field] = value
|
||||
return url_for(request.endpoint, **args)
|
||||
|
||||
|
||||
def jsonprint(value):
|
||||
return json.dumps(value, indent=2, separators=(',', ': '))
|
||||
|
||||
|
||||
5
puppetboard/version.py
Normal file
5
puppetboard/version.py
Normal file
@@ -0,0 +1,5 @@
|
||||
#
|
||||
# Puppetboard version module
|
||||
#
|
||||
|
||||
__version__ = '0.3.0'
|
||||
@@ -1,10 +1,2 @@
|
||||
-r requirements.txt
|
||||
gunicorn==19.6.0
|
||||
Flask==0.10.1
|
||||
Flask-WTF==0.12
|
||||
Jinja2==2.7.2
|
||||
MarkupSafe==0.19
|
||||
WTForms==2.1
|
||||
Werkzeug==0.11.10
|
||||
itsdangerous==0.23
|
||||
pypuppetdb==0.3.2
|
||||
requests==2.6.0
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
pep8==1.6.2
|
||||
coverage==4.0
|
||||
-r requirements.txt
|
||||
pep8==1.7.0
|
||||
coverage==4.3.4
|
||||
mock==1.3.0
|
||||
pytest==3.0.1
|
||||
pytest-pep8==1.0.5
|
||||
pytest-cov==2.2.1
|
||||
pytest==3.0.7
|
||||
pytest-pep8==1.0.6
|
||||
pytest-cov==2.4.0
|
||||
pytest-mock==1.5.0
|
||||
cov-core==1.15.0
|
||||
unittest2==1.1.0; python_version < '2.7'
|
||||
bandit
|
||||
beautifulsoup4==4.5.3
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
Flask==0.10.1
|
||||
Flask-WTF==0.12
|
||||
Jinja2==2.7.2
|
||||
MarkupSafe==0.19
|
||||
WTForms==2.1
|
||||
Werkzeug==0.11.10
|
||||
itsdangerous==0.23
|
||||
pypuppetdb==0.3.2
|
||||
requests==2.6.0
|
||||
Flask >=0.12
|
||||
Flask-WTF >=0.14.2
|
||||
Jinja2 >=2.9.5
|
||||
MarkupSafe >=0.19
|
||||
WTForms >=2.1
|
||||
Werkzeug >=0.12.1
|
||||
itsdangerous >=0.23
|
||||
pypuppetdb >=0.3.3
|
||||
requests >=2.13.0
|
||||
CommonMark==0.7.2
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import glob
|
||||
import re
|
||||
try:
|
||||
import future.utils
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
for req_file in glob.glob('requirements*.txt'):
|
||||
new_data = []
|
||||
with open(req_file, 'r') as fp:
|
||||
data = fp.readlines()
|
||||
for line in data:
|
||||
new_data.append(re.sub(r'==\d+(\.\d+){0,3}\s+$', '\n', line))
|
||||
|
||||
with open(req_file, 'w') as fp:
|
||||
fp.writelines(new_data)
|
||||
@@ -21,5 +21,6 @@ exclude=venv
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --cov=puppetboard --cov-report=term-missing
|
||||
norecursedirs = docs .tox venv
|
||||
norecursedirs = docs .tox venv .eggs lib
|
||||
pep8ignore = E402
|
||||
python_files = test/*.py
|
||||
|
||||
49
setup.py
49
setup.py
@@ -1,25 +1,48 @@
|
||||
import sys
|
||||
import os
|
||||
import codecs
|
||||
|
||||
import re
|
||||
from setuptools.command.test import test as TestCommand
|
||||
from setuptools import setup, find_packages
|
||||
from puppetboard.version import __version__
|
||||
|
||||
|
||||
if sys.argv[-1] == 'publish':
|
||||
os.system('python setup.py sdist upload')
|
||||
sys.exit()
|
||||
|
||||
VERSION = "0.2.1"
|
||||
|
||||
with codecs.open('README.rst', encoding='utf-8') as f:
|
||||
README = f.read()
|
||||
|
||||
with codecs.open('CHANGELOG.rst', encoding='utf-8') as f:
|
||||
CHANGELOG = f.read()
|
||||
|
||||
|
||||
requirements = None
|
||||
with open('requirements.txt', 'r') as f:
|
||||
requirements = [line.rstrip()
|
||||
for line in f.readlines() if not line.startswith('-')]
|
||||
|
||||
requirements_test = None
|
||||
with open('requirements-test.txt', 'r') as f:
|
||||
requirements_test = [line.rstrip() for line in f.readlines()
|
||||
if not line.startswith('-')]
|
||||
|
||||
|
||||
class PyTest(TestCommand):
|
||||
|
||||
user_options = [('pytest-args=', 'a', "Arguments to pass to pytest")]
|
||||
|
||||
def initialize_options(self):
|
||||
TestCommand.initialize_options(self)
|
||||
self.pytest_args = '--cov=puppetboard --cov-report=term-missing'
|
||||
|
||||
def run_tests(self):
|
||||
import shlex
|
||||
import pytest
|
||||
errno = pytest.main(shlex.split(self.pytest_args))
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
setup(
|
||||
name='puppetboard',
|
||||
version=VERSION,
|
||||
version=__version__,
|
||||
author='Corey Hammerton',
|
||||
author_email='corey.hammerton@gmail.com',
|
||||
packages=find_packages(),
|
||||
@@ -29,13 +52,11 @@ setup(
|
||||
include_package_data=True,
|
||||
long_description='\n'.join((README, CHANGELOG)),
|
||||
zip_safe=False,
|
||||
install_requires=[
|
||||
"Flask >= 0.10.1",
|
||||
"Flask-WTF >= 0.12, <= 0.13",
|
||||
"WTForms >= 2.0, < 3.0",
|
||||
"pypuppetdb >= 0.3.0, < 0.4.0",
|
||||
],
|
||||
install_requires=requirements,
|
||||
tests_require=requirements_test,
|
||||
extras_require={'test': requirements_test},
|
||||
keywords="puppet puppetdb puppetboard",
|
||||
cmdclass={'test': PyTest},
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Web Environment',
|
||||
|
||||
301
test/test_app.py
301
test/test_app.py
@@ -71,13 +71,6 @@ def mock_puppetdb_default_nodes(mocker):
|
||||
catalog_timestamp='2013-08-01T09:57:00.000Z',
|
||||
facts_timestamp='2013-08-01T09:57:00.000Z',
|
||||
status_report='unchanged'),
|
||||
Node('_', 'node-skipped',
|
||||
report_timestamp='2013-08-01T09:57:00.000Z',
|
||||
latest_report_hash='1234567',
|
||||
catalog_timestamp='2013-08-01T09:57:00.000Z',
|
||||
facts_timestamp='2013-08-01T09:57:00.000Z',
|
||||
status_report='skipped')
|
||||
|
||||
]
|
||||
return mocker.patch.object(app.puppetdb, 'nodes',
|
||||
return_value=iter(node_list))
|
||||
@@ -273,6 +266,9 @@ def test_offline_mode(client, mocker):
|
||||
assert soup.title.contents[0] == 'Puppetboard'
|
||||
for link in soup.find_all('link'):
|
||||
assert "//" not in link['href']
|
||||
if 'offline' in link['href']:
|
||||
static_rv = client.get(link['href'])
|
||||
assert rv.status_code == 200
|
||||
|
||||
for script in soup.find_all('script'):
|
||||
if "src" in script.attrs:
|
||||
@@ -443,7 +439,6 @@ def test_radiator_view_json(client, mocker,
|
||||
assert json_data['noop'] == 1
|
||||
assert json_data['failed'] == 1
|
||||
assert json_data['changed'] == 1
|
||||
assert json_data['skipped'] == 1
|
||||
assert json_data['unchanged'] == 1
|
||||
|
||||
|
||||
@@ -537,9 +532,6 @@ def test_json_daily_reports_chart_ok(client, mocker):
|
||||
]
|
||||
}
|
||||
|
||||
import logging
|
||||
logging.error(query_data)
|
||||
|
||||
dbquery = MockDbQuery(query_data)
|
||||
|
||||
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
|
||||
@@ -558,3 +550,290 @@ def test_json_daily_reports_chart_ok(client, mocker):
|
||||
cur_day = next_day
|
||||
|
||||
assert rv.status_code == 200
|
||||
|
||||
|
||||
def test_catalogs_disabled(client, mocker,
|
||||
mock_puppetdb_environments,
|
||||
mock_puppetdb_default_nodes):
|
||||
app.app.config['ENABLE_CATALOG'] = False
|
||||
rv = client.get('/catalogs')
|
||||
assert rv.status_code == 403
|
||||
|
||||
|
||||
def test_catalogs_view(client, mocker,
|
||||
mock_puppetdb_environments,
|
||||
mock_puppetdb_default_nodes):
|
||||
app.app.config['ENABLE_CATALOG'] = True
|
||||
rv = client.get('/catalogs')
|
||||
assert rv.status_code == 200
|
||||
soup = BeautifulSoup(rv.data, 'html.parser')
|
||||
assert soup.title.contents[0] == 'Puppetboard'
|
||||
|
||||
|
||||
def test_catalogs_json(client, mocker,
|
||||
mock_puppetdb_environments,
|
||||
mock_puppetdb_default_nodes):
|
||||
app.app.config['ENABLE_CATALOG'] = True
|
||||
rv = client.get('/catalogs/json')
|
||||
assert rv.status_code == 200
|
||||
|
||||
result_json = json.loads(rv.data.decode('utf-8'))
|
||||
assert 'data' in result_json
|
||||
|
||||
for line in result_json['data']:
|
||||
assert len(line) == 3
|
||||
found_status = None
|
||||
for status in ['failed', 'changed', 'unchanged', 'noop', 'unreported']:
|
||||
val = BeautifulSoup(line[0], 'html.parser').find_all(
|
||||
'a', {"href": "/node/node-%s" % status})
|
||||
if len(val) == 1:
|
||||
found_status = status
|
||||
break
|
||||
assert found_status, 'Line does not match any known status'
|
||||
|
||||
val = BeautifulSoup(line[2], 'html.parser').find_all(
|
||||
'form', {"method": "GET",
|
||||
"action": "/catalogs/compare/node-%s" % found_status})
|
||||
assert len(val) == 1
|
||||
|
||||
|
||||
def test_catalogs_json_compare(client, mocker,
|
||||
mock_puppetdb_environments,
|
||||
mock_puppetdb_default_nodes):
|
||||
app.app.config['ENABLE_CATALOG'] = True
|
||||
rv = client.get('/catalogs/compare/node-unreported/json')
|
||||
assert rv.status_code == 200
|
||||
|
||||
result_json = json.loads(rv.data.decode('utf-8'))
|
||||
assert 'data' in result_json
|
||||
|
||||
for line in result_json['data']:
|
||||
assert len(line) == 3
|
||||
found_status = None
|
||||
for status in ['failed', 'changed', 'unchanged', 'noop', 'unreported']:
|
||||
val = BeautifulSoup(line[0], 'html.parser').find_all(
|
||||
'a', {"href": "/node/node-%s" % status})
|
||||
if len(val) == 1:
|
||||
found_status = status
|
||||
break
|
||||
assert found_status, 'Line does not match any known status'
|
||||
|
||||
val = BeautifulSoup(line[2], 'html.parser').find_all(
|
||||
'form', {"method": "GET",
|
||||
"action": "/catalogs/compare/node-unreported...node-%s" %
|
||||
found_status})
|
||||
assert len(val) == 1
|
||||
|
||||
|
||||
def test_facts_view(client, mocker, mock_puppetdb_environments):
|
||||
query_data = {
|
||||
'fact-names': [[chr(i) for i in range(ord('a'), ord('z') + 1)]]
|
||||
}
|
||||
|
||||
dbquery = MockDbQuery(query_data)
|
||||
|
||||
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
|
||||
|
||||
rv = client.get('/facts')
|
||||
assert rv.status_code == 200
|
||||
soup = BeautifulSoup(rv.data, 'html.parser')
|
||||
assert soup.title.contents[0] == 'Puppetboard'
|
||||
|
||||
searchable = soup.find('div', {'class': 'searchable'})
|
||||
vals = searchable.find_all('div', {'class': 'column'})
|
||||
assert len(vals) == 4
|
||||
|
||||
|
||||
def test_fact_view_with_graph(client, mocker,
|
||||
mock_puppetdb_environments,
|
||||
mock_puppetdb_default_nodes):
|
||||
rv = client.get('/fact/architecture')
|
||||
assert rv.status_code == 200
|
||||
|
||||
soup = BeautifulSoup(rv.data, 'html.parser')
|
||||
assert soup.title.contents[0] == 'Puppetboard'
|
||||
|
||||
vals = soup.find_all('div', {"id": "factChart"})
|
||||
assert len(vals) == 1
|
||||
|
||||
|
||||
def test_fact_view_without_graph(client, mocker,
|
||||
mock_puppetdb_environments,
|
||||
mock_puppetdb_default_nodes):
|
||||
rv = client.get('/%2A/fact/augeas')
|
||||
assert rv.status_code == 200
|
||||
|
||||
soup = BeautifulSoup(rv.data, 'html.parser')
|
||||
assert soup.title.contents[0] == 'Puppetboard'
|
||||
|
||||
vals = soup.find_all('div', {"id": "factChart"})
|
||||
assert len(vals) == 0
|
||||
|
||||
|
||||
def test_fact_value_view(client, mocker,
|
||||
mock_puppetdb_environments,
|
||||
mock_puppetdb_default_nodes):
|
||||
rv = client.get('/fact/architecture/amd64')
|
||||
assert rv.status_code == 200
|
||||
|
||||
soup = BeautifulSoup(rv.data, 'html.parser')
|
||||
assert soup.title.contents[0] == 'Puppetboard'
|
||||
|
||||
vals = soup.find_all('div', {"id": "factChart"})
|
||||
assert len(vals) == 0
|
||||
|
||||
|
||||
def test_node_view(client, mocker,
|
||||
mock_puppetdb_environments,
|
||||
mock_puppetdb_default_nodes):
|
||||
rv = client.get('/node/node-failed')
|
||||
assert rv.status_code == 200
|
||||
|
||||
soup = BeautifulSoup(rv.data, 'html.parser')
|
||||
assert soup.title.contents[0] == 'Puppetboard'
|
||||
|
||||
vals = soup.find_all('table', {"id": "facts_table"})
|
||||
assert len(vals) == 1
|
||||
|
||||
vals = soup.find_all('table', {"id": "reports_table"})
|
||||
assert len(vals) == 1
|
||||
|
||||
|
||||
def test_fact_json_with_graph(client, mocker,
|
||||
mock_puppetdb_environments,
|
||||
mock_puppetdb_default_nodes):
|
||||
values = ['a', 'b', 'b', 'd', True, 'a\nb']
|
||||
query_data = {'facts': []}
|
||||
query_data['facts'].append([])
|
||||
for i, value in enumerate(values):
|
||||
query_data['facts'][0].append({
|
||||
'certname': 'node-%s' % i,
|
||||
'name': 'architecture',
|
||||
'value': value,
|
||||
'environment': 'production'
|
||||
})
|
||||
|
||||
dbquery = MockDbQuery(query_data)
|
||||
|
||||
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
|
||||
|
||||
rv = client.get('/fact/architecture/json')
|
||||
assert rv.status_code == 200
|
||||
|
||||
result_json = json.loads(rv.data.decode('utf-8'))
|
||||
|
||||
assert 'data' in result_json
|
||||
assert len(result_json['data']) == 6
|
||||
for line in result_json['data']:
|
||||
assert len(line) == 2
|
||||
|
||||
assert 'chart' in result_json
|
||||
assert len(result_json['chart']) == 5
|
||||
# Test group_by
|
||||
assert result_json['chart'][0]['value'] == 2
|
||||
|
||||
|
||||
def test_fact_json_without_graph(client, mocker,
|
||||
mock_puppetdb_environments,
|
||||
mock_puppetdb_default_nodes):
|
||||
values = ['a', 'b', 'b', 'd']
|
||||
query_data = {'facts': []}
|
||||
query_data['facts'].append([])
|
||||
for i, value in enumerate(values):
|
||||
query_data['facts'][0].append({
|
||||
'certname': 'node-%s' % i,
|
||||
'name': 'architecture',
|
||||
'value': value,
|
||||
'environment': 'production'
|
||||
})
|
||||
|
||||
dbquery = MockDbQuery(query_data)
|
||||
|
||||
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
|
||||
|
||||
rv = client.get('/%2A/fact/augeas/json')
|
||||
assert rv.status_code == 200
|
||||
|
||||
result_json = json.loads(rv.data.decode('utf-8'))
|
||||
|
||||
assert 'data' in result_json
|
||||
assert len(result_json['data']) == 4
|
||||
for line in result_json['data']:
|
||||
assert len(line) == 2
|
||||
|
||||
assert 'chart' not in result_json
|
||||
|
||||
|
||||
def test_fact_value_json(client, mocker,
|
||||
mock_puppetdb_environments,
|
||||
mock_puppetdb_default_nodes):
|
||||
values = ['a', 'b', 'b', 'd']
|
||||
query_data = {'facts': []}
|
||||
query_data['facts'].append([])
|
||||
for i, value in enumerate(values):
|
||||
query_data['facts'][0].append({
|
||||
'certname': 'node-%s' % i,
|
||||
'name': 'architecture',
|
||||
'value': value,
|
||||
'environment': 'production'
|
||||
})
|
||||
|
||||
dbquery = MockDbQuery(query_data)
|
||||
|
||||
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
|
||||
|
||||
rv = client.get('/fact/architecture/amd64/json')
|
||||
assert rv.status_code == 200
|
||||
|
||||
result_json = json.loads(rv.data.decode('utf-8'))
|
||||
|
||||
assert 'data' in result_json
|
||||
assert len(result_json['data']) == 4
|
||||
for line in result_json['data']:
|
||||
assert len(line) == 1
|
||||
|
||||
assert 'chart' not in result_json
|
||||
|
||||
|
||||
def test_node_facts_json(client, mocker,
|
||||
mock_puppetdb_environments,
|
||||
mock_puppetdb_default_nodes):
|
||||
values = ['a', 'b', 'b', 'd']
|
||||
query_data = {'facts': []}
|
||||
query_data['facts'].append([])
|
||||
for i, value in enumerate(values):
|
||||
query_data['facts'][0].append({
|
||||
'certname': 'node-failed',
|
||||
'name': 'fact-%s' % i,
|
||||
'value': value,
|
||||
'environment': 'production'
|
||||
})
|
||||
|
||||
dbquery = MockDbQuery(query_data)
|
||||
|
||||
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
|
||||
|
||||
rv = client.get('/node/node-failed/facts/json')
|
||||
assert rv.status_code == 200
|
||||
|
||||
result_json = json.loads(rv.data.decode('utf-8'))
|
||||
|
||||
assert 'data' in result_json
|
||||
assert len(result_json['data']) == 4
|
||||
for line in result_json['data']:
|
||||
assert len(line) == 2
|
||||
|
||||
assert 'chart' not in result_json
|
||||
|
||||
|
||||
def test_offline_static(client):
|
||||
rv = client.get('/offline/css/google_fonts.css')
|
||||
|
||||
assert 'Content-Type' in rv.headers
|
||||
assert 'text/css' in rv.headers['Content-Type']
|
||||
assert rv.status_code == 200
|
||||
|
||||
rv = client.get('/offline/Semantic-UI-2.1.8/semantic.min.css')
|
||||
assert 'Content-Type' in rv.headers
|
||||
assert 'text/css' in rv.headers['Content-Type']
|
||||
assert rv.status_code == 200
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import pytest
|
||||
from flask import Flask, current_app
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
from puppetboard import app
|
||||
|
||||
from puppetboard.errors import (bad_request, forbidden, not_found,
|
||||
precond_failed, server_error)
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
@@ -16,15 +18,17 @@ def mock_puppetdb_environments(mocker):
|
||||
return_value=environemnts)
|
||||
|
||||
|
||||
def test_error_no_content():
|
||||
result = app.no_content(None)
|
||||
assert result[0] == ''
|
||||
assert result[1] == 204
|
||||
@pytest.fixture
|
||||
def mock_server_error(mocker):
|
||||
def raiseInternalServerError():
|
||||
raise InternalServerError('Hello world')
|
||||
return mocker.patch('puppetboard.core.environments',
|
||||
side_effect=raiseInternalServerError)
|
||||
|
||||
|
||||
def test_error_bad_request(mock_puppetdb_environments):
|
||||
with app.app.test_request_context():
|
||||
(output, error_code) = app.bad_request(None)
|
||||
(output, error_code) = bad_request(None)
|
||||
soup = BeautifulSoup(output, 'html.parser')
|
||||
|
||||
assert 'The request sent to PuppetDB was invalid' in soup.p.text
|
||||
@@ -33,7 +37,7 @@ def test_error_bad_request(mock_puppetdb_environments):
|
||||
|
||||
def test_error_forbidden(mock_puppetdb_environments):
|
||||
with app.app.test_request_context():
|
||||
(output, error_code) = app.forbidden(None)
|
||||
(output, error_code) = forbidden(None)
|
||||
soup = BeautifulSoup(output, 'html.parser')
|
||||
|
||||
long_string = "%s %s" % ('What you were looking for has',
|
||||
@@ -44,7 +48,7 @@ def test_error_forbidden(mock_puppetdb_environments):
|
||||
|
||||
def test_error_not_found(mock_puppetdb_environments):
|
||||
with app.app.test_request_context():
|
||||
(output, error_code) = app.not_found(None)
|
||||
(output, error_code) = not_found(None)
|
||||
soup = BeautifulSoup(output, 'html.parser')
|
||||
|
||||
long_string = "%s %s" % ('What you were looking for could not',
|
||||
@@ -55,7 +59,7 @@ def test_error_not_found(mock_puppetdb_environments):
|
||||
|
||||
def test_error_precond(mock_puppetdb_environments):
|
||||
with app.app.test_request_context():
|
||||
(output, error_code) = app.precond_failed(None)
|
||||
(output, error_code) = precond_failed(None)
|
||||
soup = BeautifulSoup(output, 'html.parser')
|
||||
|
||||
long_string = "%s %s" % ('You\'ve configured Puppetboard with an API',
|
||||
@@ -66,8 +70,16 @@ def test_error_precond(mock_puppetdb_environments):
|
||||
|
||||
def test_error_server(mock_puppetdb_environments):
|
||||
with app.app.test_request_context():
|
||||
(output, error_code) = app.server_error(None)
|
||||
(output, error_code) = server_error(None)
|
||||
soup = BeautifulSoup(output, 'html.parser')
|
||||
|
||||
assert 'Internal Server Error' in soup.h2.text
|
||||
assert error_code == 500
|
||||
|
||||
|
||||
def test_early_error_server(mock_server_error):
|
||||
with app.app.test_request_context():
|
||||
(output, error_code) = server_error(None)
|
||||
soup = BeautifulSoup(output, 'html.parser')
|
||||
assert 'Internal Server Error' in soup.h2.text
|
||||
assert error_code == 500
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
import os
|
||||
from puppetboard import docker_settings
|
||||
from puppetboard import app
|
||||
import puppetboard.core
|
||||
|
||||
try:
|
||||
import future.utils
|
||||
@@ -100,12 +100,14 @@ def test_graph_facts_custom(cleanUpEnv):
|
||||
assert 'extra' in facts
|
||||
|
||||
|
||||
def test_bad_log_value(cleanUpEnv):
|
||||
def test_bad_log_value(cleanUpEnv, mocker):
|
||||
os.environ['LOGLEVEL'] = 'g'
|
||||
os.environ['PUPPETBOARD_SETTINGS'] = '../puppetboard/docker_settings.py'
|
||||
reload(docker_settings)
|
||||
|
||||
puppetboard.core.APP = None
|
||||
with pytest.raises(ValueError) as error:
|
||||
reload(app)
|
||||
puppetboard.core.get_app()
|
||||
|
||||
|
||||
def test_default_table_selctor(cleanUpEnv):
|
||||
@@ -116,3 +118,11 @@ def test_env_table_selector(cleanUpEnv):
|
||||
os.environ['TABLE_COUNT_SELECTOR'] = '5,15,25'
|
||||
reload(docker_settings)
|
||||
assert [5, 15, 25] == docker_settings.TABLE_COUNT_SELECTOR
|
||||
|
||||
|
||||
def test_env_column_options(cleanUpEnv):
|
||||
os.environ['DISPLAYED_METRICS'] = 'resources.total, events.failure'
|
||||
|
||||
reload(docker_settings)
|
||||
assert ['resources.total',
|
||||
'events.failure'] == docker_settings.DISPLAYED_METRICS
|
||||
|
||||
12
test/test_form.py
Normal file
12
test/test_form.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import pytest
|
||||
from puppetboard import app, forms
|
||||
|
||||
|
||||
def test_form_valid(capsys):
|
||||
for form in [forms.QueryForm]:
|
||||
with app.app.test_request_context():
|
||||
qf = form()
|
||||
out, err = capsys.readouterr()
|
||||
assert qf is not None
|
||||
assert err == ""
|
||||
assert out == ""
|
||||
@@ -12,7 +12,6 @@ from werkzeug.exceptions import NotFound, InternalServerError
|
||||
|
||||
from puppetboard import utils
|
||||
from puppetboard import app
|
||||
from puppetboard.app import NoContent
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import logging
|
||||
@@ -108,19 +107,6 @@ def test_http_connection_error(mock_log):
|
||||
mock_log.error.assert_called_with(err)
|
||||
|
||||
|
||||
def test_http_empty(mock_log, mocker):
|
||||
err = "Empty Response"
|
||||
|
||||
def connection_error():
|
||||
raise EmptyResponseError(err)
|
||||
|
||||
flask_abort = mocker.patch('flask.abort')
|
||||
with pytest.raises(NoContent):
|
||||
utils.get_or_abort(connection_error)
|
||||
mock_log.error.assert_called_with(err)
|
||||
flask_abort.assert_called_with('204')
|
||||
|
||||
|
||||
def test_db_version_good(mocker, mock_info_log):
|
||||
mocker.patch.object(app.puppetdb, 'current_version', return_value='4.2.0')
|
||||
err = 'PuppetDB Version %d.%d.%d' % (4, 2, 0)
|
||||
|
||||
Reference in New Issue
Block a user