Fix Your Interface Description with Ansible and CDP/LLDP

This topic came up via Twitter recently and I heard this use case before but wasn’t aware how easy it could be solved with Ansible, until I started thinking about it. The little playbook in this blogpost fetches all discovered neighbors per device and sets the interface description according to the remote host and port. It supports the two platforms Cisco IOS XE and NX-OS to demonstrate the path to a multivendor solution for the common brownfield networks out there.

Prerequisits

You need a working Ansible Installation and most of the time a dedicated Control Node is very useful. I personally use a CentOS VM, so in this case a simple

$ sudo yum install ansible 

gets the job done, including all dependencies. Of cause, there should be ssh access to the control node and from the control node to all network devices.


The required Ansible Inventory of my little VirtualBox powered homelab is located at the control node in the /etc/ansible/hosts path and looks like this.

[ios]
CSR-1 ansible_host=192.168.10.211 ansible_network_os=ios
CSR-2 ansible_host=192.168.10.212 ansible_network_os=ios

[nxos]
N9K-1 ansible_host=192.168.10.221 ansible_network_os=nxos

There is no need to provide credentials when the playbook is executed via the -k option, it then uses the local user and prompts for the password.

Ansible Facts

The Ansible facts function is automatically called by playbooks to gather useful variables about remote hosts, it just has to be turned on in the playbook header.

---
- name: Set Interface description based on CDP/LLDP discovery
  hosts: all
  gather_facts: yes
  connection: network_cli

All the data is stored in an ansible_facts object per remote host and can be referenced as structured JSON by every task during the playbook runtime. All kinds of useful device facts are collected, like OS version, S/N, all Interfaces including configuration, and so on. The interesting part with regards to the neighbor discovery can be found in the dictionary called net_neighbors. It shows only Interfaces with discovered neighbors via CDP or LLDP.

{
"net_neighbors": {
            "GigabitEthernet2": [
                {
                    "host": "CSR-2.example.com",
                    "port": "GigabitEthernet2"
                }
            ],
            "GigabitEthernet3": [
                {
                    "host": "CSR-2.example.com",
                    "port": "GigabitEthernet3"
                }
            ]
        }
}

Cisco IOS-XE and NX-OS Tasks

To set the right interface description the task has to loop over the net_neighbors dictionary and use the ios_config or nxos_config module to execute the configuration change.

  tasks:

  - name: Set IOS Interface Description
    ios_config:
      lines:
        - description Connected to {{ item.value[0].host }} via its {{ item.value[0].port }}
      parents: interface {{ item.key }}
      save_when: changed
    with_dict: "{{ansible_facts.net_neighbors }}"
    when: ansible_network_os == 'ios'

  - name: Set NXOS Interface Description
    nxos_config:
      lines:
        - description Connected to {{ item.value[0].host }} via its {{ item.value[0].port }}
      parents: interface {{ item.key }}
      save_when: changed
    with_dict: "{{ansible_facts.net_neighbors }}"
    when: ansible_network_os == 'nxos'

The Interface name can be referenced via the dynamic variable {{item.key}} which translates to ‘GigabitEthernet2‘ for the first item of the loop. The second level of the dictionary contains a list of key-value pairs, namely the remote host and port name, and can be referenced by {{item.value[0].host}}, ‘CSR-2.example.com‘ for instance.

The save_when: changed parameter stores the running-config, but only when changes to the interface configuration happend in this task.

Playbook Run

Given that CSR-1 has the following ‘default’ interface descriptions.

The Playbook is available for download via Github:

wget https://raw.githubusercontent.com/NWMichl/network-automation/master/fix_ifdesc.yml

$ ansible-playbook -k fix_ifdesc.yml

Et voila, CSR-1 has the desired Interface description as expected.

Caveat with the Multiprotocol Use Case

Despite the Ansible documentation the destinction between CDP and LLDP doesn’t happen on a port level, but seemingly on a device level with CDP superseeding LLDP. Meaning, that you might miss the local interface description of an LLDP only remote device. As most networks I know run

  • CDP only
  • CDP and LLDP (for server connectivity or non Cisco VoIP phones)
  • LLDP only

the playbook should at least do the job Pareto style (80%/20%). But I think this should be fixed in future releases and I’ll raise an issue with the Ansible maintainers.

Closing

So it’s 2020 and really nobody has to fix broken interface namings by hand, there are plenty of viable solutions out there, this Ansible playbook is only one of them. John (@ipvZero on the Twitters) did a similar thing via Python / Nornir in a more flexible way and at his usual scale.

6 thoughts on “Fix Your Interface Description with Ansible and CDP/LLDP

    1. Hi Richard,

      I just found time to have a look at your question and yes, this is actually quite simple!
      The ansible_facts.net_interfaces include the interface mode (access / trunk):
      net_interfaces:
      Ethernet1/10:
      bandwidth: ‘10000000’
      duplex: auto
      macaddress: 0800.2728.cb45
      mode: access
      mtu: ‘1500’
      speed: auto-speed
      state: down
      type: 100/1000/10000 Ethernet

      So the task conditional has to look like

      when: ansible_network_os == ‘nxos’ and ansible_facts.net_interfaces[item.key].mode is defined and ansible_facts.net_interfaces[item.key].mode == ‘trunk’

      Where item.key references the interface name (e.g. Ethernet1/1) of all available neighbor dicts when looping via with_dict.

      Michael

      1. Hi Michael,

        thank you for responding, I just looked in my spam folder and saw that you posted a new blog. I will fix that. I would like to try this on a catalyst switch but I am not seeing what mode it is configured for. This is a trunk port

        “ansible_facts”: {
        “ansible_network_resources”: {},
        “ansible_net_gather_network_resources”: [],
        “ansible_net_gather_subset”: [
        “interfaces”,
        “hardware”,
        “default”
        ],
        “ansible_net_all_ipv4_addresses”: [
        “172.28.5.2”,
        “172.28.20.2”
        ],
        “ansible_net_all_ipv6_addresses”: [],
        “ansible_net_neighbors”: {
        “GigabitEthernet1/0/24”: [
        {
        “host”: “MikeThunderdome-1.ne.gov”,
        “port”: “GigabitEthernet1/0/24”
        }
        ]
        },
        “ansible_net_interfaces”: {

        “GigabitEthernet1/0/24”: {
        “description”: “Connected to MikeThunderdome-1.ne.gov via its GigabitEthernet1/0/24”,
        “macaddress”: “500f.80bc.2398”,
        “mtu”: 1500,
        “bandwidth”: 1000000,
        “mediatype”: “10/100/1000BaseTX”,
        “duplex”: null,
        “lineprotocol”: null,
        “operstatus”: “up”,
        “type”: “Gigabit Ethernet”,
        “ipv4”: []

        What I do not want are the lldp neighbor info from VOIP phones or access ports

        Another thing I would like to get to is to select interfaces based on if they are trunk or access and what vlan and I hope going down this rabbit hole will help me with that.

        Thank you much Richard

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.