Nornir Napalm Plugin (VI)

Nornir Napalm Plugin (VI)
In: Nornir

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.

Nornir Network Automation Full Course
Nornir is a Python library designed for Network Automation tasks. It enables Network Engineers to use Python to manage and automate their network devices.

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 the nornir_napalm plugin takes a string containing CLI commands (config) and pushes it to the devices specified in the inventory. The configuration parameter is where you pass the configuration commands you want to apply, and dry_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
A Simple Network CI/CD Pipeline with Containerlab and Nornir
In this blog post, let’s look at a very simple Network CI/CD pipeline that manages my Containerlab network topology and configurations. We’ll start with the benefits of using CI/CD

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 %}
Containerlab - Creating Network Labs Can’t be Any Easier
What if I tell you that all you need is just a YAML file with just a bunch of lines to create a Network Lab that can run easily on your laptop? I’ll walk you through what Containerlab is

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.

Generating Cisco Interface Configurations with Jinja2 Template
In this blog post, we will explore the process of generating Cisco interface configurations using Python and Jinja2. An interface configuration can vary depending on
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 the vars 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. The dry_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
Table of Contents
Written by
Suresh Vina
Tech enthusiast sharing Networking, Cloud & Automation insights. Join me in a welcoming space to learn & grow with simplicity and practicality.
Comments
More from Packetswitch
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Packetswitch.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.