Rendering Cisco configuration with Ansible and Jinja2

Rendering Cisco configuration with Ansible and Jinja2

Hi guys! In this post I am demonstrating the use of Ansible's Jinja2 templating module, and the applications it can be utilized for within a service-provider network.

As Network Engineers, we deal with various vendor-specific configuration syntaxes on the job. For everyday device probing and auditing this is second nature, but for provisioning configuration en-masse it is appropriate to face the task from a programmable viewpoint.

I will be lightly touching on a number of elements here, and hope to provide a reasonable framework for general network automation. Obviously be sure to do some light reading on each tool to understand the available functionality and operation.


Jinja2

Jinja2 is a general-purpose templating language based on the Django Python framework. For our purposes, we can leave much of our Jinja2 elements as agnostic plain-text, but retain the ability to incorporate Pythonic logic where we can.

The below represents the Jinja2 framework for a basic IPv4 interface configuration on a Cisco IOS-XR device. I have named this "interfaces.j2" and have stored it on a Git repo:

configure terminal
{% for item in ip_interfaces %}
  interface {{ item.interface }}
    description {{ item.description }}
    ipv4 address {{ item.ip }}{{ item.cidr }}
{% if feed.mbps != "" %}
    service-policy input {{ item.mbps }}Mb
    service-policy output {{ item.mbps }}Mb
{% endif %}
    no shutdown
    exit
{% endfor %}

Worth noting now, the .j2 extension is entirely optional, but it popping it on allows for syntax highlighting in your chosen text-editor.

The only elements of this template that Jinja2 will pick up on are enclosed within curly braces, and there are 3 examples of which in the above:

  • {{ object.key }} - Value substitution
  • {% for this in that %} - Iterative loop
  • {% if this is that %} - Conditional statement

The first is an example of Jinja2 value substitution, and is likely the most prevalent Jinja2 element you will be using. This allows you to sort through defined variables to inject dynamic values into your template. The creation and processing of these templates is called Rendering.

The second element is an iterative for loop, which will render the enclosed template based on a condition. In my example, I am iterating over a list of objects to generate the IOS-XR configuration for multiple layer-3 interfaces, which can save a lot of manual work when scaling for a large provisioning project.

The final element is a very Pythonic conditional statement, and will check to see if a value has been provided for the object key before rendering the configuration. This is useful for handling optional parameters.

Now we can see that Jinja2 can already provide some powerful logic handling, but we have still yet to pass it any values to operate on. There are many ways to accomplish this, but I prefer to opt for JSON or straight YAML for Ansible standardization. Let's stick to JSON for now:


JSON

To provide the values for Jinja2 to parse, I have create a local JSON file, "vars.json":

{
   "ip_interfaces":[
      {
         "interface":"Gi0/1/0/1",
         "description":"This is interface number 1!",
         "ip":"1.1.1.1",
         "cidr":"/31",
         "mbps":"10"
      },
      {
         "interface":"Te0/7/0/1",
         "description":"And this is interface number 2!",
         "ip":"8.8.8.8",
         "cidr":"/29",
         "mbps":""
      }
   ]

}

The above should be pretty straightforward to work out, but all I am doing it defining 2 IP interfaces with 5 key values each. Notice there is a "mbps" key that will correspond to a rate-limiting policy-map on our IOS-XR device? Well the second interface has not been provided a value for this key. If our Jinja2 conditional statement is correct, the Cisco configuration for the policy-map on the second interface should be omitted from the final render.

Now JSON is not necessary, and if you are a fan of YAML then the above can be easily represented something like this as a vars.yaml:

---
ip_interfaces:
- interface: Gi0/1/0/1
  description: This is interface number 1!
  ip: 1.1.1.1
  cidr: "/31"
  mbps: '10'
- interface: Te0/7/0/1
  description: And this is interface number 2!
  ip: 8.8.8.8
  cidr: "/29"
  mbps: ''

If you so desire, you can even just go ahead and collect the data in CSV format:

This is personal preference, however if you are working with Ansible facts or a JSON API, then it may make sense for you to retain the same format throughout your automation workflow.

Now finally I will be using Ansible as the automation framework to tie the values and Jinja2 templating together.


Ansible

I will present the full Ansible Playbook further down the page, but I will start with walking through the items step by step.

First of all, we are creating this Playbook to pass our JSON values into our Jinja2 template. Let us load our JSON in the first instance as an included vars_file at the top of our Playbook:

- hosts: localhost
  connection: local
  vars_files:
    - vars.json

By including my vars file, I can keep all values external to the Playbook. There are of course other ways of passing in your values, and so I will drop a link the the official Docs here:

https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html

I mentioned earlier that I am storing my Jinja2 templates on a Git repo. This allows Ansible to dynamically pull the latest configuration templates at each run, and for templates to be modified and stored in a single central location. Let's fetch this repo as our first task using the Git module in our generator.yml:

  tasks:
    - name: Pull latest config from Gitea server
      git:
        repo: http://10.20.30.40:3000/Skylar/Jinja2_Templates
        dest: Jinja2_Templates
        update: yes

