Call a Python script from an Ansible task

I was writing a playbook where I needed to create LVM logical volumes on a bunch of physical disks, some of which were partially occupied by plain ext4 (i.e. non-LVM) partitions. This Ansible task required some arithmetic calculations and logical operations based on current disk usage and desired outcomes.

Now, Ansible is great at gathering facts and effecting changes but a bit unwieldy when you want to do arithmetic + logical things, so the solution was to call a Python script that would take the current disk layout as argument, and return the desired number/names/sizes/layouts of the LVM VGs and LVs. This Python script would be invoked from an Ansible task, and the data returned from the Python script would be the basis for subsequent Ansible tasks.

How does a Python script “take arguments from” and “return values to” an Ansible task? The answer is YAML, with the to_yaml and from_yaml filters. Here’s the basic idea:

Ansible:

- name: Render device facts in YAML
  set_fact:
    afd: "{{ ansible_devices | to_nice_yaml }}"
    afdl: "{{ ansible_device_links | to_nice_yaml }}"

- name: Invoke disk_layout helper script
  local_action: command ./disk_layout.py "{{ afd }}" "{{ afdl }}"
  register: lcmd_op

- name: Parse output of disk_layout helper script
  set_fact:
    disk_layout: "{{ lcmd_op.stdout | from_yaml }}"

Python:

def main():
    ansible_device_facts = yaml.safe_load(sys.argv[1])
    ansible_device_links_facts = read_ansible_facts(sys.argv[2])

    # The values calculated by this script will be returned in this
    # dict
    results = dict()
    
    for master in ansible_device_links_facts['masters']:
        for pv in ansible_device_links_facts['masters'][master]:
            pass
            # populate 'results'
    
    for device in ansible_device_facts:
        # Ignore removable drives
        if ansible_device_facts[device]['removable'] != '0':
            continue
        # populate 'results'

    # Return 'results' to Ansible, as a YAML string on stdout
    print(yaml.dump(results))
    sys.exit(0)