Working With Static Inventories

The most important part of any network automation solution is a reliable inventory. In large and complex network environments, a central DCIM like Netbox or Nautobot with dynamically generated inventories seems to be the gold standard.

But many of us start their automation journey with simple text files, following the idea of infrastructure as code. However, even this approach is suitable as a comprehensive device asset management and can even replace existing tooling. Here is the why and how.

Inventory basics

I’m a big fan of the clean Ansible inventory INI format with one line per device and easy grouping.

[dc_east]
dsw-e1 ansible_host=10.0.10.11 ansible_network_os=nxos
dsw-e2 ansible_host=10.0.10.12 ansible_network_os=nxos

In fact, the same inventory file in YAML format quickly becomes confusing, especially with additional variables. But in the end, it’s a personal decision which way to go.

all:
  children:
    dc_east:
      hosts:
        dsw-e1:
          ansible_host: 10.0.10.11
          ansible_network_os: nxos
        dsw-e2:
          ansible_host: 10.0.10.12
          ansible_network_os: nxos

The cool thing: You can use an Ansible inventory for Python scripts as well, thanks to the nornir-ansible plugin, to cover both major network automation frameworks via one ‘source of truth’.

These basic inventories can now be enriched with any device-specific information by simply adding variables.

Note: For overarching group or site definitions (such as VLAN, syslog, snmp, …) I like to use external, tiered IaC files in YAML format, which can be easily referenced based on the respective config mgmt task. That’s because Ansible’s and Nornir’s built-in group_vars function can lead to very complex inheritance structures if you don’t want to repeat yourself (DRY principle).

Here are a few production near inventory examples for a better understanding.

Data Center Pod with NX-OS & Fex

Usual additions would be:

  • inv – An asset ID to map with 3rd-party tools or billing systems
  • sn – The device serial number, important for Day0 configuration
  • rack – Installation location in the data center
  • con1/2 – IP address and high port for serial console access
  • sw1/sw2 – Bolean to distinguish paired switches
  • vpc – vPC Domain ID for the NX-OS MLAG feature
  • fex – Fex ID for the NX-OS Fabric Extender feature

At work, we use the Fabric Extenders exclusively single-homed and therefore simply enter the Mgmt-IP of the parent switch as ansible_host. So, a complete Inventory for one DC pod with the group name dc_west would look like this.

[dc_east]
dsw-e1 ansible_host=10.0.10.11 inv=n1001 ansible_network_os=nxos con1=172.16.1.11:2004 con2=172.16.1.11:2005 sw1=true vpc=11 sn=4JKW56HGXG6 rack=n101
dsw-e2 ansible_host=10.0.10.12 inv=n1002 ansible_network_os=nxos con1=172.16.1.11:2006 con2=172.16.1.11:2007 sw2=true vpc=11 sn=YG3WYCJQQGB rack=n102

asw-e1 ansible_host=10.0.10.11 inv=n1101 ansible_network_os=nxos fex=111 sn=PBR1OWX2U4Q rack=s111
asw-e2 ansible_host=10.0.10.12 inv=n1102 ansible_network_os=nxos fex=111 sn=IVFFBE7GT7I rack=s111
asw-e3 ansible_host=10.0.10.11 inv=n1103 ansible_network_os=nxos fex=112 sn=QTPIZXBRW6C rack=s112
asw-e4 ansible_host=10.0.10.12 inv=n1104 ansible_network_os=nxos fex=112 sn=0Q4UDI5TWXV rack=s112
asw-e5 ansible_host=10.0.10.11 inv=n1105 ansible_network_os=nxos fex=113 sn=63G1J8MHR1C rack=s113
asw-e6 ansible_host=10.0.10.12 inv=n1106 ansible_network_os=nxos fex=113 sn=VA12OT8FL4A rack=s113
asw-e7 ansible_host=10.0.10.11 inv=n1107 ansible_network_os=nxos fex=114 sn=P1BB60Q9ACW rack=s114
asw-e8 ansible_host=10.0.10.12 inv=n1108 ansible_network_os=nxos fex=114 sn=W0DRHCE3P9K rack=s114

ACI Fabric

For a Cisco ACI inventory most device variables stay the same, only a few ACI-specific parameters are added.

  • cimc – IP of APICs Baseboard Management Controller
  • pod – ID of the ACI pod the switch should be a member of
  • node – Node ID of the switch
