Merge pull request #176 from corey-hammerton/0.1.0
puppetboard: Adding PuppetDB 3.x support Some of the implementations here is: - Environment Awareness with a dropdown menu to switch environments - First implementation of pagination in the form of a macro for flexibility - Use of the PuppetDB 3.x APIs, therefore PuppetDB 2.x compatibility is broken
This commit is contained in:
@@ -4,6 +4,37 @@ Changelog
|
||||
|
||||
This is the changelog for Puppetboard.
|
||||
|
||||
0.1.0
|
||||
====
|
||||
|
||||
* Requires pypuppetdb >= 0.2.0
|
||||
* Full support for PuppetDB 3.x
|
||||
* The first directory location is now a Puppet environment which is filtered
|
||||
on all supported queries. Users can browse different environments with a
|
||||
select field in the top NavBar
|
||||
* Using limit, order_by and offset parameters adding pagaination on the Reports
|
||||
page (available in the NavBar). Functionality is available to pages that
|
||||
accept a page attribute.
|
||||
* The report page now directly queries pypuppetdb to match the report_id
|
||||
value with the report hash or configuration_version fields.
|
||||
* Catching and aborting with a 404 if the report and report_latest function
|
||||
queries do not return a generator object.
|
||||
* Adding a Catalogs page (similar to the Nodes page) with a form to compare
|
||||
one node's catalog information with that of another node.
|
||||
* Updating the Query Endpoints for the Query page.
|
||||
* Adding to ``templates/_macros.html`` status_counts that shows node/report
|
||||
status information, like what is avaiable on the index and nodes pages,
|
||||
available to the reports pages and tables also.
|
||||
* Showing report logs and metrics in the report page.
|
||||
* Removing ``limit_reports`` from ``utils.py`` because this helper function
|
||||
has been replaced by the limit PuppetDB paging function.
|
||||
|
||||
**Known Issues**
|
||||
|
||||
* fact_value pages rendered from JSON valued facts return no results. A more
|
||||
sophisticated API is required to make use of JSON valued facts (through the
|
||||
factsets, fact-paths and/or fact-contents endpoints for example)
|
||||
|
||||
0.0.5
|
||||
=====
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ try:
|
||||
except ImportError:
|
||||
from urllib.parse import unquote
|
||||
from datetime import datetime
|
||||
from itertools import tee
|
||||
|
||||
from flask import (
|
||||
Flask, render_template, abort, url_for,
|
||||
@@ -21,7 +22,7 @@ from pypuppetdb import connect
|
||||
from puppetboard.forms import (CatalogForm, QueryForm)
|
||||
from puppetboard.utils import (
|
||||
get_or_abort, yield_or_stop,
|
||||
limit_reports, jsonprint
|
||||
jsonprint, Pagination
|
||||
)
|
||||
|
||||
|
||||
@@ -58,6 +59,33 @@ def stream_template(template_name, **context):
|
||||
rv.enable_buffering(5)
|
||||
return rv
|
||||
|
||||
def url_for_pagination(page):
|
||||
args = request.view_args.copy()
|
||||
args['page'] = page
|
||||
return url_for(request.endpoint, **args)
|
||||
|
||||
def url_for_environments(env):
|
||||
args = request.view_args.copy()
|
||||
args['env'] = env
|
||||
return url_for(request.endpoint, **args)
|
||||
|
||||
def environments():
|
||||
envs = get_or_abort(puppetdb.environments)
|
||||
x = []
|
||||
|
||||
for env in envs:
|
||||
x.append(env['name'])
|
||||
|
||||
return x
|
||||
|
||||
def check_env(env):
|
||||
if env not in envs:
|
||||
abort(404)
|
||||
|
||||
app.jinja_env.globals['url_for_pagination'] = url_for_pagination
|
||||
app.jinja_env.globals['url_for_environments'] = url_for_environments
|
||||
|
||||
envs = environments()
|
||||
|
||||
@app.context_processor
|
||||
def utility_processor():
|
||||
@@ -69,36 +97,42 @@ def utility_processor():
|
||||
|
||||
@app.errorhandler(400)
|
||||
def bad_request(e):
|
||||
return render_template('400.html'), 400
|
||||
return render_template('400.html', envs=envs), 400
|
||||
|
||||
|
||||
@app.errorhandler(403)
|
||||
def forbidden(e):
|
||||
return render_template('403.html'), 400
|
||||
return render_template('403.html', envs=envs), 400
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
return render_template('404.html'), 404
|
||||
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."""
|
||||
return render_template('412.html'), 412
|
||||
return render_template('412.html', envs=envs), 412
|
||||
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(e):
|
||||
return render_template('500.html'), 500
|
||||
return render_template('500.html', envs=envs), 500
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
@app.route('/', defaults={'env': 'production'})
|
||||
@app.route('/<env>/')
|
||||
def index(env):
|
||||
"""This view generates the index page and displays a set of metrics and
|
||||
latest reports on nodes fetched from PuppetDB.
|
||||
|
||||
:param env: Search for nodes in this (Catalog and Fact) environment
|
||||
:type env: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
# TODO: Would be great if we could parallelize this somehow, doing these
|
||||
# requests in sequence is rather pointless.
|
||||
prefix = 'puppetlabs.puppetdb.query.population'
|
||||
@@ -117,7 +151,10 @@ def index():
|
||||
'avg_resources_node': "{0:10.0f}".format(avg_resources_node['Value']),
|
||||
}
|
||||
|
||||
nodes = puppetdb.nodes(
|
||||
nodes = get_or_abort(puppetdb.nodes,
|
||||
query='["and", {0}]'.format(
|
||||
", ".join('["=", "{0}", "{1}"]'.format(field, env)
|
||||
for field in ['catalog_environment', 'facts_environment'])),
|
||||
unreported=app.config['UNRESPONSIVE_HOURS'],
|
||||
with_status=True)
|
||||
|
||||
@@ -149,12 +186,15 @@ def index():
|
||||
'index.html',
|
||||
metrics=metrics,
|
||||
nodes=nodes_overview,
|
||||
stats=stats
|
||||
stats=stats,
|
||||
envs=envs,
|
||||
current_env=env
|
||||
)
|
||||
|
||||
|
||||
@app.route('/nodes')
|
||||
def nodes():
|
||||
@app.route('/nodes', defaults={'env': 'production'})
|
||||
@app.route('/<env>/nodes')
|
||||
def nodes(env):
|
||||
"""Fetch all (active) nodes from PuppetDB and stream a table displaying
|
||||
those nodes.
|
||||
|
||||
@@ -163,9 +203,17 @@ def nodes():
|
||||
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 nodes in this (Catalog and Fact) environment
|
||||
:type env: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
status_arg = request.args.get('status', '')
|
||||
nodelist = puppetdb.nodes(
|
||||
query='["and", {0}]'.format(
|
||||
", ".join('["=", "{0}", "{1}"]'.format(field, env)
|
||||
for field in ['catalog_environment', 'facts_environment'])),
|
||||
unreported=app.config['UNRESPONSIVE_HOURS'],
|
||||
with_status=True)
|
||||
nodes = []
|
||||
@@ -176,11 +224,15 @@ def nodes():
|
||||
else:
|
||||
nodes.append(node)
|
||||
return Response(stream_with_context(
|
||||
stream_template('nodes.html', nodes=nodes)))
|
||||
stream_template('nodes.html',
|
||||
nodes=nodes,
|
||||
envs=envs,
|
||||
current_env=env)))
|
||||
|
||||
|
||||
@app.route('/inventory')
|
||||
def inventory():
|
||||
@app.route('/inventory', defaults={'env': 'production'})
|
||||
@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.
|
||||
|
||||
@@ -189,7 +241,11 @@ def inventory():
|
||||
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`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
fact_desc = [] # a list of fact descriptions to go
|
||||
# in the table header
|
||||
@@ -216,7 +272,8 @@ def inventory():
|
||||
fact_desc.append(description)
|
||||
fact_names.append(name)
|
||||
|
||||
query = '["or", {0}]'.format(
|
||||
query = '["and", ["=", "environment", "{0}"], ["or", {1}]]'.format(
|
||||
env,
|
||||
', '.join('["=", "name", "{0}"]'.format(name)
|
||||
for name in fact_names))
|
||||
|
||||
@@ -238,92 +295,252 @@ def inventory():
|
||||
nodedata[node].append("undef")
|
||||
|
||||
return Response(stream_with_context(
|
||||
stream_template('inventory.html', nodedata=nodedata, fact_desc=fact_desc)))
|
||||
stream_template('inventory.html',
|
||||
nodedata=nodedata,
|
||||
fact_desc=fact_desc,
|
||||
envs=envs,
|
||||
current_env=env)))
|
||||
|
||||
|
||||
@app.route('/node/<node_name>')
|
||||
def node(node_name):
|
||||
@app.route('/node/<node_name>', defaults={'env': 'production'})
|
||||
@app.route('/<env>/node/<node_name>')
|
||||
def node(env, node_name):
|
||||
"""Display a dashboard for a node showing as much data as we have on that
|
||||
node. This includes facts and reports but not Resources as that is too
|
||||
heavy to do within a single request.
|
||||
|
||||
:param env: Ensure that the node, facts and reports are in this environment
|
||||
:type env: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
node = get_or_abort(puppetdb.node, node_name)
|
||||
facts = node.facts()
|
||||
reports = limit_reports(node.reports(), app.config['REPORTS_COUNT'])
|
||||
reports = get_or_abort(puppetdb.reports,
|
||||
query='["and", ["=", "environment", "{0}"],' \
|
||||
'["=", "certname", "{1}"]]'.format(env, node_name),
|
||||
limit=app.config['REPORTS_COUNT'],
|
||||
order_by='[{"field": "start_time", "order": "desc"}]')
|
||||
reports, reports_events = tee(reports)
|
||||
report_event_counts = {}
|
||||
|
||||
for report in reports_events:
|
||||
counts = get_or_abort(puppetdb.event_counts,
|
||||
query='["and", ["=", "environment", "{0}"],' \
|
||||
'["=", "certname", "{1}"], ["=", "report", "{2}"]]'.format(
|
||||
env,
|
||||
node_name,
|
||||
report.hash_),
|
||||
summarize_by="certname")
|
||||
try:
|
||||
report_event_counts[report.hash_] = counts[0]
|
||||
except IndexError:
|
||||
report_event_counts[report.hash_] = {}
|
||||
return render_template(
|
||||
'node.html',
|
||||
node=node,
|
||||
facts=yield_or_stop(facts),
|
||||
reports=yield_or_stop(reports),
|
||||
reports_count=app.config['REPORTS_COUNT'])
|
||||
reports_count=app.config['REPORTS_COUNT'],
|
||||
report_event_counts=report_event_counts,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
|
||||
|
||||
@app.route('/reports')
|
||||
def reports():
|
||||
"""Doesn't do much yet but is meant to show something like the reports of
|
||||
the last half our, something like that."""
|
||||
return render_template('reports.html')
|
||||
@app.route('/reports/', defaults={'env': 'production', 'page': 1})
|
||||
@app.route('/<env>/reports/', defaults={'page': 1})
|
||||
@app.route('/<env>/reports/page/<int:page>')
|
||||
def reports(env, page):
|
||||
"""Displays a list of reports and status from all nodes, retreived using the
|
||||
reports endpoint, sorted by start_time.
|
||||
|
||||
|
||||
@app.route('/reports/<node_name>')
|
||||
def reports_node(node_name):
|
||||
"""Fetches all reports for a node and processes them eventually rendering
|
||||
a table displaying those reports."""
|
||||
reports = limit_reports(
|
||||
yield_or_stop(
|
||||
puppetdb.reports(query='["=", "certname", "{0}"]'.format(node_name))),
|
||||
app.config['REPORTS_COUNT'])
|
||||
return render_template(
|
||||
'reports_node.html',
|
||||
reports=reports,
|
||||
nodename=node_name,
|
||||
reports_count=app.config['REPORTS_COUNT'])
|
||||
|
||||
|
||||
@app.route('/report/latest/<node_name>')
|
||||
def report_latest(node_name):
|
||||
"""Redirect to the latest report of a given node. This is a workaround
|
||||
as long as PuppetDB can't filter reports for latest-report? field. This
|
||||
feature has been requested: https://tickets.puppetlabs.com/browse/PDB-203
|
||||
:param env: Search for all reports in this environment
|
||||
:type env: :obj:`string`
|
||||
:param page: Calculates the offset of the query based on the report count
|
||||
and this value
|
||||
:type page: :obj:`int`
|
||||
"""
|
||||
reports = get_or_abort(puppetdb._query, 'reports',
|
||||
query='["=","certname","{0}"]'.format(node_name),
|
||||
limit=1)
|
||||
if len(reports) > 0:
|
||||
report = reports[0]['hash']
|
||||
return redirect(
|
||||
url_for('report', node_name=node_name, report_id=report))
|
||||
else:
|
||||
check_env(env)
|
||||
|
||||
reports = get_or_abort(puppetdb.reports,
|
||||
query='["=", "environment", "{0}"]'.format(env),
|
||||
limit=app.config['REPORTS_COUNT'],
|
||||
offset=(page-1) * app.config['REPORTS_COUNT'],
|
||||
order_by='[{"field": "start_time", "order": "desc"}]')
|
||||
total = get_or_abort(puppetdb._query,
|
||||
'reports',
|
||||
query='["extract", [["function", "count"]],'\
|
||||
'["and", ["=", "environment", "{0}"]]]'.format(
|
||||
env))
|
||||
total = total[0]['count']
|
||||
reports, reports_events = tee(reports)
|
||||
report_event_counts = {}
|
||||
|
||||
if total == 0 and page != 1:
|
||||
abort(404)
|
||||
|
||||
for report in reports_events:
|
||||
counts = get_or_abort(puppetdb.event_counts,
|
||||
query='["and",' \
|
||||
'["=", "environment", "{0}"],' \
|
||||
'["=", "certname", "{1}"],' \
|
||||
'["=", "report", "{2}"]]'.format(
|
||||
env,
|
||||
report.node,
|
||||
report.hash_),
|
||||
summarize_by="certname")
|
||||
try:
|
||||
report_event_counts[report.hash_] = counts[0]
|
||||
except IndexError:
|
||||
report_event_counts[report.hash_] = {}
|
||||
return Response(stream_with_context(stream_template(
|
||||
'reports.html',
|
||||
reports=yield_or_stop(reports),
|
||||
reports_count=app.config['REPORTS_COUNT'],
|
||||
report_event_counts=report_event_counts,
|
||||
pagination=Pagination(page, app.config['REPORTS_COUNT'], total),
|
||||
envs=envs,
|
||||
current_env=env)))
|
||||
|
||||
@app.route('/report/<node_name>/<report_id>')
|
||||
def report(node_name, report_id):
|
||||
|
||||
@app.route('/reports/<node_name>/', defaults={'env': 'production', 'page': 1})
|
||||
@app.route('/<env>/reports/<node_name>', defaults={'page': 1})
|
||||
@app.route('/<env>/reports/<node_name>/page/<int:page>')
|
||||
def reports_node(env, node_name, page):
|
||||
"""Fetches all reports for a node and processes them eventually rendering
|
||||
a table displaying those reports.
|
||||
|
||||
:param env: Search for reports in this environment
|
||||
:type env: :obj:`string`
|
||||
:param node_name: Find the reports whose certname match this value
|
||||
:type node_name: :obj:`string`
|
||||
:param page: Calculates the offset of the query based on the report count
|
||||
and this value
|
||||
:type page: :obj:`int`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
reports = get_or_abort(puppetdb.reports,
|
||||
query='["and",' \
|
||||
'["=", "environment", "{0}"],' \
|
||||
'["=", "certname", "{1}"]]'.format(env, node_name),
|
||||
limit=app.config['REPORTS_COUNT'],
|
||||
offset=(page-1) * app.config['REPORTS_COUNT'],
|
||||
order_by='[{"field": "start_time", "order": "desc"}]')
|
||||
total = get_or_abort(puppetdb._query,
|
||||
'reports',
|
||||
query='["extract", [["function", "count"]],' \
|
||||
'["and", ["=", "environment", "{0}"], ["=", "certname", "{1}"]]]'.format(
|
||||
env,
|
||||
node_name))
|
||||
total = total[0]['count']
|
||||
reports, reports_events = tee(reports)
|
||||
report_event_counts = {}
|
||||
|
||||
if total == 0 and page != 1:
|
||||
abort(404)
|
||||
|
||||
for report in reports_events:
|
||||
counts = get_or_abort(puppetdb.event_counts,
|
||||
query='["and",' \
|
||||
'["=", "environment", "{0}"],' \
|
||||
'["=", "certname", "{1}"],' \
|
||||
'["=", "report", "{2}"]]'.format(env, report.node, report.hash_),
|
||||
summarize_by="certname")
|
||||
try:
|
||||
report_event_counts[report.hash_] = counts[0]
|
||||
except IndexError:
|
||||
report_event_counts[report.hash_] = {}
|
||||
return render_template(
|
||||
'reports.html',
|
||||
reports=reports,
|
||||
reports_count=app.config['REPORTS_COUNT'],
|
||||
report_event_counts=report_event_counts,
|
||||
pagination=Pagination(page, app.config['REPORTS_COUNT'], total),
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
|
||||
|
||||
@app.route('/report/latest/<node_name>', defaults={'env': 'production'})
|
||||
@app.route('/<env>/report/latest/<node_name>')
|
||||
def report_latest(env, node_name):
|
||||
"""Redirect to the latest report of a given node.
|
||||
|
||||
:param env: Search for reports in this environment
|
||||
:type env: :obj:`string`
|
||||
:param node_name: Find the reports whose certname match this value
|
||||
:type node_name: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
reports = get_or_abort(puppetdb.reports,
|
||||
query='["and",' \
|
||||
'["=", "environment", "{0}"],' \
|
||||
'["=", "certname", "{1}"],' \
|
||||
'["=", "latest_report?", true]]'.format(
|
||||
env,
|
||||
node_name))
|
||||
try:
|
||||
report = next(reports)
|
||||
except StopIteration:
|
||||
abort(404)
|
||||
|
||||
return redirect(
|
||||
url_for('report', env=env, node_name=node_name, report_id=report.hash_))
|
||||
|
||||
|
||||
@app.route('/report/<node_name>/<report_id>', defaults={'env': 'production'})
|
||||
@app.route('/<env>/report/<node_name>/<report_id>')
|
||||
def report(env, node_name, report_id):
|
||||
"""Displays a single report including all the events associated with that
|
||||
report and their status.
|
||||
|
||||
The report_id may be the puppetdb's report hash or the
|
||||
configuration_version. This allows for better integration
|
||||
into puppet-hipchat.
|
||||
"""
|
||||
reports = puppetdb.reports(query='["=", "certname", "{0}"]'.format(node_name))
|
||||
|
||||
for report in reports:
|
||||
if report.hash_ == report_id or report.version == report_id:
|
||||
events = puppetdb.events(query='["=", "report", "{0}"]'.format(
|
||||
report.hash_))
|
||||
return render_template(
|
||||
'report.html',
|
||||
report=report,
|
||||
events=yield_or_stop(events))
|
||||
else:
|
||||
:param env: Search for reports in this environment
|
||||
:type env: :obj:`string`
|
||||
:param node_name: Find the reports whose certname match this value
|
||||
:type node_name: :obj:`string`
|
||||
:param report_id: The hash or the configuration_version of the desired
|
||||
report
|
||||
:type report_id: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
query = '["and", ["=", "environment", "{0}"], ["=", "certname", "{1}"],' \
|
||||
'["or", ["=", "hash", "{2}"], ["=", "configuration_version", "{2}"]]]'.format(
|
||||
env, node_name, report_id)
|
||||
reports = puppetdb.reports(query=query)
|
||||
|
||||
try:
|
||||
report = next(reports)
|
||||
except StopIteration:
|
||||
abort(404)
|
||||
|
||||
return render_template(
|
||||
'report.html',
|
||||
report=report,
|
||||
events=yield_or_stop(report.events()),
|
||||
logs=report.logs,
|
||||
metrics=report.metrics,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
|
||||
@app.route('/facts')
|
||||
def facts():
|
||||
|
||||
@app.route('/facts', defaults={'env': 'production'})
|
||||
@app.route('/<env>/facts')
|
||||
def facts(env):
|
||||
"""Displays an alphabetical list of all facts currently known to
|
||||
PuppetDB."""
|
||||
PuppetDB.
|
||||
|
||||
:param env: Serves no purpose for this function, only for consistency's
|
||||
sake
|
||||
:type env: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
facts_dict = collections.defaultdict(list)
|
||||
facts = get_or_abort(puppetdb.fact_names)
|
||||
for fact in facts:
|
||||
@@ -333,45 +550,85 @@ def facts():
|
||||
facts_dict[letter] = letter_list
|
||||
|
||||
sorted_facts_dict = sorted(facts_dict.items())
|
||||
return render_template('facts.html', facts_dict=sorted_facts_dict)
|
||||
return render_template('facts.html',
|
||||
facts_dict=sorted_facts_dict,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
|
||||
|
||||
@app.route('/fact/<fact>')
|
||||
def fact(fact):
|
||||
@app.route('/fact/<fact>', defaults={'env': 'production'})
|
||||
@app.route('/<env>/fact/<fact>')
|
||||
def fact(env, fact):
|
||||
"""Fetches the specific fact from PuppetDB and displays its value per
|
||||
node for which this fact is known."""
|
||||
node for which this fact is known.
|
||||
|
||||
:param env: Searches for facts in this environment
|
||||
:type env: :obj:`string`
|
||||
:param fact: Find all facts with this name
|
||||
:type fact: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
# 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
|
||||
localfacts = [f for f in yield_or_stop(puppetdb.facts(name=fact))]
|
||||
localfacts = [f for f in yield_or_stop(puppetdb.facts(
|
||||
name=fact,
|
||||
query='["=", "environment", "{0}"]'.format(env)))]
|
||||
return Response(stream_with_context(stream_template(
|
||||
'fact.html',
|
||||
name=fact,
|
||||
render_graph=render_graph,
|
||||
facts=localfacts)))
|
||||
facts=localfacts,
|
||||
envs=envs,
|
||||
current_env=env)))
|
||||
|
||||
|
||||
@app.route('/fact/<fact>/<value>')
|
||||
def fact_value(fact, value):
|
||||
"""On asking for fact/value get all nodes with that fact."""
|
||||
facts = get_or_abort(puppetdb.facts, fact, value)
|
||||
@app.route('/fact/<fact>/<value>', defaults={'env': 'production'})
|
||||
@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`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
facts = get_or_abort(puppetdb.facts,
|
||||
name=fact,
|
||||
value=value,
|
||||
query='["=", "environment", "{0}"]'.format(env))
|
||||
localfacts = [f for f in yield_or_stop(facts)]
|
||||
return render_template(
|
||||
'fact.html',
|
||||
name=fact,
|
||||
value=value,
|
||||
facts=localfacts)
|
||||
facts=localfacts,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
|
||||
|
||||
@app.route('/query', methods=('GET', 'POST'))
|
||||
def query():
|
||||
@app.route('/query', methods=('GET', 'POST'), defaults={'env': 'production'})
|
||||
@app.route('/<env>/query', methods=('GET', 'POST'))
|
||||
def query(env):
|
||||
"""Allows to execute raw, user created querries against PuppetDB. This is
|
||||
currently highly experimental and explodes in interesting ways since none
|
||||
of the possible exceptions are being handled just yet. This will return
|
||||
the JSON of the response or a message telling you what whent wrong /
|
||||
why nothing was returned."""
|
||||
why nothing was returned.
|
||||
|
||||
:param env: Serves no purpose for the query data but is required for the
|
||||
select field in the environment block
|
||||
:type env: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
if app.config['ENABLE_QUERY']:
|
||||
form = QueryForm()
|
||||
if form.validate_on_submit():
|
||||
@@ -383,37 +640,77 @@ def query():
|
||||
puppetdb._query,
|
||||
form.endpoints.data,
|
||||
query=query)
|
||||
return render_template('query.html', form=form, result=result)
|
||||
return render_template('query.html', form=form)
|
||||
return render_template('query.html',
|
||||
form=form,
|
||||
result=result,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
return render_template('query.html',
|
||||
form=form,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
else:
|
||||
log.warn('Access to query interface disabled by administrator..')
|
||||
abort(403)
|
||||
|
||||
|
||||
@app.route('/metrics')
|
||||
def metrics():
|
||||
metrics = get_or_abort(puppetdb._query, 'metrics', path='mbeans')
|
||||
@app.route('/metrics', defaults={'env': 'production'})
|
||||
@app.route('/<env>/metrics')
|
||||
def metrics(env):
|
||||
"""Lists all available metrics that PuppetDB is aware of.
|
||||
|
||||
:param env: While this parameter serves no function purpose it is required
|
||||
for the environments template block
|
||||
:type env: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
metrics = get_or_abort(puppetdb._query, 'mbean')
|
||||
for key, value in metrics.items():
|
||||
metrics[key] = value.split('/')[3]
|
||||
return render_template('metrics.html', metrics=sorted(metrics.items()))
|
||||
metrics[key] = value.split('/')[2]
|
||||
return render_template('metrics.html',
|
||||
metrics=sorted(metrics.items()),
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
|
||||
|
||||
@app.route('/metric/<metric>')
|
||||
def metric(metric):
|
||||
@app.route('/metric/<metric>', defaults={'env': 'production'})
|
||||
@app.route('/<env>/metric/<metric>')
|
||||
def metric(env, metric):
|
||||
"""Lists all information about the metric of the given name.
|
||||
|
||||
:param env: While this parameter serves no function purpose it is required
|
||||
for the environments template block
|
||||
:type env: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
name = unquote(metric)
|
||||
metric = puppetdb.metric(metric)
|
||||
return render_template(
|
||||
'metric.html',
|
||||
name=name,
|
||||
metric=sorted(metric.items()))
|
||||
metric=sorted(metric.items()),
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
|
||||
@app.route('/catalogs', defaults={'env': 'production'})
|
||||
@app.route('/<env>/catalogs')
|
||||
def catalogs(env):
|
||||
"""Lists all nodes with a compiled catalog.
|
||||
|
||||
:param env: Find the nodes with this catalog_environment value
|
||||
:type env: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
@app.route('/catalogs')
|
||||
def catalogs():
|
||||
if app.config['ENABLE_CATALOG']:
|
||||
nodenames = []
|
||||
catalog_list = []
|
||||
nodes = get_or_abort(puppetdb.nodes,
|
||||
query='["null?", "catalog_timestamp", false]',
|
||||
query='["and",' \
|
||||
'["=", "catalog_environment", "{0}"],' \
|
||||
'["null?", "catalog_timestamp", false]]'.format(env),
|
||||
with_status=False,
|
||||
order_by='[{"field": "certname", "order": "asc"}]')
|
||||
nodes, temp = tee(nodes)
|
||||
@@ -441,27 +738,48 @@ def catalogs():
|
||||
|
||||
return render_template(
|
||||
'catalogs.html',
|
||||
nodes=catalog_list)
|
||||
else:
|
||||
log.warn('Access to catalogs endpoint disabled by administrator')
|
||||
abort(403)
|
||||
|
||||
@app.route('/catalog/<node_name>')
|
||||
def catalog_node(node_name):
|
||||
"""Fetches from PuppetDB the compiled catalog of a given node."""
|
||||
if app.config['ENABLE_CATALOG']:
|
||||
catalog = puppetdb.catalog(node=node_name)
|
||||
return render_template('catalog.html', catalog=catalog)
|
||||
nodes=catalog_list,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
else:
|
||||
log.warn('Access to catalog interface disabled by administrator')
|
||||
abort(403)
|
||||
|
||||
@app.route('/catalog/submit', methods=['POST'])
|
||||
def catalog_submit():
|
||||
@app.route('/catalog/<node_name>', defaults={'env': 'production'})
|
||||
@app.route('/<env>/catalog/<node_name>')
|
||||
def catalog_node(env, node_name):
|
||||
"""Fetches from PuppetDB the compiled catalog of a given node.
|
||||
|
||||
:param env: Find the catalog with this environment value
|
||||
:type env: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
if app.config['ENABLE_CATALOG']:
|
||||
catalog = get_or_abort(puppetdb.catalog,
|
||||
node=node_name)
|
||||
return render_template('catalog.html',
|
||||
catalog=catalog,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
else:
|
||||
log.warn('Access to catalog interface disabled by administrator')
|
||||
abort(403)
|
||||
|
||||
@app.route('/catalog/submit', methods=['POST'], defaults={'env': 'production'})
|
||||
@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`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
if app.config['ENABLE_CATALOG']:
|
||||
form = CatalogForm(request.form)
|
||||
|
||||
@@ -471,18 +789,25 @@ def catalog_submit():
|
||||
against = form.against.data
|
||||
return redirect(
|
||||
url_for('catalog_compare',
|
||||
env=env,
|
||||
compare=compare,
|
||||
against=against))
|
||||
return redirect(url_for('catalogs'))
|
||||
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>')
|
||||
def catalog_compare(compare, against):
|
||||
@app.route('/catalogs/compare/<compare>...<against>', defaults={'env': 'production'})
|
||||
@app.route('/<env>/catalogs/compare/<compare>...<against>')
|
||||
def catalog_compare(env, compare, against):
|
||||
"""Compares the catalog of one node, parameter compare, with that of
|
||||
with that of another node, parameter against.
|
||||
|
||||
:param env: Ensure that the 2 catalogs are in the same environment
|
||||
:type env: :obj:`string`
|
||||
"""
|
||||
check_env(env)
|
||||
|
||||
if app.config['ENABLE_CATALOG']:
|
||||
compare_cat = get_or_abort(puppetdb.catalog,
|
||||
node=compare)
|
||||
@@ -491,7 +816,9 @@ def catalog_compare(compare, against):
|
||||
|
||||
return render_template('catalog_compare.html',
|
||||
compare=compare_cat,
|
||||
against=against_cat)
|
||||
against=against_cat,
|
||||
envs=envs,
|
||||
current_env=env)
|
||||
else:
|
||||
log.warn('Access to catalog interface disabled by administrator')
|
||||
abort(403)
|
||||
|
||||
@@ -17,9 +17,14 @@ class QueryForm(Form):
|
||||
('nodes', 'Nodes'),
|
||||
('resources', 'Resources'),
|
||||
('facts', 'Facts'),
|
||||
('fact-names', 'Fact Names'),
|
||||
('factsets', 'Fact Sets'),
|
||||
('fact-paths', 'Fact Paths'),
|
||||
('fact-contents', 'Fact Contents'),
|
||||
('reports', 'Reports'),
|
||||
('events', 'Events'),
|
||||
('catalogs', 'Catalogs'),
|
||||
('edges', 'Edges'),
|
||||
('environments', 'Environments'),
|
||||
])
|
||||
|
||||
class CatalogForm(Form):
|
||||
|
||||
@@ -31,7 +31,7 @@ th.tablesorter-headerDesc::after {
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 47.5%;
|
||||
width: 45%;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@
|
||||
sortList: [[0, 0]]
|
||||
})
|
||||
|
||||
$('.reports').tablesorter({
|
||||
sortList: [[0, 0]]
|
||||
})
|
||||
|
||||
$('input.filter-table').parent('div').removeClass('hide');
|
||||
|
||||
$("input.filter-table").on("keyup", function(e) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% macro facts_table(facts, autofocus=False, condensed=False, show_node=False, show_value=True, link_facts=False, margin_top=20, margin_bottom=20) -%}
|
||||
{% macro facts_table(facts, current_env, autofocus=False, condensed=False, show_node=False, show_value=True, link_facts=False, margin_top=20, margin_bottom=20) -%}
|
||||
<div class="ui fluid icon input hide" style="margin-bottom:20px">
|
||||
<input {% if autofocus %} autofocus="autofocus" {% endif %} class="filter-table" placeholder="Type here to filter...">
|
||||
</div>
|
||||
@@ -19,16 +19,24 @@
|
||||
{% for fact in facts %}
|
||||
<tr>
|
||||
{% if show_node %}
|
||||
<td><a href="{{url_for('node', node_name=fact.node)}}">{{fact.node}}</a></td>
|
||||
<td><a href="{{url_for('node', env=current_env, node_name=fact.node)}}">{{fact.node}}</a></td>
|
||||
{% else %}
|
||||
<td><a href="{{url_for('fact', fact=fact.name)}}">{{fact.name}}</a></td>
|
||||
<td><a href="{{url_for('fact', env=current_env, fact=fact.name)}}">{{fact.name}}</a></td>
|
||||
{% endif %}
|
||||
{% if show_value %}
|
||||
<td style="word-wrap:break-word">
|
||||
{% if link_facts %}
|
||||
<a href="{{url_for('fact_value', fact=fact.name, value=fact.value)}}">{{fact.value}}</a>
|
||||
{% 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 %}
|
||||
{{fact.value}}
|
||||
{% if fact.value is mapping %}
|
||||
<pre>{{fact.value|jsonprint}}</pre>
|
||||
{% else %}
|
||||
{{fact.value}}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
@@ -70,30 +78,40 @@
|
||||
</script>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro reports_table(reports, nodename, reports_count, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True) -%}
|
||||
{% macro reports_table(reports, reports_count, report_event_counts, current_env, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True, show_run_col=False, show_full_col=False, show_search_bar=False, searchable=False) -%}
|
||||
{% if show_search_bar %}
|
||||
<div class="ui fluid icon input hide" style="margin-bottom:20px">
|
||||
<input autofocus="autofocus" class="filter-table" placeholder="Type here to filter...">
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="ui info message">
|
||||
|
||||
Only showing the last {{reports_count}} reports.
|
||||
Only showing {{reports_count}} reports sorted by Start Time.
|
||||
|
||||
</div>
|
||||
<table class='ui table basic {% if condensed %}compact{% endif %}'>
|
||||
<table class='ui table basic {% if condensed %}compact{% endif %} report'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Start time</th>
|
||||
<th>Status</th>
|
||||
{% if show_host_col %}
|
||||
<th>Hostname</th>
|
||||
{% endif %}
|
||||
{% if show_run_col %}
|
||||
<th>Run time</th>
|
||||
{% endif %}
|
||||
{% if show_full_col %}
|
||||
<th>Full report</th>
|
||||
{% endif %}
|
||||
{% if show_conf_col %}
|
||||
<th>Configuration version</th>
|
||||
{% endif %}
|
||||
{% if show_agent_col %}
|
||||
<th>Agent version</th>
|
||||
{% endif %}
|
||||
{% if show_host_col %}
|
||||
<th>Hostname</th>
|
||||
{% endif %}
|
||||
<tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody {% if searchable %}class="searchable" {% endif %}>
|
||||
{% for report in reports %}
|
||||
{% if hash_truncate %}
|
||||
{% set rep_hash = "%s…"|format(report.hash_[0:10])|safe %}
|
||||
@@ -105,21 +123,78 @@
|
||||
{% else %}
|
||||
<tr>
|
||||
{% endif %}
|
||||
<td rel="utctimestamp">{{report.start}}</td>
|
||||
<td rel="utctimestamp">{{report.start}}</td>
|
||||
<td>
|
||||
{% call status_counts(status=report.status, node_name=report.node, events=report_event_counts[report.hash_], report_hash=report.hash_, current_env=current_env) %}{% endcall %}
|
||||
</td>
|
||||
{% if show_host_col %}
|
||||
<td><a href="{{url_for('node', env=current_env, node_name=report.node)}}">{{ report.node }}</a></td>
|
||||
{% endif %}
|
||||
{% if show_run_col %}
|
||||
<td>{{report.run_time}}</td>
|
||||
|
||||
<td><a href="{{url_for('report', node_name=nodename, report_id=report.hash_)}}">{{rep_hash}}</a></td>
|
||||
{% endif %}
|
||||
{% if show_full_col %}
|
||||
<td><a href="{{url_for('report', env=current_env, node_name=report.node, report_id=report.hash_)}}">{{rep_hash}}</a></td>
|
||||
{% endif %}
|
||||
{% if show_conf_col %}
|
||||
<td>{{report.version}}</td>
|
||||
{% endif %}
|
||||
{% if show_agent_col %}
|
||||
<td>{{report.agent_version}}</td>
|
||||
{% endif %}
|
||||
{% if show_host_col %}
|
||||
<td><a href="{{url_for('node', node_name=report.node)}}">{{ report.node }}</a></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endmacro %}
|
||||
{% macro status_counts(caller, status, node_name, events, current_env, unreported_time=False, report_hash=False) -%}
|
||||
<a class="ui small status label
|
||||
{% if status == 'failed' -%}
|
||||
red
|
||||
{% elif status == 'changed' -%}
|
||||
green
|
||||
{% elif status == 'unreported' -%}
|
||||
black
|
||||
{% elif status == 'noop' -%}
|
||||
blue
|
||||
{% endif -%}
|
||||
" href="
|
||||
{% if report_hash -%}
|
||||
{{url_for('report', env=current_env, node_name=node_name, report_id=report_hash)}}
|
||||
{% else -%}
|
||||
{{url_for('report_latest', env=current_env, node_name=node_name)}}
|
||||
{% endif -%}
|
||||
">
|
||||
{{status}}
|
||||
</a>
|
||||
{% if status == 'unreported' %}
|
||||
<span class="ui small label status"> {{ unreported_time }} </span>
|
||||
{% else %}
|
||||
{% if events['failures'] %}<span class="ui small count label red">{{events['failures']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
||||
{% if events['successes'] %}<span class="ui small count label green">{{events['successes']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
||||
{% if events['skips'] %}<span class="ui small count label orange">{{events['skips']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
{% macro render_pagination(pagination) -%}
|
||||
<div class="pagination">
|
||||
{% if pagination.has_prev %}
|
||||
<a href="{{url_for_pagination(1)}}">« First</a>
|
||||
<a href="{{url_for_pagination(pagination.page - 1)}}">Prev</a>
|
||||
{% endif %}
|
||||
{% for page in pagination.iter_pages() %}
|
||||
{% if page %}
|
||||
{% if page != pagination.page %}
|
||||
<a href="{{url_for_pagination(page)}}">{{page}}</a>
|
||||
{% else %}
|
||||
<span style="font-weight:bold;">{{page}}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="ellipsis">...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if pagination.has_next %}
|
||||
<a href="{{url_for_pagination(pagination.page + 1)}}">Next</a>
|
||||
<a href="{{url_for_pagination(pagination.pages)}}">Last »</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="{{url_for('node', node_name=catalog.node)}}">{{catalog.node}}</a></td>
|
||||
<td><a href="{{url_for('node', env=current_env, node_name=catalog.node)}}">{{catalog.node}}</a></td>
|
||||
<td>{{catalog.version}}</td>
|
||||
<td>{{catalog.transaction_uuid}}</td>
|
||||
</tr>
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
{% for node in nodes %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><a href="{{url_for('node', node_name=node.name)}}">{{node.name}}</a></td>
|
||||
<td><a rel="utctimestamp" href="{{url_for('catalog_node', node_name=node.name)}}">{{node.catalog_timestamp}}</a></td>
|
||||
<td><a href="{{url_for('node', env=current_env, node_name=node.name)}}">{{node.name}}</a></td>
|
||||
<td><a rel="utctimestamp" href="{{url_for('catalog_node', env=current_env, node_name=node.name)}}">{{node.catalog_timestamp}}</a></td>
|
||||
<td>
|
||||
{% if node.form %}
|
||||
<div class="ui form">
|
||||
<form method="POST" action="{{url_for('catalog_submit')}}">
|
||||
<form method="POST" action="{{url_for('catalog_submit', env=current_env)}}">
|
||||
{{node.form.csrf_token}}
|
||||
<div class="field inline">
|
||||
{{node.form.compare}}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
{{macros.facts_graph(facts, autofocus=True, show_node=True, margin_bottom=10)}}
|
||||
{% endif %}
|
||||
{% if value %}
|
||||
{{macros.facts_table(facts, autofocus=True, show_node=True, show_value=False, margin_bottom=10)}}
|
||||
{{macros.facts_table(facts, current_env=current_env, autofocus=True, show_node=True, show_value=False, margin_bottom=10)}}
|
||||
{% else %}
|
||||
{{macros.facts_table(facts, autofocus=True, show_node=True, link_facts=True, margin_bottom=10)}}
|
||||
{{macros.facts_table(facts, current_env=current_env, autofocus=True, show_node=True, link_facts=True, margin_bottom=10)}}
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<span class='ui label darkblue'>{{key}}</span>
|
||||
<ul class="searchable">
|
||||
{%- for fact in facts_list %}
|
||||
<li><a href="{{url_for('fact', fact=fact)}}">{{fact}}</a></li>
|
||||
<li><a href="{{url_for('fact', env=current_env, fact=fact)}}">{{fact}}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% import '_macros.html' as macros %}
|
||||
{% block content %}
|
||||
<div class="ui vertical grid">
|
||||
<div class="four column row">
|
||||
<div class="column">
|
||||
<a href="nodes?status=failed">
|
||||
<a href="{{url_for('nodes', env=current_env, status='failed')}}">
|
||||
<h1 class="ui red header no-margin-bottom">
|
||||
{{stats['failed']}}
|
||||
<small>{% if stats['failed']== 1 %} node {% else %} nodes {% endif %}</small>
|
||||
@@ -12,7 +13,7 @@
|
||||
<span>with status failed</span>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="nodes?status=noop">
|
||||
<a href="{{url_for('nodes', env=current_env, status='noop')}}">
|
||||
<h1 class="ui header purple no-margin-bottom">
|
||||
{{stats['noop']}}
|
||||
<small>{% if stats['noop']== 1 %} node {% else %} nodes {% endif %}</small>
|
||||
@@ -21,7 +22,7 @@
|
||||
<span>with status pending</span>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="nodes?status=changed">
|
||||
<a href="{{url_for('nodes', env=current_env, status='changed')}}">
|
||||
<h1 class="ui header green no-margin-bottom">
|
||||
{{stats['changed']}}
|
||||
<small>{% if stats['changed']== 1 %} node {% else %} nodes {% endif %}</small>
|
||||
@@ -30,7 +31,7 @@
|
||||
<span>with status changed</span>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="nodes?status=unreported">
|
||||
<a href="{{url_for('nodes', env=current_env, status='unreported')}}">
|
||||
<h1 class="ui header black no-margin-bottom">
|
||||
{{ stats['unreported'] }}
|
||||
<small>{% if stats['unreported']== 1 %} node {% else %} nodes {% endif %}</small>
|
||||
@@ -75,40 +76,21 @@
|
||||
{% if node.status != 'unchanged' %}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="ui small status label
|
||||
{% if node.status == 'failed' %}
|
||||
red
|
||||
{% elif node.status == 'changed' %}
|
||||
green
|
||||
{% elif node.status == 'unreported' %}
|
||||
black
|
||||
{% elif node.status == 'noop' %}
|
||||
blue
|
||||
{% endif %}
|
||||
" href="{{url_for('report_latest', node_name=node.name)}}">
|
||||
{{node.status}}
|
||||
</a>
|
||||
{% if node.status=='unreported'%}
|
||||
<span class="ui small label status"> {{ node.unreported_time }} </span>
|
||||
{% else %}
|
||||
{% if node.events['failures'] %}<span class="ui small count label red">{{node.events['failures']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
||||
{% if node.events['successes'] %}<span class="ui small count label green">{{node.events['successes']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
||||
{% if node.events['skips'] %}<span class="ui small count label yellow">{{node.events['skips']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
||||
{% endif %}
|
||||
{{macros.status_counts(status=node.status, node_name=node.name, events=node.events, unreported_time=node.unreported_time, current_env=current_env)}}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{url_for('node', node_name=node.name)}}">{{ node.name }}</a>
|
||||
<a href="{{url_for('node', env=current_env, node_name=node.name)}}">{{ node.name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if node.report_timestamp %}
|
||||
<a href="{{url_for('report_latest', node_name=node.name)}}" rel='utctimestamp'>{{ node.report_timestamp }}</a>
|
||||
<a href="{{url_for('report_latest', env=current_env, node_name=node.name)}}" rel='utctimestamp'>{{ node.report_timestamp }}</a>
|
||||
{% else %}
|
||||
<i class="large ban circle icon"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if node.report_timestamp %}
|
||||
<a title='Reports' href="{{url_for('reports_node', node_name=node.name)}}"><i class='large darkblue book icon'></i></a>
|
||||
<a title='Reports' href="{{url_for('reports_node', env=current_env, node_name=node.name)}}"><i class='large darkblue book icon'></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="ui fixed darkblue inverted menu">
|
||||
<div class="ui fixed darkblue inverted menu">
|
||||
<div class="title item">
|
||||
Puppetboard
|
||||
</div>
|
||||
@@ -38,10 +38,19 @@
|
||||
('query', 'Query'),
|
||||
] %}
|
||||
<a {% if endpoint == request.endpoint %} class="active item" {% else %} class="item" {% endif %}
|
||||
href="{{ url_for(endpoint) }}">{{ caption }}</a>
|
||||
href="{{ url_for(endpoint, env=current_env) }}">{{ caption }}</a>
|
||||
{%- endfor %}
|
||||
<div class="item" style="float:right"><a href="https://github.com/puppet-community/puppetboard" target="_blank">v0.0.5</a></div>
|
||||
</nav>
|
||||
<div class="ui item dropdown">
|
||||
Environments
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="menu">
|
||||
{% for env in envs %}
|
||||
<a class="item {% if env == current_env %}active{% endif %}" href="{{url_for_environments(env)}}">{{env}}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="item right"><a href="https://github.com/puppet-community/puppetboard" target="_blank">v0.1.0</a></div>
|
||||
</div>
|
||||
<div class="ui grid padding-bottom">
|
||||
<div class="one wide column"></div>
|
||||
<div class="fourteen wide column">
|
||||
@@ -81,6 +90,9 @@
|
||||
<script src="{{ url_for('static', filename='js/lists.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/tables.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/scroll.top.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
$(".ui.dropdown").dropdown();
|
||||
</script>
|
||||
|
||||
{% block script %} {% endblock script %}
|
||||
</body>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h1>Metrics</h1>
|
||||
<ul>
|
||||
{% for key,value in metrics %}
|
||||
<li><a href="{{url_for('metric', metric=value)}}">{{key}}</li>
|
||||
<li><a href="{{url_for('metric', env=current_env, metric=value)}}">{{key}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
</div>
|
||||
<div class='row'>
|
||||
<h1>Reports</h1>
|
||||
{{ macros.reports_table(reports, node.name, reports_count, condensed=True, hash_truncate=True, show_conf_col=False, show_agent_col=False, show_host_col=False)}}
|
||||
{{ macros.reports_table(reports, reports_count, report_event_counts, condensed=True, hash_truncate=True, show_conf_col=False, show_agent_col=False, show_host_col=False, current_env=current_env)}}
|
||||
</div>
|
||||
</div>
|
||||
<div class='column'>
|
||||
<h1>Facts</h1>
|
||||
{{macros.facts_table(facts, link_facts=True, condensed=True)}}
|
||||
{{macros.facts_table(facts, link_facts=True, condensed=True, current_env=current_env)}}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% import '_macros.html' as macros %}
|
||||
{% block content %}
|
||||
<div class="ui fluid icon input hide" style="margin-bottom:20px">
|
||||
<input autofocus="autofocus" class="filter-table" placeholder="Type here to filter...">
|
||||
@@ -17,40 +18,20 @@
|
||||
{% for node in nodes %}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="ui small status label
|
||||
{% if node.status == 'failed' %}
|
||||
red
|
||||
{% elif node.status == 'changed' %}
|
||||
green
|
||||
{% elif node.status == 'unreported' %}
|
||||
black
|
||||
{% elif node.status == 'noop' %}
|
||||
blue
|
||||
{% endif %}
|
||||
" href="{{url_for('report_latest', node_name=node.name)}}">
|
||||
{{node.status}}
|
||||
</a>
|
||||
{% if node.status=='unreported'%}
|
||||
<span class="ui small label status"> {{ node.unreported_time }} </label>
|
||||
{% else %}
|
||||
<span>{% if node.events['failures'] %}<span class="ui small count label red">{{node.events['failures']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
||||
{% if node.events['successes'] %}<span class="ui small count label green">{{node.events['successes']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}</span>
|
||||
{% if node.events['skips'] %}<span class="ui small count label yellow">{{node.events['skips']}}</span>{% else %}<span class="ui small count label">0</span>{% endif%}
|
||||
{% endif %}
|
||||
{{macros.status_counts(status=node.status, node_name=node.name, events=node.events, unreported_time=node.unreported_time, current_env=current_env)}}
|
||||
</td>
|
||||
<td><a href="{{url_for('node', node_name=node.name)}}">{{node.name}}</a></td>
|
||||
<td><a rel="utctimestamp" href="{{url_for('catalog_node', node_name=node.name)}}">{{node.catalog_timestamp}}</a></td>
|
||||
<td><a 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.report_timestamp %}
|
||||
<a href="{{url_for('report_latest', node_name=node.name)}}" rel='utctimestamp'>{{ node.report_timestamp }}</a>
|
||||
<a href="{{url_for('report_latest', env=current_env, node_name=node.name)}}" rel='utctimestamp'>{{ node.report_timestamp }}</a>
|
||||
{% else %}
|
||||
<i class="large ban circle icon"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if node.report_timestamp %}
|
||||
<a title='Reports' href="{{url_for('reports_node', node_name=node.name)}}"><i class='large darkblue book icon'></i></a>
|
||||
<i class='large darkblue trash icon'></i>
|
||||
<a title='Reports' href="{{url_for('reports_node', env=current_env, node_name=node.name, page=1)}}"><i class='large darkblue book icon'></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="ui form">
|
||||
<form method="POST" action="{{ url_for('query') }}">
|
||||
<form method="POST" action="{{ url_for('query', env=current_env) }}">
|
||||
{{ form.csrf_token }}
|
||||
<div class="field {% if form.query.errors %} error {% endif %}">
|
||||
{{ form.query(autofocus="autofocus", rows=5, placeholder="Enter your query: [\"=\", \"certname\", \"hostname\"]. You may omit the opening and closing bracket.") }}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="{{url_for('node', node_name=report.node)}}">{{ report.node }}</a></td>
|
||||
<td><a href="{{url_for('node', env=current_env, node_name=report.node)}}">{{ report.node }}</a></td>
|
||||
<td>
|
||||
{{report.version}}
|
||||
</td>
|
||||
@@ -48,26 +48,62 @@
|
||||
<td>{{event.item['old']}}</td>
|
||||
<td>{{event.item['new']}}</td>
|
||||
</tr>
|
||||
{# <tr>
|
||||
<td class='message' colspan='4'>
|
||||
<div id='message-event-{{loop.index}}'>
|
||||
{{event.item['message']}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>#}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h1>Logs</h1>
|
||||
<table class="ui basic table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Source</th>
|
||||
<th>Tags</th>
|
||||
<th>Message</th>
|
||||
<th>Location</th>
|
||||
<tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
{% if log.level == 'info' or log.level == 'notice' %}
|
||||
<tr class='positive'>
|
||||
{% elif log.level == 'warning' %}
|
||||
<tr class='warning'>
|
||||
{% else %}
|
||||
<tr class='error'>
|
||||
{% endif %}
|
||||
<td rel="utctimestamp">{{log.time}}</td>
|
||||
<td>{{log.source}}</td>
|
||||
<td>{{log.tags|join(', ')}}</td>
|
||||
<td>{{log.message}}</td>
|
||||
{% if log.file != None and log.line != None %}
|
||||
<td>{{log.file}}:{{log.line}}</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h1>Metrics</h1>
|
||||
<table class="ui basic table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for metric in metrics %}
|
||||
<tr>
|
||||
<td>{{metric.category}}</td>
|
||||
<td>{{metric.name}}</td>
|
||||
<td>{{metric.value|round(2)}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock content %}
|
||||
{% block script %}
|
||||
<script type='text/javascript'>
|
||||
jQuery(function ($) {
|
||||
$("[rel=tooltip]").tooltip();
|
||||
$(".event").click(function() {
|
||||
$("#message-" + this.id).slideToggle(200);
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock script %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% import '_macros.html' as macros %}
|
||||
{% block content %}
|
||||
<div class="ui warning message">
|
||||
Pending <a href="https://tickets.puppetlabs.com/browse/PDB-201">#PDB-201</a>. You can access reports for a node or individual reports through the <a href="{{url_for('nodes')}}">Nodes</a> tab.
|
||||
</div>
|
||||
{{ macros.reports_table(reports, reports_count, report_event_counts, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True, show_search_bar=True, searchable=True)}}
|
||||
{{ macros.render_pagination(pagination)}}
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% import '_macros.html' as macros %}
|
||||
{% block content %}
|
||||
{{ macros.reports_table(reports, nodename, reports_count, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True)}}
|
||||
{% endblock content %}
|
||||
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
from math import ceil
|
||||
from requests.exceptions import HTTPError, ConnectionError
|
||||
from pypuppetdb.errors import EmptyResponseError
|
||||
|
||||
@@ -29,17 +30,6 @@ def get_or_abort(func, *args, **kwargs):
|
||||
abort(204)
|
||||
|
||||
|
||||
def limit_reports(reports, limit):
|
||||
"""Helper to yield a number of from the reports generator.
|
||||
|
||||
This is an ugly solution at best...
|
||||
"""
|
||||
for count, report in enumerate(reports):
|
||||
if count == limit:
|
||||
raise StopIteration
|
||||
yield report
|
||||
|
||||
|
||||
def yield_or_stop(generator):
|
||||
"""Similar in intent to get_or_abort this helper will iterate over our
|
||||
generators and handle certain errors.
|
||||
@@ -54,3 +44,35 @@ def yield_or_stop(generator):
|
||||
raise
|
||||
except (EmptyResponseError, ConnectionError, HTTPError):
|
||||
raise StopIteration
|
||||
|
||||
class Pagination(object):
|
||||
|
||||
def __init__(self, page, per_page, total_count):
|
||||
self.page = page
|
||||
self.per_page = per_page
|
||||
self.total_count = total_count
|
||||
|
||||
@property
|
||||
def pages(self):
|
||||
return int(ceil(self.total_count / float(self.per_page)))
|
||||
|
||||
@property
|
||||
def has_prev(self):
|
||||
return self.page > 1
|
||||
|
||||
@property
|
||||
def has_next(self):
|
||||
return self.page < self.pages
|
||||
|
||||
def iter_pages(self, left_edge=2, left_current=2,
|
||||
right_current=5, right_edge=2):
|
||||
last = 0
|
||||
for num in xrange(1, self.pages + 1):
|
||||
if num <= left_edge or \
|
||||
(num > self.page - left_current - 1 and \
|
||||
num < self.page + right_current) or \
|
||||
num > self.pages - right_edge:
|
||||
if last + 1 != num:
|
||||
yield None
|
||||
yield num
|
||||
last = num
|
||||
|
||||
4
setup.py
4
setup.py
@@ -9,7 +9,7 @@ if sys.argv[-1] == 'publish':
|
||||
os.system('python setup.py sdist upload')
|
||||
sys.exit()
|
||||
|
||||
VERSION = "0.0.5"
|
||||
VERSION = "0.1.0"
|
||||
|
||||
with codecs.open('README.rst', encoding='utf-8') as f:
|
||||
README = f.read()
|
||||
@@ -23,7 +23,7 @@ setup(
|
||||
author='Daniele Sluijters',
|
||||
author_email='daniele.sluijters+pypi@gmail.com',
|
||||
packages=find_packages(),
|
||||
url='https://github.com/nedap/puppetboard',
|
||||
url='https://github.com/puppet-community/puppetboard',
|
||||
license='Apache License 2.0',
|
||||
description='Web frontend for PuppetDB',
|
||||
include_package_data=True,
|
||||
|
||||
Reference in New Issue
Block a user