Nornir 3.0 – Multi-Vendor L3VPN/VPRN (IOSxR/Nokia) IaC – Configuration Deployment via NETCONF/NORNIR

What a title to this post, right?

With the recent release of Nornir 3.0  – I wanted to explore the capabilities of Nornir and I already know, I will prob never use Ansible for Network Automation ever again.. 😉 However, the reason for this post is to give a high level overview of Nornir 3.0 and provide a guide to convert 2.x Nornir/Netconf scripts over to 3.0.

Some of the topics explored in this post will include but not limited to the following:

  1. Infrastructure as Code : (Jinja2 Template Rendering and YAML defined network state)
  2. Nornir 3.0.
    1. Installation
    2. Plugins
    3. Directory Structure
  3. NETCONF/YANG
  4. Netmiko

How to Follow Along

I’d recommed to download the code from my github and review the repo. Once you are familiar with the code you should be ready to start reading along.

CODE:

https://github.com/h4ndzdatm0ld/Norconf

Review the topology below: You will become the operator of this network throughout this journey. The solution you are implementing will ease the workload of the deployment engineers and possibly save your company some money. Depending on the issue that XYZ company is trying to solve, it’s becoming clear that not every one requires a high dollar solution to automate their networks with vendor specific nms, orchestrators, etc. Screen Shot 2020-09-21 at 3.58.38 PM

For those of you new to Nornir, it’s an automation framework written in Python.  If you are familiar with Ansible, you can adapt quite easily to Nornir as long as you know how to get around with python. You will quickly realize how flexible it is. One of my favorite features of Nornir is multithreading, allowing concurrent connections which in return makes this framework incredibly fast. We will discuss the topic of workers/threads a little more later in this post.

Getting Started

Begin by installing nornir with a simple pip3 install nornir 

Lets discuss the directory structure. You can see here there is quite a bit going on..

NOTE: All of the following files/directories have to manually be created. These are not autocreated. Take a minute and re-create the folders/files under a chosen filepath. I started a git repo and this is where I created all my folders.

We’ve created a defaults, groups and hosts yml file under our ‘inventory‘ directory. We actually have a config.yml file which specifies the path location of these filesScreen Shot 2020-09-22 at 9.37.12 AM. This config file is later passed into the nornir class that’s instantiated inside our python runbook, norconf.py. As always, our ‘templates‘ folder contains our Jinja2 files with the appropriate template files to render the configuration of the L3VPN and VPRNs for our multivendor environment. These are named according to their corresponding host platform and function.

Template Naming Example:

              • {iosxr}-(platform) -{vrf}-(purpose)-.j2 (extension). The actual template is XML data following yang models.  The reason to use the platform in the naming scheme, is to be able to use the host platform during our program execution and match the name of the file with the help of f strings. This is just one way to do it and you can find other ways that make more sense to your deployment.

Additional files in here, such as nc_tasks.py are adopted from Nick Russos project which uses nornir 2.X. He’s configured some custom netconf tasks at a time in which netconf was originally being introduced into Nornir. The Log file is self explanatory.

At the time of this writing, the nornir_netconf plugin is not yet available for Nornir 3.0 as a direct pip dowload/install.  What I have done is a series of try/except and mostly failures to get this to work. I had to take a step back and understand a lot of what’s happening under the hood of nornir.  I’ve cloned the REPO @ https://github.com/nornir-automation/nornir_netconf@first and tried to install it via Poetry, but this was mostly a huge waste of time and nothing worked, particularly with the plugin configuration of Nornir. I removed the installation and went the pip route straight from git.

I was able to install the the code by using pip + git using the following:

pip3 install git+https://github.com/nornir-automation/nornir_netconf@first

However, during the process I got an exception “AttributeError: module ‘enum’ has no attribute ‘IntFlag'” From some searching around, it’s due to a discrepency with using enum34. I ran the following to ensure the package was present and removed it.

pip freeze | grep enum34

➜ nornir_netconf-first pip3 freeze | grep enum34
enum34==1.1.10

Looks like I do have it in installed …  A quick, ‘pip3 uninstall enum34’ and re-ran the original pip3 install from git+git_page and the installation was successfull. I wonder what I broke by removing enum34 😉

