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 contentsAn 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:
Writing custom Ansible modules provides significant advantages for network automation:
Read more: Ansible Modules Types Explained
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.
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
.
What goes into that library/command_parser.py
file? Here's the essential Ansible module boilerplate structure every custom module needs:
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.
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).
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.
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.
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()
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
"msg": {
"changed": false,
"output": {
"hostname": "R3",
"domain": "lab.local",
"product_id": "ISR4431/K9",
"sn": "FGL2103A123",
"users": ["admin", "guest"]
}
}
Inputs:
running_config
(list): The output of the show run command, split by lines (stdout_lines
).Outputs:
Test your custom modules locally before you try to integrate them into big, complex playbooks.
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()
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']"
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")
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!).
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.
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.
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.
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: ""
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
Use Collections When:
Use Standalone Modules When:
For complex modules, organize code into packages:
library/
├── network_parser/
│ ├── __init__.py
│ ├── cisco_parser.py
│ ├── juniper_parser.py
│ └── utils.py
└── main_parser.py
These methods support Ansible Semaphore custom module integration and custom module with Ansible Open Tower deployments:
library/
directory in your Git repositoryproject_repo/
├── playbooks/
├── library/ # AWX scans this automatically
│ └── command_parser.py
└── requirements.yml
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/
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"
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.
When writing custom Ansible modules for network automation, follow these Ansible module development guidelines:
More reading: Best practices for writing efficient and scalable Ansible Playbooks
or How to secure your Ansible credentials using Ansible Vault
Give it a shot! Try writing your own custom Ansible module to:
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.
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.
Here are some helpful resources for learning more about Ansible:
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.
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.
*_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.
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")
.
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.
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']
.
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.
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.
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.
Primarily three ways:
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's requirements.yml
. AWX/AAP will install it into the EE.