Network Automation Cookbook

4.7 (3 reviews total)
By Karim Okasha
    Advance your knowledge in tech with a Packt subscription

  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Managing Cisco IOS Devices Using Ansible

About this book

Network Automation Cookbook is designed to help system administrators, network engineers, and infrastructure automation engineers to centrally manage switches, routers, and other devices in their organization's network. This book will help you gain hands-on experience in automating enterprise networks and take you through core network automation techniques using the latest version of Ansible and Python.

With the help of practical recipes, you'll learn how to build a network infrastructure that can be easily managed and updated as it scales through a large number of devices. You'll also cover topics related to security automation and get to grips with essential techniques to maintain network robustness. As you make progress, the book will show you how to automate networks on public cloud providers such as AWS, Google Cloud Platform, and Azure. Finally, you will get up and running with Ansible 2.9 and discover troubleshooting techniques and network automation best practices.

By the end of this book, you'll be able to use Ansible to automate modern network devices and integrate third-party tools such as NAPALM, NetBox, and Batfish easily to build robust network automation solutions.

Publication date:
April 2020
Publisher
Packt
Pages
482
ISBN
9781789956481

 

Managing Cisco IOS Devices Using Ansible

In this chapter, we will outline how to automate Cisco IOS-based devices using Ansible. We will explore the different modules available in Ansible to automate configuration and collect network information from Cisco IOS devices. This chapter will be based on the following sample network diagram, and we will walk through how we can implement this network design using Ansible:

The following table outlines the management IP addresses on the Cisco nodes, which Ansible will use to connect to the devices:

Device

Role

Vendor

MGMT Port

MGMT IP

access01

Access switch

Cisco IOS 15.1

Ethernet0/0

172.20.1.18

access02

Access switch

Cisco IOS 15.1

Ethernet0/0

172.20.1.19

core01

Core switch

Cisco IOS 15.1

Ethernet0/0

172.20.1.20

core02

Core switch

Cisco IOS 15.1

Ethernet0/0

172.20.1.21

wan01

WAN router

Cisco IOS–XE 16.6.1

GigabitEthernet1

172.20.1.22

wan02

WAN router

Cisco IOS–XE 16.6.1

GigabitEthernet1

172.20.1.23

The main recipes covered in this chapter are as follows:

  • Building an Ansible network inventory
  • Connecting to Cisco IOS devices
  • Configuring basic system information
  • Configuring interfaces on IOS devices
  • Configuring L2 VLANS on IOS devices
  • Configuring trunk and access interfaces
  • Configuring interface IP addresses
  • Configuring OSPF on IOS devices
  • Collecting IOS device facts
  • Validating network reachability on IOS devices
  • Retrieving operational data from IOS devices
  • Validating network states with pyATS and Ansible
 

Technical requirements

 

Building an Ansible network inventory

In this recipe, we will outline how to build and structure the Ansible inventory to describe the network setup outlined in the previous section.

Getting ready

Make sure that Ansible is already installed on the control machine.

How to do it...

  1. Create a new directory with the following name: ch2_ios.
  2. Inside this new folder, create the hosts file with the following content:
$ cat hosts
[access]
access01 Ansible_host=172.20.1.18
access02 Ansible_host=172.20.1.19

[core]
core01 Ansible_host=172.20.1.20
core02 Ansible_host=172.20.1.21

[wan]
wan01 Ansible_host=172.20.1.22
wan02 Ansible_host=172.20.1.23

[lan:children]
access
core

[network:children]
lan
wan
  1. Create the Ansible.cfg file with the following content:
$ cat Ansible.cfg

[defaults]
inventory=hosts
retry_files_enabled=False
gathering=explicit

How it works...

We built the Ansible inventory using the hosts file, and we defined multiple groups in order to group the different devices in our topology in the following manner:

  • We created the access group, which has both access switches (access01 and access02) in our topology.
  • We created the core group, which groups all core switches that will act as the L3 termination for all the VLANs on the access switches.
  • We created the wan group, which groups all our Cisco IOS–XE routes, which will act as our wan routers.
  • We created another group called lan, which groups both access and core groups.
  • We created the network group, which groups both lan and wan groups.

Finally, we created the Ansible.cfg file and configured it to point to our hosts file to be used as an Ansible inventory file. We disabled the setup module, which is not required when running Ansible against network nodes.

 

Connecting to Cisco IOS devices

In this recipe, we will outline how to connect to Cisco IOS devices from Ansible via SSH in order to start managing devices from Ansible.

Getting ready

In order to follow along with this recipe, an Ansible inventory file should be constructed as per the previous recipe. IP reachability between the Ansible control machine and all the devices in the network must be configured.

How to do it...

  1. Inside the ch2_ios directory, create the groups_vars folder.
  2. Inside the group_vars folder, create the network.yml file with the following content:
$cat network.yml
Ansible_network_os: ios
Ansible_connection: network_cli
Ansible_user: lab
Ansible_password: lab123
Ansible_become: yes
Ansible_become_password: admin123
Ansible_become_method: enable
  1. On all IOS devices, ensure that the following is configured to set up SSH access:
!
hostname <device_hostname>
!
ip domain name <domain_name>
!
username lab secret 5 <password_for_lab_user>.
!
enable secret 5 <enable_password>.
!
line vty 0 4
login local
transport input SSH
!
  1. Generate SSH keys on the Cisco IOS devices from the config mode, as shown in the following code snippet:
(config)#crypto key generate rsa
Choose the size of the key modulus in the range of 360 to 4096 for your
General Purpose Keys. Choosing a key modulus greater than 512 may take
a few minutes.
How many bits in the modulus [512]: 2048
% Generating 2048 bit RSA keys, keys will be non-exportable...
[OK] (elapsed time was 0 seconds)
  1. Update the Ansible.cfg file with the following highlighted parameters:
$ cat Ansible.cfg
[defaults]
host_key_checking=False

How it works...

