Network Automation Cookbook

4.7 (3 reviews total)
By Karim Okasha
  • Instant online access to over 8,000+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Building Blocks of 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

 

Building Blocks of Ansible

Ansible is an enormously popular automation framework that has been used to automate IT operations for a long time. It simplifies the management of different infrastructure nodes and translates the business logic into well-defined procedures in order to implement this business logic. Ansible is written in Python and it mainly relies on SSH to communicate with infrastructure nodes to execute instructions on them. It started support for networking devices beginning with Ansible 1.9, and with Ansible 2.9, its current support for network devices has grown extensively. It can interact with network devices using either SSH or via API if the network vendors support APIs on their equipment. It also provides multiple advantages, including the following:

  • An easy learning curve: Writing Ansible playbooks requires knowledge of YAML and Jinja2 templates, which are easy to learn, and its descriptive language is easy to understand.
  • Agentless: It doesn't require an agent to be installed on the remotely managed device in order to control this device.
  • Extensible: Ansible comes equipped with multiple modules to execute a variety of tasks on the managed nodes. It also supports writing custom modules and plugins to extend Ansible's core functionality.
  • Idempotent: Ansible will not change the state of the device unless it needs to in order to change its setting to reach the desired state. Once it is in this desired state, running Ansible Playbooks against the device will not alter its configurations.

In this chapter, we will introduce the main components of Ansible and outline the different features and options that Ansible supports. The following are the main recipes that will be covered:

  • Installing Ansible
  • Building Ansible's inventory
  • Using Ansible's variables
  • Building Ansible's playbook
  • Using Ansible's conditionals
  • Using Ansible's loops
  • Securing secrets with Ansible Vault
  • Using Jinja2 with Ansible
  • Using Ansible's filters
  • Using Ansible Tags
  • Customizing Ansible's settings
  • Using Ansible Roles

The purpose of this chapter is to have a basic understanding of the different Ansible components that we will utilize throughout this book in order to interact with the networking device. Consequently, all the examples in this chapter are not focused on managing networking devices. Instead, we will focus on understanding the different components in Ansible in order to use them effectively in the next chapters.

 

Technical requirements

Here are the requirements for installing Ansible and running all of our Ansible playbooks:

  • A Linux Virtual Machine (VM) with either of the following distributions:
    • Ubuntu 18.04 or higher
    • CentOS 7.0 or higher
  • Internet connectivity for the VM
Setting up the Linux machine is outside the scope of this recipe. However, the easiest approach to setting up a Linux VM with any OS version is by using Vagrant to create and set up the Ansible VM.
 

Installing Ansible

The machine on which we install Ansible (this is known as the Ansible control machine) should be running on any Linux distribution. In this recipe, we will outline how to install Ansible on both an Ubuntu Linux machine or a CentOS machine.

Getting ready

To install Ansible, we need a Linux VM using either an Ubuntu 18.04+ OS or CentoS 7+ OS. Furthermore, this machine needs to have internet access for Ansible to be installed on it.

How to do it...

Ansible is written in Python and all its modules need Python to be installed on the Ansible control machine. Our first task is to ensure that Python is installed on the Ansible control machine, as outlined in the following steps.

  1. Most Linux distributions have Python installed by default. However, if Python is not installed, here are the steps for installing it on Linux:
    •  On an Ubuntu OS, execute the following command:
# Install python3
$sudo apt-get install python3

# validate python is installed
$python3 --version
Python 3.6.9
    • On a CentOS OS, execute the following command:
# Install python
$sudo yum install pytho3

# validate python is installed
$python3 --version
Python 3.6.8
  1. After we have validated that Python is installed, we can start to install Ansible:
    • On an Ubuntu OS, execute the following command:
# We need to use ansible repository to install the latest version of Ansible
$ sudo apt-add-repository ppa:ansible/ansible

# Update the repo cache to use the new repo added
$ sudo apt-get update

# We install Ansible
$ sudo apt-get install ansible
    • On a CentOS OS, execute the following command:
# We need to use latest epel repository to get the latest ansible 
$ sudo yum install epel-release

# We install Ansible
$ sudo yum install ansible

How it works..

The easiest way to install Ansible is by using the package manager specific to our Linux distribution. We just need to make sure that we have enabled the required repositories to install the latest version of Ansible. In both Ubuntu and CentOS, we need to enable extra repositories that provide the latest version for Ansible. In CentOS, we need to install and enable the Extra Packages for Enterprise Linux Repository (EPEL repo), which provides extra software packages and has the latest Ansible packages for CentOS. 

Using this method, we will install Ansible and all the requisite system packages needed to run the Ansible modules. In both Ubuntu and CentOS, this method will also install Python 2 and run Ansible using Python 2. We can validate the fact that Ansible is installed and which version is used by running the following command:

$ ansible --version
ansible 2.9
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/home/vagrant/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /usr/bin/ansible
python version = 2.7.5 (default, Aug 7 2019, 00:51:29) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]

Also, we can check that Ansible is working as expected by trying to connect to the local machine using the ping module as shown:

$ ansible -m ping localhost

localhost | SUCCESS => {
"changed": false,
"ping": "pong"
}

Using this method, we can see that it has the following issues:

  • It uses Python 2 as the execution environment, but we want to use Python 3.
  • It updates the Python packages installed on the system, which might not be desirable.
  • It doesn't provide us with the granularity needed in order to select which version of Ansible to use. Using this method, we will always install the latest version of Ansible, which might not be what we need.

How it works...

In order to install Ansible in a Python 3 environment and to have more control over the version of Ansible deployed, we are going to use the pip Python program to install Ansible as shown here:

  • Install Python 3 if it is not present, as follows:
# Ubuntu
$ sudo apt-get install python3

# CentOS
sudo yum install python3
  • Install the python3-pip package:
# Ubuntu
$ sudo apt-get install python3-pip

# CentOS
$ sudo yum install python3-pip
  • Install Ansible:
# Ubuntu and CentOS
# This will install ansible for the current user ONLY
$ pip3 install ansible==2.9 --user

# We Can install ansible on the System Level
$ sudo pip3 install ansible==2.9
  • We can verify that Ansible has been installed successfully, as shown here:
$$ ansible --version
ansible 2.9
config file = None
configured module search path = ['/home/vagrant/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /home/vagrant/.local/lib/python3.6/site-packages/ansible
executable location = /home/vagrant/.local/bin/ansible
python version = 3.6.8 (default, Aug 7 2019, 17:28:10) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]

Installing Ansible using this method ensures that we are using Python 3 as our execution environment and allows us to control which version of Ansible to install, as outlined in the example shown.

We are going to use this method as our Ansible installation method and all the subsequent chapters will be based on this installation procedure.

In Chapter 13, Advanced Techniques and Best Practices for Ansible, we will outline yet another method for installing Ansible using Python virtual environments.

See also...

 

Building Ansible's inventory

After installing Ansible, we need to define Ansible's inventory, which is a text file that defines the nodes that Ansible will manage. In this recipe, we will outline how to create and structure Ansible's inventory file.

Getting ready

We need to create a folder that will contain all the code that we will outline in this chapter. We create a folder called ch1_ansible, as shown here:

$ mkdir ch1_ansible
$ cd ch1_ansible

How to do it...

Perform the following steps to create the inventory file:

  1. Create a file named hosts:
$ touch hosts
  1. Using any text editor, open the file and add the following content:
$ cat hosts

[cisco]
csr1 ansible_host=172.10.1.2
csr2 ansible_host=172.10.1.3

[juniper]
mx1 ansible_host=172.20.1.2
mx2 ansible_host=172.20.1.3

[core]
mx1
mx2

[edge]
csr[1:2]

[network:children]
core
edge
The Ansible inventory file can have any name. However, as a best practice, we will use the name hosts to describe the devices in our inventory.

How it works...

The Ansible inventory files define the hosts that will be managed by Ansible (in the preceding example, this is csr1-2 and mx1-2 ) and how to group these devices into custom-defined groups based on different criteria. The groups are defined with []. This grouping helps us to define the variables and simplify the segregation between the devices and how Ansible interacts with them. How we group the devices is based on our use case, so we can group them as per the vendor (Juniper and IOS) or function (core and edge).

