Backing up device configurations using Nornir and Ansible

In this blog, I’m going to go over how to run a configuration backup on a Cisco IOS device using Ansible and Nornir. I’ve heard many people say, “well which tool should I use to run against my infrastructure?” That’s a great question but it depends 🙂. There are many factors that could affect the decision of using a specific tool i.e skill level, time, access to tools, team preference…the list goes on.

Hopefully, the examples shown here can help you determine which tool best fits the need. I’m not going deep into the details on the different technologies or explaining all the components of each of the files, but I’ll provide some links to learn more about them if needed.

Installation

If you want to follow along with the blog, Nornir and Ansible need to be installed in the machine locally or you will need to use tools that isolate dependencies like Docker and virtual environments. I ended up using Docker so if you have Docker installed in your machine and know how to use it, then feel free to use the Dockerfile below:

FROM python:3.7-stretch

WORKDIR /ntc

RUN apt-get update \
&& apt-get install tree
RUN pip install black \
ansible \
nornir

After adding and saving the commands to the Dockerfile run the following commands to build the container image and start running the container.

docker build -t nornir .
docker run -it --name nornir_ansible -v ${PWD}:/ntc nornir_ansible:latest /bin/bash

Backing up with Nornir

Nornir is an automation tool used to interact with networking devices. This tool is a bit different compared to other tools in the way that you write your own Python code to control the automation. For example, Ansible is written in Python but uses its own DSL which you use to describe what you want to have done. Nornir does not need a DSL instead, you can write everything in Python.

Configuration File

Now that everything has been installed, the first file to create is the Nornir configuration file. This file is similar to the ansible.cfg file which helps set all the system defaults, but in this case it’s going to be used to find the inventory file. Click on the following links to learn more about Nornir and Ansible configuration files:

Create a file called nornir_config.yml and inside the file store the following content:


---
inventory:
    plugin: nornir.plugins.inventory.ansible.AnsibleInventory
    options:
        hostsfile: "./inventory"

Note: Since this container has access to the local machine, any tool or editors can be used with the same files the container is able to see. I’ll be using Visual Studio Code in my local machine.

Inside the container run the cat command to make sure changes took place in the file.

root@c2e446b364a8:/ntc# cat nornir_config.yml
---
inventory:
    plugin: nornir.plugins.inventory.ansible.AnsibleInventory
    options:
        hostsfile: "./inventory"

Note: The plugin used here AnsibleInventory allows us to source an inventory file that would normally be consumed by Ansible. There are other plugins that can be used like simple, NSOT and NetBox.

Inventory File

The inventory file built here will be used for both Ansible and Nornir. The main differences here will be the hostvars names. For now Nornir will be using hostname, platform, username and password. Later, when the Ansible section comes, up a few other variables will be added. Create a file called inventory and place it in the same directory path as the nornir_config.yml file. Save it and run the cat command to make sure it saved properly and it’s seen by the container.

root@c2e446b364a8:/ntc# cat inventory
[all]
csr1 hostname=csr1 platform=ios username=ntc_user password=pass123

Click on the following links to learn more about Nornir and Ansible inventory:

Backup Python Script

Now that the configuration and inventory file has been created it’s time to build the script that will backup the device configuration. The script will consist of functions that will build the necessary directory, backup files, and two different functions using the netmiko and napalm modules to backup the device configurations.

The first step to building the script is to import all the needed libraries. In this case, the libraries needed are:

  • os: This library is going to be used to help interact with the local environment and build the backup directory and file names for the configurations.
  • nornir: Nornir is an automation framework written in Python to be used with Python.
    • InitNornir: Object used to provide the configuration file containing system defaults. In this case, it’s being used to provide the inventory file.
    • netmiko_send_command: It’s a netmiko function used to send arbitrary commands to networking devices.
    • napalm_get: Function that comes from the NAPALM library to get facts from the device. In this case, it will be used to retrieve the device configuration.
import os
import logging
from nornir import InitNornir
from nornir.plugins.tasks.networking import netmiko_send_command, napalm_get

Two global variables need to be created. The first one, BACKUP_DIR, is the directory name used to store the backed up files and the second,nr, is used to carry the data stored in the nornir_config.yml file. In this case, it will contain the inventory file data.

