Create a simple custom module in ansible

For writing a custom module, we can use any language of our choice as long as it can return a JSON object. This post will write a simple custom module using a bash script. Typically the choice of language for writing a custom module is python; however, if, due to some reasons, you prefer to shell/bash, then this post is for you. If you are also interested in writing custom modules in python, then please check out this URL.

The following are the main requirements for a script to be used as a module:

  • Your code/script must be present in a directory called ‘library‘ at the sample level as the playbook.
  • When invoked by ansible, your script must return a valid JSON response to the playbook.
  • We should try to make our script idempotent(don’t repeat a task if its already done)


Example Usecase:


We will name our custom module ‘get_area,’ and this module will return the area of a rectangle or square. The module takes the following input parameters:
- length
- breath

Module file location


The module code must be located in a directory called ‘library‘ in the same directory as the playbook. For example, if my playbook is named example.yml, then at the same directory, I will create a directory called ‘library‘ to write my module.
Now, I will create a file called ‘get_area.sh‘ for writing my business logic. Your directory structure must look like the following snippet.

tree
.
├── example.yml
└── library
    └── get_area.sh

1 directory, 2 files

Version-1: The minimal script


I will write the following code in my get_area.sh module file.

#!/bin/bash

# you can define the default JSON object you would
# like to return back to the playbook
# for example, like every other module I would like
# to return stdout, stderr, msg, rc, changed

changed="false"
rc=0
stdout=""
stderr=""
msg=""


# ansible supply a temp file name(stored at $1) will all the input arguments
# to the script and all the necessary variables required for the task execution.
# if curious, you can print $1 and cat $1 to see the contents.
source $1


#add business logic here



msg=$((length * breath))
stdout="$msg"

# in bash you cannot return a string, so we will print it
# note that I have printed them as a JSON object so ansible could understand it.

printf "{ \"changed\": \"$changed\",
           \"rc\": \"$rc\" ,
           \"stdout\": \"$stdout\",
           \"stderr\": \"$stderr\",
           \"msg\": \"$msg\"}"
exit $rc


Using the custom module in the playbook

---
- name: "This is play-1"
  hosts: localhost
  tasks:
  - name: "Get the area of a box"
    get_area:
      length: 11
      breath: 11
    register: area

  - debug: var=area


Execution results:


Notice the area is resulting. This demonstrates how we can supply input to a shell script and invoke that shell script as a module in ansible.

 ansible-playbook  example.yml


PLAY [This is play-1] ***********************************************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************************************************
ok: [localhost]

TASK [Get the area of a box] ****************************************************************************************************************************************
ok: [localhost]

TASK [debug] ********************************************************************************************************************************************************
ok: [localhost] => {
    "area": {
        "changed": "false",
        "failed": false,
        "msg": "121",
        "rc": "0",
        "stderr": "",
        "stderr_lines": [],
        "stdout": "",
        "stdout_lines": []
    }
}

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


Version-2: A More Practical Example with stdout and stderr handling


A shell script may have many echo commands in it; that command may not be a JSON we intend to return to the playbook. This could cause great confusion between the playbook and module. We need to stop the noise coming from the script from failing the playbook. So in the below example, we will block all the stdout and stderr of the script and selectively enable it when needed to return to the playbook.

#!/bin/bash
###################################################################################
#  PART-1: Disable the stdout and stderro of the script, unless we really want
#
###################################################################################
# Ansible playbook only expect a valid JSON return value from the module
# However, shell scripts are not capable of returning a JSON objects.
# Shell scripts can only return an integer value as return code.
# So we will do a printf/echo a JSON string from the script to simulate a JSON
# return from the shell script.

# The problem with returning a string using echo/printf is the presence of multiple
# echo statements in the script, multiple echo would emit multiple stdout that will
# confuse the playbook and fail the playbook execution.
# using the below command we will redirect all the stdout and stderr to /dev/null
# we will only enable stdout and stderr only selectively later in the script when
# required. 
 

exec 3>&1 4>&2 > /dev/null 2>&1
####################################################################################


####################################################################################
# PART-2: Create a function that would temp allow stdout or stderr of the script
#         and return a valid JSON back to the playbook
####################################################################################
return_json() {
    # restoring stdout and stderr
    exec 1>&3 2>&4

    # build and print a valid JSON object
    printf "{ \"changed\": \"$changed\",
          \"rc\": \"$rc\" ,
           \"stdout\": \"$stdout\",
           \"stderr\": \"$stderr\",
           \"msg\": \"$msg\"}"
    #again blocking the stdout and stderr
    exec 3>&1 4>&2 > /dev/null 2>&1

}

#####################################################################################
# PART-3: setting the default values for returning in the JSON object
#
#####################################################################################
changed="false"
rc=0
stdout=""
stderr=""
msg=""