In our sample network, we will use SSH to set up the connection between Ansible and our Cisco devices. In this setup, Ansible will use SSH in order to establish the connection to our Cisco devices with a view to start managing it. We will use username/password authentication in order to authenticate our Ansible control node with our Cisco devices.

On the Cisco devices, we must ensure that SSH keys are present in order to have a functional SSH server on the Cisco devices. The following code snippet outlines the status of the SSH server on the Cisco device prior to generating the SSH keys:

wan01#show ip SSH
SSH Disabled - version 2.0
%Please create RSA keys to enable SSH (and of atleast 768 bits for SSH v2).
Authentication methods:publickey,keyboard-interactive,password
Authentication Publickey Algorithms:x509v3-SSH-rsa,SSH-rsa
Hostkey Algorithms:x509v3-SSH-rsa,SSH-rsa
Encryption Algorithms:aes128-ctr,aes192-ctr,aes256-ctr
MAC Algorithms:hmac-sha2-256,hmac-sha2-512,hmac-sha1,hmac-sha1-96
KEX Algorithms:diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1
Authentication timeout: 120 secs; Authentication retries: 3
Minimum expected Diffie Hellman key size : 2048 bits
IOS Keys in SECSH format(SSH-rsa, base64 encoded): NONE

Once we create the SSH keys, the SSH server on the Cisco device is operational, and is ready to accept an SSH connection from the Ansible control node.

On the Ansible machine, we include all the variables required to establish the SSH connection to the managed devices in the network.yml file. As per our inventory file, the network group includes all the devices within our topology, and so all the attributes that we configure in this file will apply to all the devices in our inventory. The following is a breakdown of the attributes that we included in the file:

  • Ansible_connection: This establishes how Ansible connects to the device. In this scenario, we set it to network_cli to indicate that we will use SSH to connect to a network device.
  • Ansible_network_os: When using network_cli as the connection plugin to connect to the network device, we must indicate which network OS Ansible will be connecting to, so as to use the correct SSH parameters with the devices. In this scenario, we will set it to ios, since all the devices in our topology are IOS-based devices.
  • Ansible_user: This parameter specifies the username that Ansible will use to establish the SSH session with the network device.
  • Ansible_password: This parameter specifies the password that Ansible will use to establish the SSH session with the network device.
  • Ansible_become: This instructs Ansible to use the enable command to enter privileged mode when configuring or executing show commands on the managed device. We set this to yes in our context, since we will require privileged mode to configure the devices.
  • Ansible_become_password: This specifies the enable password to use in order to enter privileged mode on the managed IOS device.
  • Ansible_become_method: This option specifies the method to use in order to enter privileged mode. In our scenario, this is the enable command on IOS devices.
In this recipe, I have defined the SSH password and the enable passwords as plain text just for simplicity; however, this is highly discouraged. We should use Ansible-vault to secure the passwords, as outlined in the Ansible Vault recipe in the previous chapter.

On the Cisco devices, we set up the required username and password so that Ansible can open an SSH connection to the managed Cisco IOS devices. We also configure an enable password to be able to enter privileged mode, and to make configuration changes. Once we apply all of these configurations to the devices, we are ready to set up Ansible.

In any SSH connection, when an SSH client (Ansible control node in our case) connects to an SSH server (Cisco devices in our case), the server sends a copy of its public key to the client before the client logs in. This is used to establish the secure channel between the client and the server, and to authenticate the server to the client in order to prevent any man-in-the-middle attacks. So, at the start of a new SSH session involving a new device, we see the following prompt:

$SSH [email protected]
The authenticity of host '172.20.1.18 (172.20.1.18)' can't be established.
RSA key fingerprint is SHA256:KnWOalnENZfPokYYdIG3Ogm9HDnXIwjh/it3cqdiRRQ.
RSA key fingerprint is MD5:af:18:4b:4e:84:19:a6:8d:82:17:51:d5:ee:eb:16:8d.
Are you sure you want to continue connecting (yes/no)?

When the SSH client initiates the SSH connection to the client, the SSH server sends its public key to the client in order to authenticate itself to the client. The client searches for the public key in its local known hosts files (in the ~/.SSH/known_hosts or /etc/SSH/SSH_known_hosts files). In the event that it does not find the public key for this machine in its local known hosts file, it will prompt the user to add this new key to its local database, and this is the prompt that we see when we initiate the SSH connection.

In order to simplify the SSH connection setup between the Ansible control node and its remotely managed hosts, we can disable this host checking. We can do this by telling Ansible to ignore host keys and not to add them to the known hosts files by setting host_key_checking to False in the Ansible.cfg configuration file.

Disabling host key checking is not a best practice, and we are only showing it as it is a lab setup. In the next section, we will outline an alternative method to establish the SSH connection between Ansible and its remote managed devices.

There's more...

If we need to verify the identity of the SSH hosts that we will connect to, and thereby enable host_key_checking, we can automate the addition of the SSH public key of the remote managed hosts to the ~/.SSH/known_hosts file using Ansible. We create a new Ansible playbook that will run on the Ansible control machine to connect to the remote devices using the ssk-keyscan command. We then collect the SSH public keys for the remote machines and add them to the ~/.SSH/known_hosts file. The method is outlined here:

  1. Create a new playbook pb_gather_SSH_keys.yml file and add the following play:
- name: "Play2: Record Keys in Known Hosts file"
hosts: localhost
vars:
- hosts_file: "~/.SSH/known_hosts"
tasks:
- name: create know hosts file
file:
path: "{{ hosts_file }}"
state: file
changed_when: false
  1. Update the playbook and add another play within the same playbook to save and store the SSH public keys for the remote managed nodes:
- name: "Play2: Record Keys in Known Hosts file"
hosts: localhost
vars:
- hosts_file: "~/.SSH/known_hosts"
tasks:
- name: create know hosts file
file:
path: "{{ hosts_file }}"
state: file
changed_when: false
- name: Populate the known_hosts file
blockinfile:
block: |
{% for host in groups['all'] if hostvars[host].SSH_keys.stdout != ''
%}
{{ hostvars[host].SSH_keys.stdout}}
{% endfor %}
path: "{{ hosts_file }}"
create: yes

