No, But Why? - Install and Run Ansible for Network Devices

Install Ansible and use it to perform templated baselining on network devices.

Published: Sunday, October 22nd, 2023

Description

Starting from a mostly bare Linux OS, install Ansible, configure an inventory, create a Jinja template, and then baseline two devices.

Summary and Agenda

- Objective
- Install Terminology and Requirements
- Install Ansible
- Configure an Ansible Inventory
- Configuring a Jinja Configuration Template
- The Ansible Playbook
- Limitations
- Next Steps
- Conclusion

1.   Overview

1.1   Objective

This tutorial is written to provide step-by-step instructions to install and operate Ansible for the purpose of configuring network devices. These instructions were written using Arista devices, but most modern network OSes support Ansible through a CLI or SSH plugin. These instructions can be used with little or no modification for devices from non-Arista vendors.

1.2   What is Ansible?

Ansible is a toolset developed by Michael DeHaan that is now owned and operated by the Red Hat software company. The toolset is an open-source automation suite that can be used for configuration management, provisioning systems, or orchestrating complex infrastructure deployments.

Refer to the Ansible online documentation for further details on use for a specific network OS or system.

1.3   Install Terminology and Requirements

Ansible is operated from Linux distributions, but each distribution has some key differences when installing software packages. On Debian systems, the Advanced Package Tool (apt) is used for most package management. On RHEL-based systems, the Yellowdog Updater Modifier (yum) or Dandified YUM (dnf) are used instead. For the purposes of this document, package managers largely behave the same, so their differences will be ignored.

When installing features on Linux, repositories are online databases of available software and the dependencies that must be installed to enable specific software. Some Linux software is included in the operating system’s base repositories from the start. However, different distributions have different base repos, and those repos may or may not include the software necessary for a specific task. Adding repos is required to enable many non-standard features on a Linux operating system. On Red Hat Enterprise Linux, different subscriptions may also be required depending on the version of RHEL being used. On RHEL, the system must be enabled in the subscription manager, the subscription for specific software must be enabled, and the correct repository needs to be added.

On Windows, there are not any official supported binaries to run Ansible natively. Instead, Ansible can be run on Windows by emulating or virtualizing a Linux environment for the Ansible software suite. This can be achieved by using the Windows Subsystem for Linux or Cygwin.

2.   Technical Procedures

2.1   Install Ansible

2.1.1  Ansible can be used on most operating systems with a minimal amount of pre-configuration. Installing the software packages requires privileged access to a command line interface (CLI) to install Ansible itself and a recent version of Python. For users that already have Ansible and Python, you may be able to skip this chapter. However, this DTP does include guidance on several best business practices that can be helpful for technicians that are less familiar with the software.

2.1.2   Starting from scratch:  Boot up and access a modern operating system that can support the installation of Ansible and Python. You will need a functional internet connection for these steps. Open a CLI terminal for your OS from an account that can run the administrative permissions required for installing software. For Linux systems, continue to the next step. For Windows systems, use the following sub-steps to install the Windows Subsystem for Linux with Ubuntu.

2.1.2.1   WSL installation:  Windows 10, 11, Server 2019 and Server 2022 can run Linux as a nested virtual environment that has built-in access to the file system. This document will only provide the base process, refer to online documentation for in-depth steps and assistance.

2.1.2.2  On Windows 10, 11, and Server 2022, use the command wsl --install from an elevated Command Prompt or PowerShell terminal. On Server 2019 1709 or higher, use the command Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux – on Server 2019, you also have to install the Linux Distribution as per this link: https://learn.microsoft.com/en-us/windows/wsl/install-manual#downloading-distributions

2.1.2.3  Run the WSL distribution with the command wsl and configure a username and password if this is a first time install. This Linux account is separate from your Windows username and password.

2.1.2.4  By default, WSL has very open file permissions. Because of this, Ansible may error due to running from a world writable directory. Fix this by setting the umask for WSL on boot. With umask, we can set the default file permissions for the whole system. With a text editor like gedit, nano, or vim, use sudo to open the file /etc/wsl.conf e.g. sudo vi /etc/wsl.conf – this file may not exist yet, but it’s fine to create it as a new empty text document. Then, input the following lines:

[automount]
options="metadata,umask=0033"

