How to create an Ansible collection with a simple module step by step

Creating and sharing a module is a great way of contributing to Ansible.
This guide will show you step-by-step how to create and test one.

When you later create useful modules doing real work, see the Ansible collection creator path link at the end of the topic to learn how to share them with others and even get them included in the Ansible package!

Tested on Fedora 39.

Prepare your environment

1. Install podman

$ sudo dnf install podman

2. Install ansible-core (if you already have the ansible package installed, it’ll also work)

$ pip install ansible-core

3. Create the following directories in your home directory

$ mkdir -p ~/ansible_collections/my_namespace/my_collection/plugins/modules
$ cd ~/ansible_collections/my_namespace/my_collection/

Add a module

4. Start a new module file with your text editor

$ vim plugins/modules/dummy.py

5. Add the following content to the module

#!/usr/bin/python
# -*- coding: utf-8 -*-

# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

# What you define in this section will appear on Galaxy
# and on docs.ansible.com as module documentation
# if it gets included in the Ansible package.
DOCUMENTATION = r'''
---
module: dummy

short_description: A dummy module

description:
  - Divides one number by another one and returns a quotient.

version_added: '0.1.0'

author:
  - Your name (@YourGitHubAccount)

options:
  dividend:
    description:
      - Dividend.
    type: int
    required: true

  divisor:
    description:
      - Divisor.
    type: int
    required: true
'''

EXAMPLES = r'''
- name: Divide a by b
  register: result
  my_namespace.my_collection.dummy:
    dividend: 4
    divisor: 2

- name: Print the result
  ansible.builtin.debug:
    var: result.quotient
'''

RETURN = r'''
quotient:
  description:
  - The result of division.
  returned: on success
  sample: 2
  type: int
'''

from ansible.module_utils.basic import AnsibleModule


def main():
    # The module accepts arguments declared here.
    argument_spec = {}
    argument_spec.update(
        dividend=dict(type='int', required=True),
        divisor=dict(type='int', required=True),
    )

    # Instantiate an object of the AnsibleModule class
    # provided by the Ansible Core.
    module = AnsibleModule(
        argument_spec=argument_spec,
        supports_check_mode=True,
    )

    # Assign passed options to variables
    dividend = module.params['dividend']
    divisor = module.params['divisor']

    # The work starts here.
    # All interactions with the user happen through
    # interfaces provided by the module object of
    # the AnsibleModule class of Ansible Core.
    # Let's fail when the divisor is zero using the fail_json() method.
    if divisor == 0:
        module.fail_json("Division by zero is not allowed!")

    # Do something.
    quotient = dividend // divisor

    # Exit the module.
    # Users will get the result in its JSON output after execution.
    module.exit_json(changed=False, quotient=quotient)


if __name__ == '__main__':
    main()

5. Let’s check that the module has a proper format, satisfies the code standards, etc. with running sanity tests

$ ansible-test sanity plugins/modules/dummy.py --docker

6. Create directories for integration tests

$ mkdir -p tests/integration/targets/dummy/tasks

7. Let’s check if the module works as expected by adding and running integration tests

$ vim tests/integration/targets/dummy/tasks/main.yml
- name: Divide by zero
  register: result
  ignore_errors: true
  my_namespace.my_collection.dummy:
    dividend: 4
    divisor: 0

- name: Assert the result
  ansible.builtin.assert:
    that:
    - result is failed
    - result.msg == "Division by zero is not allowed!"

- name: Divide a by b
  register: result
  my_namespace.my_collection.dummy:
    dividend: 4
    divisor: 2

- name: Assert the result
  ansible.builtin.assert:
    that:
    - result is succeeded
    - result.quotient == 2

8. Run the integration tests in a podman container

$ ansible-test integration dummy --docker -vvv

In the output you should see:

TASK [dummy : Devide a by b] ***
...
ok: [testhost] => {
    "changed": false,
    "invocation": {
        "module_args": {
            "dividend": 4,
            "divisor": 2
        }
    },
    "quotient": 2
}

TASK [dummy : Assert the result] ***
...
ok: [testhost] => {
    "changed": false,
    "msg": "All assertions passed"
}

What you could do next:

1. Create a simple info module that returns some system or service information, for example, a kernel version as a single row:

6.10.4-100.fc39.x86_64

2. When it works, make it return the version as a dictionary like:

kernel: 6
major: 10
minor: 4

3. If there are no decent collections with modules on Galaxy for the services you use at work/as hobby, create one yourself. You could start with writing an _info module that fetches useful information about the service and returns it to a user.
4. Add other modules that changes the service state in an idempotent way.
5. Read the next topic How to set up a repository for an Ansible collection step by step.
5. Want to contribute to existing modules? Read the How to submit your first pull request to Ansible step-by-step post.

Documentation:

8 Likes

This tutorial worked great, thanks!

Once I pushed to GitHub, the repo has a batch of CI tests already set up, thanks to the template. But it means I had to go in and fix a few things to get tests passing. I looked at the CI failure details, matched that to the repo .github/workflows to see how I could run them locally and fix things up.

Here’s the problems in case others run into this:

I created my own other simple module and found running ansible-test sanity --docker helpful - to run all the tests. It picked up some yamllint errors.

Other problems were related to boilerplate text in a couple of files that need to be updated to the real collection/repo names etc:

These I fixed by looking at the workflow and :

  • install the required tool - pip install antsibull-docs --disable-pip-version-check
  • Then run the test locally to see/fix the problems - antsibull-docs lint-collection-docs . --plugin-docs --skip-rstcheck

Lastly, there are a batch of errors related to'FIXME and collection github urls in galaxy.yml that need to be updated to the actual repo/namespace etc to pass. I couldn’t quite figure out how to run those tests locally ( I (Ⓐdevel+py3.9) etc that repeat for a batch of ansible-core and python versions…a lot of failures but all the same cause).

Thanks for this!

1 Like

I wasn’t sure about the naming here. If I followed this, I’d end up with ansible_collections/samccann/sample.

But I saw a lot of collections in the ansible-collections repo that looked like https://github.com/ansible-collections/community.general.

So I ended up with ansible_collections/samccann/samccann.sample. And now I’m wondering if I was a bit redundant on the naming there?

Hy ,

i folllow the naming convention
ansible_collections/companyname/sample .

1 Like

This won’t work for ansible-test, for example, you must use ansible_collections/samccann/sample.

The Git repository’s (on GitHub) name is usually different from the path where you need to check it out.

3 Likes

Yep, some adjustments have to be made to the template, but it’s a bit out of scope of this particular howto topic. I was thinking about going through that GH repo setup in another related topic, i.e. to do it in a step-by-step manner to give folks a taste of creating something working and not to overwhelm them right at the start:) Also not everyone needs a GitHub repo setup, so let’s cover this in a separate topic

1 Like

Thanks folks! I’ll change my little sample repo to samccann/sample and try this again!

1 Like

Ah got myself confused. So yes, I have the correct directory path ansible_collections/samccan/sample and my repo is GitHub - samccann/samccann.sample: dummy collection just for learning … that’s where the redundancy in my id occurred…