Introduction
Nornir is a framework for network automation, the framework is written in Python and gels Paramiko, Netmiko, Napalm, Jinja2 and Ansible. The use of framework makes the code more consistent and easier to adapt.
Secret management
Hashicorp vault is required for storing the secret in the code. The extraction of secrets from the vault uses my own self written script – pyvault2. The function which I use is get_kv2_secret(mount_path="cisco_asa", path="fw01", find="data")
.
Nornir inventory file manipulation
A yaml file is used for inventory just like Ansible. You can modify the content of the yaml file like below, you can read the documentation and test out yourself, the below is the one which worked for me.
def change_host_data(host): host.username = credential_from_vault["username"] host.password = credential_from_vault["password"] host.hostname = credential_from_vault["ip"] host.platform = "cisco_asa" nr = InitNornir( inventory={ "plugin": "nornir.plugins.inventory.simple.SimpleInventory", "options": { "host_file": "templates/hosts.yaml" }, "transform_function": change_host_data } )
Generate configuration from template and use the configuration to push to cisco asa
Read this documentation to learn how to use jinja2 in Nornir. The below is the one worked for me.
def add_acl(task): cmd = task.run(task=template_file, name="Generating template", template="asa_acl.j2", path="./templates/", acl_name="abc", action="permit", protocol="udp", src_fw_object_type="host", src_object="192.168.1.200", dst_fw_object_type="host", dst_object="8.8.8.8", svc_object_group_type="eq", svc_object_group="53") task.host["config"] = cmd.result task.run(task=netmiko_send_config, name="Sending configuration", config_commands=task.host["config"]) task.run(task=netmiko_save_config, name="Saving configuration", cmd="write memory")
This is the jinja2 template which I used.
{% if line_number is defined %} access-list {{acl_name}} line {{line_number}} extended {{action}} {{protocol}} {{src_fw_object_type}} {{src_object}} {{dst_fw_object_type}} {{dst_object}} {{svc_object_group_type}} {{svc_object_group}} log {% else %} access-list {{acl_name}} extended {{action}} {{protocol}} {{src_fw_object_type}} {{src_object}} {{dst_fw_object_type}} {{dst_object}} {{svc_object_group_type}} {{svc_object_group}} log {% endif %}
Jinja2 has defined()
which checks if the variable is specified, if not specified then defined()
returns false.
To use jinja2, first use the template_file task, then specify template and path, the template is the jinja2 template file, and path is the path to find the template file.
To push the configuration created by jinja2 template engine, use the task netmiko_send_config
, then assign the command generated by template_file to config_commands
.
Entire testing code
from nornir import InitNornir from nornir.plugins.tasks.networking import netmiko_send_config, netmiko_save_config from nornir.plugins.tasks.text import template_file from nornir.plugins.functions.text import print_result from pyvault2.vault.hvault2 import get_kv2_secret """ The credential_from_vault function can be found here: https://github.com/sirbowen78/pyvault2/blob/master/vault/hvault2.py """ credential_from_vault = get_kv2_secret(mount_path="cisco_asa", path="fw01", find="data") """ Reference: https://nornir.readthedocs.io/en/stable/tutorials/intro/grouping_tasks.html """ def change_host_data(host): host.username = credential_from_vault["username"] host.password = credential_from_vault["password"] host.hostname = credential_from_vault["ip"] host.platform = "cisco_asa" def add_acl(task): cmd = task.run(task=template_file, name="Generating template", template="asa_acl.j2", path="./templates/", acl_name="abc", action="permit", protocol="udp", src_fw_object_type="host", src_object="192.168.1.200", dst_fw_object_type="host", dst_object="8.8.8.8", svc_object_group_type="eq", svc_object_group="53") task.host["config"] = cmd.result task.run(task=netmiko_send_config, name="Sending configuration", config_commands=task.host["config"]) task.run(task=netmiko_save_config, name="Saving configuration", cmd="write memory") nr = InitNornir( inventory={ "plugin": "nornir.plugins.inventory.simple.SimpleInventory", "options": { "host_file": "templates/hosts.yaml" }, "transform_function": change_host_data } ) r = nr.run(task=add_acl) print_result(r)
Demonstration
This is what the firewall has:
To insert another line, I specify the line in my code.
cmd = task.run(task=template_file, name="Generating template", template="asa_acl.j2", path="./templates/", acl_name="abc", line_number="3", action="permit", protocol="udp", src_fw_object_type="host", src_object="192.168.1.210", dst_fw_object_type="host", dst_object="12.12.12.12", svc_object_group_type="eq", svc_object_group="443")
This is the output of the code:
This is the acl in the cisco asa:
The use of line in the access-list command allows you to insert preceding rules, but if the rule is on the next available line, then there is no need to specify line. Here’s the code without the line_number:
cmd = task.run(task=template_file, name="Generating template", template="asa_acl.j2", path="./templates/", acl_name="abc", action="permit", protocol="udp", src_fw_object_type="host", src_object="192.168.1.101", dst_fw_object_type="host", dst_object="123.123.123.123", svc_object_group_type="eq", svc_object_group="8443")
This is the output of the code:
This is the updated ACL in cisco asa: