ACI and Ansible

Potentially you are already used to Ansible, if so, just skip the introduction in this article.

Installing Ansible

Ansible is a huge and powerful tool set to automate activities – there a so many options. Ansible follows an approach they call „batteries included“ – means – all the modules and tools are always part of the distribution. And this includes as well the Cisco ACI automation capabilities.

If you look around – tutorials are available – from both the ansible team as well from other contributors.

This tutorial gives a nice start into your ansible journey. https://linuxhint.com/ansible-tutorial-beginners/

Installation on a linux box is quite simple (a „no-brainer“ as it has been called from a colleague many years ago). For CentOS/RedHat it is just a

# yum install ansible
# ansible --version
ansible 2.9.10
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Apr  2 2020, 13:16:51) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]

As you can see, config file location (for CentOS) is /etc/ansible.cfg.

Please install as well two tools:

  • yamllint
  • ansible-lint

as the yaml syntax is quite picky especially regarding indentitation.

Ansible hosts

Another important file is stored in /etc/ansible as well, the hosts file.

Let us create a simple entry there:

# This is the default ansible 'hosts' file.
#
# It should live in /etc/ansible/hosts
#
#   - Comments begin with the '#' character
#   - Blank lines are ignored
#   - Groups of hosts are delimited by [header] elements
#   - You can enter hostnames or ip addresses
#   - A hostname/ip can be a member of multiple groups

# APIC host

[APIC]
192.168.140.40 ansible_user=ansible ansible_connection=local

Next step is to create a user ansible within the fabric.

Create Ansible User

As we’ve already done – go to -> Admin -> AAA -> Users and create a new local user. Add your public key from the box you are running ansible from and store it in the SSH Keys section.

Quick check, if we are now able to connect to the box.

# ansible APIC -m ping
192.168.140.40 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    }, 
    "changed": false, 
    "ping": "pong"
}

This shows – access via ssh is possible for ansible.

Ansible .yml File

But we want to use the API – and this is really simple.

Just create a file named tenant.yml with this content:

---
- name: ACI Tenant Management
  hosts: APIC
  connection: local
  gather_facts: no
  tasks:
  - name: CONFIGURE TENANT
    aci_tenant:
      host: '{{ inventory_hostname }}'
      user: ansible
      password: SecretSecretOhSoSecret
      validate_certs: false
      tenant: "Beaker"
      description: "Beaker created Using Ansible"
      state: present
...

and run this via the command line:

# ansible-playbook tenant.yml

PLAY [ACI Tenant Management] *********************************************************************************************************************************************************************************************

TASK [CONFIGURE TENANT] **************************************************************************************************************************************************************************************************
changed: [192.168.140.40]

PLAY RECAP ***************************************************************************************************************************************************************************************************************
192.168.140.40             : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Have a look on your APIC console:



Using private keys

One last step – I’m not a big fan of storing plain passwords – starting with Ansible 2.5 it is possible to use private keys to authenticate. This requires some additional steps.

First you have to create a private key and a .crt file (to be used on APIC AAA).

# openssl req -new -newkey rsa:1024 -days 36500 -nodes -x509 -keyout ansible.key -out ansible.crt -subj '/CN=ansible/O=proGIS/C=DE'
Generating a 1024 bit RSA private key
...................................................................++++++
....++++++
writing new private key to 'ansible.key'

After running this command you’ll find two new files in the directory where you’ve executed the command.

  1. ansible.key (your private key)
  2. ansible.crt (the file required in APIC)

Go now to –> Admin -> AAA -> Users

and add the .crt file content in the user certificates section.




Just replace now the password entry with private_key details, the file looks like this:

---
- name: ACI Tenant Management
  hosts: APIC
  connection: local
  gather_facts: no
  tasks:
  - name: CONFIGURE TENANT
    aci_tenant:
      host: '{{ inventory_hostname }}'
      user: ansible
      private_key: /root/.pki/ansible.key
      validate_certs: false
      tenant: "Beaker"
      description: "Beaker created Using Ansible"
      state: present
...

This works! If you want to see more details about the activities behind the scene – please add -vvvv to you playbook command line.

# ansible-playbook tenant.yml -vvvv
ansible-playbook 2.9.10
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible-playbook
  python version = 2.7.5 (default, Apr  2 2020, 13:16:51) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
Using /etc/ansible/ansible.cfg as config file
setting up inventory plugins
host_list declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
script declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
auto declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Parsed /etc/ansible/hosts inventory source with ini plugin
Loading callback plugin default of type stdout, v2.0 from /usr/lib/python2.7/site-packages/ansible/plugins/callback/default.pyc

PLAYBOOK: tenant.yml *****************************************************************************************************************************************************************************************************
Positional arguments: tenant.yml
become_method: sudo
inventory: (u'/etc/ansible/hosts',)
forks: 5
tags: (u'all',)
verbosity: 4
connection: smart
timeout: 10
1 plays in tenant.yml

