Implement ara_record data types

Data types allows a user to specify a type when recording data.
Depending on the type, the display behavior or format will be different
in the web interface.
The first implemented types are as follows:
- text (default): straight text in <pre>
- url: value becomes a hyperlink
- json: value is json pretty-printed

Other example types that could be implemented eventually would be
markdown or rst for rich formatting.

Change-Id: I28b78a2b5899ece3b0abc4648bd8d0a2678c80ee
This commit is contained in:
David Moreau-Simard
2016-11-19 16:41:16 -05:00
parent fd8d56d8aa
commit ea1f0f9cd1
16 changed files with 174 additions and 22 deletions

View File

@@ -25,6 +25,7 @@ LIST_FIELDS = (
Field('Playbook ID', 'playbook.id'),
Field('Playbook Path', 'playbook.path'),
Field('Key'),
Field('Type')
)
SHOW_FIELDS = (
@@ -32,7 +33,8 @@ SHOW_FIELDS = (
Field('Playbook ID', 'playbook.id'),
Field('Playbook Path', 'playbook.path'),
Field('Key'),
Field('Value')
Field('Value'),
Field('Type')
)

View File

@@ -0,0 +1,24 @@
"""ara_record type
Revision ID: 2a0c6b92010a
Revises: e8e78fd08bf2
Create Date: 2016-11-19 09:48:49.231279
"""
# flake8: noqa
# revision identifiers, used by Alembic.
revision = '2a0c6b92010a'
down_revision = 'e8e78fd08bf2'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('data', sa.Column('type', sa.String(length=255), nullable=True))
### end Alembic commands ###
def downgrade():
op.drop_column('data', 'type')
### end Alembic commands ###

View File

@@ -409,6 +409,7 @@ class Data(db.Model):
playbook_id = std_fkey('playbooks.id')
key = db.Column(db.String(255))
value = db.Column(CompressedText((2 ** 32) - 1))
type = db.Column(db.String(255))
def __repr__(self):
return '<Data %s:%s>' % (self.data.playbook_id, self.data.key)

View File

@@ -60,6 +60,7 @@ EXAMPLES = '''
with_items:
- foo.key
- foo.value
- foo.type
- foo.playbook_id
'''
@@ -122,12 +123,14 @@ class ActionModule(ActionBase):
if data:
result['key'] = data.key
result['value'] = data.value
result['type'] = data.type
result['playbook_id'] = data.playbook_id
msg = "Sucessfully read data for the key {0}".format(data.key)
result['msg'] = msg
except Exception as e:
result['key'] = None
result['value'] = None
result['type'] = None
result['playbook_id'] = None
result['failed'] = True
msg = "Could not read data for key {0}: {1}".format(key, str(e))

View File

@@ -42,6 +42,11 @@ options:
description:
- Value of the key written to
required: true
type:
description:
- Type of the key
choices: [text, url, json]
default: text
requirements:
- "python >= 2.6"
@@ -62,6 +67,17 @@ EXAMPLES = '''
- ara_record:
key: "git_version"
value: "{{ git_version.stdout }}"
# Write data with a type (otherwise defaults to "text")
# This changes the behavior on how the value is presented in the web interface
- ara_record:
key: "{{ item.key }}"
value: "{{ item.value }}"
type: "{{ item.type }}"
with_items:
- { key: "log", value: "error", type: "text" }
- { key: "website", value: "http://domain.tld", type: "url" }
- { key: "data", value: "{ 'key': 'value' }", type: "json" }
'''
@@ -69,19 +85,22 @@ class ActionModule(ActionBase):
''' Record persistent data as key/value pairs in ARA '''
TRANSFERS_FILES = False
VALID_ARGS = frozenset(('key', 'value'))
VALID_ARGS = frozenset(('key', 'value', 'type'))
VALID_TYPES = ['text', 'url', 'json']
def create_or_update_key(self, playbook_id, key, value):
def create_or_update_key(self, playbook_id, key, value, type):
try:
data = (models.Data.query
.filter_by(key=key)
.filter_by(playbook_id=playbook_id)
.one())
data.value = value
data.type = type
except models.NoResultFound:
data = models.Data(playbook_id=playbook_id,
key=key,
value=value)
value=value,
type=type)
db.session.add(data)
db.session.commit()
@@ -110,14 +129,24 @@ class ActionModule(ActionBase):
key = self._task.args.get('key', None)
value = self._task.args.get('value', None)
type = self._task.args.get('type', 'text')
required = ['key', 'value']
for parameter in required:
if not self._task.args.get(parameter):
result['failed'] = True
result['msg'] = "{} parameter is required".format(parameter)
result['msg'] = "Parameter '{0}' is required".format(parameter)
return result
if type not in self.VALID_TYPES:
result['failed'] = True
msg = "Type '{0}' is not supported, choose one of: {1}".format(
type,
", ".join(self.VALID_TYPES)
)
result['msg'] = msg
return result
# Retrieve the persisted playbook_id from tmpfile
tmpfile = os.path.join(app.config['ARA_TMP_DIR'], 'ara.json')
with open(tmpfile) as file:
@@ -125,7 +154,7 @@ class ActionModule(ActionBase):
playbook_id = data['playbook']['id']
try:
self.create_or_update_key(playbook_id, key, value)
self.create_or_update_key(playbook_id, key, value, type)
result['msg'] = "Data recorded in ARA for this playbook."
except Exception as e:
result['failed'] = True

