Generate device configurations using Jinja2 templates, and use templates to manage existing devices.
Outcomes
Create Jinja2 templates from existing configuration backups by using variables and facts.
Create a playbook to apply the configuration templates on managed network nodes.
As the student user on the workstation machine, use the lab command to prepare your system for this exercise, and to ensure that all required resources are available.
This command also creates a project directory with the files needed for the exercise.
[student@workstation ~]$ lab start network-templates
Instructions
You want to use configuration backups as a template to deploy the same configuration across multiple managed network nodes, without the need to manually edit the configuration for each individual node. To do this, you need to inspect an existing configuration file and replace node-specific values with variables and facts.
Open the /home/student/network-templates directory in VS Code.
Examine the host variables and group variables for the managed Arista EOS nodes.
Open VS Code and click → .
Navigate to → and click .
If prompted, select , and then click .
Inspect the interfaces variable in the inventory/host_vars/arista1.lab.example.com/interfaces.yml file:
---
interfaces:
- name: Loopback100
address: 10.10.10.100/32
- name: Loopback101
address: 10.10.10.101/32Inspect the interfaces variable in the inventory/host_vars/arista2.lab.example.com/interfaces.yml file:
---
interfaces:
- name: Loopback100
address: 10.10.10.102/32
- name: Loopback101
address: 10.10.10.103/32Inspect the acls variable in the inventory/group_vars/all/acls.yml file:
---
acls:
- id: 10
host: 172.25.251.20
- id: 20
host: 172.25.251.21
- id: 30
host: 172.25.251.22
- id: 40
host: 172.25.251.23Inspect the eos_config.cfg configuration backup file and make a copy of this file called templates/eos_config.j2.
Convert the templates/eos_config.j2 file into a Jinja2 template by replacing the node-specific information with expressions using variables and facts.
Inspect the eos_config.cfg file.
Note all the instances of the hostname, interfaces, and access lists.
! Command: show running-config ! device: arista-host01 (vEOS-lab, EOS-4.30.1F) ! hostname arista-host01 ! interface Loopback100 description Loopback100 Interface ip address 10.0.30.2/32 ! interface Loopback101 description Loopback101 Interface ip address 10.0.31.2/32 ! ip access-list test 10 permit ip host 172.25.202.7 any log 20 permit ip host 172.25.210.7 any log !
Switch to the tab in VS Code, or change to the /home/student/network-templates directory in a GNOME terminal.
Use the cp command to create a copy of the eos_config.cfg configuration backup as the templates/eos_config.j2 template:
[student@workstation ~]$cd ~/network-templates[student@workstation network-templates]$cp -v eos_config.cfg \templates/eos_config.j2'eos_config.cfg' -> 'templates/eos_config.j2'
The hostname is different for every managed Arista EOS node.
Replace any instance of the hostname in the templates/eos_config.j2 template with the hostname variable:
...output omitted... ! device:{{ hostname }}(vEOS-lab, EOS-4.30.1F) ! hostname{{ hostname }}...output omitted...
Every managed Arista EOS node has a different IP address, and the interface names might not always be the same.
Use a for statement to replace any instances of interface names and associated IP addresses in the templates/eos_config.j2 template using the host variables for the Arista EOS hosts.
Include a comment to state the purpose of the for statement.
...output omitted... !{# loop through each interface #}{% for INTERFACE in interfaces %}interface{{ INTERFACE['name'] }}description{{ INTERFACE['name'] }}Interface ip address{{ INTERFACE['address'] }}!{% endfor %}...output omitted...
This Jinja2 template matches the format of the existing eos_config.cfg file where the interface description uses the name of the interface.
If the interfaces variable includes a description for each interface, then the description line might be written as description {{ INTERFACES['description'] }} or even description {{ INTERFACES['description'] | default(INTERFACES['name'] + ' Interface') }}.
In this example, the access lists are the same across all managed nodes.
Use a for statement to replace the Access Control Entries in the templates/eos_config.j2 template using the group variables for the all group.
Include a comment to state the purpose of the for statement.
...output omitted... ip access-list templates-project{# loop through each access list #}{% for ACL in acls %}{{ ACL['id'] }}permit ip host{{ ACL['host'] }}any log{% endfor %}! end
In this example all managed network nodes use the same access list configuration. The access lists could be hard coded into the template file in this instance, but it is still a good idea to use variables. Later in this exercise the same access list variables are used in a template to configure Cisco IOS devices. Using the group variables for the access lists prevents the need to edit hard-coded access lists across multiple template files in a multi-vendor environment.
The final templates/eos_config.j2 template must consist of the following content:
! Command: show running-config
! device: {{ hostname }} (vEOS-lab, EOS-4.30.1F)
!
hostname {{ hostname }}
!
{# loop through each interface #}
{% for INTERFACE in interfaces %}
interface {{ INTERFACE['name'] }}
description {{ INTERFACE['name'] }} Interface
ip address {{ INTERFACE['address'] }}
!
{% endfor %}
ip access-list templates-project
{# loop through each access list #}
{% for ACL in acls %}
{{ ACL['id'] }} permit ip host {{ ACL['host'] }} any log
{% endfor %}
!
endExamine the host variables for the managed Cisco IOS nodes.
Inspect the interfaces variable in the inventory/host_vars/iosxe1.lab.example.com/interfaces.yml file:
---
interfaces:
- name: Loopback100
address: 10.10.10.104
netmask: 255.255.255.255
- name: Loopback101
address: 10.10.10.105
netmask: 255.255.255.255Inspect the interfaces variable in the inventory/host_vars/iosxe2.lab.example.com/interfaces.yml file:
---
interfaces:
- name: Loopback100
address: 10.10.10.106
netmask: 255.255.255.255
- name: Loopback101
address: 10.10.10.107
netmask: 255.255.255.255Inspect the ios_config.cfg configuration backup file and make a copy of this file called templates/ios_config.j2.
Convert the templates/ios_config.j2 file into a Jinja2 template by replacing the node-specific information with expressions using variables and facts.
Inspect the ios_config.cfg file.
Note all the instances of the hostname, interfaces, and access lists.
hostname iosxe-host01 ! interface Loopback100 description Loopback100 Interface ip address 10.0.32.2 255.255.255.255 ! interface Loopback101 description Loopback101 Interface ip address 10.0.33.2 255.255.255.255 ! ip access-list extended templates-project 10 permit ip host 172.25.203.7 any log 20 permit ip host 172.25.211.7 any log ! end
Use the cp command to create a copy of the ios_config.cfg configuration backup as the templates/ios_config.j2 template:
[student@workstation network-templates]$cp -v ios_config.cfg \templates/ios_config.j2'ios_config.cfg' -> 'templates/ios_config.j2'
The hostname is different for every managed Cisco IOS node.
Replace any instances of the hostname in the templates/ios_config.j2 template with the hostname variable.
...output omitted...
hostname {{ hostname }}
...output omitted...Every managed Cisco IOS node has a different IP address, and the interface names might not always be the same.
Use a for statement to replace any instances of interface names and associated IP addresses in the templates/ios_config.j2 template using the host variables for the Cisco IOS hosts.
Include a comment to state the purpose of the for statement.
...output omitted... !{# loop through each interface #}{% for INTERFACE in interfaces %}interface{{ INTERFACE['name'] }}description{{ INTERFACE['name'] }}Interface ip address{{ INTERFACE['address'] }} {{ INTERFACE['netmask'] }}!{% endfor %}...output omitted...
In this example, the access lists are the same across all managed nodes.
Use a for statement to replace the Access Control Entries in the templates/ios_config.j2 template using the group variables for the all group.
Include a comment to state the purpose of the for statement.
...output omitted... ip access-list extended templates-project{# loop through each access list #}{% for ACL in acls %}{{ ACL['id'] }}permit ip host{{ ACL['host'] }}any log{% endfor %}! end
The final templates/ios_config.j2 template must consist of the following content:
hostname {{ hostname }}
!
{# loop through each interface #}
{% for INTERFACE in interfaces %}
interface {{ INTERFACE['name'] }}
description {{ INTERFACE['name'] }} Interface
ip address {{ INTERFACE['address'] }} {{ INTERFACE['netmask'] }}
!
{% endfor %}
ip access-list extended templates-project
{# loop through each access list #}
{% for ACL in acls %}
{{ ACL['id'] }} permit ip host {{ ACL['host'] }} any log
{% endfor %}
!
endCreate the generate_files.yml playbook to generate the configuration files from the templates.
Begin the playbook with a play that gathers facts on the eos and ios groups:
---
- name: Gather minimal facts
hosts:
- eos
- ios
gather_facts: true
tasks: []Create a second play that generates the configuration files from the templates on the workstation.lab.example.com host:
...output omitted...
- name: Generate configuration files from templates
hosts: workstation.lab.example.com
become: true
become_user: student
gather_facts: false
vars:
config_dir: "{{ playbook_dir }}/configs"
tasks:Create a task in the second play that creates the config directory in the network-templates project directory:
...output omitted...
- name: Create the '{{ config_dir }}' directory
ansible.builtin.file:
path: "{{ config_dir }}"
state: directoryCreate a second task in the second play that generates the configuration files for the managed Arista nodes:
...output omitted...
- name: Generate Arista configuration files
vars:
hostname: "{{ hostvars[item]['ansible_facts']['net_hostname'] }}"
interfaces: "{{ hostvars[item]['interfaces'] }}"
acls: "{{ hostvars[item]['acls'] }}"
ansible.builtin.template:
src: templates/eos_config.j2
dest: "{{ config_dir }}/{{ item }}.cfg"
loop: "{{ groups['eos'] }}"Create a third task in the second play that generates the configuration files for the managed Cisco nodes.
The completed generate_files.yml playbook must consist of the following content:
---
- name: Gather minimal facts
hosts:
- eos
- ios
gather_facts: true
tasks: []
- name: Generate configuration files from templates
hosts: workstation.lab.example.com
become: true
become_user: student
gather_facts: false
vars:
config_dir: "{{ playbook_dir }}/configs"
tasks:
- name: Create the '{{ config_dir }}' directory
ansible.builtin.file:
path: "{{ config_dir }}"
state: directory
- name: Generate Arista configuration files
vars:
hostname: "{{ hostvars[item]['ansible_facts']['net_hostname'] }}"
interfaces: "{{ hostvars[item]['interfaces'] }}"
acls: "{{ hostvars[item]['acls'] }}"
ansible.builtin.template:
src: templates/eos_config.j2
dest: "{{ config_dir }}/{{ item }}.cfg"
loop: "{{ groups['eos'] }}"
- name: Generate Cisco configuration files
vars:
hostname: "{{ hostvars[item]['ansible_facts']['net_hostname'] }}"
interfaces: "{{ hostvars[item]['interfaces'] }}"
acls: "{{ hostvars[item]['acls'] }}"
ansible.builtin.template:
src: templates/ios_config.j2
dest: "{{ config_dir }}/{{ item }}.cfg"
loop: "{{ groups['ios'] }}"Run the generate_files.yml playbook to generate the configuration files from the templates.
Use the ansible-navigator run command to run the generate_files.yml playbook:
[student@workstation network-templates]$ ansible-navigator run generate_files.yml
PLAY [Gather minimal facts] ******************************************************
TASK [Gathering Facts] ***********************************************************
ok: [iosxe2.lab.example.com]
ok: [iosxe1.lab.example.com]
ok: [arista1.lab.example.com]
ok: [arista2.lab.example.com]
PLAY [Generate configuration files from templates] *******************************
TASK [Create the '/home/student/Dev/network-templates/configs' directory] ********
changed: [workstation.lab.example.com]
TASK [Generate Arista configuration files] ***************************************
changed: [workstation.lab.example.com] => (item=arista1.lab.example.com)
changed: [workstation.lab.example.com] => (item=arista2.lab.example.com)
TASK [Generate Cisco configuration files] ****************************************
changed: [workstation.lab.example.com] => (item=iosxe1.lab.example.com)
changed: [workstation.lab.example.com] => (item=iosxe2.lab.example.com)
PLAY RECAP ***********************************************************************
arista1.lab.example.com : ok=1 changed=0 unreachable=0 failed=0 ...
arista2.lab.example.com : ok=1 changed=0 unreachable=0 failed=0 ...
iosxe1.lab.example.com : ok=1 changed=0 unreachable=0 failed=0 ...
iosxe2.lab.example.com : ok=1 changed=0 unreachable=0 failed=0 ...
workstation.lab.example.com : ok=3 changed=3 unreachable=0 failed=0 ...Examine the configs/arista1.lab.example.com.cfg file:
! Command: show running-config ! device: arista1.lab.example.com (vEOS-lab, EOS-4.30.1F) ! hostname arista1.lab.example.com ! interface Loopback100 description Loopback100 Interface ip address 10.10.10.100/32 ! interface Loopback101 description Loopback101 Interface ip address 10.10.10.101/32 ! ip access-list templates-project 10 permit ip host 172.25.251.20 any log 20 permit ip host 172.25.251.21 any log 30 permit ip host 172.25.251.22 any log 40 permit ip host 172.25.251.23 any log ! end
Examine the configs/iosxe1.lab.example.com.cfg file:
hostname iosxe1.lab.example.com ! interface Loopback100 description Loopback100 Interface ip address 10.10.10.104 255.255.255.255 ! interface Loopback101 description Loopback101 Interface ip address 10.10.10.105 255.255.255.255 ! ip access-list extended templates-project 10 permit ip host 172.25.251.20 any log 20 permit ip host 172.25.251.21 any log 30 permit ip host 172.25.251.22 any log 40 permit ip host 172.25.251.23 any log ! end
Create the deploy_config.yml playbook that uses the appropriate *_config modules to deploy the configuration files to the managed nodes.
Match the appropriate configuration file to the managed nodes with the correct corresponding network OS.
Create the deploy_config.yml playbook with the following content:
---
- name: Configure access lists
hosts:
- eos
- ios
gather_facts: false
tasks:
- name: Deploy Arista configuration
when: ansible_network_os == 'arista.eos.eos'
arista.eos.eos_config:
src: configs/{{ inventory_hostname }}.cfg
- name: Deploy Cisco configuration
when: ansible_network_os == 'cisco.ios.ios'
cisco.ios.ios_config:
src: configs/{{ inventory_hostname }}.cfgRun the deploy_config.yml playbook to configure the managed network nodes.
Use the ansible-navigator run command to run the deploy_config.yml playbook:
[student@workstation network-templates]$ ansible-navigator run deploy_config.yml
PLAY [Configure access lists] ****************************************************
TASK [Deploy Arista configuration] ***********************************************
skipping: [iosxe1.lab.example.com]
skipping: [iosxe2.lab.example.com]
[WARNING]: To ensure idempotency and correct diff the input configuration lines
should be similar to how they appear if present in the running configuration on
device including the indentation
changed: [arista2.lab.example.com]
changed: [arista1.lab.example.com]
TASK [Deploy Cisco configuration] ************************************************
skipping: [arista1.lab.example.com]
skipping: [arista2.lab.example.com]
changed: [iosxe1.lab.example.com]
changed: [iosxe2.lab.example.com]
PLAY RECAP ***********************************************************************
arista1.lab.example.com : ok=1 changed=1 unreachable=0 failed=0 ...
arista2.lab.example.com : ok=1 changed=1 unreachable=0 failed=0 ...
iosxe1.lab.example.com : ok=1 changed=1 unreachable=0 failed=0 ...
iosxe2.lab.example.com : ok=1 changed=1 unreachable=0 failed=0 ...Run the show_run.yml playbook to view the running configurations of the devices.
Examine the output of the playbook to verify the templates were applied.
Use the ansible-navigator run command to run the show_run.yml playbook.
Note that the output of the running configuration for each device reflects the hostname, interfaces, and acls variables used in the Jinja2 templates.
[student@workstation network-templates]$ansible-navigator run show_run.yml...output omitted... ok: [arista1.lab.example.com] => { ...output omitted... "! device: arista1.lab.example.com (vEOS-lab, EOS-4.30.1F)", ...output omitted... "hostname arista1.lab.example.com", ...output omitted... "interface Loopback100", "description Loopback100 Interface", "ip address 10.10.10.100/32", "!", "interface Loopback101", "description Loopback101 Interface", "ip address 10.10.10.101/32", ...output omitted... "ip access-list templates-project", "10 permit ip host 172.25.251.20 any log", "20 permit ip host 172.25.251.21 any log", "30 permit ip host 172.25.251.22 any log", "40 permit ip host 172.25.251.23 any log", ...output omitted... ok: [iosxe1.lab.example.com] => { ...output omitted... "hostname iosxe1.lab.example.com", ...output omitted... "interface Loopback100", "description Loopback100 Interface", "ip address 10.10.10.104 255.255.255.255", "!", "interface Loopback101", "description Loopback101 Interface", "ip address 10.10.10.105 255.255.255.255", ...output omitted... "ip access-list extended templates-project", "10 permit ip host 172.25.251.20 any log", "20 permit ip host 172.25.251.21 any log", "30 permit ip host 172.25.251.22 any log", "40 permit ip host 172.25.251.23 any log", ...output omitted...
Close the /home/student/network-templates directory in VS Code.
If you are using the GNOME terminal, return to the /home/student directory.
Click → in VS Code to close the /home/student/network-templates directory.
If you are using the GNOME terminal, run the cd command to return to the student home directory:
[student@workstation network-templates]$ cd