Installing collected packages: nornir-netconf
Successfully installed nornir-netconf-1.0.0

Python 3.8.2 (v3.8.2:7b3ab5921f, Feb 24 2020, 17:52:18)
[Clang 6.0 (clang-600.0.57)] on darwin
Type “help”, “copyright”, “credits” or “license” for more information.
>>> import nornir_netconf
>>> print(SO FAR SO GOOD!)

I was having an issue with nornir netconf plugin originally and had to investigate how to manually register a plugin. That is before I found out how to get around the hurdle and install via git+pip. Here is the code I used to manually register the plugin in my runbook directly, in case anyone ever wants to register a new plugin..although a lot has to happen for any of this to work.

from nornir.core.plugins.connections import ConnectionPluginRegister
from nornir_netconf.plugins.connections import Netconf
ConnectionPluginRegister.register(“netconf”, ConnectionPluginRegister)
So, at this point – the netconf plugin is working.  The only problem I see with the nornir_netconf plugin is the returned output. After all this work, I realized if you do a print_result, to extract all the output – you don’t exactly get what you need to verify the sucess of the rpc-operation, such as the rpc_reply. This is a little troublesome. However, I did find that the custom netconf function written by Nick Russo gave me exactly what I need. At this time, I will not be using the nornir_netconf methods and instead import the custom russo tasks. See below:

 


def netconf_edit_config(task, target, config, **kwargs):
"""
Nornir task to issue a NETCONF edit_config RPC with optional keyword
arguments. Both the target and config arguments must be specified.
"""
conn = task.host.get_connection("netconf", task.nornir.config)
result = conn.edit_config(target=target, config=config, **kwargs)
return Result(host=task.host, result=result)

Importing this function, I am actually able to receive this rpc_reply from a successfull RPC operation. This is critical to the operation of my script – as I write conditional statements depending on the returning output of the tasks.run result. 

<?xml version=”1.0″?>
<rpc-reply message-id=”urn:uuid:28f57844-94cb-4ecc-b927-ba1f5318eab7″ xmlns:nc=”urn:ietf:params:xml:ns:netconf:base:1.0″ xmlns=”urn:ietf:params:xml:ns:netconf:base:1.0″>
<ok/>
</rpc-reply>
During the time of this writing I did express my concern on the lack of response from this edit_config function on the soon to be introduced nornir_netconf plugin to Patrick Ogenstad who is leading the development of the netconf plugin. Sounds like he may be updating the code to actually return rpc reply in the output, more to come on that. As of now, I will continue with Russo’s custom function as I see this being a requirement for any netconf python script to properply acknowledge the result. Additionally, his nc_tasks.py file contains a netconf_commit function which is a necessity for applying configurations against candidate target stores.
I treat NETCONF as an API. I need a response and I need a response NOW! 😉
Okay, enough about getting NETCONF to work on this new Nornir version.
Lets go over the inventory directory and the defaults, groups and hosts file.

The Host File:

R3_CSR:
hostname: 192.168.0.223
groups:
  – CSR
R3_SROS_PE:
hostname: 192.168.0.222
groups:
  – NOKIA
data:
  region: west-region
R8_IOSXR_PE:
hostname: 192.168.0.182
groups:
  – IOSXR
data:
  region: west-region
A simple yaml file that looks very familiar.. this should be an easy transition for all the Ansible folks. You define a host, hostname and specify a group to avoid duplication of data.
This is an example of the groups file – The inheritance from the group file is passed straight to each host that’s part of the group. If there is data inside the defaults yaml file, this is also inherited to all hosts. Something to keep in mind.

The Group File

Screen Shot 2020-09-22 at 12.23.32 PM
NOTE:
  • The  data.target key is inherited and called upon during the execution of rpc-edit config to point the operation against the correct netconf data store)
  • These connection options can make or break the process

The Config File

  • A config.yaml file must specify the location of the hosts, groups and defaults fiiles.
Screen Shot 2020-09-22 at 2.10.34 PM

Threads

We have specified 100 num_workers, which really means we can have up to 100 concurrent multithreaded sessions to devices.  The way I think about Nornir running process is everything you’re doing is in a giant ‘for loop’. The tasks runs through all the devices in the inventory (unless you specify a filter) one by one.  Although there isn’t a for statement written anywhere visible, you’re looping through all the devices in your inventory. However, using threads you’re actually doing this is parallel.  You could technically specify the ‘plugin: serial’ and not take advantage of threads.

