Merge pull request #355 from redref/inventory

Inventory page revamp with paging
This commit is contained in:
Mike Terzo
2017-06-10 03:37:00 -04:00
committed by GitHub
4 changed files with 129 additions and 49 deletions

View File

@@ -323,29 +323,11 @@ def nodes(env):
current_env=env)))
@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.
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:
@@ -363,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('/<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()
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/<node_name>/',

View File

@@ -1,24 +1,21 @@
{% 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...">
</div>
<table class='ui compact very basic sortable table'>
<table id="inventory_table" class='ui fixed compact very basic sortable table'>
<thead>
<tr>
{% for head in headers %}
<th{% if loop.index == 1 %} class="default-sort"{% endif %}>{{head}}</th>
{% for head in fact_headers %}
<th>{{head}}</th>
{% endfor %}
</tr>
</thead>
<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>
</table>
{% 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

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