We can also build hierarchies for the groups using the children, which is outlined in the inventory file. The following diagram shows how the hosts are grouped and how the group hierarchy is built:

 

Using Ansible's variables

Ansible stores the information for the nodes that it manages using Ansible variables. Ansible variables can be declared in multiple locations. However, in observing the best practices for Ansible, we will outline the two main parts where Ansible looks for variables for the nodes that are declared in the inventory file.

Getting ready

In order to follow along with this recipe, an Ansible inventory file must be already defined as outlined in the previous recipes.

How to do it...

In the inventory file, we define hosts and we group the hosts into groups. We now define two directories that Ansible searches for group variables and host variables:

  1.  Create two folders, group_vars and host_vars:
$ cd ch1_ansible
$ mkdir group_vars host_vars
  1. Create ios.yml and junos.yml files inside group_vars:
$ touch group_vars/cisco.yml group_vars/juniper.yml
  1. Create mx1.yml and csr1.yml inside host_vars
$ touch host_vars/csr1.yml host_vars/mx1.yml
  1. Populate variables in all the files, as shown here:
$echo 'hostname: core-mx1' >> host_vars/mx1.yml
$echo 'hostname: core-mx2' >> host_vars/mx2.yml
$echo 'hostname: edge-csr1' >> host_vars/csr1.yml
$echo 'hostname: edge-csr2' >> host_vars/csr2.yml
$echo 'os: ios' >> group_vars/cisco.yml
$echo 'os: junos' >> group_vars/juniper.yml

How it works...

We created the following structure of directories and files to host our variables, as shown in the following diagram:

All files inside the group_vars directory contain the group variables for the groups that we have defined in our inventory and they apply to all the hosts within this group. As for the files within host_vars, they contain variables for each host. Using this structure, we can group variables from multiple hosts into a specific group file and variables that are host-specific will be placed in a separate file specific to this host.

There's more...

In addition to host_vars and group_vars, Ansible supports the definition of variables using other techniques, including the following:

  • Using the vars keyword within the play to specify multiple variables
  • Using vars_files to define variables in a file and having Ansible read these variables from this file while running the playbook
  • Specifying variables at the command line using the --e option

In addition to the user-defined variables that we can specify, Ansible has some default variables that it builds dynamically for its inventory. The following table captures some of the most frequently used variables:

 inventory_hostname  The name of the hosts as defined in the inventory (for example, csr1 and mx1)
 play_hosts  A list of all the hosts included in the play
 group_names  A list of all the groups that a specific host is a part of (for example, for csr1 this will be [edge, Cisco, network])
 

Building Ansible's playbook

An Ansible playbook is the fundamental element in Ansible that declares what actions we would like to perform on our managed hosts (specified in the inventory). An Ansible playbook is a YAML-formatted file that defines a list of tasks that will be executed on our managed devices. In this recipe, we will outline how to write an Ansible playbook and how to define the hosts that will be targeted by this playbook.

Getting ready

In order to follow along with this recipe, an Ansible inventory file must already be defined, along with all the group- and host-specific variable files created in accordance with previous recipes.

How to do it...

  1. Create a new file called playbook.yml inside the ch1_ansible folder and incorporate the following lines in this file:
$  cat playbook.yml

---
- name: Initial Playbook
hosts: all
gather_facts: no
tasks:
- name: Display Hostname
debug:
msg: "Router name is {{ hostname }}"
- name: Display OS
debug:
msg: "{{ hostname }} is running {{ os }}"
  1. Run the playbook as shown here:
$ ansible-playbook -i hosts playbook.yml

How it works...

The Ansible playbook is structured as a list of plays and each play targets a specific group of hosts (defined in the inventory file). Each play can have one or more tasks to execute against the hosts in this play. Each task runs a specific Ansible module that has a number of arguments. The general structure of the playbook is outlined in the following screenshot:

In the preceding playbook, we reference the variables that we defined in the previous recipe inside the {{ }} brackets. Ansible reads these variables from either group_vars or host_vars, and the module that we used in this playbook is the debug module, which displays as a custom message specified in the msg parameter to the Terminal output. The playbook run is shown here:

We use the -i option in the ansible-playbook command in order to point to the Ansible inventory file, which we will use as our source to construct our inventory.

In this playbook, I have used the all keyword to specify all the hosts within the inventory. This is a well-known group name that Ansible dynamically constructs for all hosts within the inventory.
 

Using Ansible's conditionals

One of the core features of Ansible is conditional task execution. This provides us with the ability to control which tasks to run on a given host based on a condition/test that we specify. In this recipe, we will outline how to configure conditional task execution. 

Getting ready

In order to follow along with this recipe, an Ansible inventory file must be present and configured as outlined in the previous recipes. Furthermore, the Ansible variables for all our hosts should be defined as outlined in the previous recipes. 

How to do it...

  1. Create a new playbook called ansible_cond.yml inside the ch1_ansible folder.
  2. Place the following content in the new playbook as shown here:
---
- name: Using conditionals
hosts: all
gather_facts: no
tasks:
- name: Run for Edge nodes Only
debug:
msg: "Router name is {{ hostname }}"
when: "'edge' in group_names"

- name: Run for Only MX1 node
debug:
msg: "{{ hostname }} is running {{ os }}"
when:
- inventory_hostname == 'mx1'
  1. Run the playbook as shown here:
$ ansible-playbook -i hosts ansible_cond.yml

How it works...

Ansible uses the when statement to provide conditional execution for the tasks. The when statement is applied at the task level and if the condition in the when statement evaluates to true, the task is executed for the given host. If false, the task is skipped for this host. The output as a result of running the preceding playbook is shown here:

The when statement can take a single condition as seen in the first task, or can take a list of conditions as seen in the second task. If when is a list of conditions, all the conditions need to be true in order for the task to be executed.

In the first task, the when statement is enclosed in "" since the statement starts with a string. However, in the second statement, we use a normal when statement with no "" since the when statement starts with a variable name.

See also...

 

Using Ansible's loops

In some cases, we need to run a task inside an Ansible playbook to loop over some data. Ansible's loops allow us to loop over a variable (a dictionary or a list) multiple times in order to achieve this behavior. In this recipe, we will outline how to use Ansible's loops.

Getting ready

In order to follow along with this recipe, an Ansible inventory file must be present and configured, as outlined in the previous recipes

How to do it...

  1. Create a new playbook called ansible_loops.yml inside the ch1_ansible folder.
  2. Inside the group_vars/cisco.yml file, incorporate the following content:
snmp_servers:
- 10.1.1.1
- 10.2.1.1
  1. Inside the group_vars/juniper.yml file, incorporate the following content:
users:
admin: admin123
oper: oper123
  1. Inside the ansible_loops.yml file, incorporate the following content:
---
- name: Ansible Loop over a List
hosts: cisco
gather_facts: no
tasks:
- name: Loop over SNMP Servers
debug:
msg: "Router {{ hostname }} with snmp server {{ item }}"
loop: "{{ snmp_servers}}"

- name: Ansible Loop over a Dictionary
hosts: juniper
gather_facts: no
tasks:
- name: Loop over Username and Passowrds
debug:
msg: "Router {{ hostname }} with user {{ item.key }} password {{ item.value }}"
with_dict: "{{ users}}"
  1. Run the playbook as shown here:
$ ansible-playbook ansible_loops.yml -i hosts

How it works..

Ansible supports looping over two main iterable data structures: lists and dictionaries. We use the loops keyword when we need to iterate over lists (snmp_servers is a list data structure) and we use with_dicts when we loop over a dictionary (users is a dictionary data structure where the username is the key and the passwords are the values). In both cases, we use the item keyword to specify the value in the current iteration. In the case of with_dicts, we get the key using item.key and we get the value using item.value.

The output of the preceding playbook run is shown here:

See also...

 

Securing secrets with Ansible Vault

When we are dealing with sensitive material that we need to reference in our Ansible playbooks, such as passwords, we shouldn't save this data in plain text. Ansible Vault provides a method to encrypt this data and therefore be safely decrypted and accessed while the playbook is running. In this recipe, we will outline how to use Ansible Vault in order to secure sensitive information in Ansible.

How to do it...

  1. Create a new file called decrypt_passwd as shown:
$ echo 'strong_password' > decrypt_passwd
  1. Using ansible-vault creates a new file called secrets, as shown here:
$ ansible-vault create --vault-id=decrypt_passwd secrets
  1. Add the following variables to this new secrets file:
ospf_password: [email protected]
bgp_password: [email protected]
  1. Create a new playbook called ansible_vault.yml, as shown here:
---
- name: Using Ansible vault
hosts: all
gather_facts: no
vars_files:
- secrets
tasks:
- name: Output OSPF passowrd
debug:
msg: "Router {{ hostname }} ospf Password {{ ospf_password }}"
when: inventory_hostname == 'csr1'

- name: Output BGP passowrd
debug:
msg: "Router {{ hostname }} BGP Password {{ bgp_password }}"
when: inventory_hostname == 'mx1'
  1. Run the playbook as shown here:
$ ansible-playbook --vault-id=decrypt_passwd ansible_vault.yml -i hosts

How it works..

We use the ansible-vault command to create a new file that is encrypted using a key specified by -- vault-id. We place this key/password in another file (which is called decrypt_passwd in our example) and we pass this file as an argument to vault-id. Inside this file, we can place as many variables as we need. Finally, we include this file as a variable file in the playbook using vars_files. The following is the content of the secret file in case we try to read it without decryption:

$ cat secrets
$ANSIBLE_VAULT;1.1;AES256
61383264326363373336383839643834386661343630393965656135666336383763343938313963
3538376230613534323833356237663532666363626462640a663839396230646634353839626461
31336461386361616261336534663137326265363261626536663564623764663861623735633865
3033356536393631320a643561623635363830653236633833383531366166326566623139633838
32633335616663623761313630613134636635663865363563366564313365376431333461623232
34633838333836363865313238363966303466373065356561353638363731616135386164373263
666530653334643133383239633237653034

In order for Ansible to decrypt this file, we must supply the decryption password (stored in a decrypt_passwd file in this example) via the --vault-id option. When we run ansible-playbook, we must supply this decryption password, otherwise the ansible-playbook fails, as shown here:

### Running the Ansible playbook without --vault-id 
$ansible-playbook ansible_vault.yml -i hosts
ERROR! Attempting to decrypt but no vault secrets found

There's more...

In case we don't want to specify the encryption/decryption password in the text file, we can use --ask-vault-pass with the ansible-playbook command in order to input the password while running the playbook, as shown here:

### Running the Ansible playbook with --ask-vault-pass
$ansible-playbook ansible_vault.yml -i hosts --ask-vault-pass
Vault password:
 

Using Jinja2 with Ansible

Jinja2 is a powerful templating engine for Python and is supported by Ansible. It is also used to generate any text-based files, such as HTML, CSV, or YAML. We can utilize Jinja2 with Ansible variables in order to generate custom configuration files for network devices. In this recipe, we will outline how to use Jinja2 templates with Ansible.

Getting ready

In order to follow along with this recipe, an Ansible inventory file must be present and configured as outlined in the previous recipes. 

How to do it...

  1. Create a new file inside the group_vars directory called network.yml:
$ cat group_vars/network.yml

---
ntp_servers:
- 172.20.1.1
- 172.20.2.1
  1. Create a new templates directory and create a new ios_basic.j2 file with the following content:
$ cat templates/ios_basic.j2
hostname {{ hostname }}
!
{% for server in ntp_servers %}
ntp {{ server }}
{% endfor %}
!
  1. Create a new junos_basic.j2 file within the templates directory with the following content:
$ cat templates/junos_basic.j2
set system host-name {{ hostname }}
{% for server in ntp_servers %}
set system ntp server {{ server }}
{% endfor %}
  1. Create a new playbook called ansible_jinja2.yml with the following content:
---
- name: Generate Cisco config from Jinja2
hosts: localhost
gather_facts: no
tasks:
- name: Create Configs Directory
file: path=configs state=directory

- name: Generate Cisco config from Jinja2
hosts: cisco
gather_facts: no
tasks:
- name: Generate Cisco Basic Config
template:
src: "templates/ios_basic.j2"
dest: "configs/{{inventory_hostname}}.cfg"
delegate_to: localhost

- name: Generate Juniper config from Jinja2
hosts: juniper
gather_facts: no
tasks:
- name: Generate Juniper Basic Config
template:
src: "templates/junos_basic.j2"
dest: "configs/{{inventory_hostname}}.cfg"
delegate_to: localhost
  1. Run the Ansible playbook as shown here:
$ ansible-playbook -i hosts ansible_jinja2.yml

How it works...

We created the network.yml file in order to group all the variables that will apply to all devices under this group. After that, we create two Jinja2 files, one for Cisco IOS devices, and the other for Juniper devices. Inside each Jinja2 template, we reference the Ansible variables using {{}}. We also use the for loop construct, {% for server in ntp_servers %} , supported by the Jinja2 templating engine in order to loop over the ntp_servers variable (which is a list) to access each item within this list. 

Ansible provides the template module that takes two parameters:

  • src: This references the Jinja2 template file.
  • dest: This specifies the output file that will be generated.

In our case, we use the {{inventory_hostname}} variable in order to make the output configuration file unique for each router in our inventory.

By default, the template modules create the output file on the remotely managed nodes. However, this is not possible in our case since the managed devices are network nodes. Consequently, we use the delegate_to: localhost option in order to run this task locally on the Ansible control machine.

The first play in the playbook creates the configs directory to store the configuration files for the network devices. The second play runs the template module on Cisco devices, and the third play runs the template task on Juniper devices.

The following is the configuration file for one of the Cisco devices:

$ cat configs/csr1.cfg
hostname edge-csr1
!
ntp 172.20.1.1
ntp 172.20.2.1
!

This is the configuration file for one of the Juniper devices:

$ cat configs/mx1.cfg
set system host-name core-mx1
set system ntp server 172.20.1.1
set system ntp server 172.20.2.1

See also...

 

Using Ansible's filters

Ansible's filters are mainly derived from Jinja2 filters, and all Ansible filters are used to transform and manipulate data (Ansible's variables). In addition to Jinja2 filters, Ansible implements its own filters to augment Jinja2 filters, while also allowing users to define their own custom filters. In this recipe, we will outline how to configure and use Ansible filters to manipulate our input data.

How to do it...

  1. Install python3-pip and Python's netaddr library, since we will be using the Ansible IP filter, which requires Python's netaddr library:
# On ubuntu
$ sudo apt-get install python3-pip

# On CentOS
$ sudo yum install python3-pip

$ pip3 install netaddr
  1. Create a new Ansible playbook called ansible_filters.yml, as shown here:
---
- name: Ansible Filters
hosts: csr1
vars:
interfaces:
- { port: FastEthernet0/0, prefix: 10.1.1.0/24 }
- { port: FastEthernet1/0, prefix: 10.1.2.0/24 }
tasks:
- name: Generate Interface Config
blockinfile:
block: |
hostname {{ hostname | upper }}
{% for intf in interfaces %}
!
interface {{ intf.port }}
ip address {{intf.prefix | ipv4(1) | ipv4('address') }} {{intf.prefix | ipv4('netmask') }}
!
{% endfor %}
dest: "configs/csr1_interfaces.cfg"
create: yes
delegate_to: localhost

How it works...

First of all, we are using the blockinfile module to create a new configuration file on the Ansible control machine. This module is very similar to the template module. However, we can write the Jinja2 expressions directly in the module in the block option. We define a new variable called interfaces using the vars parameter in the playbook. This variable is a list data structure where each item in the list is a dictionary data structure. This nested data structure specifies the IP prefix used on each interface.

