Using and Customizing Arista EOS Roles for Ansible

The Ansible automation framework includes functionality defined as a role – a means of grouping playbook tasks, handlers, and variable files to help simplify the process of working with large playbooks, as well as reusing playbook information for multiple configurations. This article will describe the use of Arista EOS Roles for Ansible, beginning with a basic overview of Ansible Roles, then installing and working with Arista EOS roles, and concluding with a more in-depth look at customizing those roles for your specific needs.

The Basics

This article assumes you are familiar with Ansible and that Ansible version 2.1 or greater is installed and configured in your environment. Using Ansible with Arista switches has been covered in a Getting Started article on EOS Central. If you are new to Ansible or using Ansible for Arista switch configuration management, the article above is a great place to start, describing the connection between Ansible and Arista EOS, as well as the basic functionality of Ansible playbooks in switch configuration management. The Ansible 2.1 release introduced core modules designed to work with Arista EOS, and the Arista roles and examples in this article make use of those modules. When working with Arista switch configurations, an Ansible 2.1 release or later is strongly suggested.

Using Ansible playbooks for switch configuration management, however, can become a daunting task when multiple switches and configurations are concerned. The concept of Roles in Ansible allows for reusing configuration templates which can greatly simplify the setup and management of multiple configurations.

Ansible Roles

A role in Ansible is simply a predefined file structure that organizes the process of importing tasks, handlers, and variables into a playbook. A playbook can include one or more roles, and the file structure from those roles will be imported to the playbook during execution.

An overview of Ansible roles is available in the Ansible documentation.

Arista EOS Roles for Ansible

The Arista EOS+ development team has created several roles for working with Arista EOS switch configs. These roles can be found through Ansible Galaxy, and installed with the ansible-galaxy command.

As of this writing, the following roles exist:

  • eos-acl – configure a limited set of ACL types and configurations
  • eos-bgp – configure BGP router, timers, neighbors, networks and listeners
  • eos-bridging – configure EOS switchports and vlans
  • eos-interfaces – configure the basics of any interface
  • eos-ipv4 – configure the IP address and other information for a layer 3 interface
  • eos-mlag – configure common MLAG parameters
  • eos-route-control – configure route-maps and IPv4 static routes
  • eos-system – configure IP routing, hostname, and CLI users
  • eos-virtual-router – configure VARP-related information
  • eos-vxlan – configure logical Vxlan interfaces, Vlan to VNI mapping, and VTEP flood lists

Additional roles, as they are created, will be available through Ansible Galaxy.

Role Installation

Installation of an Ansible role is as easy as typing

ansible-galaxy install <role_name>

where role_name is the owner.role format of the role as shown in the Ansible Galaxy online portal.

So, for example, to install the Arista eos-system role, issue the command

ansible-galaxy install arista.eos-system

Note that currently Arista roles may be shown as either ‘arista’ or ‘arista-eosplus’ being the owner, so you will need to be aware of which owner name was assigned to each role.

Role Usage

As a starting point, each Arista EOS Role for Ansible contains a README in the top level directory which explains the usage of the role, and the parameters that may be passed into the role. For the purpose of this article, we will use examples from the eos-system role we installed earlier.

For ease of implementation, we will consider all files and directories to be in the same parent directory. Although Ansible is able to handle various location paths, it is often simplest to keep all playbooks and associated files in a single path, e.g. /parent/path/hosts, /parent/path/group_vars/<files>, /parent/path/host_vars/<files>, and <playbook_files>.yml.

First, we make sure we have our hosts file configured properly:

[leafs]
leaf1.example.com
leaf2.example.com

[spines]
spine1.example.com

Next, we instantiate a host_vars file for each host we will be working with:

group_vars/all

provider:
  host: "{{ inventory_hostname }}"
  username: admin
  username: admin
  use_ssl: no
  authorize: yes
  transport: cli

eos_users:
  - name: superadmin
    encryption: md5
    secret: '$1$J0auuPhz$Pkr5NnHssW.Jqlk17Ylpk0'
    privilege: 15
    role: network-admin
  - name: simplebob
    nopassword: true
    privilege: 0
    role: network-operator

eos_ip_routing_enabled: yes

host_vars/leaf1.example.com

hostname: leaf1

host_vars/leaf2.example.com

hostname: leaf2

host_vars/spine1.example.com

hostname: spine1

