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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
5
ara/tests/integration/failed.yml
Normal file
5
ara/tests/integration/failed.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
- name: Failed playbook
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- fail:
|
||||
msg: "This is a failed playbook"
|
||||
6
ara/tests/integration/incomplete.yml
Normal file
6
ara/tests/integration/incomplete.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
- name: Incomplete playbook
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- debug:
|
||||
msg: "This playbook is meant to be interrupted"
|
||||
- command: sleep 30
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
18
ara/utils.py
18
ara/utils.py
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user