This is the final chapter, which will go through what a module is, what you need to consider before writing a module and how to write some different types of Ansible modules. After this, you can without any problem state that you know one or two things about Ansible. And don't worry, it's not that complicated.
To reiterate, modules are Ansible's tools in a toolbox. Ansible playbooks calls upon modules to get work done, as can be seen below.
👍 The first thing you should ask yourself when developing an Ansible module is, should you develop a module? Before you get started on development, walk through this little checklist.
- Does a similar module exist? See: http://docs.ansible.com/ansible/latest/modules/modules_by_category.html
- Is there development already ongoing for a similar module? See: https://github.com/ansible/ansible/labels/new_module See: https://github.com/ansible/ansible/labels/module See: https://ansible.sivel.net/pr/byfile.html
- Should you use or develop an action plugin instead?
- Should you use a role instead? (14 000 exists on Galaxy)
- Should you write one or multiple modules? (Remember that complexity kills productivity)
If you've decided to develop a module, let's review the different types of modules that exists. Don't worry if it doesn't stick at first, this is more for you to see and less for you to memorize.
- Action plugins - always execute server-side and are sometimes able to do all work there (example: debug, template)
- New-style modules - all that ship with Ansible. Arguments embedded in module instead of separate file, reducing the need for a transfer of a separate file containing arguments.
- Python - New-style Python modules use the Ansiballz framework for constructing modules. These modules use imports from ansible.module_utils in order to pull in boilerplate module code, such as argument parsing, formatting of return values as JSON and various file operations.
- Powershell - for Windows systems, they use the Module Replacer framework for constructing modules.
- JSONARGS - Scripts that arrange for an argument string to be placed within them using special string:
jsonargs = “<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>”
- Non-native WANT_JSON modules - If a module has the string WANT_JSON in it anywhere, Ansible treats it as a non-native module that accepts a filename as its only command line parameter, the format of the argument file will then be in JSON. Otherwise it will be key=value.
- Binary module - compiled small program, works like a WANT_JSON module.
- And more http://docs.ansible.com/ansible/latest/dev_guide/developing_program_flow_modules.html
Modules can be written in any language an author wishes, they just need to specify certain special strings in their code. If not, a file containing the module args will be uploaded, and the path to that file is the first argument to the module. If your module contains the string WANT_JSON, that args file will be formatted as JSON (otherwise they’re key=value pairs). As of Ansible version 2.0, modules must output JSON (key=value output is no longer allowed).
- task_executor - TaskExecutor decides if it’s an action plugin or a module. If module, it loads ‘Normal Action Plugin’ and passes info about what’s to be done.
- Normal Action Plugin - Inits connection. Pushes module to host. Executes the module on the remote host. Primary coordinator.
- module_common.py - Identifies module type, selects preprocessor.
- Module Replacer/Ansiballz - Preprocessors which does substitutions of specific substring patterns in the module file. Read more here: https://docs.ansible.com/ansible/latest/dev_guide/developing_program_flow_modules.html#module-replacer
- Passing arguments - module arguments are turned into a JSON-ified string and passed to the module.
- Internal arguments - parameters which implements global features. Often you do not need to know about these.
There are three different strategies when writing Ansible modules, each one with some different pros and cons.
PROs:
Easy to write, low learning curve.
Protects users from complexity
CONs:
Output/results have to be scraped out of the CLI output, which is very fragile and prone to error, eg:
cli_command|awk ‘{ print $6 ‘}|cut -d’/’ -f2|sed ‘s/old/new/'
Depending on use-case - only slightly more useful than using command/shell modules.
PROs:
Also very easy to get started with, since someone else has done the hard work for you.
CONs:
Extra dependencies for users running your module remotely (the library must be installed everywhere you run the module).
Modules may not cover API features you need (especially new features).
Bugs and abandonment (don’t forget to evaluate).
PROs:
No extra dependencies (Ansible provides helper code in module_utils/urls.py to make HTTP calls).
New features are accessible immediately without having to wait.
CONs:
Having to know the API and extra maintenance work for the module.
You're now stuffed with some good to know information and are soon ready to write your first Ansible modules. At the end of this chapter you will have written two simple Ansible modules which you can extend with arbitrary functionality to fit your real world use case.
Our first module will simply wrap a CLI command (touch). It will be non-native, meaning that it will receive it's arguments in a separate arguments file as the first argument passed to the module. The arguments file will be using a key=value format. Wrapping CLI commands is as stated simple, but is in general less robust and may provide limited value compared to the command or shell modules. Please keep in mind that Ansible modules can be written in any language, to make it as simple as possible, this example in written in BASH script.
💥 First, let's create a directory in which we'll develop the module.
cd
mkdir new-module
cd new-module
💥 Secondly, let's create a simple module using Bourne Again SHell (BASH) script. We start with the most simple version of this module, create it by pasting below content into your terminal:
cat << 'EOF' >new_module
#!/bin/sh
# Module which creates the file: /tmp/module-file
set -e
# First argument is the arguments file
source ${1}
# Create file
touch /tmp/module-file
# Output JSON
echo {\"changed\": true, \"msg\": \"${msg}\"}
exit 0
EOF
💥 Next, copy the module into the module directory.
mkdir -p $HOME/.ansible/plugins/modules
cp new_module $HOME/.ansible/plugins/modules/
💥 Now we'll create a playbook to test our module. Create a file called test.yml in your local directory, as follows:
cat << 'EOF' >test.yml
- hosts: localhost
gather_facts: no
tasks:
- new_module:
msg: "hello world"
EOF
💥 Now let's test our module.
ansible-playbook -vv ./test.yml
Expected output should be something like:
$ ansible-playbook -vv ./test.yml
ansible-playbook 2.6.3
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/home/student/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /usr/bin/ansible-playbook
python version = 2.7.5 (default, May 31 2018, 09:41:32) [GCC 4.8.5 20150623 (Red Hat 4.8.5-28)]
Using /etc/ansible/ansible.cfg as config file
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match
'all'
PLAYBOOK: test.yml *********************************************
1 plays in ./test.yml
PLAY [localhost] ***********************************************
META: ran handlers
TASK [new_module] **********************************************
task path: /home/student/new-module/test.yml:4
changed: [localhost] => {"changed": true, "msg": "hello world"}
META: ran handlers
META: ran handlers
PLAY RECAP ******************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0
$
Now that we have a working module, let's improve it a bit, handling the case if something goes wrong. Change the module so that it handles the case if it's possible to create a file containing the input passed to it.
💥 Create some exception handling in the module and if you detect a failure, output:
echo {\"failed\": true, \"msg\": \"${msg}\"}
❗ If you get stuck, have a look at a solution here: https://raw.githubusercontent.com/mglantz/ansible-roadshow/master/labs/lab-10/lab-solutions/module-v2.sh
💥 Re-run your test.yml playbook to ensure your modifications work, then you can try and replace /tmp/module-file in the module to /tmp/doesnotexist/module-file to cause it to fail. Then change it back to /tmp/module-file
💥 Next step is to create a simple check if the /tmp/module-file file already exists and then return JSON output with changed: false. Remember that idempotency is key in Ansible, so ofcourse we want our module to be idempotent.
❗ If you get stuck, have a look at a solution here: https://raw.githubusercontent.com/mglantz/ansible-roadshow/master/labs/lab-10/lab-solutions/module-v3.sh
The next module we'll develop is a binary module. Like we've said, you can develop Ansible modules in any language. Benefits of using a compiled binary module could be performance or containing dependencies. We'll write out simple binary module in C.
💥 First, let's create a directory in which we'll develop the module.
cd
mkdir binary-module
cd binary-module
💥 Then we'll create a simple module, which creates a file and then prints success. Create the file binary_module.c by pasting in below content into your terminal:
cat << 'EOF' >binary_module.c
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE *fp = fopen("/tmp/binary-module-file", "ab+");
printf("{\"changed\": true, \"msg\": \"Arguments file: %s\"}", argv[1]);
}
EOF
💥 To compile the code, run:
gcc -o binary_module binary_module.c
💥 Next, copy the module into the module directory.
cp binary_module $HOME/.ansible/plugins/modules/
💥 Now we'll create a playbook to test our module. Create a file called test-binary-module.yml in your local directory, as by pasting in below content in your terminal:
cat << 'EOF' >test-binary-module.yml
- hosts: localhost
gather_facts: no
tasks:
- binary_module:
msg: "hello world"
EOF
💥 Now let's test our module.
ansible-playbook -vv ./test-binary-module.yml
Expected output should be something like:
ansible-playbook 2.6.3
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/home/student/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /usr/bin/ansible-playbook
python version = 2.7.5 (default, May 31 2018, 09:41:32) [GCC 4.8.5 20150623 (Red Hat 4.8.5-28)]
Using /etc/ansible/ansible.cfg as config file
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match
'all'
PLAYBOOK: test-binary-module.yml ***************************************************************************************
1 plays in ./test-binary-module.yml
PLAY [localhost] *******************************************************************************************************
META: ran handlers
TASK [binary_module] ***************************************************************************************************
task path: /home/student/binary-module/test-binary-module.yml:4
changed: [localhost] => {"changed": true, "msg": "Arguments file: /home/student/.ansible/tmp/ansible-tmp-1535738564.44-26021666473567/args"}
META: ran handlers
META: ran handlers
PLAY RECAP *************************************************************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0
Interestingly enough, we can here observe where the temporary arguments file is put, when running the module.
First of all read the Ansible Developing Modules page. Especially the 'Should You Develop A Module?' section is relevant:-)
Next follow the steps in https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html, but skip the section 'Prerequisites Via Apt (Ubuntu)'. This has been done for you. Be aware that we use python2, so ignore python3 stuff.
For debugging in python refer to Python documentation for debugger