Plugins

Before we move forward and begin writing our runbook/code – one thing I want to emphasize is the difference in Nornir 2.x and 3.x.  You must individually install the plugins! Here is a link to the current Nornir Plugins available and documentation/how – to from Nornir.
This part is incredibly important, as the power of Nornir is basically to become a sort of orchestartor and controller of these tasks which include running code that take advantage of these plugins. In our code we will use load_yaml, template_file, netmiko and the nornir_utils.
pip3 install nornir_utils
pip3 install nornir_netmiko
pip3 install nornir_jinja2

Run Book (Python Script of Compiled ‘tasks’)

As we begin writing our runbook for our project, lets instantiate the InitNornir class and pass in the custom config.yml file:
from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command
from nornir_utils.plugins.functions import print_result
from nornir_utils.plugins.tasks.data import load_yaml
from nornir_jinja2.plugins.tasks import template_file
from nornir_netconf.plugins.tasks import netconf_edit_config
from nc_tasks import netconf_edit_config, netconf_commit
import xmltodict, json, pprint
__author__ = ‘Hugo Tinoco’
__email__ = ‘hugotinoco@icloud.com’
# Specify a custom config yaml file.
nr = InitNornir(‘config.yml‘)

Filters

Screen Shot 2020-09-21 at 3.58.38 PM

What are filters and how do we create them? A filter is a selection of hosts in which you want to execute a runbook against. For our main example in this post, we are an operator who is in charge of deploying a L3VPN/VPRN in a multi-vendor environemnt at the core. This will include Nokia SR 7750 and Cisco IOSxR. However, our hosts file contains ALL of our devices that are available in our network. The L3VPN we are deploying is only spanning across our ‘west-region’ pictured on the bottom left of the topology above. There are two CPE’s, one attached to the Nokia 7750 and one to the Cisco IOSxR. In order to deploy this service, we want to specify within Nornir that we only need to execute the tasks against these two specific routers. The rest of the network doesn’t need to know about this service.  Below is a snippet of the ‘hosts.yml’ file which has customized region key and west-region item. You can see this is duplicated to the R8_IOSXR_PE device.  That’s it! We’ve identified common ground between these devices, being in the ‘west-region’ of our network.

R3_SROS_PE:
hostname: 192.168.0.222
groups:
  – NOKIA
data:
  region: west-region
R8_IOSXR_PE:
hostname: 192.168.0.182
groups:
  – IOSXR
data:
  region: west-region

Now lets write some code to ensure nornir knows this is a filter.

# Filter the hosts by the ‘west-region’ site key.
west_region = nr.filter(region=’west-region‘)
Wow, that was a lot.  This will come in handy once we are ready to execute the entire runbook in our main() function. Remember, the west-region is specified in the hosts file. This could also be inherited from the group file, if the hosts belongs to said group.

Infrastructure as Code

We’ll be extracting information from our Yaml files which are variables inputted by the user along side our Jinja2 templates consisting of our Yang Models. We use Jinja2 to distribute the correct variables across our yang models for proper rendering. For distributing  the configurations via NETCONF across our core network we enlist the help of Nornir to manage all of theses tasks. We’re allowing Nornir to handle the flow and procedures to ensure proper deployment.

VARS

Below is the yaml file containing our vars which will be utilized to render the j2 template.  The following is for the Nokia platform:

VRF:
  – SERVICE_NAME: AVIFI
    SERVICE_ID: ‘100’
     CUSTOMER_ID: 200
    CUSTOMER_NAME: AVIFI-CO
    DESCRIPTION: AVIFI-CO
    ASN: ‘64500’
    RD: 100
    RT: 100
    INTERFACE_NAME: TEST-LOOPBACK
    INTERFACE_ADDRESS: 3.3.3.3
    INTERFACE_PREFIX: 32