#####################################################################################
# PART-4: Take the input arguments, ansible supply a temp file name(stored at $1)
#         will all the input arguments to the script and all the necessary variables
#         required for the task execution,if curious, you can print $1 and cat $1 to
#         see the contents.
#####################################################################################
source "$1"

####################################################################################
# PART-5: Do validations of your input arguments
#
####################################################################################
# Example, here I am ensuring that the the length and breath variables are set in
# the playbook, if not I am setting rc to a non-zero value and msg text.
if [ -z "$length" ];then
	msg="The value of length is not provided. Exiting"
	rc=1
	return_json
fi

if [ -z "$breath" ];then
        msg="The value of breath is not provided. Exiting"
        rc=2
	return_json
fi

######################################################################################
# PART-6: add business logic here, for example I am calculating area(lxb) and setting 
#         the msg variable. Note that this msg is part of the returned JSON object
######################################################################################
changed="false"
msg=$((length * breath))

######################################################################################
# PART-7: If all goes well, the code will reach this point and exit with the set values
######################################################################################
return_json

exit $rc


Version-3: Production-ready script skeleton (Final version)


In this script version, we have removed printf to build a JSON object for returning to the playbook. Now we are using a more JSON-aware tool called jq. Here, jq will do all the heavy lifting of ensuring that a valid JSON is returned with required escaping. In an earlier version of the script, we manually built the JSON using printf, where printf is not a JSON-aware tool, and any unexpected character in the return value could cause the script to fail.

#!/bin/bash
###################################################################################
#  PART-1: Disable the stdout and stderro of the script, unless we really want
#
###################################################################################
# Ansible playbook only expect a valid JSON return value from the module
# However, shell scripts are not capable of returning a JSON objects.
# Shell scripts can only return an integer value as return code.
# So we will do a printf/echo a JSON string from the script to simulate a JSON
# return from the shell script.

# The problem with returning a string using echo/printf is the presence of multiple
# echo statements in the script, multiple echo would emit multiple stdout that will
# confuse the playbook and fail the playbook execution.
# using the below command we will redirect all the stdout and stderr to /dev/null
# we will only enable stdout and stderr only selectively later in the script when
# required. 


exec 3>&1 4>&2 > /dev/null 2>&1
####################################################################################


####################################################################################
# PART-2: Create a function that would temp allow stdout or stderr of the script
#         and return a valid JSON back to the playbook
####################################################################################
return_json() {
    # restoring stdout and stderr
    exec 1>&3 2>&4

    # build and print a valid JSON object
    # Note that, we have used jq here to be more error-proof
    jq -n \
        --arg changed "$changed" \
        --arg rc "$rc" \
        --arg stdout "$stdout" \
        --arg stderr "$stderr" \
        --arg msg "$msg" \
       '$ARGS.named'

    #again blocking the stdout and stderr
    exec 3>&1 4>&2 > /dev/null 2>&1

}

#####################################################################################
# PART-3: setting the default values for returning in the JSON object
#
#####################################################################################
changed="false"
rc=0
stdout=""
stderr=""
msg=""


#####################################################################################
# PART-4: Take the input arguments, ansible supply a temp file name(stored at $1)
#         will all the input arguments to the script and all the necessary variables
#         required for the task execution,if curious, you can print $1 and cat $1 to
#         see the contents.
#####################################################################################
source "$1"

####################################################################################
# PART-5: Do validations of your input arguments
#
####################################################################################
# Here I am ensuring that jq is installed on the system
if !  which jq &>/dev/null; then
    exec 1>&3 2>&4
    printf "{ \"changed\": \"$changed\",
           \"rc\": \"1\" ,
           \"stdout\": \"\",
           \"stderr\": \"This module require 'jq' installed on the target host. Exiting\",
           \"msg\": \"\"}"
    exit 1
fi

# Example, here I am ensuring that the the length and breath variables are set in
# the playbook, if not I am setting rc to a non-zero value and msg text.


if [ -z "$length" ];then
	msg="The value of length is not provided. Exiting"
	rc=1
	return_json
fi

if [ -z "$breath" ];then
        msg="The value of breath is not provided. Exiting"
        rc=2
	return_json
fi

######################################################################################
# PART-6: add business logic here, for example I am calculating area(lxb) and setting 
#         the msg variable. Note that this msg is part of the returned JSON object
######################################################################################
changed="false"
msg=$((length * breath))
######################################################################################
# PART-7: If all goes well, the code will reach this point and exit with the set values
######################################################################################
return_json

exit $rc


Summary:


In this post, we have seen the example of a bash script turning into an ansible custom module. Similarly, you can use any language of interest and plug it in ansible. You may check this page if you want to write a custom python module.

Leave a Comment

Your email address will not be published.

Scroll to Top