Automate Cisco IOS/IOS-XE documentation with Ansible (with examples)

Note: This post integrates several automation tools at once. My objective is to provide some well-documented, concrete examples of executable Ansible Playbooks, D2 Diagrams, and best practice to illustrate ways to make good documentation easy.

Note: All code examples in this blog post are structured in a format to be complete and executable. They don’t necessarily represent best practice (e.g. including passwords), but are intended as a functional starting point for Ansible beginners

Ansible Setup

Welcome to the messy world of network automation! If you’re familiar with Ansible itself, there are a few things you’ll need to understand to effectively use the tool:

Ansible Inventory

Things get opinionated on how to store an inventory. Ansible’s documentation is going to be the most up-to-date and best, but it boils down to a few choices:

In this example, we’ll take a new yaml inventory and apply it. There’s more to the inventory, but the top-level hierarchy is the same. yaml and json files start with a top-level dictionary, and yaml prefers a start-of-file line (---):

---
cml_ios_xe_machines:
  hosts:
    AnsLabIOSXEv-1:
      ansible_host: "10.7.3.10"
  vars:
    ansible_network_os: "ios"
    ansible_user: "admin"
    ansible_password: "cisco"
    ansible_become: "yes"
    ansible_become_method: "enable"
    ansible_connection: "ansible.netcommon.network_cli"

Let’s cover what each of these fields does, and how it serves us:

Configuring Cisco IOS with Ansible

After firing up the CML nodes required for this lab, I was quickly reminded of how frustrating the old Cisco IOS CLI really is - let’s build a tool that will configure interfaces worth diagramming.

We’re going to run into issues here - there isn’t a cisco.ios module for Layer 3 802.1q subinterfaces. This is resolvable with a Jinja2 template, but is no longer idempotent. To use this template, simply place it in the same directory as the Ansible playbook:

{% for i in ios_interfaces %}
interface {{ i.name }}
  encapsulation dot1q {{ i.tag }}
{% endfor %}

This is a simple example of Jinja2 looping - the {% for i in ios_interfaces %} stanza receives input from the Ansible playbook as part of vars. The iterator (i) in this example will contain whatever is stored in vars (a dictionary), and can be invoked without dictionary traversal, e.g. {{ i.name }}.

Let’s try a playbook to configure some interfaces:

---
- name: "IOS Gather Facts"
  hosts: "AnsLabIOSXEv-1"
  connection: network_cli
  gather_facts: yes
  vars:
    ios_interfaces:
    - name: GigabitEthernet4.100
      tag: 100
      ipv4:
        address: 10.10.100.1/24
    - name: GigabitEthernet4.101
      tag: 101
      ipv4:
        address: 10.10.101.1/24
    - name: GigabitEthernet4.102
      tag: 102
      ipv4:
        address: 10.10.102.1/24
    - name: GigabitEthernet4.103
      tag: 103
      ipv4:
        address: 10.10.103.1/24
    - name: GigabitEthernet4.104
      tag: 104
      ipv4:
        address: 10.10.104.1/24
  tasks:
    - name: "Set Interface Config Sheet"
      template:
        src: ios_subinterfaces.j2
        dest: '{{ inventory_hostname }}.conf'
    - name: "Apply Layer 2 Configuration"
      cisco.ios.ios_config:
        src: '{{ inventory_hostname }}.conf'
    - name: "debug"
      debug:
        msg: '{{ item }}'
      with_items: '{{ ios_interfaces }}'
    - name: "Set Interface IPs!"
      cisco.ios.ios_l3_interfaces:
        config:
        - name: '{{ item.name }}'
          ipv4:
            - address: '{{ item.ipv4.address }}'
      with_items: '{{ ios_interfaces }}'

With this playbook, I invoked the specific node AnsLabIOSXEv-1, because the playbook itself includes unique data. Ansible also supports injecting variables from a separate file, e.g. ansible-playbook {{ playbook_name }} --extra-vars "@file.json".

The vars structure is doing most of the heavy lifting here - defining each interface for configuration in a concise, readable format. This may follow stricter formatting, making it the “Model” portion of the Model-View-Controller architecture.

If you have troubles coming up with a structure for your data, or constant revising causes issues, check out Openconfig for vendor-neutral, well-organized models.

tasks is where the actual work happens:

Running the playbook is fairly straightforward (truncated):