PLAY [ACI Tenant Management] *********************************************************************************************************************************************************************************************
META: ran handlers

TASK [CONFIGURE TENANT] **************************************************************************************************************************************************************************************************
task path: /root/nxos/tenant.yml:7
<192.168.140.40> ESTABLISH LOCAL CONNECTION FOR USER: root
<192.168.140.40> EXEC /bin/sh -c 'echo ~root && sleep 0'
<192.168.140.40> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /root/.ansible/tmp `"&& mkdir /root/.ansible/tmp/ansible-tmp-1594034388.72-15831-57970545921562 && echo ansible-tmp-1594034388.72-15831-57970545921562="` echo /root/.ansible/tmp/ansible-tmp-1594034388.72-15831-57970545921562 `" ) && sleep 0'
<192.168.140.40> Attempting python interpreter discovery
<192.168.140.40> EXEC /bin/sh -c 'echo PLATFORM; uname; echo FOUND; command -v '"'"'/usr/bin/python'"'"'; command -v '"'"'python3.7'"'"'; command -v '"'"'python3.6'"'"'; command -v '"'"'python3.5'"'"'; command -v '"'"'python2.7'"'"'; command -v '"'"'python2.6'"'"'; command -v '"'"'/usr/libexec/platform-python'"'"'; command -v '"'"'/usr/bin/python3'"'"'; command -v '"'"'python'"'"'; echo ENDFOUND && sleep 0'
<192.168.140.40> EXEC /bin/sh -c '/usr/bin/python && sleep 0'
Using module file /usr/lib/python2.7/site-packages/ansible/modules/network/aci/aci_tenant.py
<192.168.140.40> PUT /root/.ansible/tmp/ansible-local-15822LIJPe1/tmpfW12QD TO /root/.ansible/tmp/ansible-tmp-1594034388.72-15831-57970545921562/AnsiballZ_aci_tenant.py
<192.168.140.40> EXEC /bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1594034388.72-15831-57970545921562/ /root/.ansible/tmp/ansible-tmp-1594034388.72-15831-57970545921562/AnsiballZ_aci_tenant.py && sleep 0'
<192.168.140.40> EXEC /bin/sh -c '/usr/bin/python /root/.ansible/tmp/ansible-tmp-1594034388.72-15831-57970545921562/AnsiballZ_aci_tenant.py && sleep 0'
<192.168.140.40> EXEC /bin/sh -c 'rm -f -r /root/.ansible/tmp/ansible-tmp-1594034388.72-15831-57970545921562/ > /dev/null 2>&1 && sleep 0'
changed: [192.168.140.40] => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    }, 
    "changed": true, 
    "current": [
        {
            "fvTenant": {
                "attributes": {
                    "annotation": "", 
                    "descr": "Beaker created Using Ansible", 
                    "dn": "uni/tn-Beaker", 
                    "name": "Beaker", 
                    "nameAlias": "", 
                    "ownerKey": "", 
                    "ownerTag": "", 
                    "userdom": ":all:mgmt:common:"
                }
            }
        }
    ], 
    "invocation": {
        "module_args": {
            "certificate_name": "ansible", 
            "description": "Beaker created Using Ansible", 
            "host": "192.168.140.40", 
            "output_level": "normal", 
            "password": null, 
            "port": null, 
            "private_key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "protocol": "https", 
            "state": "present", 
            "tenant": "Beaker", 
            "timeout": 30, 
            "use_proxy": true, 
            "use_ssl": true, 
            "user": "ansible", 
            "username": "ansible", 
            "validate_certs": false
        }
    }
}
META: ran handlers
META: ran handlers

PLAY RECAP ***************************************************************************************************************************************************************************************************************
192.168.140.40             : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Starting with rel. 2.8 ansible offers encryption of the private key as well. Please find more details at the bottom of this article.

https://docs.ansible.com/ansible/latest/scenario_guides/guide_aci.html#aci-guide

A one hour webinar offered by RedHat – please watch it as well:

https://www.ansible.com/resources/webinars-training/cisco-aci-with-red-hat-ansible-collections-webinar

Query the fabric

Quite simple to query the fabric.

---
- name: ACI Get Bridge Domains
  hosts: APIC
  connection: local
  gather_facts: no
  tasks:
  - name: Get Bridge Domains
    aci_rest:
      host: '{{ inventory_hostname }}'
      username: ansible
      private_key: /root/.pki/ansible.key
      validate_certs: false
      path: /api/node/class/fvBD.json
      method: get

...

Will give you

# ansible-playbook getbridge.yml -vvvv | grep dn
                    "dn": "uni/tn-common/BD-default", 
                    "dn": "uni/tn-infra/BD-ave-ctrl", 
                    "dn": "uni/tn-infra/BD-default", 
                    "dn": "uni/tn-mgmt/BD-inb", 

We’ve got now full control in both directions (read and write).