Ansible Plugins
Introduction
Plugins are pieces of code that augment Ansible's core functionality. They extend what Ansible can do by providing additional ways to connect to systems, transform data, format output, and lookup external information. Understanding plugins is essential for advanced Ansible automation and customization.
- Lookup: Retrieve data from external sources (files, databases, APIs)
- Filter: Transform and manipulate data in templates and playbooks
- Test: Create custom conditions for use with
when - Callback: Control output formatting and logging
- Connection: Define how Ansible connects to hosts
- Inventory: Create dynamic inventory sources
- Module: Custom task implementations (covered separately)
Lookup Plugins
Built-in Lookup Plugins
Lookup plugins retrieve data from various sources at execution time:
---
- name: Lookup plugin examples
hosts: localhost
gather_facts: no
tasks:
- name: Read file contents
debug:
msg: "{{ lookup('file', '/etc/hosts') }}"
- name: Read environment variable
debug:
msg: "{{ lookup('env', 'HOME') }}"
- name: Read from multiple files
debug:
msg: "{{ item }}"
loop: "{{ lookup('fileglob', '/etc/*.conf', wantlist=True) }}"
- name: Password generation
debug:
msg: "{{ lookup('password', '/tmp/passwordfile chars=ascii_letters,digits length=16') }}"
- name: Get first available file
debug:
msg: "{{ lookup('first_found', findme) }}"
vars:
findme:
- /etc/app/config.yml
- /etc/app/config.yaml
- /etc/app/config.json
- name: DNS lookup
debug:
msg: "{{ lookup('dig', 'example.com') }}"
- name: Read from URL
debug:
msg: "{{ lookup('url', 'https://api.github.com/users/ansible') }}"
- name: Get random choice
debug:
msg: "{{ lookup('random_choice', ['apple', 'banana', 'cherry']) }}"
Advanced Lookup Examples
---
- name: Advanced lookup usage
hosts: localhost
gather_facts: no
vars:
config_paths:
- "{{ playbook_dir }}/configs/{{ environment }}/app.yml"
- "{{ playbook_dir }}/configs/default/app.yml"
tasks:
- name: Load configuration from first available file
set_fact:
app_config: "{{ lookup('first_found', config_paths) | from_yaml }}"
- name: Read all YAML files in directory
set_fact:
all_configs: "{{ all_configs | default([]) + [item | from_yaml] }}"
loop: "{{ query('fileglob', playbook_dir + '/configs/*.yml') }}"
loop_control:
label: "{{ item | basename }}"
- name: Read CSV data
debug:
msg: "{{ lookup('csvfile', 'server01 file=/path/to/servers.csv delimiter=,') }}"
- name: Read from Redis
debug:
msg: "{{ lookup('redis', 'mykey', host='localhost', port=6379) }}"
when: use_redis | default(false)
- name: Nested lookups
debug:
msg: "{{ lookup('file', lookup('env', 'CONFIG_FILE')) }}"
- name: Loop with lookup query
debug:
msg: "Processing {{ item }}"
loop: "{{ query('inventory_hostnames', 'webservers') }}"
- name: Pipe command output
debug:
msg: "{{ lookup('pipe', 'date +%Y-%m-%d') }}"
- name: Template lookup
debug:
msg: "{{ lookup('template', 'config.j2') }}"
Lookup with Variables
---
- name: Dynamic lookup usage
hosts: all
vars:
secrets_path: "/vault/{{ environment }}/{{ inventory_hostname }}"
tasks:
- name: Read host-specific secrets
set_fact:
db_password: "{{ lookup('file', secrets_path + '/db_password') }}"
api_key: "{{ lookup('file', secrets_path + '/api_key') }}"
- name: Load variables from AWS Secrets Manager
set_fact:
aws_secrets: "{{ lookup('aws_secret', 'myapp/prod/db', region='us-east-1') | from_json }}"
- name: Combine multiple data sources
set_fact:
merged_config: >-
{{
lookup('file', 'defaults.yml') | from_yaml |
combine(lookup('file', environment + '.yml') | from_yaml) |
combine(lookup('file', inventory_hostname + '.yml') | from_yaml)
}}
Filter Plugins
Built-in Filters
Filters transform data within Jinja2 templates:
---
- name: Filter plugin examples
hosts: localhost
gather_facts: no
vars:
app_name: " My Application "
version: "1.2.3"
servers: ['web01', 'web02', 'web03']
numbers: [1, 5, 3, 9, 2, 7]
tasks:
# String filters
- name: String manipulation
debug:
msg: |
Upper: {{ app_name | upper }}
Lower: {{ app_name | lower }}
Title: {{ app_name | title }}
Trim: {{ app_name | trim }}
Replace: {{ app_name | replace('Application', 'App') }}
Length: {{ app_name | length }}
# List filters
- name: List operations
debug:
msg: |
First: {{ servers | first }}
Last: {{ servers | last }}
Length: {{ servers | length }}
Join: {{ servers | join(', ') }}
Sorted: {{ numbers | sort }}
Unique: {{ [1, 2, 2, 3, 3, 3] | unique }}
Min: {{ numbers | min }}
Max: {{ numbers | max }}
Sum: {{ numbers | sum }}
# Default values
- name: Default values
debug:
msg: "{{ undefined_var | default('fallback value') }}"
# Type conversion
- name: Type conversion
debug:
msg: |
To int: {{ '42' | int }}
To float: {{ '3.14' | float }}
To bool: {{ 'yes' | bool }}
To JSON: {{ servers | to_json }}
To YAML: {{ servers | to_yaml }}
To nice JSON: {{ servers | to_nice_json }}
# Date filters
- name: Date manipulation
debug:
msg: |
Current: {{ ansible_date_time.iso8601 }}
To datetime: {{ ansible_date_time.iso8601 | to_datetime }}
Strftime: {{ ansible_date_time.epoch | int | strftime('%Y-%m-%d') }}
# Hash and encoding
- name: Hashing and encoding
debug:
msg: |
MD5: {{ 'password' | hash('md5') }}
SHA256: {{ 'password' | hash('sha256') }}
Base64 encode: {{ 'secret' | b64encode }}
Base64 decode: {{ 'c2VjcmV0' | b64decode }}
Advanced Filter Usage
---
- name: Advanced filters
hosts: localhost
gather_facts: no
vars:
servers:
- { name: 'web01', role: 'web', status: 'active', memory: 8 }
- { name: 'web02', role: 'web', status: 'maintenance', memory: 8 }
- { name: 'db01', role: 'database', status: 'active', memory: 16 }
- { name: 'db02', role: 'database', status: 'active', memory: 16 }
tasks:
# Select and reject
- name: Filter by attribute
debug:
msg: |
Active servers: {{ servers | selectattr('status', 'equalto', 'active') | list }}
Web servers: {{ servers | selectattr('role', 'equalto', 'web') | list }}
High memory: {{ servers | selectattr('memory', '>=', 16) | list }}
# Map attributes
- name: Extract attributes
debug:
msg: |
Server names: {{ servers | map(attribute='name') | list }}
Unique roles: {{ servers | map(attribute='role') | unique | list }}
Total memory: {{ servers | map(attribute='memory') | sum }}
# Combine and merge
- name: Dictionary operations
debug:
msg: "{{ defaults | combine(overrides) }}"
vars:
defaults:
port: 80
workers: 4
timeout: 30
overrides:
port: 8080
timeout: 60
# Regex filters
- name: Regular expressions
debug:
msg: |
Match: {{ 'server01' | regex_search('server(\d+)', '\\1') }}
Replace: {{ 'web-server-01' | regex_replace('-', '_') }}
Find all: {{ 'server01 server02' | regex_findall('server\d+') }}
# JSON Query (jmespath)
- name: Complex queries
debug:
msg: "{{ servers | json_query('[?status==`active`].name') }}"
# IP address filters
- name: IP address operations
debug:
msg: |
Network: {{ '192.168.1.100/24' | ipaddr('network') }}
Netmask: {{ '192.168.1.100/24' | ipaddr('netmask') }}
Is private: {{ '192.168.1.100' | ipaddr('private') }}
Next IP: {{ '192.168.1.100' | ipmath(1) }}
# Password hashing
- name: Password operations
debug:
msg: |
SHA512 crypt: {{ 'password' | password_hash('sha512') }}
MD5 crypt: {{ 'password' | password_hash('md5') }}
With salt: {{ 'password' | password_hash('sha512', 'mysalt') }}
Chaining Filters
---
- name: Filter chaining
hosts: localhost
gather_facts: no
vars:
users: ['Alice', 'bob', 'CHARLIE', 'dave']
tasks:
- name: Complex filter chains
debug:
msg: |
Normalized: {{ users | map('lower') | map('title') | sort | join(', ') }}
Processed: {{ servers |
selectattr('status', 'equalto', 'active') |
map(attribute='name') |
sort |
list }}
- name: Data transformation pipeline
set_fact:
processed_data: >-
{{
raw_data |
from_json |
json_query('[?active==`true`]') |
map(attribute='name') |
map('upper') |
unique |
sort |
list
}}
vars:
raw_data: '[{"name":"app1","active":true},{"name":"app2","active":false}]'
Test Plugins
Built-in Tests
Tests are used in conditional statements to check variable properties:
---
- name: Test plugin examples
hosts: localhost
gather_facts: no
vars:
my_string: "hello"
my_number: 42
my_list: [1, 2, 3]
my_dict: { key: value }
tasks:
# Type tests
- name: Variable type tests
debug:
msg: "{{ item.name }}: {{ item.result }}"
loop:
- { name: "Is string", result: "{{ my_string is string }}" }
- { name: "Is number", result: "{{ my_number is number }}" }
- { name: "Is list", result: "{{ my_list is iterable }}" }
- { name: "Is dict", result: "{{ my_dict is mapping }}" }
# State tests
- name: Variable state tests
debug:
msg: "{{ item }}"
loop:
- "Defined: {{ my_string is defined }}"
- "Undefined: {{ undefined_var is undefined }}"
- "None: {{ none_var is none }}"
when: true
# Comparison tests
- name: Version comparisons
debug:
msg: "Version {{ item.version }} is {{ item.comparison }}"
loop:
- { version: '1.2.3', comparison: 'greater than 1.0',
check: "{{ '1.2.3' is version('1.0', '>') }}" }
- { version: '2.0.0', comparison: 'greater or equal to 2.0.0',
check: "{{ '2.0.0' is version('2.0.0', '>=') }}" }
when: item.check
# File tests
- name: File state tests
debug:
msg: "{{ item }}"
loop:
- "File exists: {{ '/etc/hosts' is file }}"
- "Dir exists: {{ '/etc' is directory }}"
- "Is link: {{ '/usr/bin/python' is link }}"
- "Exists: {{ '/etc/hosts' is exists }}"
# String tests
- name: String matching tests
debug:
msg: "Matches"
when:
- "'hello' is match('^h')"
- "'hello' is search('ll')"
- "'server01' is regex('server\d+')"
# Subset tests
- name: Set operations
debug:
msg: "{{ item }}"
loop:
- "Is subset: {{ [1, 2] is subset([1, 2, 3, 4]) }}"
- "Is superset: {{ [1, 2, 3, 4] is superset([1, 2]) }}"
- "Any true: {{ [false, true, false] is any }}"
- "All true: {{ [true, true, true] is all }}"
Custom Tests in Conditionals
---
- name: Using tests in real scenarios
hosts: all
tasks:
- name: Install package only on newer systems
package:
name: "{{ item }}"
state: present
loop:
- modern-package
when: ansible_distribution_version is version('20.04', '>=')
- name: Configure service if it exists
service:
name: myapp
state: started
when:
- ansible_facts.services is defined
- "'myapp.service' in ansible_facts.services"
- name: Deploy only to production servers
copy:
src: prod-config.yml
dest: /etc/app/config.yml
when:
- inventory_hostname is match('^prod-')
- environment is defined
- environment is search('prod')
- name: Validate configuration
assert:
that:
- app_port is number
- app_port is version('1024', '>')
- db_host is defined
- db_host is string
- enable_ssl is boolean
fail_msg: "Configuration validation failed"
success_msg: "Configuration is valid"
Callback Plugins
Built-in Callback Plugins
Callback plugins control output format and enable logging:
# ansible.cfg
[defaults]
stdout_callback = yaml
callback_whitelist = timer, profile_tasks, profile_roles
# Available callbacks:
# - yaml: YAML formatted output (clean and readable)
# - json: JSON formatted output
# - minimal: Minimal output
# - oneline: One line per task
# - dense: Compact output
# - debug: Detailed debugging information
# - tree: Save output to files in a tree structure
# - log_plays: Log to syslog
# - mail: Send email on playbook failures
# - slack: Post to Slack channel
# - junit: Generate JUnit XML reports
# - timer: Show playbook execution time
# - profile_tasks: Show task timing
# - profile_roles: Show role timing
Using Callback Plugins
---
# Enable callbacks in playbook
- name: Playbook with callbacks
hosts: all
gather_facts: no
vars:
ansible_callback_diy_runner_on_ok: |
print("Task succeeded: %s" % result._task.name)
tasks:
- name: Example task
debug:
msg: "Task output"
# Run with specific callback
# ansible-playbook playbook.yml --stdout-callback=json
# Enable multiple callbacks
# ansible-playbook playbook.yml -e '{"ansible_callback_whitelist": ["timer", "profile_tasks"]}'
Timer and Profiling
# ansible.cfg for performance analysis
[defaults]
callback_whitelist = timer, profile_tasks, cgroup_perf_recap
# This will show:
# - Total playbook execution time
# - Time spent on each task
# - Slowest tasks highlighted
# - Memory and CPU usage per task
# Example output format:
# Playbook run took 0 days, 0 hours, 5 minutes, 23 seconds
# Task timing:
# 1. Install packages -------- 120.45s
# 2. Copy configuration ------ 45.23s
# 3. Restart services -------- 30.12s
Connection Plugins
Available Connection Types
Connection plugins define how Ansible connects to hosts:
---
- name: Connection plugin examples
hosts: all
tasks:
# SSH connection (default for Linux)
- name: Standard SSH connection
ping:
vars:
ansible_connection: ssh
ansible_ssh_private_key_file: ~/.ssh/id_rsa
# Paramiko SSH (pure Python)
- name: Paramiko connection
ping:
vars:
ansible_connection: paramiko
ansible_paramiko_host_key_checking: false
# Local connection
- name: Execute locally
command: hostname
delegate_to: localhost
vars:
ansible_connection: local
# Docker connection
- name: Execute in container
hosts: containers
vars:
ansible_connection: docker
tasks:
- name: Run command in container
command: cat /etc/os-release
# WinRM for Windows
- name: Windows hosts
hosts: windows
vars:
ansible_connection: winrm
ansible_winrm_transport: ntlm
ansible_winrm_server_cert_validation: ignore
tasks:
- name: Get Windows info
win_command: systeminfo
# Network device connections
- name: Network devices
hosts: routers
vars:
ansible_connection: network_cli
ansible_network_os: ios
tasks:
- name: Get device config
ios_command:
commands: show running-config
# Kubectl connection for Kubernetes
- name: Kubernetes pods
hosts: k8s_pods
vars:
ansible_connection: kubectl
tasks:
- name: Check pod
command: ps aux
Custom Connection Settings
---
- name: Advanced connection configuration
hosts: all
vars:
# SSH connection tuning
ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
ansible_ssh_pipelining: yes
ansible_ssh_retries: 3
# Connection timeout settings
ansible_connect_timeout: 30
ansible_command_timeout: 60
tasks:
- name: Execute with custom connection
command: uptime
vars:
ansible_ssh_extra_args: '-o ControlMaster=auto -o ControlPersist=60s'
Creating Custom Plugins
Custom Lookup Plugin
Create a custom lookup plugin in lookup_plugins/custom_lookup.py:
# lookup_plugins/custom_lookup.py
from ansible.plugins.lookup import LookupBase
from ansible.errors import AnsibleError
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
"""
Custom lookup that processes terms and returns results
Usage: {{ lookup('custom_lookup', 'term1', 'term2') }}
"""
ret = []
for term in terms:
# Custom processing logic
result = self.process_term(term)
ret.append(result)
return ret
def process_term(self, term):
# Your custom logic here
return term.upper()
Using the custom lookup:
---
- name: Use custom lookup
hosts: localhost
tasks:
- name: Custom lookup example
debug:
msg: "{{ lookup('custom_lookup', 'hello', 'world') }}"
Custom Filter Plugin
Create a custom filter in filter_plugins/custom_filters.py:
# filter_plugins/custom_filters.py
def reverse_string(value):
"""Reverse a string"""
return value[::-1]
def multiply_by(value, factor):
"""Multiply value by factor"""
return value * factor
def format_bytes(bytes_value):
"""Convert bytes to human readable format"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_value < 1024.0:
return f"{bytes_value:.2f} {unit}"
bytes_value /= 1024.0
return f"{bytes_value:.2f} PB"
class FilterModule(object):
def filters(self):
return {
'reverse': reverse_string,
'multiply': multiply_by,
'format_bytes': format_bytes,
}
Using custom filters:
---
- name: Use custom filters
hosts: localhost
vars:
message: "Hello"
number: 10
disk_usage: 52428800
tasks:
- name: Custom filter examples
debug:
msg: |
Reversed: {{ message | reverse }}
Multiplied: {{ number | multiply(5) }}
Formatted: {{ disk_usage | format_bytes }}
Custom Test Plugin
Create a custom test in test_plugins/custom_tests.py:
# test_plugins/custom_tests.py
def is_even(value):
"""Test if value is even"""
return value % 2 == 0
def is_valid_port(value):
"""Test if value is a valid port number"""
try:
port = int(value)
return 1 <= port <= 65535
except (ValueError, TypeError):
return False
def is_palindrome(value):
"""Test if string is a palindrome"""
clean = str(value).lower().replace(' ', '')
return clean == clean[::-1]
class TestModule(object):
def tests(self):
return {
'even': is_even,
'valid_port': is_valid_port,
'palindrome': is_palindrome,
}
Using custom tests:
---
- name: Use custom tests
hosts: localhost
vars:
port: 8080
number: 42
word: "racecar"
tasks:
- name: Test if port is valid
debug:
msg: "Port {{ port }} is valid"
when: port is valid_port
- name: Test if number is even
debug:
msg: "{{ number }} is even"
when: number is even
- name: Test palindrome
debug:
msg: "{{ word }} is a palindrome"
when: word is palindrome
Plugin Discovery and Loading
Plugin Paths
Ansible searches for plugins in these locations:
# ansible.cfg
[defaults]
lookup_plugins = ./lookup_plugins:/usr/share/ansible/plugins/lookup
filter_plugins = ./filter_plugins:/usr/share/ansible/plugins/filter
test_plugins = ./test_plugins:/usr/share/ansible/plugins/test
callback_plugins = ./callback_plugins:/usr/share/ansible/plugins/callback
connection_plugins = ./connection_plugins:/usr/share/ansible/plugins/connection
# Directory structure
project/
├── ansible.cfg
├── playbook.yml
├── filter_plugins/
│ └── custom_filters.py
├── lookup_plugins/
│ └── custom_lookup.py
├── test_plugins/
│ └── custom_tests.py
└── callback_plugins/
└── custom_callback.py
Listing Available Plugins
# List all available plugins
ansible-doc -t lookup -l
ansible-doc -t filter -l
ansible-doc -t callback -l
ansible-doc -t connection -l
# View plugin documentation
ansible-doc -t lookup file
ansible-doc -t filter to_json
ansible-doc -t callback yaml
# List plugin locations
ansible-config dump | grep PLUGIN
Best Practices
- Use built-in plugins first: Check if functionality already exists
- Document custom plugins: Include docstrings and examples
- Error handling: Use appropriate Ansible exceptions
- Performance: Cache expensive operations when possible
- Version compatibility: Test plugins across Ansible versions
- Security: Validate and sanitize all inputs
- Organize plugins: Use dedicated directories per type
- Share via collections: Package plugins in Ansible Collections
Next Steps
- Learn about Modules for custom task implementations
- Explore Roles and Collections for packaging plugins
- Master Advanced Topics for complex plugin scenarios
- Study Troubleshooting for debugging plugins
- Practice in the Playground with plugin examples