Avatar (Fabio Alessandro Locati|Fale)'s blog

Obtain an Ansible Job output from Ansible Tower API

April 27, 2022

A couple of years back, I wrote a blogpost on obtaining the previous Job ID in Ansible Tower workflow. Now, let’s go further and create another module to obtain the output of such a job.

Before moving further, I want to specify that I talk about Ansible Tower since this is the most known name for this software, but I could also be talking about AWX or Ansible Controller since those are the same codebase. AWX is the open-source upstream project. Ansible Tower is the former name of the Red Hat product based on AWX. Ansible Controller is the name of the Red Hat Ansible Automation Platform 2 component based on AWX.

The reasons to be interested in the output of an Ansible Job are multiple, but the most frequent two by far are notifications of failure and reports. Both would benefit from a different data structure than what Ansible usually returns. Ansible output is task-based, while a host-based output would be simpler to parse for those kinds of use.

The code of the said module is:

 3ANSIBLE_METADATA = {'metadata_version': '1.1',
 4                    'status': ['preview'],
 5                    'supported_by': 'none'}
 7from ansible.module_utils.basic import AnsibleModule
 8import json
 9import requests
11def main():
13    module = AnsibleModule(
14        supports_check_mode = True,
15        argument_spec = dict(
16            tower_base_url = dict(type='str', required=True),
17            tower_username = dict(type='str', required=True),
18            tower_password = dict(type='str', required=True, no_log=True),
19            tower_job_id = dict(type='str', required=True),
20        ),
21    )
23    output = dict(
24        changed=False,
25        value='',
26    )
28    tower_base_url = module.params['tower_base_url']
29    tower_username = module.params['tower_username']
30    tower_password = module.params['tower_password']
31    tower_job_id = module.params['tower_job_id']
33    url = '/api/v2/jobs/' + tower_job_id + '/job_events'
34    responses = []
35    hosts = {}
36    while True:
37        response = requests.get(tower_base_url + url, auth=requests.auth.HTTPBasicAuth(tower_username, tower_password), verify=False)
38        for result in response.json()['results']:
39            if result['host_name'] != "":
40                if result['changed']:
41                    hosts.setdefault(result['host_name'],[]).append(result['task'])
42        if response.json()['next'] == None:
43            break
44        url = response.json()['next']
45    output['value'] = hosts
46    module.exit_json(**output)
48if __name__ == '__main__':
49    main()

As you can see, if we exclude the Python boilerplate and the Ansible Module boilerplate, the real code is between lines 33 and 44.

Due to Ansible Tower API’s paging, the module will call the API multiple times to get results (line 37). Each response is then parsed (lines 38-41), and every task output is placed in the right place of the structure that will then be returned (line 46).

If you want to use this module in your Ansible Playbooks, you need to put the file into the library folder (i.e.: library/tower_job_output.py), and then you can call it with the following YAML code from your Ansible Playbook tasks list:

- name: Get the previous job in the current workflow
    tower_base_url: "{{ tower_base_url }}"
    tower_username: "{{ tower_username }}"
    tower_password: "{{ tower_password }}"
    tower_job_id: "{{ job_id }}"
  register: job

In my case, tower_base_url, tower_username, and tower_password are variables set from Ansible Tower secrets, and I would suggest you do it similarly so you don’t have to put your credentials within the Ansible Playbook.

The output will be a map where keys are the Ansible target hosts and the value an array of tasks output relative to that host or, more succinctly: map[host][]task_output.

I hope this can help you better understand how you can quickly write an Anisbe Module to extend the capability of your Ansible Tower to cover some edge cases that you might need in your specific situation.