Getting started with Ansible

For the greater part of the last year, I have been developing automation tools to streamline changes to my ACI environment. More specifically, I’ve been using Ansible. It’s relatively straight-forward and easy to write. Most importantly, it’s an excellent gateway tool to getting a team of network engineers acclimated to the idea of using something other than the CLI to accomplish tasks. Ansible is more approachable than something like Python.

That said, I have run into a multitude of issues with Ansible. Firstly, it’s slow. This is no surprise, and it’s not unique to the ACI modules. Ansible is notorious for being slow. Secondly, it has certain limitations. When you start pushing the boundaries of what Ansible can/should be doing, the playbooks can start to get quite complex. My first run-in with Ansible complexity with ACI occurred when I needed to automate deploying N x 48 interface selectors because our ACI object model uses a 1-1 relation between physical interface and interface selector object (e.g. Ethernet1/1 == interface selector eth1_1).

The playbook involved a double-nested loop. The outer-loop iterated over the total number of Leafs that interface selectors should be deployed to. The inner-loop iterated over a list of numbers from 1 to 48 to deploy each individual interface selector. To complicate matters further, I needed a way to distinguish fex type interface selectors from switch_port type interface selectors. To account for the fex selectors, I called them out in the vars data structure on each Leaf. It looks something like the following:

leafs:
  - id: 201
    name: Leaf201
	num_ports: 48
    fex_ports:
	  - 1/1
	  - 1/2
  - id: 202
    name: Leaf202
    num_ports: 48
	fex_ports:
	  - 1/3
	  - 1/4

For the inner loop, I use set_facts to dynamically generate a list of numbers from 1 to num_ports. Then I iterate over that list, creating an interface selector per item. I include a conditional statement to skip numbers that match items in the fex_ports list. (This requires yet another loop).

At this point, we are iterating (N x 48 x M) (or simply O(48n) ), where N equals the total number of Leafs, and M equals the number of exclusions to account for. It works, but is not very efficient!

Outer-loop example:

- name: outer loop - iterate leafs
  include_tasks:
    file: "tasks/leaf_port_selectors.yml"
    apply:
      tags:
        - int-selectors
  loop: "{{ leafs }}"
  loop_control:
    loop_var: node
  tags:
    - int-selectors

Inner-loop example:

 # Create list of numbers [1-48] or [1-24] to represent each physical port.
- set_fact:
    all_ports: "{{ range(1, 1 + node.port_count | int) | list }}"
  delegate_to: localhost

  # Create a list of all of the leaf ports which connect to FEX, if applicable.
  # This fact/variable is used to calculate the next fact, `leaf_ports`.
- set_fact:
    fex_ports: "{{ fex_ports | default([]) + [item.uplink] }}"
  loop: "{{ node.fex_ports }}"
  when: node.fex_ports is defined
  delegate_to: localhost

  # For each leaf port which does not connect to a FEX, create an interface selector of type 'switch_port'.
- name: "inner loop - iterate interface selectors"
  aci_access_port_to_interface_policy_leaf_profile:
    host: "{{ inventory_hostname }}"
    username: "{{ vault_user }}"
    password: "{{ vault_password }}"
    leaf_interface_profile: "{{ node.name }}_IntProf"
    access_port_selector: "eth1_{{ leaf_port }}"
    leaf_port_blk: "{{ leaf_port }}"
    from_port: "{{ leaf_port }}"
    to_port: "{{ leaf_port }}"
    interface_type: "{{ node.type }}"
    state: present
    validate_certs: no
    policy_group: "{{ apg }}"
  delegate_to: localhost
  loop: "{{ leaf_ports }}"
  loop_control:
    loop_var: leaf_port

Notice how the looping mechanism works by storing all of the inner tasks in its own playbook, and using iterating over the entire file using include_tasks and loop.

This has been a relatively graceful process that works well for the most part. I included this as part of a larger zero-touch Leaf deployment playbook. However, I was convinced there was a better way. Perhaps it was time to start exploring alternative methods such as the Python SDK. That led me to finding the acitoolkit library! I first found it on GitHub here and immediately jumped into testing out the sample scripts included.

Shifting to Python libraries

If you don’t already have one, before proceeding I suggest you setup a virtualenv to contain all of your network automation related libraries. I am working on macOS, and find it quite easy to get my paths out of wack. I do my best to keep everything development related contained inside of a dev virtualenv.

cd ~
pip3 install virtualenv
virtualenv dev

Here is a pretty good guide to get you started on virtualenv’s.

To switch to the virtual environment:

source nameOfVirtualEnv/bin/activate

Next, I cloned the acitoolkit repo to a PyCharm project, and installed the package.

python3 setup.py install                                                                                         

Next, you need to add the package to your $PATH so the package contents is available to execute in bash. Add the following line to your .bash_profile then reload bash to load the new path.

nano ~/.bash_profile
export PATH="/Users/YOUR_USERNAME/dev/lib/python3.7/site-packages/:$PATH"

That’s it! At this point, I was able to spin up my ACI Sim and start executing scripts from the provided samples directory.

python3 aci-create-bd.py --tenant Coke --vrf coke --bd bd1 --address "192.168.100.1/24" --scope "shared,private"

I have a lot more learning to go with this library. But already, I’m blown away by the simplicity and SPEED of it compared to Ansible. I’ll follow-up with another post once I’ve explored some more.