In our new playbook, we have a play that targets all our managed devices by setting the hosts parameter to all. In this play, we have a single task, which we run on the Ansible control node (using the delegate_to localhost) to issue the SSH-keyscan command, which returns the SSH public key for the remote device, as shown in the following code:

$ SSH-keyscan 172.20.1.22

# 172.20.1.22:22 SSH-2.0-Cisco-1.25
172.20.1.22 SSH-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTwrH4phzRnW/RsC8eXMh/accIErRfkgffDWBGSdEX0r9EwAa6p2uFMWj8dq6kvrREuhqpgFyMoWmpgdx5Cr+10kEonr8So5yHhOhqG1SJO9RyzAb93H0P0ro5DXFK8A/Ww+m++avyZ9dShuWGxKj9CDM6dxFLg9ZU/9vlzkwtyKF/+mdWNGoSiCbcBg7LrOgZ7Id7oxnhEhkrVIa+IxxGa5Pwc73eR45Uf7QyYZXPC0RTOm6aH2f9+8oj+vQMsAzXmeudpRgAu151qUH3nEG9HIgUxwhvmi4MaTC+psmsGg2x26PKTOeX9eLs4RHquVS3nySwv4arqVzDqWf6aruJ
In this task, we are using delegate_to as being equal to localhost, as Ansible will try to connect to the remote devices and issue the command on the remote device by default. In our case, this is not what we need; we need to issue this command from the Ansible control node. So, we use delegate_to as being equal to localhost in order to enforce this behavior.

We run the second play on the Ansible control host by setting hosts to localhost, and we execute tasks to create the known hosts file (if not already present) and to populate this file with the data that we captured in the first play using the SSH_keys variable. We run this playbook on the Ansible control machine to store the SSH keys from the remotely managed nodes prior to running any of our playbooks.

 

Configuring basic system information

In this recipe, we will outline how we can configure basic system parameters on Cisco IOS devices, such as setting the hostname, DNS server, and NTP servers. Following the network setup that we outlined at the start of this chapter, we will configure the following information on all the Cisco IOS devices:

  • DNS servers 172.20.1.250 and 172.20.1.251
  • NTP server 172.20.1.17

Getting ready

An Ansible inventory file must be present, as well as the configuration for Ansible to connect to the Cisco IOS devices via SSH.

How to do it...

  1. To the group_vars/network.yml file, add the following system parameters:
$ cat group_vars/network.yml
<-- Output Trimmed for brevity ------>
name_servers:
- 172.20.1.250
- 172.20.1.251
ntp_server: 172.20.1.17
  1. Create a new playbook called pb_build_network.yml with the following information:
$ cat pb_build_network.yml
---
- name: "PLAY 1: Configure All Lan Switches"
hosts: lan
tags: lan
tasks:
- name: "Configure Hostname and Domain Name"
ios_system:
hostname: "{{ inventory_hostname }}"
domain_name: "{{ domain_name }}"
lookup_enabled: no
name_servers: "{{ name_servers }}"
- name: "Configure NTP"
ios_ntp:
server: "{{ ntp_server }}"
logging: true
state: present

How it works...

In the network.yml file, we define the name_servers variable as a list of DNS servers, and we also define the ntp_servers variable, which defines the NTP servers that we want to configure on the IOS devices. Defining these parameters in the network.yml file applies these variables to all the devices within the network group.

We create a playbook and the first play targets all the hosts in the lan group (this includes both access and core devices) and, within this play, we reference two tasks:

  • ios_system: This sets the hostname and the DNS servers on the devices.
  • ios_ntp: This configures the NTP on the IOS devices and enables logging for NTP events.

Both these modules are declarative Ansible modules in which we just identify the state pertaining to our infrastructure. Ansible converts this declaration into the necessary IOS commands. The modules retrieve the configuration of the devices and compare the current state with our intended state (to have DNS and NTP configured on them) and then, if the current state does not correspond to the intended state defined by these modules, Ansible will apply the needed configuration to the devices.

When we run these tasks on all the LAN devices, the following configuration is pushed to the devices:

!
ip name-server 172.20.1.250 172.20.1.251
no ip domain lookup
ip domain name lab.net
!
ntp logging
ntp server 172.20.1.17
!

See also...

 

Configuring interfaces on IOS devices

In this recipe, we will outline how to configure the basic interface properties on Cisco IOS-based devices, such as setting the interface description, the interface maximum transmission unit (MTU), and enabling interfaces. We will configure all the links within our topology as having a link MTU of 1,500 and to be fully duplex.

Getting ready

To follow along with this recipe, an Ansible inventory is assumed to be already set up, as is IP reachability between the Ansible control node with the Cisco devices in place.

How to do it...

  1. In the group_vars/network.yml file, add the following content to define the generic interface parameters:
$ cat group_vars/network.yml
<-- Output Trimmed for brevity ------>
intf_duplex: full
intf_mtu: 1500
  1. Create a new file, lan.yml, under the group_vars folder, with the following data to define the interfaces on our Cisco devices:
$ cat group_vars/lan.yaml

interfaces:
core01:
- name: Ethernet0/1
description: access01_e0/1
mode: trunk
- name: Ethernet0/2
description: access02_e0/1
mode: trunk
- name: Ethernet0/3
description: core01_e0/3
mode: trunk
<-- Output Trimmed for brevity ------>
access01:
- name: Ethernet0/1
description: core01_e0/1
mode: trunk
- name: Ethernet0/2
description: core02_e0/1
mode: trunk
- name: Ethernet0/3
description: Data_vlan
mode: access
vlan: 10
  1. Update the pb_build_network.yml playbook file with the following task to set up the interfaces:
    - name: "P1T3: Configure Interfaces"
