Skip to content

Custom Module Development

Overview

Learn how to create custom Ansible modules when existing modules don't meet your needs.

Basic Module Structure

Simple Module Example

#!/usr/bin/python

from ansible.module_utils.basic import AnsibleModule

def run_module():
    # Define module arguments
    module_args = dict(
        name=dict(type='str', required=True),
        state=dict(type='str', default='present', choices=['present', 'absent']),
        properties=dict(type='dict', default=dict())
    )

    # Create module instance
    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True
    )

    # Initialize result dictionary
    result = dict(
        changed=False,
        original_message='',
        message=''
    )

    # Module logic here
    if module.params['state'] == 'present':
        result['changed'] = True
        result['message'] = f"Created {module.params['name']}"

    # Return results
    module.exit_json(**result)

def main():
    run_module()

if __name__ == '__main__':
    main()

Advanced Features

Complex Module Example

#!/usr/bin/python

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.dict_transformations import dict_merge
from ansible.module_utils._text import to_native
import traceback

class CustomError(Exception):
    pass

def setup_resource(module):
    name = module.params['name']
    properties = module.params['properties']

    try:
        # Resource setup logic here
        return True, {"status": "created", "name": name}
    except CustomError as e:
        module.fail_json(msg=f"Error setting up resource: {to_native(e)}")
    except Exception as e:
        module.fail_json(msg=f"Unexpected error: {to_native(e)}", 
                        exception=traceback.format_exc())

def run_module():
    module_args = dict(
        name=dict(type='str', required=True),
        state=dict(
            type='str', 
            default='present', 
            choices=['present', 'absent']
        ),
        properties=dict(
            type='dict',
            default=dict(),
            options=dict(
                timeout=dict(type='int', default=30),
                retries=dict(type='int', default=3),
                custom_option=dict(type='str')
            )
        ),
        validate=dict(type='bool', default=True)
    )

    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True,
        required_if=[
            ('state', 'present', ['properties'])
        ]
    )

    result = dict(
        changed=False,
        original_message='',
        message='',
        resource={}
    )

    if module.check_mode:
        module.exit_json(**result)

    if module.params['state'] == 'present':
        changed, resource = setup_resource(module)
        result['changed'] = changed
        result['resource'] = resource

    module.exit_json(**result)

if __name__ == '__main__':
    main()

Module with API Integration

#!/usr/bin/python

import requests
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url

class APIClient:
    def __init__(self, module):
        self.module = module
        self.base_url = module.params['api_url']
        self.token = module.params['api_token']

    def make_request(self, method, endpoint, data=None):
        headers = {
            'Authorization': f'Bearer {self.token}',
            'Content-Type': 'application/json'
        }

        url = f"{self.base_url}/{endpoint}"

        response, info = fetch_url(
            self.module,
            url,
            method=method,
            data=data,
            headers=headers
        )

        if info['status'] >= 400:
            self.module.fail_json(
                msg=f"API request failed: {info['msg']}",
                status_code=info['status']
            )

        return response.read()

def run_module():
    module_args = dict(
        api_url=dict(type='str', required=True),
        api_token=dict(type='str', required=True, no_log=True),
        resource_id=dict(type='str', required=True),
        action=dict(
            type='str',
            choices=['get', 'create', 'update', 'delete'],
            required=True
        ),
        data=dict(type='dict')
    )

    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True
    )

    client = APIClient(module)
    # Module logic here

Testing Modules

Unit Testing

#!/usr/bin/python
# test_custom_module.py

import unittest
from unittest.mock import patch
from ansible.module_utils import basic
from ansible.module_utils.common.text.converters import to_bytes
import json

def set_module_args(args):
    args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
    basic._ANSIBLE_ARGS = to_bytes(args)

class TestCustomModule(unittest.TestCase):
    def setUp(self):
        self.mock_module = patch.multiple(basic.AnsibleModule,
                                        exit_json=exit_json,
                                        fail_json=fail_json)
        self.mock_module.start()

    def tearDown(self):
        self.mock_module.stop()

    def test_module_fail_when_required_args_missing(self):
        with self.assertRaises(AnsibleFailJson) as result:
            set_module_args({})
            custom_module.main()

    def test_module_success(self):
        set_module_args({
            'name': 'test_resource',
            'state': 'present'
        })
        with self.assertRaises(AnsibleExitJson) as result:
            custom_module.main()

        self.assertTrue(result.exception.args[0]['changed'])

Integration Testing

# integration_tests/test_custom_module.yml
- hosts: localhost
  gather_facts: no
  tasks:
    - name: Test custom module
      custom_module:
        name: test_resource
        state: present
      register: result

    - name: Verify result
      assert:
        that:
          - result is changed
          - result.resource.name == 'test_resource'

Documentation

Module Documentation

#!/usr/bin/python
# Copyright: (c) 2024, Your Name <[email protected]>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

DOCUMENTATION = r'''
---
module: custom_module

short_description: Manage custom resources

version_added: "1.0.0"

description:
    - Create, update, or delete custom resources
    - Manages resource lifecycle and properties

options:
    name:
        description: Resource name
        required: true
        type: str
    state:
        description: Desired state of the resource
        choices: [ present, absent ]
        default: present
        type: str
    properties:
        description: Resource properties
        type: dict
        default: {}

author:
    - Your Name (@yourgithub)
'''

EXAMPLES = r'''
# Create a resource
- name: Create new resource
  custom_module:
    name: myresource
    state: present
    properties:
      key1: value1
      key2: value2

# Delete a resource
- name: Remove resource
  custom_module:
    name: myresource
    state: absent
'''

RETURN = r'''
resource:
    description: Resource information
    type: dict
    returned: always
    sample: {
        "name": "myresource",
        "status": "created",
        "properties": {
            "key1": "value1"
        }
    }
changed:
    description: Whether the resource was changed
    type: bool
    returned: always
'''

Best Practices

Error Handling

try:
    result = do_something()
except Exception as e:
    module.fail_json(
        msg=f"Operation failed: {to_native(e)}",
        exception=traceback.format_exc()
    )

Input Validation

def validate_input(module):
    name = module.params['name']
    if len(name) < 3:
        module.fail_json(
            msg="Name must be at least 3 characters"
        )

Return Values

result = {
    'changed': True,
    'resource': {
        'id': 'resource_id',
        'name': module.params['name'],
        'properties': module.params['properties']
    },
    'warnings': [],
    'diff': {
        'before': before_state,
        'after': after_state
    }
}