Using the official network modules with Ansible 2.0.X

March 13, 2016

About one month ago Ansible released official core modules to work with network equipment. With Ansible 2.1 they will be included in the stable-release. Their functionality is described in the documentation. Different vendors and platforms like Cisco IOS, Cisco NX-OS, Cisco IOS-XR, Juniper JUNOS or Arista EOS are supported.

Typically there are 3 types of modules available per platform. The platform I use in this blog post will be Cisco IOS:

  • command: Run arbitrary commands on IOS devices.
  • config: Manage configuration sections.
  • template: Manage device configurations.

At the moment of writing this post there is a special network branch which includes the new Ansible network modules. The branch is called stable-2.0-network. In this post I want to show how a Cisco-switch can be managed with this new possibilites. My goal is to ensure the basic configuration of the switch (like hostname, logging, ntp, snmp…) with the help of configuration templates. As a templating system jinja2 is used. The ansible playbook ensures that the templates are used for configuring the switch, and writes the config if something changed. As an example I will create a role for common-configurations like the hostname and a role for the logging-configurations.

To get started you have to download and install Ansible with the branch stable-2.0-network:

$ git clone http://github.com/ansible/ansible.git
$ cd ansible
$ git checkout stable-2.0-network
$ git submodule init
$ git submodule update
$ pip uninstall ansible
$ python setup.py install
$ ansible --version
ansible 2.0.1.0 (stable-2.0-network 49f7e6a12c) last updated 2016/03/01 20:57:24 (GMT +200)
  lib/ansible/modules/core: (detached HEAD 72540d1f0c) last updated 2016/03/01 20:57:27 (GMT +200)
  lib/ansible/modules/extras: (detached HEAD 51cddf2b35) last updated 2016/03/01 20:57:28 (GMT +200)

Now we are ready to create our project structure:

$ mkdir ansible-blog-ios-network-modules
$ cd ansible-blog-ios-network-modules
$ mkdir group_vars
$ mkdir roles
$ cd roles
$ mkdir common
$ mkdir logging
$ mkdir writecfg
$ cd common
$ mkdir meta
$ mkdir tasks
$ mkdir templates
$ cd ../logging
$ mkdir meta
$ mkdir tasks
$ mkdir templates
$ cd ../writecfg
$ mkdir handlers

Next let us create the needed files.

directory-listing

This is the yaml-file we are using as initial playbook.

$ cat site.yml
- name: Ensure basic configuration of switches
  connection: local
  hosts: all
  gather_facts: false

  roles:
    - role: common
      tags: common
    - role: logging
      tags: logging

In our inventory only one device is defined for testing. In a real world scenario this can also be a dynamic inventory like the one I mentioned in my last blog post.

$ cat inventory
[all]
switch-01 device_ip=192.168.0.200

We need some variables for our configuration templates and for connecting to the device.

$ cat group_vars/all.yml
---
username: "username"
password: "password"
secret: "password"
logserver: 192.168.1.5
mgmt_vlanid: 1

The common/meta/main.yml of our common-role has the writecfg-role as a dependency. Only if the common-role changes something on the device also the writecfg-role will be called. I use this to ensure that the configuration of the devices only gets written, if there were some changes in the running-configuration.

$ cat roles/common/meta/main.yml
---
dependencies:
  - role: writecfg

The common/tasks/main.yml executes a task in which the ios_template module of Ansible is used to ensure that the configuration defined in common.j2 exists on the IOS-device.

$ cat roles/common/tasks/main.yml
---
- name: ensure common configuration exists
  ios_template:
    src: common.j2
    username: "{{ username }}"
    password: "{{ password }}"
    host: "{{ device_ip }}"
  notify:
    - write config

Thats the jinja2-template which contains the common-configurations we want to ensure on our devices. For testing purposes this is only the hostname at the moment.

$ cat roles/common/templates/common.j2
 hostname {{ ansible_host }} 
$ cat roles/logging/meta/main.yml
---
dependencies:
  - role: writecfg
$ cat roles/logging/tasks/main.yml
---
- name: ensure logging configuration exists
  ios_template:
    src: logging.j2
    username: "{{ username }}"
    password: "{{ password }}"
    host: "{{ device_ip }}"
  notify:
    - write config
$ cat roles/logging/templates/logging.j2
logging buffered 128000
no logging console
no logging monitor
logging {{ logserver }}
logging source-interface Vlan{{ mgmt_vlanid }}

This is the handler of the writecfg-role. It only gets called by the other roles if there were some changes triggered by the common- or logging-role.

$ cat roles/writecfg/handlers/main.yml
---
- name: write config
  ios_command:
    commands: write
    username: "{{ username }}"
    password: "{{ password }}"
    host: "{{ device_ip }}"

Now lets execute this by calling the initial playbook site.yml.

» ansible-playbook site.yml -i inventory

PLAY [Ensure basic configuration of switches switches] *************************

TASK [common : ensure common configuration exists] *****************************
ok: [switch-01]

TASK [logging : ensure logging configuration exists] ***************************
ok: [switch-01]

PLAY RECAP *********************************************************************
switch-01                  : ok=2    changed=0    unreachable=0    failed=0

Ok all configurations seems to already have been on the device. Lets change the logserver-ip in group-vars/all.yml to 192.168.0.20 and execute it again.

» ansible-playbook site.yml -i inventory

PLAY [Ensure basic configuration of switches switches] *************************

TASK [common : ensure common configuration exists] *****************************
ok: [switch-01]

TASK [logging : ensure logging configuration exists] ***************************
changed: [switch-01]

RUNNING HANDLER [writecfg : write config] **************************************
ok: [switch-01]

PLAY RECAP *********************************************************************
switch-01                  : ok=3    changed=1    unreachable=0    failed=0

Notice that the status of logging is changed and the handler for writing the configuration gets called. Now two logging-servers are configured on your device. If you want to change the logging server you can delete it before adding a new one or execute a cleanup-role at the end of all roles, which deletes all configuration-snippets you dont want to use anymore. At this point you could also add other roles like ntp, aaa, ssh… be creative. If your templates are cleverly designed it would be enough to change the variables in group-vars/all.yml to the new values. With this method a basic-configuration on all your devices could be ensured very easy.

Back...