2.1.2.5  Save the file and reboot your Windows machine. The next time WSL starts, it will use these settings to make Ansible operate as expected.

2.1.3   Environment Setup and Dependencies:  We want to setup Python and its modules so that Ansible has a fully operation environment for network configuration management. To begin, update the Linux distro with sudo apt update && apt upgrade -y for Debian (Astra, Raspbian, Ubuntu) or sudo dnf update -y for RHEL-variants (CentOS, Fedora) while using sudo yum update -y on RHEL-actual. Notes: On Debian systems, use "apt update" to make sure that the system has an up-to-date list of available packages on the enabled repositories. On RHEL, register and enable the Red Hat subscription to pull updates ref: https://www.redhat.com/sysadmin/rhel-subscription-manager

2.1.3.1  Install packages required for Python by using the following commands. On Debian systems: sudo apt install -y python3 python3-pip and test the install with the python3 --version command. On RHEL-forked distros use sudo dnf install -y python3 python3-pip with the same verification. For RHEL-actual, on RHEL7 use sudo subscription-manager-repos --enable rhel-7-server-optional-rpms --enable rhel-server-rhscl-7-rpms && yum install -y @development && yum install -y rh-python36 rh-python36-python-pip – on RHEL8 use sudo yum install -y python3 python3-pip – on RHEL9 use sudo dnf install -y python3 python3-pip

2.1.3.2  Download Python packages that are necessary for basic network device configuration management. We’ll be using PipEnv to control our development environment, Paramiko to enable SSHv2 connections, and Jinja2 to build templates. These package dependencies can be managed by using Python-Pip; it is the package manager for Python projects. First, install Pipenv so the later packages will be added into a purpose-built environment for Ansible. This will prevent our Python packages from having version conflicts for other projects.

2.1.3.3  Use pip install --user pipenv to install the PipEnv package under your logged-in user profile. This prevents conflicts that can arise from installing a Python package for the system or root user. Note that we did not use "sudo" on this command. Make sure to run non-sudo commands without privilege elevation. Next, update your environment variables with the command source ~/.profile to make sure the newly installed package is accessible via the CLI. The ".profile" file should be stored in your user home directory and is usually only loaded upon login.

2.1.3.4  Make a project file directory for this guide. This will be the base directory used by Ansible and Python going forward. Use the command mkdir ansible_for_networks && cd ./ansible_for_networks to make and move into the new directory. Then install the paramiko and jinja2 packages in the project environment by typing pipenv install paramiko jinja2

2.1.3.5  Test the new virtualenv by using the pipenv shell command. The directory name should be prepended to the shell prompt e.g. (ansible_for_networks) user@hostname:/home/user/ansible_for_networks$

Use the command exit to leave the virtual environment shell.

2.1.4   Installing Ansible:

On Debian distros: sudo apt install -y software-properties-common && sudo apt-add-repository --yes --update ppa:ansible/ansible && sudo apt install -y ansible to add the required install repository and then install Ansible itself.

For a RHEL-forked distro, use sudo dnf install -y epel-release && sudo dnf install -y ansible to install the Ansible package.

For RHEL-actual, on RHEL7 use sudo subscription-manager repos --enable rhel-7-server-ansible-2.9-rpms && sudo yum install -y ansible – on RHEL8 use sudo subscription-manager repos --enable ansible-2.9-for-rhel-8-x86_64-rpms && sudo yum install -y ansible – on RHEL9 just sudo dnf install -y ansible

2.2   Configure an Ansible Inventory

2.2.1   Create the Ansible configuration file:  To perform reliably, Ansible should be configured with some default settings that meet each project’s requirements. By default, Ansible looks in several places on an OS in an attempt to automatically determine key aspects of your system. However, we can set a file in the local project directory that overrides any of that to ensure consistent functionality that aligns with our current project. With a text editor like nano or vim, create the file "ansible.cfg". The following block of text includes commands to open and close vim after saving the file; pasting the entire block will not provide the "escape" character sequence. Comments are included to explain settings.

vi ansible.cfg
i
[defaults]
# Configure the inventory location where host details and variables will be found
inventory = ./inventory
# Set default output for running Ansible playbooks
interpreter_python = auto_silent
verbosity = 1
# Ignore new host key checking; to avoid doing this, update known_hosts properly
host_key_checking = False

