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:
parent
d94cd419a2
commit
35e4778bff
228
ara/plugins/action/ara_record.py
Normal file
228
ara/plugins/action/ara_record.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user