Network device configuration backup with Ansible

Network automation or automation in general is a hot topic these days. And indeed it would be prefereble to automate repetitive tasks or even write the entire desired state of your network in a tool like Puppet or Ansible. But learning how to use such a tool like is a large task. And like the proverbial Elephant it is best to tackle this in small “slices”.

I myself decided to learn Ansible, as that is what most of my customers and partners are using, making it easier to support eachother. A first small project to start this journey is automating the configuration backup of my home network. I have been making backups using Rancid for the last two decades (yes, I’m getting old). Rancid is a great tool and even allows for relatively simple automation by using “clogin -x” to run commands from a .cmd file. But Ansible is a much more potent tool for network automation, hence my decission to learn Ansible. And the first task I set myself is writing a playbook to replace the backup functionality of Rancid.

In this post I will give an example playbooks and related configuration files but teaching Ansible from scratch is beyond the scope of this post. There is an Ansible course on CBT Nuggets that you could follow and I found a course specific to network automation on INE. If you do not have a subscription you could always check out one of the many Ansible 101 blogs, I found a very nice blog here. For this post I will assume you have basic Ansible knowledge, so let’s get going…

Below this paragraph is the playbook, it will backup Cisco, Cisco Small Business, Ubiquity (Edgemax and Unify), Mikrotik (RouterOS) and VyOS devices. You can extend this to include whatever vendors’ equipment you like. The only requirement is that you can login using SSH, althoug the use of the network_cli module is more elegant.

---
############################
### Cisco Small Business ###
############################
- name: Get config from all Cisco small business devices
  hosts: IOSSB
  gather_facts: no

  tasks:
    - name: Collect Show run from all routers
      raw: "show run"

      register: RUNCFG

    - set_fact: time="{{lookup('pipe','date \"+%Y-%m-%d-%H-%M\"')}}"

    - name: save output to a file
      connection: local
      copy:
        content="\n ===show run=== \n {{ RUNCFG.stdout }}"
        dest="../backups/IOSSB/{{ inventory_hostname }}_run_cfg_{{ time }}.txt"

#################################
### Ubiquiti EdgeOS and Unifi ###
#################################
- name: Get config from all Ubiquiti EdgeOS devices
  hosts: EdgeOS
  connection: network_cli
  gather_facts: no

  tasks:
    - name: Collect configuration
      edgeos_command:
        commands: show configuration

      register: RUNCFG

    - name: Collect configuration commands
      edgeos_command:
        commands: show configuration commands

      register: RUNCFG2

    - set_fact: time="{{lookup('pipe','date \"+%Y-%m-%d-%H-%M\"')}}"

    - name: save output to a file
      connection: local
      copy:
        content="===show configuration===\n{{ RUNCFG.stdout|join('\n') }}\n===show configuration commands===\n{{ RUNCFG2.stdout|join('\n') }}\n"
        dest="../backups/EdgeOS/{{ inventory_hostname }}_run_cfg_{{ time }}.txt"

############
### VyOS ###
############
- name: Get config from all VyOS devices
  hosts: VyOS
  connection: network_cli
  gather_facts: no

  tasks:
    - name: Collect configuration
      vyos_command:
        commands: show configuration

      register: RUNCFG

    - name: Collect configuration commands
      vyos_command:
        commands: show configuration commands

      register: RUNCFG2

    - set_fact: time="{{lookup('pipe','date \"+%Y-%m-%d-%H-%M\"')}}"

    - name: save output to a file
      connection: local
      copy:
        content="===show configuration===\n{{ RUNCFG.stdout|join('\n') }}\n===show configuration commands===\n{{ RUNCFG2.stdout|join('\n') }}\n"
        dest="../backups/VyOS/{{ inventory_hostname }}_run_cfg_{{ time }}.txt"

#########################
### Cisco classic IOS ###
#########################
- name: Get config from all Cisco IOS devices
  hosts: IOS
  gather_facts: no

  tasks:
    - name: Collect Show run from all routers
      raw: "show run"

      register: RUNCFG

    - set_fact: time="{{lookup('pipe','date \"+%Y-%m-%d-%H-%M\"')}}"

    - name: save output to a file
      connection: local
      copy:
        content="\n ===show run=== \n {{ RUNCFG.stdout }}"
        dest="../backups/IOS/{{ inventory_hostname }}_run_cfg_{{ time }}.txt"

################
### RouterOS ###
################
- name: Get config from all Mikrotik RouterOS devices
  hosts: RouterOS
  connection: network_cli
  gather_facts: no

  tasks:
    - name: Collect system package print detail output
      routeros_command:
        commands: system package print detail without-paging

      register: SYSPKGDTL

    - name: Collect system routerboard print output
      routeros_command:
        commands: system routerboard print

      register: SYSRTRBRD

    - name: Collect system license print output
      routeros_command:
        commands: system license print

      register: SYSLIC

    - name: Export configuration commands
      routeros_command:
        commands: export

      register: EXPORT

    - set_fact: time="{{lookup('pipe','date \"+%Y-%m-%d-%H-%M\"')}}"

    - name: save output to a file
      connection: local
      copy:
        content="===system package print detail===\n{{ SYSPKGDTL.stdout|join('\n') }}\n===system routerboard print===\n{{ SYSRTRBRD.stdout|join('\n') }}\n===system license print===\n{{ SYSLIC.stdout|join('\n') }}\n===export configuration===\n{{ EXPORT.stdout|join('\n') }}\n"
        dest="../backups/RouterOS/{{ inventory_hostname }}_run_cfg_{{ time }}.txt"

The playbook above uses both the network_cli and raw ssh access. On my system the files are stored in $HOME/ansible_projects/backup/backups/ and my playbook is located in $HOME/ansible_projects/backup/playbooks/. You can set the directory by changing the “dest=” lines in the playbook.

The playbook needs to know which devices to backup and which “play” to use for each device. This is set in the inventory file. The location of the inventroy file is set in the ansible.cfg file, on my system that is located in /etc/ansibel/ and it looks like this (only non default values are shown for brevity):

# config file for ansible -- http://ansible.com/
# ==============================================
# nearly all parameters can be overridden in ansible-playbook
# or with command line flags. ansible will read ANSIBLE_CONFIG,
# ansible.cfg in the current working directory, .ansible.cfg in
# the home directory or /etc/ansible/ansible.cfg, whichever it
# finds first

[defaults]
# some basic default values...
inventory      = /etc/ansible/hosts

# plays will gather facts by default, which contain information about
# the remote system.
# smart - gather by default, but don't regather if already gathered
# implicit - gather by default, turn off with gather_facts: False
# explicit - do not gather by default, must say gather_facts: True
gathering = explicit

# uncomment this to disable SSH key host checking
host_key_checking = False

# SSH timeout
timeout = 10

# by default (as of 1.4), Ansible may display deprecation warnings for language
# features that should no longer be used and will be removed in future versions.
# to disable these warnings, set the following value to False:
#deprecation_warnings = True
deprecation_warnings = False

# retry files
# When a playbook fails by default a .retry file will be created in ~/
# You can disable this feature by setting retry_files_enabled to False
# and you can change the location of the files by setting retry_files_save_path
retry_files_enabled = False

In the inventory file you can group devices together and set username and password information to be used by the playbooks. Mine looks like this:

# This is the default ansible 'hosts' file.
# It should live in /etc/ansible/hosts
#   - Comments begin with the '#' character
#   - Blank lines are ignored
#   - Groups of hosts are delimited by [header] elements
#   - You can enter hostnames or ip addresses
#   - A hostname/ip can be a member of multiple groups

[IOS]
sw2 ansible_host=your_ip_addr ansible_user=your_user ansible_ssh_pass=your_pass

[IOSSB]
sw4 ansible_host=your_ip_addr ansible_user=your_user ansible_ssh_pass=your_pass

[VyOS]
vr1 ansible_ssh_host=your_ip_addr ansible_ssh_user=your_user ansible_ssh_pass=your_pass ansible_network_os=vyos