In the Jinja2 expressions, we can see that we have used a number of filters as shown here:

  • {{ hostname | upper}}: upper is a Jinja2 filter that transforms all the letters of the input string into uppercase. In this way, we pass the value of the hostname variable to this filter and the output will be the uppercase version of this value.
  •  {{intf.prefix | ipv4(1) | ipv4('address') }}: Here, we use the Ansible IP address filter twice. ipv4(1) takes an input IP prefix and outputs the first IP address in this prefix. We then use another IP address filter, ipv4('address'), in order to only get the IP address part of an IP prefix. So in our case, we take 10.1.1.0/24 and we output 10.1.1.1 for the first interface.
  • {{intf.prefix | ipv4('netmask') }}: Here, we use the Ansible IP address filter to get the netmask of the IP address prefix, so in our case, we get the /24 subnet and transform it to 255.255.255.0.

The output file for the csr1 router after this playbook run is shown here:

$ cat configs/csr1_interfaces.cfg
# BEGIN ANSIBLE MANAGED BLOCK
hostname EDGE-CSR1
!
interface FastEthernet0/0
ip address 10.1.1.1 255.255.255.0
!
!
interface FastEthernet1/0
ip address 10.1.2.1 255.255.255.0
!
# END ANSIBLE MANAGED BLOCK
 

Using Ansible Tags

Ansible Tags is a powerful tool that allows us to tag specific tasks within a large Ansible playbook and provides us with the flexibility to choose which tasks will run within a given playbook based on the tags we specify. In this recipe, we will outline how to configure and use Ansible Tags.

How to do it...

  1. Create a new Ansible playbook called ansible_tags.yml, as shown here:
---
- name: Using Ansible Tags
hosts: cisco
gather_facts: no
tasks:
- name: Print OSPF
debug:
msg: "Router {{ hostname }} will Run OSPF"
tags: [ospf, routing]

- name: Print BGP
debug:
msg: "Router {{ hostname }} will Run BGP"
tags:
- bgp
- routing

- name: Print NTP
debug:
msg: "Router {{ hostname }} will run NTP"
tags: ntp
  1. Run the playbook as shown here:
$ ansible-playbook ansible_tags.yml -i hosts --tags routing
  1. Run the playbook again, this time using tags, as shown here:
$ ansible-playbook ansible_tags.yml -i hosts --tags ospf

$ ansible-playbook ansible_tags.yml -i hosts --tags routing

How it works...

We can use tags to mark both tasks and plays with a given tag in order to use it to control which tasks or plays get executed. This gives us more control when developing playbooks to allow us to run the same playbook. However, with each run, we can control what we are deploying. In the example playbook in this recipe, we have tagged the tasks as OSPF, BGP, or NTP and have applied the routing tag to both the OSPF and BGP tasks. This allows us to selectively run the tasks within our playbook as shown here:

  • With no tags specified, this will run all the tasks in the playbook with no change in the behavior, as shown in the following screenshot:

  • Using the ospf tag, we will only run any task marked with this tag, as shown here:

  • Using the routing tag, we will run all tasks marked with this tag, as shown here:

See also...

 

Customizing Ansible's settings

Ansible has many setting that can be adjusted and controlled using a configuration file called ansible.cfg. This file has multiple options that control many aspects of Ansible, including how Ansible looks and how it connects to managed devices. In this recipe, we will outline how to adjust some of these default settings.

How to do it...

  1. Create a new file called ansible.cfg, as shown here:
[defaults]
inventory=hosts
vault_password_file=decryption_password
gathering=explicit

How it works...

By default, Ansible's settings are controlled by the ansible.cfg file located in the /etc/ansible directory. This is the default configuration file for Ansible that controls how Ansible interacts with managed nodes. We can edit this file directly. However, this will impact any playbook that we will use on the Ansible control machine, as well as any user on this machine. A more flexible and customized option is to include a file named ansible.cfg in the project directory and this includes all the options that you need to modify from their default parameters. In the preceding example, we outline only a small subset of these options, as shown here:

  • inventory: This option modifies the default inventory file that Ansible searches in order to find its inventory (by default, this is /etc/ansible/hosts). We adjust this option in order to let Ansible use our inventory file and stop using the -i operator to specify our inventory during each playbook run.
  • vault_password_file: This option sets the file that has the secret password for encrypting and decrypting ansible-vault secrets. This option removes the need to run Ansible playbooks with the --vault-id operator when using ansible-vault-encrypted variables.

  • gathering = explicit: By default, Ansible runs a setup module to gather facts regarding the managed nodes while the playbook is running. This setup module is not compatible with network nodes since this module requires a Python interpreter on the managed nodes. By setting fact gathering to explicit, we disable this default behavior.