If you are not familiar with how a L3VPN works, this is the time you can review this topic. In order to properly configure a L3VPN service for your customer, you must provide a service name for your VRF. In the Nokia/SROS world, you must also provide a customer_id, which is passed into the creation of the service for the specific customer. A customer name is also passed into the vars file as you want to specify the name of the customer not just a numerical value(id). The Autonomous system is a requirement for the VRF, along side the route-target and route-distinguisher.  Additionally, we will be creating a loopback interface within the VRF for testing purposes. The goal here is not only to deploy the service but to validate L3 Connectivity across the core via Multi-Protocol BGP (MP-BGP). MPLS has already been configured in the core.

Jinja 2 – yang:sr:conf

There are so many important pieces to construct this automation project. The J2 template file, must include everything that is necessary to create this service. Below is the example for the Nokia device. Please see my code via the github repo at the top of this document to review the IOSxR J2 Template file. There are also supporting documents at the end of this document if you need more information on Jinja2


{% for VRF in data %}
<config>
<configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf">
<service>
<customer>
<customer-name>{{VRF.CUSTOMER_NAME}}</customer-name>
<customer-id>{{VRF.CUSTOMER_ID}}</customer-id>
</customer>
<vprn>
<service-name>{{VRF.SERVICE_NAME}}</service-name>
<service-id>{{VRF.SERVICE_ID}}</service-id>
<admin-state>enable</admin-state>
<customer>{{VRF.CUSTOMER_NAME}}</customer>
<autonomous-system>{{VRF.ASN}}</autonomous-system>
<route-distinguisher>{{VRF.ASN}}:{{VRF.RD}}</route-distinguisher>
<vrf-target>
<community>target:{{VRF.ASN}}:{{VRF.RT}}</community>
</vrf-target>
<auto-bind-tunnel>
<resolution>any</resolution>
</auto-bind-tunnel>
<interface>
<interface-name>{{VRF.INTERFACE_NAME}}</interface-name>
<loopback>true</loopback>
<ipv4>
<primary>
<address>{{VRF.INTERFACE_ADDRESS}}</address>
<prefix-length>{{VRF.INTERFACE_PREFIX}}</prefix-length>
</primary>
</ipv4>
</interface>
</vprn>
</service>
</configure>
</config>
{% endfor %}

Runbook Walkthrough


def get_vrfcli(task, servicename):
''' Retrieve VRF from IOSXR
'''
vrf = task.run(netmiko_send_command, command_string=f"sh vrf {servicename} detail")
print_result(vrf)
def get_vprncli(task, servicename):
''' Retrieve VPRN from Nokia
'''
vprn = task.run(netmiko_send_command, command_string=f"show service id {servicename} base")
print_result(vprn)
def cli_stats(task, **kwargs):
''' Revert to CLI scraping automation to retrieve simple show commands and verify status of Services / L3 connectivity.
'''
# Load Yaml file to extract specific vars
vars_yaml = f"vars/{task.host}.yml"
vars_data = task.run(task=load_yaml, file=vars_yaml)
# Capture the Service Name:
servicename = vars_data.result['VRF'][0]['SERVICE_NAME']
if task.host.platform == 'alcatel_sros':
get_vprncli(task, servicename)
elif task.host.platform == 'iosxr':
get_vrfcli(task, servicename)
else:
print(f"{task.host.platform} Not supported in this runbook")

Our overall goal is to deploy the VPRN/L3VPN. We start by creating a few custom functions.

We create get_vrfcli and get_vprncli. These two functions take advantage of netmiko_send_command plugin and are using platform specific cli commands. We will use these two commands to retrieve the service status. Then we take the two functions and wrap them inside cli_stats.  We load the yaml file using the load_yaml plugin from Nornir. Once the task is executed, we drill into our vars file and extract the service name as a variable from our loaded dictionary (yaml file). This variable is then passed into the get_vrpncli/get_vrfcli functions to execute against our devices. At this point, if we execute the cli_stats tasks against our west-region, we can use conditional statements to execute the correct command against the correct platform device. The way in which we access the platform, is by simply digging into the task.host.platform key. This will return the value of the key.

NOTE:

I am working on a video tutorial and demonstration of Nornir 3.0. During the video, I will create additional tasks in which verify the L3 Connectivity via simple ping commands.

Bulk of the Code:


def iac_render(task):
''' Load the YAML vars and render the Jinja2 Templates. Deploy L3VPN/VPRN via NETCONF.
'''
# Load Yaml Files by Hostname
vars_yaml = f"vars/{task.host}.yml"
vars_data = task.run(task=load_yaml, file=vars_yaml)
# With the YAML variables loaded, render the Jinja2 Template with the previous function: iac_render.
template= f"{task.host.platform}-vrf.j2"
vprn = task.run(task=template_file, path='templates/', template=template, data=vars_data.result['VRF'])
# Convert the generated template into a string.
payload = str(vprn.result)
# Extract the custom data.target attribute passed into the group of hosts to specifcy 'candidate' target
# configuration store to run the edit_config rpc against on our task.device.
# IOSXR/Nokia take advantage of candidate/lock via netconf.
deploy_config = task.run(task=netconf_edit_config, target=task.host['target'], config=payload)
# # Extract the new Service ID Created:
if task.host.platform == 'alcatel_sros':
for vrf in vars_data.result['VRF']:
serviceid = vrf['SERVICE_ID']
servicename = vrf['SERVICE_NAME']
# Ensure the customer – id is always interpreted as a string:
customer = vrf['CUSTOMER_ID']
customerid = str(customer)
if task.host.platform == 'iosxr':
for vrf in vars_data.result['VRF']:
servicename = vrf['SERVICE_NAME']
serviceid = None
rpcreply = deploy_config.result
# if 'ok' in result:
if rpcreply.ok:
print(f"NETCONF RPC = OK. Committing Changes:: {task.host.platform}")
task.run(task=netconf_commit)
# Validate service on the 7750.
if task.host.platform == 'alcatel_sros':
nc_getvprn(task, serviceid=serviceid, servicename=servicename, customerid=customerid)
elif task.host.platform =='iosxr':
pass
# Duplicate the getvprn function but for iosxr
elif rpcreply != rpcreply.ok:
print(rpcreply)
else:
print(f"NETCONF Error. {rpcreply}")

view raw

gistfile1.txt

hosted with ❤ by GitHub

 

Lets review the iac_render function. We simply load our yaml vars and render our j2 templates. Special attention to the following:

template= f”{task.host.platform}-vrf.j2″
This allows us to properly select a template that matches the host during the execution of the task. Using our f-strings, we pass in the task.host.platform and append the ‘-vrf’ which in turn matches the name of our stored xml templates inside our templates directory. Example: “iosxr-vrf.j2”

At this point we have our payload to deploy against our devices. One thing to note, the result of the rendered template using the nornir plugin, template_file is a Nornir Class. Make sure this gets converted to a str: “payload = str(vprn.result)”. We will pass this into our netconf_edit_config task as the payload to deploy via netconf.

deploy_config = task.run(task=netconf_edit_config, target=task.host[‘target’],             config=payload)

Lets examine this line of code. We assign ‘deploy_config’ as the variable for the returning output of our task. The task we will execute is the ‘netconf_edit_config‘ function. Again, this is a wrapper of ncclient, which I hope you’re familiar with – if not, please give it a google search or review the additional resources at the bottom of the doc.
Now, the’ target=task.host[‘target’]’ is the data store to use during our rpc NETCONF call. We specified this for our host inside our groups file. See below:

NOKIA:
username: ‘admin’
password: ‘admin’
platform: alcatel_sros
port: 22
data:
    target: candidate 

NETCONF has three data stores in which we can execute configuration changes against.

  1.  Running
  2.  Startup
  3.  Candidate

In my opinion, candidate is the most valuable operation. We are able to input a config change, validate and once we are sure of the changes we must commit the change. As the operator of this network, we must be sure not to cause any outages or create any rippling effects from our automation. We will insepct the RPC reply and ensure all is good and if so, we will commmit out changes for the customer.

Line 23, has a conditional statement where we dig into the actual platform of the hosts that’s running within our task. We simply compare it to alcatel_sros or iosxr, as those are our two core devices in this example. We extract a couple different items in the result of our loaded yaml file which we will use to return some output to the screen and provide results in a readable format. We do the same with our iosxr results.

At this point, the netconf_edit_config wrapper for ncclient should have executed the netconf rpc and editted the configuration.

We store the reply in a variable called rpcreply, by extracting the .result attribute out of our original deploy_config variable. This gives us the xml reply and we can check the result of the reply by using ‘if rpcreply.ok:’