# Disable 'requiretty' in /etc/sudoers to prevent conflicts caused by sudo operations
ANSIBLE_PIPELINING = True

# Set warnings and output configuration for per-host and per-variable responses by Ansible
retry_files_enabled = False
deprecation_warnings = False
system_warnings = True
command_warnings = False
error_on_undefined_vars = True
display_args_to_stdout = True
display_skipped_hosts = True
gather_subset = !hardware,!facter,!ohai

# Log the Ansible results somewhre
log_path = ansible_results.log

# Configure how SSH connections happen, primarily set timeouts and use SFTP for file transfers
[ssh_connection]
ssh_args = -C -o ControlMaster=no -o ControlPersist=60s -o ConnectTimeout=10
retries = 1
usetty = True
sftp_batch_mode = True
transfer_method = sftp

# Only make a change to configurations if there's a difference, prevents unneeded command signaling
[diff]
always = True
# TYPE ESCAPE - [ESC] key
# VIM command to write and quit; the colon : indicates a command, w for write, q for quit
:wq

2.2.2   Setting Up the File Structure:  Ansible tries to search through a default file structure for things like host information, variables, and templates. Again, it will look through various locations on the OS, but we’ll centralize all the files for our purposes in a very deliberate setup. This file structure can be represented as the following tree where "d" items are directories and "f" items are files with data in them.

    --d /home/user/ansible_for_networks
      --f baseline_devices_playbook.yaml
      --d inventory
        --f switch_devices.yaml
        --d group_vars
          --d devices_to_baseline
            --f vars.yaml
            --f vault.yaml
        --d host_vars
          --f bldg123_access_switch.yaml
          --f bldg500_leaf_switch.yaml
      --d templates
        --f switch_baseline.j2

We will create all of these files and directories as part of this guide. In Linux, create the folders with the "mkdir" command. We also want to make all subfolders, and we can do that by using the "-p" or "--parents" flag and using the command with an expanded path e.g. "./inventory/group_vars" versus a single item path e.g. "./group_vars"

Make sure that your current working directory is still "ansible_for_networks". Type in the following commands to create the reference files and directories:

  touch ./baseline_devices_playbook.yaml
  mkdir -p ./inventory/group_vars/devices_to_baseline
  touch ./inventory/switch_devices.yaml
  touch ./inventory/group_vars/devices_to_baseline/vars.yaml
  mkdir -p ./inventory/host_vars
  touch ./inventory/host_vars/bldg123_access_switch.yaml
  touch ./inventory/host_vars/bldg500_leaf_switch.yaml
  mkdir ./templates
  touch ./templates/switch_baseline.j2

Note, using the "touch" command only creates an empty file. We have to add our file contents over the next steps.

2.2.3   Configuring Hosts.  In Ansible, the inventory is where information is stored about your network and systems environment. With plugins and prestaging, you could build thousands of devices into different host, group, and variable files for automation. For this exercise, we’ll build out a "./inventory/switch_devices.yaml" file with a list of devices we want to configure. Then, we’ll define a separate variable file for each device as "./host_vars/item_name.yaml" to track individual device settings. Finally, we’ll put shared variables in the "./group_vars/devices_to_baseline/vars.yaml" file.

Let’s start with the "./inventory/switch_devices.yaml" file. The filename could be whatever you want, we just have to reference its data from the playbook when we get to that point. In this case, we’re also setting it up as a "yaml" file which is just a data serialization storage format. Use the following configuration text as the reference for building this file. Use a text editor to add these lines into the file we created earlier. The root working directory is still "ansible_for_networks". Comments are included to explain relevant lines.

# A group of variables can be labeled with square brackets and a name
# Below, the all:vars tells Ansible that the variables will be used for all devices
[all:vars]
# This guide is only concerned with network devices, so set the connection for a network_cli
ansible_connection=network_cli

# Define a group of hosts under the name ‘devices_to_baseline’
# In the Ansible playbook, we will refer to this group name to set a default templated baseline.
# The playbook will only operate on the hosts specified by its configuration
[devices_to_baseline]
# Hosts can be defined as an IP, a resolveable hostname, or an alias with an IP / hostname
#
# The following are valid entries:
#	192.168.15.2
#	my_device.lab_environment.localhost
#	my_local_pc ansible_host=127.0.0.1
#
# In this case, we are adding two hosts with aliases. The first string is the alias.
# e.g. with "ansible_host", we assign an IP address to the alias "bldg123_access_switch"
bldg123_access_switch ansible_host=172.28.176.150
bldg500_leaf_switch ansible_host=172.28.176.160

