How to Write Custom Ansible Modules: Complete Python Guide for Network Engineers

As network engineers, many of us have embraced Ansible for network configs and day-to-day tasks. It's simple, doesn't need agents on your gear, and has a ton of built-in modules for all sorts of infrastructures, including your Cisco, Juniper, and Arista devices.
Though, sometimes Ansible's built-in modules don't quite do exactly what you need. Maybe you've got some unique network setup, weird data formats, or device outputs that need special handling. That's where Ansible module development using Python scripting for network automation and custom Python modules comes in super handy.
It lets you stretch Ansible's capabilities to handle unique Cisco IOS configurations, Juniper JUNOS outputs, and custom network device responses that standard Ansible playbook modules can't process.
Table of contents- What is an Ansible Custom Module?
- Why Use Custom Ansible Modules?
- Ansible Module Directory Structure: Where to Place a Custom Ansible Module?
- Ansible Custom Module Example: Parse Cisco IOS Config
- How to Debug an Ansible Custom Module
- Troubleshooting Common Issues
- How to create a custom Ansible collection
- Custom module with Ansible Open Tower
- Ansible Module Development Best Practices
What is an Ansible Custom Module?
An Ansible custom module is a Python script that extends Ansible module development functionality. Instead of relying on existing Ansible modules, you define your own logic using Python. This custom script then plugs right into your Ansible playbooks, and you can use it just like any other standard Ansible module.
This is especially useful in network automation when:
- You need to slice and dice complex show command output or device configs to pull out specific details (hostname, domain name, interface status, usernames, serial numbers, routing neighbors, etc).
- Your automation logic involves making decisions or transforming data in ways that are just too clunky with built-in filters or Jinja2 templates alone.
- You want more control over how data from devices is formatted for reports or other systems.
- You want to wrap up complex, reusable bits of logic into a neat, clean package.
Why Use Custom Ansible Modules?
Writing custom Ansible modules provides significant advantages for network automation:
- Write code that perfectly parses that specific show command output or config format unique to your environment or device OS versions.
- Once you build a custom module, you can use it across tons of playbooks and projects. Consistency is king.
- Shifting complex logic or data crunching into a custom module makes your main playbooks much cleaner and easier to read and maintain.
- You can build really robust error handling, input validation, and sophisticated conditional logic right into your Python code.
- Get full control over how data is processed and exactly what information gets passed back to your playbook.
Read more: Ansible Modules Types Explained
Ansible Module Directory Structure: Where to Place a Custom Ansible Module?
Ansible looks for custom modules in a few predefined spots. The easiest and most recommended way for project-specific modules is to create a directory named library/
right in the same folder as your main playbook file.
Recommended Project Structure:
your_project/
├── playbook.yml
├── library/
│ └── command_parser.py
When your custom module (like command_parser.py
) is in this library/
directory, Ansible automatically finds it and makes it available for playbook.yml
to use. No extra config needed.
Other Options:
Set a global path in ansible.cfg
:
[defaults]
library = ./custom_modules
Use ANSIBLE_LIBRARY
environment variable to define custom module paths.
Ansible Custom Module Example: Parse Cisco IOS Config
Let's build a custom Ansible module. The goal: log into a Cisco IOS device, run show running-config
(using a built-in module), and then use our custom module to pick out key details from that output.
Playbook: playbook.yml
---
- name: IOS command parser
hosts: r3
gather_facts: false
tasks:
- name: Send show commands
cisco.ios.ios_command:
commands: show run
register: sh_command
- name: Parse Config
command_parser:
running_config: ""
register: parser_output
- debug: msg=""
In this playbook, command_parser
is our custom Python module. The file command_parser.py
needs to be in a library/
folder right next to playbook.yml
.
Anatomy of an Ansible Custom Module (The Python Script)
What goes into that library/command_parser.py
file? Here's the essential Ansible module boilerplate structure every custom module needs:
1. Import AnsibleModule:
from ansible.module_utils.basic import AnsibleModule
import re
The AnsibleModule
class is your main helper. It handles things like parsing arguments passed from the playbook, managing results, and exiting with success or failure.
2. Define Module Arguments:
This tells Ansible what inputs your module expects from the playbook.
module = AnsibleModule(
argument_spec=dict(
running_config=dict(type='list', required=True)
)
)
Our command_parser
module expects one mandatory input called running_config
, which should be a list of strings (the lines from the Cisco config).
3. Implement Your Parsing Logic:
This is the heart of your module – Python code to process the inputs. We'll use regular expressions (regex) to pull out specific info from the Cisco running config.
- Hostname
- Domain name
- PID and Serial number
- Usernames with privilege level
4. Return Output (Results):
module.exit_json(changed=False, output=parsed_data)
This returns data back to Ansible, which can be used with debug, set_fact, or further tasks.
Full Custom Module Logic: library/command_parser.py
from ansible.module_utils.basic import AnsibleModule
import re
def main():
module = AnsibleModule(
argument_spec=dict(
running_config=dict(type='list', required=True)
)
)
try:
parsed_data = {}
running_config = module.params['running_config']
output = "\n".join(running_config)
###### Regex Patterns #############
hostname_pattern = re.compile(r'hostname (\S+)')
domain_name_pattern = re.compile(r'ip domain name (.+)')
pid_pattern = re.compile(r'license udi pid (\w+) sn (\S+)')
username_pattern = re.compile(r'username (\S+) privilege (\d{1,2})')
###### Parsing Logic ##############
hostname_match = hostname_pattern.search(output)
if hostname_match:
parsed_data['hostname'] = hostname_match.group(1)
domain_name_match = domain_name_pattern.search(output)
if domain_name_match:
parsed_data['domain'] = domain_name_match.group(1)
pid_match = pid_pattern.search(output)
if pid_match:
parsed_data['product_id'] = pid_match.group(1)
parsed_data['sn'] = pid_match.group(2)
user_list = []
for user in username_pattern.finditer(output):
user_list.append(user.group(1))
parsed_data['users'] = user_list
module.exit_json(changed=False, output=parsed_data)
except Exception as e:
module.fail_json(msg="Module Failed", exception=str(e))
if __name__ == '__main__':
main()
Sample Device Config Snippet (What’s being parsed)
hostname R3
ip domain name lab.local
license udi pid ISR4431/K9 sn FGL2103A123
username admin privilege 15 secret 0 password123
username guest privilege 1 secret 0 guestpass
Sample Output (from the playbook's debug task)
"msg": {
"changed": false,
"output": {
"hostname": "R3",
"domain": "lab.local",
"product_id": "ISR4431/K9",
"sn": "FGL2103A123",
"users": ["admin", "guest"]
}
}
Ansible Module Inputs & Outputs Explained
Inputs:
running_config
(list): The output of the show run command, split by lines (stdout_lines
).
Outputs:
- A dictionary with keys like:
- hostname
- domain
- product_id
- sn
- users (list of usernames)
How to Debug an Ansible Custom Module
How can I test a custom Ansible Module locally before using it in a playbook?
Test your custom modules locally before you try to integrate them into big, complex playbooks.
Method 1: Direct Python Execution (for quick Python logic tests)
You can modify your module script to accept command-line arguments when run directly with Python (if __name__ == '__main__'
). This is limited for testing Ansible-specific stuff, though.
# Create a test script: test_module.py
import sys
sys.path.insert(0, './library')
from command_parser import main
# Test with sample data
if __name__ == '__main__':
main()
Method 2: Ansible Ad-Hoc Command (Recommended)
This is the best way to test. It fully simulates how Ansible runs your module. Run it on your Ansible control node.
ansible localhost -m command_parser -a "running_config=['hostname R3', 'ip domain name test.com']"
Method 3: Check Mode Support
Implement check mode in your module (see the module.check_mode example above). Then run your playbook with --check
. Ansible will simulate actions without actually making changes.
module = AnsibleModule(
argument_spec=dict(
running_config=dict(type='list', required=True)
),
supports_check_mode=True
)
if module.check_mode:
# Return what would happen without making changes
module.exit_json(changed=False, msg="Would parse config data")
Troubleshooting Common Issues
Why Does My Custom Module Fail with ImportError? (ImportError: No module named 'my_custom_module_name')
Fixes: Make sure your module file has execute permissions (chmod +x library/command_parser.py
). Check the Python shebang at the top (e.g., #!/usr/bin/env python3
). Verify the module is actually in a library/
directory next to your playbook, or that Ansible knows where to find it. Make sure the filename exactly matches how you call it in the playbook (no typos!).
Module Fails with "AttributeError: 'bool' object has no attribute 'get'"
Problem: Usually means you defined an argument wrong in argument_spec
in your Python script, or your playbook is passing the wrong type of data (e.g., you expected a list but got a string).
Fix: Double-check argument_spec
(the type, required fields). Make sure your playbook is passing data in the correct format.
Why should custom Ansible module filenames avoid hyphens
Problem: Usually this comes along with Syntax Error: Invalid Module Name with Hyphens. Module filename contains hyphens: command-parser.py
Fix: Use underscores instead (e.g., command_parser.py
). Python module names can't have hyphens.
Module Cannot Find Required Python Libraries (ModuleNotFoundError: No module named 'requests' (or other libraries))
Problem: Your custom module tries to import a Python library that isn't available in the environment where Ansible is running the module (your control node, or inside an Execution Environment).
Fix: Install the missing library on your Ansible control node (or in your Execution Environment if using AAP/Navigator) using pip. If you're building a module for wider distribution, package it in an Ansible Collection and specify its dependencies there. For your own shared Python helper code, you can put it in a module_utils/
directory within your custom module's structure or collection.
How to create a custom Ansible collection
How Do I Handle External Dependencies?
Method 1: Install on Control Node (for simpler setups)
If your module runs on the Ansible control node (common for network modules using ansible.netcommon.network_cli
or tasks with delegate_to: localhost
), make sure any Python packages it needs are installed there. You can even add a task to your playbook to ensure they are:
- name: Install required Python packages
pip:
name:
- requests
- paramiko
- netmiko
delegate_to: localhost
- name: Run custom module
my_network_module:
device_ip: ""
Method 2: Create an Ansible Collection (Best for sharing or EEs)
For modules you want to share, or if you're using Ansible Automation Platform (AAP) with Execution Environments (EEs), package your custom modules into an Ansible Collection. Your collection structure would look something like:
my_network_collection/
├── galaxy.yml
├── plugins/
│ └── modules/
│ ├── ios_config_parser.py
│ └── nxos_config_parser.py
└── requirements.txt
When Should I Create a Collection vs. a Standalone Module?
Use Collections When:
- You have several related custom modules, plugins, or roles.
- You want to distribute your content to others easily.
- You need robust version control and dependency management for your shared content.
- You're developing for or integrating with AAP and Execution Environments.
Use Standalone Modules When:
- The module has a single, specific purpose tied directly to one project.
- You're doing quick prototyping.
- The logic is pretty simple (basic parsing or validation).
How to Split Large Modules into Multiple Files
For complex modules, organize code into packages:
library/
├── network_parser/
│ ├── __init__.py
│ ├── cisco_parser.py
│ ├── juniper_parser.py
│ └── utils.py
└── main_parser.py
Custom module with Ansible Open Tower
How to Use Custom Modules in AWX/Tower
These methods support Ansible Semaphore custom module integration and custom module with Ansible Open Tower deployments:
Method 1: Project-Based Approach
- Include your
library/
directory in your Git repository - AWX will automatically recognize modules in the library folder
- Ensure your project structure is correct:
project_repo/
├── playbooks/
├── library/ # AWX scans this automatically
│ └── command_parser.py
└── requirements.yml
Method 2: Custom Execution Environment (Recommended for complex Python dependencies):
If your custom modules need specific Python libraries or system packages that aren't in the default EEs, you need to build a custom EE. Your execution-environment.yml
(used by ansible-builder) would include steps to copy your custom modules into the EE's module path:
# execution-environment.yml
version: 1
dependencies:
galaxy: requirements.yml
python: requirements.txt
system: bindep.txt
additional_build_steps:
prepend: |
COPY library/ /usr/share/ansible/plugins/modules/
Method 3: Package as a Collection (Most Robust):
The most robust way is to package your custom modules into a proper Ansible Collection. Build it (ansible-galaxy collection build
), upload the .tar.gz file to Ansible Galaxy, your private Automation Hub, or another repo AWX/AAP can reach. Then, just include your custom collection in your project's requirements.yml
file. AWX/AAP will install it into the Execution Environment during project sync or when the EE is built.
collections:
- name: your_company.network_tools
version: ">=1.0.0"
Common AWX/AAP Integration Issues:
Module works locally but fails in AWX/AAP: Usually because the environment is different (Python version, Ansible version, libraries) between your local machine and the EE that AWX/AAP is using. Custom EEs are the fix to ensure consistency. Dependencies missing in AWX/AAP execution: If your custom modules rely on Python libraries that aren't in the default EE, you must add them to your custom EE definition.
Ansible Module Development Best Practices
When writing custom Ansible modules for network automation, follow these Ansible module development guidelines:
- Use consistent Ansible module boilerplate structures
- Implement proper Ansible module testing procedures
- Design for Ansible collections distribution when possible
- Focus on network automation use cases specific to your environment
- Leverage Ansible custom Python module patterns for reusability
More reading: Best practices for writing efficient and scalable Ansible Playbooks
or How to secure your Ansible credentials using Ansible Vault
What's Next? Try Building Your Own!
Give it a shot! Try writing your own custom Ansible module to:
- Parse show version output to get the OS version, uptime, and serial numbers.
- Extract interface status, error counts, and descriptions from show interfaces.
- Validate security policies (ACLs, firewall rules) against a defined baseline.
- Automate device compliance checking.
- Build custom reporting modules for specific inventory details.
With Python's flexibility, Ansible's orchestration power, and access to device CLIs/APIs (via Ansible connection plugins), you as a network engineer have massive capabilities to automate configuration auditing, validation, and enforcement at scale.
Conclusion
Custom Ansible modules are a potent tool for network engineers. They let you build automation logic tailored specifically for your network environment and operational needs. Whether you're parsing complex Cisco configurations, validating command outputs, or orchestrating multi-step decisions, custom Python modules bridge the gap between Ansible's simple declarative style and the flexible imperative logic you sometimes need for tricky network tasks.
If you're comfortable with CCNA/CCNP-level concepts and have a basic understanding of Python, you can definitely build and use these modules to significantly boost your Ansible network automation game.
Automation shouldn’t be trial and error in a live production environment. This is where CloudMyLab bridges the gap, providing:
✔ Hands-on lab environments to test Ansible Automation Platform in real-world conditions.
✔ Proof of Concept labs to validate automation workflows before deployment.
✔ Enterprise-ready scalability without the need for costly on-premise infrastructure.
💡 Why risk automation errors in production when you can test in a safe, enterprise-grade sandbox?
🔹 Start your automation journey today with CloudMyLab’s Ansible lab environments.
🔹 Sign up for a free trial and take your automation strategies from theory to reality.
Learning Resources
Here are some helpful resources for learning more about Ansible:
- Ansible Documentation: https://docs.ansible.com/
- Official Ansible labs by Red Hat: https://www.redhat.com/en/interactive-labs/ansible
- Ansible Forum: https://forum.ansible.com/
- Ansible Installation and Setup guide
Frequently Asked Questions
What's the difference between Ansible modules and plugins?
Modules are chunks of code Ansible runs on managed nodes (or the control node for some tasks) to do work (like our command_parser module). Plugins extend Ansible's core functionality on the control node itself, like custom inventory scripts, ways to display output, or new Jinja2 filters.
How to call built-in Ansible module from a custom Ansible module?
Not directly. Modules are designed to be standalone. If you need functionality similar to a built-in module, you'd typically use Python's own libraries or the same underlying libraries the built-in module might use. For example, instead of trying to call ansible.builtin.copy
from your Python code, you'd just use Python's shutil
module.
When should I create a *_info or *_facts module instead of extending an existing module?
*_facts modules (like cisco.ios.ios_facts
) are designed to gather information and return it in a way that Ansible automatically adds to ansible_facts
. This makes the data easily available to all subsequent tasks in the play. Use these for broadly useful information you'll need often. *_info modules also gather info, but you typically register their output to a variable for immediate use; the data doesn't automatically become ansible_facts
unless you explicitly use set_fact
. Use these for one-time data gathering.
Can I use print() statements in custom modules for debugging?
While print()
will technically work, the output can get messy and mixed in with Ansible's own output. It's better to use Ansible's built-in logging via the AnsibleModule
object: module.debug("your debug message here")
(visible with -v
or higher verbosity), or module.warn("your warning message")
.
Can custom modules work with both Python 2.7 and Python 3?
Ansible has deprecated Python 2.7 on the control node. For modules that run on the control node (which includes most network modules using network_cli
, local_action
, or when run inside Execution Environments), you should write Python 3 code. If you have a super rare case where you must target a legacy Python 2.7 system (not the control node itself), it requires specific handling, but modern Ansible is all about Python 3.
How do I define and process arguments/parameters in a custom Ansible module?
You define the arguments your module accepts in the argument_spec
dictionary that you pass to the AnsibleModule
constructor. Each key in that dictionary is an argument name, and its value defines things like the expected type (str, list, bool), whether it's required, a default value, etc. Inside your module's code, you access the values passed from the playbook via module.params['your_argument_name']
.
Is it possible to have custom Ansible modules grouped in only one folder and have them available for all playbooks?
Yes. You can configure a global module library path in your ansible.cfg
file (e.g., library = /opt/my_shared_ansible_modules
) or by setting the ANSIBLE_LIBRARY
environment variable. However, for better organization, versioning, and distribution (especially if you have multiple shared modules), packaging them into an Ansible Collection is the recommended approach.
What is the correct way to handle check mode in a custom Ansible module?
First, instantiate your AnsibleModule
with supports_check_mode=True
. Then, in your module's logic, add a condition: if module.check_mode:
. Inside this block, you should determine if your module would make any changes if it weren't in check mode. If changes would occur, call module.exit_json(changed=True, ...)
with a descriptive message. If no changes would occur, call module.exit_json(changed=False, ...)
. Crucially, do not perform any actual changes on the target device when in check mode.
How do I return custom facts from a module so that they are accessible to other modules or tasks?
When your module is ready to exit successfully, call module.exit_json(changed=False, ansible_facts={'my_custom_fact_name': 'its_value', 'another_fact': 123})
. The key ansible_facts
is special; Ansible will take the dictionary you provide there and merge it into the main ansible_facts
available to subsequent tasks.
How do I get my custom modules into Ansible AWX / Automation Hub (or AAP)?
Primarily three ways:
- Include library/ in your SCM project (Git repo): Easiest for project-specific modules. AWX/AAP automatically finds them.
- Build a Custom Execution Environment (EE): If your modules have specific Python or system dependencies, build an EE using ansible-builder and include steps to COPY your library/ directory (or installed collection) into a standard module path inside the EE image.
- Package as an Ansible Collection (Most Robust): Build your modules into a collection (
ansible-galaxy collection build
). Upload the .tar.gz to Ansible Galaxy, your private Automation Hub, or another repository. Then, include your collection in your project'srequirements.yml
. AWX/AAP will install it into the EE.