Line 42 gives us a simple feedback to let us know the result has returned OK. We now run the netconf_commit  task and confirm the change.

Finally, lets validate some of the services applied and use our custom function ‘nc_getvprn’ against our Nokia 7750.

nc_getvprn(task, serviceid=serviceid, servicename=servicename,   customerid=customerid)

Earlier, we extrracted some vars from our yaml file and loaded them into script as the following: ‘serviceid’, ‘servicename’ and ‘customerid’. We use these variables to execute the task and get some information by parsing the result of the netconf_get_config rpc call. We process this information by using xmltodict.parse and converting the xml to a Python dictionary. We compare the values found inside our running configuration against the desired state of our network element. Infrastructue as code is fun right? Once we do some comparasion of our items, we return meaningful output to the screen to let us, the operator know that everything is configured as expected.

If you are not familiar with xmltodict, I will provide additional references at the bottom of this document.

At the time of this writing I only have completed the compliance check against the Nokia sros device. I will most likley be extending this code to do the same against the IOSxR device. Below is the customer ‘nc_getvprn” function which we just described.

 

Execution


def main():
west_region.run(task=iac_render)
west_region.run(task=cli_stats)
if __name__=="__main__":
main()

view raw

nornir-final

hosted with ❤ by GitHub

It’s that easy. We run our tasks against our filter, west_region to narrow down our hosts for our multi-vendor environment. Lets review the output!


➜ Norconf git:(master) ✗ python3 norconf.py
NETCONF RPC = OK. Committing Changes:: alcatel_sros
NETCONF RPC = OK. Committing Changes:: iosxr
SR Customer: AVIFI-CO
SR Customer ID: 200
SR Service Name: AVIFI
vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
===============================================================================
Service Basic Information
===============================================================================
Service Id : 100 Vpn Id : 0
Service Type : VPRN
MACSec enabled : no
Name : AVIFI
Description : (Not Specified)
Customer Id : 200 Creation Origin : manual
Last Status Change: 09/18/2020 08:56:02
Last Mgmt Change : 09/18/2020 08:56:02
Admin State : Up Oper State : Up
Router Oper State : Up
Route Dist. : 64500:100 VPRN Type : regular
Oper Route Dist : 64500:100
Oper RD Type : configured
AS Number : 64500 Router Id : 10.10.10.3
ECMP : Enabled ECMP Max Routes : 1
Max IPv4 Routes : No Limit
Auto Bind Tunnel
Resolution : any
Weighted ECMP : Disabled ECMP Max Routes : 1
Max IPv6 Routes : No Limit
Ignore NH Metric : Disabled
Hash Label : Disabled
Entropy Label : Disabled
Vrf Target : target:64500:100
Vrf Import : None
Vrf Export : None
MVPN Vrf Target : None
MVPN Vrf Import : None
MVPN Vrf Export : None
Car. Sup C-VPN : Disabled
Label mode : vrf
BGP VPN Backup : Disabled
BGP Export Inactv : Disabled
LOG all events : Disabled
SAP Count : 0 SDP Bind Count : 0
VSD Domain : <none>
——————————————————————————-
Service Access & Destination Points
——————————————————————————-
Identifier Type AdmMTU OprMTU Adm Opr
——————————————————————————-
No Matching Entries
===============================================================================
^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
Thu Sep 24 09:10:56.099 UTC
VRF AVIFI; RD 64500:100; VPN ID not set
VRF mode: Regular
Description AVIFI-CO
Interfaces:
Loopback100
Loopback200
Address family IPV4 Unicast
Import VPN route-target communities:
RT:64500:100
Export VPN route-target communities:
RT:64500:100
No import route policy
No export route policy
Address family IPV6 Unicast
No import VPN route-target communities
No export VPN route-target communities
No import route policy
No export route policy

view raw

nornir-result

hosted with ❤ by GitHub

From the output above, we deployed our L3VPN (IOSxR) and VPRN (NOKIA) device. After we take full advantage of our IaC+Nornir, we return back to our CLI Scraping automation and rely on Netmiko to run simple show commands to view the VRF is actually present and validate the services.

Additional Resources:

Kirk Byers Nornir 3.0 Docs

Jinja 2 – Render Templates

NETCONF – ncclient (Github)

XMLTODICT

Leave a comment