Infer and display the end status of a playbook run

This adds support for displaying in the UI an aggregate inferred
status of a playbook.
We want a way to display if a playbook was completed (or not) and if
the run was successful (or not).

- The home and all playbooks pages now display an icon to show the
  status of a playbook run. The icon has alt text to explain itself.
- Unit and integration coverage have been improved

Change-Id: I8964eb90b2cb2d2e38876f78c56229e0e3e9040e
This commit is contained in:
David Moreau-Simard
2017-01-14 10:59:31 -05:00
parent 97aaa536bf
commit 8f2f6b7b22
9 changed files with 115 additions and 7 deletions

View File

@@ -8,11 +8,12 @@
<h2><a href="https://github.com/openstack/ara">ARA</a> records <a href="https://www.ansible.com/">Ansible</a> Playbook runs seamlessly to make them easier to visualize, understand and troubleshoot.</h2>
</div>
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="col-md-12">
<h2><strong>Latest playbook runs</strong></h2>
<table class="table table-striped table-bordered table-hover table-condensed">
<thead>
<tr>
<th></th>
<th>Playbook</th>
<th class="col-md-1">Hosts</th>
<th class="date col-md-2">Date</th>
@@ -27,6 +28,7 @@
<tbody>
{% for playbook in playbooks %}
<tr>
<td class="vert-align">{{ macros.render_status(stats[playbook.id].status) }}</td>
<td>{{ macros.make_link('playbook.show_playbook', playbook.path|pathtruncate,
playbook=playbook.id) }}</td>
<td>{{ playbook.hosts|list|length }}</td>

View File

@@ -15,6 +15,18 @@
</td>
{% endmacro %}
{% macro render_status(status) %}
{% if status == 'success' %}
<span class="pficon pficon-ok list-view-pf-icon-md list-view-pf-icon-success" title="Playbook finished successfully"></span>
{% elif status == 'failed' %}
<span class="pficon pficon-error-circle-o list-view-pf-icon-md list-view-pf-icon-danger" title="Playbook finished with errors"></span>
{% elif status == 'incomplete' %}
<span class="pficon pficon-info list-view-pf-icon-md list-view-pf-icon-info" title="Playbook was interrupted: data will be incomplete."></span>
{% else %}
<span class="fa fa-question-circle" title="The status of this playbook is unknown."></span>
{% endif %}
{% endmacro %}
{% macro render_value_type(value, type) %}
{% if type == 'json' %}
<pre>{{ value |to_nice_json |safe }}</pre>

View File

@@ -3,11 +3,12 @@
{% block content %}
<div class="container-fluid container-pf-nav-pf-vertical container-pf-nav-pf-vertical-with-secondary">
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="col-md-12">
<h1><strong>All Playbooks</strong></h1>
<table class="table table-striped table-bordered table-hover table-condensed">
<thead>
<tr>
<th></th>
<th>Playbook</th>
<th class="col-md-1">Hosts</th>
<th class="date col-md-2">Date</th>
@@ -22,6 +23,7 @@
<tbody>
{% for playbook in playbooks %}
<tr>
<td class="vert-align">{{ macros.render_status(stats[playbook.id].status) }}</td>
<td>
<span class="pull-left">{{ macros.make_link('playbook.show_playbook', playbook.path|pathtruncate(35), playbook=playbook.id) }}</span>
</td>

View File

@@ -0,0 +1,5 @@
- name: Failed playbook
hosts: localhost
tasks:
- fail:
msg: "This is a failed playbook"

View File

@@ -0,0 +1,6 @@
- name: Incomplete playbook
hosts: localhost
tasks:
- debug:
msg: "This playbook is meant to be interrupted"
- command: sleep 30

View File

