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.

Plugin Types:
  • 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

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