In this section you are going to write the Python code to configure PIM Sparse-Mode on the Ethernet interfaces and configure the Rendezvous Point (RP) to allow data-plane Broadcast, Unknown Unicast, and Multicast (BUM) traffic to leverage multicast in the Underlay and control-plane using NX-API REST. This section will require some additional functions to connect and disconnect from the switches using NX-API REST. Additionally, you will need to create a template to configure PIM Sparse-Mode on the Ethernet interfaces and RP using Jinja templating as done in the previous sections.
In your VSCode Terminal, lets make use of the code
keyword to open another new file for creating a Python NX-API REST script:
code -r /home/cisco/Documents/nxapilab/scripts/nxapi_rest.py
In your VSCode, a new, unsaved file should now be open called nxapi_rest.py
.
In your new Python file, you need to import the same packages as before. Either copy or type the import statements below and place them at the top of your Python file.
import argparse
import pathlib
import sys
import jinja2
import yaml
import json
import requests
requests.packages.urllib3.disable_warnings()
Copy or type the function definition that contains the aaaUser
object for logging in and obtaining an authentication token.
The function below makes use of the aaaLogin
class and takes the parameters for the username, password, ip_addr of the switch.
The request here is also done sightly different than in the previous NX-API CLI section to give you exposure to a different way to use the request library.
Instead of using request.post(), you can also make use of request.request(). This requires you to pass the CRUD operation; below it's a "POST" to send the authentication payload.
You still are required to pass the url and payload as parameters.
Just to recap, whenever you make a call using requests, you are creating a Request object which sends a request/query off to the switch(s). Also, a Response object is generated when the Request object gets a reply. The response is stored in the response variable below as a JSON dictionary that could be manipulated as a Python dictionary. The response contains the authentication token that is parsed out by loading the response and indexing the dictionary based on the keys and list location to get the token value. The token is then placed into a auth_cookie dictionary and returned. Subsequent requests can make use of this token to perform CRUD operations.
def aaa_login(username, password, ip_addr):
payload = {
'aaaUser': {
'attributes': {
'name': username,
'pwd': password
}
}
}
url = f"https://{ip_addr}/api/mo/aaaLogin.json"
auth_cookie = {}
try:
response = requests.request("POST", url, json=payload, verify=False)
if response.status_code == requests.codes.ok:
data = json.loads(response.text)['imdata'][0]
token = str(data['aaaLogin']['attributes']['token'])
auth_cookie = {"APIC-cookie": token}
print(f"aaaLogin RESPONSE from {ip_addr}:")
print(json.dumps(json.loads(response.text)))
print()
return response.status_code, auth_cookie
except requests.exceptions.RequestException as e:
raise SystemExit(e)
After the aaa_login
function definition, copy or type the function definition that contains the aaaUser object for logging out.
The function below makes use of the aaaLogout
class and takes the parameters for the username, password, ip_addr of the switch. The request here uses request.request().
Again, this requires you to pass the CRUD operation. You still are required to pass the url and payload as parameters.
Notice, you also have to use the auth_cookie you obtained from the aaa_login
function.
def aaa_logout(username, ip_addr, auth_cookie):
payload = {
'aaaUser': {
'attributes': {
'name': username
}
}
}
url = f"https://{ip_addr}/api/mo/aaaLogout.json"
try:
response = requests.request("POST", url, json=payload, cookies=auth_cookie, verify=False)
print(f"aaaLogout RESPONSE from {ip_addr}:")
print(json.dumps(json.loads(response.text)))
print()
except requests.exceptions.RequestException as e:
raise SystemExit(e)
After the aaa_logout
function definition, copy or type the function definition that will be used to perform the actual POST request to the switch(s) REST interface.
As with the aaa functions, you will make use of requests.request().
The function takes the parameters for the url, ip_addr of the switch, payload, and the auth_cookie obtained from aaa_login
.
For the auth_cookie parameter, the aaa_login
function must be called and executed before nxapi_post since you need the authentication token.
def nxapi_post(url, auth_cookie, payload):
try:
print(f"Payload sent to {url}:")
print(payload)
response = requests.request("POST", url, json=json.loads(payload), cookies=auth_cookie, verify=False)
print("POST RESPONSE:")
print(json.dumps(json.loads(response.text)))
print()
except requests.exceptions.RequestException as e:
raise SystemExit(e)
After you have successfully created your function definitions, it's time to create the main method, that is the entry point for your Python script.
It will perform the function calls you just defined based on your command line arguments. The first part of the code below will use argparse
to define these command line arguments with subsequent if condition checks to ensure the proper file types are being used.
If the proper file types are used, the code will then open the data file and load the contents into a Python dictionary using the yaml.safe_load()
function.
Additionally, the code will open the Jinja template file, which is to be defined in the next step, and load the contents into a variable called template
.
The authentication credential variables for the switches are also defined by referencing the dictionary keys and values from the data file.
The last section of the code is similar to the previous JSON-RPC section.
You iterate over the devices defined in your data file that make up your staging fabric by referencing the dictionary keys and values from the data file down to the devices
key.
Then using the .keys()
dictionary method to iterate over the dictionary keys, which are the switch names.
Using the data for each switch device, the code will then render the Jinja template using the template.render()
function.
Like the previous section, the usage of template.render()
passes two variables, defaults
and device
,
that is your data defaults dictionary and based on where you are in the for loop for each device, it passes the device specific dictionary data to the template from your data file.
This template does not reneder raw CLI as in the previous sections since you are working with NXAPI REST.
Instead it leverages the JSON Jinja template that you will define in the next step to generate the PIM JSON payload to be sent to a switch over each for loop iteration via the nxapi_post
function.
Lastly, the code will call the aaa_logout
function to log out of the switch.
Either copy or type the code below and place it next into your Python file.
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--data", type=pathlib.Path)
parser.add_argument("-t", "--template", type=pathlib.Path)
parser.add_argument("--version", action="version", version="%(prog)s 0.1.0")
args = parser.parse_args()
if args.data.suffix in (".yaml", ".yml"):
with open(args.data) as f:
data = yaml.safe_load(f)
else:
sys.exit(f"{args.data.suffix} is not a supported file format.")
if args.template.suffix != ".j2":
sys.exit(f"{args.template.suffix} is not a supported template format.")
with open(args.template) as f:
template = jinja2.Template(f.read())
username = data['fabric']['defaults']['credentials']['username']
password = data['fabric']['defaults']['credentials']['password']
for device in data['fabric']['devices'].keys():
payload = template.render(defaults=data['fabric']['defaults'], device=data['fabric']['devices'][device])
status, auth_cookie = aaa_login(username, password, data['fabric']['devices'][device]['mgmt'])
if status == requests.codes.ok:
url = f"https://{data['fabric']['devices'][device]['mgmt']}/api/mo/sys.json"
nxapi_post(url, auth_cookie, payload)
aaa_logout(username, data['fabric']['devices'][device]['mgmt'], auth_cookie)
else:
print("Error! " + str(status))
if __name__ == '__main__':
main()
With your Python script defined, it's time to create the Jinja template file that will be used to configure PIM for each switch in your fabric. Once again, the Jinja template will be driven by the data file you defined in the first section. This template introduces you to templating something other than raw cli; in this case JSON. Now, it's very important to state that templating JSON is not the same as templating raw CLI. Each has it's own syntax and nuances, so you want to be sure to pay attention to the syntax. Alternatively, you could embed payload data structures like this direclty in Python and work with it that way.
While this may seem like a pretty big template, it is actually a pretty simple template that will be used to configure PIM Sparse-Mode on the Ethernet interfaces and configure the RP.
Under the topSystem
, top of the MIT, the template is broken down into two sections, the first section is the fmEntity
JSON object and the second section is the pimEntity
JSON object.
The fmEntity
JSON object is used to configure the fmPim
JSON object, which is used to enable the PIM feature on a switch.
The pimEntity
JSON object is used to configure the pimStaticRP
JSON object, which is used to configure the RP address and the pimIf
JSON object, which is used to enable PIM Sparse-Mode on the Ethernet interfaces.
Much like in the JSON-RPC section, an if condition is used to determine if PIM is defined in the data file in the defaults section or the device specific section.
Then a device_pim
variable is set to determine if the device specific section is defined and if not, the variable is set to None.
Using the device_pim
variable, the template will then render the JSON payload for the pimStaticRP
JSON object using the device specific RP address if defined or the default RP address; device_pim['rp_address'] | default(defaults['pim']['rp_address'])
.
Lastly, the pimIf
JSON object is rendered for each interface of device as the for loop in the Python script iterates over the devices as defined in the data file.
touch /home/cisco/Documents/nxapilab/scripts/templates/pim.j2
cat <<EOF >> /home/cisco/Documents/nxapilab/scripts/templates/pim.j2
{
"topSystem": {
"children": [
{%- if defaults['pim'] or device['pim'] %}
{%- set device_pim = device.get('pim', None) %}
{
"fmEntity": {
"children": [
{
"fmPim": {
"attributes": {
"adminSt": "enabled"
}
}
}
]
}
},
{
"pimEntity": {
"children": [
{
"pimInst": {
"children": [
{
"pimDom": {
"attributes": {
"name": "default"
},
"children": [
{
"pimStaticRPP": {
"children": [
{
"pimStaticRP": {
"attributes": {
"addr": "{{ device_pim['rp_address'] | default(defaults['pim']['rp_address']) }}"
},
"children": [
{
"pimRPGrpList": {
"attributes": {
"bidir": "no",
"grpListName": "239.1.1.0/25",
"override": "no"
}
}
}
]
}
}
]
}
}{%- for interface in device['interfaces'] %},
{
"pimIf": {
"attributes": {
"id": "{{ interface['name'][0:3].lower() + interface['name'][8:] }}",
"pimSparseMode": "yes"
}
}
}
{%- endfor %}
]
}
}
]
}
}
]
}
}
{%- endif %}
]
}
}
EOF
Upon successfully constructing the Python code above, save your nxapi_rest.py
file using Ctrl+s on the Windows keyboard or by clicking File then Save.
Be sure to save your file! Not saving will result in your code not executing.
Return to the VSCode Terminal command line to execute your code:
python nxapi_rest.py --data=data.yaml --template=templates/pim.j2
Printed below is the code execution on the switches in your staging fabric; you can see the REST payload sent to the switch(s) and the response from the switch(s):
Make note of the payload that was generated from the Jinja template.
aaaLogin RESPONSE from 10.15.1.11: {"imdata": [{"aaaLogin": {"attributes": {"token": "gzfCKFNi+Q3Q65HMF77OS7XvDBIps46rVXpBMtvQn3WVlM9f1shjUi/rI44Ozj8+ZeYz+BE3bLW3XKaO2j58oO2LqNR1raQ83oUk6KHtrea+l9OgVp6MT7M0HtYzna1jyljd+AVP7YjgIN+2nCC1oVlob+38zDJU94CM7PPX71Y=", "siteFingerprint": "", "refreshTimeoutSeconds": "600", "guiIdleTimeoutSeconds": "1200", "restTimeoutSeconds": "300", "creationTime": "1705520333", "firstLoginTime": "1705520333", "userName": "admin", "remoteUser": "false", "unixUserId": "0", "sessionId": "7hVOXPes7iXZp3BrTgM9Xg==", "lastName": "", "firstName": "", "version": "0.9(14HEAD${version.patch})", "buildTime": "Tue Dec 12 18:39:24 PST 2023", "controllerId": "0"}, "children": [{"aaaUserDomain": {"attributes": {"name": "all", "rolesR": "admin", "rolesW": "admin"}, "children": [{"aaaReadRoles": {"attributes": {}}}, {"aaaWriteRoles": {"attributes": {}, "children": [{"role": {"attributes": {"name": "network-admin"}}}]}}]}}]}}]} Payload sent to https://10.15.1.11/api/mo/sys.json: { "topSystem": { "children": [ { "fmEntity": { "children": [ { "fmPim": { "attributes": { "adminSt": "enabled" } } } ] } }, { "pimEntity": { "children": [ { "pimInst": { "children": [ { "pimDom": { "attributes": { "name": "default" }, "children": [ { "pimStaticRPP": { "children": [ { "pimStaticRP": { "attributes": { "addr": "10.250.250.1" }, "children": [ { "pimRPGrpList": { "attributes": { "bidir": "no", "grpListName": "239.1.1.0/25", "override": "no" } } } ] } } ] } }, { "pimIf": { "attributes": { "id": "eth1/1", "pimSparseMode": "yes" } } }, { "pimIf": { "attributes": { "id": "eth1/2", "pimSparseMode": "yes" } } } ] } } ] } } ] } } ] } } POST RESPONSE: {"imdata": []} aaaLogout RESPONSE from 10.15.1.11: {"imdata": []} aaaLogin RESPONSE from 10.15.1.12: {"imdata": [{"aaaLogin": {"attributes": {"token": "kTS6GVYeh2k7iuw7qWdeG8pasghinEshA0++7BxK6nvjTOoKTDmoUxI7EAH3yvbWfzm6G6f4WbWq0l1JYM+l7xG6Ewq2cbLdbEOYkNOyfPmCy+wn+wvzLn8inEmSwFO1ma5klSZccB4MFC3zmE5cnf58MOKXelwFC6XmF+UWEuI=", "siteFingerprint": "", "refreshTimeoutSeconds": "600", "guiIdleTimeoutSeconds": "1200", "restTimeoutSeconds": "300", "creationTime": "1705520333", "firstLoginTime": "1705520333", "userName": "admin", "remoteUser": "false", "unixUserId": "0", "sessionId": "U7Ukt+6pyEBONhjKSy18Ig==", "lastName": "", "firstName": "", "version": "0.9(14HEAD${version.patch})", "buildTime": "Tue Dec 12 18:39:24 PST 2023", "controllerId": "0"}, "children": [{"aaaUserDomain": {"attributes": {"name": "all", "rolesR": "admin", "rolesW": "admin"}, "children": [{"aaaReadRoles": {"attributes": {}}}, {"aaaWriteRoles": {"attributes": {}, "children": [{"role": {"attributes": {"name": "network-admin"}}}]}}]}}]}}]} Payload sent to https://10.15.1.12/api/mo/sys.json: { "topSystem": { "children": [ { "fmEntity": { "children": [ { "fmPim": { "attributes": { "adminSt": "enabled" } } } ] } }, { "pimEntity": { "children": [ { "pimInst": { "children": [ { "pimDom": { "attributes": { "name": "default" }, "children": [ { "pimStaticRPP": { "children": [ { "pimStaticRP": { "attributes": { "addr": "10.250.250.1" }, "children": [ { "pimRPGrpList": { "attributes": { "bidir": "no", "grpListName": "239.1.1.0/25", "override": "no" } } } ] } } ] } }, { "pimIf": { "attributes": { "id": "eth1/10", "pimSparseMode": "yes" } } } ] } } ] } } ] } } ] } } POST RESPONSE: {"imdata": []} aaaLogout RESPONSE from 10.15.1.12: {"imdata": []} aaaLogin RESPONSE from 10.15.1.13: {"imdata": [{"aaaLogin": {"attributes": {"token": "8ovNemeeCiS8Abu9zftPBroZwT/RBznv3R7221y/Gjox/JVK92KKw6u1n2XBCg00jP47K2y+o9gmfLFiwMdGmyzRFScQ2DYyd7hX3byziakgf1Sz64HF4rmll2X5f9ncdsZs4QRqik/0mlpqC3mY5sAYiFf9HU6hgUoR8pH5wRc=", "siteFingerprint": "", "refreshTimeoutSeconds": "600", "guiIdleTimeoutSeconds": "1200", "restTimeoutSeconds": "300", "creationTime": "1705520334", "firstLoginTime": "1705520334", "userName": "admin", "remoteUser": "false", "unixUserId": "0", "sessionId": "Pi/DLiXXZ6S5sYI8P8eyqg==", "lastName": "", "firstName": "", "version": "0.9(14HEAD${version.patch})", "buildTime": "Tue Dec 12 18:39:24 PST 2023", "controllerId": "0"}, "children": [{"aaaUserDomain": {"attributes": {"name": "all", "rolesR": "admin", "rolesW": "admin"}, "children": [{"aaaReadRoles": {"attributes": {}}}, {"aaaWriteRoles": {"attributes": {}, "children": [{"role": {"attributes": {"name": "network-admin"}}}]}}]}}]}}]} Payload sent to https://10.15.1.13/api/mo/sys.json: { "topSystem": { "children": [ { "fmEntity": { "children": [ { "fmPim": { "attributes": { "adminSt": "enabled" } } } ] } }, { "pimEntity": { "children": [ { "pimInst": { "children": [ { "pimDom": { "attributes": { "name": "default" }, "children": [ { "pimStaticRPP": { "children": [ { "pimStaticRP": { "attributes": { "addr": "10.250.250.1" }, "children": [ { "pimRPGrpList": { "attributes": { "bidir": "no", "grpListName": "239.1.1.0/25", "override": "no" } } } ] } } ] } }, { "pimIf": { "attributes": { "id": "eth1/10", "pimSparseMode": "yes" } } } ] } } ] } } ] } } ] } } POST RESPONSE: {"imdata": []} aaaLogout RESPONSE from 10.15.1.13: {"imdata": []}
Close the three (3) Python NX-API script files and your data file that was used for this section to keep your VSCode workspace clean by clicking the little x (close button) on the right-side of each opened tab:
Continue to the next section to get started using Ansible for NXOS.