Ansible Playbook Optimizing

$ cat ansible.cfg 
[defaults]
hostfile = ./hosts
host_key_checking = False
timeout = 5 
log_path = ./logfile.txt
forks = 50
gathering = smart

[ssh_connection]
pipelining = True

The above file shows the content of ansible.cfg file. I have added the following to make my playbook run faster:

forks
gathering
pipelining

Forks indicate the number of parallel processes spawned to communicate with remote hosts. Default forks is 5 in Ansible.

Gathering indicates the default policy for fact gathering. When “gather_facts” is True within the playbook, facts are gathered for each host. The facts associated with each host will be discovered only once even when the host is referred in multiple plays when we use “smart” within the ansible.cfg file.

Pipelining enabled will reduce the number of SSH operations required to execute a module on a remote host.

Ansible Components

What?

Ansible is a simple IT automation tool. Ansible exists as CLI & GUI. GUI is called the Ansible Tower and Ansible, Inc., which is owned by RedHat, officially supports this.

Controlling Nodes:

The Network infrastructure is managed from these Controlling Nodes. In an Enterprise environment, Controlling Nodes are typically Linux bastion servers.

Managed Nodes:

Managed Nodes are the Network Devices that is being managed by the Controlling Nodes. Managed Nodes are typically of Cisco, Juniper, and Arista make and can be classified as Switches, Routers, Firewalls and Load Balancers based on their function from a Network Engineer’s perspective.

Why?

There are many automation tools like Chef, Puppet, and CFEngine but in my opinion, Ansible is suited for Network Automation for the following reasons:

  1. Ansible does not require an agent to be installed in the Managed Node (Network Device).
  2. Ansible requires Python on the Managed Node and most Network Devices support Python.
  3. Ansible relies on YAML as the descriptive language and Jinja2 for templates.

Among the points mentioned above, most Network Vendors do not support the installation of agents and even if they did support the installation, it would be tough to get the relevant permissions within an organization to install the agents in an Enterprise environment that has different Network Teams managing different aspects of the infrastructure.

Fortunately, most network vendors provide native support for Python and Ansible rely on this to execute automation tasks on the “Managed Nodes”.

As a Network Engineer working in an environment with significant scale (1,000s of Network Devices across multiple datacenters), Ansible has been quite useful in obtaining data and deploying configuration. Ansible seems to have widespread support among the Network Engineers seeking automation to manage at scale and there are resources online that can be leveraged to implement Network Automation Solutions.

Ansible Components

Ansible requires the following components in order to automate Network Infrastructure:

  • Controlling Nodes
  • Managed Nodes
  • Ansible Playbook

As noted earlier, Controlling Nodes are usually Linux Bastion Servers that are used to access the switches/routers and other Network Devices. These Network Devices are referred to as the Managed Nodes. Managed Nodes are stored in the hosts file for Ansible automation.

Ansible Playbook:

Ansible Playbooks are expressed in YAML format and serve as the repository for the various tasks that will be executed on the Managed Nodes (hosts). Playbooks are a collection of tasks that will be run on one or more hosts.

Setting up Ansible:

After installing Ansible, I recommend creating a separate directory from which Ansible is executed. For this process, let’s create a directory named “AnsiblePlay”. Within the “AnsiblePlay” directory, I will have the following files:

  • ansible.cfg
  • hosts

and the following directories ./AnsiblePlay/

  • templates
  • hosts_var

Ansible Configuration File:

An Ansible Playbook utilizes the Ansible Configuration File in order to access resources required for the Ansible Playbook. For example, the configuration file stores location information for the hosts file that contains the Managed Nodes (hosts) on which the playbook is executed.

Ansible Configuration File can exist in the following locations and is utilized by the Ansible playbook in the following order.

* ANSIBLE_CONFIG (an environment variable)
* ansible.cfg (in the current directory)
* .ansible.cfg (in the home directory)
* /etc/ansible/ansible.cfg

I would recommend creating your own Ansible configuration file in the Ansible directory. I use the following:

$ cat ansible.cfg

[defaults]
hostfile = ./hosts
host_key_checking=False
timeout = 5

Inventory File or Hosts File:

Inventory File or Hosts File is a text file that contains the Managed Nodes (Hosts) that will be subjected to automation tasks defined in the playbook.

Inventory File can be static or dynamic. For now, the examples use static inventory files.

This is an inventory/host file example:

$ cat hosts
[ios]
Switch-1
Switch-2
Switch-3
Switch-4

[distro]
Distro1
Distro2

