Merge branch 'master' into facts

This commit is contained in:
Mike Terzo
2017-06-10 03:40:13 -04:00
committed by GitHub
14 changed files with 161 additions and 75 deletions

11
hooks/pre_build Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
version=$(git describe HEAD --abbrev=4)
cat << EOF > puppetboard/version.py
#
# Puppetboard version module
#
__version__ = '${version}'
EOF

View File

@@ -27,6 +27,7 @@ from puppetboard.utils import (
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 . import __version__ from . import __version__
@@ -322,29 +323,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:
@@ -362,33 +345,65 @@ 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>',
@@ -529,26 +544,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
@@ -559,7 +565,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])
@@ -605,6 +611,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,

View File

@@ -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

View File

@@ -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

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -9,6 +9,29 @@
{% 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';

View File

@@ -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 %}

View 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 -%}
]
}

View File

@@ -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}}

View File

@@ -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 %}

View File

@@ -7,3 +7,4 @@ Werkzeug >=0.12.1
itsdangerous >=0.23 itsdangerous >=0.23
pypuppetdb >=0.3.2 pypuppetdb >=0.3.2
requests >=2.13.0 requests >=2.13.0
CommonMark==0.7.2

View File

@@ -116,3 +116,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