In this lab, you will explore the basics of using Ansible to automate backup and interface addressing tasks. First, you will configure Ansible in your DevNet virtual machine. Next, you will use Ansible to connect to the virtual router and back up its configuration. Then you will configure this virtual router with IPv6 addressing and discover that idempotence in automation comes at a significant cost.
After completing the manipulation steps in this document, you will be able to:
In this part, you will configure Ansible to run from a specific directory.
Ensure the ~/labs/lab15
directory exist and navigate to this folder
mkdir -p ~/labs/lab15 && cd ~/labs/lab15
Install ansible Python virtual environement
There are two main ways to set up a new Ansible workspace. Packages and Python virtual environments are both viable options. Here we choose to install Ansible in a Python virtual environment to take advantage of the latest release.
We start by creating a requirements.txt
file.
cat << EOF > requirements.txt
ansible
ansible-lint
ansible-pylibssh
netaddr
EOF
Then we install the tools in a virtual environment called ansible
.
touch .gitignore
if ! grep -q ansible .gitignore; then
echo ansible >>.gitignore
fi
python3 -m venv ansible
source ./ansible/bin/activate
pip3 install -r requirements.txt
Create a new ansible.cfg
file in the lab15
directory from the shell prompt
cat << 'EOF' > ansible.cfg
# config file for Lab 14 virtual router management
[defaults]
# Use inventory/ folder files as source
inventory=inventory/
host_key_checking = False # Don't worry about RSA Fingerprints
retry_files_enabled = False # Do not create them
deprecation_warnings = False # Do not show warnings
interpreter_python = /usr/bin/python3
[inventory]
enable_plugins = auto, host_list, yaml, ini, toml, script
[persistent_connection]
command_timeout=100
connect_timeout=100
connect_retry_timeout=100
ssh_type = libssh
EOF
Ensure that all necessary libraries and collections are up to date.
Update Cisco Ansible collection to the lastest version
ansible-galaxy collection install -f cisco.ios --upgrade
The local collections have priority over the system collection.
ansible-galaxy collection list cisco.ios
# /home/etu/.ansible/collections/ansible_collections
Collection Version
---------- -------
cisco.ios 10.1.0
Create the inventory
directory
mkdir ~/labs/lab15/inventory
The contents of this directory will later be supplemented by the virtual router’s connection parameters.
We start with a shell test connection before to set the configuration for ansible.
Be sure the virtual router is already up and running. If it’s not the case, refer to DevNet Lab 14 – Run the Cisco IOS-XE router VM
One more time, be sure to change placeholders to match your resource allocation.
Here is a sample declaration file for programming hypervisor switch ports:
ovs:
switches:
- name: dsw-host
ports:
- name: tapXX7 # management interface connection
type: OVSPort
vlan_mode: access
tag: VVV # out-of-band VLAN id
- name: tapXX8 # in-band interface connection
type: OVSPort
vlan_mode: trunk
trunks: [300, 301, 302]
- name: tapXX9 # unused interface in this lab
type: OVSPort
vlan_mode: access
tag: 999
From the hypervisor shell, apply this switch configuration with the following command:
switch-conf.py lab15-switch.yaml
Return to the DevNet development virtual machine shell, start an initial SSH connection to the virtual router, and accept the router’s fingerprint.
ssh etu@fe80::faad:caff:fefe:XXX%enp0s1
The authenticity of host 'fe80::faad:caff:fefe:XXX%enp0s1 (fe80::faad:caff:fefe:XXX%enp0s1)' can't be established.
RSA key fingerprint is SHA256:e+oyegX4BzzQSKNVz7vjoi8psHTxUjIw/rc/44Y8tHY.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'fe80::faad:caff:fefe:2%enp0s1' (RSA) to the list of known hosts.
(etu@fe80::faad:caff:fefe:XXX%enp0s1) Password:
rtrXXX#
This SSH connection uses the default user account created by the Zero Touch Programming (ZTP) Python script. This initial router configuration only occurs when the router management interface is connected to the out-of-band auto-addressing VLAN.
Create a new vault file called .lab_passwd.yml
and enter the unique vault password which will be used for all users passwords to be stored.
ansible-vault create $HOME/.lab_passwd.yml
New Vault password:
Confirm New Vault password:
This will open the default editor which is defined by the $EDITOR
environment variable.
There we enter a variable name which will designate the password for Web server VM user account.
ansible_user_passwd: 4n51bl3
Create the ansible_user account on the IOS XE system of the C8000v virtual router
From the default user account SSH connection already opened, add a new user account with the highest privilege level and the same password as the one chosen for the ansible vault.
rtrXXX#conf terminal
Enter configuration commands, one per line. End with CNTL/Z.
rtrXXX(config)#user ansible_user privilege 15 secret 4n51bl3
rtrXXX(config)#^Z
rtrXXX#copy run start
Destination filename [startup-config]?
Building configuration...
[OK]
Here is another copy of the user creation instructions. This version does not include the prompt or messages.
conf terminal
user ansible_user privilege 15 secret 4n51bl3
end
copy running-config startup-config
Close the default user SSH connection and open a new one with the identity ansible_user and list the IPv6 addresses of this router to fill the inventory file.
First, complete your SSH config file in order to avoid identity conflicts on Local Link IPv6 addresses
cat << EOF >> $HOME/.ssh/config
Host fe80::*
CheckHostIP no
StrictHostKeyChecking no
UserKnownHostsFile=/dev/null
EOF
Second, open the new SSH conection
ssh ansible_user@fe80::faad:caff:fefe:XXX%enp0s1
Warning: Permanently added 'fe80::faad:caff:fefe:2%enp0s1' (RSA) to the list of known hosts.
(ansible_user@fe80::faad:caff:fefe:2%enp0s1) Password:
rtrXXX#sh users
Line User Host(s) Idle Location
*434 vty 0 ansible_us idle 00:00:00 FE80::BAAD:CAFF:FEFE:0
Interface User Mode Idle Peer Address
rtrXXX#sh ipv6 int br GigabitEthernet 1
GigabitEthernet1 [up/up]
FE80::FAAD:CAFF:FEFE:XXX
2001:678:3FC:VVV:FAAD:CAFF:FEFE:XXX
In the screenshot above, we have a choice of 2 IPv6 addresses: the local link address or the GUA address.
It is good practice to have the DevNet VM and the out-of-band router interface on the same VLAN.
This is why we chose to use the Local Link IPv6 address in the inventory file.
Ansible uses an inventory file called hosts, which contains device information used by Ansible playbooks.
In this lab, you will be running Ansible from the git project’s lab directory. Therefore, you will need separate hosts and ansible.cfg files for each lab.
The terms “hosts file” and “inventory file” are synonymous and are used interchangeably throughout the Ansible labs.
The Ansible inventory file defines the devices and groups of devices used by the Ansible playbook. The file can be in one of many formats, including YAML and INI, depending on your Ansible environment.
The inventory file can list devices by IP address or fully qualified domain name (FQDN), and can also include host-specific parameters.
Create the hosts.yml
inventory file in the inventory
directory, add the following content to the file and save it. Any XXX
tags must be edited to identify your own C8000v virtual router instance.
cat << EOF > inventory/hosts.yml
ios:
hosts:
rtrXXX:
ansible_host: fe80::faad:caff:fefe:XXX%enp0s1
ansible_port: 2222
vars:
ansible_ssh_user: ansible_user
ansible_ssh_pass: "{{ ansible_user_passwd }}"
ansible_connection: network_cli
ansible_network_os: ios
all:
children:
ios:
EOF
The hosts.yml
file starts with the [ios] section. This section lists aliases for a set of devices. Here we have a single alias: rtrXXX. An host group is used in the Ansible playbook to refer to a device specified by the ansible_host and ansible_port variables.
Within the [ios] section, the hosts.yml
file specifies a set of variables that the Ansible playbook will use to access the device. These are the SSH credentials that Ansible needs to securely access the c8000v virtual router.
ansible --version
ansible [core 2.18.6]
config file = /home/etu/labs/lab15/ansible.cfg
configured module search path = ['/home/etu/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /home/etu/labs/lab15/ansible/lib/python3.13/site-packages/ansible
ansible collection location = /home/etu/.ansible/collections:/usr/share/ansible/collections
executable location = /home/etu/labs/lab15/ansible/bin/ansible
python version = 3.13.3 (main, Apr 8 2025, 19:55:40) [GCC 14.2.0] (/home/etu/labs/lab15/ansible/bin/python3)
jinja version = 3.1.6
libyaml = True
Parse your own ansible.cfg file.
Now, you need to edit your ansible.cfg
file to look at the location of your hosts.yml
inventory file.
Open the ansible.cfg
file and search lines referring to inventory.
grep -A2 inventory ansible.cfg
# Use inventory/ folder files as source
inventory=inventory/
host_key_checking = False # Don't worry about RSA Fingerprints
retry_files_enabled = False # Do not create them
--
[inventory]
enable_plugins = auto, host_list, yaml, ini, toml, script
[persistent_connection]
As in Python, the # is used for comments within the ansible.cfg file.
Comments cannot follow an entry. Ansible treats the # and subsequent comment as part of the filename. Therefore, in these cases, the # comment must be on a separate line.
However, variables can have a comment on the same line, as shown for host_key_checking and retry_files_enabled.
The ansible.cfg
file tells Ansible where to find the inventory file and sets certain default parameters. The information you put in your ansible.cfg
file is:
In this part you have configured Ansible to run in the lab directory.
In this lab you will need an ansible.cfg
file in your ansible
directory and an inventory file hosts.yml
in the inventory
directory.
In the next part, you will create a playbook to tell Ansible what to do.
The inventory status can be checked using the ansible-inventory command:
ansible-inventory --yaml --list
all:
children:
ios:
hosts:
rtrXXX:
ansible_connection: network_cli
ansible_host: fe80::faad:caff:fefe:XXX%enp0s1
ansible_network_os: ios
ansible_port: 2222
ansible_ssh_pass: "{{ ansible_user_passwd }}"
ansible_ssh_user: ansible_user
The connection to the router device can be checked using the Ansible ping module:
ansible -m ping all --ask-vault-pass --extra-vars '@$HOME/.lab_passwd.yml'
Vault password:
rtrXXX | SUCCESS => {
"changed": false,
"ping": "pong"
}
In this part, you will create an Ansible playbook that automates the process of backing up the router configuration. Playbooks are at the heart of Ansible. Whenever you want Ansible to retrieve information or perform an action on a device or group of devices, you run a playbook to get the job done.
An Ansible playbook is a YAML file containing one or more plays. Each play is a collection of tasks.
A playbook may also contain roles. A role is a mechanism for splitting a playbook into multiple components or files, simplifying the playbook and making it easier to reuse. For example, the common role is used to store tasks that can be used in all of your playbooks.
Roles are beyond the scope of this lab.
The Ansible YAML playbook includes objects, lists and modules.
The ansible-playbook command uses parameters to specify
The Ansible playbook is a YAML file. Make sure you use the correct YAML indentation. Every space and hyphen is important. You may lose some formatting if you copy and paste the code in this lab.
Create a new file in the ansible
directory with the following name: backup_router_playbook.yml
Add the following information to the file.
# The purpose of this playbook is to automatically back up the running
# configuration of a Cisco router.
---
- name: AUTOMATIC BACKUP OF ROUTER CONFIGURATION
hosts: ios
tasks:
- name: BACKUP RUNNING CONFIG
cisco.ios.ios_config:
backup: true
...
The playbook you have created contains one play with one task. The following is an explanation of your playbook:
This is at the beginning of every YAML file and tells YAML that this is a separate document. Each file can contain several documents, separated by ---.
This is the name of the play.
This is the alias previously configured in the hosts.yml
file. By referencing this alias in your playbook, the playbook can use any parameters associated with this inventory file entry, including the IP address of the devices.
This keyword specifies one or more tasks to perform.
The task is to backup the router configuration.
This is an Ansible module that is used to manage an IOS device configuration. The ios_config module belongs to the cisco.ios collection.
In the Linux terminal, you can use the ansible-doc module_name command to view the manual pages for any module and the parameters associated with that module. (e.g. ansible-doc cisco.ios.ios_command or ansible-doc cisco.ios.ios_config).
This parameter is associated with the ios_config module. It is used to list IOS commands in the playbook that are to send to the remote IOS device. The resulting command output is returned.
Now you can run the Ansible playbook using the ansible-playbook command:
ansible-playbook backup_router_playbook.yml --ask-vault-pass --extra-vars '@$HOME/.lab_passwd.yml'
Vault password:
PLAY [AUTOMATIC BACKUP OF ROUTER CONFIGURATION] ***************************************
TASK [Gathering Facts] ****************************************************************
ok: [rtrXXX]
TASK [BACKUP RUNNING CONFIG] **********************************************************
changed: [rtrXXX]
PLAY RECAP ****************************************************************************
rtrXXX : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The PLAY RECAP should display ok=2 changed=1 indicating a successful playbook execution.
If your Ansible playbook fails, check the following items in your playbook:
hosts.yml
and ansible.cfg
files are correct.If you continue to have problems, check the content of the inventory file content with the ansible-inventory command:
ansible-inventory --yaml --list
all:
children:
ios:
hosts:
rtrXXX:
ansible_connection: network_cli
ansible_host: fe80::faad:caff:fefe:XXX%enp0s1
ansible_network_os: ios
ansible_port: 2222
ansible_ssh_pass: "{{ ansible_user_passwd }}"
ansible_ssh_user: ansible_user
ansible-lint backup_router_playbook.yml
Passed: 0 failure(s), 0 warning(s) on 1 files. Last profile that met the validation criteria was 'production'.
The command results will show you the lines where there is a syntax error.
List the files in the backup folder and open the most recent one. You can also use the head
command to print the first few lines of the backup file. You now have a backup of the router configuration.
ls -A backup
rtrXXX_config.2025-06-11@15:01:26
head -n 20 backup/rtrXXX_config.2025-06-11@15\:01\:26
Building configuration...
Current configuration : 6841 bytes
!
! Last configuration change at 14:47:54 WEST Wed Jun 11 2025 by etu
! NVRAM config last updated at 14:51:27 WEST Wed Jun 11 2025 by etu
!
version 17.16
service timestamps debug datetime msec
service timestamps log datetime msec
platform qfp utilization monitor load 80
platform sslvpn use-pd
platform console serial
!
hostname rtrXXX
!
boot-start-marker
boot-end-marker
!
!
In this Part, you will create another Ansible playbook to configure IPv6 addressing on the C8000v router.
Create a subdirectory named host_vars
which will store variables of any device named in the inventory
mkdir host_vars
mkdir: created directory 'host_vars'
Create a new YAML file in the host_vars
directory with the device name: rtrXXX.yml
---
rtr_id: XXX
# The IPv6 addresses are calculated by concatenating the following elements:
# the global prefix, the VLAN ID, the router ID as the interface ID, and the mask.
# For instance, the IPv6 address for VLAN 100 on router 7 is 2001:678:3fc:64::XXX/64.
ipv6_prefix: "2001:678:3fc:"
ipv6_intf_id: "{{ '%x' % rtr_id }}"
ipv6_mask: 64
# The main parent interface is GigabitEthernet2.
# The subinterfaces are created by appending the VLAN ID to the main interface.
# For instance, the subinterface for VLAN 100 is GigabitEthernet2.100.
interface:
type: GigabitEthernet
id: 2
vlans:
- name: red
id: 300
desc: RED VLAN SUBINTERFACE
- name: purple
id: 301
desc: PURPLE VLAN SUBINTERFACE
- name: blue
id: 302
desc: BLUE VLAN SUBINTERFACE
Note that the keyword interface:
is singular. It should be plural. For simplicity, we decided to limit the scope of the playbook to a single interface.
Create a new file named router_config_playbook.yml
in your lab directory, and add the following tasks to the file. Make sure you use the proper YAML indentation. Every space and dash is significant.
# The purpose of this playbook is to configure IPv6 addresses on subinterfaces
# of a Cisco router.
#
# 1. Calculate IPv6 addresses for each VLAN
# 2. Display the calculated IPv6 addresses, networks, and gateways
# 3. Configure the main parent interface
# 4. Configure sub-interfaces with dot1Q encapsulation, IPv6 addresses, and additional settings
# 5. Save the output of the IPv6 interface brief
# 6. Test reachability to the gateway address of each VLAN
# 7. Save the results of the reachability test
---
- name: VLAN SUBINTERFACES CONFIGURATION
hosts: all
tasks:
- name: CALCULATE IPv6 ADDRESSES
ansible.builtin.set_fact:
calculated_vlans: >
{{
calculated_vlans | default([]) +
[
item | combine(
{
'addr': ipv6_prefix ~
('%x' % item.id) ~
'::' ~
ipv6_intf_id ~
'/' ~
ipv6_mask
}
)
]
}}
loop: "{{ interface.vlans }}"
loop_control:
label: "{{ item.name }}"
- name: VARS ANALYSIS
ansible.builtin.debug:
msg:
- "vlan: {{ item.id }} --> address: {{ item.addr }}"
- "Network: {{ item.addr | ansible.utils.ipaddr('network') }}/{{ item.addr | ansible.utils.ipaddr('prefix') }}"
- "Gateway: {{ item.addr | ansible.utils.ipaddr('1') }}"
loop: "{{ calculated_vlans }}"
- name: MAIN PARENT INTERFACE CONFIG
cisco.ios.ios_interfaces:
config:
- name: "{{ interface.type }}{{ interface.id }}"
description: IPV6 ANSIBLE PLAYBOOK CONFIGURATION
enabled: true
- name: SUB INTERFACES CONFIG
cisco.ios.ios_config:
lines:
- description {{ item.desc }}
- encapsulation dot1Q {{ item.id }}
- ipv6 address {{ item.addr }}
- ipv6 enable
- ipv6 nd ra suppress all
parents:
- interface {{ interface.type }}{{ interface.id }}.{{ item.id }}
match: exact
replace: block
with_items: "{{ calculated_vlans }}"
- name: SHOW IPv6 INTERFACE BRIEF
cisco.ios.ios_command:
commands:
- show ipv6 interface brief
register: output
- name: ENSURE TRACE DIRECTORY EXISTS
delegate_to: localhost
ansible.builtin.file:
path: trace
state: directory
mode: "0755"
- name: SAVE OUTPUT
ansible.builtin.copy:
content: "{{ output.stdout[0] }}"
dest: "trace/ipv6_int_brief_{{ inventory_hostname }}.txt"
mode: "0644"
- name: REACHABILITY TEST
cisco.ios.ios_ping:
dest: "{{ item.addr | ansible.utils.ipaddr('1') | ansible.utils.ipaddr('address') }}"
afi: ipv6
loop: "{{ calculated_vlans }}"
register: ping_results
- name: SAVE PING RESULTS
delegate_to: localhost
ansible.builtin.copy:
content: "{{ ping_results.results | to_nice_json }}"
dest: "trace/ipv6_ping_{{ inventory_hostname }}.txt"
mode: "0644"
The main purpose of this playbook is to illustrate variable formatting and calculations. A set of VLAN identifiers is defined from the contents of the host_vars/rtrXXX.yml
file. Then IPv6 subinterface addresses are calculated using a combination of:
Note that we only need to change the router and VLAN identifiers to have all the IPv6 addresses automatically calculated.
Now, as we look at the contents of the router_config_playbook.yml
file and what it does, here is a brief description of the elements used:
ansible-doc cisco.ios.ios_config
command to see the details for the parents and match parameters used in this playbook.trace
directory in the DevNet lab tree if necessary.trace
directory on the Devnet VM.Now you can run the Ansible playbook with the ansible-playbook
command. The -vvv verbose option can be added to display the tasks being performed in the playbook.
Do not forget to replace the rtr_id value in the host_vars/rtrXXX.yaml
with your own out of band tap interface number!.
This is mandatory in order to avoid duplicate addresses on different virtual routers.
ansible-playbook router_config_playbook.yml --ask-vault-pass --extra-vars '@$HOME/.lab_passwd.yml'
Vault password:
PLAY [VLAN SUBINTERFACES CONFIGURATION] ******************************************
TASK [Gathering Facts] ***********************************************************
ok: [rtrXXX]
TASK [CALCULATE IPv6 ADDRESSES] **************************************************
ok: [rtrXXX] => (item=red)
ok: [rtrXXX] => (item=purple)
ok: [rtrXXX] => (item=blue)
TASK [VARS ANALYSIS] *************************************************************
ok: [rtrXXX] => (item={'name': 'red', 'id': 300, 'desc': 'RED VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12c::XXX/64'}) => {
"msg": [
"vlan: 300 --> address: 2001:678:3fc:12c::XXX/64",
"Network: 2001:678:3fc:12c::/64",
"Gateway: 2001:678:3fc:12c::1/64"
]
}
ok: [rtrXXX] => (item={'name': 'purple', 'id': 301, 'desc': 'PURPLE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12d::XXX/64'}) => {
"msg": [
"vlan: 301 --> address: 2001:678:3fc:12d::XXX/64",
"Network: 2001:678:3fc:12d::/64",
"Gateway: 2001:678:3fc:12d::1/64"
]
}
ok: [rtrXXX] => (item={'name': 'blue', 'id': 302, 'desc': 'BLUE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12e::XXX/64'}) => {
"msg": [
"vlan: 302 --> address: 2001:678:3fc:12e::XXX/64",
"Network: 2001:678:3fc:12e::/64",
"Gateway: 2001:678:3fc:12e::1/64"
]
}
TASK [MAIN PARENT INTERFACE CONFIG] **********************************************
ok: [rtrXXX]
TASK [SUB INTERFACES CONFIG] *****************************************************
changed: [rtrXXX] => (item={'name': 'red', 'id': 300, 'desc': 'RED VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12c::XXX/64'})
changed: [rtrXXX] => (item={'name': 'purple', 'id': 301, 'desc': 'PURPLE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12d::XXX/64'})
changed: [rtrXXX] => (item={'name': 'blue', 'id': 302, 'desc': 'BLUE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12e::XXX/64'})
[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
TASK [SHOW IPv6 INTERFACE BRIEF] *************************************************
ok: [rtrXXX]
TASK [ENSURE TRACE DIRECTORY EXISTS] *********************************************
ok: [rtrXXX -> localhost]
TASK [SAVE OUTPUT] ***************************************************************
ok: [rtrXXX]
TASK [REACHABILITY TEST] *********************************************************
ok: [rtrXXX] => (item={'name': 'red', 'id': 300, 'desc': 'RED VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12c::XXX/64'})
ok: [rtrXXX] => (item={'name': 'purple', 'id': 301, 'desc': 'PURPLE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12d::XXX/64'})
ok: [rtrXXX] => (item={'name': 'blue', 'id': 302, 'desc': 'BLUE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12e::XXX/64'})
TASK [SAVE PING RESULTS] *********************************************************
changed: [rtrXXX -> localhost]
PLAY RECAP ***********************************************************************
rtrXXX : ok=10 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
All of these playbook tasks have been successfully completed. The ok
counter has reached 10, which means that all 10 tasks have been completed.
The changed
counter is 2, meaning 2 changes have been made. If we look for the changed keyword in the playbook run screenshot above, we can identify these configuration updates.
host_vars
file.You can view the contents of the trace file with cat trace/ipv6_int_brief_Router.txt
. You now have a trace of the virtual router’s interface and subinterface configuration.
cat trace/ipv6_int_brief_rtrXXX.txt
GigabitEthernet1 [up/up]
FE80::FAAD:CAFF:FEFE:XXX
2001:678:3FC:34:FAAD:CAFF:FEFE:XXX
GigabitEthernet2 [up/up]
unassigned
GigabitEthernet2.300 [up/up]
FE80::FAAD:CAFF:FEFE:YYY
2001:678:3FC:12C::XXX
GigabitEthernet2.301 [up/up]
FE80::FAAD:CAFF:FEFE:YYY
2001:678:3FC:12D::XXX
GigabitEthernet2.302 [up/up]
FE80::FAAD:CAFF:FEFE:YYY
2001:678:3FC:12E::XXX
GigabitEthernet3 [administratively down/down]
unassigned
And here comes the devil!
When we try to run the playbook again, we notice that the changed counter is always 2. This shouldn’t be the case because the VLAN subinterfaces already have IPv6 addresses set.
Applying not-so-new addresses each time the playbook runs can cause communications to break.
[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
When we dig deeper into the IOS configuration lines, no matter how hard we try, we always get the warning. So, the indemptency claim is broken.
In our router_config_playbook.yml
playbook, we chose to use the cisco.ios.ios_config, which sends lines of IOS XE instructions in configuration mode. In addition, the replace: block
statement should ensure an idempotent configuration by replacing the entire specified configuration block for each sub-interface rather than attempting to merge individual lines. This should ensure that the interface configuration matches exactly what’s defined in the playbook.
- name: SUB INTERFACES CONFIG
cisco.ios.ios_config:
lines:
- description {{ item.desc }}
- encapsulation dot1Q {{ item.id }}
- ipv6 address {{ item.addr }}
- ipv6 enable
- ipv6 nd ra suppress all
parents:
- interface {{ interface.type }}{{ interface.id }}.{{ item.id }}
match: exact
replace: block
with_items: "{{ calculated_vlans }}"
The problem we’re encountering is symptomatic of using the syntax of a network device configuration language like IOS, which was not designed for automation in the first place.
There are two possible solutions to overcome this:
Since using APIs for our virtual router configuration is beyond the scope of this lab, we choose the latter.
Here is a new block of 3 tasks, replacing the previous single SUB INTERFACES CONFIG
task, each using a specific module to achieve idempotent processing.
- name: CONFIGURE IPv6 ADDRESSES ON SUB-INTERFACES
block:
- name: CREATE SUB-INTERFACES
cisco.ios.ios_interfaces:
config:
- name: "{{ interface.type }}{{ interface.id }}.{{ item.id }}"
description: "{{ item.desc }}"
enabled: true
state: merged
loop: "{{ calculated_vlans }}"
- name: CONFIGURE DOT1Q ENCAPSULATION AND ADDITIONAL IPv6 SETTINGS
cisco.ios.ios_config:
lines:
- encapsulation dot1Q {{ item.id }}
- ipv6 enable
- ipv6 nd ra suppress all
parents: "interface {{ interface.type }}{{ interface.id }}.{{ item.id }}"
loop: "{{ calculated_vlans }}"
- name: CONFIGURE IPv6 ADDRESSES ON SUB-INTERFACES
cisco.ios.ios_l3_interfaces:
config:
- name: "{{ interface.type }}{{ interface.id }}.{{ item.id }}"
ipv6:
- address: "{{ item.addr | ansible.utils.ipaddr('address') }}/{{ item.addr | ansible.utils.ipaddr('prefix') }}"
state: merged
loop: "{{ calculated_vlans }}"
rescue:
- name: Log configuration failure
ansible.builtin.debug:
msg: Failed to configure IPv6 on sub-interfaces. Check interface status and configuration.
- name: Notify on failure
ansible.builtin.fail:
msg: IPv6 interface configuration failed. See previous messages for details.
As we can see, the cisco.ios.ios_config
module is a last-resort solution when no other module in the collection provides the required configuration options.
Start by replacing the orignal SUB INTERFACES CONFIG
task with the suggested block of three subtasks. Then run the playbook again and look at the changed
counter value.
Here is an excerpt of the playbook run focusing on the three subinterfaces configuration processing.
PLAY [VLAN SUBINTERFACES CONFIGURATION] ***************************************************
TASK [Gathering Facts] ********************************************************************
ok: [rtrXXX]
TASK [CALCULATE IPv6 ADDRESSES] ***********************************************************
ok: [rtrXXX] => (item=red)
ok: [rtrXXX] => (item=purple)
ok: [rtrXXX] => (item=blue)
TASK [VARS ANALYSIS] **********************************************************************
ok: [rtrXXX] => (item={'name': 'red', 'id': 300, 'desc': 'RED VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12c::XXX/64'}) => {
"msg": [
"vlan: 300 --> address: 2001:678:3fc:12c::XXX/64",
"Network: 2001:678:3fc:12c::/64",
"Gateway: 2001:678:3fc:12c::1/64"
]
}
ok: [rtrXXX] => (item={'name': 'purple', 'id': 301, 'desc': 'PURPLE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12d::XXX/64'}) => {
"msg": [
"vlan: 301 --> address: 2001:678:3fc:12d::XXX/64",
"Network: 2001:678:3fc:12d::/64",
"Gateway: 2001:678:3fc:12d::1/64"
]
}
ok: [rtrXXX] => (item={'name': 'blue', 'id': 302, 'desc': 'BLUE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12e::XXX/64'}) => {
"msg": [
"vlan: 302 --> address: 2001:678:3fc:12e::XXX/64",
"Network: 2001:678:3fc:12e::/64",
"Gateway: 2001:678:3fc:12e::1/64"
]
}
TASK [MAIN PARENT INTERFACE CONFIG] *******************************************************
ok: [rtrXXX]
TASK [CREATE SUB-INTERFACES] **************************************************************
ok: [rtrXXX] => (item={'name': 'red', 'id': 300, 'desc': 'RED VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12c::XXX/64'})
ok: [rtrXXX] => (item={'name': 'purple', 'id': 301, 'desc': 'PURPLE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12d::XXX/64'})
ok: [rtrXXX] => (item={'name': 'blue', 'id': 302, 'desc': 'BLUE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12e::XXX/64'})
TASK [CONFIGURE DOT1Q ENCAPSULATION AND ADDITIONAL IPv6 SETTINGS] *************************
ok: [rtrXXX] => (item={'name': 'red', 'id': 300, 'desc': 'RED VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12c::XXX/64'})
ok: [rtrXXX] => (item={'name': 'purple', 'id': 301, 'desc': 'PURPLE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12d::XXX/64'})
ok: [rtrXXX] => (item={'name': 'blue', 'id': 302, 'desc': 'BLUE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12e::XXX/64'})
TASK [CONFIGURE IPv6 ADDRESSES ON SUB-INTERFACES] *****************************************
ok: [rtrXXX] => (item={'name': 'red', 'id': 300, 'desc': 'RED VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12c::XXX/64'})
ok: [rtrXXX] => (item={'name': 'purple', 'id': 301, 'desc': 'PURPLE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12d::XXX/64'})
ok: [rtrXXX] => (item={'name': 'blue', 'id': 302, 'desc': 'BLUE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12e::XXX/64'})
TASK [SHOW IPv6 INTERFACE BRIEF] **********************************************************
ok: [rtrXXX]
TASK [ENSURE TRACE DIRECTORY EXISTS] ******************************************************
ok: [rtrXXX -> localhost]
TASK [SAVE OUTPUT] ************************************************************************
ok: [rtrXXX]
TASK [REACHABILITY TEST] ******************************************************************
ok: [rtrXXX] => (item={'name': 'red', 'id': 300, 'desc': 'RED VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12c::XXX/64'})
ok: [rtrXXX] => (item={'name': 'purple', 'id': 301, 'desc': 'PURPLE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12d::XXX/64'})
ok: [rtrXXX] => (item={'name': 'blue', 'id': 302, 'desc': 'BLUE VLAN SUBINTERFACE', 'addr': '2001:678:3fc:12e::XXX/64'})
TASK [SAVE PING RESULTS] ******************************************************************
changed: [rtrXXX -> localhost]
PLAY RECAP ********************************************************************************
rtrXXX : ok=12 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
We win! There is no warning message or change indicator when network interfaces have their IPv6 addresses already configured.
If we look at the playbook recap, the changed
counter value is 1 because we saved the ping results as new artifacts.
PLAY RECAP ***********************************************************************
rtrXXX : ok=12 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
We can now conclude this part by acknowledging that indempotency in Ansible playbooks comes at a cost. When designing tasks, we need to think in terms of fine-grained tuning.
This lab provides a practical introduction to network automation using Ansible, with a focus on both functionality and idempotency. Through hands-on exercises, you configured Ansible, backed up router configurations, and automated IPv6 addressing on router interfaces.
By creating and refining playbooks, you experienced the process of developing efficient and idempotent automation scripts. The lab highlighted the challenges of achieving true idempotency when working with network device configuration languages, and demonstrated how to overcome these challenges by breaking down tasks and using specialized Ansible modules.
You learned that while Ansible claims to provide idempotent operations, achieving this in practice often requires careful consideration and sometimes refactoring of playbook tasks. The lab showed how to analyze playbook execution, identify non-idempotent behavior, and implement solutions to ensure that repeated playbook runs produce consistent and predictable results.
This experience serves as a foundation for more advanced network automation projects, emphasizing the importance of not just automating tasks, but doing so in a way that is reliable, repeatable, and efficient. The skills acquired in this lab will be valuable as you continue to explore and implement network automation strategies in more complex environments.