Ansible and Network Backup

Author: Kirk Byers
Date: 2017-02-27

There are obviously a lot of solutions for backing up network device configurations. Let's look at how this could be accomplished using Ansible..

Backing up Cisco IOS

First, let's start by backing up some Cisco IOS devices (i.e. just using plain-old SSH).

Now at the Ansible level, one of the first things I need to create is my Ansible inventory. I start by specifying my initial inventory as follows:

[all:vars]
ansible_python_interpreter=/home/kbyers/VENV/ansible/bin/python
ansible_connection=local

[local]
localhost

[cisco]
pynet-rtr1 host=10.10.10.70
pynet-rtr2 host=10.10.10.71

[cisco:vars] 
device_type=cisco_ios
username=pyclass
password=invalid

Note, since all the devices will be using the same 'ansible_python_interpreter' and also will be using 'ansible_connection=local', I placed these variables in the [all:vars] section.

Now let's try to construct a simple playbook that connects to these two Cisco devices and retrieves some 'facts' back from them.

We should be able to accomplish this using the 'ios_facts' module. Consequently, I constructed an Ansible playbook as follows:

---
- name: Save Configurations (IOS)
  hosts: cisco
  vars:
    creds:
      host: "{{ host }}"
      username: "{{ username }}"
      password: "{{ password }}"
  tasks:
    - ios_facts:
        provider: "{{ creds }}"

Pretty simple, I call the ios_facts module and pass in the {{ creds }} variable which contains the 'host', 'username', and 'password' (where all of these variables are pulled from the Ansible inventory).

Executing this playbook works as expected (and I can view the view the facts output by adding the '-v' argument).

$ ansible-playbook save_config.yml 

PLAY [Save Configurations (IOS)] ***********************************************

TASK [setup] *******************************************************************
ok: [pynet-rtr1]
ok: [pynet-rtr2]

TASK [ios_facts] ***************************************************************
ok: [pynet-rtr2]
ok: [pynet-rtr1]

PLAY RECAP *********************************************************************
pynet-rtr1                 : ok=2    changed=0    unreachable=0    failed=0   
pynet-rtr2                 : ok=2    changed=0    unreachable=0    failed=0

Obtaining the running-config

Now let me modify this playbook and obtain the running configuration; for this I will use the 'ios_command' module.

Here is the new playbook using ios_command. A few things to note, I am using the 'register' command to save the the output into a variable named 'show_run'. I am also using the 'debug' module to print the show_run variable to the screen.

---
- name: Save Configurations (IOS)
  hosts: cisco
  vars:
    creds:
      host: "{{ host }}"
      username: "{{ username }}"
      password: "{{ password }}"
  tasks:
    - ios_command:
        provider: "{{ creds }}"
        commands: show run 
      register: show_run

    - debug:
        msg: "{{ show_run }}"

Now one thing I want to look at here is the data structure that is returned by ios_command. In other words, what exactly does the 'show_run' variable contain. Is it a string, a dictionary, or something else.

Upon inspection of the output, I worked out that 'show_run' is a dictionary and the key that I need to access is named 'stdout'. Additionally, inside this is a list and I need element 0. Consequently, what I need is {{ show_run.stdout[0] }}. Updating my playbook to reflect this I have:

---
    - debug:
        msg: "{{ show_run.stdout[0] }}"

I then verify that I am obtaining the running config by executing the playbook:

$ ansible-playbook save_config.yml 

PLAY [Save Configurations (IOS)] ***********************************************

TASK [setup] *******************************************************************
ok: [pynet-rtr1]
ok: [pynet-rtr2]

TASK [ios_command] *************************************************************
ok: [pynet-rtr2]
ok: [pynet-rtr1]

TASK [debug] *******************************************************************
ok: [pynet-rtr1] => {
    "msg": "Building configuration...\n\nCurrent configuration : 5566 bytes\n!\n! Last configuration change at 04:15:45 PST Sun Feb 26 2017 by pyclass\n! ...omitted rest of the config"
}
ok: [pynet-rtr2] => {
    "msg": "Building configuration...\n\nCurrent configuration : 2424 bytes\n!\n! Last configuration change at 17:30:28 PST Sun Feb 26 2017 by pyclass\n! ...omitted rest of the config"
}

