Custom Ansible Module Hello World

What is an ansible module?

Ansible modules are the building blocks for building ansible playbooks. They are small pieces of python code that can be triggered from the yaml in a playbook.

Ansible provides a bunch of modules which cover most of your needs .. but sometimes not all of them!

When and why create a custom module?

Most of the time there is no need to create a custom module. However, sometimes it's the best (or even only way) to interact with a certain piece of software.

Often: I find the modules are a nice way to interact more fluently with services that provide a RESTful API. For example Github or Pivotal. You can, of course, interact with these services with the URI module, but it can sometimes be a little clumsy!

Creating a simple custom module

Luckily creating Ansible Modules is ridiculously easy!

We'll create a quick little module that can create or delete a repository on github.

Let's start with the very least that we can do in order to see some output.

Crete the following file structure:

play.yml  
[library]
  |_ github_repo.py
  |_ test_github_repo.py

library/github_repo.py

#!/usr/bin/python

from ansible.module_utils.basic import *

def main():

    module = AnsibleModule(argument_spec={})
    response = {"hello": "world"}
    module.exit_json(changed=False, meta=response)


if __name__ == '__main__':  
    main()

Notes

  • main() is the entrypoint into your module.
  • #!/usr/bin/python is required. Leave it out and you've got some hard debugging time on your hands!
  • AnsibleModule comes from from ansible.module_utils.basic import *. It has to be imported with the *
  • AnsibleModule helps us handle incoming parameters and exiting the program (module.exit_json()). We'll add some parameters shortly

play.yml

- hosts: localhost
  tasks:
    - name: Test that my module works
      github_repo: 
      register: result

    - debug: var=result    

A simple playbook which runs our module and will dump the output to debug

We can run our playbook:

$ ansible-playbook play.yml