ios_interface:
name: "{{ item.name }}"
description: "{{ item.description }}"
duplex: "{{ intf_duplex }}"
mtu: "{{ intf_mtu }}"
state: up
loop: "{{ interfaces[inventory_hostname] }}"
register: ios_intf

How it works...

In this recipe, we outline how to configure the physical interfaces on IOS devices. We first declare the generic parameters (interface duplex and MTU) that apply to all the interfaces. These parameters are defined under the network.yml file. Next, we define all the interface-specific parameters for all our LAN devices under the lan.yml file to be applied to all devices. All these parameters are declared in the interfaces dictionary data structure.

We update our playbook with a new task to configure the physical parameters for all of our LAN devices in our network. We use the ios_interface module to provision all the interface parameters, and we loop over all the interfaces in each node using the interfaces data structure. We set the state to up in order to indicate that the interface should be present and operational.

See also...

 

Configuring L2 VLANs on IOS devices

In this recipe, we will outline how to configure L2 VLANs on Cisco IOS devices, as per the network topology discussed in the introduction to this chapter. We will outline how to declare VLANs as Ansible variables, and how to use suitable Ansible modules to provision these VLANs on the network.

Getting ready

We will be building on the previous recipes discussed in this chapter to continue to configure the L2 VLANs on all the LAN devices within our sample topology.

How to do it...

  1. Update the group_vars/lan.yml file with the VLAN definition, as outlined in the following code:
$ cat group_vars/lan.yaml

vlans:
- name: Data
vlan_id: 10
- name: Voice
vlan_id: 20
- name: Web
vlan_id: 100
  1. Update the pb_build.yml playbook with the following task to provision the VLANs:
  - name: "P1T4: Create L2 VLANs"
ios_vlan:
vlan_id: "{{ item.vlan_id }}"
name: "{{ item.name }}"
loop: "{{ vlans }}"
tags: vlan

How it works...

In the group_vars/lan.yml file, we define a vlans list data structure that holds the VLAN definition that we need to apply to all our core and access switches. This variable will be available for all the core and access switches, and Ansible will use this variable in order to provision the required VLANs on the remote devices.

We use another declarative module, ios_vlan, which takes the VLAN definition (its name and the VLAN ID) and configures these VLANs on the remote managed device. It pulls the existing configuration from the device and compares it with the list of devices that need to be present, while only pushing the delta.

We use the loop construct to go through all the items in the vlans list, and configure all the respective VLANs on all the devices.

After running this task on the devices, the following is the output from one of the access switches:

access01#sh vlan
VLAN Name Status Ports
---- -------------------------------- --------- -------------------------------
1 default active Et1/0, Et1/1, Et1/2, Et1/3
10 Data active Et0/3
20 Voice active
100 Web active
 

Configuring trunk and access interfaces

In this recipe, we will show how to configure access and trunk interfaces on Cisco IOS-based devices, and how to map interfaces to an access VLAN, as well as how to allow specific VLANs on the trunks.

Getting ready

Following our sample topology, we will configure the interfaces on the devices. As shown in this table, we are only showing the VLANs for access01 and core01— the other devices are exact replicas:

Device

Interface

Mode

VLANs

Core01

Ethernet0/1

Trunk

10,20,100

Core01

Ethernet0/2

Trunk

10,20,100

Core01

Ethernet0/3

Trunk

10,20,100,200

Access01

Ethernet0/1

Trunk

10,20,100

Access01

Ethernet0/2

Trunk

10,20,100

Access01

Ethernet0/3

Access

10

How to do it...

  1. Create a new core.yml file under group_vars and include the following core_vlans definition:
core_vlans:
- name: l3_core_vlan
vlan_id: 200
interface: Ethernet0/3
  1. Update the pb_build_network.yml playbook with the following tasks to configure all trunk ports:
  - name: "Configure L2 Trunks"
ios_l2_interface:
name: "{{ item.name }}"
mode: "{{ item.mode }}"
trunk_allowed_vlans: "{{ vlans | map(attribute='vlan_id') | join(',') }}"
state: present
loop: "{{ interfaces[inventory_hostname] |
selectattr('mode','equalto','trunk') | list }}"
- name: "Enable dot1q Trunks"
ios_config:
lines:
- switchport trunk encapsulation dot1q
parents: interface {{item.name}}
loop: "{{ interfaces[inventory_hostname] |
selectattr('mode','equalto','trunk') | list }}"
tags: dot1q
  1. Update the playbook with the following task to configure all access ports:
  - name: "Configure Access Ports"
ios_l2_interface:
name: "{{ item.name }}"
mode: "{{ item.mode}}"
access_vlan: "{{ item.vlan }}"
state: present
loop: "{{ interfaces[inventory_hostname] |
selectattr('mode','equalto','access') | list }}"

How it works...

We are using the same data structure in the lan.yml file that defines all the interfaces within the LAN network and describes their type (access/trunk). In the case of access ports, we define which access interface is part of which VLAN. We will reference this list data structure to configure the access and trunk ports on all the devices within the lan group.
The interfaces within our layer2 network are one of the following two options:

Access:

  • We use ios_l2_interface with the access_vlan parameter to configure the correct access VLAN on the interface.
  • We select only the access interfaces for each device using the selectattr jinja2 filter, and we match only one interface with a mode equal to access, and we loop over this list for each device.

Trunk:

  • We use ios_l2_interface with the trunk_allowed_vlans parameter to add all the VLANs to the trunk ports, on both access and core switches.
  • We create the permitted VLAN list using the Jinja2 map and join filters and we apply this filter to the vlans list data structure. This outputs a string similar to the following: 10,20,100.
  • We select only the trunk ports using the selectattr Jinja2 filter from the interface's data structure per node.
  • We need to configure these trunks as dot1q ports; however, this attribute is still not enabled on ios_l2_interface. Hence, we use another module, ios_config, to send the required Cisco IOS command to set up the dot1q trunks.