The above configuration files define the expected configuration for the various switches in our inventory. In the example, we have three switches: spine1, leaf1, and leaf2. The group_vars/all file specifies configuration information for all the hosts in the inventory. Each will have the same connection information (the provider key, which tells Ansible how to connect to the switch), and each switch will be configured with the same two users and with ip routing enabled. Each individual host_vars file, named after each host, specifies configuration information that is specific to that individual switch.

If desired, we could also have specified a group specific configuration under the group_vars directory, such as a user that should only be on all spine nodes and another user that should only be on all leaf nodes:

group_vars/spines

eos_users:
  - name: spineuser
    secret: spineuser
    privilege: 10
    role: network-operator

group_vars/leafs

eos_users:
  - name: leafuser
    secret: leafuser
    privilege: 10
    role: network-operator

The naming of the group_vars files should match the groups defined in the hosts file. Finally, we construct a simple playbook that will execute the Arista eos-system role using the above configuration files: all_hosts.yml

- hosts: leafs:spines
  roles:
    - arista.eos-system

The hosts line specifies to use all hosts from both the leafs and spines groups. We could have specified

- hosts: *
  roles:
    - arista.eos-system

or

- hosts: all
  roles:
    - arista.eos-system

which both mean ‘all devices in the hosts file’. Alternatively, we may also create separate playbooks for the spine nodes and the leaf nodes: spines.yml

- hosts: spines
  roles:
    - arista.eos-system

leafs.yml

- hosts: leafs
  roles:
    - arista.eos-system

This would allow us to run the Arista eos-system role against only the spine nodes or only the leaf nodes, as may be necessary.

We can run each of the playbooks with the following commands:

ansible-playbook -i hosts all_hosts.yml
ansible-playbook -i hosts spines.yml
ansible-playbook -i hosts leafs.yml

Role Customization

Now we come to the fun part – customizing the Arista EOS roles to your specific needs.

In many cases, the existing Arista EOS Roles for Ansible will be enough to meet the needs of a basic configuration setup. However, there may come a point when the standard configuration templates provided by a specific role may not provide the level of configuration needed for your environment. At this point, additional functionality can be easily implemented into an existing role or a completely separate role may be created if desired.

Basic Structure of a Role

Ansible roles are a predefined structure of files that center around a set of tasks, templates, and handlers. The tasks define the steps that a role will take upon the devices that are defined by the hosts file. These tasks work in the same way as individual tasks that can be defined by a single playbook, and the tasks are imported into the playbook by the role. Templates are jinja2 formatted scripts that can manipulate input data into a form that is usable by the Ansible playbook. In the case of the Arista EOS roles, the template files are used to generate a formatted configuration block that can be passed into the Ansible core module eos_template. Ansible handlers are tasks that are triggered at the completion of a playbook based on whether the tasks that notify the handlers produced a change. Regardless of the number of times a handler is notified during a playbook, each handler will only run once after the playbook has completed. For the Arista EOS roles, the common handler executes a task that will save the changed running-config of a switch to the startup-config for that switch if the user has not explicitly specified to not do so.

Ansible roles also include a filter_plugin mechanism that enables us to create our own jinja2 type filters that can be used within the role tasks or in the template files used by the role. The Arista EOS roles all include a filter_plugin file named config_block, which provides filters for extracting a section of an Arista EOS configuration based on a block header that identifies the block, such as interface Ethernet1, which would return the indented block of the configuration bounded by the header interface Ethernet1. Also in the config_block filter_plugin file are filters for searching a configuration or block for a line matching a regex string, or all matching lines.

Finally, the Arista EOS roles may contain a defaults file (defaults/main.yml) which specifies default values for variables used by the role, to be used when the playbook calling the role does not specify a value for those variables. Not all variables available in the role require a default value, and in some cases, a default should not be specified to facilitate conditional task execution (skipping a task if a give variable is not defined).

Now that we have an overview of our file structure, let’s take a look at an example of customizing a role.

Customizing an Arista EOS Role for Ansible

For our example, we will be customizing the Arista eos-acl role to include some features that are currently not implemented. These features are highly improbable, and are for the sake of this article only, but are designed to give an overview of the various customizations that may be implemented for a role.

If you have not done so, you might wish to install the Arista eos-acl role in your Ansible workspace and follow along with the examples. The role is available through Ansible Galaxy as arista-eosplus.eos-acl, and may be installed with the ansible-galaxy command line tool as described above.

