From 8e5dcd976a4042c634693b6faadd8f4089bd1007 Mon Sep 17 00:00:00 2001 From: David Moreau Simard Date: Tue, 19 Nov 2019 11:02:52 -0500 Subject: [PATCH] Add new Ansible plugins: ara_playbook and ara_api These new action and lookup plugins makes it easier to query ARA from inside a playbook. In terms of practical use, this first iteration allows us to have an integration test playbook that asserts some of ARA's features such as playbook names and labels. Change-Id: I38bea1062f0002886ecde70827f70d27248a1868 --- ara/plugins/action/ara_playbook.py | 102 +++++++++++++++++++++++++++ ara/plugins/lookup/ara_api.py | 63 +++++++++++++++++ ara/setup/__init__.py | 1 + ara/setup/ansible.py | 5 +- ara/setup/env.py | 5 +- ara/setup/lookup_plugins.py | 23 ++++++ doc/source/ansible-configuration.rst | 17 ++++- doc/source/ara-api-lookup.rst | 58 +++++++++++++++ doc/source/index.rst | 1 + tests/basic.yaml | 4 ++ tests/integration/lookups.yaml | 33 +++++++++ tests/test_tasks.yaml | 4 ++ 12 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 ara/plugins/action/ara_playbook.py create mode 100644 ara/plugins/lookup/ara_api.py create mode 100644 ara/setup/lookup_plugins.py create mode 100644 doc/source/ara-api-lookup.rst create mode 100644 tests/integration/lookups.yaml diff --git a/ara/plugins/action/ara_playbook.py b/ara/plugins/action/ara_playbook.py new file mode 100644 index 00000000..33da11e9 --- /dev/null +++ b/ara/plugins/action/ara_playbook.py @@ -0,0 +1,102 @@ +# Copyright (c) 2019 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 . + +from ansible.playbook.play import Play +from ansible.plugins.action import ActionBase + +from ara.clients import utils as client_utils + +DOCUMENTATION = """ +--- +module: ara_playbook +short_description: Retrieves either a specific playbook from ARA or the one currently running +version_added: "2.9" +author: "David Moreau-Simard " +description: + - Retrieves either a specific playbook from ARA or the one currently running +options: + playbook_id: + description: + - id of the playbook to retrieve + - if not set, the module will use the ongoing playbook's id + required: false + +requirements: + - "python >= 3.5" + - "ara >= 1.4.0" +""" + +EXAMPLES = """ +- name: Get a specific playbook + ara_playbook: + playbook_id: 5 + register: playbook_query + +- name: Get current playbook by not specifying a playbook id + ara_playbook: + register: playbook_query + +- name: Do something with the playbook + debug: + msg: "Playbook report: http://ara_api/playbook/{{ playbook_query.playbook.id | string }}.html" +""" + +RETURN = """ +playbook: + description: playbook object returned by the API + returned: on success + type: dict +""" + + +class ActionModule(ActionBase): + """ Retrieves either a specific playbook from ARA or the one currently running """ + + TRANSFERS_FILES = False + VALID_ARGS = frozenset(("playbook_id")) + + def __init__(self, *args, **kwargs): + super(ActionModule, self).__init__(*args, **kwargs) + self.client = client_utils.active_client() + + 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) + 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"] + + result["playbook"] = self.client.get("/api/v1/playbooks/%s" % playbook_id) + result["changed"] = False + result["msg"] = "Queried playbook %s from ARA" % playbook_id + + return result diff --git a/ara/plugins/lookup/ara_api.py b/ara/plugins/lookup/ara_api.py new file mode 100644 index 00000000..84ff7155 --- /dev/null +++ b/ara/plugins/lookup/ara_api.py @@ -0,0 +1,63 @@ +# Copyright (c) 2019 Red Hat, Inc. +# +# This file is part of ARA Records Ansible. +# +# 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 . + + +from __future__ import absolute_import, division, print_function + +from ansible.plugins.lookup import LookupBase + +from ara.clients import utils as client_utils + +__metaclass__ = type + +DOCUMENTATION = """ + lookup: ara_api + author: David Moreau-Simard (@dmsimard) + version_added: "2.9" + short_description: Queries the ARA API for data + description: + - Queries the ARA API for data + options: + _terms: + description: + - The endpoint to query + type: list + elements: string + required: True +""" + +EXAMPLES = """ + - debug: msg="{{ lookup('ara_api','/api/v1/playbooks/1') }}" +""" + +RETURN = """ + _raw: + description: response from query +""" + + +class LookupModule(LookupBase): + def __init__(self, *args, **kwargs): + super(LookupModule, self).__init__(*args, **kwargs) + self.client = client_utils.active_client() + + def run(self, terms, variables, **kwargs): + ret = [] + for term in terms: + ret.append(self.client.get(term)) + + return ret diff --git a/ara/setup/__init__.py b/ara/setup/__init__.py index e413f344..8ef87a08 100644 --- a/ara/setup/__init__.py +++ b/ara/setup/__init__.py @@ -23,3 +23,4 @@ path = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) plugins = os.path.abspath(os.path.join(path, "plugins")) action_plugins = os.path.abspath(os.path.join(plugins, "action")) callback_plugins = os.path.abspath(os.path.join(plugins, "callback")) +lookup_plugins = os.path.abspath(os.path.join(plugins, "lookup")) diff --git a/ara/setup/ansible.py b/ara/setup/ansible.py index d2ef26a0..49172668 100644 --- a/ara/setup/ansible.py +++ b/ara/setup/ansible.py @@ -17,14 +17,15 @@ from __future__ import print_function -from . import action_plugins, callback_plugins +from . import action_plugins, callback_plugins, lookup_plugins config = """ [defaults] callback_plugins={} action_plugins={} +lookup_plugins={} """.format( - callback_plugins, action_plugins + callback_plugins, action_plugins, lookup_plugins ) if __name__ == "__main__": diff --git a/ara/setup/env.py b/ara/setup/env.py index 51bdf469..305a2b56 100644 --- a/ara/setup/env.py +++ b/ara/setup/env.py @@ -20,13 +20,14 @@ from __future__ import print_function import os from distutils.sysconfig import get_python_lib -from . import action_plugins, callback_plugins +from . import action_plugins, callback_plugins, lookup_plugins exports = """ export ANSIBLE_CALLBACK_PLUGINS=${{ANSIBLE_CALLBACK_PLUGINS:-}}${{ANSIBLE_CALLBACK_PLUGINS+:}}{} export ANSIBLE_ACTION_PLUGINS=${{ANSIBLE_ACTION_PLUGINS:-}}${{ANSIBLE_ACTION_PLUGINS+:}}{} +export ANSIBLE_LOOKUP_PLUGINS=${{ANSIBLE_LOOKUP_PLUGINS:-}}${{ANSIBLE_LOOKUP_PLUGINS+:}}{} """.format( - callback_plugins, action_plugins + callback_plugins, action_plugins, lookup_plugins ) if "VIRTUAL_ENV" in os.environ: diff --git a/ara/setup/lookup_plugins.py b/ara/setup/lookup_plugins.py new file mode 100644 index 00000000..5c247832 --- /dev/null +++ b/ara/setup/lookup_plugins.py @@ -0,0 +1,23 @@ +# Copyright (c) 2019 Red Hat, Inc. +# +# This file is part of ARA Records Ansible. +# +# 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 . + +from __future__ import print_function + +from . import lookup_plugins + +if __name__ == "__main__": + print(lookup_plugins) diff --git a/doc/source/ansible-configuration.rst b/doc/source/ansible-configuration.rst index 6102aa24..ae4afe60 100644 --- a/doc/source/ansible-configuration.rst +++ b/doc/source/ansible-configuration.rst @@ -13,9 +13,11 @@ Once you've set up the ``callback_plugins`` configuration or the ``ANSIBLE_CALLBACK_PLUGINS`` environment variable, Ansible will automatically use the ARA callback plugin to start recording data. -If you'd like to use the ``ara_record`` action plugin to record arbitrary data -during your playbook, you'll need to set ``action_plugins`` and -``ANSIBLE_ACTION_PLUGINS`` as well. +``ANSIBLE_ACTION_PLUGINS`` or ``action_plugins`` must be set if you'd like to +use the ``ara_record`` or ``ara_playbook`` action plugins. + +If you would like to use the ``ara_api`` lookup plugin, then +``ANSIBLE_LOOKUP_PLUGINS`` or ``lookup_plugins`` must also be set. Using setup helper modules -------------------------- @@ -36,17 +38,22 @@ The modules can be used directly on the command line: $ python3 -m ara.setup.callback_plugins /usr/lib/python3.7/site-packages/ara/plugins/callback + $ python3 -m ara.setup.lookup_plugins + /usr/lib/python3.7/site-packages/ara/plugins/lookup + # Note: This doesn't export anything, it only prints the commands. # If you want to export directly from the command, you can use: # source <(python3 -m ara.setup.env) $ python3 -m ara.setup.env export ANSIBLE_CALLBACK_PLUGINS=/usr/lib/python3.7/site-packages/ara/plugins/callback export ANSIBLE_ACTION_PLUGINS=/usr/lib/python3.7/site-packages/ara/plugins/action + export ANSIBLE_LOOKUP_PLUGINS=/usr/lib/python3.7/site-packages/ara/plugins/lookup $ python3 -m ara.setup.ansible [defaults] callback_plugins=/usr/lib/python3.7/site-packages/ara/plugins/callback action_plugins=/usr/lib/python3.7/site-packages/ara/plugins/action + lookup_plugins=/usr/lib/python3.7/site-packages/ara/plugins/lookup Or from python, for example: @@ -59,3 +66,7 @@ Or from python, for example: >>> from ara.setup import action_plugins >>> print(action_plugins) /usr/lib/python3.7/site-packages/ara/plugins/action + + >>> from ara.setup import lookup_plugins + >>> print(lookup_plugins) + /usr/lib/python3.7/site-packages/ara/plugins/lookup diff --git a/doc/source/ara-api-lookup.rst b/doc/source/ara-api-lookup.rst new file mode 100644 index 00000000..d38f9071 --- /dev/null +++ b/doc/source/ara-api-lookup.rst @@ -0,0 +1,58 @@ +.. _ara_api_lookup: + +Querying ARA from inside playbooks +================================== + +ara_api +------- + +ARA comes with a built-in Ansible lookup plugin called ``ara_api`` that can be +made available by :ref:`configuring Ansible ` with the +``ANSIBLE_LOOKUP_PLUGINS`` environment variable or the ``lookup_plugins`` +setting in an ``ansible.cfg`` file. + +There is no other configuration required for this lookup plugin to work since +it retrieves necessary settings (such as API server endpoint and authentication) +from the callback plugin. + +The ``ara_api`` lookup plugin can be used to do free-form queries to the +ARA API while the playbook is running: + +.. code-block:: yaml + + - name: Test playbook + hosts: localhost + tasks: + - name: Get list of playbooks + set_fact: + playbooks: "{{ lookup('ara_api', '/api/v1/playbooks') }}" + +ara_playbook +------------ + +The ``ara_playbook`` Ansible action plugin can be enabled by +:ref:`configuring Ansible ` with the +``ANSIBLE_ACTION_PLUGINS`` environment variable or the ``action_plugins`` +setting in an ``ansible.cfg`` file. + +There is no other configuration required for this action plugin to work since +it retrieves necessary settings (such as API server endpoint and authentication) +from the callback plugin. + +The ``ara_playbook`` action plugin can be used in combination with ``ara_api`` +to query the API about the current playbook: + +.. code-block:: yaml + + - name: Test playbook + hosts: localhost + tasks: + - name: Get the currently running playbook + ara_playbook: + register: playbook_query + + - name: Get failed tasks for the currently running playbook + vars: + playbook_id: "{{ playbook_query.playbook.id | string }}" + set_fact: + tasks: "{{ lookup('ara_api', '/api/v1/tasks?status=failed&playbook=' + playbook_id) }}" diff --git a/doc/source/index.rst b/doc/source/index.rst index be667b34..e827836a 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -20,6 +20,7 @@ Table of Contents API: Distributed sqlite backend Setting playbook names and labels Recording arbitrary data in playbooks + Querying ARA from inside playbooks Contributing to ARA .. toctree:: diff --git a/tests/basic.yaml b/tests/basic.yaml index e0744dda..bf2394f3 100644 --- a/tests/basic.yaml +++ b/tests/basic.yaml @@ -105,6 +105,7 @@ - environment: ANSIBLE_CALLBACK_PLUGINS: "{{ ara_setup_plugins.stdout }}/callback" ANSIBLE_ACTION_PLUGINS: "{{ ara_setup_plugins.stdout }}/action" + ANSIBLE_LOOKUP_PLUGINS: "{{ ara_setup_plugins.stdout }}/lookup" ARA_DEBUG: "{{ ara_api_debug }}" ARA_LOG_LEVEL: "{{ ara_api_log_level }}" ARA_BASE_DIR: "{{ ara_api_root_dir }}/server" @@ -120,6 +121,9 @@ - name: Run smoke.yaml integration test command: "ansible-playbook -vvv {{ _test_root }}/smoke.yaml" + - name: Run lookup integration tests + command: "ansible-playbook -vvv {{ _test_root }}/lookups.yaml" + - name: Run hosts.yaml integration test command: "ansible-playbook -vvv {{ _test_root }}/hosts.yaml" diff --git a/tests/integration/lookups.yaml b/tests/integration/lookups.yaml new file mode 100644 index 00000000..7e55cbdd --- /dev/null +++ b/tests/integration/lookups.yaml @@ -0,0 +1,33 @@ +- name: Assert playbook properties + hosts: localhost + gather_facts: yes + vars: + ara_playbook_name: ARA self tests + ara_playbook_labels: + - lookup-tests + tasks: + - name: Retrieve the current playbook so we can get the ID + ara_playbook: + register: playbook_query + + - name: Recover data from ARA + vars: + playbook_id: "{{ playbook_query.playbook.id | string }}" + set_fact: + playbook: "{{ lookup('ara_api', '/api/v1/playbooks/' + playbook_id) }}" + tasks: "{{ lookup('ara_api', '/api/v1/tasks?playbook=' + playbook_id) }}" + results: "{{ lookup('ara_api', '/api/v1/results?playbook=' + playbook_id) }}" + + - name: Assert playbook properties + assert: + that: + - playbook.name == 'ARA self tests' + - "playbook.labels | selectattr('name', 'search', 'lookup-tests') | list | length == 1" + - playbook.ansible_version == ansible_version.full + - playbook_dir in playbook.path + - "'tests/integration/lookups.yaml' in playbook.path" + - "playbook.files | length == playbook['items']['files']" + - "playbook.hosts | length == playbook['items']['hosts']" + - "playbook.plays | length == playbook['items']['plays']" + - "tasks.results | length == playbook['items']['tasks']" + - "results.results | length == playbook['items']['results']" diff --git a/tests/test_tasks.yaml b/tests/test_tasks.yaml index 956818a1..df56b396 100644 --- a/tests/test_tasks.yaml +++ b/tests/test_tasks.yaml @@ -78,6 +78,7 @@ - environment: ANSIBLE_CALLBACK_PLUGINS: "{{ ara_setup_plugins.stdout }}/callback" ANSIBLE_ACTION_PLUGINS: "{{ ara_setup_plugins.stdout }}/action" + ANSIBLE_LOOKUP_PLUGINS: "{{ ara_setup_plugins.stdout }}/lookup" ARA_SETTINGS: "{{ ara_api_settings }}" ARA_API_CLIENT: "{{ ara_api_client | default('offline') }}" ARA_API_SERVER: "{{ ara_api_server | default('http://127.0.0.1:8000') }}" @@ -90,6 +91,9 @@ - name: Run smoke.yaml integration test command: "ansible-playbook -vvv {{ _test_root }}/smoke.yaml" + - name: Run lookups.yaml integration test + command: "ansible-playbook -vvv {{ _test_root }}/lookups.yaml" + - name: Run hosts.yaml integration test command: "ansible-playbook -vvv {{ _test_root }}/hosts.yaml"