The following output outlines the configuration applied to the access01 device as an example for both access and trunk ports:

!
interface Ethernet0/3 >> Access Port
description Data_vlan
switchport access vlan 10
switchport mode access

!
interface Ethernet0/1 >> Trunk Port
description core01_e0/1
switchport trunk encapsulation dot1q
switchport trunk allowed vlan 10,20,100
switchport mode trunk

See also...

 

Configuring interface IP addresses

In this recipe, we will explore how to configure the interface IP address on Cisco IOS devices. We will use the sample topology to configure the VLAN interfaces on both the core switches. We will outline how to configure VRRP between the core switches for all the VLAN interfaces. We will configure the following IP addresses:

Interface

Prefix

VRRP IP address

VLAN10

10.1.10.0/24

10.1.10.254

VLAN20

10.1.20.0/24

10.1.20.254

VLAN100

10.1.100.0/24

10.1.100.254

Getting ready

This recipe assumes that the interface and VLANs are configured as per the previous recipes in this chapter.

How to do it...

  1. Update the group_vars/core.yml file with following data to define the SVI interfaces:
$ cat group_vars/core.yml
<-- Output Trimmed for brevity ------>
svi_interfaces:
- name: Vlan10
ipv4: 10.1.10.0/24
vrrp: yes
ospf: passive
- name: Vlan20
ipv4: 10.1.20.0/24
vrrp: yes
ospf: passive
- name: Vlan100
ipv4: 10.1.100.0/24
vrrp: yes
ospf: passive
  1. Create core01.yml and core02.yml files under the host_vars folder and add the following content:
$ cat host_vars/core01.yml
hst_svi_id: 1
hst_vrrp_priority: 100
$ cat host_vars/core02.yml
hst_svi_id: 2
hst_vrrp_priority: 50
  1. Update the pb_build_network.yml playbook with the following tasks to create and enable the L3 SVI interfaces:
- name: "PLAY 2: Configure Core Switches"
hosts: core
tags: l3_core
tasks:
<-- Output Trimmed for brevity ------>
- name: "Create L3 VLAN Interfaces"
ios_l3_interface:
name: "{{item.name }}"
ipv4: "{{item.ipv4 | ipv4(hst_svi_id)}}"
loop: "{{svi_interfaces}}"
tags: l3_svi
- name: "Enable the VLAN Interfaces"
ios_interface:
name: "{{ item.name }}"
state: up
loop: "{{ svi_interfaces }}"
  1. Update the playbook with the following task to set up VRRP configuration on the SVI interfaces:
    - name: "Create VRRP Configs"
ios_config:
parents: interface {{ item.name }}
lines:
- vrrp {{item.name.split('Vlan')[1]}} priority {{ hst_vrrp_priority }}
- vrrp {{item.name.split('Vlan')[1]}} ip {{item.ipv4 | ipv4(254)|ipaddr('address')}}
loop: "{{svi_interfaces | selectattr('vrrp','equalto',true) | list }}"

How it works...

In this section, we are configuring the IP addresses for the L3 VLAN interfaces on the core switches, as well as configuring VRRP on all the L3 VLAN interfaces to provide L3 redundancy.

We are using a new list data structure called svi_interfaces, which describes all the SVI interfaces with L3 IP addresses, and also some added parameters to control both the VRRP and OSPF configured on these interfaces. We also set up two new variables on each core router, hst_svi_id and hst_vrrp_priority, which we will use in the playbook to control the IP address on each core switch, as well as the VRPP priority.

We use the ios_l3_interface Ansible module to set the IPv4 addresses on the VLAN interfaces. On each core switch, we loop over the svi_interfaces data structure, and for each VLAN we configure the IPv4 address on the corresponding VLAN interface. We determine which IP address is configured on each router using the Ansible ipaddr filter, along with the hst_svi_id parameter {{item.ipv4 | ipv4(hst_svi_id)}} . So, for example, for VLAN10, we will assign 10.1.10.1/24 for core01 and 10.1.10.2/24 for core02.


When first creating the VLAN interface on Cisco IOS devices, they are in a state of shutdown, so we need to enable them. We use the ios_interface module to enable the interfaces.

For the VRRP part, we return to using the ios_config module to set up the VRRP configuration on all the VLAN interfaces, and we use hst_vrrp_priority to correctly set up core01 as the master VRRP for all VLANs.

The following is a sample of the configuration that is pushed on the devices after running the playbook:

Core01
========
!
interface Vlan10
ip address 10.1.10.1 255.255.255.0
vrrp 10 ip 10.1.10.254
!
Core02
=======
!
interface Vlan10
ip address 10.1.10.2 255.255.255.0
vrrp 10 ip 10.1.10.254
vrrp 10 priority 50

See also...

 

Configuring OSPF on IOS devices

In this recipe, we will outline how to configure OSPF on Cisco IOS devices with Ansible. Using our sample network topology, we will set up OSPF between core switches and WAN routers, as well as advertising the SVI interface via OSPF.

Getting ready

This recipe assumes that all the interfaces are already configured with the correct IP addresses and are following the same procedures outlined in previous recipes.

How to do it...

  1. Update the group_vars/core.yml file with the following data to define core links between core switches and WAN routers:
core_l3_links:
core01:
- name: Ethernet1/0
description: wan01_Gi2
ipv4: 10.3.1.0/30
ospf: yes
ospf_metric: 100
peer: wan01
core02:
- name: Ethernet1/0
description: wan02_Gi2
ipv4: 10.3.1.4/30
ospf: yes
ospf_metric: 200
peer: wan02
  1. Update the pb_build_network.yml playbook with the following tasks to set up OSPF:
- name: "PLAY 2: Configure Core Switches"
hosts: core
tags: l3_core
tasks:
< -------- Snippet -------- >
- name: "P2T9: Configure OSPF On Interfaces"
ios_config:
parents: interface {{ item.name }}
lines:
- ip ospf {{ ospf_process }} area {{ ospf_area }}
- ip ospf network point-to-point
- ip ospf cost {{item.ospf_metric | default(ospf_metric)}}
loop: "{{ (svi_interfaces + core_l3_links[inventory_hostname]) | selectattr('ospf') | list }}"
- name: "P2T10: Configure OSPF Passive Interfaces"
ios_config:
parents: router ospf {{ ospf_process }}
lines: passive-interface {{item.name}}
loop: "{{ (svi_interfaces + core_l3_links[inventory_hostname]) | selectattr('ospf','equalto','passive') | list }}"

How it works...

We created another dictionary data structure in the core.yml file that describes the L3 links between the core switches and the WAN routers. We specified whether they will run OSPF and what the OSPF metric is on these links.

Currently, Ansible doesn't provide a declarative module to manage OSPF configuration on IOS-based devices. Therefore, we need to push the required configuration using the ios_config module. We created two separate tasks using ios_config in order to push the OSPF-related configuration on each device. In the first task, we configured the interface-related parameters under each interface, and we looped over both the svi_interface and core_l3_interfaces data structures to enable OSPF on all the OSPF-enabled interfaces. We used the Jinja2 selectattr filter to select all the interfaces that have the OSPF attribute set to yes/true.

In the last task, we applied the passive interface configuration to all the interfaces that have the passive flag enabled on them. We used the Jinja2 selectattr filter to select only those interfaces with the passive parameter set to yes/true.

 

Collecting IOS device facts

In this recipe, we will outline how to collect device facts from Cisco devices with Ansible. This information includes the serial number, IOS version, and all the interfaces on the devices. Ansible executes several commands on managed IOS devices in order to collect this information.

Getting ready

The Ansible controller must have IP connectivity with the managed network devices, and SSH must be enabled on the IOS devices.

How to do it...

  1. Create a new playbook called pb_collect_facts.yml in the same ch2_ios folder with the following information:
---
- name: "PLAY 1: Collect Device Facts"
hosts: core,wan
tasks:
- name: "P1T1: Gather Device Facts"
ios_facts:
register: device_facts
- debug: var=device_facts

How it works...

We run this new playbook against all nodes within the core and wan group, and we use the ios_facts module to collect the information from the managed IOS devices. In this recipe, we use the debug module to print out the information that was collected from the ios_facts module. The following is a subset of the information that was discovered:

ok: [core01 -> localhost] => {
"Ansible_facts": {
"net_all_ipv4_addresses": [
"172.20.1.20",
< ---------- Snippet ------------ >
"10.1.100.1"
],
"net_hostname": "core01",
"net_interfaces": {
< ---------- Snippet ------------ >
"Vlan10": {
"bandwidth": 1000000,
"description": null,
"duplex": null,
"ipv4": [
{
"address": "10.1.10.1",
"subnet": "24"
}
],
"lineprotocol": "up",
"macaddress": "aabb.cc80.e000",
"mediatype": null,
"mtu": 1500,
"operstatus": "up",
"type": "Ethernet SVI"
},

},
"net_iostype": "IOS",
"net_serialnum": "67109088",
"net_system": "ios",
"net_version": "15.1",
}
< ------------ Snippet ------------ >
}

From the preceding output, we can see some of the main facts that the ios_facts module has captured from the devices, including the following:

  • net_all_ipv4_addresses: This list data structure contains all the IPv4 addresses that are configured on all the interfaces on the IOS device.
  • net_interfaces: This dictionary data structure captures the status of all of the interfaces on this device and their operational state, as well as other important information, such as a description and their operational state.
  • net_serialnum: This captures the serial number of the device.
  • net_version: This captures the IOS version running on this device.

There's more...

Using the information that is collected from the ios_facts module, we can generate structured reports for the current state of the network and use these reports in further tasks. In this section, we will outline how to modify our playbook to build this report.

Add a new task to the pb_collect_facts.yml playbook, as shown in the following code:

- name: "P1T2: Write Device Facts"
blockinfile:
path: ./facts.yml
create: yes
block: |
device_facts:
{% for host in play_hosts %}
{% set node = hostvars[host] %}
{{ node.Ansible_net_hostname }}:
serial_number: {{ node.Ansible_net_serialnum }}
ios_version: {{ node.Ansible_net_version }}
{% endfor %}
all_loopbacks:
{% for host in play_hosts %}
{% set node = hostvars[host] %}
{% if node.Ansible_net_interfaces is defined %}
{% if node.Ansible_net_interfaces.Loopback0 is defined %}
- {{ node.Ansible_net_interfaces.Loopback0.ipv4[0].address }}
{% endif %}
{% endif %}
{% endfor %}
run_once: yes
delegate_to: localhost

We use the blockinfile module to build a YAML file called facts.yml. We use Jinja2 expressions within the blockinfile module to customize and select the information we want to capture from the Ansible facts that were captured from the ios_facts task. When we run the pb_collect_facts.yml playbook, we generate the facts.yml file, which has the following data:

device_facts:
wan01:
serial_number: 90L4XVVPL7V
ios_version: 16.06.01
wan02:
serial_number: 9UOFOO7FH19
ios_version: 16.06.01
core01:
serial_number: 67109088
ios_version: 15.1
core02:
serial_number: 67109104
ios_version: 15.1
all_loopbacks:
- 10.100.1.3
- 10.100.1.4
- 10.100.1.1
- 10.100.1.2

See also...

 

Validating network reachability on IOS devices

In this recipe, we will outline how to validate network reachability via ping using Ansible. ICMP allows us to validate proper forwarding across our network. Using Ansible to perform this task provides us with a robust tool to validate proper traffic forwarding, since we can perform this task from each node simultaneously and collect all the results for further inspection.

Getting ready

This recipe is built based on the network setup that was outlined in the chapter introduction, and I am assuming that the network has already been built in accordance with all the previous recipes in this chapter.