BACKUP_DIR = "backups/"

nr = InitNornir(config_file="./nornir_config.yml")

The create_backups_dir is used to create a local directory called backups and it’s using the os library to create a directory with the name stored in the BACKUP_DIR variable.

def create_backups_dir():
    if not os.path.exists(BACKUP_DIR):
        os.mkdir(BACKUP_DIR)

The save_config_to_file function is used to create the backed up device configuration files inside the backups directory. Notice the parameters used will take as input method, hostname and config, these parameters will get their data from the get_netmiko_backups() and get_napalm_backups() functions that will be created in the next few steps.

def save_config_to_file(method, hostname, config):
    filename =  f"{hostname}-{method}.cfg"
    with open(os.path.join(BACKUP_DIR, filename), "w") as f:
        f.write(config)

The get_netmiko_backups function will run the netmiko_send_command module to send a “show run” command to the network device stored in the nr variable and it will be stored in the backups_results variable.

def get_netmiko_backups():
    backup_results = nr.run(
        task=netmiko_send_command, 
        command_string="show run"
        )

    for hostname in backup_results:
        save_config_to_file(
            method="netmiko",
            hostname=hostname,
            config=backup_results[hostname][0].result,
        )

The backups_results variable will end up with a value that stores the hostname and configuration as a result looking something like this:

>>> print(backup_results)
AggregatedResult (netmiko_send_command): {'csr1': MultiResult: [Result: "netmiko_send_command"]}
>>>
>>>

Note: AggregatedResult is a dict-like object that aggregates the results for all devices. You can access each individual result by doing my_aggr_result["hostname_of_device"]

The data inside backups_results can be extracted by using a for loop and stored in hostname. The data iterated through with the for loop will return the hostname information and backup configuration.

The get_napalm_backups function will be built the same way, except this time it will be using the napalm_get module to extract the device configuration.

def get_napalm_backups():
    backup_results = nr.run(task=napalm_get, getters=["config"])

    for hostname in backup_results:
        config = backup_results[hostname][0].result["config"]["startup"]
        save_config_to_file(method="napalm", hostname=hostname, config=config)

The last function main() will execute all the functions and the code entry point is added to start the script when executed.

def main():
    create_backups_dir()
    get_netmiko_backups()
    get_napalm_backups()

if __name__ == "__main__":
    main()

Once the script has been built, execute the command black to clean up and make sure all formatting is correct and use the command cat to make sure everything has been saved correctly. Click on the following link to learn more about black.

root@c2e446b364a8:/ntc# black backups.py
All done! ✨ 🍰 ✨
1 file left unchanged.
root@6b91330f9674:/ntc# cat backups.py
import os
from nornir import InitNornir
from nornir.plugins.tasks.networking import netmiko_send_command, napalm_get

BACKUP_DIR = "backups/"

nr = InitNornir(config_file="./nornir_config.yml")


def create_backups_dir():
    if not os.path.exists(BACKUP_DIR):
        os.mkdir(BACKUP_DIR)


def save_config_to_file(method, hostname, config):
    filename =  f"{hostname}-{method}.cfg"
    with open(os.path.join(BACKUP_DIR, hostname), "w") as f:
        f.write(config)


def get_netmiko_backups():
    backup_results = nr.run(
        task=netmiko_send_command, 
        command_string="show run"
        )

    for hostname in backup_results:
        save_config_to_file(
            method="netmiko",
            hostname=hostname,
            config=backup_results[hostname][0].result,
        )


def get_napalm_backups():
    backup_results = nr.run(task=napalm_get, getters=["config"])

    for hostname in backup_results:
        config = backup_results[hostname][0].result["config"]["startup"]
        save_config_to_file(method="napalm", hostname=hostname, config=config)


def main():
    create_backups_dir()
    get_netmiko_backups()
    get_napalm_backups()


if __name__ == "__main__":
    main()

Running the Python Script

Now that the script is complete, it’s time to execute it by using the Python command and file name right next to it python backups.py.

root@c2e446b364a8:/ntc# python backups.py
root@c2e446b364a8:/ntc#

The tree command can be used to view the configuration files stored in the directory backups configuration files stored inside of it with the specified file names built in the save_config_to_file function.

