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"
|
- "2.7"
|
||||||
- "3.5"
|
- "3.5"
|
||||||
- "3.6"
|
- "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:
|
install:
|
||||||
- if [ "${PINNED}" == "FALSE" ]; then python scripts/unpin.py; fi
|
- pip install -r requirements-test.txt
|
||||||
- pip install -r requirements.txt
|
- pip install -q coveralls --use-wheel
|
||||||
- pip install -U -r requirements-test.txt
|
|
||||||
- pip install -q coverage coveralls --use-wheel
|
|
||||||
script:
|
script:
|
||||||
- py.test --cov=puppetboard --pep8 -v
|
- pytest --pep8
|
||||||
- ./bandit.sh
|
- if [ "${TRAVIS_PYTHON_VERSION}" != "2.6" ]; then
|
||||||
|
pip install bandit;
|
||||||
|
bandit -r puppetboard;
|
||||||
|
fi
|
||||||
after_success:
|
after_success:
|
||||||
- coveralls
|
- coveralls
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ Changelog
|
|||||||
|
|
||||||
This is the changelog for Puppetboard.
|
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
|
0.2.1
|
||||||
=====
|
=====
|
||||||
|
|
||||||
|
|||||||
13
Dockerfile
13
Dockerfile
@@ -1,12 +1,15 @@
|
|||||||
FROM python:2.7-alpine
|
FROM python:2.7-alpine
|
||||||
|
|
||||||
ENV PUPPETBOARD_PORT 80
|
ENV PUPPETBOARD_PORT 80
|
||||||
ENV PUPPETBOARD_SETTINGS docker_settings.py
|
EXPOSE 80
|
||||||
RUN mkdir -p /usr/src/app
|
|
||||||
WORKDIR /usr/src/app
|
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
|
COPY . /usr/src/app
|
||||||
|
|
||||||
CMD gunicorn -b 0.0.0.0:${PUPPETBOARD_PORT} --access-logfile=/dev/stdout puppetboard.app: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`_ |
|
| `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/
|
.. _ArchLinux: https://aur.archlinux.org/packages/python2-puppetboard/
|
||||||
.. _Tim Meusel: https://github.com/bastelfreak
|
.. _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/
|
.. _OpenBSD: http://www.openbsd.org/cgi-bin/cvsweb/ports/www/puppetboard/
|
||||||
.. _OpenSuSE Build Service: https://build.opensuse.org/package/show/systemsmanagement:puppet/python-puppetboard
|
.. _OpenSuSE Build Service: https://build.opensuse.org/package/show/systemsmanagement:puppet/python-puppetboard
|
||||||
.. _OpenSuSE 12/13: 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
|
.. 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
|
You're advised to do this inside a virtualenv specifically created to work on
|
||||||
Puppetboard as to not pollute your global Python installation.
|
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,
|
* ``OFFLINE_MODE``: If set to ``True`` load static assets (jquery,
|
||||||
semantic-ui, etc) from the local web server instead of a CDN.
|
semantic-ui, etc) from the local web server instead of a CDN.
|
||||||
Defaults to ``False``.
|
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
|
.. _pypuppetdb documentation: http://pypuppetdb.readthedocs.org/en/v0.1.0/quickstart.html#ssl
|
||||||
.. _Flask documentation: http://flask.pocoo.org/docs/0.10/quickstart/#sessions
|
.. _Flask documentation: http://flask.pocoo.org/docs/0.10/quickstart/#sessions
|
||||||
@@ -285,6 +300,14 @@ scenarios:
|
|||||||
If you deploy Puppetboard through a different setup we'd welcome a pull
|
If you deploy Puppetboard through a different setup we'd welcome a pull
|
||||||
request that adds the instructions to this section.
|
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
|
Apache + mod_wsgi
|
||||||
^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
@@ -683,11 +706,11 @@ Some people have already started building things with and around Puppetboard.
|
|||||||
|
|
||||||
Packages
|
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/>`_.
|
* 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
|
.. _Julien K.: https://github.com/juliengk
|
||||||
|
|
||||||
Contributing
|
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 logging
|
||||||
import collections
|
import collections
|
||||||
try:
|
try:
|
||||||
from urllib import unquote
|
from urllib import unquote, unquote_plus, quote_plus
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote, unquote_plus, quote_plus
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from itertools import tee
|
from itertools import tee
|
||||||
|
|
||||||
@@ -15,19 +15,23 @@ from flask import (
|
|||||||
Response, stream_with_context, redirect,
|
Response, stream_with_context, redirect,
|
||||||
request, session, jsonify
|
request, session, jsonify
|
||||||
)
|
)
|
||||||
|
from jinja2.utils import contextfunction
|
||||||
|
|
||||||
from pypuppetdb import connect
|
|
||||||
from pypuppetdb.errors import EmptyResponseError
|
|
||||||
from pypuppetdb.QueryBuilder import *
|
from pypuppetdb.QueryBuilder import *
|
||||||
|
|
||||||
from puppetboard.forms import (CatalogForm, QueryForm)
|
from puppetboard.forms import QueryForm
|
||||||
from puppetboard.utils import (
|
from puppetboard.utils import (get_or_abort, yield_or_stop,
|
||||||
get_or_abort, yield_or_stop, get_db_version,
|
get_db_version)
|
||||||
jsonprint, prettyprint
|
|
||||||
)
|
|
||||||
from puppetboard.dailychart import get_daily_reports_chart
|
from puppetboard.dailychart import get_daily_reports_chart
|
||||||
|
|
||||||
import werkzeug.exceptions as ex
|
import werkzeug.exceptions as ex
|
||||||
|
import CommonMark
|
||||||
|
|
||||||
|
from puppetboard.core import get_app, get_puppetdb, environments
|
||||||
|
import puppetboard.errors
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
|
|
||||||
|
|
||||||
REPORTS_COLUMNS = [
|
REPORTS_COLUMNS = [
|
||||||
{'attr': 'end', 'filter': 'end_time',
|
{'attr': 'end', 'filter': 'end_time',
|
||||||
@@ -40,31 +44,26 @@ REPORTS_COLUMNS = [
|
|||||||
'name': 'Agent version'},
|
'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']
|
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)
|
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)
|
logging.basicConfig(level=numeric_level)
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
puppetdb = get_puppetdb()
|
||||||
|
|
||||||
|
|
||||||
|
@app.template_global()
|
||||||
|
def version():
|
||||||
|
return __version__
|
||||||
|
|
||||||
|
|
||||||
def stream_template(template_name, **context):
|
def stream_template(template_name, **context):
|
||||||
app.update_template_context(context)
|
app.update_template_context(context)
|
||||||
@@ -74,29 +73,10 @@ def stream_template(template_name, **context):
|
|||||||
return rv
|
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):
|
def check_env(env, envs):
|
||||||
if env != '*' and env not in envs:
|
if env != '*' and env not in envs:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
app.jinja_env.globals['url_for_field'] = url_for_field
|
|
||||||
|
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def utility_processor():
|
def utility_processor():
|
||||||
@@ -106,58 +86,6 @@ def utility_processor():
|
|||||||
return dict(now=now)
|
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('/', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
@app.route('/<env>/')
|
@app.route('/<env>/')
|
||||||
def index(env):
|
def index(env):
|
||||||
@@ -330,29 +258,11 @@ def nodes(env):
|
|||||||
current_env=env)))
|
current_env=env)))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/inventory', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
def inventory_facts():
|
||||||
@app.route('/<env>/inventory')
|
# a list of facts descriptions to go in table header
|
||||||
def inventory(env):
|
headers = []
|
||||||
"""Fetch all (active) nodes from PuppetDB and stream a table displaying
|
# a list of inventory fact names
|
||||||
those nodes along with a set of facts about them.
|
fact_names = []
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# load the list of items/facts we want in our inventory
|
# load the list of items/facts we want in our inventory
|
||||||
try:
|
try:
|
||||||
@@ -370,38 +280,70 @@ def inventory(env):
|
|||||||
headers.append(desc)
|
headers.append(desc)
|
||||||
fact_names.append(name)
|
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()
|
query = AndOperator()
|
||||||
fact_query = OrOperator()
|
fact_query = OrOperator()
|
||||||
fact_query.add([EqualsOperator("name", name) for name in fact_names])
|
fact_query.add([EqualsOperator("name", name) for name in fact_names])
|
||||||
|
query.add(fact_query)
|
||||||
|
|
||||||
if env != '*':
|
if env != '*':
|
||||||
query.add(EqualsOperator("environment", env))
|
query.add(EqualsOperator("environment", env))
|
||||||
|
|
||||||
query.add(fact_query)
|
|
||||||
|
|
||||||
# get all the facts from PuppetDB
|
|
||||||
facts = puppetdb.facts(query=query)
|
facts = puppetdb.facts(query=query)
|
||||||
|
|
||||||
|
fact_data = {}
|
||||||
for fact in facts:
|
for fact in facts:
|
||||||
if fact.node not in fact_data:
|
if fact.node not in fact_data:
|
||||||
fact_data[fact.node] = {}
|
fact_data[fact.node] = {}
|
||||||
|
|
||||||
fact_data[fact.node][fact.name] = fact.value
|
fact_data[fact.node][fact.name] = fact.value
|
||||||
|
|
||||||
return Response(stream_with_context(
|
total = len(fact_data)
|
||||||
stream_template(
|
|
||||||
'inventory.html',
|
return render_template(
|
||||||
headers=headers,
|
'inventory.json.tpl',
|
||||||
fact_names=fact_names,
|
draw=draw,
|
||||||
fact_data=fact_data,
|
total=total,
|
||||||
envs=envs,
|
total_filtered=total,
|
||||||
current_env=env
|
fact_data=fact_data,
|
||||||
)))
|
columns=fact_names)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/node/<node_name>/',
|
@app.route('/node/<node_name>',
|
||||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
@app.route('/<env>/node/<node_name>/')
|
@app.route('/<env>/node/<node_name>')
|
||||||
def node(env, node_name):
|
def node(env, node_name):
|
||||||
"""Display a dashboard for a node showing as much data as we have on that
|
"""Display a dashboard for a node showing as much data as we have on that
|
||||||
node. This includes facts and reports but not Resources as that is too
|
node. This includes facts and reports but not Resources as that is too
|
||||||
@@ -420,21 +362,20 @@ def node(env, node_name):
|
|||||||
query.add(EqualsOperator("certname", node_name))
|
query.add(EqualsOperator("certname", node_name))
|
||||||
|
|
||||||
node = get_or_abort(puppetdb.node, node_name)
|
node = get_or_abort(puppetdb.node, node_name)
|
||||||
facts = node.facts()
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'node.html',
|
'node.html',
|
||||||
node=node,
|
node=node,
|
||||||
facts=yield_or_stop(facts),
|
|
||||||
envs=envs,
|
envs=envs,
|
||||||
current_env=env,
|
current_env=env,
|
||||||
columns=REPORTS_COLUMNS[:2])
|
columns=REPORTS_COLUMNS[:2])
|
||||||
|
|
||||||
|
|
||||||
@app.route('/reports/',
|
@app.route('/reports',
|
||||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT'],
|
defaults={'env': app.config['DEFAULT_ENVIRONMENT'],
|
||||||
'node_name': None})
|
'node_name': None})
|
||||||
@app.route('/<env>/reports/', defaults={'node_name': None})
|
@app.route('/<env>/reports', defaults={'node_name': None})
|
||||||
@app.route('/reports/<node_name>/',
|
@app.route('/reports/<node_name>',
|
||||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
@app.route('/<env>/reports/<node_name>')
|
@app.route('/<env>/reports/<node_name>')
|
||||||
def reports(env, 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_column = int(request.args.get('order[0][column]', 0))
|
||||||
order_filter = REPORTS_COLUMNS[order_column].get(
|
order_filter = REPORTS_COLUMNS[order_column].get(
|
||||||
'filter', REPORTS_COLUMNS[order_column]['attr'])
|
'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)
|
order_args = '[{"field": "%s", "order": "%s"}]' % (order_filter, order_dir)
|
||||||
status_args = request.args.get('columns[1][search][value]', '').split('|')
|
status_args = request.args.get('columns[1][search][value]', '').split('|')
|
||||||
max_col = len(REPORTS_COLUMNS)
|
max_col = len(REPORTS_COLUMNS)
|
||||||
@@ -538,26 +479,17 @@ def reports_ajax(env, node_name):
|
|||||||
reports_events = []
|
reports_events = []
|
||||||
total = 0
|
total = 0
|
||||||
|
|
||||||
report_event_counts = {}
|
# Convert metrics to relational dict
|
||||||
# Create a map from the metrics data to what the templates
|
metrics = {}
|
||||||
# use to express the data.
|
|
||||||
report_map = {
|
|
||||||
'success': 'successes',
|
|
||||||
'failure': 'failures',
|
|
||||||
'skipped': 'skips',
|
|
||||||
'noops': 'noop'
|
|
||||||
}
|
|
||||||
for report in reports_events:
|
for report in reports_events:
|
||||||
if total is None:
|
if total is None:
|
||||||
total = puppetdb.total
|
total = puppetdb.total
|
||||||
|
|
||||||
report_counts = {'successes': 0, 'failures': 0, 'skips': 0}
|
metrics[report.hash_] = {}
|
||||||
for metrics in report.metrics:
|
for m in report.metrics:
|
||||||
if 'name' in metrics and metrics['name'] in report_map:
|
if m['category'] not in metrics[report.hash_]:
|
||||||
key_name = report_map[metrics['name']]
|
metrics[report.hash_][m['category']] = {}
|
||||||
report_counts[key_name] = metrics['value']
|
metrics[report.hash_][m['category']][m['name']] = m['value']
|
||||||
|
|
||||||
report_event_counts[report.hash_] = report_counts
|
|
||||||
|
|
||||||
if total is None:
|
if total is None:
|
||||||
total = 0
|
total = 0
|
||||||
@@ -568,7 +500,7 @@ def reports_ajax(env, node_name):
|
|||||||
total=total,
|
total=total,
|
||||||
total_filtered=total,
|
total_filtered=total,
|
||||||
reports=reports,
|
reports=reports,
|
||||||
report_event_counts=report_event_counts,
|
metrics=metrics,
|
||||||
envs=envs,
|
envs=envs,
|
||||||
current_env=env,
|
current_env=env,
|
||||||
columns=REPORTS_COLUMNS[:max_col])
|
columns=REPORTS_COLUMNS[:max_col])
|
||||||
@@ -614,6 +546,8 @@ def report(env, node_name, report_id):
|
|||||||
except StopIteration:
|
except StopIteration:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
report.version = CommonMark.commonmark(report.version)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'report.html',
|
'report.html',
|
||||||
report=report,
|
report=report,
|
||||||
@@ -638,108 +572,178 @@ def facts(env):
|
|||||||
check_env(env, envs)
|
check_env(env, envs)
|
||||||
facts = []
|
facts = []
|
||||||
order_by = '[{"field": "name", "order": "asc"}]'
|
order_by = '[{"field": "name", "order": "asc"}]'
|
||||||
|
facts = get_or_abort(puppetdb.fact_names)
|
||||||
|
|
||||||
if env == '*':
|
facts_columns = [[]]
|
||||||
facts = get_or_abort(puppetdb.fact_names)
|
letter = None
|
||||||
else:
|
letter_list = None
|
||||||
query = ExtractOperator()
|
break_size = (len(facts) / 4) + 1
|
||||||
query.add_field(str('name'))
|
next_break = break_size
|
||||||
query.add_query(EqualsOperator("environment", env))
|
count = 0
|
||||||
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)
|
|
||||||
for fact in facts:
|
for fact in facts:
|
||||||
letter = fact[0].upper()
|
count += 1
|
||||||
letter_list = facts_dict[letter]
|
|
||||||
letter_list.append(fact)
|
if letter != fact[0].upper() or not letter:
|
||||||
facts_dict[letter] = letter_list
|
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',
|
return render_template('facts.html',
|
||||||
facts_dict=sorted_facts_dict,
|
facts_columns=facts_columns,
|
||||||
facts_len=(sum(map(len, facts_dict.values())) +
|
|
||||||
len(facts_dict) * 5),
|
|
||||||
envs=envs,
|
envs=envs,
|
||||||
current_env=env)
|
current_env=env)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/fact/<fact>', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
@app.route('/fact/<fact>',
|
||||||
@app.route('/<env>/fact/<fact>')
|
defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'value': None})
|
||||||
def fact(env, fact):
|
@app.route('/<env>/fact/<fact>', defaults={'value': None})
|
||||||
"""Fetches the specific fact from PuppetDB and displays its value per
|
@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.
|
node for which this fact is known.
|
||||||
|
|
||||||
:param env: Searches for facts in this environment
|
:param env: Searches for facts in this environment
|
||||||
:type env: :obj:`string`
|
:type env: :obj:`string`
|
||||||
:param fact: Find all facts with this name
|
:param fact: Find all facts with this name
|
||||||
:type fact: :obj:`string`
|
:type fact: :obj:`string`
|
||||||
"""
|
:param value: Find all facts with this value
|
||||||
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
|
|
||||||
:type value: :obj:`string`
|
:type value: :obj:`string`
|
||||||
"""
|
"""
|
||||||
envs = environments()
|
envs = environments()
|
||||||
check_env(env, envs)
|
check_env(env, envs)
|
||||||
|
|
||||||
if env == '*':
|
render_graph = False
|
||||||
query = None
|
if fact in graph_facts and not value:
|
||||||
else:
|
render_graph = True
|
||||||
query = EqualsOperator("environment", env)
|
|
||||||
|
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(
|
return render_template(
|
||||||
'fact.html',
|
'fact.html',
|
||||||
name=fact,
|
fact=fact,
|
||||||
value=value,
|
value=value,
|
||||||
facts=localfacts,
|
value_safe=value_safe,
|
||||||
|
render_graph=render_graph,
|
||||||
envs=envs,
|
envs=envs,
|
||||||
current_env=env)
|
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'),
|
@app.route('/query', methods=('GET', 'POST'),
|
||||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
@app.route('/<env>/query', methods=('GET', 'POST'))
|
@app.route('/<env>/query', methods=('GET', 'POST'))
|
||||||
@@ -828,9 +832,14 @@ def metric(env, metric):
|
|||||||
current_env=env)
|
current_env=env)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/catalogs', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
@app.route('/catalogs',
|
||||||
@app.route('/<env>/catalogs')
|
defaults={'env': app.config['DEFAULT_ENVIRONMENT'],
|
||||||
def catalogs(env):
|
'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.
|
"""Lists all nodes with a compiled catalog.
|
||||||
|
|
||||||
:param env: Find the nodes with this catalog_environment value
|
:param env: Find the nodes with this catalog_environment value
|
||||||
@@ -839,53 +848,80 @@ def catalogs(env):
|
|||||||
envs = environments()
|
envs = environments()
|
||||||
check_env(env, envs)
|
check_env(env, envs)
|
||||||
|
|
||||||
if app.config['ENABLE_CATALOG']:
|
if not 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)
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'catalogs.html',
|
|
||||||
nodes=catalog_list,
|
|
||||||
envs=envs,
|
|
||||||
current_env=env)
|
|
||||||
else:
|
|
||||||
log.warn('Access to catalog interface disabled by administrator')
|
log.warn('Access to catalog interface disabled by administrator')
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'catalogs.html',
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/catalog/<node_name>',
|
@app.route('/catalog/<node_name>',
|
||||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
@@ -911,40 +947,6 @@ def catalog_node(env, node_name):
|
|||||||
abort(403)
|
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>',
|
@app.route('/catalogs/compare/<compare>...<against>',
|
||||||
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
@app.route('/<env>/catalogs/compare/<compare>...<against>')
|
@app.route('/<env>/catalogs/compare/<compare>...<against>')
|
||||||
@@ -1090,3 +1092,15 @@ def daily_reports_chart(env):
|
|||||||
certname=certname,
|
certname=certname,
|
||||||
)
|
)
|
||||||
return jsonify(result=result)
|
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
|
NORMAL_TABLE_COUNT = 100
|
||||||
LITTLE_TABLE_COUNT = 10
|
LITTLE_TABLE_COUNT = 10
|
||||||
TABLE_COUNT_SELECTOR = [10, 20, 50, 100, 500]
|
TABLE_COUNT_SELECTOR = [10, 20, 50, 100, 500]
|
||||||
|
DISPLAYED_METRICS = ['resources.total',
|
||||||
|
'events.failure',
|
||||||
|
'events.success',
|
||||||
|
'resources.skipped',
|
||||||
|
'events.noop']
|
||||||
OFFLINE_MODE = False
|
OFFLINE_MODE = False
|
||||||
ENABLE_CATALOG = False
|
ENABLE_CATALOG = False
|
||||||
OVERVIEW_FILTER = None
|
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_SELECTOR = [int(x) for x in os.getenv('TABLE_COUNT_SELECTOR',
|
||||||
TABLE_COUNT_DEF).split(',')]
|
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')
|
OFFLINE_MODE = bool(os.getenv('OFFLINE_MODE', 'False').upper() == 'TRUE')
|
||||||
ENABLE_CATALOG = bool(os.getenv('ENABLE_CATALOG', 'False').upper() == 'TRUE')
|
ENABLE_CATALOG = bool(os.getenv('ENABLE_CATALOG', 'False').upper() == 'TRUE')
|
||||||
OVERVIEW_FILTER = os.getenv('OVERVIEW_FILTER', None)
|
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 = [x.strip() for x in os.getenv('GRAPH_FACTS',
|
||||||
GRAPH_FACTS_DEFAULT).split(',')]
|
GRAPH_FACTS_DEFAULT).split(',')]
|
||||||
|
|
||||||
|
|
||||||
GRAPH_TYPE = os.getenv('GRAPH_TYPE', 'pie')
|
GRAPH_TYPE = os.getenv('GRAPH_TYPE', 'pie')
|
||||||
|
|
||||||
# Tuples are hard to express as an environment variable, so here
|
# 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 unicode_literals
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
from flask.ext.wtf import Form
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import (
|
from wtforms import (
|
||||||
HiddenField, RadioField, SelectField,
|
HiddenField, RadioField, SelectField,
|
||||||
TextAreaField, BooleanField, validators
|
TextAreaField, BooleanField, validators
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class QueryForm(Form):
|
class QueryForm(FlaskForm):
|
||||||
"""The form used to allow freeform queries to be executed against
|
"""The form used to allow freeform queries to be executed against
|
||||||
PuppetDB."""
|
PuppetDB."""
|
||||||
query = TextAreaField('Query', [validators.Required(
|
query = TextAreaField('Query', [validators.Required(
|
||||||
@@ -28,9 +28,3 @@ class QueryForm(Form):
|
|||||||
('pql', 'PQL'),
|
('pql', 'PQL'),
|
||||||
])
|
])
|
||||||
rawjson = BooleanField('Raw JSON')
|
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;
|
color: #AA4643;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.label.failed {
|
.ui.label.failed, .ui.label.events.failure {
|
||||||
background-color: #AA4643;
|
background-color: #AA4643;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ h1.ui.header.no-margin-bottom {
|
|||||||
color: #4572A7;
|
color: #4572A7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.label.changed {
|
.ui.label.changed, .ui.label.events.success {
|
||||||
background-color: #4572A7;
|
background-color: #4572A7;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,10 +68,14 @@ h1.ui.header.no-margin-bottom {
|
|||||||
color: #DB843D;
|
color: #DB843D;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.label.noop {
|
.ui.label.noop, .ui.label.events.noop {
|
||||||
background-color: #DB843D;
|
background-color: #DB843D;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.label.resources.total {
|
||||||
|
background-color: #989898;
|
||||||
|
}
|
||||||
|
|
||||||
.ui.label.unchanged {
|
.ui.label.unchanged {
|
||||||
background-color: #89A54E;
|
background-color: #89A54E;
|
||||||
}
|
}
|
||||||
@@ -80,7 +84,7 @@ h1.ui.header.no-margin-bottom {
|
|||||||
color: orange;
|
color: orange;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.label.skipped {
|
.ui.label.skipped, .ui.label.resources.skipped {
|
||||||
background-color: orange;
|
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 .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 {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: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 .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:#ee7722;}
|
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.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 .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,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.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 .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:#eddc21;}
|
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.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 .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:#009933;}
|
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.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 .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:#2198ed;}
|
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.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 {color:#fff;background-color:#181818;}
|
||||||
body.radiator_controller table.node_summary tr.total .percent {background-color:white;border-radius:0 3px 3px 0;}
|
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 ($) {
|
jQuery(function ($) {
|
||||||
function generateChart(el) {
|
function generateChart(el) {
|
||||||
var url = "/daily_reports_chart.json";
|
var url = "daily_reports_chart.json";
|
||||||
var certname = $(el).attr('data-certname');
|
var certname = $(el).attr('data-certname');
|
||||||
if (typeof certname !== typeof undefined && certname !== false) {
|
if (typeof certname !== typeof undefined && certname !== false) {
|
||||||
url = url + "?certname=" + certname;
|
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) -%}
|
{% 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>
|
<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' %}
|
{% if status == 'unreported' %}
|
||||||
@@ -57,9 +9,33 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{%- endmacro %}
|
{%- 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) -%}
|
{% macro datatable_init(table_html_id, ajax_url, default_length, length_selector, extra_options=None) -%}
|
||||||
// Init datatable
|
// Init datatable
|
||||||
$.fn.dataTable.ext.errMode = 'throw';
|
$.fn.dataTable.ext.errMode = 'throw';
|
||||||
|
|
||||||
var table = $('#{{ table_html_id }}').DataTable({
|
var table = $('#{{ table_html_id }}').DataTable({
|
||||||
// Permit flow auto-readjust (responsive)
|
// Permit flow auto-readjust (responsive)
|
||||||
"autoWidth": false,
|
"autoWidth": false,
|
||||||
@@ -76,12 +52,19 @@
|
|||||||
// Paging options
|
// Paging options
|
||||||
"lengthMenu": {{ length_selector }},
|
"lengthMenu": {{ length_selector }},
|
||||||
"pageLength": {{ default_length }},
|
"pageLength": {{ default_length }},
|
||||||
|
// Search as regex (does not apply if serverSide)
|
||||||
|
"search": {"regex": true},
|
||||||
// Default sort
|
// Default sort
|
||||||
"order": [[ 0, "desc" ]],
|
"order": [[ 0, "desc" ]],
|
||||||
// Custom options
|
// Custom options
|
||||||
{% if extra_options %}{% call extra_options() %}Callback to parent defined options{% endcall %}{% endif %}
|
{% 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.on('draw.dt', function(){
|
||||||
$('#{{ table_html_id }} [rel=utctimestamp]').each(
|
$('#{{ table_html_id }} [rel=utctimestamp]').each(
|
||||||
function(index, timestamp){
|
function(index, timestamp){
|
||||||
|
|||||||
@@ -1,40 +1,21 @@
|
|||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% import '_macros.html' as macros %}
|
{% import '_macros.html' as macros %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui fluid icon input hide" style="margin-bottom:20px">
|
<table id="catalogs_table" class='ui very basic table stackable'>
|
||||||
<input autofocus="autofocus" class="filter-table" placeholder="Type here to filter...">
|
|
||||||
</div>
|
|
||||||
<table class='ui very basic very compact table nodes'>
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
{% for column in columns %}
|
||||||
<th>Certname</th>
|
<th>{{ column.name }}</th>
|
||||||
<th>Compile Time</th>
|
{% endfor %}
|
||||||
<th>Compare With</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="searchable">
|
<tbody>
|
||||||
{% 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>
|
</table>
|
||||||
{% endblock content %}
|
{% 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' %}
|
{% extends 'layout.html' %}
|
||||||
{% import '_macros.html' as macros %}
|
{% 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 %}
|
{% block onload_script %}
|
||||||
$('table').tablesort();
|
{% macro extra_options(caller) %}
|
||||||
{% if render_graph %}
|
// No per page AJAX
|
||||||
chart = c3.generate({
|
'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',
|
bindto: '#factChart',
|
||||||
data: {
|
data: {
|
||||||
columns: realdata,
|
columns: realdata,
|
||||||
type : '{{config.GRAPH_TYPE|default('pie')}}',
|
type : '{{config.GRAPH_TYPE|default('pie')}}',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
{% endif %}
|
})
|
||||||
|
{% endif %}
|
||||||
{% endblock onload_script %}
|
{% endblock onload_script %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if render_graph %}
|
||||||
<div id="factChart" width="300" height="300"></div>
|
<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 %}
|
{% 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 %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -4,26 +4,19 @@
|
|||||||
<input autofocus="autofocus" class="filter-list" placeholder="Type here to filter...">
|
<input autofocus="autofocus" class="filter-list" placeholder="Type here to filter...">
|
||||||
</div>
|
</div>
|
||||||
<div class="ui searchable stackable doubling four column grid factlist">
|
<div class="ui searchable stackable doubling four column grid factlist">
|
||||||
|
{%- for column in facts_columns %}
|
||||||
<div class="column">
|
<div class="column">
|
||||||
{%- set facts_count = 0 -%}
|
{%- for letter in column %}
|
||||||
{%- set break = facts_len//4 + 1 -%}
|
|
||||||
{%- for key,facts_list in facts_dict %}
|
|
||||||
<div class="ui list_hide_segment segment">
|
<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>
|
<ul>
|
||||||
{%- for fact in facts_list %}
|
{%- for fact in letter %}
|
||||||
<li><a href="{{url_for('fact', env=current_env, fact=fact)}}">{{fact}}</a></li>
|
<li><a href="{{url_for('fact', env=current_env, fact=fact)}}">{{ fact }}</a></li>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{%- set facts_count = facts_count + facts_list|length -%}
|
{%- endfor %}
|
||||||
{%- if facts_count >= break -%}
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
{%- set break = facts_len//4 + 1 + break -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- set facts_count = facts_count + 5 -%}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{%- endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="five wide">Status</th>
|
<th class="five wide">Status</th>
|
||||||
<th class="five wide">Certname</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>
|
<th class="one wide"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
|
{% import '_macros.html' as macros %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui fluid icon input hide" style="margin-bottom:20px">
|
<table id="inventory_table" class='ui fixed compact very basic sortable table'>
|
||||||
<input autofocus="autofocus" class="filter-table" placeholder="Type here to filter...">
|
|
||||||
</div>
|
|
||||||
<table class='ui compact very basic sortable table'>
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% for head in headers %}
|
{% for head in fact_headers %}
|
||||||
<th{% if loop.index == 1 %} class="default-sort"{% endif %}>{{head}}</th>
|
<th>{{head}}</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="searchable">
|
<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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endblock content %}
|
{% 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>
|
</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('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 %}
|
{% else %}
|
||||||
<link href='//fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css' />
|
<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'>
|
<link href='//cdnjs.cloudflare.com/ajax/libs/datatables/1.10.13/css/dataTables.semanticui.min.css' rel='stylesheet' type='text/css'>
|
||||||
{% endif %}
|
{% 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" />
|
<link href="{{ url_for('static', filename='css/puppetboard.css') }}" rel="stylesheet" />
|
||||||
|
|
||||||
{% if config.OFFLINE_MODE %}
|
{% if config.OFFLINE_MODE %}
|
||||||
@@ -87,7 +88,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<div class="ui grid padding-bottom">
|
<div class="ui grid padding-bottom">
|
||||||
<div class="one wide column"></div>
|
<div class="one wide column"></div>
|
||||||
|
|||||||
@@ -17,7 +17,13 @@
|
|||||||
'pagingType': 'simple',
|
'pagingType': 'simple',
|
||||||
"bFilter": false,
|
"bFilter": false,
|
||||||
{% endmacro %}
|
{% 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="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 %}
|
{% endblock onload_script %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -69,7 +75,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='column'>
|
<div class='column'>
|
||||||
<h1>Facts</h1>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th class="default">Certname</th>
|
<th class="default">Certname</th>
|
||||||
<th class="date default-sort">Catalog</th>
|
<th class="default-sort">Catalog</th>
|
||||||
<th class="date">Report</th>
|
<th>Report</th>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -52,10 +52,10 @@
|
|||||||
<td>
|
<td>
|
||||||
<div>
|
<div>
|
||||||
<p class='label'>
|
<p class='label'>
|
||||||
<span>Pending</span>
|
<span>Noop</span>
|
||||||
</p>
|
</p>
|
||||||
<p class='percent' style='width:{{stats['noop_percent']}}%'>
|
<p class='percent' style='width:{{stats['noop_percent']}}%'>
|
||||||
<span>Pending</span>
|
<span>Noop</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{url_for('node', env=current_env, node_name=report.node)}}">{{ report.node }}</a></td>
|
<td><a href="{{url_for('node', env=current_env, node_name=report.node)}}">{{ report.node }}</a></td>
|
||||||
<td>
|
<td>
|
||||||
{{report.version}}
|
{{report.version|safe}}
|
||||||
</td>
|
</td>
|
||||||
<td rel="utctimestamp">
|
<td rel="utctimestamp">
|
||||||
{{report.start}}
|
{{report.start}}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"<span rel=\"utctimestamp\">{{ report[column.attr] }}</span>"
|
"<span rel=\"utctimestamp\">{{ report[column.attr] }}</span>"
|
||||||
{%- elif column.type == 'status' -%}
|
{%- elif column.type == 'status' -%}
|
||||||
{% filter jsonprint -%}
|
{% 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 %}
|
{%- endfilter %}
|
||||||
{%- elif column.type == 'node' -%}
|
{%- elif column.type == 'node' -%}
|
||||||
{% filter jsonprint %}<a href="{{url_for('node', env=current_env, node_name=report.node)}}">{{ report.node }}</a>{% endfilter %}
|
{% 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 absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import os.path
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -8,8 +9,8 @@ from math import ceil
|
|||||||
from requests.exceptions import HTTPError, ConnectionError
|
from requests.exceptions import HTTPError, ConnectionError
|
||||||
from pypuppetdb.errors import EmptyResponseError
|
from pypuppetdb.errors import EmptyResponseError
|
||||||
|
|
||||||
from flask import abort
|
from flask import abort, request, url_for
|
||||||
|
from jinja2.utils import contextfunction
|
||||||
|
|
||||||
# Python 3 compatibility
|
# Python 3 compatibility
|
||||||
try:
|
try:
|
||||||
@@ -20,6 +21,21 @@ except NameError:
|
|||||||
log = logging.getLogger(__name__)
|
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):
|
def jsonprint(value):
|
||||||
return json.dumps(value, indent=2, separators=(',', ': '))
|
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
|
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
|
-r requirements.txt
|
||||||
coverage==4.0
|
pep8==1.7.0
|
||||||
|
coverage==4.3.4
|
||||||
mock==1.3.0
|
mock==1.3.0
|
||||||
pytest==3.0.1
|
pytest==3.0.7
|
||||||
pytest-pep8==1.0.5
|
pytest-pep8==1.0.6
|
||||||
pytest-cov==2.2.1
|
pytest-cov==2.4.0
|
||||||
pytest-mock==1.5.0
|
pytest-mock==1.5.0
|
||||||
cov-core==1.15.0
|
cov-core==1.15.0
|
||||||
unittest2==1.1.0; python_version < '2.7'
|
unittest2==1.1.0; python_version < '2.7'
|
||||||
bandit
|
|
||||||
beautifulsoup4==4.5.3
|
beautifulsoup4==4.5.3
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
Flask==0.10.1
|
Flask >=0.12
|
||||||
Flask-WTF==0.12
|
Flask-WTF >=0.14.2
|
||||||
Jinja2==2.7.2
|
Jinja2 >=2.9.5
|
||||||
MarkupSafe==0.19
|
MarkupSafe >=0.19
|
||||||
WTForms==2.1
|
WTForms >=2.1
|
||||||
Werkzeug==0.11.10
|
Werkzeug >=0.12.1
|
||||||
itsdangerous==0.23
|
itsdangerous >=0.23
|
||||||
pypuppetdb==0.3.2
|
pypuppetdb >=0.3.3
|
||||||
requests==2.6.0
|
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]
|
[tool:pytest]
|
||||||
addopts = --cov=puppetboard --cov-report=term-missing
|
addopts = --cov=puppetboard --cov-report=term-missing
|
||||||
norecursedirs = docs .tox venv
|
norecursedirs = docs .tox venv .eggs lib
|
||||||
pep8ignore = E402
|
pep8ignore = E402
|
||||||
|
python_files = test/*.py
|
||||||
|
|||||||
49
setup.py
49
setup.py
@@ -1,25 +1,48 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import codecs
|
import codecs
|
||||||
|
import re
|
||||||
|
from setuptools.command.test import test as TestCommand
|
||||||
from setuptools import setup, find_packages
|
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:
|
with codecs.open('README.rst', encoding='utf-8') as f:
|
||||||
README = f.read()
|
README = f.read()
|
||||||
|
|
||||||
with codecs.open('CHANGELOG.rst', encoding='utf-8') as f:
|
with codecs.open('CHANGELOG.rst', encoding='utf-8') as f:
|
||||||
CHANGELOG = f.read()
|
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(
|
setup(
|
||||||
name='puppetboard',
|
name='puppetboard',
|
||||||
version=VERSION,
|
version=__version__,
|
||||||
author='Corey Hammerton',
|
author='Corey Hammerton',
|
||||||
author_email='corey.hammerton@gmail.com',
|
author_email='corey.hammerton@gmail.com',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
@@ -29,13 +52,11 @@ setup(
|
|||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
long_description='\n'.join((README, CHANGELOG)),
|
long_description='\n'.join((README, CHANGELOG)),
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
install_requires=[
|
install_requires=requirements,
|
||||||
"Flask >= 0.10.1",
|
tests_require=requirements_test,
|
||||||
"Flask-WTF >= 0.12, <= 0.13",
|
extras_require={'test': requirements_test},
|
||||||
"WTForms >= 2.0, < 3.0",
|
|
||||||
"pypuppetdb >= 0.3.0, < 0.4.0",
|
|
||||||
],
|
|
||||||
keywords="puppet puppetdb puppetboard",
|
keywords="puppet puppetdb puppetboard",
|
||||||
|
cmdclass={'test': PyTest},
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 3 - Alpha',
|
'Development Status :: 3 - Alpha',
|
||||||
'Environment :: Web Environment',
|
'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',
|
catalog_timestamp='2013-08-01T09:57:00.000Z',
|
||||||
facts_timestamp='2013-08-01T09:57:00.000Z',
|
facts_timestamp='2013-08-01T09:57:00.000Z',
|
||||||
status_report='unchanged'),
|
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 mocker.patch.object(app.puppetdb, 'nodes',
|
||||||
return_value=iter(node_list))
|
return_value=iter(node_list))
|
||||||
@@ -273,6 +266,9 @@ def test_offline_mode(client, mocker):
|
|||||||
assert soup.title.contents[0] == 'Puppetboard'
|
assert soup.title.contents[0] == 'Puppetboard'
|
||||||
for link in soup.find_all('link'):
|
for link in soup.find_all('link'):
|
||||||
assert "//" not in link['href']
|
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'):
|
for script in soup.find_all('script'):
|
||||||
if "src" in script.attrs:
|
if "src" in script.attrs:
|
||||||
@@ -443,7 +439,6 @@ def test_radiator_view_json(client, mocker,
|
|||||||
assert json_data['noop'] == 1
|
assert json_data['noop'] == 1
|
||||||
assert json_data['failed'] == 1
|
assert json_data['failed'] == 1
|
||||||
assert json_data['changed'] == 1
|
assert json_data['changed'] == 1
|
||||||
assert json_data['skipped'] == 1
|
|
||||||
assert json_data['unchanged'] == 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)
|
dbquery = MockDbQuery(query_data)
|
||||||
|
|
||||||
mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get)
|
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
|
cur_day = next_day
|
||||||
|
|
||||||
assert rv.status_code == 200
|
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
|
import pytest
|
||||||
from flask import Flask, current_app
|
from flask import Flask, current_app
|
||||||
|
from werkzeug.exceptions import InternalServerError
|
||||||
from puppetboard import app
|
from puppetboard import app
|
||||||
|
from puppetboard.errors import (bad_request, forbidden, not_found,
|
||||||
|
precond_failed, server_error)
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
@@ -16,15 +18,17 @@ def mock_puppetdb_environments(mocker):
|
|||||||
return_value=environemnts)
|
return_value=environemnts)
|
||||||
|
|
||||||
|
|
||||||
def test_error_no_content():
|
@pytest.fixture
|
||||||
result = app.no_content(None)
|
def mock_server_error(mocker):
|
||||||
assert result[0] == ''
|
def raiseInternalServerError():
|
||||||
assert result[1] == 204
|
raise InternalServerError('Hello world')
|
||||||
|
return mocker.patch('puppetboard.core.environments',
|
||||||
|
side_effect=raiseInternalServerError)
|
||||||
|
|
||||||
|
|
||||||
def test_error_bad_request(mock_puppetdb_environments):
|
def test_error_bad_request(mock_puppetdb_environments):
|
||||||
with app.app.test_request_context():
|
with app.app.test_request_context():
|
||||||
(output, error_code) = app.bad_request(None)
|
(output, error_code) = bad_request(None)
|
||||||
soup = BeautifulSoup(output, 'html.parser')
|
soup = BeautifulSoup(output, 'html.parser')
|
||||||
|
|
||||||
assert 'The request sent to PuppetDB was invalid' in soup.p.text
|
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):
|
def test_error_forbidden(mock_puppetdb_environments):
|
||||||
with app.app.test_request_context():
|
with app.app.test_request_context():
|
||||||
(output, error_code) = app.forbidden(None)
|
(output, error_code) = forbidden(None)
|
||||||
soup = BeautifulSoup(output, 'html.parser')
|
soup = BeautifulSoup(output, 'html.parser')
|
||||||
|
|
||||||
long_string = "%s %s" % ('What you were looking for has',
|
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):
|
def test_error_not_found(mock_puppetdb_environments):
|
||||||
with app.app.test_request_context():
|
with app.app.test_request_context():
|
||||||
(output, error_code) = app.not_found(None)
|
(output, error_code) = not_found(None)
|
||||||
soup = BeautifulSoup(output, 'html.parser')
|
soup = BeautifulSoup(output, 'html.parser')
|
||||||
|
|
||||||
long_string = "%s %s" % ('What you were looking for could not',
|
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):
|
def test_error_precond(mock_puppetdb_environments):
|
||||||
with app.app.test_request_context():
|
with app.app.test_request_context():
|
||||||
(output, error_code) = app.precond_failed(None)
|
(output, error_code) = precond_failed(None)
|
||||||
soup = BeautifulSoup(output, 'html.parser')
|
soup = BeautifulSoup(output, 'html.parser')
|
||||||
|
|
||||||
long_string = "%s %s" % ('You\'ve configured Puppetboard with an API',
|
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):
|
def test_error_server(mock_puppetdb_environments):
|
||||||
with app.app.test_request_context():
|
with app.app.test_request_context():
|
||||||
(output, error_code) = app.server_error(None)
|
(output, error_code) = server_error(None)
|
||||||
soup = BeautifulSoup(output, 'html.parser')
|
soup = BeautifulSoup(output, 'html.parser')
|
||||||
|
|
||||||
assert 'Internal Server Error' in soup.h2.text
|
assert 'Internal Server Error' in soup.h2.text
|
||||||
assert error_code == 500
|
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 pytest
|
||||||
import os
|
import os
|
||||||
from puppetboard import docker_settings
|
from puppetboard import docker_settings
|
||||||
from puppetboard import app
|
import puppetboard.core
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import future.utils
|
import future.utils
|
||||||
@@ -100,12 +100,14 @@ def test_graph_facts_custom(cleanUpEnv):
|
|||||||
assert 'extra' in facts
|
assert 'extra' in facts
|
||||||
|
|
||||||
|
|
||||||
def test_bad_log_value(cleanUpEnv):
|
def test_bad_log_value(cleanUpEnv, mocker):
|
||||||
os.environ['LOGLEVEL'] = 'g'
|
os.environ['LOGLEVEL'] = 'g'
|
||||||
os.environ['PUPPETBOARD_SETTINGS'] = '../puppetboard/docker_settings.py'
|
os.environ['PUPPETBOARD_SETTINGS'] = '../puppetboard/docker_settings.py'
|
||||||
reload(docker_settings)
|
reload(docker_settings)
|
||||||
|
|
||||||
|
puppetboard.core.APP = None
|
||||||
with pytest.raises(ValueError) as error:
|
with pytest.raises(ValueError) as error:
|
||||||
reload(app)
|
puppetboard.core.get_app()
|
||||||
|
|
||||||
|
|
||||||
def test_default_table_selctor(cleanUpEnv):
|
def test_default_table_selctor(cleanUpEnv):
|
||||||
@@ -116,3 +118,11 @@ def test_env_table_selector(cleanUpEnv):
|
|||||||
os.environ['TABLE_COUNT_SELECTOR'] = '5,15,25'
|
os.environ['TABLE_COUNT_SELECTOR'] = '5,15,25'
|
||||||
reload(docker_settings)
|
reload(docker_settings)
|
||||||
assert [5, 15, 25] == docker_settings.TABLE_COUNT_SELECTOR
|
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 utils
|
||||||
from puppetboard import app
|
from puppetboard import app
|
||||||
from puppetboard.app import NoContent
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import logging
|
import logging
|
||||||
@@ -108,19 +107,6 @@ def test_http_connection_error(mock_log):
|
|||||||
mock_log.error.assert_called_with(err)
|
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):
|
def test_db_version_good(mocker, mock_info_log):
|
||||||
mocker.patch.object(app.puppetdb, 'current_version', return_value='4.2.0')
|
mocker.patch.object(app.puppetdb, 'current_version', return_value='4.2.0')
|
||||||
err = 'PuppetDB Version %d.%d.%d' % (4, 2, 0)
|
err = 'PuppetDB Version %d.%d.%d' % (4, 2, 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user