@@ -29,7 +29,8 @@ class TestAra(unittest.TestCase):
m.db.drop_all()
def ansible_run(complete=True, gather_facts=True, ara_record=False):
def ansible_run(complete=True, failed=False, gather_facts=True,
ara_record=False):
'''Simulate a simple Ansible run by creating the
expected database objects. This roughly approximates the
following playbook:
@@ -49,6 +50,8 @@ def ansible_run(complete=True, gather_facts=True, ara_record=False):
Set the `complete` parameter to `False` to simulate an
aborted Ansible run.
Set the `failed` parameter to `True` to simulate a
failed Ansible run.
Set the `gathered_facts` parameter to `False` to simulate a run with no
facts gathered.
Set the `ara_record` parameter to `True` to simulate a run with an
@@ -72,13 +75,21 @@ def ansible_run(complete=True, gather_facts=True, ara_record=False):
result = m.TaskResult(task=task, status='ok', host=host, result=msg)
task_skipped = m.Task(play=play, playbook=playbook, action='foo')
result_skipped = m.TaskResult(task=task_skipped, status='skipped',
host=host, result='Conditional check failed')
task_failed = m.Task(play=play, playbook=playbook, action='bar')
result_failed = m.TaskResult(task=task_failed, status='failed', host=host,
result='Failed to do thing')
ctx = dict(
playbook=playbook,
play=play,
file=playbook_file,
task=task,
host=host,
result=result)
result=result,
file=playbook_file,
host=host)
if gather_facts:
facts = m.HostFacts(host=host, values='{"fact": "value"}')
@@ -93,10 +104,19 @@ def ansible_run(complete=True, gather_facts=True, ara_record=False):
obj.start()
db.session.add(obj)
extra_tasks = [task_skipped, result_skipped]
if failed:
extra_tasks.append(task_failed)
extra_tasks.append(result_failed)
for obj in extra_tasks:
db.session.add(obj)
db.session.commit()
if complete:
stats = m.Stats(playbook=playbook, host=host)
stats = m.Stats(playbook=playbook, host=host, ok=1, skipped=1,
failed=int(failed))
ctx['stats'] = stats
db.session.add(stats)
ctx['playbook'].complete = True

View File

@@ -1,6 +1,7 @@
import ara.utils as u
import json
from common import ansible_run
from common import TestAra
@@ -21,6 +22,42 @@ class TestUtils(TestAra):
res = u.status_to_query('changed')
self.assertEqual(res, {'status': 'ok', 'changed': True})
def test_get_summary_stats_complete(self):
ctx = ansible_run()
playbook = ctx['playbook'].id
res = u.get_summary_stats([ctx['playbook']], 'playbook_id')
self.assertEqual(1, res[playbook]['ok'])
self.assertEqual(0, res[playbook]['changed'])
self.assertEqual(0, res[playbook]['failed'])
self.assertEqual(1, res[playbook]['skipped'])
self.assertEqual(0, res[playbook]['unreachable'])
self.assertEqual('success', res[playbook]['status'])
def test_get_summary_stats_incomplete(self):
ctx = ansible_run(complete=False)
playbook = ctx['playbook'].id
res = u.get_summary_stats([ctx['playbook']], 'playbook_id')
self.assertEqual(0, res[playbook]['ok'])
self.assertEqual(0, res[playbook]['changed'])
self.assertEqual(0, res[playbook]['failed'])
self.assertEqual(0, res[playbook]['skipped'])
self.assertEqual(0, res[playbook]['unreachable'])
self.assertEqual('incomplete', res[playbook]['status'])
def test_get_summary_stats_failed(self):
ctx = ansible_run(failed=True)
playbook = ctx['playbook'].id
res = u.get_summary_stats([ctx['playbook']], 'playbook_id')
self.assertEqual(1, res[playbook]['ok'])
self.assertEqual(0, res[playbook]['changed'])
self.assertEqual(1, res[playbook]['failed'])
self.assertEqual(1, res[playbook]['skipped'])
self.assertEqual(0, res[playbook]['unreachable'])
self.assertEqual('failed', res[playbook]['status'])
def test_format_json(self):
data = json.dumps({'name': 'value'})
res = u.format_json(json.dumps(data))

View File

@@ -73,9 +73,27 @@ def get_summary_stats(items, attr):
'skipped': sum([int(stat.skipped) for stat in stats]),
'unreachable': sum([int(stat.unreachable) for stat in stats])
}
# If we're aggregating stats for a playbook, also infer status
if attr is "playbook_id":
data[item.id]['status'] = infer_status(item, data[item.id])
return data
def infer_status(playbook, playbook_stats):
try:
if not playbook.complete:
return 'incomplete'
if playbook_stats['failed'] >= 1 or playbook_stats['unreachable'] >= 1:
return 'failed'
else:
return 'success'
except Exception as e:
return 'unknown: ' + str(e)
def format_json(val):
try:
return json.dumps(json.loads(val),

View File

@@ -35,6 +35,12 @@ export ARA_DATABASE="sqlite:///${DATABASE}"
# Run test playbooks
ansible-playbook -vv ara/tests/integration/smoke.yml
ansible-playbook -vv ara/tests/integration/hosts.yml
# This playbook is meant to fail
ansible-playbook -vv ara/tests/integration/failed.yml || true
# This playbook is meant to be interrupted
ansible-playbook -vv ara/tests/integration/incomplete.yml &
sleep 5
kill $!
# Run test commands
pbid=$(ara playbook list -c ID -f value |head -n1)