How to do it...

  1. Create a new playbook called pb_net_validate.yml and add the following task to store all SVI IP addresses:
---
- name: "PLay 1: Validate Network Reachability"
hosts: core,wan
vars:
host_id: 10
packet_count: 10
tasks:
- name: "Get all SVI Prefixes"
set_fact:
all_svi_prefixes: "{{ svi_interfaces | selectattr('vrrp') |
map(attribute='ipv4') | list }}"
run_once: yes
delegate_to: localhost
tags: svi
  1. Update the pb_net_validate.yml playbook with the following task to ping all the SVI interfaces:
      - name: "Ping Hosts in all VLANs"
ios_ping:
dest: "{{ item | ipaddr(10) | ipaddr('address') }}"
loop: "{{ all_svi_prefixes }}"
ignore_errors: yes
tags: svi

How it works...

In this playbook, we are using the ios_ping module, which logs into each node defined in our Ansible inventory, and pings the destination specified by the dest attribute. In this sample playbook, we would like to validate network reachability to a single host within the data, voice, and web VLANs, and we choose the tenth host in all these VLANs (just as an example). In order to build all the VLAN prefixes we set in the first task, we add a new variable called all_svi_prefixes and use multiple jinja2 filters to collect only those prefixes that are running VRRP (so as to remove any core VLANs). We get only the IPv4 attributes for these SVI interfaces. The following are the contents of this new variable after running the first task:

ok: [core01 -> localhost] => {
"all_svi_prefixes": [
"10.1.10.0/24",
"10.1.20.0/24",
"10.1.100.0/24"
]
}

We supply this new list data structure to the ios_ping module and we specify that we need to ping the tenth host within each subnet. As long as the ping succeeds, the task will succeed. However, if there is a connectivity problem from the router/switch to this host, the task will fail. We are using the ignore_errors parameter in order to ignore any failure that might occur owing to the fact that the host is unreachable/down, and to run any subsequent tasks. The following code snippet outlines the successful run:

TASK [P1T2: Ping Hosts in all VLANs] *****************************
ok: [core01] => (item=10.1.10.0/24)
ok: [core02] => (item=10.1.10.0/24)
ok: [wan01] => (item=10.1.10.0/24)
ok: [wan02] => (item=10.1.10.0/24)
ok: [core01] => (item=10.1.20.0/24)
ok: [core02] => (item=10.1.20.0/24)
ok: [core01] => (item=10.1.100.0/24)
ok: [wan01] => (item=10.1.20.0/24)
ok: [wan02] => (item=10.1.20.0/24)
ok: [core02] => (item=10.1.100.0/24)
ok: [wan01] => (item=10.1.100.0/24)
ok: [wan02] => (item=10.1.100.0/24)
 

Retrieving operational data from IOS devices

In this recipe, we will outline how to execute operational commands on IOS devices and store these outputs to text files for further processing. This allows us to capture any operational commands from IOS devices during pre- or post-validation after we perform any deployment so that we can compare the results.

Getting ready

In order to follow along with this recipe, an Ansible inventory file should be in place and the network should already be set up as per the previous recipes.

How to do it...

  1. Create a new playbook called pb_op_cmds.yml and populate it with the following tasks to create the directory structure to save the output from the devices:
---
- name: "Play 1: Execute Operational Commands"
hosts: network
vars:
config_folder: "configs"
op_folder: "op_data"
op_cmds:
- show ip ospf neighbor
- show ip route
tasks:
- name: "P1T1: Build Directories to Store Data"
block:
- name: "Create folder to store Device config"
file:
path: "{{ config_folder }}"
state: directory
- name: "Create Folder to store operational commands"
file:
path: "{{ op_folder }}"
state: directory
run_once: yes
delegate_to: localhost
  1. Update the pb_op_cmds.yml playbook and populate it with the following tasks to retrieve the running configuration from the devices:
      - name: "P1T2: Get Running configs from Devices"
ios_command:
commands: show running-config
register: show_run
- name: "P1T3: Save Running Config per Device"
copy:
content: "{{ show_run.stdout[0] }}"
dest: "{{ config_folder }}/{{ inventory_hostname }}.cfg"
  1. Update the playbook and populate it with the following tasks to retrieve the operational commands from the devices and save it:
      - name: "P1T4: Create Folder per Device"
file:
path: "{{ op_folder}}/{{ inventory_hostname }}"
state: directory
delegate_to: localhost
- name: "P1T5: Get Operational Data from Devices"
ios_command:
commands: "{{ item }}"
register: op_output
loop: "{{ op_cmds }}"
- name: "P1T6: Save output per each node"
copy:
content: "{{ item.stdout[0] }}"
dest: "{{ op_folder}}/{{ inventory_hostname }}/{{item.item | replace(' ', '_')}}.txt"
loop: "{{ op_output.results }}"

How it works...

In this recipe, we are using the ios_command module in order to execute operational commands on the IOS devices, and saving them to text files. In order to achieve this goal, we perform the following steps:

  • We create the folders that we will store the output to, and we create a folder called configs to store the running config of all the devices. We also create an op_data file to store the output of the operational commands that we will get from the devices.
  • We then execute the show running command on all the IOS devices in our inventory and we register the output in a new variable called show_run.
  • We use the copy module to save the output from the previous task to a file for each device. The output from the command run is saved in the stdout variable. As we executed a single command, the stdout variable only has a single item (stdout[0]).

Once we execute this task, we can see that the configs folder is populated as shown in the following output:

$ tree configs/
configs/
├── access01.cfg
├── access02.cfg
├── core01.cfg
├── core02.cfg
├── isp01.cfg
├── wan01.cfg
└── wan02.cfg

For the next part, we create a folder for each node to store the output from the multiple show commands that we will execute on the IOS devices.

We use the ios_command module to execute the show commands on the devices, and save all the output in a new variable called op_output. We use the copy execute command, show ip route, and we create a file for the output of this command with the name show_ip_route.txt.

