Port ara_record to use the ARA 1.0 API

This port uses the (outdated) flask 1.0 as a base [1] since it had
already been switched to use the flask API and required less changes.
It otherwise doesn't change how the module itself works or the use
case for it.

The only tricky part in this implementation revolves around letting
the action plugin recover the playbook id for the playbook the plugin
is running from.
One one end, the callback saves the play uuid [2][3].
The play uuid is available to the action plugin by recursing through
its task inheritance to find the parent play's uuid.
Once the plugin has the play uuid, it is able to search for it and
recover the parent playbook's id.

[1]: 0d915282e6/ara/plugins/actions/ara_record.py
[2]: https://review.openstack.org/#/c/609076/
[3]: https://review.openstack.org/#/c/609078/

Change-Id: I396ff26a53caedef7dfbd7dafa7eba9a515ea7a3
Depends-On: https://review.openstack.org/#/c/618228/
This commit is contained in:
David Moreau Simard 2018-10-14 11:24:05 -04:00
parent d94cd419a2
commit 35e4778bff
No known key found for this signature in database
GPG Key ID: CBEB466764A9E621
1 changed files with 228 additions and 0 deletions

View File

@ -0,0 +1,228 @@
# Copyright (c) 2018 Red Hat, Inc.
#
# This file is part of ARA: Ansible Run Analysis.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
from ansible import constants as ansible_constants
from ansible.playbook.play import Play
from ansible.plugins.action import ActionBase
from ara.clients.http import AraHttpClient
from ara.clients.offline import AraOfflineClient
DOCUMENTATION = """
---
module: ara_record
short_description: Ansible module to record persistent data with ARA.
version_added: "2.0"
author: "David Moreau-Simard <dmsimard@redhat.com>"
description:
- Ansible module to record persistent data with ARA.
options:
playbook_id:
description:
- id of the playbook to write the key to
- if not set, the module will use the ongoing playbook's id
required: false
key:
description:
- Name of the key to write data to
required: true
value:
description:
- Value of the key written to
required: true
type:
description:
- Type of the key
choices: [text, url, json, list, dict]
default: text
requirements:
- "python >= 3.5"
- "ara >= 1.0.0"
"""
EXAMPLES = """
- name: Associate specific data to a key for a playbook
ara_record:
key: "foo"
value: "bar"
- name: Associate data to a playbook that previously ran
ara_record:
playbook_id: 21
key: logs
value: "{{ lookup('file', '/var/log/ansible.log') }}"
type: text
- name: Retrieve the git version of the development repository
shell: cd dev && git rev-parse HEAD
register: git_version
delegate_to: localhost
- name: Record and register the git version of the playbooks
ara_record:
key: "git_version"
value: "{{ git_version.stdout }}"
register: version
- name: Print recorded data
debug:
msg: "{{ version.playbook_id }} - {{ version.key }}: {{ version.value }}
# Write data with a type (otherwise defaults to "text")
# This changes the behavior on how the value is presented in the web interface
- name: Record different formats of things
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" }
- { key: "somelist", value: ['one', 'two'], type: "list" }
- { key: "somedict", value: {'key': 'value' }, type: "dict" }
"""
RETURN = """
playbook:
description: ID of the playbook the data was recorded in
returned: on success
type: int
sample: 1
key:
description: Key where the record is saved
returned: on success
type: str
sample: log_url
value:
description: Value of the key
returned: on success
type: complex
sample: http://logurl
type:
description: Type of the key
returned: on success
type: string
sample: url
created:
description: Date the record was created (ISO-8601)
returned: on success
type: str
sample: 2018-11-15T17:27:41.597234Z
updated:
description: Date the record was updated (ISO-8601)
returned: on success
type: str
sample: 2018-11-15T17:27:41.597265Z
"""
class ActionModule(ActionBase):
""" Record persistent data as key/value pairs in ARA """
TRANSFERS_FILES = False
VALID_ARGS = frozenset(("playbook_id", "key", "value", "type"))
VALID_TYPES = ["text", "url", "json", "list", "dict"]
def __init__(self, *args, **kwargs):
super(ActionModule, self).__init__(*args, **kwargs)
# Retrieves the runtime plugin options for the ara_default callback plugin
options = ansible_constants.config.get_plugin_options("callback", "ara_default")
if options["api_client"] == "offline":
self.client = AraOfflineClient()
elif options["api_client"] == "http":
self.client = AraHttpClient(endpoint=options["api_server"], timeout=options["timeout"])
else:
raise Exception("Unsupported API client: %s. Please use 'offline' or 'http'" % options["api_client"])
def create_or_update_key(self, playbook, key, value, type):
changed = False
record = self.client.get("/api/v1/records?playbook=%s&key=%s" % (playbook, key))
if record["count"] == 0:
# Create the record if it doesn't exist
record = self.client.post("/api/v1/records", playbook=playbook, key=key, value=value, type=type)
changed = True
else:
# Otherwise update it if the data is different (idempotency)
old = record["results"][0]
if old["value"] != value or old["type"] != type:
record = self.client.patch("/api/v1/records/%s" % old["id"], key=key, value=value, type=type)
changed = True
else:
record = old
return record, changed
def run(self, tmp=None, task_vars=None):
if task_vars is None:
task_vars = dict()
for arg in self._task.args:
if arg not in self.VALID_ARGS:
result = {"failed": True, "msg": "{0} is not a valid option.".format(arg)}
return result
result = super(ActionModule, self).run(tmp, task_vars)
playbook_id = self._task.args.get("playbook_id", None)
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 '{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
if playbook_id is None:
# Retrieve the playbook id by working our way up from the task to find
# the play uuid. Once we have the play uuid, we can find the playbook.
parent = self._task
while not isinstance(parent._parent._play, Play):
parent = parent._parent
play = self.client.get("/api/v1/plays?uuid=%s" % parent._parent._play._uuid)
playbook_id = play["results"][0]["playbook"]
try:
data, changed = self.create_or_update_key(playbook_id, key, value, type)
result["changed"] = changed
result["key"] = data["key"]
result["value"] = data["value"]
result["type"] = data["type"]
result["playbook_id"] = data["playbook"]
result["created"] = data["created"]
result["updated"] = data["updated"]
if result["changed"]:
result["msg"] = "Record created or updated in ARA"
else:
result["msg"] = "Record unchanged in ARA"
except Exception as e:
result["changed"] = False
result["failed"] = True
result["msg"] = "Record failed to be created or updated in ARA: {0}".format(str(e))
return result