Getting the Playbook Ready

Let’s create a simple playbook, example.yaml:

- hosts: eos_switches
  gather_facts: no
  connection: local
  roles:
    - arista-eosplus.eos-acl

And a hosts file to match:

[eos_switches]
arista_sw1
arista_sw2

Next we’ll need a way to connect to the switches, defined in group_vars/eos_switches.yaml:

---
ansible_connection: local

provider:
  host: "{{ inventory_hostname }}"
  username: admin
  password: admin
  use_ssl: no
  authorize: yes
  transport: cli

And finally, we need to have an ACL configuration to pass along to the switches, one for each switch. First host_vars/arista_sw1:

acls:
  - name: ACL-1
    action: permit
    seqno: 10
    description: Adds rule 10 as a host address
    type: standard
    srcaddr: 10.11.12.13
    log: false

And then host_vars/arista_sw2:

acls:
  - name: ACL-2
    action: permit
    seqno: 10
    description: Adds rule 10 as a host address
    type: standard
    srcaddr: 10.11.12.14
    log: false

Now, when we run our playbook, the ACLs should be added to the switches.

arista_sw1 before:

arista_sw1(config)#show running-config all section ACL-1
arista_sw1(config)#

Run the playbook:

ansible-playbook -i hosts example.yaml

Check the config on arista_sw1:

arista_sw1(config)#show running-config all section ACL-1
ip access-list standard ACL-1
   no statistics per-entry
   fragment-rules
   10 permit host 10.11.12.13
arista_sw1(config)#

Excellent! Just what we wanted. And if we check arista_sw2, we will see a similar ACL entry, but named ACL-2 and with the IP address 10.11.12.14.

So what do we do if we want to put some custom information into that ACL configuration? Maybe we want to put a remark into each ACL that is configured by our role. The eos-acl role does not implement the remark ACL configuration command, but we can easily customize our role to handle it. We’ll look at two different ways to do this.

Add a Fixed Value Configuration Line

First, suppose we simply want to add a remark to all ACLs that have been configured with the Arista EOS role. We want to put the same remark in every ACL, just to show everyone how efficient we can be.

So, in our copy of the role (found in <ansible_role_path>/arista-eosplus.eos-acl), let’s open up the file templates/acl.j2. Remember that the template files use input data to build a configuration block that will be applied to our switches by the Ansible core module eos_template.

At the top of the acl.j2 file are a few lines of Jinja2 environment initialization commands, followed by several lines of variable assignment for the template itself. About 1/3 of the way into the file we see the beginning of an if..then block that tests the status of state. The variable state is used in most Arista EOS roles to indicate whether a section we are processing is to be present on the switch (add or update the configuration block as necessary) or absent from the switch (remove the configuration block or reset it to its default values). In the acl.j2 template, you will see something like the following

! templates/acl.j2

....
....