Saving that file, next let’s build the "./inventory/group_vars" files. For this file, we’re defining variables that will be shared for all or most of the devices being managed by the playbook. This could be a group from a specific vendor, different hardware types, or devices that will fulfill a shared function like Layer2 or Leaf switches. In this case, we’re defining access and leaf switches that will have common VLANs. These variables specify that we'll be using Arista EOS devices and we'll use "ansible_become" to elevate our privileges with the enable command. We’ll use the username "admin" and add a predefined set of VLANs onto each relevant device.

This file also introduces a line with an Ansible variable. The line with "ansible_ssh_pass" is followed by the value "{{ base_provision_password }}". This is how Ansible references a pre-defined variable that is pulled from its configuration or somewhere in the inventory. In this case, that variable will be defined in an Ansible vault. Ansible vaults allow us to store sensitive data in an encrypted file. This is a best practice as we should never store passwords or private API keys in plain text. After this file, we will be creating the Ansible vault for the devices_to_baseline group.

Open "./inventory/group_vars/devices_to_baseline/vars.yaml" and include the following:

---
# Variables for Provisioning Arista EOS devices
ansible_network_os: eos
ansible_become: yes
ansible_become_method: enable
ansible_user: admin
ansible_ssh_pass: "{{ base_provision_password }}"

vlans:
  - id: 10
    name: "VOICE"
  - id: 50
    name: "DATA"
  - id: 100
    name: "SERVICES"
  - id: 124
    name: "PROVISIONING"
  - id: 200
    name: "LAB"
  - id: 600
    name: "IN_BAND_MANAGEMENT"
  - id: 1000
    name: "UNUSED_SINKHOLE"
  - id: 4094
    name: "LACP_MLAG"

Next up, let’s create the vault for that group’s password. This uses the command "ansible-vault" to create a file with a password. For the purpose of this guide, we’ll use the password ‘admin’ to secure the vault and as the base provisioning password.

Open the file with the following command and add a password:
ansible-vault create ./inventory/group_vars/switch_shared_defaults/vault.yaml

Add the following text into the file, save the file, and then close the vault:

      base_provision_password: admin
      

Moving on to the next files, we’re going to create our individual host variable files. These contain unique values that are specific to individual devices. These would be configuration items like management IPs, hostnames, Loopback addresses, or a unique BGP AS. Note, as we’re building our Ansible inventory, keep in mind that variables won’t be used automatically. Each of these files can serve as the source data for a template or playbook, but we have to reference the variables explicitly for that to happen.

That said, create the following files in the "./inventory/host_vars" directory. We’ll start with "./inventory/host_vars/bldg123_access_switch.yaml". Open this file for editing and add the following lines:

# host_vars/bldg123_access_switch.yaml (this is a yaml vars file for Ansible)
---
interfaces:
  - name: "Management1"
    enable: yes
    description: "Uplink to OOB_MGMT_SW ; to G1/0/1 ; OOB Device Management"
    layer3_port: yes
    ip_addr: "172.28.176.150 255.255.255.0"
  - name: "Ethernet1"
    enable: yes
    description: "User A Access Port ; to NIC_001 ; Data VLAN"
    vlan_mode: "untagged"
    vlan_id: "50"
    include_voice: yes
    voice_id: "10"
  - name: "Ethernet2"
    enable: yes
    description: "Webserver Access Port ; to NIC_003 ; Services VLAN"
    vlan_mode: "untagged"
    vlan_id: "50"
  - name: "Ethernet3"
    enable: yes
    description: "Uplink to bldg123_distro_switch ; to G1/0/50 ; 802.1Q Tagging VLANs"
    vlan_mode: "tagged"
    vlan_id: "10,50,100,124,200,600,4094"
  - name: "Vlan600"
    enable: yes
    layer3_port: yes
    description: "In-band VLAN for remote access and management"
    ip_addr: "10.10.10.1 255.255.255.240"

Now do the same for the second switch. Open the file "./inventory/host_vars/bldg500_leaf_switch.yaml" and add the following lines:

# host_vars/bldg500_leaf_switch.yaml (this is a yaml vars file for Ansible)
---
interfaces:
  - name: "GigabitEthernet1/0/1"
    enable: yes
    description: "P2P Link to Spine-01 ; to G1/0/5 at 10.50.10.2 ; BGP Peer"
    layer3_port: yes
    ip_addr: "10.50.10.1 255.255.255.252"
    mtu: 9214
  - name: "GigabitEthernet1/0/2"
    enable: yes
    description: "P2P Link to Spine-02 ; to G1/0/5 at 10.50.10.6 ; BGP Peer"
    layer3_port: yes
    ip_addr: "10.50.10.5 255.255.255.252"
    mtu: 9214
  - name: "GigabitEthernet1/0/3"
    enable: yes
    description: "P2P Link to Spine-03 ; to G1/0/5 at 10.20.10.10 ; BGP Peer"
    layer3_port: yes
    ip_addr: "10.50.10.9 255.255.255.252"
    mtu: 9214
  - name: "GigabitEthernet2/0/1"
    enable: yes
    description: "Webserver Access Port ; to NIC_001 ; Services VLAN"
    vlan_mode: "untagged"
    vlan_id: "50"
  - name: "Loopback0"
    enable: yes
    description: "Router ID and Multicast Source / RP"
    layer3_port: yes
    ip_addr: "192.168.30.5 255.255.255.255"
enable_routing: yes
enable_bgp: yes
bgp_as: "65020"
bgp_id: "192.168.30.5"

After editing and saving both host_var files, we’ve staged example variables that are needed for individualized templating of our devices. Each of these files can be replicated and edited to include the details for an entire campus or data center of devices. This creates a baseline templating system that doesn’t require manual changes to whole configs. Instead, Ansible will insert the appropriate individual details by using a template that references those values.

2.3   Configuring a Jinja Configuration Template

2.3.1   A Brief Overview of Jinja:  Jinja is a templating engine that was built for the Python language. Developed around 2008, it is widely used for everything from building web pages to generate device configuration files. The engine allows for variable processing, conditional statement evaluation, and loops to iterate through lists and dictionaries. Anyone familiar with Python should have an easy experience picking up Jinja and creating custom templates.

2.3.1.1  Writing a template is as easy as pasting in some known good text and using variable calls to inject custom values. For a network device, consider the following example: "hostname {{ device.hostname }}". Though all this does is set the device hostname, this is a valid Jinja template. When paired with Ansible, this template will be loaded and Ansible will use data from the inventory in an attempt to apply the referenced variable "device.hostname".

2.3.1.2  Logic capabilities make Jinja templates extremely flexible and powerful. We can adjust settings based on data from a target device, use pre-staged variables to adjust template output for multiple hosts, or send a repetitive command by looping through a list of different values. Using most of Python’s standard syntax, Jinja identifies logic evaluation by surrounding them with the brackets "{% evaluate %}". Without these brackets, Jinja will output exactly what’s written in the template. As an example, we could write an if / else statement like so:

  {% if item.enable | default(false) %}
      no shutdown
  {% else %}
      shutdown
  {% endif %}

In this example, first we test if the variable "item.enable" is set to yes or no or true or false. Using the pipe, we’ve also explicitly defined that Jinja should consider this value as false if the variable doesn’t exist. If "item.enable" is true, Jinja will output the value "no shutdown" into the template. Otherwise, the value will be "shutdown" and the port will be deactivated. A default value will allow for dynamic values without having to explicitly define them every time. Defaults also prevent Ansible from failing due to errors from undefined variables. Whenever scripting out variables, it is best practice to plan for improperly set values. Always validate a variable’s value or, at a minimum, create a logic flow that can handle unexpected values.

2.3.1.3  Loops make it easy to create templates that include repetitive configuration items that only have a few small changes between each item. On network devices, loops are especially useful for things like interfaces, VLANs, or access lists / firewall filters. The interfaces defined in our host_vars files are a great example of using loops. In the Jinja template, we’ll use the "interfaces" object variable and pass the data into a loop like so:

  {% for item in hostvars[inventory_hostname].interfaces %}
      interface {{ item.name }}
      {% if item.description is defined %}
          description {{ item.description }}
      {% endif %}

      {% if item.layer3_port | default(false) %}
          no switchport
      {% endif %}

      {% if item.ip_addr is defined %}
          ip address {{ item.ip_addr }}
      {% endif %}
  {% endfor %}