View File

@@ -55,5 +55,6 @@ EXAMPLES = '''
with_items:
- foo.key
- foo.value
- foo.type
- foo.playbook_id
'''

View File

@@ -22,12 +22,11 @@
DOCUMENTATION = '''
---
module: ara_record
short_description: Ansible module to record data with ARA
short_description: Ansible module to record persistent data with ARA.
version_added: "2.0"
author: "RDO Community <rdo-list@redhat.com>"
description:
- Ansible module to record data with ARA. This module should always be
executed wherever the playbook is run from.
- Ansible module to record persistent data with ARA.
options:
key:
description:
@@ -37,6 +36,11 @@ options:
description:
- Value of the key written to
required: true
type:
description:
- Type of the key
choices: [text, url, json]
default: text
requirements:
- "python >= 2.6"
@@ -57,4 +61,15 @@ EXAMPLES = '''
- ara_record:
key: "git_version"
value: "{{ git_version.stdout }}"
# Write data with a type (otherwise defaults to "text")
# This changes the behavior on how the value is presented in the web interface
- ara_record:
key: "{{ item.key }}"
value: "{{ item.value }}"
type: "{{ item.type }}"
with_items:
- { key: "log", value: "error", type: "text" }
- { key: "website", value: "http://domain.tld", type: "url" }
- { key: "data", value: "{ 'key': 'value' }", type: "json" }
'''

View File

@@ -14,4 +14,14 @@
{% endif %}
</td>
{% endmacro %}
{% macro render_value_type(value, type) %}
{% if type == 'json' %}
<pre>{{ value |to_nice_json |safe }}</pre>
{% elif type == 'url' %}
<a href="{{ value }}" target="_blank">{{ value }}</a>
{% else %}
<pre>{{ value }}</pre>
{% endif %}
{% endmacro %}
</div>

View File

@@ -166,7 +166,7 @@
{% for item in data %}
<tr>
<td>{{ macros.make_link('playbook.playbook_data', item.key, playbook=playbook.id, _anchor=item.id) }}</td>
<td>{{ item.value }}</td>
<td>{{ macros.render_value_type(item.value, item.type) }}</td>
</tr>
{% endfor %}
</tbody>
@@ -175,4 +175,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -20,7 +20,7 @@
{% for item in data %}
<tr>
<td id="{{ item.id }}"><a href="#{{ item.id }}">{{ item.key }}</a></td>
<td><pre>{{ item.value | to_nice_json }}</pre></td>
<td>{{ macros.render_value_type(item.value, item.type) }}</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -33,6 +33,20 @@ order to register whatever you'd like in a key/value format, for example::
key: "git_version"
value: "{{ git_version.stdout }}"
It also supports data types which will have an impact on how the value will be
displayed in the web interface. The default type if not specified is "text".
Example usage::
---
- ara_record:
key: "{{ item.key }}"
value: "{{ item.value }}"
type: "{{ item.type }}"
with_items:
- { key: "log", value: "error", type: "text" }
- { key: "website", value: "http://domain.tld", type: "url" }
- { key: "data", value: '{ "key": "value" }', type: "json" }
This data will be recorded inside ARA's database and associated with the
particular playbook run that was executed.

Binary file not shown.

View File

@@ -7,3 +7,4 @@ successfully.
- 000: Unstable database schema (assumed ~0.9.3)
- 001: First stable database schema
- 002: ara_record module data added
- 003: ara_record type added

View File

@@ -15,20 +15,36 @@
#
# ARA module specific tests
# Record text (default)
- name: Record a k/v pair without type with ara_record
ara_record:
key: "notype"
value: "text"
# Record text, update to url
- name: Record a k/v pair with ara_record
ara_record:
key: "foo"
value: "bar"
type: "text"
- name: Update a k/v pair with ara_record
ara_record:
key: "foo"
value: "barfoo"
value: "http://barfoo"
type: "url"
# Record json
- name: Add another k/v pair with ara_record
ara_record:
key: "bar"
value: "foo"
value: '{ "foo": "bar" }'
type: "json"
# Read things
- name: Read the value of notype
ara_read:
key: "notype"
- name: Read the value of foo
ara_read:

View File

@@ -105,7 +105,8 @@ class TestModule(TestCase):
self.task.async = MagicMock()
self.task.args = {
'key': 'test-key',
'value': 'test-value'
'value': 'test-value',
'type': 'text'
}
action = ara_record.ActionModule(self.task, self.connection,
@@ -170,11 +171,12 @@ class TestModule(TestCase):
self.assertEqual(r_data.playbook_id, r_playbook.id)
self.assertEqual(r_data.key, 'test-key')
self.assertEqual(r_data.value, 'test-value')
self.assertEqual(r_data.type, 'text')
self.assertEqual(data['playbook_id'], r_data.playbook_id)
self.assertEqual(data['key'], r_data.key)
self.assertEqual(data['value'], r_data.value)
self.assertEqual(data['type'], r_data.type)
def test_read_record_with_no_key(self):
"""

View File

@@ -140,7 +140,8 @@ class TestModule(TestCase):
task.async = MagicMock()
task.args = {
'key': 'test-key',
'value': 'test-value'
'value': 'test-value',
'type': 'text'
}
action = ara_record.ActionModule(task, self.connection,
@@ -157,11 +158,11 @@ class TestModule(TestCase):
self.assertEqual(r_data.playbook_id, r_playbook.id)
self.assertEqual(r_data.key, 'test-key')
self.assertEqual(r_data.value, 'test-value')
self.assertEqual(r_data.type, 'text')
def test_update_record(self):
def test_create_record_with_no_type(self):
"""
Update an existing record by running ara_record a second time on the
same key.
Create a new record with ara_record with no type specified.
"""
task = MagicMock(Task)
task.async = MagicMock()
@@ -184,10 +185,41 @@ class TestModule(TestCase):
self.assertEqual(r_data.playbook_id, r_playbook.id)
self.assertEqual(r_data.key, 'test-key')
self.assertEqual(r_data.value, 'test-value')
self.assertEqual(r_data.type, 'text')
def test_update_record(self):
"""
Update an existing record by running ara_record a second time on the
same key.
"""
task = MagicMock(Task)
task.async = MagicMock()
task.args = {
'key': 'test-key',
'value': 'test-value',
'type': 'text'
}
action = ara_record.ActionModule(task, self.connection,
self.play_context, loader=None,
templar=None, shared_loader_obj=None)
action.run()
r_playbook = m.Playbook.query.first()
self.assertIsNotNone(r_playbook)
r_data = m.Data.query.filter_by(playbook_id=r_playbook.id,
key='test-key').one()
self.assertIsNotNone(r_data)
self.assertEqual(r_data.playbook_id, r_playbook.id)
self.assertEqual(r_data.key, 'test-key')
self.assertEqual(r_data.value, 'test-value')
self.assertEqual(r_data.type, 'text')
task.args = {
'key': 'test-key',
'value': 'another-value'
'value': 'http://another-value',
'type': 'url'
}
action = ara_record.ActionModule(task, self.connection,
self.play_context, loader=None,
@@ -196,7 +228,9 @@ class TestModule(TestCase):
r_data = m.Data.query.filter_by(playbook_id=r_playbook.id,
key='test-key').one()
self.assertEqual(r_data.value, 'another-value')
self.assertEqual(r_data.value, 'http://another-value')
self.assertEqual(r_data.type, 'url')
def test_record_with_no_key(self):
"""