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.
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
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.
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.
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:
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:
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 doingmy_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()
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
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.
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.
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.ansible.cfg
file to prevent a new warning added to the new version of Ansible and specify what interpreter to use.root@c2e446b364a8:/ntc# cat ansible.cfg
[defaults]
inventory = ./inventory
host_key_checking = False
interpreter_python = /usr/bin/python
gathering = smart
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
The first part to the Playbook is the play definition. The play definition will contain a list of keys such as:
network_cli
which is basically SSH.The second part of the Playbook nested under the tasks
key will have a list of key:value
pair tasks.
ios
devices.Note: A recent parameter was added as
backup_options
with sub-options to change the directory path rather than the default which isbackup
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
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
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
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
Share details about yourself & someone from our team will reach out to you ASAP!