Bookmark this page

Customizing Data with Jinja2 Templates

Objectives

After completing this section, you should be able to generate configurations by running a playbook binding values to template slots.

Ansible Variables Use Jinja2

Ansible is written in Python, and uses the Jinja2 templating engine to implement variable substitution in playbooks, roles, and other Ansible YAML files. Variable substitution happens on the control node before tasks are sent and performed on the target machine.

---
- name: >
    this play gathers facts from device and
    shows inventory_hostname and configured hostname
  hosts:
    - router01
    - router02

tasks:
  - name: use ios_facts to gather facts from {{ inventory_hostname }}
    ios_facts:
      gather_subset: all

- name: show the configured hostname
  debug:
    msg: >
      the configured hostname of {{ inventory_hostname }}
      is {{ ansible_net_hostname }}

The Playbook as Jinja2 Interface

Ansible Playbooks can be used as a front-end interface to the Jinja2 template engine. You can take advantage of Jinja2's powerful template capabilities without writing a single line of Python code.

Modeling the network as an autogenerated set of network device configurations promotes consistency and standardization. Many changes can be implemented by updating variable values or adding new ones. This is separation of concerns in action: data elements are managed independently of vendor- and model- specific implementation details, which are encoded in templates.

Name Format Description
Variables file .ini or .yml/.yaml File containing individual data elements; group_vars/groupname or host_vars/hostname.yml, and so on.
Template file .j2 Template file containing Jinja2 parameters (variable names), vendor- and model- specific contextual elements, and other boilerplate text.
Ansible playbook .yml The controlling file, an Ansible playbook.

Configuring Devices from Templates

To configure network devices using templates:

  1. Separate the platform-independent infrastructure data from platform-dependent configuration statement syntax.

  2. Put the former into a vars file and the latter into a Jinja2 template. By convention, the directory locations for these are vars/ and j2/.

  3. Use the vars_files directive to load data as play variables.

  4. Tasks can use the src option with *os_config modules to feed global configuration statements to target devices directly from Jinja2 templates.

An example is provided to illustrate how this works.

Scope of Template Configuration

Feeding configuration statements from templates to devices works only with global config statements.

The *os_config modules for platforms with hierarchical interfaces have a parents option. It is used to apply config statements at the correct context of a command-line interface hierarchy. Parents only works with the lines option, which takes a list of config statements. The src option, which can be used with template files, is mutually exclusive with lines and parents.

$ ansible-doc ios_config
...output omitted...
- parents
    The ordered set of parents that uniquely identify the section or
    hierarchy the commands should be checked against. If the parents
    argument is omitted, the commands are checked against the set of top
    level or global commands.
...output omitted...
- src
    Specifies the source path to the file that contains the configuration
    or configuration template to load. The path to the source file can
    either be the full path on the Ansible control host or a relative path
    from the playbook or role root directory. This argument is
    mutually exclusive with 'lines', 'parents'.

Configuring from a Template

The configuration of static, local host names on IOS devices is illustrated here.

When a static host name exists on an IOS device, you can use the name instead of the IP address on that device wherever you would use an address: with the ping command, with ACLs, and so forth.

Note that these name-to-IP address mappings exist only on devices where they are configured. They are not part of the DNS service.

Static host names are configured with IOS CLI using this syntax:

(config)#ip host hostname ipv4_address

Identifying Data for Static Host Names

The data to support IOS static host names consists of a list of hash/dicts that map names to IPv4 addresses. Each row represents a host, and each row consists of a name field and an ipv4 field. The file is saved as vars/hostnames.yml.

hostname_data:
- { name: spine01, ipv4: 10.0.0.1/32 }
- { name: spine02, ipv4: 10.0.0.11/32 }
- { name: leaf01, ipv4: 192.168.0.1/32 }
- { name: leaf02, ipv4: 192.168.0.2/32 }
- { name: cs01, ipv4: 172.16.0.1/32 }
- { name: server01, ipv4: 10.10.10.2/32 }
- { name: server02, ipv4: 192.168.10.2/32 }
- { name: server03, ipv4: 172.16.10.2/32 }

It is often useful to store IPv4 information using CIDR notation. This makes it possible to extract various forms of information from a single unit of data: network address, subnet mask, machine address, and so forth. It is recommended practice to use that, as illustrated in the previous definition of the hostname_data variable.

Building a Template

The data has been defined as a sequence of hashes/dictionaries, named hostname_data. Given that data, the template used to generate IOS static host name commands is relatively simple. The file name is saved as j2/ios-static-hostnames.j2.

{% for host in hostname_data %}
ip host {{ host.name }} {{ host.ipv4 | ipaddr('address') }}
{% endfor %}

The Jinja2 for command is used to loop over individual members of the hostname_data sequence as a dict object named host. The host.name variable refers to the name field of the host object and host.ipv4 refers to its ipv4 field.

The ipaddr filter is used, together with the address argument, to obtain an IPv4 address from the original CIDR form of the data.

Viewing the Results

The IOS CLI command that displays static host names is show hosts. You can use an Ansible ad hoc command with the ios_command module to view the results.

$ ansible -m ios_command -a "commands='show hosts'"
cs01 | SUCCESS => {
    "changed": false,
    "stdout_lines": [
        [
            "Host                   Port  Flags     Age Type Address(es)",
            "spine01                None (perm, OK) 0 IP      10.0.0.1",
            "spine02                None (perm, OK) 0 IP      10.0.0.11",
            "leaf01                 None (perm, OK) 0 IP      192.168.0.1",
            "leaf02                 None (perm, OK) 0 IP      192.168.0.2",
            "cs01                   None (perm, OK) 0 IP      172.16.0.1",
            "server01               None (perm, OK) 0 IP      10.10.10.2",
            "server02               None (perm, OK) 0 IP      192.168.10.2",
            "server03               None (perm, OK) 0 IP      172.16.10.2"
        ]
    ]
}
Revision: do457-2.5-4693601