Developing Callback Plugins
Relevant source files
The following files were used as context for generating this wiki page:
Callback plugins in Ansible provide a flexible way to hook into the execution of playbooks and react to events. They are primarily used to control and customize the output displayed to the user, but can also be used for logging, sending notifications, or integrating with other systems. This allows developers to extend Ansible's functionality by triggering custom actions at various points in a playbook's lifecycle.
Callback plugins are executed on the Ansible controller node and can respond to events such as a play starting, a task succeeding, or a host failing. The junit callback plugin, for example, aggregates task results and generates a JUnit formatted XML report, which is useful for CI/CD integration.
Core Concepts
All callback plugins are Python classes that inherit from a base class and implement specific methods that act as event hooks.
Plugin Class Definition
A callback plugin must be defined as a class named CallbackModule that inherits from CallbackBase.
Sources: lib/ansible/plugins/callback/junit.py:118
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
# ... plugin implementation ...
The plugin class must also define several metadata attributes that identify it to Ansible.
| Attribute | Description | Example (from junit) |
|---|---|---|
CALLBACK_VERSION | The version of the callback API the plugin uses. | 2.0 |
CALLBACK_TYPE | The type of plugin. For callbacks, this can be aggregate or other types. | aggregate |
CALLBACK_NAME | The name of the callback plugin, used to enable it in ansible.cfg. | junit |
CALLBACK_NEEDS_ENABLED | A boolean indicating if the plugin must be explicitly enabled to run. | True |
Sources: lib/ansible/plugins/callback/junit.py:151-155
Event Hooks
Callback plugins work by overriding methods from the CallbackBase class. These methods correspond to specific events in the Ansible execution flow. When an event occurs, Ansible calls the corresponding method in all enabled callback plugins.
The following diagram illustrates the sequence of some common event hooks as demonstrated in the junit plugin.
This sequence diagram shows the order in which event hooks are called on a callback plugin during a typical playbook run.
Sources: lib/ansible/plugins/callback/junit.py:320-344
Key event hooks found in the junit plugin include:
v2_playbook_on_start(playbook): Called at the beginning of a playbook run.v2_playbook_on_play_start(play): Called when a new play starts.v2_playbook_on_task_start(task, is_conditional): Called when a task is about to start.v2_runner_on_ok(result): Called when a task completes successfully on a host.v2_runner_on_failed(result, ignore_errors): Called when a task fails on a host.v2_runner_on_skipped(result): Called when a task is skipped on a host.v2_playbook_on_include(included_file): Called when a file is included.v2_playbook_on_stats(stats): Called at the end of the playbook run, providing final statistics. This is often used for final processing or report generation.
File Structure and Location
New plugins should be developed within Ansible Collections, not in the core ansible-core repository.
Sources: AGENTS.md:202
Within a collection, callback plugins reside in the plugins/callback/ directory.
my_namespace/
└── my_collection/
├── galaxy.yml
└── plugins/
└── callback/
└── my_callback.py
Sources: test/integration/targets/ansible-galaxy-collection-cli/files/expected.txt:44
Implementation Example: The junit Plugin
The junit callback plugin serves as a practical example of how to build a stateful, aggregate callback. It collects results from all tasks and generates a JUnit XML report at the end of the playbook run.
Initialization and Configuration
The __init__ method is used to set up the plugin's initial state and read configuration from environment variables.
# lib/ansible/plugins/callback/junit.py:157-166
def __init__(self) -> None:
super(CallbackModule, self).__init__()
self._output_dir = os.getenv('JUNIT_OUTPUT_DIR', os.path.expanduser('~/.ansible.log'))
self._task_class = os.getenv('JUNIT_TASK_CLASS', 'False').lower()
self._fail_on_change = os.getenv('JUNIT_FAIL_ON_CHANGE', 'False').lower()
# ... other options
self._playbook_path = None
self._playbook_name = None
self._task_data: dict[str, TaskData] = {}
The junit plugin is configured via environment variables, as defined in its DOCUMENTATION string.
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
output_dir | JUNIT_OUTPUT_DIR | ~/.ansible.log | Directory to write XML files to. |
task_class | JUNIT_TASK_CLASS | False | Configure the output to be one class per yaml file. |
task_relative_path | JUNIT_TASK_RELATIVE_PATH | none | Use relative paths to a given directory. |
fail_on_change | JUNIT_FAIL_ON_CHANGE | False | Consider tasks reporting "changed" as a failure. |
fail_on_ignore | JUNIT_FAIL_ON_IGNORE | False | Consider failed tasks as a failure even if ignore_errors is set. |
include_setup_tasks_in_report | JUNIT_INCLUDE_SETUP_TASKS_IN_REPORT | True | Include setup tasks in the final report. |
hide_task_arguments | JUNIT_HIDE_TASK_ARGUMENTS | False | Hide task arguments in the output. |
test_case_prefix | JUNIT_TEST_CASE_PREFIX | <empty> | Prefix to identify tasks as test cases. |
Sources: lib/ansible/plugins/callback/junit.py:11-78
State Management
The junit plugin uses two internal data classes, TaskData and HostData, to store state during the playbook run.
This class diagram shows the relationship between CallbackModule, TaskData, and HostData for state management in the junit plugin.
v2_playbook_on_task_start: ATaskDataobject is created and stored inself._task_datadictionary, keyed by the task's UUID.v2_runner_on_*hooks: AHostDataobject is created with the result and status for a specific host and added to the correspondingTaskDataobject.v2_playbook_on_stats: This hook triggers_generate_report, which iterates through the collected_task_datato build and write the final XML report.
Sources: lib/ansible/plugins/callback/junit.py:178-232, lib/ansible/plugins/callback/junit.py:302-344, lib/ansible/plugins/callback/junit.py:346-378
Documentation
All plugins require a DOCUMENTATION string, which is a YAML-formatted string containing metadata about the plugin. This documentation is used by ansible-doc to generate help text.
Key fields in the DOCUMENTATION string include:
name: The plugin name.type: The plugin type (e.g.,aggregate).short_description: A one-line summary.description: A more detailed explanation.version_added: The version of Ansible or the collection when the plugin was introduced.options: A dictionary describing the configuration options for the plugin.
Sources: lib/ansible/plugins/callback/junit.py:9-79, lib/ansible/cli/doc.py:532-540
Alternatively, documentation can be provided in a "sidecar" .yml file with the same base name as the plugin file.
Sources: lib/ansible/cli/doc.py:203-205
Validation and Best Practices
- Import Order: In module and plugin files, all Python
importstatements must appear after theDOCUMENTATION,EXAMPLES, andRETURNstring variables.
Sources: test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py:465-474 - Licensing: All code contributed to
ansible-coremust be GPLv3 compatible.
Sources: AGENTS.md:15 - Testing: Use the
ansible-testcommand to run sanity, unit, and integration tests for your plugin.
Sources: AGENTS.md:30-49
# Run all sanity tests
ansible-test sanity -v --docker default
# Run integration tests
ansible-test integration -v --docker ubuntu2204