PLAY RECAP *********************************************************************
pynet-rtr1                 : ok=3    changed=0    unreachable=0    failed=0   
pynet-rtr2                 : ok=3    changed=0    unreachable=0    failed=0

Now I have the running-config, let me save it to the file system. This can be accomplished pretty easily using the Ansible copy module. Consequently, I added the following task to my playbook:

    - copy:
        content: "{{ show_run.stdout[0] }}"
        dest: "CFGS/{{ inventory_hostname }}.txt"

Here the playbook will take the contents of the variable show_run.stdout[0] (i.e. the running-config) and save it to the ./CFGS directory using the device name defined in inventory.

Now I can execute my playbook and verify that my configurations have been saved (note, I disabled the Ansible 'debug' task in the playbook as it was no longer needed).

$ ansible-playbook save_config.yml 

PLAY [Save Configurations (IOS)] ***********************************************

TASK [setup] *******************************************************************
ok: [pynet-rtr1]
ok: [pynet-rtr2]

TASK [ios_command] *************************************************************
ok: [pynet-rtr2]
ok: [pynet-rtr1]

TASK [copy] ********************************************************************
changed: [pynet-rtr2]
changed: [pynet-rtr1]

PLAY RECAP *********************************************************************
pynet-rtr1                 : ok=3    changed=1    unreachable=0    failed=0   
pynet-rtr2                 : ok=3    changed=1    unreachable=0    failed=0   

$ ls CFGS/
pynet-rtr1.txt  pynet-rtr2.txt

Expanding to Arista EOS and Cisco NXOS

Now we have a working solution for Cisco IOS let's expand this system to add some Arista EOS devices and some Cisco NXOS switches.

For Arista EOS we will use Arista's EAPI; for Nexus I will use SSH. Note, I originally intended to use NX-API for Nexus, but ran into some problems with the nxos_command module and NX-API. I eventually obtained a solution that used NX-API, but it was after I had completed most of this article.

The first thing we need to update is the Ansible inventory. Now instead of two Cisco IOS switches, I will have two Cisco IOS switches, four Arista vEOS switches, and two NXOSv switches. My new inventory file looks as follows:

$ cat ~/ansible-hosts
[all:vars]
ansible_python_interpreter=/home/kbyers/VENV/ansible/bin/python
ansible_connection=local

[local]
localhost

[cisco]
pynet-rtr1 host=10.10.10.70
pynet-rtr2 host=10.10.10.71

[cisco:vars] 
device_type=cisco_ios
username=pyclass
password=invalid

[arista]
pynet-sw1 host=10.10.10.72
pynet-sw2 host=10.10.10.73
pynet-sw3 host=10.10.10.74
pynet-sw4 host=10.10.10.75

[arista:vars]
username=admin1
password=invalid
eapi_port=443

[nxos]
nxos1 host=10.10.10.126
nxos2 host=10.10.10.240

[nxos:vars]
username=pyclass
password=invalid

I now need to construct a new playbook.

Initially, I tried to construct this playbook using a single Ansible play with multiple tasks. Unfortunately, this led me to add a lot of logic into the playbook (i.e. a lot of Ansible conditionals). Eventually, I concluded I would be better of having multiple Ansible plays (one play for Cisco IOS; one play for Arista; and one play for NXOS).

Here is what the finished playbook looks like:

---
- name: Save Configurations (IOS)
  hosts: cisco
  gather_facts: no
  vars:
    creds:
      host: "{{ host }}"
      username: "{{ username }}"
      password: "{{ password }}"
  tasks:
    - ios_command:
        provider: "{{ creds }}"
        commands: show run
      register: show_run

    - copy:
        content: "{{ show_run.stdout[0] }}"
        dest: "CFGS/{{ inventory_hostname }}.txt"