On the top line, we tell Jinja to use a variable from the hostvars collected by Ansible from inventory values. The "inventory_hostname" is a variable referencing the specific hostname of the current device being evaluated by Ansible. Therefore, "hostvars[inventory_hostname].interfaces" passes the Python dictionary object from one of our two host_vars files into the for loop. Then, Jinja outputs configuration lines based on the values for each interface defined in that file. Whether there are two or twenty interfaces, Jinja will output lines for as many as are defined.

2.3.2   Configuring a Jinja Baseline Template:  With those explanations out of the way, let’s define a Jinja template that can baseline network switches. Open the file "./templates/switch_baseline.j2" and enter the following lines:

#jinja2: lstrip_blocks: "True"
! Generated from templates/switch_baseline.j2
{% for vlan in vlans %}
vlan {{ vlan.id }}
  {% if vlan.name is defined %}
 name {{ vlan.name }}
  {% endif %}
!
{% endfor %}
{% for item in hostvars[inventory_hostname].interfaces %}
interface {{ item.name }}
  {% if item.description is defined %}
 description {{ item.description }}
  {% endif %}
!
  {% if item.layer3_port | default(false) %}
 no switchport 
    {% if item.ip_addr is defined %}
 ip address {{ item.ip_addr }}
    {% endif %}
    {% if item.mtu is defined %}
 mtu {{ item.mtu }}
    {% endif %}
  {% else %}
    {% if item.vlan_mode is defined %}
      {% if item.vlan_mode == "untagged" %}
 switchport mode access
 switchport access vlan {{ item.vlan_id }}
      {% endif %}

      {% if item.vlan_mode == "tagged" %}
 switchport mode trunk
 switchport trunk allow vlan {{ item.vlan_id }}
      {% endif %}
      {% if item.include_voice | default(false) %}
 switchport voice vlan {{ item.vlan_id }}
      {% endif %}
    {% endif %}
  {% endif %}
  {% if item.enable | default(false) %}
 no shutdown
  {% else %}
 shutdown
  {% endif %}
!
{% endfor %}
!
{% if enable_routing | default(false) %}
ip routing
  {% if enable_bgp | default(false) %}
    {% if bgp_as is defined %}
router bgp {{ bgp_as }}
       {% if bgp_id is defined %}
 router-id {{ bgp_id }}
      {% endif %}
    {% endif %}
  {% endif %}
{% endif %}

As an overview, these lines instruct Jinja to create any VLANs defined for each device and any interfaces defined as well. For VLANs, the template outputs a configuration line to create and name each VLAN. For the interfaces, the template sets them as a Layer3, tagged, or untagged interface. Then it sets a description, IP address, and VLAN assignments as necessary and if defined. It also shuts or no shuts the ports based on the enable variable. Finally, there's a check to see if the device should have Layer3 routing enabled. If so, it will also check to see if it should enable BGP; additional routing protocols could be added by replicating these BGP portions. Template defined, it’s time to create a playbook.

2.4   The Ansible Playbook

2.4.1   Introduction to Playbooks:  Every Ansible playbook can be defined for a specific purpose or for broad and generalized configuration procedures. In this guide, we’re defining a playbook that will configure devices to a known-good baseline. Such a playbook could be used to provision new switches or to return fielded devices to an expected state.

With a playbook, we could just manually define each configuration line in the base file without using any variables or templates. However, that means having hundreds of repeated configuration lines for each device or group of devices. That would require manually changing each file for each device. Variables and templates allow us to make one or two changes in a way that will affect all devices. With a well-planned inventory, this method also makes it easy to provision dozens or hundreds of new devices on the fly and without changing more than a few lines per system.

2.4.2   Defining a Playbook:   A playbook consists of three main items to work: what devices are we working with, what tasks will be performed for those devices, and what data will Ansible use to fulfill those tasks. In our inventory, we created the file "./inventory/switch_devices.yaml". This file tells Ansible to connect to these devices using the "network_cli" plugin and defines a group of devices under the "devices_to_baseline" group name. We can tell Ansible to operate on that entire group and it will iterate through each device hostname and IP. It will use files in the group_vars directory that match this group name, and it will use files in the host_vars directory to match the hostname, IP, or alias for each device in each group. Knowing that, let’s define the playbook by editing the file "./baseline_devices_playbook.yaml". Reference the following configuration lines:

# A human-readable name for descriptive purposes
- name: Baseline Arista Network Devices
  # Tell ansible which hosts / devices to run this playbook for
  hosts: devices_to_baseline
  # For this guide, we are not going to query information about the environment or remote devices
  gather_facts: no

  # Run the following tasks for each device in the "devices_to_baseline" group
  tasks:
    # Tasks should have a descriptive name that summarizes the action to be performed
    - name: Configure hostname
      # We can run direct commands on a device with plugins like ios_config or eos_config
      # Commands must follow Network OS syntax and can use any variables pulled by Ansible
      eos_config:
        lines:
          # Change the device hostname
          - "hostname {{ inventory_hostname }}"
    - name: Configure the device with a generalized baseline template
      # Ansible will attempt to automatically pull data from all applicable sources, but
      # we can also explicitly define the variables to use 
      vars:
        group_data: "{{ groups['devices_to_baseline'] }}"
      eos_config:
        # Tell Ansible to run the Jinja template we defined for each device
        src: templates/switch_baseline.j2

2.4.3   Staging Devices:  At this point, we need to make sure our network devices are staged and reachable. To operate on a device, Ansible needs to be able to connect over SSH with pre-staged credentials. At a minimum, that means configuring an IP address, SSH credentials, and SSH settings. With the right setup, those settings could be staged using some form of zero touch provisioning. For this guide, we’ll paste in the changes manually.

2.4.3.1  This guide is built with the following lab setup. However, IP Addresses, usernames, passwords, and port designations should reflect the requirements of your environment. Make sure to change any templates or variables to match the setup being used relevant to your systems. Also ensure that the host where Ansible is installed is on the same network as the devices to be managed.

Figure 01 - Lab Network Diagram

2.4.3.2  Use the following configuration as a reference for enabling remote access on both devices. Log into the devices via a console connection, enable, and enter global configuration mode with the configuration terminal command. Modify and then input the configuration lines as required.

      username admin privilege 15 secret admin
      !
      ! Uncomment the next line for Cisco devices
      ! aaa new-model
      aaa authentication login default local
      aaa authorization exec default local
      ! Cisco console authorization
      ! aaa authorization console
      ! Arista console authorization
      aaa authorization serial-console
      !
      !
      ! Cisco domain name
      ! ip domain-name lab.local
      ! Arista domain name
      dns domain lab.local
      !
      ! Cisco SSH key generation for legacy devices
      ! crypto key generate rsa modulus 2048
      !
      interface Management1
       description Uplink to OOB_MGMT_SW ; to G1/0/X ; OOB Device Management
      ! Update the IP address to reflect the device as required
       ip address 172.28.176.1XX 255.255.255.0
       no shutdown
      !
      ! Cisco extended ACL
      ! ip access-list extended SSH
      ! Arista named ACL
      ip access-list SSH
       permit tcp 172.28.176.0 0.0.0.255 any eq 22 log
       deny   ip any any log
      !
      ! Cisco line protections
      !line con 0
      ! exec-timeout 5 0
      ! logging synchronous
      ! transport output none
      !line aux 0
      ! transport output none
      !line vty 0 4
      ! access-class SSH in
      ! exec-timeout 5 0
      ! logging synchronous
      ! transport input ssh
      ! transport output ssh
      !line vty 5 1500
      ! access-class SSH in
      ! logging synchronous
      ! transport input none
      ! transport output none
      !
      ! Arista line protections
      management console
       idle-timeout 5
      management ssh
       idle-timeout 5
       ip access-group SSH in
       connection limit 5
      

2.4.4   Running the Playbook:  Once it’s defined, we can immediately run the playbook. The base command is "ansible-playbook -i inventory playbook_file.yaml". This tells Ansible to reach out to the devices defined in our inventory and change their configurations based on any templates and variables required by that playbook. However, we have a few additional requirements to consider.

2.4.4.1  First, let’s test the playbook by using the "dry run" feature of Ansible. Enable this feature by passing the "--check" command flag from the command line. Also, because we defined a vault, pass the "--ask-vault-pass" command flag to get Ansible to prompt for that password. Start our pipenv with the command:

pipenv shell