root@c2e446b364a8:/ntc# tree
.
├── Dockerfile
├── backups
│   ├── csr1-napalm.cfg
│   └── csr1-netmiko.cfg
├── backups.py
├── inventory
├── nornir.log
└── nornir_config.yml

1 directory, 7 files

Note: Take a look at the backed up configuration files using the cat command or an editor of choice. They are left off from this as they take up a lot of screen space.

By default, Nornir automatically configures logging when InitNornir is called and a file is created locally that can be viewed what functions where ran that belong to the Nornir library.

root@c2e446b364a8:/ntc# cat nornir.log
2020-01-02 21:33:26,519 -  nornir.core -     INFO -        run() - Running task 'netmiko_send_command' with args {'command_string': 'show run'} on 1 hosts
2020-01-02 21:33:32,134 -  nornir.core -     INFO -        run() - Running task 'napalm_get' with args {'getters': ['config']} on 1 hosts

Backing up with Ansible

Ansible is a configuration management tool originally created to interact with Linux servers, eventually it started gaining traction in the networking industry to manage networking device configurations. Ansible was built using Python but it uses YAML to configure all the automation tasks. The idea is to simplify the work that it takes to start automating.

Configuration file

The ansible.cfg configuration file is used to set some of the system default values. Create the ansible.cfg file in the root of the directory next to all the other files created previously. Then use the cat command to make sure everything was stored correctly.

  • inventory: Sets the location of the inventory file, by default it will look for it in etc/ansible/hosts or while running the playbook the -i flag can be used to point to the location of the file. In this case, it is pointed to the inventory file we will create in the next step within the same directory.
  • host_key_checking: This has been disabled to prevent host key checking and prevent issues when running the playbook.
  • interpreter_python: Has been set in the ansible.cfg file to prevent a new warning added to the new version of Ansible and specify what interpreter to use.
  • gathering: Is set to smart to gather facts by default, but don’t regather if already gathered.
root@c2e446b364a8:/ntc# cat ansible.cfg
[defaults]
inventory      = ./inventory
host_key_checking = False
interpreter_python = /usr/bin/python
gathering = smart

Inventory

Edit the same inventory file that was used for Nornir, except this time add the following variables for Ansible.

  • ansible_user=ntc_user
  • ansible_password=pass123
  • ansible_network_os=ios

Again use the cat command to make sure the changes took effect.

root@c2e446b364a8:/ntc# cat inventory
[all]
csr1 hostname=csr1 platform=ios username=ntc_user password=pass123 ansible_user=ntc_user ansible_password=pass123 ansible_network_os=ios

Playbook

The first part to the Playbook is the play definition. The play definition will contain a list of keys such as:

  • name: Any arbitrary name for the Playbook.
  • hosts: The devices that will be targeted in the scope of the play.
  • connection: Type of connection. In this case network_cli which is basically SSH.
  • tasks: The key that contains all the automation steps that belong to the play.
  • gather_facts: Before Ansible version 2.9, this key was normally disabled when interacting with networking devices. Now, it has the ability to gather device facts at the play definition level rather than at the task level.

The second part of the Playbook nested under the tasks key will have a list of key:value pair tasks.

  • name: Optional but recommended key that can have any arbitrary name for the task.
  • ios_config: This is the Python module used to run configuration commands against ios devices.
  • backup: Module parameter used to backup the device configuration. By default, if the backup directory is not created then it will create one in the same location as the Ansible playbook and store the configuration files in the backup directory.

Note: A recent parameter was added as backup_options with sub-options to change the directory path rather than the default which is backup and the ability to change the filename of the device configuration. In this case, the default value is set with this format <hostname>_config.<current-date>@<current-time>

Create the Ansible Playbook and call it backup_playbook.yml. Use the cat command to make sure the content is stored correctly:

root@c2e446b364a8:/ntc# cat backup_playbook.yml
---
    - name: BACKUP PLAYBOOK
      hosts: csr1
      connection: network_cli
      gather_facts: yes
      tasks:

        - name: BACKUP USING IOS_CONFIG
          ios_config:
            backup: yes

Running the Playbook