- name: Save running config to file (Arista)
  hosts: arista
  gather_facts: no
  tasks:
    - eos_command:
        host: "{{ host }}"
        username: "{{ username }}"
        password: "{{ password }}"
        transport: https
        commands: show running-config
        encoding: text
      register: show_run

    - copy:
        content: "{{  show_run.output[0].result.output }}"
        dest: "CFGS/{{ inventory_hostname }}.txt"

- name: Save running config to file (NXOS)
  hosts: nxos
  gather_facts: no
  vars:
    creds:
        host: "{{ host }}"
        username: "{{ username }}"
        password: "{{ password }}"
        transport: cli

  tasks:
    - nxos_command:
        provider: "{{ creds }}"
        commands: show running-config
      register: show_run

    - copy:
        content: "{{ show_run.stdout[0] }}"
        dest: "CFGS/{{ inventory_hostname }}.txt"

And for the grand finale, here is what the playbook looks like when it runs:

$ ansible-playbook save_config.yml

PLAY [Save Configurations (IOS)] ***********************************************

TASK [ios_command] *************************************************************
ok: [pynet-rtr2]
ok: [pynet-rtr1]

TASK [copy] ********************************************************************
ok: [pynet-rtr1]
changed: [pynet-rtr2]

PLAY [Save running config to file (Arista)] ************************************

TASK [eos_command] *************************************************************
ok: [pynet-sw3]
ok: [pynet-sw1]
ok: [pynet-sw2]
ok: [pynet-sw4]

TASK [copy] ********************************************************************
changed: [pynet-sw2]
changed: [pynet-sw1]
changed: [pynet-sw3]
changed: [pynet-sw4]

PLAY [Save running config to file (NXOS)] **************************************

TASK [nxos_command] ************************************************************
ok: [nxos2]
ok: [nxos1]

TASK [copy] ********************************************************************
changed: [nxos2]
changed: [nxos1]

PLAY RECAP *********************************************************************
nxos1                      : ok=2    changed=1    unreachable=0    failed=0   
nxos2                      : ok=2    changed=1    unreachable=0    failed=0   
pynet-rtr1                 : ok=2    changed=0    unreachable=0    failed=0   
pynet-rtr2                 : ok=2    changed=1    unreachable=0    failed=0   
pynet-sw1                  : ok=2    changed=1    unreachable=0    failed=0   
pynet-sw2                  : ok=2    changed=1    unreachable=0    failed=0   
pynet-sw3                  : ok=2    changed=1    unreachable=0    failed=0   
pynet-sw4                  : ok=2    changed=1    unreachable=0    failed=0

And at the end of this, I have saved the configurations of the eight network devices:

$ ls -al CFGS/
total 68
drwxrwxr-x 2 kbyers kbyers  4096 Feb 26 18:07 .
drwxrwxr-x 3 kbyers kbyers  4096 Feb 26 18:04 ..
-rw-rw-r-- 1 kbyers kbyers 13851 Feb 26 18:07 nxos1.txt
-rw-rw-r-- 1 kbyers kbyers 13851 Feb 26 18:07 nxos2.txt
-rw-rw-r-- 1 kbyers kbyers  5627 Feb 26 17:54 pynet-rtr1.txt
-rw-rw-r-- 1 kbyers kbyers  2485 Feb 26 18:07 pynet-rtr2.txt
-rw-rw-r-- 1 kbyers kbyers  1053 Feb 26 18:07 pynet-sw1.txt
-rw-rw-r-- 1 kbyers kbyers  1123 Feb 26 18:07 pynet-sw2.txt
-rw-rw-r-- 1 kbyers kbyers   971 Feb 26 18:07 pynet-sw3.txt
-rw-rw-r-- 1 kbyers kbyers   971 Feb 26 18:07 pynet-sw4.txt

Conclusions

The documentation of the Ansible Core networking modules (like eos_facts, eos_command, nxos_command) is very poor. This caused me to waste a lot of time (trying to working out which arguments to use). Similarly, I was frustrated by inconsistencies in the output and behavior of the different modules across platforms.

Kirk Byers

@kirkbyers

You might also be interested in: