Ansible triggered by a consumed RabbitMQ message

At work we settled on Ansible Tower to take care of scheduling/triggering Ansible playbook runs, however, during the evaluation of Tower the question was always there:  “Do we need Tower?  Can we trigger Ansible playbooks another way with existing systems?”

Tower is the right choice for us at the moment due to all the features it brings, such as, role based access control (RBAC), API interfaces, logging and much more…

At home the question still intrigued me,  how to trigger an Ansible playbook run in a eloquent manner.

Whilst it would be simple and effective enough to SSH into a box and execute an ansible-playbook command line (ssh user@box ‘ansible-playbook playbook.yml’), it didn’t feel eloquent.

Under the hood Tower uses RabbitMQ, celery, postgresql and Django.  I wondered what it would take to trigger an Ansible playbook run via a RabbitMQ message.

Ansible-runner to the rescue

https://github.com/ansible/ansible-runner states:

A tool and python library that helps when interfacing with Ansible directly or as part of another system whether that be through a container image interface, as a standalone tool, or as a Python module that can be imported. The goal is to provide a stable and consistent interface abstraction to Ansible.

Fantastic!  From the documentation site: https://ansible-runner.readthedocs.io/en/latest/

Ansible Runner represents the modularization of the part of Ansible Tower/AWX that is responsible for running ansible and ansible-playbook tasks and gathers the output from it. It does this by presenting a common interface that doesn’t change, even as Ansible itself grows and evolves.

Workflow

Breaking down the requirement, I wanted to:

  1. Connect to a RabbitMQ server and subscribe to a queue.
  2. Upon receiving a message on the queue, trigger an Ansible runner playbook execution, or, ad-hoc command.
  3. Print out results to console.
  4. Sit and wait for the next message on the queue.

Python packages required

The following packages make it very easy to do this.

pika is a python package written in python for talking to RabbitMQ (link):

Pika is a pure-Python implementation of the AMQP 0-9-1 protocol that tries to stay fairly independent of the underlying network support library.

ansible-runner is as discussed above… checkout the documentation (here).

The script below basically smashes together two examples from:

You obviously need a RabbitMQ server and queue setup to run this.

 

Using pika and ansible-runner to execute ansible via a message queue

The example code is available here: https://github.com/Im0/ansible-runner-rabbitmq

import ansible_runner
import pika

'''
Just a rough a ready example of combining ansible_runner with pika.  Pika subscribes to a
channel and when a message is present, it grabs the message and fires ansible-runner.

Basically smashing together examples from:
* https://ansible-runner.readthedocs.io/en/latest/python_interface.html#usage-examples
* https://pika.readthedocs.io/en/stable/examples/blocking_consume.html

Consider:
* If content-type of the rabbitmq message is JSON... load that into a dict.
* Using data from the queue in the ansible-runner (extravars).
* How to pass large data to ansible-runner.
* Security considerations
'''

def on_message(channel, method_frame, header_frame, body):
    print(method_frame.delivery_tag)
    print(body)
    print()
    exec_ansible_runner()
    channel.basic_ack(delivery_tag=method_frame.delivery_tag)

def exec_ansible_runner():
    # Use private_data_dir if you want the output of the ansible run saved
    #r = ansible_runner.run(private_data_dir='/tmp/demo', host_pattern='localhost', module='shell', module_args='whoami')
    r = ansible_runner.run(json_mode=True, host_pattern='localhost', module='shell', module_args='whoami')
    print("{}: {}".format(r.status, r.rc))
    # successful: 0
    for each_host_event in r.events:
        print(each_host_event['event'])
    print("Final status:")
    print(r.stats)

def main():
    url = 'amqp://guest:[email protected]:5672/%2F'
    parameters = pika.URLParameters(url)
    connection = pika.BlockingConnection(parameters)
    channel = connection.channel()
    channel.basic_consume(on_message, 'hello')
    try:
        channel.start_consuming()
    except KeyboardInterrupt:
        channel.stop_consuming()
    connection.close()

if __name__ == "__main__":
    main()


This was only intended as a basic example, which, could be extended.

 

Leave a Reply

Your email address will not be published. Required fields are marked *