Automation
Showing results for 
Search instead for 
Do you mean 

Expert Advice: Writing Good Configuration Templates

by Juniper Employee on ‎10-05-2015 11:07 AM - edited on ‎09-11-2017 05:12 PM by Administrator Administrator

Writing Good Configuration Templates

 

When designing a configuration template there are a few things you need to consider. In this article I will explain these considerations and how they affect the design of the variables and templates. I will do this by comparing two templates that produce the same configuration.

 

The templates will produce configuration for interfaces. These templates are written in the Jinja2 format and the data used to populate the template is written in YAML.

 

The advice given here is not specific to these technologies and can be applied to any templating system.

 

Here is the first template. Following that is the YAML.

 

interfaces {
    {% for ifs in interfaces %}
    {% set intf = interfaces[ifs] %}
    {{ ifs }} {
        {% if intf.description is defined %}
        description "{{ intf.description }}";
        {% endif %}
        {% if intf.units is defined and intf.units is iterable %}
        {% for unit in intf.units %}
        unit {{ unit.id }} {
            family inet {
                address {{ unit.ip }}/{{ unit.mask_bits }};
                {% if unit.filter is defined %}
                filter {
                    {% if unit.filter.in is defined %}
                    input {{unit.filter.in}};
                    {% endif %}
                    {% if unit.filter.out is defined %}
                    output {{ unit.filter.out }};
                    {% endif %}
                }
                {% endif %}
            }
            {% if intf.vlan_tagging is defined %}
            description "{{ unit.description }}";
            vlan-id {{unit.vlan_id}};
            {% endif %}
            {% if intf.syslog is defined %}
            description "{{ unit.description }}";
            {% endif %}
        }
        {% endfor %}
        {% endif %}
        {% if intf.redundant_parent is defined %}
        gigether-options {
            redundant-parent {{ intf.redundant_parent }};
        }
        {% endif %}
        {% if intf.mtu is defined %}
        mtu {{ intf.mtu }};
        {% endif %}
        {% if intf.vlan_tagging is defined %}
        vlan-tagging;
        {% endif %}
        {% if intf.redundant_options is defined %}
        redundant-ether-options {
            redundancy-group {{ intf.redundant_options.group }};
            {% if intf.lacp is defined %}
            lacp {
                active;
                periodic fast;
            }
            {% endif %}
        }
        {% endif %}
    }
    {% endfor %}
}

 

Here is the YAML:

 

interfaces:
  reth0:
    description: core facing interface
    lacp: true
    mtu: 9154
    redundant_options: { group: 1 }
    reth: true
    units:
      - { id: 0, ip: 10.0.0.1, mask_bits: 31 }
  ge-0/0/0:
    description: access facing interface
    mtu: 1500
    vlan_tagging: true
    units:
      - description: customer 1
        id: 1
        vlan_id: 1
        ip: 10.0.1.1
        mask_bits: 30
        filter:
          in: CUSTOMER_IN_FILTER
          out: CUSTOMER_OUT_FILTER
- description: customer 2
id: 2
vlan_id: 2
ip: 10.0.2.1
mask_bits: 30
filter:
in: CUSTOMER_IN_FILTER
out: CUSTOMER_OUT_FILTER xe-1/0/0: { description: core1.site1 xe-1/0/0, redundant_parent: reth0 } xe-2/0/0: { description: core1.site1 xe-2/0/0, redundant_parent: reth0 }

This example generates the configuration for a core interface and access interface on an SRX. The core interface is an RETH interface with two members. The access interface is a VLAN tagged interface where each customer is assigned a single VLAN.

 

This template certainly works and has all the required information to produce valid configuration. However, it has a number of problems with it. The most critical problem is that enforcement of the design is completely up to the person filling in the YAML. There are a few examples of this: applying the filter; setting the MTU; setting the subnet mask; enabling LACP.

 

If one of these values is missing it does not guarantee that the config will not apply. For example it is possible to miss the LACP setting on the core interface. If that happens valid configuration is produced but it does not conform to the design.

 

There is also repetition of variables. A good example of that is the id and vlan_id. These typically are the same value. The customer facing firewall filters are the same too. The redundant_parent field is also repeated for each member link in the RETH.

 

The template is particularly complicated. Within each interface it is catering for a wide range of capabilities. This makes updating the template very complex and error prone. The editor of the template must take into consideration the design and options of both the core and access interfaces and make sure they do not break either.

 

Now consider the revised Jinja2 and YAML:

 

interfaces {
    {% for port, int in core_interfaces.items() %}
    {{ port }} {
        description "{{int.description }}";
        mtu 9154;
        redundant-ether-options {
            redundancy-group {{ int.reth_group }};
            lacp {
                active;
                periodic fast;
            }
        }
        unit 0 {
            family inet {
                address {{ unit.ip }}/31;
            }
        }
    }
    {% for member in int.member.ints %}
    {{ member }} {
        description "{{ int.member.neighbor }} {{ int.member.ints[member] }}";
        gigether-options {
            redundant-parent {{ port }};
        }
    }
    {% endfor %}
    {% endfor %}
    {% for port, int in access_interfaces.items() %}
    {{ port }} {
        description "{{int.description }}";
        mtu 1500;
        vlan-tagging;
        {% for unit in int.units %}
        unit {{ unit.id }} {
            family inet {
                address {{ unit.ip_address }}/30;
                filter {
                    input CUSTOMER_IN_FILTER;
                    output CUSTOMER_OUT_FILTER;
                }
            }
            description "{{ unit.description }}";
            vlan-id {{ unit.id }};
        }
        {% endfor %}
    }
    {% endfor %}
}

Here is the associated YAML:

 

core_interfaces:
  reth0:
    description: core facing interface
    reth_group: 1
    ip_address: 10.0.0.1
    member:
      neighbor: core1.site1
      ints:
        xe-1/0/0: xe-1/0/0
        xe-2/0/0: xe-2/0/0
access_interfaces:
  ge-0/0/0:
    description: access facing interface
    units:
      - description: customer 1
        id: 1
        ip_address: 10.0.1.1
- description: customer 2
id: 2
ip_address: 10.0.2.1

With this Jinja2 and YAML design I have split them into two sections. Each section focuses on the purpose of that section. This reduced the number of variables needed for each section. In turn this helps enforce the configuration design - for example it is no longer possible to miss LACP from a core interface; core interfaces are not VLAN enabled; firewall filters are always applied to customer facing interfaces. 

 

It also simplifies editing the Jinja2 template too. The editor can easily see exactly which part of the design there are changes (core or access) and can edit it without fear of breaking the other.

 

When writing a template consider the purpose of that part of the template. As a general rule, the less you need to define in the YAML the better. Each variable you define is an opportunity for a mistake. The people updating the YAML will also thank you. They will much prefer to define three variables per customer than seven!

 

I hope you can see that by not generalising the template too much you improve its readability and maintainability. By using good variable names in the YAML you also convey each sections purpose.