Ansible and Cisco IOS

Matt Haught
Matt Haught
Ansible and Cisco IOS

Ansible is a wonderful tool for configuring and automating Cisco devices, but there are some quarks to working with these devices and maintaining idempotency in your tasks. With IOS/IOSXE you are not editing a file, but instead you are executing commands which modify the config. The config will match lines in the config... usually.

Shortened Commands

With IOS you can shorten commands and they will be accepted, but this breaks idempotency.

SwitchA(config)# int gi0/0/0

The actual config will be:

interface GigabitEthernet0/0/0

It is essential that the full command is used in your playbook tasks. Without doing so the cisco ios modules will always see the need to run the config line even if it is already there.

Input != Output

Just as with the shortened commands, sometimes IOS changes what you input to better match what it wants in the config. For example, when adding a rule to a standard ACL, it has the common format ip {{ ip address }} {{ wildcard }}. So for a single IP you can use a wildcard of, and the syntax is accepted.


To me it is more consistent and it "works", but IOS changes this to:

  permit host

This again makes your task not idempotent. So you need to check that what you give for a command matches up with what it stores in the config.

Configuration is Instant

When making changes in configuration mode on IOS, the changes are applied instantly for the most part unlike many other network OSs where you commit changes. This can pose problems when working with for example ACLs. If a task removes all of a sections' settings before adding the ones you want, you could lock yourself out. If you remove radius configs before adding the correct ones and an error occurs, well you are going to be making an drive out to an IDF.

Removing Old Config Lines

So plopping in new config lines is crazy easy, and if you run your playbook a second time, the module will check and see that your entries are already there and skip them. This works great for single items or a set of lines in a section, but what about old cruft settings in your config? I really want to provide a config state and only have the settings I provide in the config. So take this example config for syslog logging:


Well lets say that first entry is now old and a new server has been provisioned at that you need to add. To make matters worse, you have years of handcrafted configs in production where there are a number of old entries for syslog logging sprinkled around thousands of switches. You could maybe no all the entries, but again that breaks idempotency. There are a few ways to solve this, here is what I have done for many of those tasks:

  register: get_syslog_config
      - "show running-config | include ^logging [0-9]+.[0-9]+.[0-9]+.[0-9]+"

  with_items: "{{ lookup('template', 'ios_syslog.j2').splitlines() }}"
  when: "(item not in get_syslog_config.stdout_lines[0])"
    lines: "{{ item }}"

  with_items: "{{ get_syslog_config.stdout_lines[0] }}"
  when: "(get_syslog_config.stdout_lines[0][0] != '') and (item not in lookup('template', 'ios_syslog.j2').splitlines())"
    lines: "no {{ item }}"
{% for line in syslog_servers %}
logging host {{ line }}{% if not loop.last %}{{ '\n' }}{% endif %}
{% endfor %}

The first task grabs the running config for just syslog lines, and the second and third tasks use a jinja2 template to do a comparison between what we want the config to look like and what we currently have. We add missing syslog lines in the second, and remove lines in the third.

I have used this and similar techniques successfully or setting syslog, name-servers, radius, tacacs+, ntp...


Working with Ansible and the Cisco CLI is challenging but it is feasible with a little practice. NETCONF/YANG are enticing alternatives as the biggest issues with the CLI are that you are working with unstructured data.