Next, run this command for our playbook like so:

ansible-playbook -i inventory baseline_devices_playbook.yaml --check --ask-vault-pass

If all goes well, Ansible will output the changes it will make and whether it connected successfully to each device. It will also provide a "PLAY RECAP" that summarizes the number of devices connected, how many changes were made, and the number of errors.

Figure 02 - Summary Results from an Ansible Dry Run

2.4.4.2  During the dry run phase, double-check the output for each device. This is a good time to validate that the right things will be changed. In particular, operating on remote devices means a playbook could disconnect you from the system you’re managing. Take this time to correct any errors, troubleshoot template misconfigurations, or validate settings like IPs and port names. However, if all looks as it should, run the actual operation with:

ansible-playbook -i inventory baseline_devices_playbook.yaml --ask-vault-pass

2.4.4.3  Ansible will go through the inventory devices and implement the changes or each device. At this point, the general setup and implementation is complete. These same processes can be used to make additional playbooks, hosts, variables, and templates.

3   Wrapping Up

3.1   Limitations

As noted earlier, using Ansible does require connectivity to the devices in the first place. In a way, this might feel like it defeats the purpose of automation. After all, if you have to manually setup the device, you might as well paste in a good config at the time of provisioning. However, devices still need maintenance and upkeep after provisioning. New circuits need to be added, user ports will change, and security measure have to be adjusted. Plus, tools like Ansible can make replacing a device easier by allowing for small changes to variables rather than whole baseline configuration files.

Additionally, automation tools like Ansible can be tied into zero touch provisioning capabilities for a truly automated experience. Most modern network hardware has ZTP enabled at some level, so building in that automatic boot setup would be the first step to making playbooks even more valuable.

3.2   Next Steps

When building network automation capabilities, think through the lifecycle of an organization’s network hardware. As an example, how would we fully automate replacing the equipment for an entire campus and its supporting data center? First, the systems could be staged in racks and connected through an OOB network. Next, a ZTP process would provision these devices for remote connectivity. After that, Ansible could finalize each device with predefined templates aligned to purposes and locations.

Finally, the devices would be pulled into a network manager or SDN controller to enable overlay traffic. Once everything reports in and the network converges, swap over users and services to the new infrastructure. Spend time on toolsets that enable this kind of holistic approach to the organization’s network and it will pay dividends in saved time, money, and resources.

Ideally, we should also look for ways to operationalize the use of tools like Ansible. In essence, how do we prove the value of tweaking technical buttons and knobs? To do that, our efforts have to enable monitoring, reporting, and security. That may be through integrations with network vendor suites, Red Hat’s Ansible Tower, or we could develop custom packages built with a GUI or web stack. That’s the way to gather metrics on the benefits of our automation efforts.

A toolset that operates in a vacuum is doomed to be ignored and replaced. Leadership has to buy into technical developments to guarantee your focus and prioritization on those developments. It’s easy to get absorbed into fiddling and making the next cool thing, but make sure to take some time to showcase what that means to the organization.

3.3   Conclusion

This guide was written to provide a well-rounded intro to setting up Ansible, creating an inventory, and running playbooks with basic variables and vaults. The intent was to start from the ground floor and move all the way up to a working templated playbook. With these basics, even basic modifications can provide nearly-infinite combinations for managing networks and systems. Hopefully, it’s enough to get processes started to make everyday life just a little bit easier. Thanks for reading. 


References

Gonzalez, Noe (2022, April 26). How to Install Ansible with pipenv and pyenv. Retrieved on October 15, 2023 from https://www.buildahomelab.com/2022/04/26/how-to-install-ansible-with-pipenv-pyenv/

JetBrains (2023, September 27). Configure a pipenv environment. Retrieved on October 8, 2023 from https://www.jetbrains.com/help/pycharm/pipenv.html

Python Software Foundation (2022, April 28). Jinja2 3.1.2 - A very fast and expressive template engine. Retrieved on October 11, 2023 from https://pypi.org/project/Jinja2/

Red Hat (2021, September 15). What is Ansible. Retrieved on October 3, 2023 from https://www.redhat.com/en/technologies/management/ansible/what-is-ansible

Rometsch, Ben (2021, February 16). How Ansible got started and grew. Retrieved on October 11, 2023 from https://opensource.com/article/21/2/ansible-origin-story