In every serious Network Automation project, we hit the stage where neither built-in Ansible modules nor Ansible Galaxy modules are sufficient to fit our custom requirements. Therefore, we turn to writing our own custom Ansible modules. Thanks to Ansible’s flexible design, this is accomplished very easily by means of Python programming. For many custom Ansible modules, having functional Python code will do the work. In some cases, especially within the context of Network Automation, understanding, maintaining and navigating through functional Python code simply becomes too much of a hassle. In this tutorial, we will demonstrate how to achieve the same module capability with functional style and class (OOP) style coding.
Let’s imagine we are tasked to create a straightforward module which generates Junos CLI from a given data model. We are focused solely on address and address groups. For address name ADDRESS_SRC1 and address subnet 10.10.10.0/24 the following set command should be generated: set security address-book global address ADDRESS_SRC1 10.10.10.0/24
. Provided that this address object should be associated with address group named AG_SRC the module will generate the following config line: set security address-book global address-set AG_SRC address ADDRESS_SRC1
.
Our data module should look like this:
main.yml
- hosts: localhost
gather_facts: no
vars:
addresses_list_of_dicts:
- name: ADDRESS_SRC1
subnet: 10.10.10.0/24
- name: ADDRESS_SRC2
subnet: 11.11.11.0/24
- name: ADDRESS_DST1
subnet: 200.200.200.0/24
- name: ADDRESS_DST2
subnet: 222.222.222.0/24
address_groups_list_of_dicts:
- name: AG_SRC
addresses:
- ADDRESS_SRC1
- ADDRESS_SRC2
- name: AG_DST
addresses:
- ADDRESS_DST1
- ADDRESS_DST2
Each item under <strong>addresses_list_of_dicts</strong>
should translate into a new address object. Similarly, for each item under <strong>address_groups_list_of_dicts</strong>
an address group object is to be created.
We want to name our custom Ansible module create_junos_config1
which means there must be a file create_junos_config1.py
in the <strong>library</strong>
folder. We want to pass the input data like this:
main.yml
- hosts: localhost
gather_facts: no
vars:
addresses_list_of_dicts:
- name: ADDRESS_SRC1
subnet: 10.10.10.0/24
- name: ADDRESS_SRC2
subnet: 11.11.11.0/24
- name: ADDRESS_DST1
subnet: 200.200.200.0/24
- name: ADDRESS_DST2
subnet: 222.222.222.0/24
address_groups_list_of_dicts:
- name: AG_SRC
addresses:
- ADDRESS_SRC1
- ADDRESS_SRC2
- name: AG_DST
addresses:
- ADDRESS_DST1
- ADDRESS_DST2
tasks:
- name: Generate Junos CLI commands
create_junos_config1:
addresses: "{{ addresses_list_of_dicts }}"
address_groups: "{{ address_groups_list_of_dicts }}"
register: junos_config1
- name: Print Junos CLI commands
debug:
msg: "{{ junos_config1 }}"
The Python code of the module is as following:
create_junos_config1.py
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule
def main():
# Define input params
inputparams = {
"addresses": {"required": True, "type": "list"},
"address_groups": {"required": True, "type": "list"},
}
# Read input params
module = AnsibleModule(argument_spec=inputparams)
addresses = module.params["addresses"]
address_groups = module.params["address_groups"]
# Outputs
config = []
# Business logic
for addr in addresses:
cmd = (
f"set security address-book global address {addr['name']}"
f" {addr['subnet']}"
)
config.append(cmd)
for ag in address_groups:
for addr in ag["addresses"]:
cmd = (
f"set security address-book global address-set {ag['name']}"
f" address {addr}"
)
config.append(cmd)
# Exit module, back to playbook
module.exit_json(
changed=False,
config=config,
)
if __name__ == "__main__":
main()
There are some well known sections to it:
- necessary tooling
<strong>AnsibleModule</strong>
is imported - section with inputs, as it should be laid out in the Ansible playbook, is defined
- section for reading the passed params in Python code from Ansible playbook (
<strong>addresses</strong>
andaddress_groups
) - business logic, which is actually doing what we want the module to do (generation of set commands)
- exiting (Python) module and returning results back to Ansible playbook
Running the main.yml
playbook results in:
run
(ansible) devuser@vmi520322:~/work/075_ansible_modules_with_classes$ ansible-playbook main.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [localhost] ****************
TASK [Generate Junos CLI commands] ****************
ok: [localhost]
TASK [Print Junos CLI commands] ****************
ok: [localhost] => {
"msg": {
"changed": false,
"config": [
"set security address-book global address ADDRESS_SRC1 10.10.10.0/24",
"set security address-book global address ADDRESS_SRC2 11.11.11.0/24",
"set security address-book global address ADDRESS_DST1 200.200.200.0/24",
"set security address-book global address ADDRESS_DST2 222.222.222.0/24",
"set security address-book global address-set AG_SRC address ADDRESS_SRC1",
"set security address-book global address-set AG_SRC address ADDRESS_SRC2",
"set security address-book global address-set AG_DST address ADDRESS_DST1",
"set security address-book global address-set AG_DST address ADDRESS_DST2"
],
"failed": false
}
}
PLAY RECAP ****************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The results look good. The list of returned command can be loaded into Juniper device with either juniper.device.config or junipernetworks.junos.junos_config Ansible modules.
Now, let’s create create_junos_config2 custom Ansible module, which will keep exactly the same functionality but the code style will be different. We will be using Python class.
create_junos_config2.py
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule
class JunosConfig:
def __init__(self, module, addresses, address_groups):
# inputs
self.module = module
self.addresses = addresses
self.address_groups = address_groups
# outputs
self.config = []
def _generate_address_config(self):
for addr in self.addresses:
cmd = (
f"set security address-book global address {addr['name']}"
f" {addr['subnet']}"
)
self.config.append(cmd)
def _generate_group_config(self):
for ag in self.address_groups:
for addr in ag["addresses"]:
cmd = (
f"set security address-book global address-set {ag['name']}"
f" address {addr}"
)
self.config.append(cmd)
def generate_config(self):
# Business logic
self._generate_address_config()
self._generate_group_config()
# Exit module, back to playbook
self.module.exit_json(
changed=False,
config=self.config,
)
def main():
# Define input params
inputparams = {
"addresses": {"required": True, "type": "list"},
"address_groups": {"required": True, "type": "list"},
}
module = AnsibleModule(argument_spec=inputparams)
juniper_config = JunosConfig(
addresses=module.params["addresses"],
address_groups=module.params["address_groups"],
module=module,
)
juniper_config.generate_config()
if __name__ == "__main__":
main()
There are a couple of important things to note here:
- Business logic is contained in
JunosConfig
class. - The only purpose of
main()
is to instantiate the class and invoke thegenerate_config
method. - We must pass
<strong>module</strong>
to class instance too as we handle module exit from the class. - We conveniently break the business logic of the class into smaller chunks by leveraging two helper methods (
_generate_address_config
and_generate_group_config
).
The full code for main.yml
now looks like:
main.yml
- hosts: localhost
gather_facts: no
vars:
addresses_list_of_dicts:
- name: ADDRESS_SRC1
subnet: 10.10.10.0/24
- name: ADDRESS_SRC2
subnet: 11.11.11.0/24
- name: ADDRESS_DST1
subnet: 200.200.200.0/24
- name: ADDRESS_DST2
subnet: 222.222.222.0/24
address_groups_list_of_dicts:
- name: AG_SRC
addresses:
- ADDRESS_SRC1
- ADDRESS_SRC2
- name: AG_DST
addresses:
- ADDRESS_DST1
- ADDRESS_DST2
tasks:
- name: Generate Junos CLI commands
create_junos_config1:
addresses: "{{ addresses_list_of_dicts }}"
address_groups: "{{ address_groups_list_of_dicts }}"
register: junos_config1
- name: Print Junos CLI commands
debug:
msg: "{{ junos_config1 }}"
- name: Generate Junos CLI commands
create_junos_config2:
addresses: "{{ addresses_list_of_dicts }}"
address_groups: "{{ address_groups_list_of_dicts }}"
register: junos_config2
- name: Print Junos CLI commands
debug:
msg: "{{ junos_config2 }}"
When we now run it, we get the exact same result twice:
run
Conclusion:
The internal architecture of custom Ansible module can be as simple as one function or multi-layered with a complex class inheritance. Depending on your needs, you may decide to opt for a more complex class-style rather than breaking business logic into hard to manage functional programming chunks. In this article, we have shown how to achieve the same result with both styles. As a rule of thumb, if you are unsure which style to use, you should start with functional style and convert to class style when you reach a certain level of code complexity.