See also...

 

Using Ansible Roles

Ansible Roles promotes code reusability and provides a simple method for packaging Ansible code in a simple way that can be shared and consumed. An Ansible role is a collection of all the required Ansible tasks, handlers, and Jinja2 templates that are packaged together in a specific structure. A role should be designed in order to deliver a specific function/task. In this recipe, we will outline how to create an Ansible role and how to use it in our playbooks.

How to do it...

  1. Inside the ch1_ansible folder, create a new folder called roles and create a new role called basic_config, as shown here:
$ mkdir roles
$ cd roles
$ ansible-galaxy init basic_config
  1. Update the basic_config/vars/main.yml file with the following variable:
$ cat roles/basic_config/vars/main.yml

---
config_dir: basic_config
  1. Update the basic_config/tasks/main.yml file with the following tasks:
$ cat roles/basic_config/tasks/main.yml

---
- name: Create Configs Directory
file:
path: "{{ config_dir }}"
state: directory
run_once: yes

- name: Generate Cisco Basic Config
template:
src: "{{os}}.j2"
dest: "{{config_dir}}/{{inventory_hostname}}.cfg"
  1. Inside the basic_config/templates folder, create the following structure:
$ tree roles/basic_config/templates/

roles/basic_config/templates/
├── ios.j2
└── junos.j2

$ cat roles/basic_config/templates/ios.j2
hostname {{ hostname }}
!
{% for server in ntp_servers %}
ntp {{ server }}
{% endfor %}
  1. Create a new playbook, pb_ansible_role.yml, with the following content to use our role:
$ cat pb_ansible_role.yml
---
- name: Build Basic Config Using Roles
hosts: all
connection: local
roles:
- basic_config

How it works...

In this recipe, we start by creating the roles directory within our main folder. By default, when using roles, Ansible will look for roles in the following location in this order:

  • The roles folder within the current working directory
  • /etc/ansible/roles

Consequently, we create the roles folder within our current working directory (ch1_ansible) in order to host all the roles that we will create in this folder. We create the role using the ansible-galaxy command with the init option and the role name (basic_config), which will create the following role structure inside our roles folder:

$ tree roles/
roles/
└── basic_config
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── README.md
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml

As can be seen from the preceding output, this folder structure is created using the ansible-galaxy command and this command builds the role in keeping with the best practice role layout. Not all these folders need to have a functional role that we can use, and the following list outlines the main folders that are commonly used:

  • The tasks folder: This contains the main.yml file, which lists all the tasks that should be executed when we use this role.
  • The templates folder: This contains all the Jinja2 templates that we will use as part of this role.
  • The vars folder: This contains all the variables that we want to define and that we will use in our role. The variables inside the vars folder have very high precedence when evaluating the variables while running the playbook.
  • The handlers folder: This contains the main.yml file, which includes all the handlers that should run as part of this role.

The role that we created has a single purpose, which is to build the basic configuration for our devices. In order to accomplish this task, we need to define some Ansible tasks as well as use a number of Jinja2 templates in order to generate the basic configuration for the devices. We list all the tasks that we need to run in the tasks/main.yml file and we include all the necessary Jinja2 templates in the templates folder. We define any requisite variable that we will use in our role in the vars folder.

We create a new playbook that will use our new role in order to generate the configuration for the devices. We call all the roles that we want to run as part of our playbook in the roles parameter. In our case, we have a single role that we want to run, which is the basic_config role.

Once we run our playbook, we can see that a new directory called basic_config is created with the following content:

$ tree basic_config/
basic_config/
├── csr1.cfg
├── csr2.cfg
├── mx1.cfg
└── mx2.cfg

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

Book Title
Unlock this full book with a FREE 10-day trial
Start Free Trial