diff --git a/hooks/pre_build b/hooks/pre_build new file mode 100755 index 0000000..8c914a4 --- /dev/null +++ b/hooks/pre_build @@ -0,0 +1,11 @@ +#!/bin/bash + +version=$(git describe HEAD --abbrev=4) + +cat << EOF > puppetboard/version.py +# +# Puppetboard version module +# +__version__ = '${version}' +EOF + diff --git a/puppetboard/app.py b/puppetboard/app.py index f1c0363..52dc51e 100644 --- a/puppetboard/app.py +++ b/puppetboard/app.py @@ -27,6 +27,7 @@ from puppetboard.utils import ( from puppetboard.dailychart import get_daily_reports_chart import werkzeug.exceptions as ex +import CommonMark from . import __version__ @@ -322,29 +323,11 @@ def nodes(env): current_env=env))) -@app.route('/inventory', defaults={'env': app.config['DEFAULT_ENVIRONMENT']}) -@app.route('//inventory') -def inventory(env): - """Fetch all (active) nodes from PuppetDB and stream a table displaying - those nodes along with a set of facts about them. - - Downside of the streaming aproach is that since we've already sent our - headers we can't abort the request if we detect an error. Because of this - we'll end up with an empty table instead because of how yield_or_stop - works. Once pagination is in place we can change this but we'll need to - provide a search feature instead. - - :param env: Search for facts in this environment - :type env: :obj:`string` - """ - envs = environments() - check_env(env, envs) - - headers = [] # a list of fact descriptions to go - # in the table header - fact_names = [] # a list of inventory fact names - fact_data = {} # a multidimensional dict for node and - # fact data +def inventory_facts(): + # a list of facts descriptions to go in table header + headers = [] + # a list of inventory fact names + fact_names = [] # load the list of items/facts we want in our inventory try: @@ -362,33 +345,65 @@ def inventory(env): headers.append(desc) fact_names.append(name) + return headers, fact_names + + +@app.route('/inventory', defaults={'env': app.config['DEFAULT_ENVIRONMENT']}) +@app.route('//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('//inventory/json') +def inventory_ajax(env): + """Backend endpoint for inventory table""" + draw = int(request.args.get('draw', 0)) + + envs = environments() + check_env(env, envs) + headers, fact_names = inventory_facts() + query = AndOperator() fact_query = OrOperator() fact_query.add([EqualsOperator("name", name) for name in fact_names]) + query.add(fact_query) if env != '*': query.add(EqualsOperator("environment", env)) - query.add(fact_query) - - # get all the facts from PuppetDB facts = puppetdb.facts(query=query) + fact_data = {} for fact in facts: if fact.node not in fact_data: fact_data[fact.node] = {} - fact_data[fact.node][fact.name] = fact.value - return Response(stream_with_context( - stream_template( - 'inventory.html', - headers=headers, - fact_names=fact_names, - fact_data=fact_data, - envs=envs, - current_env=env - ))) + total = len(fact_data) + + return render_template( + 'inventory.json.tpl', + draw=draw, + total=total, + total_filtered=total, + fact_data=fact_data, + columns=fact_names) @app.route('/node/', @@ -529,26 +544,17 @@ def reports_ajax(env, node_name): reports_events = [] total = 0 - report_event_counts = {} - # Create a map from the metrics data to what the templates - # use to express the data. - report_map = { - 'success': 'successes', - 'failure': 'failures', - 'skipped': 'skips', - 'noops': 'noop' - } + # Convert metrics to relational dict + metrics = {} for report in reports_events: if total is None: total = puppetdb.total - report_counts = {'successes': 0, 'failures': 0, 'skips': 0} - for metrics in report.metrics: - if 'name' in metrics and metrics['name'] in report_map: - key_name = report_map[metrics['name']] - report_counts[key_name] = metrics['value'] - - report_event_counts[report.hash_] = report_counts + metrics[report.hash_] = {} + for m in report.metrics: + if m['category'] not in metrics[report.hash_]: + metrics[report.hash_][m['category']] = {} + metrics[report.hash_][m['category']][m['name']] = m['value'] if total is None: total = 0 @@ -559,7 +565,7 @@ def reports_ajax(env, node_name): total=total, total_filtered=total, reports=reports, - report_event_counts=report_event_counts, + metrics=metrics, envs=envs, current_env=env, columns=REPORTS_COLUMNS[:max_col]) @@ -605,6 +611,8 @@ def report(env, node_name, report_id): except StopIteration: abort(404) + report.version = CommonMark.commonmark(report.version) + return render_template( 'report.html', report=report, diff --git a/puppetboard/default_settings.py b/puppetboard/default_settings.py index 717925b..7bd6cc1 100644 --- a/puppetboard/default_settings.py +++ b/puppetboard/default_settings.py @@ -18,6 +18,11 @@ LOGLEVEL = 'info' NORMAL_TABLE_COUNT = 100 LITTLE_TABLE_COUNT = 10 TABLE_COUNT_SELECTOR = [10, 20, 50, 100, 500] +DISPLAYED_METRICS = ['resources.total', + 'events.failure', + 'events.success', + 'resources.skipped', + 'events.noop'] OFFLINE_MODE = False ENABLE_CATALOG = False OVERVIEW_FILTER = None diff --git a/puppetboard/docker_settings.py b/puppetboard/docker_settings.py index e32ac2a..3f61b33 100644 --- a/puppetboard/docker_settings.py +++ b/puppetboard/docker_settings.py @@ -33,6 +33,13 @@ TABLE_COUNT_DEF = "10,20,50,100,500" TABLE_COUNT_SELECTOR = [int(x) for x in os.getenv('TABLE_COUNT_SELECTOR', TABLE_COUNT_DEF).split(',')] +DISP_METR_DEF = ','.join(['resources.total', 'events.failure', + 'events.success', 'resources.skipped', + 'events.noop']) + +DISPLAYED_METRICS = [x.strip() for x in os.getenv('DISPLAYED_METRICS', + DISP_METR_DEF).split(',')] + OFFLINE_MODE = bool(os.getenv('OFFLINE_MODE', 'False').upper() == 'TRUE') ENABLE_CATALOG = bool(os.getenv('ENABLE_CATALOG', 'False').upper() == 'TRUE') OVERVIEW_FILTER = os.getenv('OVERVIEW_FILTER', None) @@ -46,7 +53,6 @@ GRAPH_FACTS_DEFAULT = ','.join(['architecture', 'clientversion', 'domain', GRAPH_FACTS = [x.strip() for x in os.getenv('GRAPH_FACTS', GRAPH_FACTS_DEFAULT).split(',')] - GRAPH_TYPE = os.getenv('GRAPH_TYPE', 'pie') # Tuples are hard to express as an environment variable, so here diff --git a/puppetboard/static/css/puppetboard.css b/puppetboard/static/css/puppetboard.css index 19506f5..db50a38 100644 --- a/puppetboard/static/css/puppetboard.css +++ b/puppetboard/static/css/puppetboard.css @@ -44,7 +44,7 @@ h1.ui.header.no-margin-bottom { color: #AA4643; } -.ui.label.failed { +.ui.label.failed, .ui.label.events.failure { background-color: #AA4643; } @@ -52,7 +52,7 @@ h1.ui.header.no-margin-bottom { color: #4572A7; } -.ui.label.changed { +.ui.label.changed, .ui.label.events.success { background-color: #4572A7; } @@ -68,10 +68,14 @@ h1.ui.header.no-margin-bottom { color: #DB843D; } -.ui.label.noop { +.ui.label.noop, .ui.label.events.noop { background-color: #DB843D; } +.ui.label.resources.total { + background-color: #989898; +} + .ui.label.unchanged { background-color: #89A54E; } @@ -80,7 +84,7 @@ h1.ui.header.no-margin-bottom { color: orange; } -.ui.label.skipped { +.ui.label.skipped, .ui.label.resources.skipped { background-color: orange; } diff --git a/puppetboard/static/js/dailychart.js b/puppetboard/static/js/dailychart.js index a14dca2..f68d428 100644 --- a/puppetboard/static/js/dailychart.js +++ b/puppetboard/static/js/dailychart.js @@ -1,6 +1,6 @@ jQuery(function ($) { function generateChart(el) { - var url = "/daily_reports_chart.json"; + var url = "daily_reports_chart.json"; var certname = $(el).attr('data-certname'); if (typeof certname !== typeof undefined && certname !== false) { url = url + "?certname=" + certname; diff --git a/puppetboard/templates/_macros.html b/puppetboard/templates/_macros.html index 0e4828d..65defad 100644 --- a/puppetboard/templates/_macros.html +++ b/puppetboard/templates/_macros.html @@ -9,6 +9,29 @@ {% endif %} {%- endmacro %} +{% macro report_status(caller, status, node_name, metrics, current_env, unreported_time=False, report_hash=False) -%} + {{ status|upper }} + {% if status == 'unreported' %} + {{ unreported_time|upper }} + {% 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 %} + {{ format_str|format(value) }} + {% else %} + 0 + {% endif%} + {% endfor %} + {% endif %} +{%- endmacro %} + {% macro datatable_init(table_html_id, ajax_url, default_length, length_selector, extra_options=None) -%} // Init datatable $.fn.dataTable.ext.errMode = 'throw'; diff --git a/puppetboard/templates/inventory.html b/puppetboard/templates/inventory.html index 5eedeb7..098e1d8 100644 --- a/puppetboard/templates/inventory.html +++ b/puppetboard/templates/inventory.html @@ -1,24 +1,21 @@ {% extends 'layout.html' %} +{% import '_macros.html' as macros %} {% block content %} -
- -
- +
- {% for head in headers %} - {{head}} + {% for head in fact_headers %} + {% endfor %} - {% for node, facts in fact_data.iteritems() %} - - {% for name in fact_names %} - - {% endfor %} - - {% endfor %}
{{head}}
{{facts.get(name, 'undef')}}
{% 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 %} diff --git a/puppetboard/templates/inventory.json.tpl b/puppetboard/templates/inventory.json.tpl new file mode 100644 index 0000000..4d94bcd --- /dev/null +++ b/puppetboard/templates/inventory.json.tpl @@ -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 %}{{ node }}{% endfilter %} + {%- elif fact_data[node][column] -%} + {{ fact_data[node][column] | jsonprint }} + {%- else -%} + "" + {%- endif -%} + {%- endfor -%} + ] + {% endfor -%} + ] +} diff --git a/puppetboard/templates/report.html b/puppetboard/templates/report.html index 0989f76..276dc74 100644 --- a/puppetboard/templates/report.html +++ b/puppetboard/templates/report.html @@ -14,7 +14,7 @@ {{ report.node }} - {{report.version}} + {{report.version|safe}} {{report.start}} diff --git a/puppetboard/templates/reports.json.tpl b/puppetboard/templates/reports.json.tpl index 2c24659..b773105 100644 --- a/puppetboard/templates/reports.json.tpl +++ b/puppetboard/templates/reports.json.tpl @@ -15,7 +15,7 @@ "{{ report[column.attr] }}" {%- elif column.type == 'status' -%} {% filter jsonprint -%} - {{ macros.status_counts(status=report.status, node_name=report.node, events=report_event_counts[report.hash_], report_hash=report.hash_, current_env=current_env) }} + {{ macros.report_status(status=report.status, node_name=report.node, metrics=metrics[report.hash_], report_hash=report.hash_, current_env=current_env) }} {%- endfilter %} {%- elif column.type == 'node' -%} {% filter jsonprint %}{{ report.node }}{% endfilter %} diff --git a/requirements.txt b/requirements.txt index 0f51820..489e34f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ Werkzeug >=0.12.1 itsdangerous >=0.23 pypuppetdb >=0.3.2 requests >=2.13.0 +CommonMark==0.7.2 diff --git a/test/test_app.py b/test/test_app.py index 7a6acd6..beaa5e9 100644 --- a/test/test_app.py +++ b/test/test_app.py @@ -820,4 +820,4 @@ def test_node_facts_json(client, mocker, for line in result_json['data']: assert len(line) == 2 - assert 'chart' not in result_json + assert 'chart' not in result_json \ No newline at end of file diff --git a/test/test_docker_settings.py b/test/test_docker_settings.py index 4b36a6d..6a44ab1 100644 --- a/test/test_docker_settings.py +++ b/test/test_docker_settings.py @@ -116,3 +116,11 @@ def test_env_table_selector(cleanUpEnv): os.environ['TABLE_COUNT_SELECTOR'] = '5,15,25' reload(docker_settings) assert [5, 15, 25] == docker_settings.TABLE_COUNT_SELECTOR + + +def test_env_column_options(cleanUpEnv): + os.environ['DISPLAYED_METRICS'] = 'resources.total, events.failure' + + reload(docker_settings) + assert ['resources.total', + 'events.failure'] == docker_settings.DISPLAYED_METRICS