A custom module in Python
A module is anything that prints a JSON object Ansible can interpret.
The Python helper AnsibleModule handles argument parsing, check
mode, and failure reporting for you.
Create library/cac_greet.py:
#!/usr/bin/python3
# NOTE: prefer an absolute path here rather than `/usr/bin/env python3`.
# Ansible reads the shebang to pick the remote interpreter, and a path
# with a space (anything after `env`) confuses its quoting.
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=True),
shout=dict(type='bool', default=False),
),
supports_check_mode=True,
)
name = module.params['name']
shout = module.params['shout']
greeting = f"Hello, {name}!"
if shout:
greeting = greeting.upper()
module.exit_json(changed=False, greeting=greeting)
if __name__ == '__main__':
main()
Make it executable:
chmod +x library/cac_greet.py
Then greet.yml:
---
- name: Use the custom module
hosts: centos1
gather_facts: false
tasks:
- name: Polite greeting
cac_greet:
name: "{{ inventory_hostname }}"
register: nice
- name: Loud greeting
cac_greet:
name: "{{ inventory_hostname }}"
shout: true
register: loud
- name: Show both
debug:
msg: "{{ nice.greeting }} | {{ loud.greeting }}"
...
Run it:
ansible-playbook greet.yml
You should see Hello, centos1! | HELLO, CENTOS1!. Ansible found the
module by name because library/ next to the playbook is on the
module search path by default.
Click Verify step.
Hint
Place under `library/<name>.py`; use `AnsibleModule()` from `ansible.module_utils.basic`.
A custom module in Bash
The module contract is language-agnostic. Anything that reads its
single command-line argument (a path to a temp JSON file with the
task's args) and prints JSON to stdout can be a module.
Create library/cac_filesize:
#!/bin/bash
# Cloud AI Campus — minimal bash module returning a file's size.
# The literal token WANT_JSON below tells ansible to pass arguments as
# a JSON file rather than the default space-separated key=value form.
# WANT_JSON
ARGS_FILE=$1
PATH_VAL=$(python3 -c 'import sys, json; print(json.load(open(sys.argv[1])).get("path", ""))' "$ARGS_FILE")
if [ -z "$PATH_VAL" ]; then
printf '{"failed": true, "msg": "path argument required"}\n'
exit 1
fi
if [ ! -e "$PATH_VAL" ]; then
printf '{"failed": true, "msg": "%s does not exist"}\n' "$PATH_VAL"
exit 1
fi
SIZE=$(stat -c %s "$PATH_VAL" 2>/dev/null || stat -f %z "$PATH_VAL")
printf '{"changed": false, "path": "%s", "size": %d}\n' "$PATH_VAL" "$SIZE"
The WANT_JSON marker is a literal string that ansible greps for in the
module source. With it, ansible passes a JSON file. Without it, args
come in as key=value space-separated pairs (the WANT_JSON-less form
is harder to parse safely, so always opt in to JSON).
chmod +x library/cac_filesize
Then filesize.yml:
---
- name: Probe sizes with the bash module
hosts: centos1
gather_facts: false
tasks:
- name: Measure /etc/hosts
cac_filesize:
path: /etc/hosts
register: sz
- name: Show the result
debug:
msg: "{{ sz.path }} is {{ sz.size }} bytes"
...
ansible-playbook filesize.yml
The trick: Ansible ships the module to the target, runs it there, and
parses whatever JSON it prints. Anything more elaborate (real arg
parsing, types, check-mode) is exactly why the Python AnsibleModule
helper exists.
Click Verify step.
Hint
Modules are anything that reads JSON args from $1 and prints JSON result to stdout.
A filter plugin
Filters are Jinja2 transforms — {{ value | filter_name(args) }}.
Ansible ships hundreds (upper, default, regex_replace, etc.),
but you can add your own.
Create filter_plugins/reverse_words.py:
# Filter plugin: reverse the WORDS in a string.
# Usage: "{{ 'hello world from ansible' | reverse_words }}"
def reverse_words(value):
return ' '.join(reversed(str(value).split()))
class FilterModule:
def filters(self):
return {
'reverse_words': reverse_words,
}
Then filter.yml:
---
- name: Use the custom filter
hosts: centos1
gather_facts: false
tasks:
- name: Reverse word order
debug:
msg: "{{ 'cloud ai campus dive into ansible' | reverse_words }}"
- name: Chain with built-in filters
debug:
msg: "{{ 'how now brown cow' | reverse_words | upper }}"
...
ansible-playbook filter.yml
Output:
ansible into dive campus ai cloud
COW BROWN NOW HOW
Filters can take arguments like any function: | my_filter(extra='arg').
Click Verify step.
Hint
`filter_plugins/<name>.py` exposes a class with a `filters()` method returning a dict of name→callable.
A lookup plugin
Lookups resolve external data at template time — useful for pulling
secrets, generating values, or talking to APIs.
Create lookup_plugins/yelling_uuid.py:
# Lookup plugin: generate a SHOUTING uuid for each "term" passed in.
# Usage: "{{ lookup('yelling_uuid', 'tag-a', 'tag-b') }}"
import uuid
from ansible.plugins.lookup import LookupBase
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
out = []
for term in terms:
generated = str(uuid.uuid4()).upper()
out.append(f"{term}-{generated}")
return out
Then lookup.yml:
---
- name: Use the custom lookup
hosts: centos1
gather_facts: false
tasks:
- name: Mint two IDs
debug:
msg: "{{ lookup('yelling_uuid', 'session', 'student') }}"
- name: Or assign into a variable
set_fact:
my_session_id: "{{ lookup('yelling_uuid', 'sess')[0] }}"
- name: Use it
debug:
msg: "my_session_id={{ my_session_id }}"
...
ansible-playbook lookup.yml
lookup() always returns a list even for a single term — that's
why we index with [0] when we want a scalar.
Click Verify step.
Hint
`lookup_plugins/<name>.py` defines a `LookupModule` subclass with `run(terms, variables, ...)`.