[aggr]
Aggr1
Aggr2

Lists: [ ]

Dictionaries: { }. Dictionary has “Key: Value” pair.

YAML – Anything is a string. Quoting strings is optional most of the times.

FACTS:

FACTS are data about the Managed Nodes. Example: Code Version running on Managed Node.

 TASK:

Ansible playbook contains one or more tasks. A task makes sure that the hosts exist in a specific state. When there are multiple tasks and if any task fails, subsequent tasks will not be executed.

 Idempotent:

Running a task once or multiple times is the same in terms of the final output. For example, a task that involves creating a user in the Managed Node will create the user only once no matter how many times the task is executed.

 

Ansible – The Why ?

What ?

Ansible is a simple IT automation tool.

Ansible exists as CLI & GUI. GUI is called the Ansible Tower and Ansible, Inc., which is owned by RedHat, officially supports this.

Controlling Nodes:

The Network infrastructure is managed from Controlling Nodes. In an Enterprise environment, Controlling Nodes are typically Linux bastion servers.

Managed Nodes:

Managed Nodes are the Network Devices that is being managed by the Controlling Nodes. Managed Nodes are typically of Cisco, Juniper, and Arista make and can be classified as Switches, Routers, Firewalls and Load Balancers based on their function from a Network Engineer’s perspective.

Why ?

There are many automation tools like Chef, Puppet, CFEngine but in my opinion, Ansible is suited for Network Automation for the following reasons:

  1. Ansible does not require an agent to be installed in the Managed Node (Network Device).
  2. Ansible requires Python on the Managed Node and most Network Devices support Python.
  3. Ansible relies on YAML as the descriptive language and Jinja2 for templates.

Among the points mentioned above, most Network Vendors do not support the installation of agents and even if they did support the installation, it would be tough to get the relevant permissions within an organization to install the agents in an Enterprise environment that has different Network Teams managing different aspects of the infrastructure.

Fortunately, most network vendors provide native support for Python and Ansible rely on this to execute automation tasks on the “Managed Nodes”.

As a Network Engineer working in an environment with significant scale (1,000s of Network Devices across multiple datacenters), Ansible has been quite useful in obtaining data and deploying configuration. Ansible seems to have widespread support among the Network Engineers seeking automation to manage at scale and there are resources online that can be leveraged to implement Network Automation Solutions.

Ansible – Cisco Config Implementation

The goal of this article is to explain configuration implementation on Cisco IOS after the config has been generated as shown in Ansible Config Generator III:

config-implementation.yml

---
- hosts: switch
  gather_facts: true
  connection: local
 
  tasks:
  - name: OBTAIN LOGIN CREDENTIALS
    include_vars: secrets.yml
 
  - name: DEFINE PROVIDER
    set_fact:
      provider:
        host: "{{ inventory_hostname }}"
        username: "{{ creds['username'] }}"
        password: "{{ creds['password'] }}"
        auth_pass: "{{ creds['auth_pass'] }}"
 
  - name: CONFIGURE INTERFACE
    ios_config:
      provider: "{{ provider }}"
      authorize: yes
      lines:
        - "{{ lookup('file', './config-output/{{ inventory_hostname }}.conf') }}"

The “hosts” file contains the switch names:

[switch]
switch-1
switch-2
switch-3
switch-4

secrets.yml

---
creds:
  username: cisco
  password: ciscopassword
  auth_pass: ciscoauthpassword

“./config-output/” directory contains the following files:

switch-1.conf
switch-2.conf
switch-3.conf
switch-4.conf

The inventory file (hosts) contains the switches in which the configuration from corresponding file name will be added. “switch-1.conf” file contents will be utilized to configure “switch-1” so on and so forth.

~/ansible_play/config-output$ cat switch-1.conf 
            
int Gi1/1                   
switchport access vlan 195
            
int Gi1/10                   
switchport access vlan 188
 
~/ansible_play/config-output$ cat switch-2.conf 
            
int Gi1/2                   
switchport access vlan 295
            
int Gi1/20                   
switchport access vlan 288
 
~/ansible_play/config-output$ cat switch-3.conf 
            
int Gi1/3                   
switchport access vlan 395
            
int Gi1/30                   
switchport access vlan 388
 
~/ansible_play/config-output$ cat switch-4.conf 
            
int Gi1/4                   
switchport access vlan 495
            
int Gi1/40                   
switchport access vlan 488

Ansible – Config Generator III

For Part I & Part II of this series.