[EdgeOS]
usg1 ansible_ssh_host=your_ip_addr ansible_ssh_user=your_user ansible_ssh_pass=your_pass ansible_network_os=edgeos

[RouterOS]
mt-r1 ansible_ssh_host=your_ip_addr ansible_ssh_user=your_user ansible_ssh_pass=your_pass ansible_network_os=routeros

[DEBIAN]
nurse ansible_host=your_ip_addr ansible_connection=ssh ansible_user=your_user ansible_ssh_pass=your_pass

[UBUNTU]
lab ansible_host=your_ip_addr ansible_connection=ssh ansible_user=your_user ansible_ssh_pass=your_pass

[APT:children]
DEBIAN
UBUNTU

[ALL:children]
IOS
IOSSB
EdgeOS
VyOS
RouterOS

In the inventory file I have listed 1 example device per category, but when you specify more devices under for example the “[RouterOS]” header the playbook will process them all. You can make groups by using a “[GROUPNAME:children]” header, as you can see at the bottom of my inventory file. You may also have noticed the Linux systems in my inventory. I use ansible to update them periodically and automatically, I will write about that in a separate post.

With the playbook, the configuration file and the inventory file we have all the ingredients we need. Let’s run the playbook. Before running a playbook you can check if the syntax is correct.

mloesb@nurse:~/ansible_projects/backup$ ansible-playbook playbooks/backup_network_devices.yml --syntax-check

playbook: playbooks/backup_network_devices.yml

When the syntax check returns only the name of the playbook (as in the example above) it means that your syntax is correct and you can proceed to running the playbook.

mloesb@nurse:~/ansible_projects/backup$ ansible-playbook playbooks/backup_network_devices.yml

PLAY [Get config from all Cisco small business devices] ******************************************************************************************************

TASK [Collect Show run from all routers] *********************************************************************************************************************
changed: [sw4]

TASK [set_fact] **********************************************************************************************************************************************
ok: [sw4]

TASK [save output to a file] *********************************************************************************************************************************
changed: [sw4]

PLAY [Get config from all Ubiquiti EdgeOS devices] ***********************************************************************************************************
skipping: no hosts matched

PLAY [Get config from all VyOS devices] **********************************************************************************************************************

TASK [Collect configuration] *********************************************************************************************************************************
ok: [nld-stb-dwh-r1]
ok: [r1-dmvpn]
ok: [vr1]

TASK [Collect configuration commands] ************************************************************************************************************************
ok: [nld-stb-dwh-r1]
ok: [r1-dmvpn]
ok: [vr1]

TASK [set_fact] **********************************************************************************************************************************************
ok: [r1-dmvpn]
ok: [nld-stb-dwh-r1]
ok: [vr1]

TASK [save output to a file] *********************************************************************************************************************************
changed: [nld-stb-dwh-r1]
changed: [vr1]
changed: [r1-dmvpn]

PLAY [Get config from all Cisco IOS devices] *****************************************************************************************************************

TASK [Collect Show run from all routers] *********************************************************************************************************************
changed: [sw2]

TASK [set_fact] **********************************************************************************************************************************************
ok: [sw2]

TASK [save output to a file] *********************************************************************************************************************************
changed: [sw2]

PLAY [Get config from all Mikrotik RouterOS devices] *********************************************************************************************************

TASK [Collect system package print detail output] ************************************************************************************************************
ok: [mt-r2]
ok: [mt-r1]

TASK [Collect system routerboard print output] ***************************************************************************************************************
ok: [mt-r1]
ok: [mt-r2]

TASK [Collect system license print output] *******************************************************************************************************************
ok: [mt-r2]
ok: [mt-r1]

TASK [Collect system routerboard print output] ***************************************************************************************************************
ok: [mt-r1]
ok: [mt-r2]

TASK [Collect system license print output] *******************************************************************************************************************
ok: [mt-r2]
ok: [mt-r1]

TASK [Export configuration commands] *************************************************************************************************************************
ok: [mt-r1]
ok: [mt-r2]