Now I promised you it would be easy once we had our JSON values and .j2 template, and so here we go with the Jinja2 Template module:

    - name: Generate IOS-XR Config
      template:
        src: "{{ item.src }}"
        dest: "{{ item.dest }}"
        mode: 0777
      with_items:
        - {src: 'Jinja2_Templates/interfaces.j2',dest: 'interfaces.config'}

Now this task will iterate though the with_items list and will render every .j2 template with the values we have provided. If we later decide to render multiple templates in the same Playbook, we can add another item under with_items.

Our Playbook is good to go, and is looking like this so far:

- hosts: localhost
  connection: local
  vars_files:
    - vars.json
  tasks:
    - name: Pull latest config from Gitea
      git:
        repo: http://10.20.30.40:3000/Skylar/Jinja2_Templates
        dest: Jinja2_Templates
        update: yes
   - name: Generate IOS-XR Config
      template:
        src: "{{ item.src }}"
        dest: "{{ item.dest }}"
        mode: 0777
      with_items:
        - {src: 'Jinja2_Templates/interfaces.j2',dest: 'interfaces.config'}

Shall we try it out?


Our first Render

Let's jump straight in and run this with ansible-playbook generator.yml:

You should now see a new file in your working directory named interfaces.config which will contain the following:

configure terminal
  interface Gi0/1/0/1
    description This is interface number 1!
    ipv4 address 1.1.1.1/31
    service-policy input 10Mb
    service-policy output 10Mb
    no shutdown
    exit
  interface Te0/7/0/1
    description And this is interface number 2!
    ipv4 address 8.8.8.8/29
    no shutdown
    exit

Fantastic! So what can we see here:

  • Firstly, our variable substitution has gone through with no issue
  • Secondly, our for loop executed successfully and generated the configuration for the second interface
  • Finally, our conditional check was successful and the service-policy application on the second interface was ignored

We can add a 3rd object into our JSON list and render again:

{
   "ip_interfaces":[
      {
         "interface":"Gi0/1/0/1",
         "description":"This is interface number 1!",
         "ip":"1.1.1.1",
         "cidr":"/31",
         "mbps":"10"
      },
      {
         "interface":"Te0/7/0/1",
         "description":"And this is interface number 2!",
         "ip":"8.8.8.8",
         "cidr":"/29",
         "mbps":""
      },
      {
         "interface":"Te0/7/0/2",
         "description":"And this is interface number 3!",
         "ip":"10.10.10.10",
         "cidr":"/21",
         "mbps":"50"
      }
   ]

}

Which renders as:

configure terminal
  interface Gi0/1/0/1
    description This is interface number 1!
    ipv4 address 1.1.1.1/31
    service-policy input 10Mb
    service-policy output 10Mb
    no shutdown
    exit
  interface Te0/7/0/1
    description And this is interface number 2!
    ipv4 address 8.8.8.8/29
    no shutdown
    exit
  interface Te0/7/0/1
    description And this is interface number 3!
    ipv4 address 10.10.10.10/21
    service-policy input 50Mb
    service-policy output 50Mb
    no shutdown
    exit

So our configuration is ready to copy and paste onto our IOS-XR device, but why not let Ansible do it?


Ansible IOSXR Configuration

So the iosxr_config module can handle the SSH session establishment and configuration of our end devices, provided your host file and group vars have the neccessary configuration. Adding the below task to our playbook will push our rendered configuration to any specified hosts or host groups:

    - name: Apply infrastructure config
      iosxr_config:
        src: interfaces.config
        replace: config
        backup: yes
        when:
          - ansible_network_os == 'iosxr'

I personally always include the below prompt task beforehand so that I have the option to cancel the iosxr_config task, as I may or may not want Ansible to automate the configuration every time:

    - name: Interface config confirmation
      pause:
        prompt: 'Applying interfaces.config to host(s). Press return to continue. Press Ctrl+c and then "A" to abort'

This will greet you with a friendly prompt to confirm:

Alternatively you can abort the Playbook, and apply your rendered config manually:

So we have a robust automation framework in place now, utilising JSON value handling and version-controlled Jinja2 templating all tied together in an Ansible Playbook. Where else can we go from here?


What's next?

Since our Jinja2 templates are stored on our Git repo, we can modify them on the fly with the confidence that our automated Ansible framework will always pull the latest template. We can also begin to fill the repo with more templates for configurations that may be programmable. A few more examples could be:

  • MPLS L2VPN Pseudowires:
  • Rate-Limiting Policy-Maps:
  • IPv4 and VPNv4 BGP neighborships:

And so many more! Access-Lists, Prefix-Lists, LACP Bundles, VRRP, VLANs, Route-Redistribution...the list of network configuration elements that can be handled in a programmable and automated approach is endless.

And all this takes is to add in extra list items to our JSON vars file, like so:

To generate IOSXR configurations like this:


This has been a run through of an example automation workflow that can be tailored to suit your needs.

Thank you for reading!

Related Article