The goal of this playbook is to be able to generate unique configuration for each switch. In this case, we are configuring a port to work as an access-port for a specific vlan. The port and vlan variable is different for each switch.

config-gen.yml

---
- hosts: 127.0.0.1
  connection: local
  gather_facts: no

  tasks:
  - name: GET DATA
    include_vars: ./host_vars/file.yml

  - name: GENERATE CONFIG
    template:
      src: ./templates/accessvlan.j2
      dest: ./config-output/{{ item.switch }}.conf
    with_items: "{{ file_vlan }}"

file.yml

---
file_vlan:
- { switch: switch-1, port: Gi1/8,  vlan: 395 }
- { switch: switch-1, port: Gi1/23, vlan: 388 }
- { switch: switch-2, port: Gi1/8,  vlan: 395 }
- { switch: switch-2, port: Gi1/23, vlan: 388 }
- { switch: switch-3, port: Gi1/9,  vlan: 395 }
- { switch: switch-3, port: Gi1/24, vlan: 388 }
- { switch: switch-4, port: Gi1/9,  vlan: 395 }
- { switch: switch-4, port: Gi1/24, vlan: 388 }

accessvlan.j2

{% for grouper, host in file_vlan|groupby('switch') %}
{% if item.switch == grouper %}
{% for item in host %}            
int {{ item.port }}                   
switchport access vlan {{ item.vlan }}
{% endfor %}
{% endif %}
{% endfor %}

When the “config-gen.yml” playbook is executed:

ansible-playbook config-gen.yml

we get the following output files:

~/ansible_play/config-output$ cat switch-1.conf 
            
int Gi1/1                   
switchport access vlan 195
            
int Gi1/10                   
switchport access vlan 188
 
~/ansible_play/config-output$ cat switch-2.conf 
            
int Gi1/2                   
switchport access vlan 295
            
int Gi1/20                   
switchport access vlan 288
 
~/ansible_play/config-output$ cat switch-3.conf 
            
int Gi1/3                   
switchport access vlan 395
            
int Gi1/30                   
switchport access vlan 388
 
~/ansible_play/config-output$ cat switch-4.conf 
            
int Gi1/4                   
switchport access vlan 495
            
int Gi1/40                   
switchport access vlan 488

Ansible – Config Generator – II

For the first part of this series, check this – 1st part of this series.

---
- hosts: local
  connection: local
  gather_facts: no

  tasks:
  - name: GET DATA
    include_vars: ./host_vars/file.yml

  - name: GENERATE CONFIG
    template:
      src: ./SVI.j2
      dest: ./{{ item.vlan }}.conf
    with_items: "{{ file_vlan }}"

 

This is the file.yml that is being referenced in the “include_vars”

---
file_vlan:
- { vrf: NET1, vlan: 502, vlanname: VLAN-502-NAME, net: 10.80.120.128/29 }
- { vrf: NET1, vlan: 503, vlanname: VLAN-503-NAME, net: 10.80.120.136/29 }

Compared to the 1st part of this series, we are moving the contents of the “with_items” to a separate YAML file and calling it based on the variable name “file_vlan” that is part of the file content.

Ansible – Config Generator – I

Before proceeding, make sure to install “netaddr” as this is required for “ipaddr()” used in the Jinja2 configuration template.

pip install netaddr

aggr.yml is the playbook that will be utilized for generating L3 SVI configuration:

---
- hosts: local
  connection: local
  gather_facts: no

tasks:
- name: GENERATE CONFIG
  template:
  src: ./SVI.j2
  dest: ./{{ item.vlan }}.conf
  with_items:
  - { vrf: NET1, vlan: 502, vlanname: VLAN-502-NAME, net: 10.80.120.128/29 }
  - { vrf: NET1, vlan: 503, vlanname: VLAN-503-NAME, net: 10.80.120.136/29 }

This is the SVI.j2 Jinja2 template that is being utilized to generate the final configuration:

###### A-Side ######
vlan {{ item.vlan }}
 name {{ item.vlanname }}

interface Vlan {{ item.vlan }}
 description {{ item.vlanname }}
 mtu 9100
 vrf member {{ item.vrf }}
 no ip redirects
 ip address {{ item.net | ipaddr('2') }}
 hsrp version 2
 hsrp 1
 authentication md5 key-string password{{ item.vlan }}
 preempt delay minimum 120
 priority 120
 timers 1 3
 ip {{ item.net | ipaddr('1') | ipaddr('address') }}

###### B-Side ######
vlan {{ item.vlan }}
 name {{ item.vlanname }}

interface Vlan {{ item.vlan }}
 description {{ item.vlanname }}
 mtu 9100
 vrf member {{ item.vrf }}
 no ip redirects
 ip address {{ item.net | ipaddr('3') }}
 hsrp version 2
 hsrp 1
 authentication md5 key-string password{{ item.vlan }}
 preempt delay minimum 120
 priority 110
 timers 1 3
 ip {{ item.net | ipaddr('1') | ipaddr('address') }}

In my setup, the play-book (aggr.yml), source template SVI.j2 file and the destination file exist in the same folder.

Reference Github.

Ansible – Encrypting Password

Basic Ansible automation playbook provides a method for accessing Cisco IOS devices and executing “show commands”. The “secrets.yml” file contains the username and password in plain-text. ansible-vault can be utilized to encrypt the “secrets.yml” file.

Encrypt a file using ansible-vault:

ansible-vault encrypt secrets.yml

View the contents of an encrypted file:

ansible-vault view secrets.yml

Decrypt a file using ansible-vault:

ansible-vault decrypt secrets.yml

Ansible – Basic Playbook

The goal of this post is to provide you with a simple way to utilize Ansible 2.x and obtain data from Cisco IOS devices by running “show” commands. Github Reference.

Ansible Installation:

Before starting, make sure you have ansible installed.

Create a working directory:

mkdir ansible_play
cd ansible_play

Create the following 4 files within the “ansible_play” directory:

ansible.cfg
hosts
secrets.yml
show_code.yml

Among the 4 files provided in this example, the “hosts” and “secrets.yml” file would have to be altered to suit your requirements. The other 2 files, “ansible.cfg” and “show_code.yml” can be used as-is.

The contents of the above files will be like this:

$ cat ansible.cfg

[defaults]
hostfile = ./hosts
host_key_checking=False
timeout = 5

For now, use the contents as provided above for the “ansible.cfg” file.

$ cat hosts

[ios]
switch1

The hosts file will have the switches/routers that you would like to use for running the ansible-playbook. In my case, I am using “switch1”. The 1st line “[ios]” is the name of the hosts that will be referenced in the ansible-playbook.

$ cat secrets.yml

---
creds:
  username: cisco
  password: ciscopassword
  auth_pass: ciscoauth

This file contains the login information required to access the devices in the hosts file.

$ cat show_code.yml

---
- hosts: ios
  gather_facts: no
  connection: local
 
  tasks:
  - name: OBTAIN LOGIN CREDENTIALS
    include_vars: secrets.yml
 
  - name: DEFINE PROVIDER
    set_fact:
      provider:
        host: "{{ inventory_hostname }}"
        username: "{{ creds['username'] }}"
        password: "{{ creds['password'] }}"
        auth_pass: "{{ creds['auth_pass'] }}"
 
  - name: SHOW VERSION
    ios_command:
      provider: "{{ provider }}"
      commands:
        - show version | i Version
    register: write
 
  - debug: var=write.stdout_lines

Executing the ansible playbook:

$ ansible-playbook show_code.yml

PLAY [ios] *********************************************************************

TASK [OBTAIN LOGIN CREDENTIALS] ************************************************
ok: [switch1]

TASK [DEFINE PROVIDER] *********************************************************
ok: [switch1]

TASK [SHOW VERSION] ************************************************************
ok: [switch1]

TASK [debug] *******************************************************************
ok: [switch1] => {
    "write.stdout_lines": [
        [
            "Cisco IOS Software, Catalyst 4500 L3 Switch  Software (cat4500e-ENTSERVICESK9-M), Version 15.2(2)E3, RELEASE SOFTWARE (fc3)"
        ]
    ]
}

PLAY RECAP *********************************************************************
switch1               : ok=4    changed=0    unreachable=0    failed=0

 

 

Ansible – Installation

If you have root access to your box, you can utilize the following link in order to install Ansible. I would recommend Ansible 2.1 & later version if your goal is to utilize Ansible as a Network Automation tool.

Creating a Virtual Environment with Ansible:

If you don’t have root access to the bastion host that is used to access the network infrastructure, you can utilize ansible in virtual environment.

pip install --upgrade pip virtualenv virtualenvwrapper
virtualenv ansible2.1
source ansible2.1/bin/activate
pip install ansible==2.2.1

Whenever required, the virtual environment can be accessed using:
source ansible2.1/bin/activate

root@ansible:~$ source ansible2.1/bin/activate

(ansible2.1)root@ansible:~$ ansible --version
ansible 2.1.0.0
  config file = 
  configured module search path = Default w/o overrides