To run the Ansible Playbook use the command ansible-playbook backup_playbook.yml. The output is shown below:

Note: There is an extra task that says Gathering Facts which is a new feature added to Ansible that now gathers facts from networking devices at the play definition level. Take a look at this blog Ansible gather_facts for Networking Devices to learn a little more about it.

root@c2e446b364a8:/ntc# ansible-playbook backup_playbook.yml

PLAY [BACKUP PLAYBOOK] **********************************************************************************

TASK [Gathering Facts] **********************************************************************************
[WARNING]: Ignoring timeout(10) for ios_facts

ok: [csr1]

TASK [BACKUP USING IOS_CONFIG] ***************************************************************************
changed: [csr1]

PLAY RECAP ***********************************************************************************************
csr1                 : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Use the tree command to view the content inside and notice how the device configuration is stored in the backup directory:

root@c2e446b364a8:/ntc# tree
.
├── Dockerfile
├── ansible.cfg
├── backup
│   └── csr1_config.2020-01-03@15:43:16
├── backup_playbook.yml
├── backups
│   ├── csr1-napalm.cfg
│   └── csr1-netmiko.cfg
├── backups.py
├── inventory
├── nornir.log
└── nornir_config.yml

2 directories, 10 files

Backing up with ios_facts

There’s another way of backing up the device configurations by using ios_facts, except this time rather than creating a new task to gather facts we can leverage the gathering of facts that takes place at the play definition level and add a task that will create a new directory to store the device configurations and another task to copy the content from ansible_facts as a configuration file.


---
    - name: BACKUP PLAYBOOK
      hosts: csr1
      connection: network_cli
      gather_facts: yes
      tasks:

        - name: BACKUP USING IOS_CONFIG
          ios_config:
            backup: yes
            
        - name: CREATE BACKUP DIRECTORY
          file:
            path: ./ansible_backup
            state: directory
       
        - name: BACKUP USING IOS_FACTS
          copy:
            content: "{{ ansible_net_config }}"
            dest: ./ansible_backup/{{ inventory_hostname }}_config.{{ now() }}.cfg

In the playbook above, the copy module is used to copy content stored in the ansible_net_config key that is found inside ansible_facts. Any net_* key under ansible_facts can be accessed directly by using the ansible_net_* key rather than accessing the nested data structure. Now the new tasks have been added the playbook can be run again.

root@c2e446b364a8:/ntc# ansible-playbook backup_playbook.yml

PLAY [BACKUP PLAYBOOK] **********************************************************************************

TASK [Gathering Facts] **********************************************************************************
[WARNING]: Ignoring timeout(10) for ios_facts

ok: [csr1]

TASK [BACKUP USING IOS_CONFIG] ***************************************************************************
changed: [csr1]

TASK [CREATE BACKUP DIRECTORY] ****************************************************************************
changed: [csr1]

TASK [BACKUP USING IOS_FACTS] *****************************************************************************
changed: [csr1]

PLAY RECAP *************************************************************************************************
csr1            : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Notice how every time the first task is run a new backup configuration is created since it’s been run with a different time stamp. The same can be done with the ios_facts backup method.

root@c2e446b364a8:/ntc# tree<br>.<br>├── Dockerfile<br>├── ansible.cfg<br>├── ansible_backup<br>│   └── csr1_config.2020-01-03 19:01:43.277008.cfg<br>├── backup<br>│   ├── csr1_config.2020-01-03@15:43:16<br>│   └── csr1_config.2020-01-03@19:01:42<br>├── backup_playbook.yml<br>├── backups<br>│   ├── csr1-napalm.cfg<br>│   └── csr1-netmiko.cfg<br>├── backups.py<br>├── csr1_config.2020-01-03@15:44:07<br>├── inventory<br>├── nornir.log<br>└── nornir_config.yml<br><br>3 directories, 13 files

Conclusion

There are many automation tools and modules that can help automate the backup of device configurations. The ones shown in this blog are some of the basic ways to do it, which ever method you choose will do the job. There can be different modifications that can be done to the Nornir script or even the Ansible playbook to better fit your application and hopefully what I have shown here can give you a start on what tool to use.

-Hector


Tags :
No blog categories assigned to this post.

ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Author