[aci_fab1]
apic1-1 ansible_host=10.0.0.11 inv=a1001 cimc=10.0.0.21 rack=n101
apic1-2 ansible_host=10.0.0.13 inv=a1002 cimc=10.0.0.22 rack=n102
apic1-3 ansible_host=10.0.0.13 inv=a1003 cimc=10.0.0.23 rack=n201

spine1-1 ansible_host=10.0.0.201 inv=s1001 pod=1 node=201 sn=CWIK5XGALBD rack=n101
spine1-2 ansible_host=10.0.0.202 inv=s1002 pod=1 node=202 sn=6WP1ZWZSAC8 rack=n201

leaf1-1 ansible_host=10.0.0.101 inv=l1001 pod=1 node=101 vpc=101 sn=B9IGS3YD35F rack=s101
leaf1-2 ansible_host=10.0.0.102 inv=l1002 pod=1 node=102 vpc=101 sn=9SDXW8SUPDW rack=s101
leaf1-3 ansible_host=10.0.0.103 inv=l1003 pod=1 node=103 vpc=102 sn=2LBN384V5IS rack=s102
leaf1-4 ansible_host=10.0.0.104 inv=l1004 pod=1 node=104 vpc=102 sn=R3I14KVWD4N rack=s102
leaf1-5 ansible_host=10.0.0.105 inv=l1005 pod=1 node=105 vpc=201 sn=DEMVPI6HMCF rack=s201
leaf1-6 ansible_host=10.0.0.106 inv=l1006 pod=1 node=106 vpc=201 sn=2AISE18WIDC rack=s201
leaf1-7 ansible_host=10.0.0.107 inv=l1007 pod=1 node=107 vpc=202 sn=0PCGK0NW6JJ rack=s201
leaf1-8 ansible_host=10.0.0.108 inv=l1008 pod=1 node=108 vpc=202 sn=EVWRLT031C1 rack=s201

Staging Group

This very special group should not be forgotten. It’s meant to plan and park new devices during commissioning and helps to distinguish from devices in production. Otherwise, regular tasks like config backup or policy enforcement might fail.

Make it a ‘Website’

At this point we have a smart inventory file to manage all devices, but that may be a bit unwieldy for operations. Sure, there’s grep on your Linux control node and search functions in central repositories, but a nice website should definitely be more popular. Documentation as Code to the rescue! Why not simply use existing tooling from our IaC pipeline to turn the inventory file into a markdown summary and make it available via GitLab?

A Jinja2 template and only one additional task in an Ansible based pipeline gets this job done.

#jinja2: lstrip_blocks: True
# Device Inventory

{% for group in groups.keys() | list | sort %}
  {% if 'all' not in group and 'ungrouped' not in group %}
