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 fromfrom 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
, adefault
value, and limit possibly inputs withchoices
.
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: