Merge pull request #355 from redref/inventory
Inventory page revamp with paging
This commit is contained in:
@@ -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,
|
||||
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/<node_name>/',
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
23
puppetboard/templates/inventory.json.tpl
Normal file
23
puppetboard/templates/inventory.json.tpl
Normal 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 -%}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user