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:
@@ -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')
|
||||
)
|
||||
|
||||
|
||||
|
||||
24
ara/db/versions/003_ara_record_type.py
Normal file
24
ara/db/versions/003_ara_record_type.py
Normal 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 ###
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -55,5 +55,6 @@ EXAMPLES = '''
|
||||
with_items:
|
||||
- foo.key
|
||||
- foo.value
|
||||
- foo.type
|
||||
- foo.playbook_id
|
||||
'''
|
||||
|
||||
@@ -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" }
|
||||
'''
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
BIN
tests/integration/databases/003-2a0c6b92010a.sqlite
Normal file
BIN
tests/integration/databases/003-2a0c6b92010a.sqlite
Normal file
Binary file not shown.
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user