{% if state == 'absent' %} {# remove the acl if it exists #}

   {% if acl_block %}
      ....
      ....

{% elif state == 'present' %}

ip access-list {{ type }} {{ name }}

   {% if acl_block %}
      {# acl defined, update/add rule by removing all matching entries #}
      {% set match = acl_block | join('\n') | re_findall("^(\d+)\s*%s %s%s" % (action, netaddr, log_rule)) %}
      {% if match %}
         {% for entry in match %}
            {# only remove entries that do not match seqno. if seqno matches
            {# the requested seqno, then the entry exists and we do not
            {# want to remove it #}
            {% if entry != "%s" % seqno %}

   no {{ entry }}

            {% endif %} {# seqno does not match #}
         {% endfor %} {# entry in match #}
      {% endif %} {# match #}

      {# now remove any entry with the same seqno that does not match the full requested entry #}
      {% set match = acl_block | join('\n') | re_search("^%s .*" % seqno) %}
      {% if match and match.group(0) != rule %}

   no {{ seqno }}

      {% endif %} {# match #}

   {% endif %} {# acl_block #}
   {# finally, add the rule and exit #}

   {{ rule }}
   exit

{% endif %}

We are interested in the section of the if..else..then block that deals with the present state of ACLs. Before we customize the template with our remark line, let’s take a look at what is happening in that block of code.

Jinja2 uses brace-percent delimiters, {% … %}, for parts of the template that are executable or control operations. Brace-hash delimiters, {# … #}, represent comments, and can be placed at the end of a line and may span multiple lines. Any line that is not completely surrounded by braces is an output line. In our roles, these are the lines that produce the configuration block we will pass into Ansible’s eos_template module. Variables in output lines are represented by surrounding them with double braces.

Once we are in this block, our first operation is to output the block header line for the ACL. The line

ip access-list {{ type }} {{ name }}

will substitute the type and name of our ACL where shown, and output the line

ip access-list standard ACL-1

for our arista_sw1 device.

The next two sections of the block check if our switch already has an ACL block with the defined name, and if so we examine our requested ACL entry and remove any duplication from the existing ACL block. We also remove an existing sequence number from the block if that sequence number rule does not exactly match our requested rule.

Finally, just before the end of the file, we output our rule and an ‘exit’ command (to commit the changes to the running-config). So for the arista_sw1 device we defined for our playbook, the output will be

   10 permit 10.11.12.13
   exit

Notice the indentation for those two commands. The Ansible eos_template module is very particular when it comes to indentation. The indentation must match the indentation produced by the EOS ‘show running-config’ command. If the indentation does not match, then we may get unexpected results.

So the final output from our template, using the input for arista_sw1, would be

ip access-list standard ACL-1
   10 permit 10.11.12.13
   exit

Ansible will pass that information into its eos_template module, which will compare the requested template against the current running-config, and add the lines if they do not exist. The eos_template module is aware of the ‘exit’ command as well, knowing that it should never appear in the config output, keeping it when necessary to commit a change, and removing it when it becomes irrelevant (preserving idempotency in the playbook execution).

Now, let’s add a line to our template that will put a remark in every ACL that is added, updated, or verified through our role. Since we want this line in every ACL we handle with our role, we can simply add a line to the template. Let’s add a remark rule right before the exit command, so our template now looks like

   {{ rule }}
   100000 remark This ACL was efficiently implemented with an Arista Role for Ansible
   exit

{% endif %}

And when we run our playbook as before, our arista_sw1 configuration now look like this:

arista_sw1(config)#show running-config all section ACL-1
ip access-list standard ACL-1
   no statistics per-entry
   fragment-rules
   10 permit host 10.11.12.13
   100000 remark This ACL was efficiently implemented with an Arista Role for Ansible
arista_sw1(config)#

Awesome!

But what if we want to allow adding a different remark to each ACL we implement? This will take a little extra work, but nothing we can’t handle easily.

Use Role Parameters Within the Template

Our second way of implementing the ACL remark command will be to add a variable to our role and pass that information into the template where it will be used to build the rule and attach the remark. We’re going to deviate a little from the format of the eos-acl template and define a rule and a remark in the same acl object. The eos-acl role is designed to work with a single permit or deny rule in each object. A remark is in a sense a special type of action, but to enable passing ‘remark’ as the ACL action would require additional handling in the template that cannot be covered in this article. So for our purposes, we will allow each ACL object (rule) to contain a separate remark. Duplicate remarks in different ACL objects, but that apply to the same access list will ultimately only appear once in the ACL.

So let’s use a variable for the remark sequence number and another for the remark text. We’ll name them rmk_seqno and rmk_text respectively. Let’s change our host_vars/switch_sw1 file to set a remark on the ACL rule we have previously defined:

acls:
  - name: ACL-1
    action: permit
    seqno: 10
    description: Adds rule 10 as a host address
    type: standard
    srcaddr: 10.11.12.13
    log: false
    rmk_seqno: 110000
    rmk_text: This remark was added by variables in our ACL object

Now if we run our playbook, we will not see any errors, but we will also not see the remark in our ACL. This is because the variables have been defined, but have not been used. The values are available to our Jinja2 template, but we have not done anything with them yet.

Let’s go back to our acl.j2 file and add a few lines to make it all work. At the top of the file, you will see a series of assignment statements where our template is gathering information from the ACL object, which is stored in the variable item. The assignment of the object to the item variable was handled in the tasks/main.yml file through the Ansible operation with_items, which iterates through a list and performs a task on each item in the list, automatically assigning each to the item variable in turn.

To use the rmk_seqno and rmk_text values, we need to either reference them from the item object, or assign them to an easier to reference variable. Let’s do the second option, and assign them to a local variable that uses the same names as the object’s variables. So we’ll add two lines near the top of acl.j2, below the ‘set log’ line:

{% set state = item.state | default(eos_acl_default_state) %}
{% set name = item.name %}
{% set type = item.type | default(eos_acl_default_type) %}
{% set seqno = item.seqno | default(0) %}
{% set action = item.action %}
{% set srcaddr = item.srcaddr %}
{% set srcprefixlen = item.srcprefixlen | default(eos_acl_default_srcprefixlen) %}
{% set log = item.log | default(eos_acl_default_log) %}
{% set rmk_seqno = item.rmk_seqno | default(false) %}
{% set rmk_text = item.rmk_text | default(false) %}

What is the pipe to default(false) after the item reference? This is a Jinja2 filter. The pipe character sends the preceding value to a filter specified after the pipe. Default is a built-in Jinja2 filter that returns the value within the parentheses if the calling value is undefined. In our example, if rmk_seqno or rmk_text is not defined, then they will be set to the boolean value false. We’ll use that knowledge to test our variables and set a remark if both are defined. Still in acl.j2, let’s replace our hard-coded comment from before with some handling for the new method of setting remarks.

   {{ rule }}
   
   {% if rmk_seqno and rmk_text %}
      {# rmk_seqno and rmk_text are both defined and evaluate to true #}

   {{ rmk_seqno }} remark {{ rmk_text }}

   {% endif %} {# rmk_seqno and rmk_text #}

   exit

{% endif %}

Notice the extra spacing around the remark output line, as is also evident around the rule output and exit lines. This is mostly for our own benefit, to help us distinguish what lines we are expecting output from, and which lines are part of the Jinja2 processing. The Arista roles for Ansible also use a 3 space indentation for the entire file, just to keep things consistent (and easier to verify that configuration lines are at 3 spaces), though it is not required.

Now when we run our playbook again, we should see the following:

arista_sw1(config)#show running-config all section ACL-1
ip access-list standard ACL-1
   no statistics per-entry
   fragment-rules
   10 permit host 10.11.12.13
   100000 remark This ACL was efficiently implemented with an Arista Role for Ansible
   110000 remark This remark was added by variables in our ACL object
arista_sw1(config)#

Notice the hard-coded comment from before is still present in the ACL. That’s because we did not change the ACL before we ran our updated playbook. So the existing ACL matched the one specified in the playbook, and the only change was adding the remark through our newly implemented parameters.

If we run the following command on our switch,

arista_sw1(config)#no ip access-list standard ACL-1

then re-run our playbook, we should see the same results, but without remark number 100000:

arista_sw1(config)#show running-config all section ACL-1
ip access-list standard ACL-1
   no statistics per-entry
   fragment-rules
   10 permit host 10.11.12.13
   110000 remark This remark was added by variables in our ACL object
arista_sw1(config)#

Excellent!

Now we know how to pass variables around our role and how to put those variables to use in our role templates. We also can easily insert configuration lines directly into a template if the need arises. As we are doing this, we are always conscientious of our indentation, making sure we keep everything indented properly at 3 spaces to match our EOS configuration format.

Create a Filter Plugin

At this point, you should have the basic knowledge to implement most customizations into the Arista roles for Ansible.

However, a closer examination of the template code acl.j2, and in many of the other Arista roles, reveals that there can be some rather involved processing to generate the configuration lines needed to implement an EOS command. As an example, consider the vxlan flood vtep add command for interfaces. The command itself to add an IP address may not match the resulting configuration line returned from show running-config. In this case, the template must check for the existence of the IP address within the current running-config, and only output the command if the IP address is not present. See the vtep.j2 template in the arista.eos-vxlan role for further details.

In the vxlan example mentioned, the vtep.j2 template uses some built-in Jinja2 filters plus the config_block custom filter mentioned earlier in the article to determine whether the vxlan flood vtep add command should be output for a given setup. Let’s look at how we can create a custom filter plugin for our own use.

Imagine that we want to add a fixed remark to each ACL we work with based on the ACL identifier. We’ll look at the last digit in the ACL name and build a string based on that. To do this, let’s create a custom filter that we can use in our template.

First, we’ll create a filter plugin file. Filter plugins are python functions that can be called as a filter from within a Jinja2 template or from the Ansible playbook tasks in a role. This allows us to do extra processing on a value or values that we would be unable to do with the simple Jinja2 processing commands. An easy way to start is to make a copy of the filter_plugins/config_block.py plugin file that is included in every Arista role for Ansible. But for our example, you can copy the following text into a new file and save it as filter_plugins/remarks.py.

__metaclass__ = type

def remark_string(acl_name):
    """ Builds a remark string based on the name of the ACL.

    Args:
        acl_name (str): The name of the ACL

    Returns:
        str: A remark string that can be applied to the ACL
    """

    # Get the final character of the acl_name
    last = acl_name[-1:]

    if last.isdigit():
        if int(last) % 2 == 0:
            return "This ACL is EVEN better than before!!"
        else:
            return "This ACL is really ODD!!"

    return "This ACL is a string!"


class FilterModule(object):

    def filters(self):
        return {
            'remark_string': remark_string,
        }

Here we have a function, remark_string, that receives a string as input, and returns a string based on the last character of the input string. If the last character is a digit, the returned string is based on whether the digit is odd or even. If the last character is not a digit, a completely different string is returned.

At the end of the file, we create a class named FilterModule that associates the remark_string function as a Jinja2 filter of the same name. This file could contain multiple filters, and even helper functions for those filters. Each filter would be included in the list returned by the filters function in the FilterModule class.

Now let’s put that filter into use in our template. Back in the acl.j2 file, let’s update the end of the file to look like the code below:

   {{ rule }}

   {% if rmk_seqno and rmk_text %}
      {# rmk_seqno and rmk_text are both defined and evaluate to true #}

   {{ rmk_seqno }} remark {{ rmk_text }}

   {% endif %} {# rmk_seqno and rmk_text #}

   {% set rmk_str = name | remark_string() %}

   120000 remark {{ rmk_str }}
   exit

{% endif %}

Notice in the set rmk_str line how we call the name of the ACL by itself, then pipe that to the remark_string() filter. This is the behavior of Jinja2 filters – the first parameter in the filter function is the value that is piped to the filter. Additional parameters in the filter function are then passed into the filter as parameters. As an example, suppose our remark_string function had three parameters instead of one. It might be defined in the filter_plugins/remarks.py file as

def remark_string(acl_name, seqno, prefix):

and we could use seqno and prefix somewhere within our function. Then the template call to the filter could be formatted as

   {% set rmk_str = name | remark_string(seqno, prefix) %}

where seqno and prefix have been defined somewhere in our template.

Now before we run our playbook, let’s update our host_vars/arista_sw1 definition to include several different ACL names so we can observe the results:

acls:
  - name: ACL-1
    action: permit
    seqno: 10
    description: Adds rule 10 as a host address
    type: standard
    srcaddr: 10.11.12.13
    log: false
    rmk_seqno: 110000
    rmk_text: This remark was added by variables in our ACL object
  - name: ACL-2
    action: permit
    seqno: 12
    description: Adds rule 10 as a host address
    type: standard
    srcaddr: 10.11.12.14
    log: false
  - name: ACL-X
    action: permit
    seqno: 10
    description: Adds rule 10 as a host address
    type: standard
    srcaddr: 10.11.12.15
    log: false

Now let’s run our playbook and see what happens.

arista_sw1(config)#show running-config all section ACL-1
ip access-list standard ACL-1
   no statistics per-entry
   fragment-rules
   10 permit host 10.11.12.13
   110000 remark This remark was added by variables in our ACL object
   120000 remark This ACL is really ODD!!
arista_sw1(config)#show running-config all section ACL-1
ip access-list standard ACL-2
   no statistics per-entry
   fragment-rules
   10 permit host 10.11.12.13
   120000 remark This ACL is EVEN better than before!!
arista_sw1(config)#show running-config all section ACL-1
ip access-list standard ACL-MINE
   no statistics per-entry
   fragment-rules
   10 permit host 10.11.12.13
   120000 remark This ACL is a string!
arista_sw1(config)#

Success! Each of our ACLs has a remark indicating something about its name.

Conclusion

We have seen in this article how with a few straightforward changes, we can easily customize any of the Arista EOS roles for Ansible. We can include a hard-coded configuration line within any template to include that line in every configuration we build with the role. We can use parameters in our host definitions and then use those parameters within the template to further customize our configuration for each host definition. And we can even build custom filters of python code that can be included in the role for even more control on what configuration we build with that role.

Hopefully this article has provided the information and inspiration you need to get you on your way to using and building your own customized versions of our Arista EOS roles for Ansible.