├── ansible.cfg ├── server_config.ini ├── requirements.txt ├── playbook.yml ├── Makefile ├── TODO.md ├── api_server ├── README.md └── server.py ├── README.md └── callback_plugins └── example_callback_plugin.py /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | callback_plugins = ./callback_plugins 3 | retry_files_enabled = False 4 | -------------------------------------------------------------------------------- /server_config.ini: -------------------------------------------------------------------------------- 1 | [defaults] 2 | host = 127.0.0.1 3 | port = 5000 4 | debug_enabled = True 5 | 6 | [auth] 7 | username = admin 8 | password = password 9 | 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==3.0.2 2 | certifi==2018.4.16 3 | chardet==3.0.4 4 | click==6.7 5 | configparser==3.5.0 6 | Flask==1.0.2 7 | Flask-HTTPAuth==3.2.4 8 | Flask-RESTful==0.3.6 9 | idna==2.7 10 | itsdangerous==0.24 11 | Jinja2==2.10 12 | MarkupSafe==1.0 13 | pytz==2018.5 14 | requests==2.19.1 15 | six==1.11.0 16 | urllib3==1.23 17 | Werkzeug==0.14.1 18 | -------------------------------------------------------------------------------- /playbook.yml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ansible-playbook 2 | # 3 | --- 4 | - name: call the callback plugin with a failed task 5 | hosts: localhost 6 | connection: local 7 | gather_facts: False 8 | vars: 9 | output: b 10 | 11 | tasks: 12 | - debug: 13 | var: test 14 | 15 | - name: assert that output is a 16 | assert: 17 | that: output == 'a' 18 | msg: output does not equal a 19 | register: assert_register 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: run 2 | run: 3 | python api_server/server.py 4 | 5 | .PHONY: list 6 | list: 7 | curl -X GET http://127.0.0.1:5000/tasks -v 8 | 9 | .PHONY: post 10 | post: 11 | curl -u admin:password -X POST http://127.0.0.1:5000/tasks -d "task_name=test" -d "task_output=test_output" -d "state=failed" -v 12 | ` 13 | .PHONY: playbook 14 | playbook: 15 | ansible-playbook playbook.yml -e callback_url="http://127.0.0.1:5000/tasks" -e username=admin -e password=password 16 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - have playbook send stats from endofplay to this endpoint 2 | - be able to send data to a endpoint for plays 3 | - implement async timeout somewhere to simulate long running posts 4 | - find out what impact is on playbook execution for waiting for a post to get through 5 | - make target for succes 6 | - make target for failed 7 | - make target for stats 8 | - make __main__ only run functions 9 | - provide option for capturing a play with tasks 10 | - provide option for capturing only tasks and posting tasks seperately 11 | 12 | - implement simple db for storing plays and tasks 13 | -------------------------------------------------------------------------------- /api_server/README.md: -------------------------------------------------------------------------------- 1 | # mock-server for callback plugin testing 2 | 3 | ## Configure the server 4 | 5 | Using the server\_config.ini file 6 | 7 | ### configuration parameters 8 | host = the ip address the server should serve from - default: 0.0.0.0 9 | port = the port the server should server from - default: 5000 10 | user = the username for basic auth - default: admin 11 | password = the password for basic auth - default: password 12 | 13 | ## To run the server 14 | 15 | From repository root: 16 | 17 | * `python3 -m venv env` or `virtualenv env` (for python2.x) 18 | * `source env/bin/activate` 19 | * `pip install -r requirements.txt` 20 | * optionally configure api server defaults in server\_config.ini 21 | * `make run` 22 | 23 | ### available functions 24 | 25 | * post using `make post` (requires auth) 26 | * list using `make list` 27 | -------------------------------------------------------------------------------- /api_server/server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from flask import Flask 3 | from flask_restful import reqparse, Resource, Api 4 | from flask_httpauth import HTTPBasicAuth 5 | 6 | try: 7 | import configparser 8 | except ImportError: 9 | import ConfigParser as configparser 10 | 11 | app = Flask(__name__) 12 | auth = HTTPBasicAuth() 13 | api = Api(app) 14 | 15 | # api in memory endpoints 16 | tasks = { 17 | '1': {'task_name': 'sometask', 18 | 'task_output': 'I have failed', 19 | 'state': 'failed'} 20 | } 21 | 22 | # parser configuration for POST request 23 | parser = reqparse.RequestParser() 24 | parser.add_argument('task_name') 25 | parser.add_argument('task_output') 26 | parser.add_argument('state') 27 | 28 | @auth.get_password 29 | def get_pw(username): 30 | if username in users: 31 | return users.get(username) 32 | return None 33 | 34 | class TaskList(Resource): 35 | def get(self): 36 | return tasks 37 | 38 | @auth.login_required 39 | def post(self): 40 | args = parser.parse_args() 41 | task_id = str(len(tasks.keys()) + 1) 42 | tasks[task_id] = {'task_name': args['task_name'], 43 | 'task_output': args['task_output'], 44 | 'state': args['state']} 45 | print(task_id) 46 | print(tasks[task_id]) 47 | return tasks[task_id], 201 48 | 49 | api.add_resource(TaskList, '/tasks') 50 | 51 | if __name__ == "__main__": 52 | 53 | config = configparser.ConfigParser() 54 | config_file = ('server_config.ini') 55 | 56 | try: 57 | open('server_config.ini', 'r') 58 | except Exception as err: 59 | print('Error: {0}\n Unable to get read file {1}.'.format(err, config_file)) 60 | sys.exit(1) 61 | 62 | config.read(config_file) 63 | 64 | # defaults 65 | host = config.get('defaults', 'host') 66 | port = config.get('defaults', 'port') 67 | debug_enabled = config.get('defaults', 'debug_enabled') 68 | 69 | # auth 70 | username = config.get('auth', 'username') 71 | password = config.get('auth', 'password') 72 | users = { 73 | username: password 74 | } 75 | 76 | # start up the server 77 | app.run(debug=debug_enabled, host=host, port=port) 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ANSIBLE CALLBACK PLUGIN 2 | 3 | This repository shows how to use a custom ansible callback plugin to send the output 4 | of every task to an API endpoint. 5 | 6 | The plugin will record the play and upon detecting a succesfull or failed task will be triggered to store the output of that task and send it using the python requests module to an API endpoint. 7 | 8 | Configuration of the plugin is done by passing extra\_vars to the playbook that will use the callback plugin. 9 | 10 | We will be using a mock api server to be found in the api\_server directory to handle the requests that we send to it to demonstrate the working of the callback plugin. 11 | 12 | ## Configuration parameters 13 | 14 | It is possible to expose configuration parameters to the callback plugin as extra\_vars or from playbook variables. 15 | 16 | - API Endpoint url 17 | - username 18 | - password 19 | 20 | [extra\_vars] 21 | callback\_url = the api endpoint callback url 22 | username = the username for api authentication 23 | password = the password for api authentication 24 | 25 | ## Outputs 26 | 27 | For tasks the current payload is sent to the API backend 28 | 29 | state = success or failed 30 | task\_name = the name of the task that was succesfull or failed 31 | task\_output = the output of the task 32 | 33 | ## How it works 34 | 35 | The custom ansible callback plugin is to be placed in a directory called callback\_plugins in the directory where the playbook is run from. 36 | In the ansible.cfg file specify the custom callback\_plugins directory: 37 | [defaults] 38 | callback\_plugins = ./callback\_plugins 39 | 40 | Every callback plugin there of type notification will be picked up and run for all the runner calls it is configured for. 41 | 42 | The plugin will make use of the python requests library to send the captured payload from a runner call as payload to a configured API endpoint. HTTPBasicAuth is supported for simulating calls that require API authentication. 43 | 44 | For simulating the API endpoint a small server is implemented using Flask and flask\_restfull, this can be found in the api\_server directory. 45 | 46 | All default available callback\_plugins can be found in your ansible install location, usually `/usr/lib/python2.7/site-packages/ansible/plugins/callback` 47 | 48 | ## How to run 49 | 50 | Be sure to have the api\_server provided running or have your own API endpoint available which supports the fields {'state, 'task\_name', task\_output'} 51 | 52 | ### Setup api server 53 | Using the provided api\_server: 54 | * `python3 -m venv env` or `virtualenv env` (for python2.x) 55 | * `source env/bin/activate` 56 | * `pip install -r requirements.txt` 57 | * optionally configure api server defaults in server\_config.ini 58 | * `make run` 59 | 60 | ### Run the callback plugin using Makefile 61 | Be sure to run from virtualenv as python requests library is required. 62 | `make playbook` 63 | You might have to change the extra vars passed for that makefile target to fit your configuration. 64 | 65 | ### Or run Manual 66 | `ansible-playbook playbook.yml -e callback_url= -e username= -e password=` 67 | -------------------------------------------------------------------------------- /callback_plugins/example_callback_plugin.py: -------------------------------------------------------------------------------- 1 | # written for python2.7 2 | # Make coding more python3-ish 3 | from __future__ import (absolute_import, division, print_function) 4 | __metaclass__ = type 5 | 6 | import requests 7 | from requests.auth import HTTPBasicAuth 8 | 9 | from ansible.plugins.callback import CallbackBase 10 | from ansible import constants as C 11 | from __main__ import cli 12 | 13 | try: 14 | import configparser 15 | except ImportError: 16 | import ConfigParser as configparser 17 | 18 | DOCUMENTATION = ''' 19 | callback: example_callback_plugin 20 | type: notification 21 | short_description: Send callback on various runners to an API endpoint. 22 | description: 23 | - On ansible runner calls report state and task output to an API endpoint. 24 | - Configuration via callback_config.ini, place the file in the same directory 25 | as the plugin. 26 | requirements: 27 | - python requests library 28 | - HTTPBasicAuth library from python requests.auth 29 | - ConfigParser for reading configuration file 30 | ''' 31 | 32 | class CallbackModule(CallbackBase): 33 | 34 | ''' 35 | Callback to API endpoints on ansible runner calls. 36 | ''' 37 | 38 | CALLBACK_VERSION = 2.0 39 | CALLBACK_TYPE = 'notification' 40 | CALLBACK_NAME = 'example_callback_plugin' 41 | 42 | def __init__(self, *args, **kwargs): 43 | super(CallbackModule, self).__init__() 44 | 45 | def v2_playbook_on_start(self, playbook): 46 | self.playbook = playbook 47 | print(playbook.__dict__) 48 | 49 | def v2_playbook_on_play_start(self, play): 50 | self.play = play 51 | self.extra_vars = self.play.get_variable_manager().extra_vars 52 | self.callback_url = self.extra_vars['callback_url'] 53 | self.username = self.extra_vars['username'] 54 | self.password = self.extra_vars['password'] 55 | 56 | print('\nExtra vars that were passed to playbook are accessible to the callback plugin by calling the variable_manager on the play object for the method v2_playbook_on_play_start:\nextra_vars: {0}'.format(self.extra_vars)) 57 | 58 | def v2_runner_on_ok(self, result): 59 | payload = {'state': 'success', 60 | 'task_name': result.task_name, 61 | 'task_output' : result._result 62 | } 63 | print('On a succesfull task - Sending to endpoint:\n{0}\n'. 64 | format(requests.post(self.callback_url, 65 | auth=(self.username,self.password), 66 | data=payload).json())) 67 | pass 68 | 69 | def v2_runner_on_failed(self, result, ignore_errors=False): 70 | payload = {'state': 'failed', 71 | 'task_name': result.task_name, 72 | 'task_output' : result._result['msg'] 73 | } 74 | 75 | print('On a failed task - Sending to endpoint:\n{0}\n' 76 | .format(requests.post(self.callback_url, 77 | auth=(self.username,self.password), 78 | data=payload).json())) 79 | pass 80 | 81 | def v2_playbook_on_stats(self, stats): 82 | hosts = sorted(stats.processed.keys()) 83 | print(dir(stats)) 84 | print(stats.__dict__) 85 | for host in hosts: 86 | print(stats.summarize(host)) 87 | --------------------------------------------------------------------------------