**[{{ group }}](#{{ group }})**<br>
  {% endif %}
{% endfor %}


{% for group in groups.keys() | list | sort %}
  {% if 'all' not in group and 'ungrouped' not in group %}
## **{{ group }}**

| **Hostname** | **INV-ID** | **Node/Fex** | **Mgmt-IP** | **Con1/CIMC** | **Con2** | **S/N** | **Rack** |
| ------------ | ---------- | ------------ | ------------| ------------- | -------- | ------- | -------- |
    {% for host in groups[group] | sort %}
      {% set cell_inv = hostvars[host].inv | default('') %}
      {% set cell_node_fex = hostvars[host].node | default('') | string + hostvars[host].fex | default('') | string %}
      {% set cell_mgmt_ip = '[' + hostvars[host].ansible_host | default('') + '](ssh://' + hostvars[host].ansible_host | default('') + ')' %}
      {% set cell_con1_cimc = '[' + hostvars[host].con1 | default('') + hostvars[host].cimc | default('') + '](ssh://' + hostvars[host].con1 | default('') + hostvars[host].cimc | default('') + ')' %}
      {% set cell_con2 = '[' + hostvars[host].con2 | default('') + '](ssh://' + hostvars[host].con2 | default('') + ')' %}
      {% set cell_sn = hostvars[host].sn | default('') %}
      {% set cell_rack = hostvars[host].rack | default('') %}
| {{ host }} | {{ cell_inv }} | {{ cell_node_fex }} | {{ cell_mgmt_ip }} | {{ cell_con1_cimc }} | {{ cell_con2 }} | {{ cell_sn }} | {{ cell_rack }} |
    {% endfor %}

  {% endif %}
{% endfor %}

The template loops through all groups in the hosts inventory except the special ‘all’ and ‘ungrouped’, and creates a table based overview of all devices, including the interesting variables. It can be invoked by this very simple Ansible playbook via ansible-playbook -i hosts md_my_hosts.yml , but is of course better off as part of an IaC pipeline.

---
- name: Generate nice Markdown from hosts file
  hosts: localhost
  gather_facts: false

  tasks:

  - name: TEMPLATE OUT MARKDOWN FILE WITH JINJA2
    template: src=md_my_hosts.j2 dest="hosts.md"

The markdown result hosts.md looks like this

# Device Inventory

**[aci_fab1](#aci_fab1)**<br>
**[dc_core](#dc_core)**<br>
**[dc_east](#dc_east)**<br>
**[dc_west](#dc_west)**<br>
**[staging](#staging)**<br>


## **aci_fab1**

| **Hostname** | **INV-ID** | **Node/Fex** | **Mgmt-IP** | **Con1/CIMC** | **Con2** | **S/N** | **Rack** |
| ------------ | ---------- | ------------ | ------------| ------------- | -------- | ------- | -------- |
| apic1-1 | a1001 | 1 | [10.0.0.11](ssh://10.0.0.11) | [10.0.0.21](ssh://10.0.0.21) | [](ssh://) |  | n101 |
| apic1-2 | a1002 | 2 | [10.0.0.13](ssh://10.0.0.13) | [10.0.0.22](ssh://10.0.0.22) | [](ssh://) |  | n102 |
| apic1-3 | a1003 | 3 | [10.0.0.13](ssh://10.0.0.13) | [10.0.0.23](ssh://10.0.0.23) | [](ssh://) |  | n201 |
| leaf1-1 | l1001 | 101 | [10.0.0.101](ssh://10.0.0.101) | [](ssh://) | [](ssh://) | B9IGS3YD35F | s101 |
| leaf1-2 | l1002 | 102 | [10.0.0.102](ssh://10.0.0.102) | [](ssh://) | [](ssh://) | 9SDXW8SUPDW | s101 |
| leaf1-3 | l1003 | 103 | [10.0.0.103](ssh://10.0.0.103) | [](ssh://) | [](ssh://) | 2LBN384V5IS | s102 |
| leaf1-4 | l1004 | 104 | [10.0.0.104](ssh://10.0.0.104) | [](ssh://) | [](ssh://) | R3I14KVWD4N | s102 |
| leaf1-5 | l1005 | 105 | [10.0.0.105](ssh://10.0.0.105) | [](ssh://) | [](ssh://) | DEMVPI6HMCF | s201 |
| leaf1-6 | l1006 | 106 | [10.0.0.106](ssh://10.0.0.106) | [](ssh://) | [](ssh://) | 2AISE18WIDC | s201 |
| leaf1-7 | l1007 | 107 | [10.0.0.107](ssh://10.0.0.107) | [](ssh://) | [](ssh://) | 0PCGK0NW6JJ | s201 |
| leaf1-8 | l1008 | 108 | [10.0.0.108](ssh://10.0.0.108) | [](ssh://) | [](ssh://) | EVWRLT031C1 | s201 |
| spine1-1 | s1001 | 201 | [10.0.0.201](ssh://10.0.0.201) | [](ssh://) | [](ssh://) | CWIK5XGALBD | n101 |
| spine1-2 | s1002 | 202 | [10.0.0.202](ssh://10.0.0.202) | [](ssh://) | [](ssh://) | 6WP1ZWZSAC8 | n201 |

and GitLab renders it automagically into a nice HTML version with active links (something GitHub isn’t able to, btw).

Please visit this GitLab repository to find all the files and adapt the project to your needs. A local copy can even be viewed with every markdown supporting application like VSCode, notepad++, you name it … – a real added value as part of a disaster prevention strategy.

I hope I was able to show that it doesn’t always has to be the big solution from the start. You can get along with text file based inventories for a long time and manage hundreds if not thousands of devices. And don’t be afraid of a migration to another, database-based tool that might be pending in the future. It’s a walk in the park because the inevitable csv export already exists. 😉

Leave a comment

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