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)))
|
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:
|
||||||
@@ -363,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>/',
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
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" %
|
"action": "/catalogs/compare/node-unreported...node-%s" %
|
||||||
found_status})
|
found_status})
|
||||||
assert len(val) == 1
|
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