(note: I haven't specified an inventory. As of Ansible 2 (perhaps before), this assumes localhost). You may need to supply one with -i.

Output:

PLAY ***************************************************************************

TASK [setup] *******************************************************************  
ok: [localhost]

TASK [Test that my module works] ***********************************************  
ok: [localhost]

TASK [debug] *******************************************************************  
ok: [localhost] => {  
    "result": {
        "changed": false, 
        "meta": {
            "hello": "world"
        }
    }
}

PLAY RECAP *********************************************************************  
localhost                  : ok=3    changed=0    unreachable=0    failed=0   

Cool. it worked! From here-on it's just Python! I don't know about you, but that was a lot easier than I expected it to be!

Processing input

Of course, for our module to be any use at all, we'll need some inputs. Since we're creating a Github repo, we'll just borrow the example off the docs. Let's allow for the following fields

    - name: Create a github Repo
      github_repo:
        github_auth_key: "..."
        name: "Hello-World",
        description: "This is your first repository",
        private: yes
        has_issues: no
        has_wiki: no
        has_downloads: no
        state: present
      register: result

In our code, we can specify the inputs like so:

def main():

  fields = {
        "github_auth_key": {"required": True, "type": "str"},
        "name": {"required": True, "type": "str" },
        "description": {"required": False, "type": "str"},
        "private": {"default": False, "type": "bool" },
        "has_issues": {"default": True, "type": "bool" },
        "has_wiki": {"default": True, "type": "bool" },
        "has_downloads": {"default": True, "type": "bool" },
        "state": {
            "default": "present", 
            "choices": ['present', 'absent'],  
            "type": 'str' 
        },
    }

    module = AnsibleModule(argument_spec=fields)
    module.exit_json(changed=False, meta=module.params)

notes:

  • The expected inputs are defined as a dictionary.
  • Available types (that I am aware of) are: str, bool, dict, list, ...
  • You can specify if it's required, a default value, and limit possibly inputs with choices.

In the above code, we simply accept the inputs and pipe the parsed inputs (module.params) to exit_json.

Now, if you run the playbook (ansible-playbook play.yml), debug should spit out a nice dictionary of the inputs we passed in.

Cool. So now we know how to handle the inputs, let's start actually executing some code.

To the top of the file add:

def github_repo_present(data):  
    has_changed = False
    meta = {"present": "not yet implemented"}
    return (has_changed, meta)

def github_repo_absent(data=None):  
    has_changed = False
    meta = {"absent": "not yet implemented"}

These are the functions that will actually do the work. Back in our main() method, let's add some code to call the desired function:

def main():  
    fields = {..}
    choice_map = {
      "present": github_repo_present,
      "absent": github_repo_absent, 
    }
    module = AnsibleModule(argument_spec=fields)
    has_changed, result = choice_map.get(module.params['state'])(module.params)
    module.exit_json(changed=has_changed, meta=result)

We use a map to map the state provided to a function that will handle that state.

The last thing to do is to write the code to for github_repo_present and github_repo_absent:

def github_repo_present(data):

    api_key = data['github_auth_key']

    del data['state']
    del data['github_auth_key']

    headers = {
        "Authorization": "token {}" . format(api_key)
    }
    url = "{}{}" . format(api_url, '/user/repos')
    result = requests.post(url, json.dumps(data), headers=headers)

    if result.status_code == 201:
        return False, True, result.json()
    if result.status_code == 422:
        return False, False, result.json()

    # default: something went wrong
    meta = {"status": result.status_code, 'response': result.json()}
    return True, False, meta


def github_repo_absent(data=None):  
    headers = {
        "Authorization": "token {}" . format(data['github_auth_key'])
    }
    url = "{}/repos/{}/{}" . format(api_url, "toast38coza", data['name'])
    result = requests.delete(url, headers=headers)

    if result.status_code == 204:
        return False, True, {"status": "SUCCESS"}
    if result.status_code == 404:
        result = {"status": result.status_code, "data": result.json()}
        return False, False, result
    else:
        result = {"status": result.status_code, "data": result.json()}
        return True, False, result

Notes:

  • Notice how we do our best to work out if the state has changed or not. (in the delete method it's a little tricky, cause github will also return a 404 if you are not properly authenticated).
  • It's useful to return the response in the meta .. makes it easier to debug.
  • With Ansible modules, it's important that

We can add a block to delete the API just after we created it. Our final playbook might look like this:

- hosts: localhost
  vars: 
    - github_token: "..."
  tasks:
    - name: Create a github Repo
      github_repo:
        github_auth_key: "{{github_token}}"
        name: "Hello-World"
        description: "This is your first repository"
        private: yes
        has_issues: no
        has_wiki: no
        has_downloads: no

    - name: Delete that repo 
      github_repo:
        github_auth_key: "{{github_token}}"
        name: "Hello-World"
        state: absent

You can run this to create and then delete a repo with the name Hello-World.

Last step, we need to add our documentation to the module

Ansible likes us to include strings for DOCUMENTATION and EXAMPLES at the top of our module. It should explain how the module can be used. We can pretty much just use our playbook from above:

DOCUMENTATION = '''  
---
module: github_repo  
short_description: Manage your repos on Github  
'''

EXAMPLES = '''  
- name: Create a github Repo
  github_repo:
    github_auth_key: "..."
    name: "Hello-World"
    description: "This is your first repository"
    private: yes
    has_issues: no
    has_wiki: no
    has_downloads: no
  register: result

- name: Delete that repo 
  github_repo:
    github_auth_key: "..."
    name: "Hello-World"
    state: absent
  register: result
'''  

Conclusion

And that's it. Congrats! You've successfully created a module that can create and remove a Github Repo with Ansible.

Next steps:

There's a lot of room for improvement in this module. You could:

  • Update the present state so that if the repo already exists, it will update the existing repo.
  • This plugin currently only handles creating repos for users. Extend it to also handle creating repos for users.
  • You could handle all the possible inputs for CREATE ..

Here is the final code for our module: