Get Python ansible_runner module STDOUT Key Value

1.7k Views Asked by At

I'm using ansbile_runner Python module as bellow:

import ansible_runner
r = ansible_runner.run(private_data_dir='/tmp/demo', playbook='test.yml')

When I execute the above code, it will show the output without printing in Python. What I want is to save the stdout content into a Python variable for further text processing.

2

There are 2 best solutions below

1
TRW On

Did you read the manual under https://ansible-runner.readthedocs.io/en/stable/python_interface/ ? There is an example where you add another parameter, which is called output_fd and that could be a file handler instead of sys.stdout.

Sadly, this is a parameter of the run_command function and the documentation is not very good. A look into the source code at https://github.com/ansible/ansible-runner/blob/devel/ansible_runner/interface.py could help you.

According to the implementation details in https://github.com/ansible/ansible-runner/blob/devel/ansible_runner/runner.py it looks like, the run() function always prints to stdout.

According to the interface, there is a boolean flag in run(json_mode=TRUE) that stores the response in JSON (I expect in r instead of stdout) and there is another boolean flag quiet.

I played around a little bit. The relevant option to avoid output to stdout is quiet=True as run() attribute.

Ansible_Runner catches the output and writes it to a file in the artifacts directory. Every run() command produces that directory as described in https://ansible-runner.readthedocs.io/en/stable/intro/#runner-artifacts-directory-hierarchy. So there is a file called stdout in the artifact directory. It contains the details. You can read it as JSON.

But also the returned object contains already some relevant data. Here is my example

playbook = 'playbook.yml'
private_data_dir = 'data/' # existing folder with inventory etc
work_dir = 'playbooks/' # contains playbook and roles

try:
  logging.debug('Running ansible playbook {} with private data dir {} in project dir {}'.format(playbook, private_data_dir, work_dir))
  runner = ansible_runner.run(
    private_data_dir=private_data_dir, 
    project_dir=work_dir, 
    playbook=playbook,
    quiet=True,
    json_mode=True
  )

  processed = runner.stats.get('processed')
  failed = runner.stats.get('failures')
  
  # TODO inform backend
  for host in processed:
    if host in failed:
      logging.error('Host {} failed'.format(host))
    else:
      logging.debug('Host {} backupd'.format(host))

  logging.error('Playbook runs into status {} on inventory {}'.format(runner.status, inventory.get('name')))

  if runner.rc != 0:
    # we have an overall failure
  else:
    # success message
except BaseException as err:
  logging.error('Could not process ansible playbook {}\n{}'.format(inventory.get('name'),err))

So this outputs all processed hosts and informs about failures per host. Concrete more output can be found in the stdout file in artifact directory.

0
Mike Pennington On

@Mahdi said:

yes I read that manual. I need to save the content of stdout to a variable in run command.

This is an example of how to get stdout from your question...

Code in the OP's Question

import ansible_runner
r = ansible_runner.run(private_data_dir='/tmp/demo', playbook='test.yml')

Retrieving stdout as a variable

Use python contextlib to save the output of ansible_runner.run() stdout as a variable named stdout...

For simplicity, I assume there is only one ansible host in the inventory.

import contextlib
import json
import io

import ansible_runner

def unpack_ansible_results(json_data: list[dict]) -> str:

    # Magic failure string...
    stdout = "__FAILURE__"

    for jsonl in json_data:

        # Get all event_data...
        event_data = jsonl.get("event_data")
        if isinstance(event_data, dict):
            # Check if the result is in event_data...
            result = event_data.get("res")

            if isinstance(result, dict) and "stdout" in result.keys():
                stdout = result["stdout"]

    return stdout

def main():

    success = False

    # Sometimes ansible runs fail, this runs in a loop until 
    #     it succeeds.  A simple modification to this might be limiting
    #     the number of retries.
    while not success:
        ooo = io.StringIO()
        with contextlib.redirect_stdout(ooo):

            # ansible_runner.run(json_mode=True) sends something similar 
            #      to jsonl lines to stdout
            runner = ansible_runner.run(private_data_dir=".",
                                        # send ouptut to stdout as json
                                        json_mode=True,
                                        inventory='ansible_inventory.ini',
                                        playbook='ansible_playbook.yml')

        #######################################################################
        # Get ansible_runner stdout from contextlib
        #######################################################################

        #     Build a json string from the returned ansible jsonl lines...
        json_string = "[" + ",".join(ooo.getvalue().strip().splitlines()) + "]"

        json_data = None
        try:
            json_data = json.loads(json_string)
            success = True
        except json.decoder.JSONDecodeError:
            print("FAILURE!")

    processed = runner.stats.get('processed')
    for host in processed:
        print("GOOD", host)

    failed = runner.stats.get('failures')
    for host in failed:
        print("FAIL", host)

    stdout = unpack_ansible_results(json_data)
    
if __name__ == "__main__":
    main()

I'm using this in my playbook to get the directory output from a webserver called foo.localdomain. It is assumed that you can already ssh to the server with public-key authentication and that the server is listed in the ansible_inventory.ini file.

ansible_playbook.yml

---
- name: Perform directory on server
  hosts: foo.localdomain
  remote_user: mpenning

  tasks:
  - name: Run a remote unix ls command
    shell:
      cmd: ls -la | grep mpenning
      chdir: public_html

Packages

This answer uses:

$ pip freeze | grep -i ansible
ansible==9.1.0
ansible-base==2.10.17
ansible-core==2.16.2
ansible-runner==2.3.4
$