Jinja Template Inheritance

block and extends

12 August 2020   4 min read

Jinja template inheritance uses the concept of block to define sections of the base parent template that can be overridden by sections from a child template. An extends statement links the child template to the parent template so that when the child template is rendered the parent template is also rendered and the block statement contents inherited by the parent template.

Parent template

All that needs to be defined in the parent template are the block statements. If a block is empty it will be ignored when the template is rendered unless the child template has a block statement with the exact same name. Block names must be unique within a template, no duplicates.

{% block svc_tnt_show_vpc %}
      peer-link_vlans: "1-2"
{% endblock %}

Child template

If a template contains an ‘extends’ statement it’s considered as being a child template. It tells the template engine that this template extends into another template, meaning the other template inherits from this one.

  • Can only have 1 extends statement in each template
  • The block statement must have the same name in both templates and can only be used once
  • Block statements of the same name will override other higher level template block statements
  • The data in the child template is actually rendered before the parent template is run
  • The output of the rendered child template is added to the parent template and then the parent template rendered
{% extends ansible_network_os + "/bse_fbc_val_tmpl.j2" %}
{% block svc_tnt_show_vpc %}
      peer-link_vlans: {{ stp_fwd_vlans }}
{% endblock %}

By using the same value for the block name in each template a hierarchy of inheritance can be created were the lowest child block statement values (first run against) will override those of the higher level child and parent templates. Due to the overriding nature of block statements an extra block (show_vpc_intf) is required to inherit blocks from a child template where overriding the values in higher child or parent is not desirable.

parent_template.j2: Block show_vpc_intf is empty so this value is only inherited

      vpc_peer_keepalive_status: peer-alive
      vpc_peer_status: peer-ok
{% block show_vpc %}
      peer-link_vlans: "1-2"
{% endblock %}
{% block show_vpc_intf %}
{% endblock %}

child1_template.j2: Block show_vpc is rendered and overrides the parent whilst show_vpc_intf is only inherited and passed down to the parent

{% extends "parent_template.j2" %}
{% block show_vpc %}
      peer-link_vlans: 1-2,10-13,20,24,30,40,110-112,120,210,220,3001-3002
{% endblock %}
{% block show_vpc_intf %}
{% endblock %}

child2_template.j2: Block show_vpc_intf is rendered and output passed down to child1 which in turn passes down to the parent template

{% extends "Child1_template.j2" %}
{% block show_vpc_intf %}
      Po26:
        consistency_status: SUCCESS
        port_status: "1"
        vpc_num: "26"
        active_vlans: "10,15,20,30,510,515,530"
{% endblock %}

This Ansible template module is run against the child2 template, the same principle would apply if rendering the template with Python.

template:
  src: "child2.j2"
  dest: "vpc_desired_state.yml"

The output saved to the vpc_desired_state.yml file is the result of the parent template adding vpc_peer_keepalive_status and vpc_peer_status, the child1 template replacing the parents peer-link_vlans and the child2 template adding the port-channel details.

      vpc_peer_keepalive_status: peer-alive
      vpc_peer_status: peer-ok
      peer-link_vlans: 1-2,10-13,20,24,30,40,110-112,120,210,220,3001-3002
      Po26:
        consistency_status: SUCCESS
        port_status: "1"
        vpc_num: "26"
        active_vlans: "10,15,20,30,510,515,530"

An easier way to do this without the extra show_vpc_intf block is to use super(). A super() block statement first renders the contents of the preceding block before merging (not overwriting) and returning the results of the proceeding template as well as current template. The below templates will produce the same output as the previous example.

parent_template.j2: Only has the one block statement, show_vpc

      vpc_peer_keepalive_status: peer-alive
      vpc_peer_status: peer-ok
{% block show_vpc %}
      peer-link_vlans: "1-2"
{% endblock %}

child1_template.j2: Renders peer-link_vlan which will override the block of the same name in the parent template

{% extends "parent_template.j2" %}
{% block show_vpc %}
      peer-link_vlans: 1-2,10-13,20,24,30,40,110-112,120,210,220,3001-3002
{% endblock %}

child2_template.j2: super() ensures that show_vpc is rendered and added to the rendered content of child1_template.j2 show_vpc block

{% extends "child1_template.j2" %}
{% block show_vpc_intf %}
{% super()%}
      Po26:
        consistency_status: SUCCESS
        port_status: "1"
        vpc_num: "26"
        active_vlans: "10,15,20,30,510,515,530"
{% endblock %}

Macros - using multiple blocks

Macros can be used in the destination template get round the limitation only using a block statement once.

{%- macro macro_get_bgp_neighbors() -%}
{% block get_bgp_neighbors %}{% endblock %}
{%- endmacro -%}

By putting that block statement in a macro it can then be referenced multiple times within the destination template wherever the block is needed.

- get_bgp_neighbors:
   global:
     router_id: {{ intf_lp[0].ip |ipaddr('address')  }}
     peers:
       _mode: strict
{% if bse.device_name.spine in inventory_hostname %}
{% for x in groups[bse.device_name.leaf.split('-')[-1].lower()] + groups[bse.device_name.border.split('-')[-1].lower()] %}
       {{ hostvars[x].intf_lp[0].ip |ipaddr('address') }}:
         is_enabled: true
         is_up: true
{{ macro_get_bgp_neighbors() }}
{% endfor %}{% else %}
{% for x in groups[bse.device_name.spine.split('-')[-1].lower()] %}
       {{ hostvars[x].intf_lp[0].ip |ipaddr('address') }}:
         is_enabled: true
         is_up: true
{{ macro_get_bgp_neighbors() }}
{% endfor %}{% endif %}
{% block get_bgp_neighbors_ipv4_global %}
{% endblock %}