Welcome back to part 6 of the 'Nornir Network Automation Course'. Up to this point, we've explored Nornir basics, how to use Netmiko with Nornir, and Nornir plugins such as load_yaml
and nornir_jinja2
. In this part, we will dive into the nornir_napalm
plugin, which integrates the functionality of Napalm into Nornir.
If you are completely new to Nornir, I recommend checking out the introduction post linked below to get up to speed.
A Simple Napalm Example
Let’s start with a straightforward example using the Napalm plugin in Nornir. This example shows how to push a couple of lines of NTP configuration to network devices. First, ensure you have the necessary plugin by installing nornir_napalm
with the following command.
pip install nornir_napalm
Here’s a basic script to push NTP server configurations
from nornir import InitNornir
from nornir_utils.plugins.functions import print_result
from nornir_napalm.plugins.tasks import napalm_configure
config = """
ntp server 1.2.3.4
ntp server 2.3.4.5
"""
nr = InitNornir(config_file='config.yaml')
result = nr.run(task=napalm_configure,
configuration=config,
dry_run=False)
print_result(result)
- Initialization - The script starts by importing necessary modules and initializing Nornir using the
config.yaml
file. This file contains the network inventory including devices and their connection details. - Napalm Configure Task - The main action occurs with
napalm_configure
. This task from thenornir_napalm
plugin takes a string containing CLI commands (config
) and pushes it to the devices specified in the inventory. Theconfiguration
parameter is where you pass the configuration commands you want to apply, anddry_run=False
ensures that the changes are actually applied to the devices. - Output - After executing the task, the
print_result
function is used to display the outcome.
napalm_configure****************************************************************
* access-01 ** changed : True **************************************************
vvvv napalm_configure ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
@@ -36,4 +36,7 @@
!
ip route 0.0.0.0/0 192.168.100.1
!
+ntp server 1.2.3.4
+ntp server 2.3.4.5
+!
end
^^^^ END napalm_configure ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* access-02 ** changed : True **************************************************
vvvv napalm_configure ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
@@ -36,4 +36,7 @@
!
ip route 0.0.0.0/0 192.168.100.1
!
+ntp server 1.2.3.4
+ntp server 2.3.4.5
+!
end
^^^^ END napalm_configure ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* aggr-01 ** changed : True ****************************************************
vvvv napalm_configure ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
@@ -36,4 +36,7 @@
!
ip route 0.0.0.0/0 192.168.100.1
!
+ntp server 1.2.3.4
+ntp server 2.3.4.5
+!
end
^^^^ END napalm_configure ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* core-01 ** changed : True ****************************************************
vvvv napalm_configure ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
@@ -32,4 +32,7 @@
!
ip route 0.0.0.0/0 192.168.100.1
!
+ntp server 1.2.3.4
+ntp server 2.3.4.5
+!
end
^^^^ END napalm_configure ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If I log into one of the devices, I can see the configuration correctly applied as shown below.
no ip routing
!
ip route 0.0.0.0/0 192.168.100.1
!
ntp server 1.2.3.4 << here
ntp server 2.3.4.5 << here
!
end
Let's Dive Deep into Napalm Plugin
In this second example, let's look at a bit more advanced scenario. Our goal here is to manage the full configurations of a few network devices using both Napalm and Nornir. While Nornir will continue to manage device inventory and task execution, we'll use Napalm to push configurations to the devices. We'll also make use of two other plugins load_yaml
and nornir_jinja2
.
Instead of manually configuring the devices or dealing with configurations in a CLI-like syntax, we will have a YAML file that describes the configuration. We will then use a Jinja2 template to render these configurations into CLI-like commands. Finally, using Napalm, we will push these configurations to the devices.
Diagram - Simple Network Topology
For this example, we are working with a very simple network topology. Please bear in mind, that this topology does not include redundancy; the simplicity is intentional to keep our focus on the task at hand.
Our network consists of one core switch, one aggregation switch, and two access switches. We will be configuring two VLANs, VLAN 10 and VLAN 20 on all switches. A port-channel between the core and the aggregation switch which will also be a trunk port. The link between aggregation and the access switch is also a trunk port. Some ports on the access switches will be set as access ports for these VLANs. Additionally, the gateway for the clients will be configured on the core switch (core-01
) as SVIs.
Nornir Files
As always, I will set up the necessary Nornir files to manage our network automation tasks. This includes creating the inventory files and the Nornir configuration file, alongside a Python script to execute our automation tasks.
In addition to these standard files, I'm also creating a vars.yaml
file where we'll store the configuration variables for our network devices. Furthermore, I will also create a config.j2
Jinja2 template file, which will be used to render these configuration variables into CLI-compatible commands that can be pushed to devices via Napalm.
#directory structure
.
├── config.yaml
├── defaults.yaml
├── groups.yaml
├── hosts.yaml
├── push_config.py
├── templates
│ └── config.j2
└── vars
└── vars.yaml
#config.yaml
---
inventory:
plugin: SimpleInventory
options:
host_file: 'hosts.yaml'
group_file: 'groups.yaml'
defaults_file: 'defaults.yaml'
runner:
plugin: threaded
options:
num_workers: 5
#hosts.yaml
---
core-01:
hostname: 192.168.100.210
groups:
- arista
aggr-01:
hostname: 192.168.100.211
groups:
- arista
access-01:
hostname: 192.168.100.215
groups:
- arista
access-02:
hostname: 192.168.100.216
groups:
- arista
#groups.yaml
---
arista:
connection_options:
napalm:
platform: eos
extras:
optional_args:
transport: ssh
#defaults.yaml
---
username: admin
password: admin
#vars.yaml
---
core-01:
ip_routing: True
vlans:
- 10
- 20
interfaces:
- name: eth1
mode: trunk
vlan: 10,20
po: 1
- name: eth2
mode: trunk
vlan: 10,20
po: 1
- name: po1
mode: trunk
vlan: 10,20
- name: vlan 10
ip: 10.125.10.1/24
- name: vlan 20
ip: 10.125.20.1/24
aggr-01:
vlans:
- 10
- 20
interfaces:
- name: eth1
mode: trunk
vlan: 10,20
po: 1
- name: eth2
mode: trunk
vlan: 10,20
po: 1
- name: po1
mode: trunk
vlan: 10,20
- name: eth3
mode: trunk
vlan: 10,20
- name: eth4
mode: trunk
vlan: 10,20
access-01:
vlans:
- 10
- 20
interfaces:
- name: eth1
mode: trunk
vlan: 10,20
- name: eth5
mode: access
vlan: 10
- name: eth6
mode: access
vlan: 10
- name: eth7
mode: access
vlan: 20
access-02:
vlans:
- 10
- 20
interfaces:
- name: eth1
mode: trunk
vlan: 10,20
- name: eth5
mode: access
vlan: 10
- name: eth6
mode: access
vlan: 10
- name: eth7
mode: access
vlan: 20
#config.j2
!
{% if ip_routing is defined %}
ip routing
{% endif %}
{% for vlan in vlans %}
vlan {{ vlan }}
!
{% endfor %}
{% for interface in interfaces %}
interface {{ interface.name }}
no shut
{% if interface.ip is defined %}
ip address {{ interface.ip }}
{% elif interface.mode == "trunk" and interface.po is defined %}
channel-group {{ interface.po }} mode active
{% elif interface.mode == "trunk"%}
switchport mode trunk
switchport trunk allowed vlan {{ interface.vlan }}
{% elif interface.mode == "access"%}
switchport mode access
switchport access vlan {{ interface.vlan }}
{% endif %}
!
{% endfor %}
The vars.yaml
file is where we store all our device configurations in a clear and organized way. This makes it easy to read and update settings without dealing with the command-line interface directly. It's also handy if you ever need to switch to a different vendor, as the main structure of the file won't change, just the details/template specific to the new devices.
The config.j2
template takes the information from vars.yaml
and turns it into commands that network devices can understand. It automatically adjusts commands based on whether an interface is for access or trunk ports, or if it needs specific IP addresses. If you are new to Jinja2, please check out my other introductory post here.
from nornir import InitNornir
from nornir_jinja2.plugins.tasks import template_file
from nornir_utils.plugins.functions import print_result
from nornir_utils.plugins.tasks.data import load_yaml
from nornir_napalm.plugins.tasks import napalm_configure
def render_config(task):
read_vars = task.run(
task=load_yaml,
file=f"vars/vars.yaml"
)
vars = read_vars.result
task.host.data.update(vars[task.host.name])
result = task.run(
task=template_file,
template='config.j2',
path='templates/',
**task.host
)
rendered_config = result[0].result
task.host['rendered_config'] = rendered_config
def napalm_send_config(task):
host = task.host
config = host['rendered_config']
task.run(task=napalm_configure, configuration=config, dry_run=False)
nr = InitNornir(config_file='config.yaml')
nr.run(task=render_config)
config_result = nr.run(task=napalm_send_config)
print_result(config_result)
In the above Nornir script,
- The
render_config
function is called for each device in the network. It first loads the variables specific to each device from a YAML file located in thevars
folder. - These variables are then used to fill out a Jinja2 template (
config.j2
), which converts them into the actual CLI commands needed for the device configurations. - Once the configurations are rendered into their final form, the
napalm_send_config
function takes over. It uses the Napalm plugin to push these configurations directly to the devices. Thedry_run=False
parameter ensures that the changes are actually applied.
Here is the output when you run the script.
napalm_send_config**************************************************************
* access-01 ** changed : True **************************************************
vvvv napalm_send_config ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- napalm_configure ** changed : True ---------------------------------------- INFO
@@ -12,6 +12,8 @@
!
spanning-tree mode mstp
!
+vlan 10,20
+!
management api http-commands
no shutdown
!
@@ -22,12 +24,17 @@
transport ssh default
!
interface Ethernet1
+ switchport trunk allowed vlan 10,20
+ switchport mode trunk
!
interface Ethernet5
+ switchport access vlan 10
!
interface Ethernet6
+ switchport access vlan 10
!
interface Ethernet7
+ switchport access vlan 20
!
interface Management0
ip address 192.168.100.215/24
^^^^ END napalm_send_config ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* access-02 ** changed : True **************************************************
vvvv napalm_send_config ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- napalm_configure ** changed : True ---------------------------------------- INFO
@@ -12,6 +12,8 @@
!
spanning-tree mode mstp
!
+vlan 10,20
+!
management api http-commands
no shutdown
!
@@ -22,12 +24,17 @@
transport ssh default
!
interface Ethernet1
+ switchport trunk allowed vlan 10,20
+ switchport mode trunk
!
interface Ethernet5
+ switchport access vlan 10
!
interface Ethernet6
+ switchport access vlan 10
!
interface Ethernet7
+ switchport access vlan 20
!
interface Management0
ip address 192.168.100.216/24
^^^^ END napalm_send_config ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* aggr-01 ** changed : True ****************************************************
vvvv napalm_send_config ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- napalm_configure ** changed : True ---------------------------------------- INFO
@@ -12,6 +12,8 @@
!
spanning-tree mode mstp
!
+vlan 10,20
+!
management api http-commands
no shutdown
!
@@ -21,13 +23,23 @@
management api netconf
transport ssh default
!
+interface Port-Channel1
+ switchport trunk allowed vlan 10,20
+ switchport mode trunk
+!
interface Ethernet1
+ channel-group 1 mode active
!
interface Ethernet2
+ channel-group 1 mode active
!
interface Ethernet3
+ switchport trunk allowed vlan 10,20
+ switchport mode trunk
!
interface Ethernet4
+ switchport trunk allowed vlan 10,20
+ switchport mode trunk
!
interface Management0
ip address 192.168.100.211/24
^^^^ END napalm_send_config ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* core-01 ** changed : True ****************************************************
vvvv napalm_send_config ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- napalm_configure ** changed : True ---------------------------------------- INFO
@@ -12,6 +12,8 @@
!
spanning-tree mode mstp
!
+vlan 10,20
+!
management api http-commands
no shutdown
!
@@ -21,14 +23,26 @@
management api netconf
transport ssh default
!
+interface Port-Channel1
+ switchport trunk allowed vlan 10,20
+ switchport mode trunk
+!
interface Ethernet1
+ channel-group 1 mode active
!
interface Ethernet2
+ channel-group 1 mode active
!
interface Management0
ip address 192.168.100.210/24
!
-no ip routing
+interface Vlan10
+ ip address 10.125.10.1/24
+!
+interface Vlan20
+ ip address 10.125.20.1/24
+!
+ip routing
!
ip route 0.0.0.0/0 192.168.100.1
!
^^^^ END napalm_send_config ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When you execute the script, Nornir picks each device in turn and retrieves the variables specific to that device from the vars.yaml
file. These variables are then passed to Jinja2, which uses them to generate the configuration for that particular device.
Once the configuration is generated, it’s handed over to Napalm. Napalm takes this configuration and pushes it directly to the device. All of these steps happen concurrently for each device, meaning that the configurations are pushed out simultaneously to all devices.
Verification
Let's log in to one of the switches and make sure we can ping the end devices in both VLANs.
core-01>en
core-01#ping 10.125.10.11
PING 10.125.10.11 (10.125.10.11) 72(100) bytes of data.
80 bytes from 10.125.10.11: icmp_seq=1 ttl=64 time=20.8 ms
80 bytes from 10.125.10.11: icmp_seq=2 ttl=64 time=11.7 ms
80 bytes from 10.125.10.11: icmp_seq=3 ttl=64 time=8.73 ms
80 bytes from 10.125.10.11: icmp_seq=4 ttl=64 time=7.74 ms
80 bytes from 10.125.10.11: icmp_seq=5 ttl=64 time=9.95 ms
--- 10.125.10.11 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 55ms
rtt min/avg/max/mdev = 7.747/11.803/20.856/4.718 ms, pipe 3, ipg/ewma 13.839/16.135 ms
core-01#ping 10.125.20.10
PING 10.125.20.10 (10.125.20.10) 72(100) bytes of data.
80 bytes from 10.125.20.10: icmp_seq=1 ttl=64 time=21.3 ms
80 bytes from 10.125.20.10: icmp_seq=2 ttl=64 time=12.9 ms
80 bytes from 10.125.20.10: icmp_seq=3 ttl=64 time=9.20 ms
80 bytes from 10.125.20.10: icmp_seq=4 ttl=64 time=7.69 ms
80 bytes from 10.125.20.10: icmp_seq=5 ttl=64 time=7.20 ms
--- 10.125.20.10 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 56ms
rtt min/avg/max/mdev = 7.209/11.676/21.329/5.231 ms, pipe 3, ipg/ewma 14.166/16.210 ms