From 709d14e9a2020254b6597ad5afdeb3e2d1f0bdd1 Mon Sep 17 00:00:00 2001 From: redref Date: Sat, 4 Feb 2017 20:03:09 +0100 Subject: [PATCH 1/2] Inventory revamp - client side --- puppetboard/app.py | 84 ++++++++++++++---------- puppetboard/templates/inventory.html | 23 +++---- puppetboard/templates/inventory.json.tpl | 23 +++++++ 3 files changed, 82 insertions(+), 48 deletions(-) create mode 100644 puppetboard/templates/inventory.json.tpl diff --git a/puppetboard/app.py b/puppetboard/app.py index 9dfe18a..05ce75d 100644 --- a/puppetboard/app.py +++ b/puppetboard/app.py @@ -343,29 +343,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: @@ -383,33 +365,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, + total = len(fact_data) + + return render_template( + 'inventory.json.tpl', + draw=draw, + total=total, + total_filtered=total, fact_data=fact_data, - envs=envs, - current_env=env - ))) + columns=fact_names) @app.route('/node//', 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 -%} + ] +} From 507df872340635743a43ae107dbfff7a6a8f74bb Mon Sep 17 00:00:00 2001 From: redref Date: Sat, 4 Feb 2017 22:21:01 +0100 Subject: [PATCH 2/2] Inventory page testing --- puppetboard/app.py | 12 ++++++------ test/test_app.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/puppetboard/app.py b/puppetboard/app.py index 05ce75d..6fce637 100644 --- a/puppetboard/app.py +++ b/puppetboard/app.py @@ -418,12 +418,12 @@ def inventory_ajax(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) + 'inventory.json.tpl', + draw=draw, + total=total, + total_filtered=total, + fact_data=fact_data, + columns=fact_names) @app.route('/node//', diff --git a/test/test_app.py b/test/test_app.py index a3cc494..fc1ec6e 100644 --- a/test/test_app.py +++ b/test/test_app.py @@ -620,3 +620,49 @@ def test_catalogs_json_compare(client, mocker, "action": "/catalogs/compare/node-unreported...node-%s" % found_status}) assert len(val) == 1 + + +def test_inventory_view(client, mocker, mock_puppetdb_environments): + rv = client.get('/inventory') + assert rv.status_code == 200 + soup = BeautifulSoup(rv.data, 'html.parser') + assert soup.title.contents[0] == 'Puppetboard' + + +def test_inventory_json(client, mocker, mock_puppetdb_environments): + facts = ['fqdn', 'ipaddress', 'lsbdistdescription', 'hardwaremodel', + 'kernelrelease', 'puppetversion'] + values = [ + ['node-1', 'X.X.X.X', 'os7', 'server', '4.3', 'X.X.X'], + ['node-2', 'X.X.X.X', 'os5', 'server', '4.1', 'X.X.X'], + ['node-3', 'X.X.X.X', 'os6', 'server', '4.2', 'X.X.X'], + ['node-4', 'X.X.X.X', 'os4', 'server', '4.3', 'X.X.X'], + ] + query_data = {'facts': []} + query_data['facts'].append([]) + + for i, value in enumerate(values): + for idx, column in enumerate(facts): + query_data['facts'][0].append({ + 'certname': value[0], + 'name': column, + 'value': value[idx], + 'environment': 'production' + }) + + dbquery = MockDbQuery(query_data) + + mocker.patch.object(app.puppetdb, '_query', side_effect=dbquery.get) + + rv = client.get('/inventory/json') + assert rv.status_code == 200 + result_json = json.loads(rv.data.decode('utf-8')) + assert 'data' in result_json + + for value in values: + for line in result_json['data']: + if value[0] in line[0]: + assert line[1:] == value[1:] + break + else: + raise Exception("Input %s not found" % value)