ansible-playbook set_interfaces_anslabiosxev-1.yml
TASK [Set Interface IPs!] ******************************************************
ok: [AnsLabIOSXEv-1] => (item={'name': 'GigabitEthernet4.100', 'tag': 100, 'ipv4': {'address': '10.10.100.1/24'}})
ok: [AnsLabIOSXEv-1] => (item={'name': 'GigabitEthernet4.101', 'tag': 101, 'ipv4': {'address': '10.10.101.1/24'}})
ok: [AnsLabIOSXEv-1] => (item={'name': 'GigabitEthernet4.102', 'tag': 102, 'ipv4': {'address': '10.10.102.1/24'}})
ok: [AnsLabIOSXEv-1] => (item={'name': 'GigabitEthernet4.103', 'tag': 103, 'ipv4': {'address': '10.10.103.1/24'}})
ok: [AnsLabIOSXEv-1] => (item={'name': 'GigabitEthernet4.104', 'tag': 104, 'ipv4': {'address': '10.10.104.1/24'}})
PLAY RECAP *********************************************************************
AnsLabIOSXEv-1             : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

The above command is a re-run, but note how there are 2 changed tasks. This is an indicator that a change is not idempotent.

Generating IOS Documentation with Ansible

Now, we finally have a router to automatically document.

As part of the playbook process, Ansible will try and gather a number of details about the system it intends to change. These facts aid idempotency but also provides important context in a way that’s easy to tap for automation engineers. Our previous instructions in the inventory provided Ansible important context, so most of the data required to auto-document will already be there.

This playbook will collect all possible supported information about a node and print it without logging in to the node. Handy, isn’t it?

---
- name: "IOS Gather Facts"
  hosts: "cml_ios_xe_machines"
  connection: network_cli
  gather_facts: yes
  tasks:
    - name: "Collect Data"
      cisco.ios.ios_facts:
        gather_subset: 'all'
      register: 'ios_deadbeef'
    - name: "Print Data"
      debug:
        msg: '{{ ios_deadbeef }}'

Note that the Jinja2 escaping ('{{}}') is required to print a variable with the debug -> msg combination.

I’d rather not hand off Ansible playbook logs to other engineers, clients, and executives as network documentation, though. Let’s try to make something pretty with Jinja2 and D2, starting with a definitely not confusing .j2 file:

'{{ inventory_hostname }}': {
  icon: 'router.png'
  interfaces: |md
{% for i in ansible_facts.net_interfaces|dict2items %}
{% if i.value.ipv4|length %}
    * {{ i.key }}: {% for ii in i.value.ipv4 %}{{ ii.address }}/{{ ii.subnet }}{% endfor %}  ({{ i.value.type }})
{% endif %}
{% endfor %}
  |
}

This Jinja2 template will be universal to any router it’s executed on, and print all interfaces with ipv4 addresses. We’re also using |dict2items because the key for i isn’t visible otherwise. It formats the dictionary like so:

{
    "key": "name"
    "value": {
        "key": "stuff"
    }
}

Rendering a D2 document will require several stages:

After all that work, this is all that’s required to auto-document an IOS node:

---
- name: "IOS Gather Facts"
  hosts: "cml_ios_xe_machines"
  connection: network_cli
  gather_facts: yes
  tasks:
    - name: "Use facts to draw a node diagram"
      template:
        src: 'node_diagram.j2'
        dest: '{{ inventory_hostname }}.d2'
    - name: "Render node diagrams!"
      ansible.builtin.shell: 'd2 "{{ item }}" "{{ item }}.png" --sketch'
      with_fileglob: '*.d2'

With the tool provided, it’ll generate a network diagram node (as always with the glorious Crayon Visio stencils):

Network Diagram Node

Conclusions / Lessons Learned

Network Engineers tend to be pretty rigid about standards - which lends itself to this type of automation. The examples in this blog post are designed to scale - it’ll generate hundreds of images if given hundreds of IOS nodes, saving unimaginable hours of time.

In the future, I’d expand the scope of templating far beyond a simple diagram. Auto-generating HTML, Markdown, Microsoft Word documents (if you must) are all well-supported by Jinja2 - it just needs to be available as text somehow. There’s a distinct beauty to providing solution delivery complete with unique, use-case customized documentation every time.

I can see full operating manuals and nighttime procedure runbooks being delivered to IT consumers using the simple methods outlined here - it’s a bright outlook for tomorrow’s IT service quality.