After running this task, we can see that this is the current structure of the op_data folder:

$ tree op_data/
op_data/
├── access01
│ ├── show_ip_ospf_neighbor.txt
│ └── show_ip_route.txt
├── access02
│ ├── show_ip_ospf_neighbor.txt
│ └── show_ip_route.txt
├── core01
│ ├── show_ip_ospf_neighbor.txt
│ └── show_ip_route.txt
├── core02
│ ├── show_ip_ospf_neighbor.txt
│ └── show_ip_route.txt
├── isp01
│ ├── show_ip_ospf_neighbor.txt
│ └── show_ip_route.txt
├── wan01
│ ├── show_ip_ospf_neighbor.txt
│ └── show_ip_route.txt
└── wan02
├── show_ip_ospf_neighbor.txt
└── show_ip_route.txt

We can check the content of one of the files to confirm that all the data has been stored:

$ head op_data/core01/show_ip_ospf_neighbor.txt

Neighbor ID Pri State Dead Time Address Interface
10.100.1.3 0 FULL/ - 00:00:37 10.3.1.2 Ethernet1/0
10.100.1.2 0 FULL/ - 00:00:36 10.1.200.2 Vlan200
 

Validating network states with pyATS and Ansible

In this recipe, we will outline how to use Ansible and the Cisco pyATS Python library to execute and parse operational commands on Cisco devices. Using these parsed commands, we can validate various aspects of the network.

Getting ready

This recipe assumes that the network has already been built and configured as outlined in all the previous recipes.

How to do it...

  1. Install the Python libraries needed for pyATS:
$ sudo pip3 install pyats genie
  1. Create the roles directory and then create the requirements.yml file with the following data:

$ cat roles/requirements.yml
- src: https://github.com/CiscoDevNet/Ansible-pyats
scm: git
name: Ansible-pyats
  1. Install the Ansible-pyats role as shown in the following code:
 $ Ansible-galaxy install -r requirements.yml
  1. Create a new playbook called pb_validate_pyats.yml and populate it with the following task to collect the ospf neighbor from the wan devices.
---
- name: Network Validation with pyATS
hosts: wan
roles:
- Ansible-pyats
vars:
Ansible_connection: local
tasks:
- pyats_parse_command:
command: show ip ospf neighbor
register: ospf_output
vars:
Ansible_connection: network_cli
  1. Update the playbook with the following tasks to extract the data for OSPF peer information:
      - name: "FACT >> Pyats OSPF Info"
set_fact:
pyats_ospf_data: "{{ ospf_output.structured.interfaces }}"

- name: " FACT >> Set OSPF peers"
set_fact:
OSPF_PEERS: "{{ wan_l3_links[inventory_hostname] | selectattr('ospf','equalto',true) | list }}"
  1. Update the playbook with the following tasks to validate OSPF peers and the OSPF peer state:
      - name: Validate Number of OSPF Peers
assert:
that:
- pyats_ospf_data | length == OSPF_PEERS | length
loop: "{{ OSPF_PEERS }}"

- name: Validate All Peers are in Full State
assert:
that:
- pyats_ospf_data[item.name] | json_query('neighbors.*.state') | first == 'FULL/ -'
loop: "{{ OSPF_PEERS }}"

How it works...

In this recipe, we are exploring how to use the pyATS framework to perform network validation. pyATS is an open source Python library developed by Cisco as a testing framework for network testing. Genie is another Python library that provides parsing capabilities for transforming CLI-based output to Python data structures that we can consume in our automation scripts. Cisco released an Ansible role that uses the pyATS and Genie libraries. Within this role, there are multiple modules that we can use in order to build more robust Ansible validation playbooks to validate the network state. In order to start working with this role, we need to perform the following steps:

  1. Install pyats and enie Python packages using python-pip.
  2. Install the Ansible-pyats role using Ansible-galaxy.

In this recipe, we are using one of the modules within the Ansible-pyats role, which is pyats_parse_command. This module executes an operational command on the remote managed device and returns both the CLI output for this command and the parsed structured output for this command. The following code snippet outlines the structured data returned by this module for ip ospf neigbor on the wan01 device:

"structured": {
"interfaces": {
"GigabitEthernet2": {
"neighbors": {
"10.100.1.1": {
"address": "10.3.1.1",
"dead_time": "00:00:37",
"priority": 0,
"state": "FULL/ -"
}
}
}
}
}

We save the data returned by this module to the ospf_output variable and we use the set_fact module to capture the structured data returned by this module, before saving it to a new variable – pyats_ospf_data. Then, we use the set_fact module to filter the links defined in wan_l3_interfaces to just the ports that are enabled for OSPF.

Using the structured data returned by pyats_parse_command, we can validate this data and compare it with our OSPF peer definition using the assert module so as to validate the correct number of OSPF peers and their states.

To extract the OSPF peer state, we use the json_query filter to filter the returned data and provide just the OSPF state for each neighbor.

We are setting Ansible_connection to local on the play level, and setting it to network_cli on the pyats_parse_command task level, since we only need to connect to the device in this task. All the other tasks can run locally on the Ansible machine.

See also...

About the Author

  • Karim Okasha

    Karim Okasha is a network consultant with over 15 years of experience in the ICT industry. He is specialized in the design and operation of large telecom and service provider networks and has lots of experience in network automation. Karim has a bachelor's degree in telecommunications and holds several expert-level certifications, such as CCIE, JNCIE, and RHCE. He is currently working in Red Hat as a network automation consultant, helping large telecom and service providers to design and implement innovative network automation solutions. Prior to joining Red Hat, he worked for Saudi Telecom Company as well as Cisco and Orange S.A.

    Browse publications by this author

Latest Reviews

(3 reviews total)
Proceso de compra sencillo y ràpido.
Just the best thing I ever bought
Book by Eric is essential requirement for my line of work

Recommended For You

Network Automation Cookbook
Unlock this book and the full library for FREE
Start free trial