TASK [set_fact] **********************************************************************************************************************************************
ok: [mt-r2]
ok: [mt-r1]

TASK [save output to a file] *********************************************************************************************************************************
changed: [mt-r1]
changed: [mt-r2]

PLAY RECAP ***************************************************************************************************************************************************
mt-r1                      : ok=6    changed=1    unreachable=0    failed=0    skipped=0
mt-r2                      : ok=6    changed=1    unreachable=0    failed=0    skipped=0
nld-stb-dwh-r1             : ok=4    changed=1    unreachable=0    failed=0    skipped=0
r1-dmvpn                   : ok=4    changed=1    unreachable=0    failed=0    skipped=0
sw2                        : ok=3    changed=2    unreachable=0    failed=0    skipped=0
sw4                        : ok=3    changed=2    unreachable=0    failed=0    skipped=0
vr1                        : ok=4    changed=1    unreachable=0    failed=0    skipped=0

mloesb@nurse:~/ansible_projects/backup$

In the summary you can see if everything ran as you expected. We can manually check the results by looking in the outpur directory:

mloesb@nurse:~/ansible_projects/backup/backups$ ls -1
EdgeOS
IOS
IOSSB
RouterOS
VyOS

mloesb@nurse:~/ansible_projects/backup/backups/RouterOS$ ls -lart | tail -3
-rw-r--r-- 1 mloesb mloesb  7459 Oct 19 11:16 mt-r2_run_cfg_2019-10-19-11-16.txt
-rw-r--r-- 1 mloesb mloesb  6458 Oct 19 11:16 mt-r1_run_cfg_2019-10-19-11-16.txt
drwxr-xr-x 2 mloesb mloesb 12288 Oct 19 11:16 .

As you see I used a “tail -3” to show only the last 3 lines of output. This is because there are hundreds of backups in this directory. That is a potential problem as the playbook does not yet do any housekeeping. You have to remove old backups yourself. I could write a small bash script to do basic housekeeping and delete files older than N days but I’m interested in an ansible based solution, when I find out how to do housekeeping in ansible I will write a post about it.

I hope this post has been useful to you. If you are looking for a relatively cheap and very versatile router to test with you could considder the Mikrotik hEX lite router or the Mikrotik HAP ac lite wireless router. If you want to buy this router I would appreciate if you use the affiliate link. This will help enable me to keep writing posts. Should you want to buy the router somewhere else that is offcourse perfectly fine as well.

The HAP ac lite also makes for a very nice travel router that you can take with you on your trips to safely connect all your devices to public WiFi. The HAP ac lite can act as a firewall between your devices and the public network. I will write more about this in the next post. For now I hope this post was interesting and I would like to thank you for reading.

Update (November 15th 2019):The backup of Cisco Small Business switches started to consistently fail some time ago. Maybe related to an update of the firmware. An internet searched learned me that changing the playbook helps, I had to change it to this;

    ############################
    ### Cisco Small Business ###
    ############################
    - name: Get config from all Cisco small business devices
      hosts: IOSSB
      connection: local
      gather_facts: no
    
      tasks:
      - name: gather running config
        ios_command:
          commands:
            - terminal datadump
            - show run
    #      provider: "{{ cli }}"
    
        register: RUNCFG
    
      - set_fact: time="{{lookup('pipe','date \"+%Y-%m-%d-%H-%M\"')}}"
    
      - name: save output to a file
        connection: local
        copy:
          content="\n ===show run=== \n {{ RUNCFG.stdout }}"
          dest="../backups/IOSSB/{{ inventory_hostname }}_run_cfg_{{ time }}.txt"

A problem with the new playbook is that the backup of small business switches is now all on one line with “\n” where a new line should have been. I make the backup files readable when I need them by running them through this little bash oneliner;

sed 's/\\n/\n/g' unreadable_backup_file > human_readable_file

Of course, should I find a fix for the playbook that makes the original backup file human readable I will update this post.

This entry was posted in Cisco, Linux, Mikrotik, Networking, Systems, Ubiquiti. Bookmark the permalink.